Plain is headed towards 1.0! Subscribe for development updates →

  1from typing import TYPE_CHECKING
  2
  3from plain.models import Q
  4from plain.urls import reverse_lazy
  5
  6from .base import URL_NAMESPACE, StaffDetailView, StaffListView, StaffUpdateView
  7
  8if TYPE_CHECKING:
  9    from plain import models
 10    from plain.views import View
 11
 12
 13def get_model_field(instance, field):
 14    if "__" in field:
 15        # Allow __ syntax like querysets use,
 16        # also automatically calling callables (like __date)
 17        result = instance
 18        for part in field.split("__"):
 19            result = getattr(result, part)
 20
 21            # If we hit a None, just return it
 22            if not result:
 23                return result
 24
 25            if callable(result):
 26                result = result()
 27
 28        return result
 29
 30    return getattr(instance, field)
 31
 32
 33class StaffModelListView(StaffListView):
 34    show_search = True
 35    allow_global_search = True
 36
 37    model: "models.Model"
 38
 39    fields: list = ["pk"]
 40    queryset_order = []
 41    search_fields: list = ["pk"]
 42
 43    @classmethod
 44    def get_title(cls) -> str:
 45        return getattr(cls, "title", cls.model._meta.model_name.capitalize() + "s")
 46
 47    @classmethod
 48    def get_slug(cls) -> str:
 49        return cls.model._meta.model_name
 50
 51    def get_template_context(self):
 52        context = super().get_template_context()
 53
 54        order_by = self.request.GET.get("order_by", "")
 55        if order_by.startswith("-"):
 56            order_by_field = order_by[1:]
 57            order_by_direction = "-"
 58        else:
 59            order_by_field = order_by
 60            order_by_direction = ""
 61
 62        context["order_by_field"] = order_by_field
 63        context["order_by_direction"] = order_by_direction
 64
 65        return context
 66
 67    def get_objects(self):
 68        queryset = self.get_initial_queryset()
 69        queryset = self.order_queryset(queryset)
 70        queryset = self.search_queryset(queryset)
 71        return queryset
 72
 73    def get_initial_queryset(self):
 74        # Separate override for the initial queryset
 75        # so that annotations can be added BEFORE order_by, etc.
 76        return self.model.objects.all()
 77
 78    def order_queryset(self, queryset):
 79        if order_by := self.request.GET.get("order_by"):
 80            queryset = queryset.order_by(order_by)
 81        elif self.queryset_order:
 82            queryset = queryset.order_by(*self.queryset_order)
 83
 84        return queryset
 85
 86    def search_queryset(self, queryset):
 87        if search := self.request.GET.get("search"):
 88            filters = Q()
 89            for field in self.search_fields:
 90                filters |= Q(**{f"{field}__icontains": search})
 91
 92            queryset = queryset.filter(filters)
 93
 94        return queryset
 95
 96    def get_field_value(self, obj, field: str):
 97        return get_model_field(obj, field)
 98
 99    def get_field_value_template(self, obj, field: str, value):
100        templates = super().get_field_value_template(obj, field, value)
101        if hasattr(obj, f"get_{field}_display"):
102            # Insert before the last default template,
103            # so it can still be overriden by the user
104            templates.insert(-1, "staff/values/get_display.html")
105        return templates
106
107
108class StaffModelDetailView(StaffDetailView):
109    model: "models.Model"
110    fields: list = []
111
112    @classmethod
113    def get_title(cls) -> str:
114        return getattr(cls, "title", cls.model._meta.model_name.capitalize())
115
116    @classmethod
117    def get_slug(cls) -> str:
118        return f"{cls.model._meta.model_name}_detail"
119
120    @classmethod
121    def get_path(cls) -> str:
122        return f"{cls.model._meta.model_name}/<int:pk>/"
123
124    def get_template_context(self):
125        context = super().get_template_context()
126        context["fields"] = self.fields or ["pk"] + [
127            f.name for f in self.object._meta.get_fields() if not f.remote_field
128        ]
129        return context
130
131    def get_field_value(self, obj, field: str):
132        return get_model_field(obj, field)
133
134    def get_object(self):
135        return self.model.objects.get(pk=self.url_kwargs["pk"])
136
137    def get_template_names(self) -> list[str]:
138        return super().get_template_names() + [
139            "staff/detail.html",
140        ]
141
142    def get_links(self):
143        links = super().get_links()
144        if hasattr(self.object, "get_absolute_url"):
145            links["View in app"] = self.object.get_absolute_url()
146        if update_url := self.get_update_url(self.object):
147            links["Update"] = update_url
148        return links
149
150
151class StaffModelUpdateView(StaffUpdateView):
152    model: "models.Model"
153    form_class = None  # TODO type annotation
154    success_url = "."  # Redirect back to the same update page by default
155
156    @classmethod
157    def get_title(cls) -> str:
158        return getattr(cls, "title", f"Update {cls.model._meta.model_name}")
159
160    @classmethod
161    def get_slug(cls) -> str:
162        return f"{cls.model._meta.model_name}_update"
163
164    @classmethod
165    def get_path(cls) -> str:
166        return f"{cls.model._meta.model_name}/<int:pk>/update/"
167
168    def get_object(self):
169        return self.model.objects.get(pk=self.url_kwargs["pk"])
170
171    def get_links(self):
172        links = super().get_links()
173        if hasattr(self.object, "get_absolute_url"):
174            links["View in app"] = self.object.get_absolute_url()
175        if detail_url := self.get_detail_url(self.object):
176            links["Detail"] = detail_url
177        return links
178
179
180class StaffModelViewset:
181    @classmethod
182    def get_views(cls) -> list["View"]:
183        views = []
184
185        if hasattr(cls, "ListView") and hasattr(cls, "DetailView"):
186            cls.ListView.get_detail_url = lambda self, obj: reverse_lazy(
187                f"{URL_NAMESPACE}:{cls.DetailView.view_name()}",
188                kwargs={"pk": obj.pk},
189            )
190
191            cls.DetailView.parent_view_class = cls.ListView
192
193        if hasattr(cls, "ListView") and hasattr(cls, "UpdateView"):
194            cls.ListView.get_update_url = lambda self, obj: reverse_lazy(
195                f"{URL_NAMESPACE}:{cls.UpdateView.view_name()}",
196                kwargs={"pk": obj.pk},
197            )
198
199            cls.UpdateView.parent_view_class = cls.ListView
200
201        if hasattr(cls, "DetailView") and hasattr(cls, "UpdateView"):
202            cls.DetailView.get_update_url = lambda self, obj: reverse_lazy(
203                f"{URL_NAMESPACE}:{cls.UpdateView.view_name()}",
204                kwargs={"pk": obj.pk},
205            )
206
207            cls.UpdateView.get_detail_url = lambda self, obj: reverse_lazy(
208                f"{URL_NAMESPACE}:{cls.DetailView.view_name()}",
209                kwargs={"pk": obj.pk},
210            )
211
212        if hasattr(cls, "ListView"):
213            views.append(cls.ListView)
214
215        if hasattr(cls, "DetailView"):
216            views.append(cls.DetailView)
217
218        if hasattr(cls, "UpdateView"):
219            views.append(cls.UpdateView)
220
221        return views