djust 0.5.7rc1

Pre-releaseReleased

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.ResumableUploadWriter wraps any existing UploadWriter (S3 MPU, GCS, Azure, tempfile) and persists chunk-level state into a pluggable UploadStateStore. Two stores ship in core: InMemoryUploadState (default, single-process) and RedisUploadState (requires djust[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 endpoint GET /djust/uploads/<upload_id>/status (session-scoped, cross-user probes blocked). Client-side IndexedDB cache in 15-uploads.js lets 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 across python/djust/uploads/ (__init__.py modified, resumable.py, storage.py, views.py added), python/djust/websocket.py, python/djust/static/djust/src/15-uploads.js (+ 03-websocket.js dispatch), full wire-protocol spec + failure-mode + security analysis in docs/adr/010-resumable-uploads.md. 44 unit tests in python/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 in tests/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_presigned module lets clients upload directly to S3 via a pre-signed URL; djust only signs and observes completion via S3 event webhook. New djust.contrib.uploads.gcs.GCSMultipartWriter and djust.contrib.uploads.azure.AzureBlockBlobWriter ship as first-class UploadWriter subclasses with consistent error taxonomy (UploadError, UploadNetworkError, UploadCredentialError, UploadQuotaError, re-exported from djust.uploads). Client-side djust.uploads.uploadPresigned(spec, file, hooks) streams bytes straight to object storage via XHR (progress via xhr.upload.onprogress), bypassing the WS upload machinery. Optional extras: djust[s3], djust[gcs], djust[azure]. ~650 LOC + 50 regression tests (mocked SDKs) across python/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. See docs/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 for dj-virtual variable-height mode at docs/website/guides/virtual-lists.md (#952).

  • dj-virtual variable-height items via ResizeObserver — closes #797 — PR #796 shipped dj-virtual with fixed-height items only. This adds opt-in variable-height support via a new dj-virtual-variable-height boolean attribute. Implementation: ResizeObserver per rendered item feeds a Map<index, number> height cache; a lazily-computed prefix-sum array drives offset math and the virtual spacer total. Unmeasured items fall back to a configurable dj-virtual-estimated-height (default 50px). Fixed-height mode (dj-virtual-item-height="N") is unchanged — tested explicitly as a regression guard. Updated 29-virtual-list.js (~180 LOC net) and 4 new JSDOM cases in tests/js/virtual_list.test.js covering 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.py parses phrases like N JSDOM cases, N regression tests, N unit tests, N test cases, N parameterized cases in the [Unreleased] section, resolves every backticked tests/js/*.test.js / python/djust/tests/*.py / tests/unit/*.py path 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.yaml as a local hook scoped to ^CHANGELOG\.md$ and exposed as make check-changelog. Self-tested by 7 cases in tests/test_changelog_test_counts.py covering match/mismatch, JSDOM-vs-py file resolution, multi-file summing, delta ignore, and missing-section tolerance.

  • Tooling: CodeQL triage script — closes #916scripts/codeql-triage.sh [rule-id] paginates /repos/{owner}/{repo}/code-scanning/alerts?state=open via gh api and emits a markdown triage doc grouped by rule.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 in scripts/README.md.

  • Tooling: CodeQL sanitizer MaD model — closes #934 — new extension pack at .github/codeql/models/ (qlpack.yml + djust-sanitizers.model.yml) teaches CodeQL that djust._log_utils.sanitize_for_log() is a log-injection sanitizer. Referenced from .github/codeql/codeql-config.yml via a new packs: section. Closes the class of false-positive py/log-injection alerts we've been dismissing individually. Verification lands with the next main-branch CodeQL scan. See .github/codeql/README.md for the tuple shape, fallback plan (hand-written LogInjectionFlowConfiguration override), 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 in LiveViewConsumer. 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.

Fixed

  • Framework cleanup (closes #762, #890) — djust.A010 / A011 system checks now recognize proxy-trusted deployments: when SECURE_PROXY_SSL_HEADER + DJUST_TRUSTED_PROXIES are 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.) from LiveView.get_state(), the WS _snapshot_assigns change-detection path, and the _debug.state_sizes observability payload — user's reactive state is no longer swamped by framework config. Non-breaking fix via a new live_view._FRAMEWORK_INTERNAL_ATTRS frozenset; attribute names unchanged. 14 new regression tests in python/djust/tests/test_a010_proxy_trusted_890.py and python/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-key attribute (configurable via dj-virtual-key-attr), falling back to index when absent — cached heights survive item reorders (#951). Consolidated JSDOM test helpers at tests/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 to scripts/build-client.sh to fail fast if tests/js/_helpers.js ever leaks into the production bundle.

  • Hygiene batch (closes #791, #794, #795, #818, #948) — bumped ruff-pre-commit from v0.8.4 to v0.15.11 (#948) and applied ruff format to all resulting drift (#791 — expanded beyond the original 5 files due to modern-ruff disagreements; 19 files total across python/djust/ and tests/). Added logger.debug notice in components/suspense.py when {% dj_suspense await=X %} receives a non-AsyncResult value so a typo surfaces during development (#794), simplified a redundant or not value.ok check near suspense.py:138 given the AsyncResult mutually-exclusive-flag invariant (#795), wrapped the namespaced data-hook attribute value with django.utils.html.escape() for defense-in-depth in templatetags/live_tags.py (#818), and corrected stale test-count claims in two historical CHANGELOG bullets (test_assign_async.py 11 → 18, test_suspense.py 11 → 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.py deliberately sanitize error payloads (don't echo RuntimeError / internal method names to clients; send to server logs instead). Tests now verify the sanitized contract ("server logs" in error, handler_name / session_id echo) rather than the leaked details. Fixes test_api_response.py::test_dispatch_serialize_str_missing_method_returns_500, test_observability_eval_handler.py::test_eval_500_when_handler_raises, and test_observability_reset_view.py::test_reset_500_when_mount_raises. #921: expanded open-redirect audit beyond PR #920mixins/request.py now validates hook_redirect returned by developer-defined on_mount hooks via url_has_allowed_host_and_scheme, falling back to "/" and logging a WARNING on unsafe targets. auth/mixins.pyLoginRequiredLiveViewMixin.dispatch now validates the computed login URL as defense-in-depth against misconfigured settings.LOGIN_URL, falling back to "/accounts/login/". #922: 7 new regression tests in python/djust/tests/test_security_redirects_paths.pyjavascript: 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-site LOGIN_URL fallback. #910: added upper-bound ceilings to all runtime + dev dependencies in pyproject.toml (e.g. requests>=2.28,<3, orjson>=3.11.6,<4, nh3>=0.2,<1). Prevents uncontrolled major bumps during uv lock refresh (see PR #909 which caught Django 6.x resolving under >=4.2). Ceiling policy documented in a comment above [project.dependencies]. Verified with uv lock — only material change is redis 7.3 -> 6.4 (stays under new <7 ceiling).

  • UploadMixin defensive replay for schema-changed configs — closes #892_restore_upload_configs now wraps each per-slot allow_upload(**cfg) in try/except TypeError. 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 = 1 for future explicit migrations. Regression tests in tests/unit/test_mixin_replay_schema_cross_loop_892_896.py.
  • NotificationMixin cross-loop restore — closes #896_restore_listen_channels now detects when the PostgresNotifyListener singleton 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 new PostgresNotifyListener.reset_for_new_loop() classmethod to drop the singleton before replay. The pre-check inspects listener._loop.is_closed(); a per-channel except RuntimeError branch handles the race where the loop closes between the pre-check and the ensure_listening call (resets and retries once). Prevents silent NOTIFY drops on cross-loop restore. Regression tests in tests/unit/test_mixin_replay_schema_cross_loop_892_896.py.

  • Observer JS — closes #879, #880, #881, #882#879: 37-dj-mutation.js and 38-dj-sticky-scroll.js document-level root observers now detect attribute REMOVAL on already-observed elements (via attributes: true + attributeFilter: ['dj-mutation'] / ['dj-sticky-scroll']) and call the module's teardown helper. Previously removing the attribute from an element left a stale MutationObserver + scroll listener attached. #880: documented the Map-vs-WeakMap choice in 39-dj-track-static.js — the reconnect-diff iterates all tracked elements to compare snapshot URLs, and WeakMap does not support iteration; the isConnected check in _checkStale handles detached elements. #881: documented unconditional scroll-to-bottom on install in 38-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 in tests/js/dj_mutation.test.js — no dj-mutation-fire CustomEvent fires when the element is removed before the debounce timer expires (existing _tearDownDjMutation path correctly clears the pending timer on removal).

Tests

  • dj-transition-group follow-ups — closes #905, #906#905 The VDOM RemoveChild integration test in tests/js/dj_transition_group.test.js waited 700 ms per run for the default dj-remove fallback timer. Pinned dj-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 to inner gets 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-applied ruff 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 via parser.parse(("endform_array",)) but FormArrayNode.render never rendered that nodelist — users' inner template markup silently disappeared. Fixed by rendering the nodelist once per row with row, row_index, and forloop (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 missing name= attribute: TagInput._render_custom rendered a visible "type to add" <input class="tag-input-field" placeholder="..."> with no name=, 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 whenever self.name is non-empty; hidden value is html.escape'd. #933 gallery/registry.py dead discover_* path: discover_template_tags() and discover_component_classes() were public helpers exported from djust.components.gallery.__init__ but get_gallery_data() never called them — a developer adding a new @register.tag or Component subclass without updating the curated EXAMPLES / CLASS_EXAMPLES dicts had that new thing silently missing from the rendered gallery. Fixed by wiring both helpers into get_gallery_data() as a cross-check: any registered tag / component class missing an example entry emits a logger.debug warning naming the missing entries, and discovery failures are caught so the gallery never breaks at runtime. 14 regression tests across python/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 in 42-dj-remove.js so _finalizeRemoval and _cancelRemoval no longer duplicate the clearTimeout + removeEventListener + observer.disconnect + _pendingRemovals.delete block (Stage 11 nit from PR #898). Added a debug warning (gated on globalThis.djustDebug) when _parseRemoveSpec encounters a 2-token value like dj-remove="fade-out 300" — previously silent fall-through. 2 new JSDOM regression cases in tests/js/dj_remove.test.js (12/12 passing).

  • dj-transition edge cases — closes #886, #887, #888#886_parseSpec in 41-dj-transition.js now rejects comma, paren, and bracket separators up front (returns null and emits a debug warning gated on globalThis.djustDebug) instead of letting classList.add throw InvalidCharacterError at runtime — matches the dj-remove #901 loud-in-debug / silent-in-prod pattern. #887 The cleanup callback (both transitionend handler and 600 ms fallback path) now guards with el.isConnected — if the element has been detached from the DOM before cleanup fires, we skip classList and listener work and just drop the _djTransitionState entry. Prevents any future parentNode.X access from NPE'ing on a detached node. #888 Unskipped the two previously-flaky transitionend tests in tests/js/dj_transition.test.js by swapping timing-sensitive setTimeout(..., 30) waits for synchronous el.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.

All releases · Atom feed