Plain is headed towards 1.0! Subscribe for development updates →

  1from typing import TYPE_CHECKING
  2
  3from plain import models
  4from plain.models import Manager, Q
  5
  6from .objects import (
  7    AdminCreateView,
  8    AdminDeleteView,
  9    AdminDetailView,
 10    AdminListView,
 11    AdminUpdateView,
 12)
 13
 14if TYPE_CHECKING:
 15    from plain import models
 16
 17
 18def 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
 44class 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_path(cls) -> str:
 72        if path := super().get_path():
 73            return path
 74
 75        return f"{cls.model._meta.model_name}/"
 76
 77    def get_template_context(self):
 78        context = super().get_template_context()
 79
 80        order_by = self.request.GET.get("order_by", "")
 81        if order_by.startswith("-"):
 82            order_by_field = order_by[1:]
 83            order_by_direction = "-"
 84        else:
 85            order_by_field = order_by
 86            order_by_direction = ""
 87
 88        context["order_by_field"] = order_by_field
 89        context["order_by_direction"] = order_by_direction
 90
 91        return context
 92
 93    def get_objects(self):
 94        queryset = self.get_initial_queryset()
 95        queryset = self.order_queryset(queryset)
 96        queryset = self.search_queryset(queryset)
 97        return queryset
 98
 99    def get_initial_queryset(self):
100        # Separate override for the initial queryset
101        # so that annotations can be added BEFORE order_by, etc.
102        return self.model.objects.all()
103
104    def order_queryset(self, queryset):
105        if order_by := self.request.GET.get("order_by"):
106            queryset = queryset.order_by(order_by)
107        elif self.queryset_order:
108            queryset = queryset.order_by(*self.queryset_order)
109
110        return queryset
111
112    def search_queryset(self, queryset):
113        if search := self.request.GET.get("search"):
114            filters = Q()
115            for field in self.search_fields:
116                filters |= Q(**{f"{field}__icontains": search})
117
118            queryset = queryset.filter(filters)
119
120        return queryset
121
122    def get_field_value(self, obj, field: str):
123        try:
124            return super().get_field_value(obj, field)
125        except (AttributeError, TypeError):
126            return get_model_field(obj, field)
127
128    def get_field_value_template(self, obj, field: str, value):
129        templates = super().get_field_value_template(obj, field, value)
130        if hasattr(obj, f"get_{field}_display"):
131            # Insert before the last default template,
132            # so it can still be overriden by the user
133            templates.insert(-1, "admin/values/get_display.html")
134        return templates
135
136
137class AdminModelDetailView(AdminDetailView):
138    model: "models.Model"
139
140    def get_title(self) -> str:
141        return str(self.object)
142
143    @classmethod
144    def get_path(cls) -> str:
145        if path := super().get_path():
146            return path
147
148        return f"{cls.model._meta.model_name}/<int:pk>/"
149
150    def get_fields(self):
151        if fields := super().get_fields():
152            return fields
153
154        return ["pk"] + [f.name for f in self.object._meta.get_fields() if f.concrete]
155
156    def get_field_value(self, obj, field: str):
157        try:
158            return super().get_field_value(obj, field)
159        except (AttributeError, TypeError):
160            return get_model_field(obj, field)
161
162    def get_object(self):
163        return self.model.objects.get(pk=self.url_kwargs["pk"])
164
165    def get_template_names(self) -> list[str]:
166        template_names = super().get_template_names()
167
168        if not self.template_name and isinstance(self.object, models.Model):
169            object_meta = self.object._meta
170            template_names = [
171                f"admin/{object_meta.package_label}/{object_meta.model_name}{self.template_name_suffix}.html"
172            ] + template_names
173
174        return template_names
175
176
177class AdminModelCreateView(AdminCreateView):
178    model: "models.Model"
179    form_class = None  # TODO type annotation
180
181    def get_title(self) -> str:
182        if title := super().get_title():
183            return title
184
185        return f"New {self.model._meta.model_name}"
186
187    @classmethod
188    def get_path(cls) -> str:
189        if path := super().get_path():
190            return path
191
192        return f"{cls.model._meta.model_name}/create/"
193
194    def get_template_names(self):
195        template_names = super().get_template_names()
196
197        if not self.template_name and issubclass(self.model, models.Model):
198            model_meta = self.model._meta
199            template_names = [
200                f"admin/{model_meta.package_label}/{model_meta.model_name}{self.template_name_suffix}.html"
201            ] + template_names
202
203        return template_names
204
205
206class AdminModelUpdateView(AdminUpdateView):
207    model: "models.Model"
208    form_class = None  # TODO type annotation
209    success_url = "."  # Redirect back to the same update page by default
210
211    def get_title(self) -> str:
212        if title := super().get_title():
213            return title
214
215        return f"Update {self.object}"
216
217    @classmethod
218    def get_path(cls) -> str:
219        if path := super().get_path():
220            return path
221
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
239class 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_path(cls) -> str:
247        if path := super().get_path():
248            return path
249
250        return f"{cls.model._meta.model_name}/<int:pk>/delete/"
251
252    def get_object(self):
253        return self.model.objects.get(pk=self.url_kwargs["pk"])