Plain is headed towards 1.0! Subscribe for development updates →

  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)