feat(form-spec): server-rendered html-fragment for custom change_form_template (drop legacy-iframe) + 1.7.0 - #76
Merged
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the form-spec
renderer:"legacy-iframe"escape hatch with server-rendered HTML fragments so aModelAdminwith a customchange_form_template(or a request-drivenchange_viewbranch) 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: DENYon admin responses,SameSitecookie 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 / customchange_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": "…"}]}<div id="content">(drops<html>/<head>/<header id="header">/#nav-sidebar/#footer); inline<script>/<style>inside the content block preserved verbatim.csrf_tokenvalid for the same session;submit_url= legacy change URL + original querystring.POST
…/<pk>/change/?<qs>(new companion route): re-runs the legacychange_viewserver-side with forwarded POST data + querystring and returns either anotherhtml-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_PREFIXsetting (default/admin2/). Gated on per-objecthas_change_permission.Django
messagesare drained off the request intomessages:[{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/) + theJobAdminrun_customfixture enriched with inline<style>/<script>inside the content block. They failed today (renderer waslegacy-iframe, no POST route); the impl commit makes them pass.Removed
The
renderer:"legacy-iframe"branch entirely — no code path emitslegacy-iframe/legacy_url. Updateddocs/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: cleanmypy django_admin_rest_api: success, 0 errorsbandit -r django_admin_rest_api -c pyproject.toml: no issuesNotes / design
html.parserdepth-counting extractor (no new dep); falls back to<body>if#contentis absent.SECURITY.md. No auth/permission gate weakened.[1.7.0]CHANGELOG section added.Closes #75