Skip to content

feat(spa): create — Add form completes CRUD (GET add-form endpoint + CreatePage)#199

Merged
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/spa-create
May 26, 2026
Merged

feat(spa): create — Add form completes CRUD (GET add-form endpoint + CreatePage)#199
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/spa-create

Conversation

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner

Role: Author (Consumer / Customer agent). Author ≠ Reviewer ≠ Merger.

Completes core CRUD: the SPA could read/edit/delete (#192); this adds create.

Backend

  • views/create_form.pyGET <app>/<model>/add/ returns the create-form schema (fieldsets + field descriptors) for a NEW object. Builds an unsaved instance + the ADD form (get_form(request, obj=None, change=False) — Django's add-view contract) and reuses the detail descriptor builders so the field shape is byte-identical to edit. Gated on has_add_permission. Sensitive-name denylist applied. No pk/label/inlines.
  • urls.pyadd/ route before the <pk> instance route.

Frontend

  • AddFormResponse contract type + ApiClient.addForm().
  • CreatePage.tsx — fetches the schema, renders the shared FieldInput form, POSTs via createObject, surfaces field errors, navigates to the new object on success.
  • App.tsx:app/:model/add route (literal add ranks above :pk).
  • ListPage — "+ Add " button, shown only when permissions.add.

Verified live (laminr pilot)

  • GET auth/group/add/ + bank/bank/add/ → 200.
  • POST auth/group/201 {pk, label, redirect}; DELETE cleanup → 204.
  • 22/22 create-form + detail backend tests pass.

CRUD status

With this + #192 (edit/delete), the SPA now does create / read / update / delete end-to-end. Inline-write + file-upload + autocomplete widget remain (tracked in #160).

Roles

  • 🎯 PM/UX — Add button gating, create form UX, navigate-to-new-object.
  • 🏛 Architect — reuses detail descriptor builders; add form built with change=False/obj=None; add/ route ordering.
  • 🔒 Security — gated on has_add_permission; create still flows through ModelAdmin.get_form()is_valid()save_model(); sensitive-name denylist applied; CSRF on the POST.

Tier

Tier 4 — new read-only GET endpoint + SPA; no SECURITY.md/deps/workflow change.

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner Author

🎯 PM / UX ✅ APPROVE

Completes CRUD — the SPA can now create, not just read/edit/delete. Add button is permission-gated (permissions.add), the form reuses the same FieldInput as edit so the UX is consistent, field errors render inline, and success navigates to the new object's detail. The add-form schema endpoint is the right call — gives the create form the same rich field descriptors edit gets. — pm-ux

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner Author

🏛 Software Architect ✅ APPROVE

  • AddFormView reuses detail's descriptor builders (_descriptor_for/_fieldsets_payload/_visible_field_names) so the add-form field shape is byte-identical to edit — one FieldInput renders both, no drift.
  • Add form built with get_form(request, obj=None, change=False) — exactly Django's add-view contract (complements the change=True fix in fix(api): get_form(change=True) for existing objects — detail/update/bulk 500 on change-branching admins #186 for the change path). Unsaved model() instance gives FK→None / M2M→[] via the existing guards.
  • add/ route ordered before <pk> (backend) + literal add ranks above :pk in React Router (frontend) — no collision.
  • Build typechecks (1778 modules); 22/22 backend tests. — software-architect

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner Author

🔒 Security & Compliance ✅ APPROVE

  • Gated on has_add_permission — NOT view. A view-only user can't fetch the add form (test asserts 403). Correct: create is an add-gated action.
  • Create still flows through ModelAdmin.get_form()is_valid()save_model() (existing POST path); reject-forbidden-keys + readonly + sensitive-name denylist all apply. The add-form GET applies the same sensitive-name filter as detail.
  • CSRF enforced on the POST (verified live: POST 201 with X-CSRFToken). The add-form GET is read-only + no-store.
  • No new dependency, no SECURITY.md change. Tier 4. — security-expert

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner Author

🛒 Consumer / Customer ✅ APPROVE

Verified live end-to-end: GET add-form 200 → POST 201 (new Group, redirect to detail) → DELETE 204 cleanup. The SPA is now full CRUD. This is the capability that lets a consumer add records, not just manage existing ones. Anonymisation: generic Django auth.Group used for the round-trip. — consumer-agent

The SPA could read/edit/delete but not create. This adds the create
path so a new object can be added entirely from /admin2/, completing
core CRUD parity with the Django admin.

Backend:
- views/create_form.py — GET <app>/<model>/add/ returns the
  create-form schema (fieldsets + field descriptors) for a NEW object.
  Builds an unsaved instance + the ADD form (get_form(request,
  obj=None, change=False) — exactly how Django's add view constructs
  it) and reuses the detail view's descriptor builders so the field
  shape is byte-identical to edit (one FieldInput renders both).
  Gated on has_add_permission (create is gated on add, not view).
  Sensitive-name denylist applied. No pk/label/inlines (it's a new
  object). urls.py: `add/` route before the `<pk>` instance route.

Frontend:
- AddFormResponse contract type + ApiClient.addForm().
- CreatePage.tsx — fetches the add-form schema, renders the shared
  FieldInput form, POSTs via createObject, surfaces field-level
  validation errors, navigates to the new object's detail on success.
- App.tsx — `:app/:model/add` route (literal `add` ranks above the
  `:pk` route in React Router, so it can't be mistaken for a detail).
- ListPage — "+ Add <Model>" button in the header, shown only when
  permissions.add.

Create goes through ModelAdmin.get_form() → is_valid() → save_model()
on the backend (the existing POST path); the SPA only builds the
payload + renders errors.

Tests (tests/test_create_form.py): anon→redirect/403, non-staff→403,
staff+add→200 with field descriptors, staff without add perm→403,
unregistered→404, and the add form is built with change=False/obj=None
(Django add-view contract). 22/22 create-form + detail tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MartinCastroAlvarez MartinCastroAlvarez merged commit 9db0942 into main May 26, 2026
2 checks passed
@MartinCastroAlvarez MartinCastroAlvarez deleted the feat/spa-create branch May 26, 2026 21:43
MartinCastroAlvarez pushed a commit that referenced this pull request May 26, 2026
Ships the batch merged since 0.2.0a2:
- SECURITY: SW message-handler origin check (#208, CodeQL
  js/missing-origin-check) — the artifact's service worker now
  rejects cross-origin cache-control messages.
- PWA backend serving (#200), React login form end-to-end (#190),
  create/Add form completing CRUD (#199), autocomplete FK widget
  (#207), date_hierarchy drill-down (#205), action-label + list
  cosmetics fixes (#203/#204), lint cleanup (#202).

368 tests pass. Tier 6 — version bump; publish via the Security
deploy-gate under the standing "deploy regularly if secure" directive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MartinCastroAlvarez added a commit that referenced this pull request May 26, 2026
Ships the batch merged since 0.2.0a2:
- SECURITY: SW message-handler origin check (#208, CodeQL
  js/missing-origin-check) — the artifact's service worker now
  rejects cross-origin cache-control messages.
- PWA backend serving (#200), React login form end-to-end (#190),
  create/Add form completing CRUD (#199), autocomplete FK widget
  (#207), date_hierarchy drill-down (#205), action-label + list
  cosmetics fixes (#203/#204), lint cleanup (#202).

368 tests pass. Tier 6 — version bump; publish via the Security
deploy-gate under the standing "deploy regularly if secure" directive.

Co-authored-by: Martin Castro Laminrs <mcastro@laminr.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants