djust 0.5.2rc1

Pre-releaseReleased

Added

  • WebSocket per-message compression toggle — DJUST_WS_COMPRESSION (v0.6.0) — VDOM patches compress extremely well (repetitive HTML fragments + JSON structure → 60-80 % wire-size reduction via zlib). Uvicorn and Daphne both negotiate permessage-deflate with browsers out of the box, so the wire-level compression is already free in most deployments — this change adds the declarative config toggle + documentation so operators can verify it's active, reason about the ~64 KB per-connection zlib context cost, and disable it cleanly on extreme-connection-density deployments or when running behind a compressing CDN. New websocket_compression config key (default True) exposed via djust.config.config, bridged from a top-level settings.DJUST_WS_COMPRESSION for discoverability, and surfaced to the injected client bootstrap as window.DJUST_WS_COMPRESSION (application code can branch on it to skip manual JSON.stringify optimizations that only help without wire-level compression). 6 tests in tests/unit/test_ws_compression_config.py cover default, override to True/False, truthy/falsy coercion, and client-script emission. Deployment guide (docs/website/guides/deployment.md) gains a new "WebSocket per-message compression" section covering the memory tradeoff, CDN double-compression footgun, and Uvicorn/Daphne flags. (python/djust/config.py, python/djust/mixins/post_processing.py)

  • Declarative UX attributes — dj-mutation, dj-sticky-scroll, dj-track-static (v0.6.0) — Three small client-side declarative attributes that replace boilerplate dj-hooks every production app tends to write. dj-mutation (new static/djust/src/37-dj-mutation.js, ~100 LOC) fires a dj-mutation-fire CustomEvent when the marked element's attributes or children change via MutationObserver, with dj-mutation-attr="class,style" for targeted attribute filters and dj-mutation-debounce="N" for burst coalescing (default 150 ms). dj-sticky-scroll (new 38-dj-sticky-scroll.js, ~90 LOC) keeps a scrollable container pinned to the bottom when children are appended but backs off when the user scrolls up to read history and resumes when they return to the bottom — the canonical chat / log viewer UX with a 1 px sub-pixel tolerance. dj-track-static (new 39-dj-track-static.js, ~90 LOC; Phoenix phx-track-static parity) snapshots tracked <script src> / <link href> values on page load and, on every subsequent djust:ws-reconnected event, diffs against the snapshot — dispatches dj:stale-assets CustomEvent on changed URLs, or calls window.location.reload() when the changed element carried dj-track-static="reload". Without this last one, clients on long-lived WebSocket connections silently run stale JS after a deploy — zero-downtime on the server but broken behavior on connected clients. Supporting change in 03-websocket.js: onopen now dispatches document.dispatchEvent(new CustomEvent('djust:ws-reconnected')) on every reconnect so application code (not just dj-track-static) can hook reconnects without touching internal WS state. Convenience Django template tag {% djust_track_static %} in live_tags.py emits the bare attribute for discoverability. All three attributes live-register via a document-level MutationObserver root (same pattern as dj-dialog) so VDOM morphs that inject or remove the marker re-wire observers automatically. 15 JSDOM test cases across tests/js/dj_mutation.test.js, tests/js/dj_sticky_scroll.test.js, tests/js/dj_track_static.test.js; 4 Python test cases in tests/unit/test_djust_track_static_tag.py. (python/djust/static/djust/src/37-dj-mutation.js, 38-dj-sticky-scroll.js, 39-dj-track-static.js, 03-websocket.js, python/djust/templatetags/live_tags.py)

    See docs/website/guides/declarative-ux-attrs.md.

  • djust.db.untrack(model) — disconnect signal receivers wired by @notify_on_save (#809) — Previously the only way to detach the post_save / post_delete receivers from a @notify_on_save-decorated model was to clear the entire signals.receivers list, which scorched unrelated test fixtures. untrack() now disconnects exactly the two receivers stashed on model._djust_notify_receivers and wipes the introspection attributes (_djust_notify_channel, _djust_notify_receivers) so a re-decoration goes through cleanly with a fresh channel. Returns True on success, False on a never-decorated model — idempotent, safe to call twice. Primarily for pytest teardowns in projects that decorate models at class-definition time. 5 tests in tests/unit/test_db_notifications.py::TestUntrack. Exported from djust.db and documented in the djust.db module docstring. (python/djust/db/decorators.py, python/djust/db/__init__.py)

    See docs/website/guides/database-notifications.md.

  • Pre-minified client.js distribution (v0.6.0 P1) — Production now serves client.min.js (terser-minified) instead of the 35-module readable concat, with .gz and .br pre-compressed siblings built alongside it for whitenoise / nginx static serving. Measured impact: client.js 410 KB → client.min.js 146 KB raw → 39 KB gzip → 33 KB brotli (~92% reduction wire-size over the raw file). DEBUG=True continues to serve the readable client.js so stack traces point at meaningful line numbers and contributors can poke at source directly. An explicit DJUST_CLIENT_JS_MINIFIED setting (bool) overrides the DEBUG heuristic in either direction so operators can validate the minified file locally or keep the readable build in production if they want to debug in-situ. scripts/build-client.sh gained a minify_and_compress helper that runs terser (from node_modules/.bin/terser or PATH), then gzip -9 and brotli -q 11; the step is skipped gracefully when terser isn't installed so contributors can still iterate on raw sources without npm install. Source-maps (.min.js.map) are emitted for production-side debugging. djust.C012 system check now recognizes both client.js and client.min.js in manual-loading detection. 6 tests in tests/unit/test_client_minified.py cover build-artifact presence + size reduction, DEBUG-vs-production script selection, and the explicit override in both directions. (scripts/build-client.sh, python/djust/mixins/post_processing.py, python/djust/checks.py, package.json)

Changed

  • Documented block-handler nesting + loader-access constraints (#803, #804) — Two low-priority gaps deferred from PR #802 are now surfaced in both the Rust-side register_block_tag_handler docstring (crates/djust_templates/src/registry.rs) and the Python-side .pyi stub (python/djust/_rust.pyi). The "no parent-tag propagation" constraint (#804) means a nested block handler is not informed it sits inside a parent handler — pass a hint through context instead. The "no loader access from handlers" constraint (#803) means block handlers cannot call {% render_template %}-style loads — pre-render child templates in the view. Both constraints were silently-true before this change; surfacing them prevents surprise when handler authors reach for features the current dispatcher doesn't yet support. No runtime behavior change. (crates/djust_templates/src/registry.rs, python/djust/_rust.pyi)

Fixed

  • assign_async concurrent same-name cancellation semantics (#793) — Two rapid assign_async("metrics", loader) calls used to race: the first loader's worker thread could still be in-flight when the second call scheduled a new task, and when the slow loader finally completed, its setattr(self, "metrics", AsyncResult.succeeded(stale)) clobbered the fresh AsyncResult.pending() that the second call had just written. assign_async() now maintains a per-attribute generation counter (self._assign_async_gens[name]) bumped on every call; each loader's runner closure captures the generation at creation time and short-circuits on both the success and error paths when a newer call has superseded it. The in-flight stale runner still completes (no mid-flight cancellation), but its result is discarded via a DEBUG log — the fresh pending state survives. 4 regression cases in tests/unit/test_assign_async.py: sync success-path, sync error-path, async-loader success-path, and a generation-counter sanity check. (python/djust/mixins/async_work.py)

  • Template dep-tracking: filter-arg bare identifiers (#787){{ value|default:fallback }} now tracks fallback as a template dependency alongside value. Previously the dep-extractor walked filter chains but dropped all filter arguments, so a pattern like {% if show %}{{ value|default:dynamic }}{% endif %} would fail to re-render when only dynamic changed — the render cache classified the node as dep-clean and the partial-render pipeline skipped it. Literal filter args (default:"none", default:'none', default:0, default:-1) are correctly excluded from the dep set; only bare identifiers and dotted paths are tracked. Landed via a two-step: parse_filter_specs now preserves surrounding quotes on literal args so the extractor can distinguish literals from identifiers, and render-time filter application strips quotes via the new strip_filter_arg_quotes helper. No change to filter runtime semantics. 15 regression cases in tests/unit/test_template_dep_tracking_787_806.py. (crates/djust_templates/src/parser.rs, crates/djust_templates/src/renderer.rs)

  • Template for-iterables resolve through getattr walk (#806){% for x in foo.bar %} now uses Context::resolve (which walks getattr through the raw-PyObject sidecar) with a fallback to Context::get, instead of only consulting the value-stack. Previously dotted iterables silently rendered as empty when the attribute was not a top-level dict key — affecting Django QuerySet relations (user.orders), dataclass attributes, and nested Python objects. Covered by two direct-access tests (nested attributes + relation stub) + existing top-level + empty-block + missing-attr regression tests. (crates/djust_templates/src/renderer.rs)

  • send_pg_notify payload size guard (#810) — PostgreSQL caps NOTIFY payloads at 8000 bytes. send_pg_notify() now warns at 4KB (soft limit) and drops + error-logs at 7500 bytes (hard limit). (python/djust/db/decorators.py)

  • PostgresNotifyListener.areset_for_tests() awaits task cancellation (#811) — The existing reset_for_tests() fire-and-forget cancel is now documented as such; new async variant awaits the cancelled task so async test teardowns don't race. (python/djust/db/notifications.py)

  • db_notify render-lock timeout documented (#813) — 100ms timeout is best-effort under contention; dropped notifications do not queue. (python/djust/websocket.py)

  • Regression test: consumer handles views without NotificationMixin (#812) — Locks in that getattr(view, '_listen_channels', None) + truthy gate handles both absent-attr and empty-set paths. (tests/unit/test_db_notifications.py)

  • stream() with limit=N pre-trims emitted inserts (#799) — Server trims items_list to at-most limit before emitting inserts. (python/djust/mixins/streams.py)

  • teardownVirtualList restores original children (#798) — Teardown now restores pre-virtualization children and removes the shell/spacer. (python/djust/static/djust/src/29-virtual-list.js)

  • stream_prune.children filter redundancy removed (#801) — Cosmetic cleanup. (python/djust/static/djust/src/17-streaming.js)

  • LiveViewTestClient.render_async() invokes handle_async_result (#843) — Test-client drain now mirrors the production WS consumer. (python/djust/testing.py)

  • LiveViewTestClient.follow_redirect() refuses to pick silently when multiple redirects queued (#844) — Raises AssertionError with all queued paths. (python/djust/testing.py)

  • UploadWriter close() return validated as JSON-serializable (#825) — Non-JSON returns caught at finalize time and abort the upload cleanly. (python/djust/uploads.py)

  • BufferedUploadWriter write_chunk() after close() raises (#823)_finalized flag now actively enforced; repeated close() is idempotent. (python/djust/uploads.py)

  • Upload-manager drops trailing chunks silently after abort (#824, partial) — Fast-path at DEBUG log; writer.abort() called once. (python/djust/uploads.py)

  • Morph-path honors dj-ignore-attrs (#815) — The VDOM morph loop at python/djust/static/djust/src/12-vdom-patch.js:746-758 previously stripped and overwrote attributes without consulting djust.isIgnoredAttr. Attributes listed in dj-ignore-attrs would survive individual SetAttr patches (the guard added in PR #814) but could still get wiped during a full-element morph. The morph-path remove-loop and set-loop both now skip ignored attribute names. Two regression tests in tests/js/ignore_attrs.test.js cover remove-loop and set-loop preservation. (python/djust/static/djust/src/12-vdom-patch.js)

Changed

  • dj-ignore-attrs CSV empty-token hardening (#816)isIgnoredAttr now skips empty tokens produced by double-comma ("open,,close") or trailing-comma ("open,") CSV values, and rejects empty attribute-name queries. Previously those edge cases could accidentally match an empty attribute name. Four regression tests in tests/js/ignore_attrs.test.js cover empty string, whitespace-only, double comma, and trailing comma. (python/djust/static/djust/src/31-ignore-attrs.js)

Added

  • djust_typecheck{% firstof %} / {% cycle %} / {% blocktrans with %} tag support (#850) — The extractor now captures positional context-variable references in {% firstof a b c %} and {% cycle a b c %} (string literals and as <name> suffixes are correctly ignored), and the with x=expr (and count x=expr) clauses of {% blocktrans %} / {% blocktranslate %} produce both the template-local binding (x) and the reference (expr). Eliminates a class of false positives (blocktrans locals) and false negatives (firstof/cycle args). (python/djust/management/commands/djust_typecheck.py)

    See docs/website/guides/typecheck.md.

Changed

  • djust_typecheck — walk MRO for parent-class self.foo = ... assigns (#851)_extract_context_keys_from_ast now iterates cls.__mro__ (skipping djust.*, djust_*, django.*, rest_framework.*, and builtins), so a child view that relies on attributes set in a parent mount() no longer produces spurious "unresolved" reports. The filter drops Django's View / namespace-framework attrs (request, head, kwargs, args) that would otherwise surface from the base class. (python/djust/management/commands/djust_typecheck.py)

  • Shared class-introspection helpers (#852)_walk_subclasses, _is_user_class, and _app_label_for_class are now a single source of truth in the new djust.management._introspect module; djust_audit and djust_typecheck both import from it. No behavior change; purely a refactor to prevent drift as the set of management commands grows. _introspect.walk_subclasses also gained cycle-safety (diamond-inheritance deduplication) which the old recursive implementation lacked. (python/djust/management/_introspect.py, python/djust/management/commands/djust_audit.py, python/djust/management/commands/djust_typecheck.py)

  • Service worker + main-only middleware follow-ups to PR #826 (closes #827/#828/#829/#830)

    • #828DjustMainOnlyMiddleware now early-returns on responses with status_code >= 400. Error pages render full-page layouts (status message, "go back" link, etc.); trimming them to <main> would strip that context from shell-navigation clients. Regression tests cover 4xx and 5xx.
    • #830 — HTML response detection widened to include application/xhtml+xml in addition to text/html. Charset and boundary suffixes (text/html; charset=utf-8; boundary=xyz) are stripped before matching. Defensive test confirms application/rss+xml is still treated as non-HTML.
    • #829djust.registerServiceWorker() is now idempotent. A second call returns the cached registration promise without re-running initInstantShell / initReconnectionBridge, so drain listeners and the WS sendMessage patch are applied at most once. Previous behavior caused buffered replays to double on repeat init.
    • #827 — Documented the <script>-inside-<main> limitation of the instant-shell innerHTML swap at the top of 33-sw-registration.js. The doc block was also corrected: dj-click/dj-submit/etc. work through document-level event delegation (not MutationObserver), and dj-hook now explicitly re-runs via a djust.reinitAfterDOMUpdate(placeholder) call after the swap — dj-hook content inside <main> actually works post-swap as a result (previous implementation silently skipped hook re-binding).

    Tests: 9 → 13 Python cases in tests/unit/test_main_only_middleware.py, +2 JS cases in tests/js/service_worker.test.js (12 total). (python/djust/middleware.py, python/djust/static/djust/src/33-sw-registration.js)

All releases · Atom feed