plain.admin 0.14.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/admin/README.md +260 -0
- plain/admin/__init__.py +5 -0
- plain/admin/assets/admin/admin.css +108 -0
- plain/admin/assets/admin/admin.js +79 -0
- plain/admin/assets/admin/chart.js +19 -0
- plain/admin/assets/admin/jquery-3.6.1.slim.min.js +2 -0
- plain/admin/assets/admin/list.js +57 -0
- plain/admin/assets/admin/popper.min.js +5 -0
- plain/admin/assets/admin/tippy-bundle.umd.min.js +1 -0
- plain/admin/assets/toolbar/toolbar.js +51 -0
- plain/admin/cards/__init__.py +10 -0
- plain/admin/cards/base.py +86 -0
- plain/admin/cards/charts.py +153 -0
- plain/admin/cards/tables.py +26 -0
- plain/admin/config.py +21 -0
- plain/admin/dates.py +254 -0
- plain/admin/default_settings.py +4 -0
- plain/admin/impersonate/README.md +44 -0
- plain/admin/impersonate/__init__.py +3 -0
- plain/admin/impersonate/middleware.py +38 -0
- plain/admin/impersonate/models.py +0 -0
- plain/admin/impersonate/permissions.py +16 -0
- plain/admin/impersonate/settings.py +8 -0
- plain/admin/impersonate/urls.py +10 -0
- plain/admin/impersonate/views.py +23 -0
- plain/admin/middleware.py +12 -0
- plain/admin/querystats/README.md +191 -0
- plain/admin/querystats/__init__.py +3 -0
- plain/admin/querystats/core.py +153 -0
- plain/admin/querystats/middleware.py +99 -0
- plain/admin/querystats/urls.py +9 -0
- plain/admin/querystats/views.py +27 -0
- plain/admin/templates/admin/base.html +160 -0
- plain/admin/templates/admin/cards/base.html +30 -0
- plain/admin/templates/admin/cards/card.html +17 -0
- plain/admin/templates/admin/cards/chart.html +25 -0
- plain/admin/templates/admin/cards/table.html +35 -0
- plain/admin/templates/admin/delete.html +17 -0
- plain/admin/templates/admin/detail.html +24 -0
- plain/admin/templates/admin/form.html +13 -0
- plain/admin/templates/admin/index.html +5 -0
- plain/admin/templates/admin/list.html +194 -0
- plain/admin/templates/admin/page.html +3 -0
- plain/admin/templates/admin/search.html +27 -0
- plain/admin/templates/admin/values/UUID.html +1 -0
- plain/admin/templates/admin/values/bool.html +9 -0
- plain/admin/templates/admin/values/datetime.html +1 -0
- plain/admin/templates/admin/values/default.html +5 -0
- plain/admin/templates/admin/values/dict.html +1 -0
- plain/admin/templates/admin/values/get_display.html +1 -0
- plain/admin/templates/admin/values/img.html +4 -0
- plain/admin/templates/admin/values/list.html +1 -0
- plain/admin/templates/admin/values/model.html +15 -0
- plain/admin/templates/admin/values/queryset.html +7 -0
- plain/admin/templates/elements/admin/Checkbox.html +8 -0
- plain/admin/templates/elements/admin/CheckboxField.html +7 -0
- plain/admin/templates/elements/admin/FieldErrors.html +5 -0
- plain/admin/templates/elements/admin/Input.html +9 -0
- plain/admin/templates/elements/admin/InputField.html +5 -0
- plain/admin/templates/elements/admin/Label.html +3 -0
- plain/admin/templates/elements/admin/Select.html +11 -0
- plain/admin/templates/elements/admin/SelectField.html +5 -0
- plain/admin/templates/elements/admin/Submit.html +6 -0
- plain/admin/templates/querystats/querystats.html +78 -0
- plain/admin/templates/querystats/toolbar.html +79 -0
- plain/admin/templates/toolbar/toolbar.html +91 -0
- plain/admin/templates.py +25 -0
- plain/admin/toolbar.py +36 -0
- plain/admin/urls.py +45 -0
- plain/admin/views/__init__.py +41 -0
- plain/admin/views/base.py +140 -0
- plain/admin/views/models.py +254 -0
- plain/admin/views/objects.py +399 -0
- plain/admin/views/registry.py +117 -0
- plain/admin/views/types.py +6 -0
- plain/admin/views/viewsets.py +54 -0
- plain_admin-0.14.1.dist-info/METADATA +275 -0
- plain_admin-0.14.1.dist-info/RECORD +80 -0
- plain_admin-0.14.1.dist-info/WHEEL +4 -0
- plain_admin-0.14.1.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,254 @@
|
|
1
|
+
from typing import TYPE_CHECKING
|
2
|
+
|
3
|
+
from plain import models
|
4
|
+
from plain.models import Manager, Q
|
5
|
+
|
6
|
+
from .objects import (
|
7
|
+
AdminCreateView,
|
8
|
+
AdminDeleteView,
|
9
|
+
AdminDetailView,
|
10
|
+
AdminListView,
|
11
|
+
AdminUpdateView,
|
12
|
+
)
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from plain import models
|
16
|
+
|
17
|
+
|
18
|
+
def get_model_field(instance, field):
|
19
|
+
if "__" in field:
|
20
|
+
# Allow __ syntax like querysets use,
|
21
|
+
# also automatically calling callables (like __date)
|
22
|
+
result = instance
|
23
|
+
for part in field.split("__"):
|
24
|
+
result = getattr(result, part)
|
25
|
+
|
26
|
+
# If we hit a None, just return it
|
27
|
+
if not result:
|
28
|
+
return result
|
29
|
+
|
30
|
+
if callable(result):
|
31
|
+
result = result()
|
32
|
+
|
33
|
+
return result
|
34
|
+
|
35
|
+
attr = getattr(instance, field)
|
36
|
+
|
37
|
+
if isinstance(attr, Manager):
|
38
|
+
# Automatically get .all() of related managers
|
39
|
+
return attr.all()
|
40
|
+
|
41
|
+
return attr
|
42
|
+
|
43
|
+
|
44
|
+
class AdminModelListView(AdminListView):
|
45
|
+
show_search = True
|
46
|
+
allow_global_search = True
|
47
|
+
|
48
|
+
model: "models.Model"
|
49
|
+
|
50
|
+
fields: list = ["pk"]
|
51
|
+
queryset_order = []
|
52
|
+
search_fields: list = ["pk"]
|
53
|
+
|
54
|
+
def get_title(self) -> str:
|
55
|
+
if title := super().get_title():
|
56
|
+
return title
|
57
|
+
|
58
|
+
return self.model._meta.model_name.capitalize() + "s"
|
59
|
+
|
60
|
+
@classmethod
|
61
|
+
def get_nav_title(cls) -> str:
|
62
|
+
if cls.nav_title:
|
63
|
+
return cls.nav_title
|
64
|
+
|
65
|
+
if cls.title:
|
66
|
+
return cls.title
|
67
|
+
|
68
|
+
return cls.model._meta.model_name.capitalize() + "s"
|
69
|
+
|
70
|
+
@classmethod
|
71
|
+
def get_slug(cls) -> str:
|
72
|
+
return cls.model._meta.model_name
|
73
|
+
|
74
|
+
def get_template_context(self):
|
75
|
+
context = super().get_template_context()
|
76
|
+
|
77
|
+
order_by = self.request.GET.get("order_by", "")
|
78
|
+
if order_by.startswith("-"):
|
79
|
+
order_by_field = order_by[1:]
|
80
|
+
order_by_direction = "-"
|
81
|
+
else:
|
82
|
+
order_by_field = order_by
|
83
|
+
order_by_direction = ""
|
84
|
+
|
85
|
+
context["order_by_field"] = order_by_field
|
86
|
+
context["order_by_direction"] = order_by_direction
|
87
|
+
|
88
|
+
return context
|
89
|
+
|
90
|
+
def get_objects(self):
|
91
|
+
queryset = self.get_initial_queryset()
|
92
|
+
queryset = self.order_queryset(queryset)
|
93
|
+
queryset = self.search_queryset(queryset)
|
94
|
+
return queryset
|
95
|
+
|
96
|
+
def get_initial_queryset(self):
|
97
|
+
# Separate override for the initial queryset
|
98
|
+
# so that annotations can be added BEFORE order_by, etc.
|
99
|
+
return self.model.objects.all()
|
100
|
+
|
101
|
+
def order_queryset(self, queryset):
|
102
|
+
if order_by := self.request.GET.get("order_by"):
|
103
|
+
queryset = queryset.order_by(order_by)
|
104
|
+
elif self.queryset_order:
|
105
|
+
queryset = queryset.order_by(*self.queryset_order)
|
106
|
+
|
107
|
+
return queryset
|
108
|
+
|
109
|
+
def search_queryset(self, queryset):
|
110
|
+
if search := self.request.GET.get("search"):
|
111
|
+
filters = Q()
|
112
|
+
for field in self.search_fields:
|
113
|
+
filters |= Q(**{f"{field}__icontains": search})
|
114
|
+
|
115
|
+
queryset = queryset.filter(filters)
|
116
|
+
|
117
|
+
return queryset
|
118
|
+
|
119
|
+
def get_field_value(self, obj, field: str):
|
120
|
+
try:
|
121
|
+
return super().get_field_value(obj, field)
|
122
|
+
except (AttributeError, TypeError):
|
123
|
+
return get_model_field(obj, field)
|
124
|
+
|
125
|
+
def get_field_value_template(self, obj, field: str, value):
|
126
|
+
templates = super().get_field_value_template(obj, field, value)
|
127
|
+
if hasattr(obj, f"get_{field}_display"):
|
128
|
+
# Insert before the last default template,
|
129
|
+
# so it can still be overriden by the user
|
130
|
+
templates.insert(-1, "admin/values/get_display.html")
|
131
|
+
return templates
|
132
|
+
|
133
|
+
|
134
|
+
class AdminModelDetailView(AdminDetailView):
|
135
|
+
model: "models.Model"
|
136
|
+
|
137
|
+
def get_title(self) -> str:
|
138
|
+
return str(self.object)
|
139
|
+
|
140
|
+
@classmethod
|
141
|
+
def get_slug(cls) -> str:
|
142
|
+
return f"{cls.model._meta.model_name}_detail"
|
143
|
+
|
144
|
+
@classmethod
|
145
|
+
def get_path(cls) -> str:
|
146
|
+
return f"{cls.model._meta.model_name}/<int:pk>/"
|
147
|
+
|
148
|
+
def get_fields(self):
|
149
|
+
if fields := super().get_fields():
|
150
|
+
return fields
|
151
|
+
|
152
|
+
return ["pk"] + [f.name for f in self.object._meta.get_fields() if f.concrete]
|
153
|
+
|
154
|
+
def get_field_value(self, obj, field: str):
|
155
|
+
try:
|
156
|
+
return super().get_field_value(obj, field)
|
157
|
+
except (AttributeError, TypeError):
|
158
|
+
return get_model_field(obj, field)
|
159
|
+
|
160
|
+
def get_object(self):
|
161
|
+
return self.model.objects.get(pk=self.url_kwargs["pk"])
|
162
|
+
|
163
|
+
def get_template_names(self) -> list[str]:
|
164
|
+
template_names = super().get_template_names()
|
165
|
+
|
166
|
+
if not self.template_name and isinstance(self.object, models.Model):
|
167
|
+
object_meta = self.object._meta
|
168
|
+
template_names = [
|
169
|
+
f"admin/{object_meta.package_label}/{object_meta.model_name}{self.template_name_suffix}.html"
|
170
|
+
] + template_names
|
171
|
+
|
172
|
+
return template_names
|
173
|
+
|
174
|
+
|
175
|
+
class AdminModelCreateView(AdminCreateView):
|
176
|
+
model: "models.Model"
|
177
|
+
form_class = None # TODO type annotation
|
178
|
+
|
179
|
+
def get_title(self) -> str:
|
180
|
+
if title := super().get_title():
|
181
|
+
return title
|
182
|
+
|
183
|
+
return f"New {self.model._meta.model_name}"
|
184
|
+
|
185
|
+
@classmethod
|
186
|
+
def get_slug(cls) -> str:
|
187
|
+
return f"{cls.model._meta.model_name}_create"
|
188
|
+
|
189
|
+
@classmethod
|
190
|
+
def get_path(cls) -> str:
|
191
|
+
return f"{cls.model._meta.model_name}/create/"
|
192
|
+
|
193
|
+
def get_template_names(self):
|
194
|
+
template_names = super().get_template_names()
|
195
|
+
|
196
|
+
if not self.template_name and issubclass(self.model, models.Model):
|
197
|
+
model_meta = self.model._meta
|
198
|
+
template_names = [
|
199
|
+
f"admin/{model_meta.package_label}/{model_meta.model_name}{self.template_name_suffix}.html"
|
200
|
+
] + template_names
|
201
|
+
|
202
|
+
return template_names
|
203
|
+
|
204
|
+
|
205
|
+
class AdminModelUpdateView(AdminUpdateView):
|
206
|
+
model: "models.Model"
|
207
|
+
form_class = None # TODO type annotation
|
208
|
+
success_url = "." # Redirect back to the same update page by default
|
209
|
+
|
210
|
+
def get_title(self) -> str:
|
211
|
+
if title := super().get_title():
|
212
|
+
return title
|
213
|
+
|
214
|
+
return f"Update {self.object}"
|
215
|
+
|
216
|
+
@classmethod
|
217
|
+
def get_slug(cls) -> str:
|
218
|
+
return f"{cls.model._meta.model_name}_update"
|
219
|
+
|
220
|
+
@classmethod
|
221
|
+
def get_path(cls) -> str:
|
222
|
+
return f"{cls.model._meta.model_name}/<int:pk>/update/"
|
223
|
+
|
224
|
+
def get_object(self):
|
225
|
+
return self.model.objects.get(pk=self.url_kwargs["pk"])
|
226
|
+
|
227
|
+
def get_template_names(self):
|
228
|
+
template_names = super().get_template_names()
|
229
|
+
|
230
|
+
if not self.template_name and isinstance(self.object, models.Model):
|
231
|
+
object_meta = self.object._meta
|
232
|
+
template_names = [
|
233
|
+
f"admin/{object_meta.package_label}/{object_meta.model_name}{self.template_name_suffix}.html"
|
234
|
+
] + template_names
|
235
|
+
|
236
|
+
return template_names
|
237
|
+
|
238
|
+
|
239
|
+
class AdminModelDeleteView(AdminDeleteView):
|
240
|
+
model: "models.Model"
|
241
|
+
|
242
|
+
def get_title(self) -> str:
|
243
|
+
return f"Delete {self.object}"
|
244
|
+
|
245
|
+
@classmethod
|
246
|
+
def get_slug(cls) -> str:
|
247
|
+
return f"{cls.model._meta.model_name}_delete"
|
248
|
+
|
249
|
+
@classmethod
|
250
|
+
def get_path(cls) -> str:
|
251
|
+
return f"{cls.model._meta.model_name}/<int:pk>/delete/"
|
252
|
+
|
253
|
+
def get_object(self):
|
254
|
+
return self.model.objects.get(pk=self.url_kwargs["pk"])
|
@@ -0,0 +1,399 @@
|
|
1
|
+
from plain.htmx.views import HTMXViewMixin
|
2
|
+
from plain.http import Response, ResponseRedirect
|
3
|
+
from plain.models import Model
|
4
|
+
from plain.paginator import Paginator
|
5
|
+
from plain.views import (
|
6
|
+
CreateView,
|
7
|
+
DeleteView,
|
8
|
+
DetailView,
|
9
|
+
UpdateView,
|
10
|
+
)
|
11
|
+
|
12
|
+
from .base import AdminView
|
13
|
+
|
14
|
+
|
15
|
+
class AdminListView(HTMXViewMixin, AdminView):
|
16
|
+
template_name = "admin/list.html"
|
17
|
+
fields: list[str]
|
18
|
+
actions: list[str] = []
|
19
|
+
displays: list[str] = []
|
20
|
+
page_size = 100
|
21
|
+
show_search = False
|
22
|
+
allow_global_search = False
|
23
|
+
|
24
|
+
def get_template_context(self):
|
25
|
+
context = super().get_template_context()
|
26
|
+
|
27
|
+
# Make this available on self for usage in get_objects and other methods
|
28
|
+
self.display = self.request.GET.get("display", "")
|
29
|
+
|
30
|
+
# Make this available to get_displays and stuff
|
31
|
+
self.objects = self.get_objects()
|
32
|
+
|
33
|
+
page_size = self.request.GET.get("page_size", self.page_size)
|
34
|
+
paginator = Paginator(self.objects, page_size)
|
35
|
+
self._page = paginator.get_page(self.request.GET.get("page", 1))
|
36
|
+
|
37
|
+
context["paginator"] = paginator
|
38
|
+
context["page"] = self._page
|
39
|
+
context["objects"] = self._page # alias
|
40
|
+
context["fields"] = self.get_fields()
|
41
|
+
context["actions"] = self.get_actions()
|
42
|
+
context["displays"] = self.get_displays()
|
43
|
+
|
44
|
+
context["current_display"] = self.display
|
45
|
+
|
46
|
+
# Implement search yourself in get_objects
|
47
|
+
context["search_query"] = self.request.GET.get("search", "")
|
48
|
+
context["show_search"] = self.show_search
|
49
|
+
|
50
|
+
context["table_style"] = getattr(self, "_table_style", "default")
|
51
|
+
|
52
|
+
context["get_object_pk"] = self.get_object_pk
|
53
|
+
context["get_field_value"] = self.get_field_value
|
54
|
+
context["get_field_value_template"] = self.get_field_value_template
|
55
|
+
|
56
|
+
context["get_object_url"] = self.get_object_url
|
57
|
+
context["get_object_links"] = self.get_object_links
|
58
|
+
|
59
|
+
return context
|
60
|
+
|
61
|
+
def get(self) -> Response:
|
62
|
+
if self.is_htmx_request:
|
63
|
+
hx_from_this_page = self.request.path in self.request.headers.get(
|
64
|
+
"HX-Current-Url", ""
|
65
|
+
)
|
66
|
+
if not hx_from_this_page:
|
67
|
+
self._table_style = "simple"
|
68
|
+
else:
|
69
|
+
hx_from_this_page = False
|
70
|
+
|
71
|
+
response = super().get()
|
72
|
+
|
73
|
+
if self.is_htmx_request and not hx_from_this_page and not self._page:
|
74
|
+
# Don't render anything
|
75
|
+
return Response(status=204)
|
76
|
+
|
77
|
+
return response
|
78
|
+
|
79
|
+
def post(self) -> Response:
|
80
|
+
# won't be "key" anymore, just list
|
81
|
+
action_name = self.request.POST.get("action_name")
|
82
|
+
actions = self.get_actions()
|
83
|
+
if action_name and action_name in actions:
|
84
|
+
target_pks = self.request.POST["action_pks"].split(",")
|
85
|
+
response = self.perform_action(action_name, target_pks)
|
86
|
+
if response:
|
87
|
+
return response
|
88
|
+
else:
|
89
|
+
# message in session first
|
90
|
+
return ResponseRedirect(".")
|
91
|
+
|
92
|
+
raise ValueError("Invalid action")
|
93
|
+
|
94
|
+
def perform_action(self, action: str, target_pks: list) -> Response | None:
|
95
|
+
raise NotImplementedError
|
96
|
+
|
97
|
+
def get_objects(self) -> list:
|
98
|
+
return []
|
99
|
+
|
100
|
+
def get_fields(self) -> list:
|
101
|
+
return (
|
102
|
+
self.fields.copy()
|
103
|
+
) # Avoid mutating the class attribute if using append etc
|
104
|
+
|
105
|
+
def get_actions(self) -> dict[str]:
|
106
|
+
return self.actions.copy() # Avoid mutating the class attribute itself
|
107
|
+
|
108
|
+
def get_displays(self) -> list[str]:
|
109
|
+
return self.displays.copy() # Avoid mutating the class attribute itself
|
110
|
+
|
111
|
+
def get_field_value(self, obj, field: str):
|
112
|
+
try:
|
113
|
+
# Try basic dict lookup first
|
114
|
+
if field in obj:
|
115
|
+
return obj[field]
|
116
|
+
except TypeError:
|
117
|
+
pass
|
118
|
+
|
119
|
+
# Try dot notation
|
120
|
+
if "." in field:
|
121
|
+
field, subfield = field.split(".", 1)
|
122
|
+
return self.get_field_value(obj[field], subfield)
|
123
|
+
|
124
|
+
# Try regular object attribute
|
125
|
+
attr = getattr(obj, field)
|
126
|
+
|
127
|
+
# Call if it's callable
|
128
|
+
if callable(attr):
|
129
|
+
return attr()
|
130
|
+
else:
|
131
|
+
return attr
|
132
|
+
|
133
|
+
def get_object_pk(self, obj):
|
134
|
+
try:
|
135
|
+
return self.get_field_value(obj, "pk")
|
136
|
+
except AttributeError:
|
137
|
+
return self.get_field_value(obj, "id")
|
138
|
+
|
139
|
+
def get_field_value_template(self, obj, field: str, value):
|
140
|
+
type_str = type(value).__name__.lower()
|
141
|
+
return [
|
142
|
+
f"admin/values/{type_str}.html", # Create a template per-type
|
143
|
+
f"admin/values/{field}.html", # Or for specific field names
|
144
|
+
"admin/values/default.html",
|
145
|
+
]
|
146
|
+
|
147
|
+
def get_list_url(self) -> str | None:
|
148
|
+
return None
|
149
|
+
|
150
|
+
def get_create_url(self) -> str | None:
|
151
|
+
return None
|
152
|
+
|
153
|
+
def get_detail_url(self, obj) -> str | None:
|
154
|
+
return None
|
155
|
+
|
156
|
+
def get_update_url(self, obj) -> str | None:
|
157
|
+
return None
|
158
|
+
|
159
|
+
def get_delete_url(self, obj) -> str | None:
|
160
|
+
return None
|
161
|
+
|
162
|
+
def get_object_url(self, obj) -> str | None:
|
163
|
+
if url := self.get_detail_url(obj):
|
164
|
+
return url
|
165
|
+
if url := self.get_update_url(obj):
|
166
|
+
return url
|
167
|
+
if url := self.get_delete_url(obj):
|
168
|
+
return url
|
169
|
+
return None
|
170
|
+
|
171
|
+
def get_object_links(self, obj) -> dict[str]:
|
172
|
+
links = {}
|
173
|
+
if self.get_detail_url(obj):
|
174
|
+
links["View"] = self.get_detail_url(obj)
|
175
|
+
if self.get_update_url(obj):
|
176
|
+
links["Edit"] = self.get_update_url(obj)
|
177
|
+
if self.get_delete_url(obj):
|
178
|
+
links["Delete"] = self.get_delete_url(obj)
|
179
|
+
return links
|
180
|
+
|
181
|
+
def get_links(self):
|
182
|
+
links = super().get_links()
|
183
|
+
|
184
|
+
# Not tied to a specific object
|
185
|
+
if create_url := self.get_create_url():
|
186
|
+
links["New"] = create_url
|
187
|
+
|
188
|
+
return links
|
189
|
+
|
190
|
+
|
191
|
+
class AdminCreateView(AdminView, CreateView):
|
192
|
+
template_name = None
|
193
|
+
|
194
|
+
def get_list_url(self) -> str | None:
|
195
|
+
return None
|
196
|
+
|
197
|
+
def get_create_url(self) -> str | None:
|
198
|
+
return None
|
199
|
+
|
200
|
+
def get_detail_url(self, obj) -> str | None:
|
201
|
+
return None
|
202
|
+
|
203
|
+
def get_update_url(self, obj) -> str | None:
|
204
|
+
return None
|
205
|
+
|
206
|
+
def get_delete_url(self, obj) -> str | None:
|
207
|
+
return None
|
208
|
+
|
209
|
+
def get_success_url(self, form):
|
210
|
+
if list_url := self.get_list_url():
|
211
|
+
return list_url
|
212
|
+
|
213
|
+
return super().get_success_url(form)
|
214
|
+
|
215
|
+
|
216
|
+
class AdminDetailView(AdminView, DetailView):
|
217
|
+
template_name = None
|
218
|
+
nav_section = ""
|
219
|
+
fields: list[str] = []
|
220
|
+
|
221
|
+
def get_template_context(self):
|
222
|
+
context = super().get_template_context()
|
223
|
+
context["get_field_value"] = self.get_field_value
|
224
|
+
context["get_field_value_template"] = self.get_field_value_template
|
225
|
+
context["fields"] = self.get_fields()
|
226
|
+
return context
|
227
|
+
|
228
|
+
def get_template_names(self) -> list[str]:
|
229
|
+
return super().get_template_names() + [
|
230
|
+
"admin/detail.html",
|
231
|
+
]
|
232
|
+
|
233
|
+
def get_description(self):
|
234
|
+
return repr(self.object)
|
235
|
+
|
236
|
+
def get_field_value(self, obj, field: str):
|
237
|
+
try:
|
238
|
+
# Try basic dict lookup first
|
239
|
+
if field in obj:
|
240
|
+
return obj[field]
|
241
|
+
except TypeError:
|
242
|
+
pass
|
243
|
+
|
244
|
+
# Try dot notation
|
245
|
+
if "." in field:
|
246
|
+
field, subfield = field.split(".", 1)
|
247
|
+
return self.get_field_value(obj[field], subfield)
|
248
|
+
|
249
|
+
# Try regular object attribute
|
250
|
+
attr = getattr(obj, field)
|
251
|
+
|
252
|
+
# Call if it's callable
|
253
|
+
if callable(attr):
|
254
|
+
return attr()
|
255
|
+
else:
|
256
|
+
return attr
|
257
|
+
|
258
|
+
def get_field_value_template(self, obj, field: str, value):
|
259
|
+
templates = []
|
260
|
+
|
261
|
+
# By type name
|
262
|
+
type_str = type(value).__name__.lower()
|
263
|
+
templates.append(f"admin/values/{type_str}.html")
|
264
|
+
|
265
|
+
# By field name
|
266
|
+
templates.append(f"admin/values/{field}.html")
|
267
|
+
|
268
|
+
# As any model
|
269
|
+
if isinstance(value, Model):
|
270
|
+
templates.append("admin/values/model.html")
|
271
|
+
|
272
|
+
# Default
|
273
|
+
templates.append("admin/values/default.html")
|
274
|
+
|
275
|
+
return templates
|
276
|
+
|
277
|
+
def get_list_url(self) -> str | None:
|
278
|
+
return None
|
279
|
+
|
280
|
+
def get_create_url(self) -> str | None:
|
281
|
+
return None
|
282
|
+
|
283
|
+
def get_detail_url(self, obj) -> str | None:
|
284
|
+
return None
|
285
|
+
|
286
|
+
def get_update_url(self, obj) -> str | None:
|
287
|
+
return None
|
288
|
+
|
289
|
+
def get_delete_url(self, obj) -> str | None:
|
290
|
+
return None
|
291
|
+
|
292
|
+
def get_fields(self):
|
293
|
+
return self.fields.copy() # Avoid mutating the class attribute itself
|
294
|
+
|
295
|
+
def get_links(self):
|
296
|
+
links = super().get_links()
|
297
|
+
|
298
|
+
if hasattr(self.object, "get_absolute_url"):
|
299
|
+
links["View in app"] = self.object.get_absolute_url()
|
300
|
+
|
301
|
+
if update_url := self.get_update_url(self.object):
|
302
|
+
links["Edit"] = update_url
|
303
|
+
|
304
|
+
if delete_url := self.get_delete_url(self.object):
|
305
|
+
links["Delete"] = delete_url
|
306
|
+
|
307
|
+
return links
|
308
|
+
|
309
|
+
def get_success_url(self, form):
|
310
|
+
if detail_url := self.get_detail_url(self.object):
|
311
|
+
return detail_url
|
312
|
+
|
313
|
+
if list_url := self.get_list_url():
|
314
|
+
return list_url
|
315
|
+
|
316
|
+
if update_url := self.get_update_url(self.object):
|
317
|
+
return update_url
|
318
|
+
|
319
|
+
return super().get_success_url(form)
|
320
|
+
|
321
|
+
|
322
|
+
class AdminUpdateView(AdminView, UpdateView):
|
323
|
+
template_name = None
|
324
|
+
nav_section = ""
|
325
|
+
|
326
|
+
def get_list_url(self) -> str | None:
|
327
|
+
return None
|
328
|
+
|
329
|
+
def get_create_url(self) -> str | None:
|
330
|
+
return None
|
331
|
+
|
332
|
+
def get_detail_url(self, obj) -> str | None:
|
333
|
+
return None
|
334
|
+
|
335
|
+
def get_update_url(self, obj) -> str | None:
|
336
|
+
return None
|
337
|
+
|
338
|
+
def get_delete_url(self, obj) -> str | None:
|
339
|
+
return None
|
340
|
+
|
341
|
+
def get_description(self):
|
342
|
+
return repr(self.object)
|
343
|
+
|
344
|
+
def get_links(self):
|
345
|
+
links = super().get_links()
|
346
|
+
|
347
|
+
if hasattr(self.object, "get_absolute_url"):
|
348
|
+
links["View in app"] = self.object.get_absolute_url()
|
349
|
+
|
350
|
+
if detail_url := self.get_detail_url(self.object):
|
351
|
+
links["View"] = detail_url
|
352
|
+
|
353
|
+
if delete_url := self.get_delete_url(self.object):
|
354
|
+
links["Delete"] = delete_url
|
355
|
+
|
356
|
+
return links
|
357
|
+
|
358
|
+
|
359
|
+
class AdminDeleteView(AdminView, DeleteView):
|
360
|
+
template_name = "admin/delete.html"
|
361
|
+
nav_section = ""
|
362
|
+
|
363
|
+
def get_description(self):
|
364
|
+
return repr(self.object)
|
365
|
+
|
366
|
+
def get_list_url(self) -> str | None:
|
367
|
+
return None
|
368
|
+
|
369
|
+
def get_create_url(self) -> str | None:
|
370
|
+
return None
|
371
|
+
|
372
|
+
def get_detail_url(self, obj) -> str | None:
|
373
|
+
return None
|
374
|
+
|
375
|
+
def get_update_url(self, obj) -> str | None:
|
376
|
+
return None
|
377
|
+
|
378
|
+
def get_delete_url(self, obj) -> str | None:
|
379
|
+
return None
|
380
|
+
|
381
|
+
def get_links(self):
|
382
|
+
links = super().get_links()
|
383
|
+
|
384
|
+
if hasattr(self.object, "get_absolute_url"):
|
385
|
+
links["View in app"] = self.object.get_absolute_url()
|
386
|
+
|
387
|
+
if detail_url := self.get_detail_url(self.object):
|
388
|
+
links["View"] = detail_url
|
389
|
+
|
390
|
+
if update_url := self.get_update_url(self.object):
|
391
|
+
links["Edit"] = update_url
|
392
|
+
|
393
|
+
return links
|
394
|
+
|
395
|
+
def get_success_url(self, form):
|
396
|
+
if list_url := self.get_list_url():
|
397
|
+
return list_url
|
398
|
+
|
399
|
+
return super().get_success_url(form)
|