Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/polymorphic/admin/parentadmin.py: 29%
178 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"""
2The parent admin displays the list view of the base model.
3"""
4from django.contrib import admin
5from django.contrib.admin.helpers import AdminErrorList, AdminForm
6from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
7from django.contrib.contenttypes.models import ContentType
8from django.core.exceptions import ImproperlyConfigured, PermissionDenied
9from django.db import models
10from django.http import Http404, HttpResponseRedirect
11from django.template.response import TemplateResponse
12from django.urls import URLResolver
13from django.utils.encoding import force_str
14from django.utils.http import urlencode
15from django.utils.safestring import mark_safe
16from django.utils.translation import gettext_lazy as _
18from polymorphic.utils import get_base_polymorphic_model
20from .forms import PolymorphicModelChoiceForm
23class RegistrationClosed(RuntimeError):
24 "The admin model can't be registered anymore at this point."
27class ChildAdminNotRegistered(RuntimeError):
28 "The admin site for the model is not registered."
31class PolymorphicParentModelAdmin(admin.ModelAdmin):
32 """
33 A admin interface that can displays different change/delete pages, depending on the polymorphic model.
34 To use this class, one attribute need to be defined:
36 * :attr:`child_models` should be a list models.
38 Alternatively, the following methods can be implemented:
40 * :func:`get_child_models` should return a list of models.
41 * optionally, :func:`get_child_type_choices` can be overwritten to refine the choices for the add dialog.
43 This class needs to be inherited by the model admin base class that is registered in the site.
44 The derived models should *not* register the ModelAdmin, but instead it should be returned by :func:`get_child_models`.
45 """
47 #: The base model that the class uses (auto-detected if not set explicitly)
48 base_model = None
50 #: The child models that should be displayed
51 child_models = None
53 #: Whether the list should be polymorphic too, leave to ``False`` to optimize
54 polymorphic_list = False
56 add_type_template = None
57 add_type_form = PolymorphicModelChoiceForm
59 #: The regular expression to filter the primary key in the URL.
60 #: This accepts only numbers as defensive measure against catch-all URLs.
61 #: If your primary key consists of string values, update this regular expression.
62 pk_regex = r"(\d+|__fk__)"
64 def __init__(self, model, admin_site, *args, **kwargs):
65 super().__init__(model, admin_site, *args, **kwargs)
66 self._is_setup = False
68 if self.base_model is None: 68 ↛ 69line 68 didn't jump to line 69, because the condition on line 68 was never true
69 self.base_model = get_base_polymorphic_model(model)
71 def _lazy_setup(self):
72 if self._is_setup: 72 ↛ 73line 72 didn't jump to line 73, because the condition on line 72 was never true
73 return
75 self._child_models = self.get_child_models()
77 # Make absolutely sure that the child models don't use the old 0.9 format,
78 # as of polymorphic 1.4 this deprecated configuration is no longer supported.
79 # Instead, register the child models in the admin too.
80 if self._child_models and not issubclass(self._child_models[0], models.Model): 80 ↛ 81line 80 didn't jump to line 81, because the condition on line 80 was never true
81 raise ImproperlyConfigured(
82 "Since django-polymorphic 1.4, the `child_models` attribute "
83 "and `get_child_models()` method should be a list of models only.\n"
84 "The model-admin class should be registered in the regular Django admin."
85 )
87 self._child_admin_site = self.admin_site
88 self._is_setup = True
90 def register_child(self, model, model_admin):
91 """
92 Register a model with admin to display.
93 """
94 # After the get_urls() is called, the URLs of the child model can't be exposed anymore to the Django URLconf,
95 # which also means that a "Save and continue editing" button won't work.
96 if self._is_setup:
97 raise RegistrationClosed("The admin model can't be registered anymore at this point.")
99 if not issubclass(model, self.base_model):
100 raise TypeError(
101 "{} should be a subclass of {}".format(model.__name__, self.base_model.__name__)
102 )
103 if not issubclass(model_admin, admin.ModelAdmin):
104 raise TypeError(
105 "{} should be a subclass of {}".format(
106 model_admin.__name__, admin.ModelAdmin.__name__
107 )
108 )
110 self._child_admin_site.register(model, model_admin)
112 def get_child_models(self):
113 """
114 Return the derived model classes which this admin should handle.
115 This should return a list of tuples, exactly like :attr:`child_models` is.
117 The model classes can be retrieved as ``base_model.__subclasses__()``,
118 a setting in a config file, or a query of a plugin registration system at your option
119 """
120 if self.child_models is None: 120 ↛ 121line 120 didn't jump to line 121, because the condition on line 120 was never true
121 raise NotImplementedError("Implement get_child_models() or child_models")
123 return self.child_models
125 def get_child_type_choices(self, request, action):
126 """
127 Return a list of polymorphic types for which the user has the permission to perform the given action.
128 """
129 self._lazy_setup()
130 choices = []
131 content_types = ContentType.objects.get_for_models(
132 *self.get_child_models(), for_concrete_models=False
133 )
135 for model, ct in content_types.items():
136 perm_function_name = f"has_{action}_permission"
137 model_admin = self._get_real_admin_by_model(model)
138 perm_function = getattr(model_admin, perm_function_name)
139 if not perm_function(request):
140 continue
141 choices.append((ct.id, model._meta.verbose_name))
142 return choices
144 def _get_real_admin(self, object_id, super_if_self=True):
145 try:
146 obj = (
147 self.model.objects.non_polymorphic().values("polymorphic_ctype").get(pk=object_id)
148 )
149 except self.model.DoesNotExist:
150 raise Http404
151 return self._get_real_admin_by_ct(obj["polymorphic_ctype"], super_if_self=super_if_self)
153 def _get_real_admin_by_ct(self, ct_id, super_if_self=True):
154 try:
155 ct = ContentType.objects.get_for_id(ct_id)
156 except ContentType.DoesNotExist as e:
157 raise Http404(e) # Handle invalid GET parameters
159 model_class = ct.model_class()
160 if not model_class:
161 # Handle model deletion
162 raise Http404("No model found for '{}.{}'.".format(*ct.natural_key()))
164 return self._get_real_admin_by_model(model_class, super_if_self=super_if_self)
166 def _get_real_admin_by_model(self, model_class, super_if_self=True):
167 # In case of a ?ct_id=### parameter, the view is already checked for permissions.
168 # Hence, make sure this is a derived object, or risk exposing other admin interfaces.
169 if model_class not in self._child_models:
170 raise PermissionDenied(
171 "Invalid model '{}', it must be registered as child model.".format(model_class)
172 )
174 try:
175 # HACK: the only way to get the instance of an model admin,
176 # is to read the registry of the AdminSite.
177 real_admin = self._child_admin_site._registry[model_class]
178 except KeyError:
179 raise ChildAdminNotRegistered(
180 "No child admin site was registered for a '{}' model.".format(model_class)
181 )
183 if super_if_self and real_admin is self:
184 return super()
185 else:
186 return real_admin
188 def get_queryset(self, request):
189 # optimize the list display.
190 qs = super().get_queryset(request)
191 if not self.polymorphic_list:
192 qs = qs.non_polymorphic()
193 return qs
195 def add_view(self, request, form_url="", extra_context=None):
196 """Redirect the add view to the real admin."""
197 ct_id = int(request.GET.get("ct_id", 0))
198 if not ct_id:
199 # Display choices
200 return self.add_type_view(request)
201 else:
202 real_admin = self._get_real_admin_by_ct(ct_id)
203 # rebuild form_url, otherwise libraries below will override it.
204 form_url = add_preserved_filters(
205 {
206 "preserved_filters": urlencode({"ct_id": ct_id}),
207 "opts": self.model._meta,
208 },
209 form_url,
210 )
211 return real_admin.add_view(request, form_url, extra_context)
213 def change_view(self, request, object_id, *args, **kwargs):
214 """Redirect the change view to the real admin."""
215 real_admin = self._get_real_admin(object_id)
216 return real_admin.change_view(request, object_id, *args, **kwargs)
218 def changeform_view(self, request, object_id=None, *args, **kwargs):
219 # The `changeform_view` is available as of Django 1.7, combining the add_view and change_view.
220 # As it's directly called by django-reversion, this method is also overwritten to make sure it
221 # also redirects to the child admin.
222 if object_id:
223 real_admin = self._get_real_admin(object_id)
224 return real_admin.changeform_view(request, object_id, *args, **kwargs)
225 else:
226 # Add view. As it should already be handled via `add_view`, this means something custom is done here!
227 return super().changeform_view(request, object_id, *args, **kwargs)
229 def history_view(self, request, object_id, extra_context=None):
230 """Redirect the history view to the real admin."""
231 real_admin = self._get_real_admin(object_id)
232 return real_admin.history_view(request, object_id, extra_context=extra_context)
234 def delete_view(self, request, object_id, extra_context=None):
235 """Redirect the delete view to the real admin."""
236 real_admin = self._get_real_admin(object_id)
237 return real_admin.delete_view(request, object_id, extra_context)
239 def get_preserved_filters(self, request):
240 if "_changelist_filters" in request.GET:
241 request.GET = request.GET.copy()
242 filters = request.GET.get("_changelist_filters")
243 f = filters.split("&")
244 for x in f:
245 c = x.split("=")
246 request.GET[c[0]] = c[1]
247 del request.GET["_changelist_filters"]
248 return super().get_preserved_filters(request)
250 def get_urls(self):
251 """
252 Expose the custom URLs for the subclasses and the URL resolver.
253 """
254 urls = super().get_urls()
256 # At this point. all admin code needs to be known.
257 self._lazy_setup()
259 return urls
261 def subclass_view(self, request, path):
262 """
263 Forward any request to a custom view of the real admin.
264 """
265 ct_id = int(request.GET.get("ct_id", 0))
266 if not ct_id:
267 # See if the path started with an ID.
268 try:
269 pos = path.find("/")
270 if pos == -1:
271 object_id = int(path)
272 else:
273 object_id = int(path[0:pos])
274 except ValueError:
275 raise Http404(
276 "No ct_id parameter, unable to find admin subclass for path '{}'.".format(path)
277 )
279 ct_id = self.model.objects.values_list("polymorphic_ctype_id", flat=True).get(
280 pk=object_id
281 )
283 real_admin = self._get_real_admin_by_ct(ct_id)
284 resolver = URLResolver("^", real_admin.urls)
285 resolvermatch = resolver.resolve(path) # May raise Resolver404
286 if not resolvermatch:
287 raise Http404(f"No match for path '{path}' in admin subclass.")
289 return resolvermatch.func(request, *resolvermatch.args, **resolvermatch.kwargs)
291 def add_type_view(self, request, form_url=""):
292 """
293 Display a choice form to select which page type to add.
294 """
295 if not self.has_add_permission(request):
296 raise PermissionDenied
298 extra_qs = ""
299 if request.META["QUERY_STRING"]:
300 # QUERY_STRING is bytes in Python 3, using force_str() to decode it as string.
301 # See QueryDict how Django deals with that.
302 extra_qs = "&{}".format(force_str(request.META["QUERY_STRING"]))
304 choices = self.get_child_type_choices(request, "add")
305 if len(choices) == 0:
306 raise PermissionDenied
307 if len(choices) == 1:
308 return HttpResponseRedirect(f"?ct_id={choices[0][0]}{extra_qs}")
310 # Create form
311 form = self.add_type_form(
312 data=request.POST if request.method == "POST" else None,
313 initial={"ct_id": choices[0][0]},
314 )
315 form.fields["ct_id"].choices = choices
317 if form.is_valid():
318 return HttpResponseRedirect("?ct_id={}{}".format(form.cleaned_data["ct_id"], extra_qs))
320 # Wrap in all admin layout
321 fieldsets = ((None, {"fields": ("ct_id",)}),)
322 adminForm = AdminForm(form, fieldsets, {}, model_admin=self)
323 media = self.media + adminForm.media
324 opts = self.model._meta
326 context = {
327 "title": _("Add %s") % force_str(opts.verbose_name),
328 "adminform": adminForm,
329 "is_popup": ("_popup" in request.POST or "_popup" in request.GET),
330 "media": mark_safe(media),
331 "errors": AdminErrorList(form, ()),
332 "app_label": opts.app_label,
333 }
334 return self.render_add_type_form(request, context, form_url)
336 def render_add_type_form(self, request, context, form_url=""):
337 """
338 Render the page type choice form.
339 """
340 opts = self.model._meta
341 app_label = opts.app_label
342 context.update(
343 {
344 "has_change_permission": self.has_change_permission(request),
345 "form_url": mark_safe(form_url),
346 "opts": opts,
347 "add": True,
348 "save_on_top": self.save_on_top,
349 }
350 )
352 templates = self.add_type_template or [
353 f"admin/{app_label}/{opts.object_name.lower()}/add_type_form.html",
354 "admin/%s/add_type_form.html" % app_label,
355 "admin/polymorphic/add_type_form.html", # added default here
356 "admin/add_type_form.html",
357 ]
359 request.current_app = self.admin_site.name
360 return TemplateResponse(request, templates, context)
362 @property
363 def change_list_template(self):
364 opts = self.model._meta
365 app_label = opts.app_label
367 # Pass the base options
368 base_opts = self.base_model._meta
369 base_app_label = base_opts.app_label
371 return [
372 f"admin/{app_label}/{opts.object_name.lower()}/change_list.html",
373 "admin/%s/change_list.html" % app_label,
374 # Added base class:
375 "admin/%s/%s/change_list.html" % (base_app_label, base_opts.object_name.lower()),
376 "admin/%s/change_list.html" % base_app_label,
377 "admin/change_list.html",
378 ]