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