-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathadmin.py
More file actions
112 lines (95 loc) · 5.18 KB
/
admin.py
File metadata and controls
112 lines (95 loc) · 5.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
"""``JobAdmin`` — the custom-form fixture that exercises the html-fragment path.
This admin uses *only* documented Django ``ModelAdmin`` hooks — no
django-admin-react / -rest-api / -mcp-specific API. The point: if this runs
inside the React admin shell at ``/admin2/`` with the server-rendered
html-fragment renderer, then any legacy ``/admin/`` ModelAdmin does too.
Two rendering paths:
* **Path A** — ``/admin2/jobs/job/<pk>/change/`` (no query). The stock
change form, fully describable by the form-spec contract.
``formfield_for_dbfield`` swaps ``metadata`` to a large textarea, which
the SPA renders natively from ``widget.kind == "textarea"``.
* **Path B** — ``?run_custom=1``. ``change_view`` returns a hand-rolled
dual-listbox template (not a ModelForm, not fieldsets). The form-spec
resolver (rest-api 1.7.0+, #75) detects the custom render, renders it
SERVER-SIDE, strips the admin chrome, and returns
``{renderer: "html-fragment", html, csrf_token, submit_url, method,
messages}``. The SPA injects that HTML inside its own shell — breadcrumb /
sidebar / title / toolbar stay React-rendered, no iframe (#679). The
injected ``<form>`` POSTs back through the round-trip route, which
re-renders on a validation error (``messages.error`` + redirect-to-self) or
returns ``{renderer: "redirect", to}`` on success — surfaced as a SPA
``navigate()`` plus toasts.
"""
from __future__ import annotations
from django.contrib import admin
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.shortcuts import render
from django.urls import reverse
from examples.jobs.models import Job
def get_step_registry() -> list[dict]:
"""The catalogue of pipeline steps a Job can run (plain demo data)."""
return [
{"name": "fetch", "label": "Fetch inputs", "default_order": 1, "is_default": True},
{"name": "validate", "label": "Validate", "default_order": 2, "is_default": True},
{"name": "transform", "label": "Transform", "default_order": 3, "is_default": True},
{"name": "dry_run", "label": "Dry run", "default_order": 4, "is_default": False},
{"name": "notify", "label": "Notify stakeholders", "default_order": 5, "is_default": False},
{"name": "archive", "label": "Archive outputs", "default_order": 6, "is_default": False},
]
@admin.register(Job)
class JobAdmin(admin.ModelAdmin):
list_display = ("name", "status")
actions = ["run_with_custom_steps"]
# 1) request-aware widget override on a single DB field (Path A).
def formfield_for_dbfield(self, db_field, request, **kwargs): # noqa: ANN001
if db_field.name == "metadata":
kwargs["widget"] = admin.widgets.AdminTextareaWidget(attrs={"class": "vLargeTextField"})
return super().formfield_for_dbfield(db_field, request, **kwargs)
# 2) action that redirects to the ?run_custom=1 variant of the change page.
@admin.action(description="Run (Custom)")
def run_with_custom_steps(self, request, queryset): # noqa: ANN001
if queryset.count() != 1:
self.message_user(request, "Pick exactly one row.", level=messages.ERROR)
return None
obj = queryset.get()
return HttpResponseRedirect(
reverse("admin:jobs_job_change", args=[obj.pk]) + "?run_custom=1"
)
# 3) change_view override branching on request.GET.
def change_view(self, request, object_id, form_url="", extra_context=None): # noqa: ANN001
if request.GET.get("run_custom") == "1":
return self.run_custom_view(request, object_id)
return super().change_view(request, object_id, form_url, extra_context)
# 4) custom view rendering a custom template — not a ModelForm / fieldsets.
def run_custom_view(self, request, object_id): # noqa: ANN001
obj = self.get_object(request, object_id)
if request.method == "POST":
selected = request.POST.getlist("selected_steps") # ordered list
if not selected:
messages.error(request, "Pick at least one step.")
return redirect(request.get_full_path())
messages.success(request, f"Queued {' → '.join(selected)}")
return redirect("admin:jobs_job_change", object_id)
all_steps = get_step_registry()
selected_steps = sorted(
(s for s in all_steps if s["is_default"]),
key=lambda s: s["default_order"],
)
available_steps = [s for s in all_steps if not s["is_default"]]
context = dict(
self.admin_site.each_context(request),
title=f"Configure Step Sequence: {obj.name}",
item_type="Job",
item_name=obj.name,
obj=obj,
available_steps=available_steps,
selected_steps=selected_steps,
all_steps=sorted(all_steps, key=lambda s: s["default_order"]),
back_url=reverse("admin:jobs_job_changelist"),
cancel_url=reverse("admin:jobs_job_change", args=[obj.pk]),
opts=self.model._meta,
preserved_filters=self.get_preserved_filters(request),
)
return render(request, "admin/jobs/job/run_custom.html", context)