Skip to content

feat(api): object-level change-page actions (#236)#540

Merged
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/object-actions-236-135300
May 28, 2026
Merged

feat(api): object-level change-page actions (#236)#540
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/object-actions-236-135300

Conversation

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner

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 registered ModelAdmin can 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.

  • Backend: object_actions.py (action discovery + allowed_permissions gate), views/object_action.py (run endpoint), wired in api/urls.py; detail view surfaces the per-object action list.
  • Frontend: detail page renders the action buttons + result handling; new types in contract.ts, client/data wiring.
  • Tests: test_object_actions.py covers permission gates, the change-actions fallback, and the run flow.

Unblocks #455 (defense-in-depth on change_actions allowed_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.py green (81 backend).
  • Full vitest (142) + pnpm -r typecheck + eslint + ruff + mypy clean.

🤖 Generated with Claude Code

Copy link
Copy Markdown
Contributor

@martin-castro-laminr-ai martin-castro-laminr-ai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — non-author (architect/security)

Same security design as the original #427 draft I reviewed positively (gate order is_admin_userresolve_modelload_object_or_none → per-object has_change_permissionnamepermitted_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
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