This is a pre-release. djust 1.2.0 has shipped since.
Part of djust 1.2 — read the 1.2 release notes.
Added
djust.T018system check — undefined template variable detection:manage.py check(anddjust_check) now warns when a LiveView template references a variable that resolves nowhere and silently renders as an empty string. Compares template variable references against the view's statically-determinable context (public class attributes,self.x = ...assignments, literalget_context_data()dict keys, template-declared loop vars, and framework/Django-injected names), reusing the same extractionmanage.py djust_typecheckhas shipped since v0.5.1. Covers bothtemplate_name(file) and inlinetemplate = "..."views. Advisory (WARNING, not ERROR); suppress project-wide withDJUST_CONFIG = {'suppress_checks': ['T018']}or per-name with the existing{# djust_typecheck: noqa name #}template comment. Known v1 limitation:{% extends %}templates are skipped entirely (block-override context is inheritance-scoped and not statically checkable here). Also fixes a pre-existingdjust_typecheckfalse positive where a dotted{% if x.y %}expression incorrectly reported the attribute tail (y) as its own undefined top-level reference.LiveComponentTestClient— test aLiveComponentin isolation: the component counterpart toLiveViewTestClient. A component has no view, notemplate_namedispatch and no URL, so it could not be mounted through the view client; testing one previously meant wrapping it in a throwaway parentLiveView.LiveComponentTestClient(MyComponent).mount(**props)returns the client,send_event(name, **params)calls the component's own handler method,get_state()returns its public (non-underscore) attributes, andrender()delegates toLiveComponent.render. A missing handler raisesNoHandlerFoundErrorrather than silently no-opping, matchingLiveViewTestClient.send_event(#2823) — a renamed handler should fail loudly, not leave a test passing while the feature is broken. Like the view client it calls handlers directly rather than routing through the WebSocket consumer, so@event_handlermetadata is not enforced; useLiveViewTestClientwhen the routing contract itself is under test.dj-mouseenter/dj-mouseleaveare now real directives (#2869). The attributes were previously stamp-listed by{% live_render %}— the framework asserted they existed — but the client never bound them, so a developer writing<div dj-mouseenter="highlight">got a stamped attribute, no listener, and no warning. They are now wired as first-class event directives:mouseenter/mouseleavedo not bubble, so the client attaches listeners DIRECTLY to the declaring element through the existing scoped-listener machinery instead of the delegated root-level shapedj-clickuses. Nesting semantics are the platform's own: moving the pointer from an element into one of its children fires neither the element'sdj-mouseleavenor a seconddj-mouseenter(entering a child is not leaving the parent — the reason these event types exist overmouseover/mouseout). Bind-pass safety follows the house rules: the #2845 skip-on-unchanged-value / evict-and-rebuild rule prevents double-attach across morphs, the #2832 sweep detaches listeners when the template stops declaring the attribute, and the handler closure re-reads the attribute at fire time (#2858) so a value change under a surviving element is honoured.dj-debounce/dj-throttle,dj-confirm,data-*params, inline handler args, and{% live_render %}embedded routing (view_idfrom the stamp) all work as on sibling directives. Documented indocs/website/core-concepts/events.md("Mouse Enter / Leave") and the template cheatsheet quick-reference card.- Theming: the domain model is now importable, and the request-scoped helpers exist:
from djust.theming import ThemePackpreviously raisedImportError. The types (ThemePack,DesignSystem,SurfaceTreatment,TypographyStyle,LayoutStyle,SurfaceStyle,IconStyle,AnimationStyle,InteractionStyle,PatternStyle,IllustrationStyle) were defined intheming/_types.py— a private module holding public types, with its own__all__advertising them — but were never re-exported fromdjust.theming, andregister_theme_packaccepts aThemePack, so the documented model was unusable end to end. The theming documentation also instructsfrom djust.theming import get_active_pack, set_active_pack, get_active_mode, set_active_mode, reset_to_defaults, get_theme_css_url, none of which existed. Those six are now implemented intheming/api.pyas thin, request-scoped wrappers overThemeManager— theme state is per-request (session-backed, with a cookie fallback for anonymous users), so every one takesrequest, andget_active_modereturns the resolved mode so a caller on'system'learns which one they are actually seeing. Supporting additions:ThemeManager.set_pack()(the missing sibling ofset_theme/set_preset/set_mode) andThemeManager.reset(), which removes the session entry rather than writing defaults into it so a later change to configured defaults still applies.
Fixed
LiveViewTestClient.mount()never took the WebSocket branch, so WS-only setup code gated onhasattr(self, "_websocket_session_id")— the pattern djust's own#1612presence guard documents — had zero coverage under a green test suite (#2821).mount()now stamps the same identity attributes the real WS/SSE mount path sets on a live connection (_websocket_session_id,_websocket_path,_websocket_query_string,_djust_mount_view_path, mirroringViewRuntime.dispatch_mount) by default, making the documented "simulates the WebSocket mount process" claim true. Passmount(via_websocket=False)to instead exercise the HTTP-prerender branch; the branch taken is recorded asclient.via_websocketso a test can assert it explicitly rather than the branch being implied. Because that branch is now the default, a view'smount()-timetrack_presence()really runs under the client — which exposed a degenerate identity: with the defaultpresence_unique_per_connection = Falsethe identity isf"anon_{session_key}", and the client's session is deliberately unsaved, so every client collapsed toanon_Noneand two clients counted as ONE presence. A missingsession_keyno longer produces a degenerate identity. 8 regression cases inpython/djust/tests/test_testclient_websocket_fidelity_2821.py.- Tick path now honors
_skip_render:_tick_oncepreviously called_snapshot_assigns()unconditionally before AND after everyhandle_tick(), even when the handler explicitly set_skip_render = True(e.g. a non-host session's tick that early-returns). The tick path now checks_skip_renderright afterhandle_tick()runs — mirroring the event paths (server_push,db_notify,runtime.dispatch_event) — and skips both the render AND the second (expensive)_snapshot_assigns()/deep_fingerprintcall entirely when set._force_full_html(#1981) still wins over_skip_render, so an explicit forced re-render is never silently dropped. Closes #2822. LiveViewTestClient.send_event()returned a{"success": False, ...}envelope instead of raising when no handler existed for the event name, so a typo'd or renamed handler could pass silently in a suite that doesn't inspect every return value (#2823).send_event()now raisesdjust.testing.NoHandlerFoundErrorby default — matching the production WebSocket consumer, where a missing handler is an error frame, not a silent no-op. Passsend_event(name, raise_on_missing=False)to opt back into the old envelope-return behavior, e.g. for a test that deliberately probes the error shape. The sibling param-validation-failure branch is unchanged. 4 regression cases inpython/djust/tests/test_testclient_send_event_raises_2823.py. Existing coverage inTestLiveViewTestClient(the testing-utils suite) was updated for the new default, with new cases covering the escape hatch.- V008's return-annotation escape hatch never fired for methods or for dotted calls, so the documented non-
noqaremedy — annotate the helper-> str— silently did nothing (#2825). The collector only inspected module-level functions and stored bare names, while every attribute call resolves to a dotted name; together the two gaps covered both shapes a realmount()call site takes. Methods of module-level classes are now collected and the comparison uses the call's final name segment, so a same-module helper or method annotated-> str(or any primitive) settles the check without# noqaor suppression. Unannotated helpers still report; annotations on helpers imported from other modules are still unresolved, and the check's hint now says so. 7 regression cases inpython/tests/test_checks_v008_escape_hatch_2825.py. - T018 /
manage.py djust_typecheckfalse-positived on every template reference to a framework mixin's injected state —{{ form_data }}/{{ field_errors }}on anyFormMixin-based view reported as resolving to nothing (#2827). The static context extraction deliberately skipsdjust.*modules when AST-walking the MRO, so the mixin's runtimeself.form_data = ...assignments were invisible to it. Framework mixins can now declare their injected template-visible keys in a_djust_injects_contextclass manifest that the extractor reads without needing to AST-walk framework source;FormMixinships the manifest, and an anti-drift test pins it to cover every publicself.x = ...assignment in the mixin so future additions cannot silently go blind again. New cases inpython/djust/tests/test_djust_typecheck.pycover the manifest read, the end-to-end_check_viewpath, and the pin. - A server-initiated (tick/async) patch arriving while a user event was in flight forced a spurious full-HTML recovery morph (#2829). The client buffers such a frame while events are pending, and never applied its
version, so the next event response failed the strictclientVdomVersion !== data.version - 1check and loggedVDOM version mismatch! Expected vN, got vN+2— the recovery storm #1677 fixed for thepush_to_viewself-broadcast path only; the tick and async paths never went throughserver_push. The version is now consumed when the frame is buffered, but ONLY when it is contiguous with the cursor (it has arrived and will be applied), and the deferred replay is marked so it can neither move the cursor backwards nor trigger a recovery. Contiguity is the guard that keeps detection intact: consuming a NON-contiguous version would vouch for versions the server allocated and never shipped — the drop class_hotreload_broadcast_suppressedexists for — leaving the client permanently diverged with recovery never firing. A dropped patch, whether outside the buffering window or INSIDE it, still forces recovery — and the decision to decline a version survives to the flush, because a servernoopcarries no version of its own and would otherwise close the window with no strict check at all, letting the replay vouch for the gap. The client-owned flags are stripped by a shared helper each transport calls at its inbound entry (WebSocket, SSE, HTTP fallback), so a wire-supplied flag cannot suppress detection on any of them. 7 regression cases intests/js/tick_buffer_version_desync_2829.test.js, plus a wiring case in the SSE suite (LiveViewSSE). _run_async_workrendered without the consumer's_render_lock(#2830). Three of the four render paths —server_push,db_notifyand_tick_once— acquire it; the async-work path did not, while calling the same render helper whose docstring states that "the caller MUST already holdself._render_lock". That let a background result re-render concurrently with an event-path render on the same PyO3 view, whose VDOM baseline is not thread-safe. It is also the server-side enabler of the client-side version desync fixed in #2829: a lock-free async render is what can put a server-initiated frame in flight mid-event-turn. BOTH render arms of the async path now serialise on the same lock — the success arm and the error arm, which re-renders to display the error state and was missed by the first version of this fix — and unlike the sibling paths they WAIT rather than taking a bounded wait and skipping — a skipped async render is a result the client never receives, whereas a delayed one still lands. 2 regression cases inpython/djust/tests/test_async_work_render_lock_2830.py.- Dotted
dj-keydown.enter/.escape/.space(anddj-keyup.*) never fired (#2831). A dot is a legal attribute-name character, sodj-keydown.enteris ONE literal attribute thatclosest('[dj-keydown]')cannot match andgetAttribute('dj-keydown')cannot read — meaning the modifier-parsing block below it was dead code by construction, and the directive was silently inert. The framework documents the form as legitimate (_warnUnrecognizedDjModifiersdeliberately does not warn about it) and it is now discovered by attribute NAME, the same prefix-matching scan that warning function uses. The required key comes from the attribute name, mapped through the module's own_normalizeKeyName, so the whole key set works instead of a three-entry allowlist — including the bare direction words:docs/website/guides/tutorials.mddocumentsdj-keydown.right, which fell through the name map to the raw string whilee.keyisArrowRight, so that documented example never fired either. - Keyboard delegation now dispatches every matching binding, not just the nearest one (#2831). Three defects fell out of the old single-
closest()dispatch: an element carrying several dotted bindings (dj-keydown.enter+dj-keydown.escape, documented as a pair incore-concepts/events.md,core-concepts/templates.md,guides/template-cheatsheet.mdandai/templates.md) had only the first reachable; a binding whose key did not match returned early and swallowed the event before a container-level handler could see it; and the per-element rate-limit cache was keyed by element alone, so a morph swappingdj-keydown.enterfordj-keydownon a surviving node kept dispatching the stale binding forever. That same single slot also rebuilt the wrapper on every keystroke for an element carrying two bindings, resetting the timer insidedebounce()— sodj-debouncefired N events for N keystrokes — and, fordj-debounce="blur", leaking oneblurlistener per keystroke, since the deferred form attaches its listener when the wrapper is created andcancel()clears timer state only. The cache is now keyed by (element, matched attribute): one persistent wrapper per binding. - Behaviour change: two bindings for the same directive on one path both fire (#2831). With the old nearest-
closest()lookup a descendant carryingdj-keydownshadowed an ancestor's, so<div dj-keydown="outer"><input dj-keydown="inner">fired onlyinner; it now fires both. This matches the scopeddj-window-keydown/dj-document-keydowndelegation, which already dispatches every matching registry entry. No documentation pins the nesting semantics either way, so it is called out rather than changed silently.dj-keyis deliberately NOT read as a key: it is the VNode list-identity attribute (heading "dj-key/data-key— Stable List Identity" indocs/website/advanced/vdom-architecture.md; the gloss "Analogous to Reactkey" is indocs/website/guides/template-cheatsheet.md— two different files), and consulting it silenced handlers on keyed list rows. 15 cases intests/js/keydown_dotted_modifier_2831.test.js. - A scoped event listener (
dj-shortcut,dj-click-away) no longer keeps firing after the template stops declaring the directive on an element that survives the VDOM patch (#2832). The invariant — a scoped listener is evicted when its element detaches, when the server drops the declaring attribute, or when no LiveView root governs the element anymore — was implemented twice, once per scoped-listener path, and the #2108 fix landed on only one of the copies (the same parallel-path drift as #1646/#2110). Both paths now evict through one shared predicate, eviction is per declaring attribute (an element may carry several scoped directives and lose only one), and eviction clears the bound-handler marker so a re-declared attribute re-binds with the current value instead of serving the old closure. - T018's
{% extends %}skip was invisible:manage.py check/djust_checkreported "All djust checks passed!" for an app whose extends-based templates the check never examined — indistinguishable from a run that examined everything and found nothing (#2833). The skip itself is unchanged (the documented v1 trade-off: block-override context is inheritance-scoped), but a run that skipped one or more views now emits one Info-leveldjust.T018message with the skipped-view count, so a pass is falsifiable.docs/system-checks.mdalso no longer claims the two entry points "can never drift apart" — extraction is shared, but coverage deliberately differs: T018 additionally covers inlinetemplate = "..."views and skips extends templates, whilemanage.py djust_typecheckcovers extendstemplate_nametemplates but not inline ones. 2 regression cases inpython/tests/test_checks_t018_extends_skip_info_2833.py. set_changed_keys()can no longer be silently dropped by_skip_renderon any render path: when a handler set_skip_render = Truealongsideset_changed_keys()(the_force_full_htmlhatch, #1981), most render paths resolved the contradiction as "skip" — silently dropping the explicitly requested render and leaking the flag into a later unrelated turn (the #1646 silently-dropped-hatch class) — while the tick path rendered. All render paths (events, broadcasts, DB notifies, ticks) now resolve the two flags through one shared decision where an explicit forced render always wins and both flags are consumed on the turn that serves them. A handler setting only_skip_renderis unaffected. Closes #2834.check-changelog-test-countsno longer counts prose as JS tests (#2839). The JS test-function regex was "deliberately loose" — any whitespace/;/{-precededit/testbefore(counted — so a comment sentence like "…block below it (requiredKey)…" was counted as a test and a correct changelog claim was rejected as drift (the workaround was rewording the prose, which is backwards, #2238); a stray mention could equally inflate a count so a genuinely wrong claim passed. The regex now matches test declarations in statement position (line start, any indentation) and also counts the previously-missedit.each/test.eachdeclarations (one per declaration, mirroring the Python side's parametrize-as-one convention). Validated against all 178 files intests/js/with per-file ground truth fromvitest run: the old regex was wrong on 10 files (8 prose phantom matches across 6 files, 4 missedit.eachdeclarations across 3 files — two files wrong in both ways); the new count is correct everywhere the count is statically knowable. 4 new cases intests/test_changelog_test_counts.pycover the issue's exact prose example, the false-negative direction, the real-corpus comment shapes, andit.eachcounting.handle_async_resultnow runs under the render lock on both arms of the async-work paths (#2840). A background-work handler that mutates view state (the documentedself.result/self.errorpattern) could previously interleave with a concurrent lock-holding render of the same view.LiveViewConsumer._run_async_workawaits the handler inside its existing_render_lockregion (success + error arms; the error arm's handler → identity re-check → re-render ordering is preserved), and the runtime twinViewRuntime._execute_async_task— which serves WS events'start_asyncwork after the ADR-022 flip — borrows the consumer's lock viatransport.event_context()for its handler + render and gained the missing #1940 teardown identity-guards on both arms. 6 regression cases inpython/djust/tests/test_async_result_render_lock_2840.py.- Embedded-view stamping missed dotted event attributes (
dj-keydown.enterwas never stamped, #2841).{% live_render %}stampsdata-djust-embeddedon every event-bearing element so the client can route the element's events to the embedded child view, but the compiled matcher required=immediately after the bare attribute name — and a dot is a legal attribute-name character (#2831,#1999), sodj-keydown.enter="go"is ONE literal attribute that could not match. An element whose ONLY event attribute was dotted got no stamp, and its events routed to the parent instead of the embedded view; a bare event attribute elsewhere on the same element masked the failure. The matcher now accepts dotted in-name modifiers (dj-keydown.enter, the multi-dotdj-keydown.enter.shiftthe runtime reads as.enter, and whitespace before=per the HTML tokenizer). The scoped family (dj-window-keydown/dj-document-*for keydown, keyup, click, scroll, resize) was missing from the attribute list entirely — missed dotted AND bare — and is now stamped too; its dispatch path consumes the sameview_idfromaddEventContext. 8 cases inTestDottedKeyboardEventStamping(tests/unit/test_live_render_tag.py). - Unknown dotted keyboard modifiers are no longer silently inert — debug warning added (#2842).
dj-keydown.f1="go"ordj-keydown.esc="go"never fired and never logged anything, even in DEBUG:_normalizeKeyNamemaps a fixed set of names and falls back to the RAW name, so.f1resolved to"f1"whilee.keyis"F1". The raw fallback stays (it is what lets single characters like.afire), but_warnUnrecognizedDjModifiers— the #1999 debug-only warning channel, zero production cost — now also flags any multi-character key modifier ondj-keydown/dj-keyup/dj-window-keydown.*/dj-document-keydown.*that is not in the map, naming the attribute and the recognized names. Warning is once per bind, never per keystroke. Notably, HTML parsers lowercase attribute names, sodj-keydown.PageUparrives as.pageupand cannot match"PageUp"either — casing cannot rescue a multi-character suffix, and.F1/.PageUp-style spellings warn like every other inert name. The map moved from a local of_normalizeKeyNameto a module-level_KEY_NAME_MAPso the warning pass reads the same source of truth. 11 cases intests/js/keydown_key_warning_2842.test.js. - A stale handler closure kept serving
dj-shortcut/dj-click-awayafter a re-render changed the attribute VALUE on an element that survives the VDOM patch (#2845). The bind loops skipped already-marked elements on the_isHandlerBoundmarker alone, and the #2832 eviction predicate judges attribute PRESENCE — so a present-but-changed attribute kept dispatching the old handler name (and, fordj-shortcut, the old key bindings and the staledj-shortcut-in-inputgate). The skip is now keyed on the value the listener was built from: an unchanged value skips exactly as before, a changed value evicts the old listener and rebuilds the closure from the new value — the sweep path's counterpart of the #2108 registry refresh. 5 regression cases intests/js/stale-scoped-listeners-2845.test.js. - Both deferred-activity re-dispatch paths resolved
_skip_rendervs_force_full_htmlthe pre-#2834 way (#2847). With both flags set by the same handler,ViewRuntime._dispatch_single_eventand the WS consumer's_dispatch_single_eventsent anoop— silently dropping the forced full-HTML render and leaking_force_full_htmlinto a later, unrelated turn. Both now resolve through the shared_resolve_skip_renderhelper (force wins;_skip_renderconsumed whenever set), and the helper's docstring no longer falsely claims to own only the four paths #2834 named. New cases inTestDispatchSingleEventParity2847(python/djust/tests/test_skip_render_force_parity_2834.py). - Release gates: a tagged release can no longer ship without its CHANGELOG section, and fragment path/class claims are now checked (#2854, #2849). v1.1.3 shipped to PyPI with no
## [1.1.3]section while the shipped-section pin reported OK against v1.1.2 — the pin's anchor silently fell back to the newest sectioned tag.scripts/check-changelog-tagged-sections.pynow fails for every release tag above that anchor reachable from HEAD whose version has no working-tree section, andmake releaserefuses to tag whenCHANGELOG.mdhas no section for the target version (the pre-commit hook cannot see this: at the version-bump commit the tag does not exist yet). Separately, a new fragment reference check resolves backtick-quoted file paths and test-class names in pending fragments against the tree, closing the same #2652-shaped gap for fragments; count claims were already covered byscripts/check-changelog-test-counts.py. - Three remaining stale-closure sites of the #2845 class now rebuild when the declaring attribute changes on a surviving element (#2858).
dj-poll(09-event-binding.js),_bindModel(20-model-binding.js) andbindUploadHandlers(15-uploads.js) skipped already-bound elements on the bind marker alone, so a morphdom-surviving element kept dispatching the old closure — the old poll handler and cadence, the old model field, the old upload slot. Each site now keys the skip on the value the closure was built from: an unchanged value skips exactly as before (fordj-poll, an unchanged value/interval pair does NOT restart the poll phase — the bind loop runs on every patch and a restart would reset the interval timer), a changed value evicts the old listeners and rebuilds, and the poll phase reads itsdata-*params at fire time likedj-click/dj-changedo. 10 regression cases intests/js/stale-closures-2858.test.js. dj-shortcutkey names that can never match now warn in debug mode (#2859). The comma syntax (pageup:handler) resolves through the same_normalizeKeyNamehelper as the dotted keyboard directives, and an all-lowercase multi-character name (pageup) can never equal a KeyboardEvent.key — the binding was dead on arrival with no warning. Attribute VALUES keep their casing, so the correctly-cased raw spelling (PageUp:handler) fires and is deliberately not warned about; each distinct dead name warns at most once per bind pass.dj-document-scroll/dj-document-resizenow warn in debug mode (#2859). Both were recognised by_scanScopedElementsand their entries registered, but the document-level listener was deliberately never installed (resizenever fires ondocument), so the attributes parsed and did nothing — the same silently-inert class as #2842. The warning points at the documenteddj-window-scroll/dj-window-resizetwins.- Deleting an already-shipped
CHANGELOG.mdsection no longer passes the shipped-section pin silently (#2862). The #2028 pin iterated only the sections present in the working tree, so a shipped section deleted from the tree was never compared — removing the## [1.2.0rc6]section exited 0 withOK: 133 shipped CHANGELOG section(s) match the newest release tag. The pin now iterates the union of the working tree's sections at or below the anchor and the anchor tag's frozen snapshot, so deletion and rewrite are two symptoms of one comparison; a deleted section fails by name with a restore instruction, a distinct message from the rewrite diff because the operator's next action differs. The multi-branch scoping from #2861 is preserved — the new demand reads the anchor's snapshot, never the tag list, so a maintenance branch is not failed for main's sections. Covered by synthetic tests inTestDeletedShippedSectionand a real-tree canary intests/test_changelog_tagged_sections.py. - A full
CHANGELOG.mdwipe passed the shipped-section gate silently (#2865).scripts/check-changelog-tagged-sections.pyanchored on the newest tagged section and failed OPEN when it could not select one: deleting one shipped section was caught (#2854 absence, #2862 deletion-below-anchor), but deleting EVERY tagged section — the realistic tail of a cross-branchCHANGELOG.mdmerge resolved toward a branch without the shipped history (the v1.1.0rc5 consolidation class) — left the check with no anchor and exited 0. The no-anchor state is now disambiguated by the release tags reachable fromHEAD, enumerated by the #2861 walk (kept--merged HEAD, extracted into one shared helper — no third tag call): with no release tag reachable, the fresh / pre-first-release pass stands; with some, every one of them lacks its section and the check fails naming the newest tag and its restore source (git show v<newest>:CHANGELOG.md). New cases inTestWipedTaggedSectionsintests/test_changelog_tagged_sections.py. dj-keypress,dj-viewport-enter, anddj-viewport-leaveremoved from the{% live_render %}stamp list (#2869). They sat in_LIVE_RENDER_EVENT_ATTRS— the list{% live_render %}scans when stampingview_idonto embedded elements — but no client module ever bound them: zero mentions instatic/djust/src/, zero occurrences in the built client. An element carrying one of them inside an embedded child was stamped exactly like a working directive while no listener was ever installed — no error, no warning, no event.dj-keypressis also deprecated in the DOM in favour ofkeydown(which djust ships with a full modifier system); there is noviewportenterDOM event for the viewport pair to bind at all.dj-keypress,dj-viewport-enter, anddj-viewport-leavewere all introduced in the same v0.6.0 stamp-list commit (e9907c7f) — the invariant test below caught the viewport pair on its first run. A new invariant test — new cases intests/unit/test_live_render_event_attrs_invariant.py— fails whenever a stamp-list entry has no client-side binding: each entry must appear as a quoted string literal in somestatic/djust/src/module, or belong to thedj-window-/dj-document-scoped cross product derived from the client's ownscopedPrefixes×scopedEventTypesarrays. Its blind spots (a quoted mention inside a comment, a dead string reference that never dispatches, dynamic names outside the scoped path) are documented in the module docstring.- Template pipeline performance: Reduce template rendering and reactive update overhead by reusing exact immutable temporal values after protection checks, memoizing filesystem include selection per render, streaming VDOM serialization into one buffer, and reducing lazy tag-binding conversion overhead. Preserve live subclass and custom-timezone behavior, template reloads between renders, and escaped hydrated HTML output.
- Avoid repeated
dir()scans for numeric lookups on exact built-in sequences during template rendering, while preserving Django's lookup behavior for custom objects and subclasses.
Security
- HTTP POST fallback (
RequestMixin.post()) enforced none of djust's three authorization layers, so an unauthenticated or under-privileged caller could drive@event_handlermethods on alogin_required = Trueview with a plain POST.get()and every WS/SSE event path already enforced view-levelcheck_view_auth, handler-levelcheck_handler_permission, and the ADR-017 object-level check; the POST transport now runs all three before dispatch, with the same denial shapes (403{"redirect": <login_url>}for the unauthenticated case, 403{"error": "Permission denied"}for view/handler permission denials, 403{"error": "Access denied for this object."}for object-level denials). Anonymous POSTs to views without auth requirements are unchanged. Also fixes anUnboundLocalErrorinpost()'s own error path: a body that was not valid JSON raised from theexcepthandler itself, masking the real exception as an unlogged 500. 7 regression cases inpython/tests/test_http_post_authz.py.