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"])