Skip to content

feat(form-spec): server-rendered html-fragment for custom change_form_template (drop legacy-iframe) + 1.7.0 - #76

Merged
MartinCastroAlvarez merged 2 commits into
mainfrom
feat/html-fragment-renderer-75
Jun 2, 2026
Merged

feat(form-spec): server-rendered html-fragment for custom change_form_template (drop legacy-iframe) + 1.7.0#76
MartinCastroAlvarez merged 2 commits into
mainfrom
feat/html-fragment-renderer-75

Conversation

@MartinCastroAlvarez

Copy link
Copy Markdown
Owner

Summary

Replaces the form-spec renderer:"legacy-iframe" escape hatch with server-rendered HTML fragments so a ModelAdmin with a custom change_form_template (or a request-driven change_view branch) renders inside the SPA shell — plug-and-play, no iframe. Upstream blocker for react #679 + mcp #84.

Iframe embedding never worked in practice (X-Frame-Options: DENY on admin responses, SameSite cookie constraints, cross-origin SPA deploys). The new contract renders the integrator's own template server-side and hands the SPA the content-block HTML to inject in place.

Contract

GET …/<pk>/form-spec/ (custom template / custom change_view):

{"renderer": "html-fragment", "html": "<form …>…</form>", "csrf_token": "",
 "submit_url": "/admin/jobs/job/1/change/?run_custom=1", "method": "POST",
 "messages": [{"level": "success", "text": ""}]}
  • Admin chrome stripped to the inside of <div id="content"> (drops <html>/<head>/<header id="header">/#nav-sidebar/#footer); inline <script>/<style> inside the content block preserved verbatim.
  • csrf_token valid for the same session; submit_url = legacy change URL + original querystring.

POST …/<pk>/change/?<qs> (new companion route): re-runs the legacy change_view server-side with forwarded POST data + querystring and returns either another html-fragment (validation errors — incl. the Post/Redirect/Get-to-self idiom, followed server-side) or:

{"renderer": "redirect", "to": "/admin2/jobs/job/1/change/", "messages": [...]}

Legacy redirect target rewritten onto the configurable SPA_URL_PREFIX setting (default /admin2/). Gated on per-object has_change_permission.

Django messages are drained off the request into messages:[{level,text}] (no browser session round-trip).

Reproduction

Committed first: the 3 contract tests from #75 (adapted to this repo's real urlconf — form-spec at /admin-api/api/v1/<app>/<model>/<pk>/form-spec/, POST companion at …/<pk>/change/) + the JobAdmin run_custom fixture enriched with inline <style>/<script> inside the content block. They failed today (renderer was legacy-iframe, no POST route); the impl commit makes them pass.

Removed

The renderer:"legacy-iframe" branch entirely — no code path emits legacy-iframe/legacy_url. Updated docs/api-contract.md §4.1, README carry-through table, SECURITY.md (new trusted-HTML boundary section + GET-idempotency note), schema.py, and the Job fixture docs.

Verification

  • pytest -q: 616 passed (91% cov)
  • ruff check / ruff format --check: clean
  • mypy django_admin_rest_api: success, 0 errors
  • bandit -r django_admin_rest_api -c pyproject.toml: no issues

Notes / design

  • Chrome-stripping uses a stdlib html.parser depth-counting extractor (no new dep); falls back to <body> if #content is absent.
  • The fragment is injected as trusted HTML — same trust boundary as the integrator's legacy admin, gated behind the identical staff + permission checks. Documented in SECURITY.md. No auth/permission gate weakened.
  • Version bumped 1.6.0 → 1.7.0; [1.7.0] CHANGELOG section added.

Closes #75

martin-castro-laminr-ai and others added 2 commits June 2, 2026 17:34
… tests (#75)

Add the three html-fragment contract tests from issue #75 (adapted to this
repo's real urlconf: form-spec at /admin-api/api/v1/<app>/<model>/<pk>/form-spec/,
POST companion at /admin-api/api/v1/<app>/<model>/<pk>/change/). Enrich the
JobAdmin run_custom fixture template with inline <style>/<script> inside the
content block so the chrome-stripping contract (preserve them verbatim) is
exercised.

These fail today: the resolver emits renderer:"legacy-iframe" and there is no
POST change route. This is the reproduction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…trip (drop legacy-iframe) (#75)

Replace the form-spec `renderer:"legacy-iframe"` escape hatch with
server-rendered HTML fragments so a ModelAdmin with a custom
change_form_template (or a request-driven change_view branch) renders
inside the SPA shell — no iframe, no X-Frame-Options / SameSite failure.

- form_spec.py: _custom_renderer invokes the real change/add view with the
  SPA caller's request, strips admin chrome to the inside of
  <div id="content"> via a stdlib html.parser extractor (inline
  <script>/<style> preserved verbatim), and returns
  {renderer:"html-fragment", html, csrf_token, submit_url, method, messages}.
- render_change_post + ChangePostView: companion POST route
  /api/v1/<app>/<model>/<pk>/change/?<qs> re-runs change_view and returns a
  redirect payload (legacy target mapped onto SPA_URL_PREFIX, default
  /admin2/) or another html-fragment for re-renders, including following the
  PRG-to-self validation-error idiom server-side. Gated on per-object
  has_change_permission.
- Django messages drained off the request into {level,text} (no session
  round-trip).
- Dropped the legacy-iframe branch entirely; updated docs/api-contract.md
  §4.1, README carry-through table, SECURITY.md (trusted-HTML boundary),
  schema.py, and the Job fixture docs/templates.
- SPA_URL_PREFIX setting; 1.7.0 + CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MartinCastroAlvarez
MartinCastroAlvarez merged commit 4a3a900 into main Jun 2, 2026
18 checks passed
@MartinCastroAlvarez
MartinCastroAlvarez deleted the feat/html-fragment-renderer-75 branch June 2, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Form-spec: render ModelAdmin.change_form_template server-side and return as html-fragment (drop iframe fallback)

2 participants