djust 1.0.0rc16

Pre-releaseReleased
Install
pip install djust==1.0.0rc16

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

Before you upgrade, read the upgrade guide.

Added

  • MoveSubtree VDOM patch — matched {% if %} boundaries can now be repositioned (#1666). When a dj-if conditional boundary is present in both the old and new render but its position among the parent's significant children shifts (siblings added/removed/reordered around it), the markers — id-less #comment nodes that a plain MoveChild can't target — previously stayed anchored at their old position, so the conditional rendered in the wrong place (or the round-trip diverged). The differ now emits MoveSubtree { id, path, d, index } (the "move" verb for boundary spans, completing the Remove/Insert/Move trio), and the client (12-vdom-patch.js) locates the <!--dj-if id="X"-->...<!--/dj-if--> marker pair by id, detaches the whole range, and re-inserts it at the target index — preserving inner node identity (and any state/focus tied to inner dj-ids), unlike a Remove+Insert. Applied in a new phase after the path/index child ops so the surrounding siblings have settled. A client-faithful differential harness measured the matched-boundary-reposition residual drop from ~22 to ~6 per 6000 adversarial re-renders. Wire shape pinned in wire_protocol_snapshot.rs; behavior pinned by matched_djif_boundary_repositioned_via_move_subtree (Rust) and tests/js/move_subtree_patch.test.js (client).

  • LiveViewTestClient.assert_allowlisted() + assert_all_routed_liveviews_allowlisted() — make the allowlist gap testable (#1674). A URL-routed LiveView forgotten from LIVEVIEW_ALLOWED_MODULES has its WebSocket mount rejected and silently degrades to full-page HTTP re-renders — but mount() instantiates the view class directly, bypassing the allowlist, so the misconfiguration was invisible to the unit suite. client.assert_allowlisted() fails fast for one view; the standalone assert_all_routed_liveviews_allowlisted() walks the root URLconf and guards every routed view in a single test. Both mirror the runtime enforcement exactly (non-empty allowlist, prefix match) and are no-ops when the allowlist is unset/empty.

Fixed

  • VDOM InsertChild.ref_d no longer mis-inserts under sibling reorder. The differ populated ref_d with a positional guess (old.get(new_index).djust_id) — the dj-id of whatever old node happened to sit at the new node's index. The client (12-vdom-patch.js) honors ref_d (it does insertBefore(node, querySelector(':scope > [dj-id=ref_d]')) and only falls back to the index when ref_d is absent), so under a reorder that guess is the wrong reference and the new node lands in the wrong position. The differ now emits ref_d: None and relies on the index: InsertChild is applied last in the client's phase order (after RemoveSubtree/InsertSubtree/RemoveChild/MoveChild), so the index is resolved against the settled new-frame and is reliable. A client-faithful differential harness measured ~18 mis-inserts per 6000 adversarial re-renders before the fix, 0 after. The #1408 invariant is preserved (a ref_d, when present, must resolve in the OLD tree). Regression-pinned by keyed_insert_ref_d_is_safe_not_a_wrong_guess in crates/djust_vdom/tests/test_diff_robustness_gaps.rs.

  • Custom template filter that mark_safe()s its output at runtime is no longer HTML-escaped (#1660). The Rust renderer decided auto-escaping purely by filter name (the static is_safe=True flag / the built-in safe_output_filters list), never by the filter result's runtime SafeString-ness — unlike Django's render_value_in_context, which escapes iff the final value lacks __html__. So a filter like @register.filter def md(v): return mark_safe(...) (no is_safe=True) had its HTML escaped on both the HTTP-prerender and WS-mount paths. apply_custom_filter now reports whether the Python result carries __html__; apply_filter_full_safe threads that out as a per-filter runtime-safe flag; and the renderer (Variable + InlineIf arms) honours the last filter's runtime safeness in its escape decision (additive — a later plain-returning filter re-taints, matching Django). Security-hardened: the runtime-safe marker is honoured only for a genuine str subclass with __html__ (a real SafeString) — Django stringifies any non-str value before the __html__ check, and djust's Value extraction stringifies arbitrary objects via __str__, so trusting __html__ alone would let a non-str object's attacker-controlled __str__ reach output unescaped (XSS). Workaround no longer needed: @register.filter(is_safe=True) and pre-render+|safe still work. Pinned by the runtime-SafeString matrix (text/attribute context, x|md|upper re-taint, x|upper|md, plain-filter-still-escaped) and the non-str-__html__ impostor XSS guard in tests/unit/test_rust_custom_filters_1121.py; gate-off self-test (Action #1200/#1468) + a 5-lens adversarial XSS verification confirmed non-tautological. The parallel {% firstof %}/{% cycle %} get_value pipe path still over-escapes a runtime-safe value (fail-safe, not XSS) — tracked in #1672.

  • Unallowlisted URL-routed LiveView no longer degrades silently — actionable signal at ship-time and runtime (#1674). A URL-routed LiveView whose module is missing from LIVEVIEW_ALLOWED_MODULES has its WebSocket mount correctly rejected, then silently falls back to full-page HTTP re-renders that look like the app works (events fire, DB updates, page re-renders) — a DX trap, not a security/correctness bug. Two fixes: (1) the djust.V005 system check now also discovers URL-routed views by walking the root URLconf (_routed_liveview_classes), not only __subclasses__() (which is import-timing dependent), so the misconfiguration is caught at manage.py check/ship-time; V005's matching was aligned with the WebSocket mount enforcement (websocket.py) — prefix match, enforced only when the allowlist is non-empty — fixing a pre-existing false positive where an empty [] flagged every view and a prefix allowlist (['myapp']) wrongly flagged myapp.views (parallel-path-drift, CLAUDE.md #1646). (2) The client's HTTP-fallback notice (11-event-handler.js), previously a djustDebug-gated console.log (silent by default), is now a once-per-session, un-gated console.warn pointing the developer at LIVEVIEW_ALLOWED_MODULES. Pinned by python/djust/tests/test_v005_routed_allowlist_1674.py, test_assert_allowlisted_1674.py, and tests/js/http_fallback_warning_1674.test.js; dogfooded against the demo (1 real finding, no flood).

Tests

  • Apply-level regression pin for the #1636 {% if %} 1/N patch failure. #1636's InsertChild 1/N patches failed (a false→true {% if %} add) was a manifestation of #1640 (the index resolver getSignificantChildren and the path walker getNodeByPath disagreeing on whether a regular HTML comment counts) and was confirmed fixed on rc14 by the consumer. #1640 shipped a predicate-level test (in the significant_children_comment_filter_1640 suite); this adds the missing applied-patch-level pin for the chronically-reopened if-block cluster (#1358/#1408/#1550/#1552/#1555/#1636): an InsertChild whose index points past a regular <!-- comment --> lands between the right significant siblings, not one slot early (the 1/N). Gate-off proof (Action #1200/#1468): flipping isSignificantChild back to counting all comments and rebuilding makes the mid-insert case fail (the inserted node lands at significant index 0 instead of 1). 3 regression cases in tests/js/insert_child_regular_comment_1636.test.js. No source change — guards an already-shipped fix.

All releases · Atom feed