djust 0.9.2rc2

Pre-releaseReleased

This is a pre-release. djust 0.9.2 has shipped since: read the djust 0.9.2 release notes.

Fixed

  • dj-transition now accepts 1-token short form, matching documented grammar. Closes #1273. The dj-transition-group docs at 43-dj-transition-group.js:22-23 advertise short form like dj-transition-group="fade-in | fade-out" where each half can be a 1-token spec — but _parseSpec at 41-dj-transition.js:45-60 required 3 tokens, so the ENTER side was silently rejected and no animation fired. Fix: extend _parseSpec to accept 1-token form, return {single: <class>}; _runTransition handles single by applying the class on next frame and waiting for transitionend. 2-token form remains rejected as ambiguous (matches dj-remove). 2 new regression cases in TestParseSpecAcceptsShortForm + behavioral test (tests/js/dj_transition.test.js).

  • AsyncResult now serializes to a dict templates can navigate. Closes #1274. assign_async() returns AsyncResult instances that templates expect to read as {{ users.loading }}, {{ users.ok }}, {{ users.failed }}, {{ users.result }}, {{ users.error }}. Before this fix, neither normalize_django_value nor DjangoJSONEncoder.default had an AsyncResult branch — the value fell through to str(), producing "AsyncResult(loading=True, ...)" which templates couldn't navigate. Result: every assign_async demo rendered blank. Fix: new AsyncResult.to_dict() method + register in both serializer paths; normalize_django_value recurses into the dict so a non-primitive result payload (Django Model, datetime, Decimal nested in result) is normalized too. 11 regression cases in python/djust/tests/test_async_result_serializer.py.

  • Form submit now flushes pending debounced dj-input handlers before dispatching. Closes #1278. Text/email/password inputs with dj-input defaulted to 300ms debounce; a user who typed and immediately clicked submit raced the submit handler past the pending input events. Views that depended on server-side state populated by dj-input handlers (e.g., WizardMixin's wizard_step_data) saw stale state at submit time. Fix: debounce() now exposes a .flush() method; new _flushPendingDebouncesInForm(form) iterates the form's [dj-input] descendants and flushes any pending wrappers; _handleDjSubmit calls it before reading FormData / dispatching. 4 regression cases in tests/js/dj_submit_debounce_flush.test.js.

  • dj-dialog client-close (ESC/backdrop/dialog.close()) now syncs back to server. Closes #1267. Previously dj-dialog was one-way (server→client); the user closing a dialog client-side left server state believing it was still open. Re-opening from the server became a no-op because re-asserting dj-dialog="open" wasn't a value change. Fix: new dj-dialog-close-event="..." attribute opts into a native close event listener that dispatches the configured event name to the server. Idempotent across re-syncs (WeakMap guard); reads attribute at fire time so morph updates take effect. 5 new regression cases in tests/js/dj_dialog.test.js.

  • SSE transport: EventSource and dispatch POST now send Django session cookie. Closes #1277. Authenticated views over SSE failed check_view_auth on every mount because the EventSource GET (and the message POST) didn't carry credentials. Result: infinite mount→navigate loop on authenticated views. Fix: 03b-sse.js:69 opens EventSource with {withCredentials: true}; 03b-sse.js:367 sendMessage POST sets credentials: 'include'. 2 new regression cases in tests/js/sse-transport.test.js.

  • mount() lifecycle: queued async work and push events are now drained after the mount frame. Closes #1280 (assign_async() / start_async() called from mount() never resolved over WebSocket — view stayed at initial loading-state HTML forever) and #1283 (push_event() called from mount() or on_mount hooks queued events that never reached the client). Both root at the same site: LiveViewConsumer.handle_mount() ended with send_json(response) without draining _async_tasks or _pending_push_events. The fix mirrors the established pattern in handle_event() / _flush_deferred_activity_events(): send the response frame, then drain push events, then dispatch async work. 3 regression cases in TestHandleMountSourceShape (python/djust/tests/test_handle_mount_drains_queues.py).

  • data_table integration restored over WebSocket — emit defaults renamed to match on_table_* mixin handler convention. Closes #1275 (tag emitted 23 event names that didn't match any handler), #1291 (pagination handlers entirely missing from the mixin), #1279 (handlers mutate state but never refresh rows). Single root cause: the WS dispatcher does exact-match getattr(view, event_name, None) (websocket_utils.py:173), but the tag-emit defaults previously used bare table_* strings while DataTableMixin uses on_* Phoenix-style handler names — so every default WS interaction returned "no handler found". Fix: rename tag-emit defaults across 4 files (92 lines: templatetags/djust_components.py, mixins/data_table.py class-level attrs + _PRE_MOUNT_TABLE_CONTEXT, components/rust_handlers.py, templatetags/_forms.py); add on_table_prev / on_table_next handlers (clamped to [1, table_total_pages]); call refresh_table() from sort/search/filter/page/prev/next handlers (selection handler deliberately exempt — UI state). 15 regression cases in TestDataTableEmitToHandlerCrossReference, TestPaginationHandlersExist, TestRowAffectingHandlersCallRefresh (python/djust/tests/test_data_table_handler_contracts.py). Subclasses that overrode table_X_event class attrs are unaffected — only the bare-default path was broken.

  • @action no longer re-raises after recording exception state. Closes #1276. The decorator's docstring promised templates could read {{ <name>.error }} after an exception, but the implementation re-raised — the dispatcher's exception-frame path then bypassed the re-render and the template never saw the recorded error field. Fix: catch Exception (not BaseException), record state, log at ERROR level via logger.exception, return None. BaseException subclasses (KeyboardInterrupt, SystemExit, GeneratorExit) still propagate by Python convention. 8 new regression cases in TestActionExceptionDoesNotPropagate + TestActionSuccessRecordsState

    • TestActionLazyInitializesActionState (python/djust/tests/test_action_decorator_contract.py); 6 existing tests in test_action_decorator.py updated to the new contract. Docstring at decorators.py:262-272 rewritten to match. Behavior change: code that wraps @action calls in try/except to handle the re-raise now sees a clean return. Mirror the old behavior by re-raising explicitly inside the handler.

