Added
Resumable uploads across WebSocket disconnects (v0.5.7 — closes #821) — Long mobile uploads now survive network hiccups, backgrounded tabs, and brief WS drops. New
djust.uploads.resumable.ResumableUploadWriterwraps any existingUploadWriter(S3 MPU, GCS, Azure, tempfile) and persists chunk-level state into a pluggableUploadStateStore. Two stores ship in core:InMemoryUploadState(default, single-process) andRedisUploadState(requiresdjust[redis], multi-process / multi-host). New WS message{"type":"upload_resume","ref":X}returns{"type":"upload_resumed","status":"resumed|not_found|locked","bytes_received":N,"chunks_received":[...]}. New HTTP status endpointGET /djust/uploads/<upload_id>/status(session-scoped, cross-user probes blocked). Client-side IndexedDB cache in15-uploads.jslets tabs resume uploads after reload if the file reference can be re-selected. State is capped at 16 KB per upload_id (run-length-compressed chunk ranges) with 24-hour default TTL. Opt-in per slot:allow_upload("video", writer=S3Resumable, resumable=True). ~1,050 LOC net acrosspython/djust/uploads/(__init__.pymodified,resumable.py,storage.py,views.pyadded),python/djust/websocket.py,python/djust/static/djust/src/15-uploads.js(+03-websocket.jsdispatch), full wire-protocol spec + failure-mode + security analysis indocs/adr/010-resumable-uploads.md. 44 unit tests inpython/djust/tests/test_resumable_uploads_821.py(compaction, in-memory + fake-Redis roundtrip, writer lifecycle, resume resolution, TTL expiry via mock clock, concurrent-resume rejection, HTTP status view) plus 2 async WS handler cases in the same file, plus 9 JSDOM cases intests/js/upload_resume.test.js(file-hint fingerprint, UUID round-trip, IDB shim roundtrip, cleanup on complete).Upload writers — S3 pre-signed PUT URLs + first-class GCS/Azure backends (v0.5.7 — closes #820, #822) — New
djust.contrib.uploads.s3_presignedmodule lets clients upload directly to S3 via a pre-signed URL; djust only signs and observes completion via S3 event webhook. Newdjust.contrib.uploads.gcs.GCSMultipartWriteranddjust.contrib.uploads.azure.AzureBlockBlobWritership as first-classUploadWritersubclasses with consistent error taxonomy (UploadError,UploadNetworkError,UploadCredentialError,UploadQuotaError, re-exported fromdjust.uploads). Client-sidedjust.uploads.uploadPresigned(spec, file, hooks)streams bytes straight to object storage via XHR (progress viaxhr.upload.onprogress), bypassing the WS upload machinery. Optional extras:djust[s3],djust[gcs],djust[azure]. ~650 LOC + 50 regression tests (mocked SDKs) acrosspython/djust/tests/test_presigned_s3_820.py,python/djust/tests/test_gcs_upload_writer_822.py,python/djust/tests/test_azure_upload_writer_822.py. Seedocs/website/guides/uploads.md.Docs cleanup: 4 issues closed — dj-remove no-CSS-transition gotcha (#902), dj-transition-group long-form precedence (#907), Django 5.1 + 5.2 classifiers in
pyproject.toml(#912), new guide page fordj-virtualvariable-height mode atdocs/website/guides/virtual-lists.md(#952).dj-virtual variable-height items via ResizeObserver — closes #797 — PR #796 shipped
dj-virtualwith fixed-height items only. This adds opt-in variable-height support via a newdj-virtual-variable-heightboolean attribute. Implementation: ResizeObserver per rendered item feeds aMap<index, number>height cache; a lazily-computed prefix-sum array drives offset math and the virtual spacer total. Unmeasured items fall back to a configurabledj-virtual-estimated-height(default 50px). Fixed-height mode (dj-virtual-item-height="N") is unchanged — tested explicitly as a regression guard. Updated29-virtual-list.js(~180 LOC net) and 4 new JSDOM cases intests/js/virtual_list.test.jscovering attribute activation, mixed-height prefix-sum math, RO-driven cache updates, and fixed-mode regression.Tooling: CHANGELOG test-count validator — closes #908 — new
scripts/check-changelog-test-counts.pyparses phrases likeN JSDOM cases,N regression tests,N unit tests,N test cases,N parameterized casesin the[Unreleased]section, resolves every backtickedtests/js/*.test.js/python/djust/tests/*.py/tests/unit/*.pypath inside the same bullet, counts test functions in each, and fails if the claim doesn't match reality. Delta phrases (2 new cases,3 additional tests) are deliberately skipped — they can't be verified without git history. Wired into.pre-commit-config.yamlas a local hook scoped to^CHANGELOG\.md$and exposed asmake check-changelog. Self-tested by 7 cases intests/test_changelog_test_counts.pycovering match/mismatch, JSDOM-vs-py file resolution, multi-file summing, delta ignore, and missing-section tolerance.Tooling: CodeQL triage script — closes #916 —
scripts/codeql-triage.sh [rule-id]paginates/repos/{owner}/{repo}/code-scanning/alerts?state=openviagh apiand emits a markdown triage doc grouped byrule.id, sorted within each group by file/line. Optional positional arg filters to a single rule for focused triage sessions. Turns the raw alert dump (noisy JSON) into something reviewable in a PR comment or a doc. Documented inscripts/README.md.Tooling: CodeQL sanitizer MaD model — closes #934 — new extension pack at
.github/codeql/models/(qlpack.yml +djust-sanitizers.model.yml) teaches CodeQL thatdjust._log_utils.sanitize_for_log()is a log-injection sanitizer. Referenced from.github/codeql/codeql-config.ymlvia a newpacks:section. Closes the class of false-positivepy/log-injectionalerts we've been dismissing individually. Verification lands with the next main-branch CodeQL scan. See.github/codeql/README.mdfor the tuple shape, fallback plan (hand-writtenLogInjectionFlowConfigurationoverride), and links to CodeQL's data-extensions docs.ADR-009: Mixin side-effect replay on WebSocket state restoration — closes #897 — formalizes the
_restore_<concept>()pattern first shipped ad-hoc in PRs #891 (UploadMixin, #889) and #895 (PresenceMixin- NotificationMixin, #893 / #894). Codifies the serialization contract (JSON-only saved attrs), error handling (WARNING-level wrap, never kill the WS), convergence/idempotency requirement, naming convention (
_restore_<concept>), and call ordering inLiveViewConsumer. Documents the rejected alternatives: don't-skip-mount (perf cost), snapshot-entire-managers (serialization complexity), pickle-to-session (security + format stability). New file:docs/adr/009-mixin-side-effect-replay.md.
- NotificationMixin, #893 / #894). Codifies the serialization contract (JSON-only saved attrs), error handling (WARNING-level wrap, never kill the WS), convergence/idempotency requirement, naming convention (
Fixed
Framework cleanup (closes #762, #890) — djust.A010 / A011 system checks now recognize proxy-trusted deployments: when
SECURE_PROXY_SSL_HEADER+DJUST_TRUSTED_PROXIESare both set,ALLOWED_HOSTS=['*']is accepted (supports AWS ALB, Cloudflare, Fly.io, and other L7 load balancers where task private IPs rotate). Also filters ~25 framework-internal attrs (sync_safe,login_required,template_name,http_method_names,on_mount_count,page_meta, etc.) fromLiveView.get_state(), the WS_snapshot_assignschange-detection path, and the_debug.state_sizesobservability payload — user's reactive state is no longer swamped by framework config. Non-breaking fix via a newlive_view._FRAMEWORK_INTERNAL_ATTRSfrozenset; attribute names unchanged. 14 new regression tests inpython/djust/tests/test_a010_proxy_trusted_890.pyandpython/djust/tests/test_get_state_filter_762.py. Deployment guide updated with the proxy-trusted escape-hatch pattern.JS-centric batch (closes #949, #951, #953) — tag_input hidden-input payload now JSON-encoded instead of comma-separated, so tag values containing commas round-trip intact (#949). dj-virtual variable-height cache now keyed by
data-keyattribute (configurable viadj-virtual-key-attr), falling back to index when absent — cached heights survive item reorders (#951). Consolidated JSDOM test helpers attests/js/_helpers.js(createDom,nextFrame,fireDomContentLoaded,makeMessageEvent,mountAndWait) and refactored 3 test files to use them (#953). 2 new Python regression tests (commas + quotes round-trip) and 3 new JSDOM cases (reorder survival, index fallback, custom key attribute). Guardrail added toscripts/build-client.shto fail fast iftests/js/_helpers.jsever leaks into the production bundle.Hygiene batch (closes #791, #794, #795, #818, #948) — bumped
ruff-pre-commitfrom v0.8.4 to v0.15.11 (#948) and appliedruff formatto all resulting drift (#791 — expanded beyond the original 5 files due to modern-ruff disagreements; 19 files total acrosspython/djust/andtests/). Addedlogger.debugnotice incomponents/suspense.pywhen{% dj_suspense await=X %}receives a non-AsyncResult value so a typo surfaces during development (#794), simplified a redundantor not value.okcheck nearsuspense.py:138given the AsyncResult mutually-exclusive-flag invariant (#795), wrapped the namespaceddata-hookattribute value withdjango.utils.html.escape()for defense-in-depth intemplatetags/live_tags.py(#818), and corrected stale test-count claims in two historical CHANGELOG bullets (test_assign_async.py11 → 18,test_suspense.py11 → 12) flagged by the #795 reviewer. No behavior change.Security + cleanup: pre-existing test failures, redirect audit, dep ceilings, edge tests — closes #910, #921, #922, #935 — #935: fixed 3 stale test assertions that were checking for leaked exception-class names in API error responses. The implementations in
api/dispatch.py,observability/views.pydeliberately sanitize error payloads (don't echoRuntimeError/ internal method names to clients; send to server logs instead). Tests now verify the sanitized contract ("server logs"inerror, handler_name / session_id echo) rather than the leaked details. Fixestest_api_response.py::test_dispatch_serialize_str_missing_method_returns_500,test_observability_eval_handler.py::test_eval_500_when_handler_raises, andtest_observability_reset_view.py::test_reset_500_when_mount_raises. #921: expanded open-redirect audit beyond PR #920 —mixins/request.pynow validateshook_redirectreturned by developer-definedon_mounthooks viaurl_has_allowed_host_and_scheme, falling back to"/"and logging a WARNING on unsafe targets.auth/mixins.pyLoginRequiredLiveViewMixin.dispatchnow validates the computed login URL as defense-in-depth against misconfiguredsettings.LOGIN_URL, falling back to"/accounts/login/". #922: 7 new regression tests inpython/djust/tests/test_security_redirects_paths.py—javascript:scheme rejection, HTTPS-to-HTTP downgrade, null-byte path-injection, uppercase/case-sensitive allowlist, hook_redirect off-site rejection, hook_redirect same-site acceptance, and off-siteLOGIN_URLfallback. #910: added upper-bound ceilings to all runtime + dev dependencies inpyproject.toml(e.g.requests>=2.28,<3,orjson>=3.11.6,<4,nh3>=0.2,<1). Prevents uncontrolled major bumps duringuv lockrefresh (see PR #909 which caught Django 6.x resolving under>=4.2). Ceiling policy documented in a comment above[project.dependencies]. Verified withuv lock— only material change isredis7.3 -> 6.4 (stays under new<7ceiling).UploadMixin defensive replay for schema-changed configs — closes #892 —
_restore_upload_configsnow wraps each per-slotallow_upload(**cfg)in try/exceptTypeError. On signature mismatch (kwarg added / renamed / removed between djust versions), logs a WARNING identifying the slot- the mismatched kwarg, then falls back to
allow_upload(slot_name)— bare-minimum replay — so uploads for that slot still work with default config. One broken saved dict no longer kills replay for every other slot on the page. Each saved dict is now tagged with_upload_configs_version = 1for future explicit migrations. Regression tests intests/unit/test_mixin_replay_schema_cross_loop_892_896.py.
- the mismatched kwarg, then falls back to
NotificationMixin cross-loop restore — closes #896 —
_restore_listen_channelsnow detects when thePostgresNotifyListenersingleton is stranded on a closed event loop (server restart with fresh ASGI loop, test harness per-test loops, sticky-session LB cross-worker handoff) and calls a newPostgresNotifyListener.reset_for_new_loop()classmethod to drop the singleton before replay. The pre-check inspectslistener._loop.is_closed(); a per-channelexcept RuntimeErrorbranch handles the race where the loop closes between the pre-check and theensure_listeningcall (resets and retries once). Prevents silent NOTIFY drops on cross-loop restore. Regression tests intests/unit/test_mixin_replay_schema_cross_loop_892_896.py.Observer JS — closes #879, #880, #881, #882 — #879:
37-dj-mutation.jsand38-dj-sticky-scroll.jsdocument-level root observers now detect attribute REMOVAL on already-observed elements (viaattributes: true+attributeFilter: ['dj-mutation']/['dj-sticky-scroll']) and call the module's teardown helper. Previously removing the attribute from an element left a staleMutationObserver+ scroll listener attached. #880: documented theMap-vs-WeakMapchoice in39-dj-track-static.js— the reconnect-diff iterates all tracked elements to compare snapshot URLs, andWeakMapdoes not support iteration; theisConnectedcheck in_checkStalehandles detached elements. #881: documented unconditional scroll-to-bottom on install in38-dj-sticky-scroll.js— matches Phoenix phx-auto-scroll / Ember scroll-into-view behavior (sticky-scroll is an "opt into bottom-pinning" attribute; authors want the initial view pinned to the most recent content: chat, log output). #882: regression test intests/js/dj_mutation.test.js— nodj-mutation-fireCustomEvent fires when the element is removed before the debounce timer expires (existing_tearDownDjMutationpath correctly clears the pending timer on removal).
Tests
- dj-transition-group follow-ups — closes #905, #906 — #905 The VDOM
RemoveChildintegration test intests/js/dj_transition_group.test.jswaited 700 ms per run for the default dj-remove fallback timer. Pinneddj-remove-duration="50"on the child and reduced the wait to ~80 ms, dropping this file's wallclock from ~1.2 s to ~600 ms. #906 Added a nested-group regression test — outer + inner[dj-transition-group]parents each install their own per-parent observer (subtree:false), so a new child appended toinnergets the inner group's enter/leave specs and is not clobbered by the outer's. Pins the subtree-scoping invariant relied on by the phase-2c implementation.
Fixed
Mechanical cleanup — closes #914, #915 — #914: dropped redundant
ch == " "clause in_log_utils.sanitize_for_log— ASCII space is already printable so the explicit check was dead. #915: bulk-appliedruff format(pinned pre-commit version 0.8.4) to 4 pre-drifted files (3 theming test files +uploads.py) to bring them to canonical form. No behavior change in either fix.3 latent bugs caught by prior CodeQL-cleanup audits — closes #930, #932, #933 — #930 FormArrayNode inner content:
{% form_array %}...{% endform_array %}parsed the block body into a nodelist viaparser.parse(("endform_array",))butFormArrayNode.rendernever rendered that nodelist — users' inner template markup silently disappeared. Fixed by rendering the nodelist once per row withrow,row_index, andforloop(dict shape:{counter, counter0, first, last}) pushed onto the template context; empty or whitespace-only blocks keep the original single-input-per-row default output, so existing users see no change. #932 tag_input missingname=attribute:TagInput._render_customrendered a visible "type to add"<input class="tag-input-field" placeholder="...">with noname=, so form submissions silently dropped the tag list from POST data. Fixed by emitting a<input type="hidden" name="<self.name>" value="<csv of tags>">alongside the visible input wheneverself.nameis non-empty; hidden value ishtml.escape'd. #933 gallery/registry.py dead discover_* path:discover_template_tags()anddiscover_component_classes()were public helpers exported fromdjust.components.gallery.__init__butget_gallery_data()never called them — a developer adding a new@register.tagorComponentsubclass without updating the curatedEXAMPLES/CLASS_EXAMPLESdicts had that new thing silently missing from the rendered gallery. Fixed by wiring both helpers intoget_gallery_data()as a cross-check: any registered tag / component class missing an example entry emits alogger.debugwarning naming the missing entries, and discovery failures are caught so the gallery never breaks at runtime. 14 regression tests acrosspython/djust/tests/test_form_array_930.py,python/djust/tests/test_tag_input_932.py,python/djust/tests/test_gallery_registry_933.py(7 of which fail on main pre-fix; 2 added later under #949 for commas-in-values round-trip). No behavior change for non-broken inputs. (python/djust/components/templatetags/djust_components.py,python/djust/components/components/tag_input.py,python/djust/components/gallery/registry.py)dj-remove follow-ups — closes #900, #901 — Extracted shared
_teardownState(el, state)helper in42-dj-remove.jsso_finalizeRemovaland_cancelRemovalno longer duplicate the clearTimeout + removeEventListener + observer.disconnect + _pendingRemovals.delete block (Stage 11 nit from PR #898). Added a debug warning (gated onglobalThis.djustDebug) when_parseRemoveSpecencounters a 2-token value likedj-remove="fade-out 300"— previously silent fall-through. 2 new JSDOM regression cases intests/js/dj_remove.test.js(12/12 passing).dj-transition edge cases — closes #886, #887, #888 — #886
_parseSpecin41-dj-transition.jsnow rejects comma, paren, and bracket separators up front (returnsnulland emits a debug warning gated onglobalThis.djustDebug) instead of lettingclassList.addthrowInvalidCharacterErrorat runtime — matches the dj-remove #901 loud-in-debug / silent-in-prod pattern. #887 Thecleanupcallback (bothtransitionendhandler and 600 ms fallback path) now guards withel.isConnected— if the element has been detached from the DOM before cleanup fires, we skip classList and listener work and just drop the_djTransitionStateentry. Prevents any futureparentNode.Xaccess from NPE'ing on a detached node. #888 Unskipped the two previously-flakytransitionendtests intests/js/dj_transition.test.jsby swapping timing-sensitivesetTimeout(..., 30)waits for synchronousel.dispatchEvent(new Event('transitionend'))— deterministic under vitest parallel load. Added one new test covering the #886 parser rejection path. All 9 dj-transition tests pass deterministically.