djust 1.0.1

StableReleased
Install
pip install djust==1.0.1

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 --mangle renamed cross-module applyPatchesie, fixed with --keep-fnames; #1688/#1690 bare applyPatches reference in 45-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 concatenates python/djust/static/djust/src/[0-9]*.js; modules 00-20 sit INSIDE the double-load-guard else {} block (so function foo() {} declared there is block-scoped), while modules 22-51 run 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 via globalThis.djust.X is out of scope even unminified (the typeof guard silently returns "undefined" and the feature no-ops) and throws ReferenceError under terser-minified bundles. The check reuses the check-bundle-init-order.mjs walker model (in-memory bundle build, acorn parse, line→module map), builds the djust-published function set, computes the guard else {} scope span, and flags any bare cross-scope reference (browser globals and djust.X member 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 CI javascript-tests job (which also gains the init-order check, previously pre-commit-only). Pinned by 6 cases in tests/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 new docs/website/guides/migrating-from-django-tenants.md is the step-by-step migration recipe. Covers: (1) a mental-model translation table (schema → tenant_id column; TenantMainMiddlewaredjust.tenants.middleware.TenantMiddleware; SHARED_APPS/TENANT_APPS → one unified INSTALLED_APPS; Domain model → DJUST_CONFIG['TENANT_RESOLVER']); (2) the schema-to-row data-migration recipe (nullable-add → INSERT ... SELECT per schema → tighten, with explicit handling of FK remapping, cross-tenant unique constraints → composite (tenant_id, …), sequences, and indexes); (3) code migration (add tenant_id; filter via explicit tenant_id, TenantScopedMixin.get_tenant_queryset(), or TenantQuerySet.as_manager(tenant_field=…); swap middleware); (4) settings diff (collapse the app split, swap middleware, drop DATABASE_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 real djust.tenants modules (notably: the scoped-queryset helper is get_tenant_queryset(), not tenant_queryset). Linked from _config.yaml, index.md, and the Multi-Tenant guide.
  • New T015 system check — detects the legacy data-djust-root / data-djust-view root attributes (#1602). Pre-1.0 templates declared the LiveView root with data-djust-root / data-djust-view; djust 1.0 renamed these to dj-root / dj-view (the data- prefix is no longer required). When a template still uses the old spelling, the generic T012 ("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. T015 scans user template files and emits a Warning that names the rename explicitly, with a fix_hint per offending occurrence (file:line). The match is scoped via a negative-lookahead to exactly data-djust-root / data-djust-view, so other data-djust-* attributes (data-djust-embedded, data-djust-activity, data-djust-view-model, …) never false-match. Suppressible via DJUST_CONFIG = {"suppress_checks": ["T015"]}. Scope: static check only — the runtime does not accept the legacy attributes (a separate change). New cases in TestT015LegacyRootAttrs (empirical-canary + gate-off verified; dogfooded clean against the demo project).
  • DJUST_NOTIFY_DATABASE_URL — optional dedicated DSN for the djust.db LISTEN connection (#1687). db.notifications._build_dsn() previously always derived the long-lived LISTEN AsyncConnection DSN from settings.DATABASES['default'], so the listener could not be isolated from the request-path connection pool (downstream djustlive #380: pgbouncer session-pool saturation → /health hangs). A new optional DJUST_NOTIFY_DATABASE_URL setting (also honored as an environment variable of the same name) supplies a DATABASE_URL-style override (postgres://user:pass@host:port/dbname) that is preferred BEFORE the DATABASES['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 raises DatabaseNotificationNotSupported), and the override URL/password is never logged. New _dsn_from_url() helper parses the URL via urllib.parse (no new dependency). Pinned by 7 cases in TestBuildDsnOverride (gate-off verified).
  • DJUST_NOTIFY_DATABASE_URL now 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. host precedence: a ?host= query item REPLACES the URL netloc host (so the output carries exactly one host key — the deterministic unix-socket behavior; the netloc host becomes an ignored placeholder). Credential safety: unknown query keys are silently dropped, and user/password/dbname are 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 uses urllib.parse only (no new dependency); the URL/DSN/password is never logged. New cases in TestDsnQueryParams (gate-off verified).

Changed

  • CI now dogfoods djust_check against the demo project (#1708, CI infra — enforces CLAUDE.md #1060). #1683 shipped dead @click buttons to the 1.0 GA demo even though the T001 system check existed — because the demo templates were never run through djust_check in CI. A new step in the playwright-tests job runs scripts/ci_djust_check_demo.py, a wrapper around manage.py djust_check --json. The wrapper is necessary because djust_check itself 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 classes T001/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 report T001 and exit 1; the clean demo exits 0. The step inherits the job's continue-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.py now scans docs/website/guides/*.md for 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 hallucinated djust.tenants symbols 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 use print() in demo examples, so the style verdict is out of scope). New --guides-dir / --no-guides flags (guides scanned by default; an explicit missing --guides-dir is a usage error, exit 2). Wired into CI (test.yml) and the pre-commit hook (its files: 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_themingdjust.theming in components.md; djust.live_view.statedjust.decorators.state in state-primitives.md; djust.uploads.storesdjust.uploads.storage in uploads.md) and 6 intentionally-illustrative blocks (external celery, placeholder yourapp.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 in TestCheckGuides (gate-off + submodule-fallback regression, empirical-canary verified: re-introducing from djust.tenants import tenant_queryset makes the checker exit 1 and name the symbol). No framework behavior change — CI/docs only.
  • C014's hint and fix_hint now link the new django-tenants migration guide (#1559). The check (django-tenants + ASGI without TENANT_LIMIT_SET_CALLS) already led with the migrate-to-djust.tenants recommendation and the strategy-decision guide (multi-tenant.md); both the hint and fix_hint now also point at docs/website/guides/migrating-from-django-tenants.md for the step-by-step recipe. No logic change — same trigger conditions, same suppression (DJUST_CONFIG = {'suppress_checks': ['C014']}), and all existing multi-tenant.md / djust.tenants / TENANT_LIMIT_SET_CALLS hint 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(…) in crates/djust_vdom/src/patch.rs (descending removes via std::cmp::Reverse, ascending inserts plain) and crates/djust_vdom/src/lib.rs (two descending offset sorts via Reverse), and a collapsible nested if folded into the match arm guard in crates/djust_templates/src/parser.rs. Two eslint errors resolved: the redundant 'use strict' inside the IIFE-module js/pwa.js is removed (rule strict), and the XSS-sanitizer denylist match val.startsWith('javascript:') in security.js carries an explanatory // eslint-disable-next-line no-script-url (the check itself is unchanged — no-script-url was a false positive flagging a denylist MATCH, not a script-URL USE). (2) De-noise CI: .github/workflows/pre-release-security-audit.yml now sets workflow-level CARGO_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.md artifacts. (3) Gate eslint on errors: the JS-scan eslint step dropped || true for set -o pipefail + captured-exit, so a real severity-2 regression (e.g. an XSS / no-script-url error) 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 makes npm run lint exit 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 when create_issue=false was passed — GitHub Actions coerces a boolean false and '' both to 0, so false == '' is true; dropped the == '' clause (inputs.create_issue == true is 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 the security-audit-report artifact, 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-existent djust.tenants symbols (#1699). The guide cited several APIs that do not exist, so copy-pasted examples would ImportError/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 is mixin, 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 a RESOLVER_REGISTRY key ('subdomain'/'path'/'header'/'session'/'custom', or a list for chained resolution); the per-strategy DJUST_TENANT_CONFIG nested dicts were folded into flat DJUST_CONFIG keys (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 → real get_tenant_object/create_for_tenant), the Testing section (which imported a non-existent djust.tenants.test module — rewritten to use set_current_tenant + TenantInfo, mirroring the verified migrating-from-django-tenants.md guide), and TenantInfo(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 real djust.tenants API.
  • Demo/example apps no longer ship dead @click/@input/@change/@submit handler bindings; migrated to dj-* (#1683). The shipped client (09-event-binding.js) binds dj-* 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 .py views/demo-classes plus the 3 demo .md translation guides), top-level examples/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" with x-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) and manage.py djust_check (no T001/@click findings for migrated views).
  • Runtime SafeString from 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 parallel get_value pipe helper (used by the {% firstof a|md %} / {% cycle a|md ... %} emit path) applied filters via the plain apply_filter_full, dropping the runtime-safe flag. A custom filter that mark_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 &lt;em&gt;Hi&lt;/em&gt;. This was fail-safe over-escaping, NOT an XSS — a parity gap, not a security hole. Fix: a new get_value_safe returns (Value, bool runtime_safe), threading the safe flag out of the pipe loop via apply_filter_full_safe (mirroring the per-iteration runtime_safe = produced_safe pattern from the #1660 Variable arm); the FirstOf/Cycle emit arms skip auto-escaping when the value is a genuine runtime SafeString. get_value is 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 real str-subclass with __html__), so it can never under-escape a plain value. Pinned by 7 regression cases in TestFirstofCycleRuntimeSafe_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 RUNTIME mark_safe()-ness through the {% firstof %} / {% cycle %} emit path via get_value_safe, but that helper did not consult the name-based safe_output_filters whitelist (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 %} (where urlize emits 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 custom is_safe=True filter), mirroring the Variable/InlineIf arms exactly. The whitelist was hoisted from two inline copies into a single shared module const SAFE_OUTPUT_FILTERS so 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 in renderer::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 in tests/unit/test_rust_firstof_cycle_named_safe_1692.py (gate-off verified).
  • client.min.js no longer logs Uncaught ReferenceError: applyPatches is not defined in production (#1688). A recurrence of the #1676 terser-mangle × IIFE class, different manifestation. 45-child-view.js referenced the bare applyPatches symbol at two sites (_applyScopedPatches and the djust._applyPatches expose block), but applyPatches is declared inside 12-vdom-patch.js's own inner IIFE and published only as globalThis.djust.applyPatches. The bare cross-IIFE reference is out of scope: it silently no-ops in the unminified bundle (leaving djust._applyPatches unwired, so emitChildMountedEvents — the child-mounted lifecycle for embedded/sticky views — never runs) and throws in the terser-minified production bundle (served when DEBUG=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 alias globalThis.djust.applyPatches at both sites, which is minification-independent and also restores the intended _applyPatches wiring. Pinned by a behavioral regression (tests/js/min_bundle_applypatches_1688.test.js) asserting djust._applyPatches is wired after load (gate-off verified: undefined on the pre-fix bundle).
  • dj-dialog-close-event (35-dj-dialog.js) and keyboard-nav dj-click dispatch (51-keyboard-nav.js) no longer reference a bare out-of-scope handleEvent (#1706). Found by the new cross-IIFE static guard (above): both modules referenced the bare handleEvent symbol, which is declared in 11-event-handler.js inside the double-load-guard else {} block (block-scoped) and published only as globalThis.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 — the typeof handleEvent === 'function' guard returned "undefined", so the dialog close event and the keyboard-nav dj-click activation silently no-opped — and would throw ReferenceError under terser-minified bundles. Exactly the #1688 class, two more sites. Fix: read the published alias globalThis.djust.handleEvent at all four sites (minification-independent). The dj_dialog / keyboard_nav test 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 ↔ manifest import SCC in djust.theming (#1662, follow-up to #1661). After #1661 extracted get_theme_config to the leaf _config, the AST import graph (lazy + eager) still had a pre-existing, never-CodeQL-flagged strongly-connected component: theme_packs/manifest imported registry.get_registry while registry imported theme_packs/manifest for discovery. Fix (same leaf-module pattern as #1661): extract ThemeRegistry + get_registry into a new leaf module _registry_accessor.py (imports only stdlib) so theme_packs / manifest / presets / etc. reach the singleton WITHOUT importing back into registry; discovery (the only registry → theme_packs/manifest edges) stays in registry.py and is installed as a hook via set_discovery_hook, making the dependency one-directional. registry.py re-exports ThemeRegistry / get_registry / register_* so from djust.theming.registry import … keeps working — no runtime, behavior, or public-API change. test_theming_no_cyclic_import.py is tightened to assert the WHOLE djust.theming package import graph is acyclic (Tarjan over all modules, counting both from .X import and from . import X edges) plus a leaf-purity gate — previously only presets/manager/css_generator were 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].

All releases · Atom feed