Documentation

  • Lifecycle Coverage Audit + Decorator/Tag Contract Audit (docs/audits/lifecycle-2026-05.md, docs/audits/decorator-contract-2026-05.md). Two companion audit docs modeled on the v0.9.2-4 VDOM audit. Document the canonical state-type × lifecycle-hook matrix and the decorator/tag-name dispatch contract, surfaced from 10 downstream consumer bug reports (#1267, #1273-#1281). The lifecycle audit catalogues 8 ranked weaknesses including the central control-flow gaps in mount() (#1280, #1281). The decorator/tag audit catalogues 8 weaknesses including the data_table tag emitting 23 event names that don't match any DataTableMixin handler (#1275 generalized). Each audit ships with a 4-phase improvement roadmap, test gaps, strategic observations, and a companion canon update for CLAUDE.md / PR-checklist. Pre-staged issues filed for each not-yet-tracked weakness (#1283-#1291). Audit-driven Phase 1 fixes blocking v0.9.2 stable will land in the v0.9.2-5 drain bucket; Phase 2/3 fixes targeted for v0.9.3.

  • Production Deployment guide extended with Tier 1/2/3 patterns (docs/website/guides/deployment.md). Adds 8 new sections to the canonical deployment guide based on patterns surfaced from real-world djust deployments:

    • Channel Layer (cross-process push) — separate concern from DJUST_STATE_BACKEND, required when any view uses push_to_view, presence, or cursor tracking.
    • Database Connection Pooling — three-layer guidance (CONN_MAX_AGE, PgBouncer, RDS Proxy), with the LISTEN/NOTIFY caveat for transaction-mode pooling.
    • Celery Integration — broker choice (Redis vs SQS), pool choice (prefork vs gevent), beat-singleton invariant, gevent monkey-patch gotcha, queue-depth-based worker auto-scaling.
    • Static and Media Files — cloud-agnostic CDN options, S3 + CloudFront config, ASGI_SERVE_STATIC=False opt-out for offloading static-file serving from the ASGI server.
    • WebSocket stickiness on AWS ALB — the simpler "stick on Django sessionid" pattern as an alternative to a custom application-set cookie.
    • Sizing and Scaling Tiers — concrete vCPU/RAM recommendations indexed to concurrent active users (≤50, 50-500, >500), with explicit "when to escalate" triggers.
    • "What's Already Production-Ready in djust" — anti-recommendation list (Redis state, channels_redis, sync_to_async, transaction.on_commit, Origin check, HSTS) so users don't re-evaluate canonical patterns on every deployment.
    • Extended Gunicorn+Uvicorn workers section with concrete production CMD + flag rationale (-w sizing, --timeout 120, --keep-alive 5). Cloud-agnostic where possible; AWS as canonical example with PgBouncer / GCS / Cloudflare R2 noted in parallel.

Developer Experience

  • Pipeline-template canon: Stage 7 self-applicability check for canon PRs (#1248). New optional checklist item in .pipeline-templates/{feature,bugfix}-state.json Stage 7 fires when a PR adds new mandatory rules. Asks: (a) does the new rule false-positive on this PR's own diff? (b) would the new rule have caught the originating bug at the stage it adds? Both must be explicitly answered. v0.9.2-2 retro Action Tracker #206.
  • Pipeline-template canon: Stage 5/9/10 bundling check (#1251). New mandatory checklist item runs git diff --cached --stat immediately before git commit and verifies the staged line counts match the planned scope. Catches the failure mode where git add <file> silently bundles pre-existing uncommitted modifications (the pattern that hit pipeline-skill commit bf1a67f, silently bundling 130 unintended lines). v0.9.2-2 retro Action Tracker #209.
  • Audit script: extract retro-marker regex to shared constants module (#1249). Created scripts/lib/retro_markers.py with RETRO_MARKER_REGEX. The audit script (scripts/audit-pipeline-bypass.py) now imports the canonical constant rather than embedding the literal. Stage 14 subagent_prompt text in both pipeline templates references the script-canonical file rather than re-defining the regex. Single source of truth across consumers. v0.9.2-2 retro Action Tracker #207. 4 unit tests at scripts/lib/test_retro_markers.py.
  • Audit script: scan direct-to-main commits + Audit-bypass-reason: trailer support (#1250). The retro-gate audit GHA previously scanned merged PRs only; direct commits to main bypassed it (e.g., the v0.9.2-2 milestone-open commit 18e5b117). The audit now also lists direct-to-main commits since the lookback window, filters out PR-squash commits via (#NNN) subject suffix, and honors an Audit-bypass-reason: <text> commit-message trailer for legitimate exemptions (e.g., docs-only ROADMAP updates per the pipeline-drain skill). v0.9.2-2 retro Action Tracker #208.

Fixed

  • VDOM: mixed keyed/unkeyed children diff round-trip correctness (#1260). Surfaced by proptest during v0.9.2rc1 pre-flight. The LIS optimization in diff_keyed_children (crates/djust_vdom/src/diff.rs) skipped emitting MoveChild patches for keyed children whose trivial-length-1 LIS made them appear "in place" — relying on other patches' implicit position shifts to land them at the correct absolute index. This works for fully-keyed sibling lists (where all moves coordinate via absolute indices) but breaks when unkeyed siblings are interleaved (their patches use positions relative to other unkeyed nodes only). The keyed child ended up stranded at an arbitrary index after all patches applied. Fix detects has_unkeyed_siblings upfront; in the mixed case, falls back to "always emit MoveChild when old_idx != new_idx" instead of the LIS-implicit-position optimization. The fully-keyed path is unchanged. Audit weakness #5/#6 (rated 🟡 with warnings only) upgraded to 🟠 by this fuzz finding; this is the actual fix. 4 deterministic regression tests in crates/djust_vdom/tests/test_mixed_keyed_unkeyed_reorder_1260.rs
    • permanent proptest seed in fuzz_test.proptest-regressions.

All releases · Atom feed