1from plain.htmx.views import HTMXViewMixin
2from plain.http import Response, ResponseRedirect
3from plain.models import Model
4from plain.paginator import Paginator
5from plain.views import (
6 CreateView,
7 DeleteView,
8 DetailView,
9 UpdateView,
10)
11
12from .base import AdminView
13
14
15class 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
191class 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
216class 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
310class AdminUpdateView(AdminView, UpdateView):
311 template_name = None
312 nav_section = ""
313
314 def get_list_url(self) -> str | None:
315 return None
316
317 def get_create_url(self) -> str | None:
318 return None
319
320 def get_detail_url(self, obj) -> str | None:
321 return None
322
323 def get_update_url(self, obj) -> str | None:
324 return None
325
326 def get_delete_url(self, obj) -> str | None:
327 return None
328
329 def get_description(self):
330 return repr(self.object)
331
332 def get_links(self):
333 links = super().get_links()
334
335 if hasattr(self.object, "get_absolute_url"):
336 links["View in app"] = self.object.get_absolute_url()
337
338 if detail_url := self.get_detail_url(self.object):
339 links["View"] = detail_url
340
341 if delete_url := self.get_delete_url(self.object):
342 links["Delete"] = delete_url
343
344 return links
345
346 def get_success_url(self, form):
347 if detail_url := self.get_detail_url(self.object):
348 return detail_url
349
350 if list_url := self.get_list_url():
351 return list_url
352
353 if update_url := self.get_update_url(self.object):
354 return update_url
355
356 return super().get_success_url(form)
357
358
359class 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)