Added
- New static guard
scripts/check-cross-iife-refs.mjs— retires the whole #1676 cross-IIFE ReferenceError class (#1706). The "minified client crashes on a cross-IIFE symbol" class recurred three times (#1676 terser--manglerenamed cross-moduleapplyPatches→ie, fixed with--keep-fnames; #1688/#1690 bareapplyPatchesreference in45-child-view.js; #1689 dup) and every prior fix was per-symbol. This adds a build-tooling lint that catches the whole class statically. The mechanism: the client bundle concatenatespython/djust/static/djust/src/[0-9]*.js; modules00-20sit INSIDE the double-load-guardelse {}block (sofunction foo() {}declared there is block-scoped), while modules22-51run at the bundle's true top level OUTSIDE that block. A bare reference from a top-level module to a guard-block function published only viaglobalThis.djust.Xis out of scope even unminified (thetypeofguard silently returns"undefined"and the feature no-ops) and throwsReferenceErrorunder terser-minified bundles. The check reuses thecheck-bundle-init-order.mjswalker model (in-memory bundle build, acorn parse, line→module map), builds the djust-published function set, computes the guardelse {}scope span, and flags any bare cross-scope reference (browser globals anddjust.Xmember access are inherently never flagged; a locally re-bound name is excluded). Wired into the pre-commit hook (alongside the #1372 init-order lint) and the CIjavascript-testsjob (which also gains the init-order check, previously pre-commit-only). Pinned by 6 cases intests/js/check-cross-iife-refs-1706.test.js(real-tree-clean + #1688-shape flag + empirical canary #252 + member-access-not-flagged + intra-guard-not-flagged + local-rebind-not-flagged; gate-off verified non-tautological). - New guide: Migrating from django-tenants → row-level
djust.tenants(#1559). Schema-per-tenant (the external django-tenants library) is deprecated under djust; the newdocs/website/guides/migrating-from-django-tenants.mdis the step-by-step migration recipe. Covers: (1) a mental-model translation table (schema →tenant_idcolumn;TenantMainMiddleware→djust.tenants.middleware.TenantMiddleware;SHARED_APPS/TENANT_APPS→ one unifiedINSTALLED_APPS;Domainmodel →DJUST_CONFIG['TENANT_RESOLVER']); (2) the schema-to-row data-migration recipe (nullable-add →INSERT ... SELECTper schema → tighten, with explicit handling of FK remapping, cross-tenant unique constraints → composite(tenant_id, …), sequences, and indexes); (3) code migration (addtenant_id; filter via explicittenant_id,TenantScopedMixin.get_tenant_queryset(), orTenantQuerySet.as_manager(tenant_field=…); swap middleware); (4) settings diff (collapse the app split, swap middleware, dropDATABASE_ROUTERS, set the resolver); (5) rollout strategy (big-bang vs tenant-by-tenant, isolation verification, suppressing/stopgapping C014 during rollout); (6) what doesn't translate (hard-compliance schema isolation → engage upstream rather than silently staying on the deprecated path); and (7) a copy-pasteable cross-tenant-leak canary pytest. Every cited symbol/API is verified against the realdjust.tenantsmodules (notably: the scoped-queryset helper isget_tenant_queryset(), nottenant_queryset). Linked from_config.yaml,index.md, and the Multi-Tenant guide. - New
T015system check — detects the legacydata-djust-root/data-djust-viewroot attributes (#1602). Pre-1.0 templates declared the LiveView root withdata-djust-root/data-djust-view; djust 1.0 renamed these todj-root/dj-view(thedata-prefix is no longer required). When a template still uses the old spelling, the genericT012("dj-* directives but no dj-view") doesn't recognise that a view IS declared — so the path from symptom (the LiveView never connects over WebSocket) to fix is non-obvious.T015scans user template files and emits a Warning that names the rename explicitly, with afix_hintper offending occurrence (file:line). The match is scoped via a negative-lookahead to exactlydata-djust-root/data-djust-view, so otherdata-djust-*attributes (data-djust-embedded,data-djust-activity,data-djust-view-model, …) never false-match. Suppressible viaDJUST_CONFIG = {"suppress_checks": ["T015"]}. Scope: static check only — the runtime does not accept the legacy attributes (a separate change). New cases inTestT015LegacyRootAttrs(empirical-canary + gate-off verified; dogfooded clean against the demo project). DJUST_NOTIFY_DATABASE_URL— optional dedicated DSN for thedjust.dbLISTEN connection (#1687).db.notifications._build_dsn()previously always derived the long-livedLISTENAsyncConnectionDSN fromsettings.DATABASES['default'], so the listener could not be isolated from the request-path connection pool (downstream djustlive #380: pgbouncer session-pool saturation →/healthhangs). A new optionalDJUST_NOTIFY_DATABASE_URLsetting (also honored as an environment variable of the same name) supplies aDATABASE_URL-style override (postgres://user:pass@host:port/dbname) that is preferred BEFORE theDATABASES['default']fallback — point it at a direct, session-mode Postgres endpoint so the listener can't saturate a shared transaction-pool. Backwards-compatible: when unset, the produced DSN is byte-identical to prior releases. The postgres-only engine check still applies to the override (a non-postgresql URL scheme raisesDatabaseNotificationNotSupported), and the override URL/password is never logged. New_dsn_from_url()helper parses the URL viaurllib.parse(no new dependency). Pinned by 7 cases inTestBuildDsnOverride(gate-off verified).DJUST_NOTIFY_DATABASE_URLnow honors a known-safe libpq query-param allowlist (#1696, follow-up to #1687).db.notifications._dsn_from_url()previously parsed scheme/user/password/host/port/dbname from the override URL but silently DROPPED the query string — so the two most common direct-to-Postgres LISTEN needs,?sslmode=require(TLS) and the unix-socket form?host=/var/run/postgresql, were impossible to express. The parser now appends an explicit allowlist of libpq connection parameters from the query string to the produced DSN:sslmode,sslrootcert,sslcert,sslkey,host,application_name,connect_timeout. Values are percent-decoded consistently with the userinfo fields and libpq-quoted ('…'with backslash-escaping) when they contain whitespace.hostprecedence: a?host=query item REPLACES the URL netloc host (so the output carries exactly onehostkey — the deterministic unix-socket behavior; the netloc host becomes an ignored placeholder). Credential safety: unknown query keys are silently dropped, anduser/password/dbnameare deliberately NOT in the allowlist, so a query string can never override the URL-derived credentials. Backwards-compatible: a no-query URL produces a DSN byte-identical to the #1695 output. Still usesurllib.parseonly (no new dependency); the URL/DSN/password is never logged. New cases inTestDsnQueryParams(gate-off verified).
Changed
- CI now dogfoods
djust_checkagainst the demo project (#1708, CI infra — enforces CLAUDE.md #1060).#1683 shipped dead@clickbuttons to the 1.0 GA demo even though theT001system check existed — because the demo templates were never run throughdjust_checkin CI. A new step in theplaywright-testsjob runsscripts/ci_djust_check_demo.py, a wrapper aroundmanage.py djust_check --json. The wrapper is necessary becausedjust_checkitself ALWAYS exits 0 (handle()only prints results — no exit-code logic), so a bare invocation can never fail CI. The wrapper parses the JSON summary and exits non-zero ONLY on error-severity checks and the deprecated-attribute classesT001/T014/T015(the exact #1683 bug class) — NOT on the demo's intentional warnings (S005 public-view-without-auth, T012 partial-fragment templates, V004 informational). Empirically verified: re-introducing a single@click=into a scratch demo template makes the step reportT001and exit 1; the clean demo exits 0. The step inherits the job'scontinue-on-error: true, so it is NON-BLOCKING on its first runner iterations (CLAUDE.md rc4 retro finding #3: a new CI check exercising an env the dev machine can't fully mirror needs ≥1 runner-only iteration budgeted); promote it to a blocking gate once it has shipped green on the runner. No framework behavior change — CI config only. scripts/check-doc-snippets.pynow scansdocs/website/guides/*.mdfor symbol/import resolvability (#1707, CI infra — extends the #1500 guard). The checker previously validated only README.md + QUICKSTART.md, so guide prose could drift from the real API with no CI guard — exactly how #1559/#1699 shipped ~10 hallucinateddjust.tenantssymbols undetected. The part-(a) check (AST-parse + import/symbol resolution) now also runs over all 57 guides; parts (b) (Django-floor / JS-size claims) and (c) (security/style lint) stay README/QUICKSTART-specific (guides legitimately useprint()in demo examples, so the style verdict is out of scope). New--guides-dir/--no-guidesflags (guides scanned by default; an explicit missing--guides-diris a usage error, exit 2). Wired into CI (test.yml) and the pre-commit hook (itsfiles:scope now includes the guides dir). Survey of the current tree surfaced 10 part-(a) flags across 9 guides: 3 real wrong-import-path fixes (djust_theming→djust.themingin components.md;djust.live_view.state→djust.decorators.statein state-primitives.md;djust.uploads.stores→djust.uploads.storagein uploads.md) and 6 intentionally-illustrative blocks (externalcelery, placeholderyourapp.models, list-indented fragments, an API-doc signature stub) marked with the existing<!-- doc-snippet-check: skip -->directive — no guide needed a follow-up rewrite. Also fixed a resolver false-positive (from X import submodule, e.g.from django.db import migrations, now falls back to importing the dotted submodule before declaring the symbol missing). New cases inTestCheckGuides(gate-off + submodule-fallback regression, empirical-canary verified: re-introducingfrom djust.tenants import tenant_querysetmakes the checker exit 1 and name the symbol). No framework behavior change — CI/docs only.- C014's
hintandfix_hintnow link the new django-tenants migration guide (#1559). The check (django-tenants + ASGI withoutTENANT_LIMIT_SET_CALLS) already led with the migrate-to-djust.tenantsrecommendation and the strategy-decision guide (multi-tenant.md); both thehintandfix_hintnow also point atdocs/website/guides/migrating-from-django-tenants.mdfor the step-by-step recipe. No logic change — same trigger conditions, same suppression (DJUST_CONFIG = {'suppress_checks': ['C014']}), and all existingmulti-tenant.md/djust.tenants/TENANT_LIMIT_SET_CALLShint substrings preserved. - CI lint cleanup + de-noised Pre-Release Security Audit + eslint now gates on errors (#1717, CI/lint hygiene — no framework behavior change). Three coordinated cleanups: (1) Lint fixes (all pre-existing): 5 clippy style warnings rewritten behavior-neutrally —
sort_by(|a,b| …cmp…)→sort_by_key(…)incrates/djust_vdom/src/patch.rs(descending removes viastd::cmp::Reverse, ascending inserts plain) andcrates/djust_vdom/src/lib.rs(two descending offset sorts viaReverse), and a collapsible nestediffolded into thematcharm guard incrates/djust_templates/src/parser.rs. Two eslint errors resolved: the redundant'use strict'inside the IIFE-modulejs/pwa.jsis removed (rulestrict), and the XSS-sanitizer denylist matchval.startsWith('javascript:')insecurity.jscarries an explanatory// eslint-disable-next-line no-script-url(the check itself is unchanged —no-script-urlwas a false positive flagging a denylist MATCH, not a script-URL USE). (2) De-noise CI:.github/workflows/pre-release-security-audit.ymlnow sets workflow-levelCARGO_TERM_COLOR: never+NO_COLOR: "1"so cargo/clippy/cargo-audit/eslint emit no raw ANSI escapes, and each verbose scan step's console output is wrapped in::group::/::endgroup::(collapsed by default in the Actions UI); the FULL detail still flows into the uploaded*-report.mdartifacts. (3) Gate eslint on errors: the JS-scan eslint step dropped|| trueforset -o pipefail+ captured-exit, so a real severity-2 regression (e.g. an XSS /no-script-urlerror) now FAILS the step while warnings stay non-fatal (eslint exits non-zero only on errors by default); the report artifact is still written. The pre-commit eslint hook is aligned to the same policy (dropped--max-warnings 0). Verified:cargo clippy --all-targets -- -W clippy::all -W clippy::complexity -D clippy::correctness -D clippy::suspicious→ 0 warnings;npm run lint→ 0 errors (33 warnings surfaced, non-fatal); djust_vdom/djust_templates Rust tests green (sort/patch ordering preserved); a synthetic severity-2 eslint error makesnpm run lintexit non-zero (gating demonstrated, then removed). - Fixed the Pre-Release Security Audit's "Create tracking issue" step (CI-internal; no framework change). Two pre-existing bugs, surfaced by a manual
workflow_dispatch: (1) the guard(inputs.create_issue == true || inputs.create_issue == '')ran the step even whencreate_issue=falsewas passed — GitHub Actions coerces a booleanfalseand''both to0, sofalse == ''istrue; dropped the== ''clause (inputs.create_issue == trueis correct, push is already excluded by the event guard). (2) The issue body (audit template + full scan summary) had no length cap and exceeded GitHub's 65536-char issue limit → HTTP 422; it is now truncated to 65000 with a pointer to thesecurity-audit-reportartifact, which always carries the full detail. The scans themselves were unaffected (all green); only issue-creation failed.
Fixed
- Multi-tenant guide (
docs/website/guides/multi-tenant.md) no longer documents non-existentdjust.tenantssymbols (#1699). The guide cited several APIs that do not exist, so copy-pasted examples wouldImportError/AttributeError. Corrected three error classes plus follow-on inaccuracies, all verified against the real API (python/djust/tenants/mixin.py,resolvers.py): (1)self.tenant_queryset(...)→self.get_tenant_queryset(model=None)(mixin.py:214); (2)from djust.tenants.mixins import ...→from djust.tenants import ...(module ismixin, singular); (3)DJUST_TENANT_RESOLVER = 'djust.tenants.resolvers.XResolver'(a non-existent top-level setting whose value was a class path) →DJUST_CONFIG = {'TENANT_RESOLVER': '<short-name>'}where the value is aRESOLVER_REGISTRYkey ('subdomain'/'path'/'header'/'session'/'custom', or a list for chained resolution); the per-strategyDJUST_TENANT_CONFIGnested dicts were folded into flatDJUST_CONFIGkeys (TENANT_MAIN_DOMAIN,TENANT_SUBDOMAIN_EXCLUDE,TENANT_PATH_POSITION,TENANT_HEADER,TENANT_SESSION_KEY,TENANT_CUSTOM_RESOLVER,TENANT_DEFAULT). Also fixed the API-reference table (tenant_get_object_or_404/tenant_filter→ realget_tenant_object/create_for_tenant), the Testing section (which imported a non-existentdjust.tenants.testmodule — rewritten to useset_current_tenant+TenantInfo, mirroring the verifiedmigrating-from-django-tenants.mdguide), andTenantInfo(id=...)→TenantInfo(tenant_id=...)(the real first positional kwarg). Docs-only; no framework behavior change. Verified: grep confirms zero old forms remain; every cited symbol/module/config key import-checks against the realdjust.tenantsAPI. - Demo/example apps no longer ship dead
@click/@input/@change/@submithandler bindings; migrated todj-*(#1683). The shipped client (09-event-binding.js) bindsdj-*ONLY — the@event=form is deprecated (T001 system check) AND non-functional, so demo controls authored with it rendered as inert "dead buttons." Migrated 117 handler bindings →dj-*across 32 files:examples/demo_project/(djust_demos,demo_app,djust_homepage,djust_shared— inline-HTML strings in.pyviews/demo-classes plus the 3 demo.mdtranslation guides), top-levelexamples/rust_components_demo.py/examples/range_component_demo.py, and framework docstrings/comments (live_tags.py,websocket.py,websocket_utils.py,validation.py). The Alpine.js dropdown component (python/djust/components/ui/dropdown_simple.py—@click="open = !open"withx-data/x-show) is a client-side JS expression, NOT a djust handler, and is intentionally preserved unchanged. Demo-only change; no framework behavior change. Verified by grep (zero in-scope@event=bindings remain) andmanage.py djust_check(no T001/@clickfindings for migrated views). - Runtime
SafeStringfrom a custom filter is no longer over-escaped inside{% firstof %}/{% cycle %}(#1672, follow-up to #1660).#1660 threaded runtime-safeness through the{{ var|filter }}Variable and InlineIf render arms, but the parallelget_valuepipe helper (used by the{% firstof a|md %}/{% cycle a|md ... %}emit path) applied filters via the plainapply_filter_full, dropping the runtime-safe flag. A custom filter thatmark_safe()s its output AT RUNTIME (without@register.filter(is_safe=True)) was therefore double-escaped in those tags — e.g.<em>Hi</em>rendered as<em>Hi</em>. This was fail-safe over-escaping, NOT an XSS — a parity gap, not a security hole. Fix: a newget_value_safereturns(Value, bool runtime_safe), threading the safe flag out of the pipe loop viaapply_filter_full_safe(mirroring the per-iterationruntime_safe = produced_safepattern from the #1660 Variable arm); theFirstOf/Cycleemit arms skip auto-escaping when the value is a genuine runtimeSafeString.get_valueis preserved as a thin wrapper so its other callers are untouched. The fix is strictly additive (only ever marks MORE values safe, only when the last filter produced a realstr-subclass with__html__), so it can never under-escape a plain value. Pinned by 7 regression cases inTestFirstofCycleRuntimeSafe_1672(gate-off verified) plus parallel-path-drift code comments per CLAUDE.md #1646. {% firstof x|safe %}/{% cycle x|urlize %}no longer over-escape the output of NAME-based safe filters (#1692, completes the #1660→#1672 lineage).#1672 threaded RUNTIMEmark_safe()-ness through the{% firstof %}/{% cycle %}emit path viaget_value_safe, but that helper did not consult the name-basedsafe_output_filterswhitelist (safe,safeseq,force_escape,json_script,urlize,urlizetrunc,unordered_list) that the{{ var|filter }}Variable render arm uses. So a chain ending in one of those filters — e.g.{% firstof x|safe %}or{% cycle x|urlize %}(whereurlizeemits its own<a href=…>HTML) — was still double-escaped in those two tags. Fix:get_value_safe's filter loop now also marks the value safe when the applied filter NAME is in the whitelist (or is a customis_safe=Truefilter), mirroring the Variable/InlineIf arms exactly. The whitelist was hoisted from two inline copies into a single shared module constSAFE_OUTPUT_FILTERSso all three render paths reference one source of truth (parallel-path-drift, CLAUDE.md #1646). Fail-safe, like #1672: it only ever ADDS safeness for the established whitelisted names / genuine runtime SafeStrings; a plain/unknown filter (e.g.upper) stays escaped, and LAST-filter re-taint semantics are preserved ({% firstof x|safe|upper %}re-escapes). Pinned by 4 Rust cases inrenderer::tests(test_firstof_safe_filter_not_double_escaped,test_cycle_urlize_filter_not_double_escaped,test_firstof_nonsafe_filter_still_escaped,test_firstof_safe_then_plain_filter_re_taints) + 4 Python cases intests/unit/test_rust_firstof_cycle_named_safe_1692.py(gate-off verified).client.min.jsno longer logsUncaught ReferenceError: applyPatches is not definedin production (#1688). A recurrence of the #1676 terser-mangle × IIFE class, different manifestation.45-child-view.jsreferenced the bareapplyPatchessymbol at two sites (_applyScopedPatchesand thedjust._applyPatchesexpose block), butapplyPatchesis declared inside12-vdom-patch.js's own inner IIFE and published only asglobalThis.djust.applyPatches. The bare cross-IIFE reference is out of scope: it silently no-ops in the unminified bundle (leavingdjust._applyPatchesunwired, soemitChildMountedEvents— the child-mounted lifecycle for embedded/sticky views — never runs) and throws in the terser-minified production bundle (served whenDEBUG=False), logging an alarming uncaught error in every console at page load. Non-fatal — core LiveView (WebSocket connect, event dispatch, DOM patching via the in-scope applier) keeps working. Fix: read the published aliasglobalThis.djust.applyPatchesat both sites, which is minification-independent and also restores the intended_applyPatcheswiring. Pinned by a behavioral regression (tests/js/min_bundle_applypatches_1688.test.js) assertingdjust._applyPatchesis wired after load (gate-off verified:undefinedon the pre-fix bundle).dj-dialog-close-event(35-dj-dialog.js) and keyboard-navdj-clickdispatch (51-keyboard-nav.js) no longer reference a bare out-of-scopehandleEvent(#1706). Found by the new cross-IIFE static guard (above): both modules referenced the barehandleEventsymbol, which is declared in11-event-handler.jsinside the double-load-guardelse {}block (block-scoped) and published only asglobalThis.djust.handleEvent. Since both modules run at the bundle's true top level (OUTSIDE the guard block), the bare reference was out of scope even unminified — thetypeof handleEvent === 'function'guard returned"undefined", so the dialogcloseevent and the keyboard-navdj-clickactivation silently no-opped — and would throwReferenceErrorunder terser-minified bundles. Exactly the #1688 class, two more sites. Fix: read the published aliasglobalThis.djust.handleEventat all four sites (minification-independent). Thedj_dialog/keyboard_navtest stubs were updated to spy on the alias (production's actual invoke path) rather than the stale bare global.- Broke the latent
registry ↔ theme_packs/registry ↔ manifestimport SCC indjust.theming(#1662, follow-up to #1661). After #1661 extractedget_theme_configto the leaf_config, the AST import graph (lazy + eager) still had a pre-existing, never-CodeQL-flagged strongly-connected component:theme_packs/manifestimportedregistry.get_registrywhileregistryimportedtheme_packs/manifestfor discovery. Fix (same leaf-module pattern as #1661): extractThemeRegistry+get_registryinto a new leaf module_registry_accessor.py(imports only stdlib) sotheme_packs/manifest/presets/ etc. reach the singleton WITHOUT importing back intoregistry; discovery (the onlyregistry → theme_packs/manifestedges) stays inregistry.pyand is installed as a hook viaset_discovery_hook, making the dependency one-directional.registry.pyre-exportsThemeRegistry/get_registry/register_*sofrom djust.theming.registry import …keeps working — no runtime, behavior, or public-API change.test_theming_no_cyclic_import.pyis tightened to assert the WHOLEdjust.themingpackage import graph is acyclic (Tarjan over all modules, counting bothfrom .X importandfrom . import Xedges) plus a leaf-purity gate — previously onlypresets/manager/css_generatorwere gated and the registry SCC was explicitly allowed. Gate-off verified: the tightened test fails on pre-fix code with SCC[manifest, registry, theme_packs].