djust 0.7.0rc1

Pre-releaseReleased

Added

  • Streaming Markdown {% djust_markdown %} (v0.7.0) — server-side Markdown renderer built on pulldown-cmark 0.12 with three safety guarantees wired in at the crate level: raw HTML in the source is escaped (Options::ENABLE_HTML is never set; because pulldown-cmark 0.12 still emits Event::Html / Event::InlineHtml when that flag is off, sanitise_event re-routes those events to Event::Text so the writer escapes them), javascript: / vbscript: / data: URL schemes in link/image destinations are rewritten to # (case-insensitive, leading-whitespace tolerant), and inputs larger than 10 MiB (per-call input cap, not a concurrency limiter) are returned as an escaped <pre class="djust-md-toobig"> block without invoking the parser. A provisional-line splitter renders a partially-typed trailing line as escaped text inside <p class="djust-md-provisional">, eliminating mid-token flicker for streaming LLM output. Exposed three ways: the {% djust_markdown expr [kwargs] %} tag (registered via the existing Rust tag-handler registry), the Python helper djust.render_markdown(src, **opts) returning a SafeString, and the PyO3 function djust._rust.render_markdown. Kwargs: provisional, tables, strikethrough, task_lists. Note on deviation from plan: autolinks was dropped from the public surface — pulldown-cmark 0.12 does not expose a GFM_AUTOLINK / ENABLE_AUTOLINK options flag, so plain-text URLs stay as text unless wrapped in explicit [text](url) syntax. Will be reconsidered when the upstream parser is bumped. Covered by 24 Rust tests (crates/djust_templates/src/markdown.rs, including regression cases for vbscript:, data:, mixed-case JavaScript:, leading-whitespace URLs, <iframe> escaping, image-src neutralisation, and the 10 MiB cap) and 14 Python tests (python/djust/tests/test_markdown.py + tests/unit/test_markdown_tag.py), plus 3 A090 system-check tests — 41 total (24 Rust + 14 Python/tag + 3 A090). Demo at /demos/markdown-stream/; full write-up in docs/website/guides/streaming-markdown.md.
  • Admin widgets & bulk-action progress (v0.7.0) — two additions to djust.admin_ext close the most-requested gaps in the alternative reactive admin:
    • DjustModelAdmin.change_form_widgets / change_list_widgets class attributes accept any list of LiveView subclasses; each is embedded via {% live_render %} on the matching admin page. Permission filtering honours permission_required on the widget class. See docs/website/guides/admin-widgets.md.
    • @admin_action_with_progress (in djust.admin_ext.progress) turns any DjustModelAdmin action into a background daemon thread and redirects the user to a BulkActionProgressWidget page at <admin>/djust-progress/<job_id>/. The page polls the job every 500 ms, re-renders the progress bar / message / log, and wires a Cancel button that atomically flips done and cancelled. Queryset is eagerly pinned to PKs before the thread starts (no lazy-eval foot-guns). Cancellation is cooperative — clicking Cancel flips progress.cancelled = True; the action body must periodically check if progress.cancelled: return to actually stop (Python cannot safely interrupt a running thread mid-statement).
    • Server-side permission enforcement@admin_action_with_progress(permissions=[...]) stamps allowed_permissions on the wrapped action; ModelListView.run_action now calls request.user.has_perms(allowed) before dispatching the action and raises PermissionDenied if the user lacks any declared perm. Closes the gap where has_*_permission returns True for any staff user.
    • Bounded server state: _JOBS is LRU-capped at _MAX_JOBS = 500 (oldest entries evicted on insert once the cap is reached), and Job.message / Job.error are individually truncated to _MAX_MESSAGE_CHARS = 4096 on each progress.update(...) call. Job.error is a generic user-facing string ("Action failed — see server logs for details"); the raw exception text lives only on the server-side Job._error_raw attribute and is always logged at ERROR level via logger.exception (logger name djust.admin_ext.progress).
    • New setting: DJUST_ASGI_WORKERS (default 1) — declares the number of ASGI workers in the deployment. Gates the A073 system check (fires only when DJUST_ASGI_WORKERS > 1) so single-worker development stays silent.
    • Defense-in-depth allowlist: DJUST_LIVE_RENDER_ALLOWED_MODULES (optional) restricts the dotted-path module prefixes that {% live_render %} will resolve — any widget slot path outside the allowlist raises TemplateSyntaxError at render time.
    • Two new system checks: djust.A072 (warning) fires if a non- LiveView class is registered in a widget slot; djust.A073 (info, gated on DJUST_ASGI_WORKERS > 1) fires at startup if any admin site hosts a @admin_action_with_progress-decorated action, noting the v0.7.0 single-worker _JOBS limitation and pointing at the v0.7.1 channel-layer follow-up.
    • 25 new tests: python/djust/tests/test_bulk_progress.py (12) + python/djust/tests/test_admin_widgets_per_page.py (13); +A072/A073 check tests in python/tests/test_checks.py.
  • {% dj_activity %} + ActivityMixin (v0.7.0) — React 19.2 <Activity> parity: pre-rendered hidden regions of a LiveView that preserve their local DOM state (form inputs, scroll, transient JS) across show/hide cycles. The new block tag {% dj_activity "name" visible=expr eager=expr %}...{% enddj_activity %} emits a wrapper <div> carrying data-djust-activity, data-djust-visible, and — when not visible — the HTML hidden attribute plus aria-hidden="true". The body is rendered unconditionally in every pass so local state isn't lost. ActivityMixin (composed into LiveView AFTER StickyChildRegistry, BEFORE View) provides the server-side API: set_activity_visible(name, visible), is_activity_visible(name), declarative eager_activities: frozenset class attr, and an internal FIFO deferred-event queue (cap 100, overridable via activity_event_queue_cap) drained by the WebSocket consumer after every handle_event / handle_info dispatch. Client runtime (python/djust/static/djust/src/49-activity.js) exposes window.djust.activityVisible(name) and dispatches a bubbling djust:activity-shown CustomEvent when a panel flips hidden → visible. The event-dispatch gate in 11-event-handler.js drops events whose trigger sits inside a hidden non-eager activity client-side (stamping _activity on all other events for server-side deferral). The VDOM patcher in 12-vdom-patch.js skips subtree patches targeting nodes inside a hidden non-eager activity so DOM state is preserved. Two new system checks: A070 (Warning — missing name argument) and A071 (Error — duplicate activity name within one template). See docs/website/guides/activity.md for the full guide + {% if %} / {% live_render %} / sticky / dj-prefetch comparison matrix. Demo at examples/demo_project/djust_demos/views/activity_demo.py.
  • Intent-Based Prefetch (dj-prefetch, v0.7.0) — hover- and touch-driven navigation prefetch that complements the existing service-worker-mediated hover prefetch. Links opting in with <a dj-prefetch href="..."> are prefetched after a 65 ms hover debounce (cancelled on mouseleave before the debounce fires) and immediately on touchstart — mobile users commit to a tap fast, so no debounce is applied there. Prefetch uses <link rel="prefetch" as="document"> injection so the browser manages the cache lifecycle (falls back to low-priority fetch + AbortController when relList doesn't advertise 'prefetch'). Same-origin only; javascript: / data: URLs blocked; dedup'd per URL via a Set that window.djust._prefetch.clear() wipes on SPA navigation. Opt out per-link with dj-prefetch="false". Respects navigator.connection.saveData. New client surface: window.djust._intentPrefetch for test/diagnostic access. Scope: client-side only — no new server endpoint. Contract: dj-prefetch is intended for author-controlled navigation links only; don't put it on links that perform state-changing GETs (see the module header in python/djust/static/djust/src/22-prefetch.js for the full safety contract). See docs/website/guides/prefetch.md for the guide and the SW-hover-vs-intent comparison table.
  • Server Functions (@server_function / djust.call(), v0.7.0) — same-origin browser RPC without VDOM re-render. Decorate a LiveView method with @server_function and invoke it from JavaScript as await djust.call('<view_slug>', '<fn>', {params}); the return value is JSON-serialized straight back to the caller. The three primitives now split cleanly by intent:
    • @event_handler — WebSocket, triggers a VDOM re-render (UI interactions: click, submit, input).
    • @event_handler(expose_api=True) — HTTP (ADR-008), triggers a re-render AND exposes the handler to mobile / S2S / AI-agent callers via OpenAPI.
    • @server_function — HTTP, no re-render, no OpenAPI, no api_response / serialize= hooks. Designed exclusively for in-browser RPC; response envelope is the minimal {"result": <value>}. Session-cookie auth + CSRF are both required unconditionally — no auth-class opt-out. Request body shape is strict: only an empty body, {}, or {"params": {...}} are accepted; any other shape (flat objects, wrapped objects with sibling keys) returns 400 invalid_body. This deliberately removes the ambiguity where a caller's own field named params would be silently unwrapped and every sibling key dropped. The dispatcher reuses the ADR-008 pipeline unchanged: parameter coercion via validate_handler_params, @permission_required gating via check_handler_permission, and @rate_limit via the same LRU-capped _rate_buckets OrderedDict. Both sync and async def functions are supported via _call_possibly_async. Stacking @event_handler and @server_function on the same method raises TypeError at decoration time — a function either re-renders the view or returns an RPC result, never both. New URL: POST /djust/api/call/<view_slug>/ <function_name>/, declared BEFORE the catch-all dispatch pattern so it can't be shadowed. New public surface: djust.decorators.server_function, is_server_function, djust.api.DjustServerFunctionView, dispatch_server_function (in python/djust/api/dispatch.py), iter_server_functions. New client module python/djust/static/djust/src/48-server-functions.js (~40 LOC, ~430 B gzipped delta). Demo: examples/demo_project/djust_demos/ adds a product-search view demonstrating both features end-to-end. See docs/website/guides/server-functions.md for the full API reference, error-code table, and comparison vs. @event_handler and @event_handler(expose_api=True).

All releases · Atom feed