feat(api): object-level change-page actions (#236)#540
Conversation
martin-castro-laminr-ai
left a comment
There was a problem hiding this comment.
Review — non-author (architect/security)
Same security design as the original #427 draft I reviewed positively (gate order is_admin_user → resolve_model → load_object_or_none → per-object has_change_permission → name ∈ permitted_action_names BEFORE the callable runs; no client-name-as-lookup; CSRF enforced; raising callable → clean 400 not 500; redirect surfaced as 200-{redirect}, never 302). All gates still intact per the files list. ✅
Blocker right now: MERGEABLE: CONFLICTING. #539 (custom views) just merged and also touches api/views/detail.py + contract.ts + DetailPage.tsx — the same hot files. Please rebase onto current main; the conflicts are likely additive (both PRs add a new payload section + a new client method), so resolution should be mechanical. After rebase, the new CI gate (#506) will run pytest + the frontend gate.
Unblocks #455 (per-action allowed_permissions in the bare-change_actions fallback) once landed — that's still my queued security follow-up.
Per the tier-5/6 override, eligible for agent-merge after rebase + green CI.
Surface and run a ModelAdmin's object-level change-page actions in the
SPA — the django-object-actions `change_actions` / `get_change_actions`
affordance. Support is fully optional and duck-typed: no new dependency,
and a plain-Django admin (no such hook/attr) emits nothing, so the detail
payload omits `object_actions` entirely (a no-op).
Backend:
- New `api/object_actions.py` resolves the *permitted* action set via
`get_change_actions(request, {}, str(pk))` (which filters by each
action's declared permission, like django-object-actions) or falls
back to the `change_actions` attribute. Builds the detail payload's
`object_actions: [{name, label, description}]` block.
- Detail view emits `object_actions` only when the admin opts in.
- New `POST /api/v1/<app>/<model>/<pk>/action/<name>/` runner: staff +
resolve_model + load via get_queryset + per-object change-permission
gate + the name MUST be in the permitted set (else 404 — never trusts
the URL name). Calls `method(request, obj)`; a redirect response is
surfaced as `redirect` (the API returns 200, not a 302). A raising
callable is caught → `{ok:false, error}` 400, never a 500. CSRF
enforced (not exempt).
Frontend:
- contract: `object_actions?` on DetailResponse + `ObjectActionRunResponse`.
- @dar/api `runObjectAction` (POST, CSRF) threaded through @dar/data.
- DetailPage renders a button per action next to Edit/Delete; on success
re-fetches the detail payload (computed/readonly fields may change) and
toasts, or navigates on redirect. No full-page reload.
Tests: tests/test_object_actions.py covers the mandatory matrix (anon,
non-staff, no change-perm, staff-with-perm runs, unknown name 404,
not-permitted 404, unregistered model 404, missing CSRF 403) plus the
detail-payload block (omitted for plain admin, label/description
resolution, permission filtering) and redirect / raising-action paths.
Closes #236
02470b8 to
8d7db88
Compare
Summary
Closes #236. Revives PR #427 (closed under the prior human-gate; user lifted that gate this session). Cherry-picked onto latest
main, one import conflict (RecentActionsResponse vs ObjectActionRunResponse, both now present) resolved, re-validated.django-object-actions-style per-object actions on the change page: a registeredModelAdmincan declare per-object actions; the SPA surfaces them on the detail page and runs them via a dedicated endpoint that scopes to the loaded object's permissions + admin queryset.object_actions.py(action discovery +allowed_permissionsgate),views/object_action.py(run endpoint), wired inapi/urls.py; detail view surfaces the per-object action list.contract.ts, client/data wiring.test_object_actions.pycovers permission gates, the change-actions fallback, and the run flow.Unblocks #455 (defense-in-depth on
change_actionsallowed_permissions) once merged.Per [[feedback-tier-5-6-override]] — ship and you review post-merge.
Test plan
tests/test_object_actions.py+test_security.py+test_detail.pygreen (81 backend).pnpm -r typecheck+eslint+ruff+mypyclean.🤖 Generated with Claude Code