Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/polymorphic/admin/childadmin.py: 30%
99 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 child admin displays the change/delete view of the subclass model.
3"""
4import inspect
6from django.contrib import admin
7from django.urls import resolve
8from django.utils.translation import gettext_lazy as _
10from polymorphic.utils import get_base_polymorphic_model
12from ..admin import PolymorphicParentModelAdmin
15class ParentAdminNotRegistered(RuntimeError):
16 "The admin site for the model is not registered."
19class PolymorphicChildModelAdmin(admin.ModelAdmin):
20 """
21 The *optional* base class for the admin interface of derived models.
23 This base class defines some convenience behavior for the admin interface:
25 * It corrects the breadcrumbs in the admin pages.
26 * It adds the base model to the template lookup paths.
27 * It allows to set ``base_form`` so the derived class will automatically include other fields in the form.
28 * It allows to set ``base_fieldsets`` so the derived class will automatically display any extra fields.
29 """
31 #: The base model that the class uses (auto-detected if not set explicitly)
32 base_model = None
34 #: By setting ``base_form`` instead of ``form``, any subclass fields are automatically added to the form.
35 #: This is useful when your model admin class is inherited by others.
36 base_form = None
38 #: By setting ``base_fieldsets`` instead of ``fieldsets``,
39 #: any subclass fields can be automatically added.
40 #: This is useful when your model admin class is inherited by others.
41 base_fieldsets = None
43 #: Default title for extra fieldset
44 extra_fieldset_title = _("Contents")
46 #: Whether the child admin model should be visible in the admin index page.
47 show_in_index = False
49 def __init__(self, model, admin_site, *args, **kwargs):
50 super().__init__(model, admin_site, *args, **kwargs)
52 if self.base_model is None: 52 ↛ 53line 52 didn't jump to line 53, because the condition on line 52 was never true
53 self.base_model = get_base_polymorphic_model(model)
55 def get_form(self, request, obj=None, **kwargs):
56 # The django admin validation requires the form to have a 'class Meta: model = ..'
57 # attribute, or it will complain that the fields are missing.
58 # However, this enforces all derived ModelAdmin classes to redefine the model as well,
59 # because they need to explicitly set the model again - it will stick with the base model.
60 #
61 # Instead, pass the form unchecked here, because the standard ModelForm will just work.
62 # If the derived class sets the model explicitly, respect that setting.
63 kwargs.setdefault("form", self.base_form or self.form)
65 # prevent infinite recursion when this is called from get_subclass_fields
66 if not self.fieldsets and not self.fields:
67 kwargs.setdefault("fields", "__all__")
69 return super().get_form(request, obj, **kwargs)
71 def get_model_perms(self, request):
72 match = resolve(request.path_info)
74 if (
75 not self.show_in_index
76 and match.app_name == "admin"
77 and match.url_name in ("index", "app_list")
78 ):
79 return {"add": False, "change": False, "delete": False}
80 return super().get_model_perms(request)
82 @property
83 def change_form_template(self):
84 opts = self.model._meta
85 app_label = opts.app_label
87 # Pass the base options
88 base_opts = self.base_model._meta
89 base_app_label = base_opts.app_label
91 return [
92 f"admin/{app_label}/{opts.object_name.lower()}/change_form.html",
93 "admin/%s/change_form.html" % app_label,
94 # Added:
95 "admin/%s/%s/change_form.html" % (base_app_label, base_opts.object_name.lower()),
96 "admin/%s/change_form.html" % base_app_label,
97 "admin/polymorphic/change_form.html",
98 "admin/change_form.html",
99 ]
101 @property
102 def delete_confirmation_template(self):
103 opts = self.model._meta
104 app_label = opts.app_label
106 # Pass the base options
107 base_opts = self.base_model._meta
108 base_app_label = base_opts.app_label
110 return [
111 "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()),
112 "admin/%s/delete_confirmation.html" % app_label,
113 # Added:
114 "admin/%s/%s/delete_confirmation.html"
115 % (base_app_label, base_opts.object_name.lower()),
116 "admin/%s/delete_confirmation.html" % base_app_label,
117 "admin/polymorphic/delete_confirmation.html",
118 "admin/delete_confirmation.html",
119 ]
121 @property
122 def object_history_template(self):
123 opts = self.model._meta
124 app_label = opts.app_label
126 # Pass the base options
127 base_opts = self.base_model._meta
128 base_app_label = base_opts.app_label
130 return [
131 f"admin/{app_label}/{opts.object_name.lower()}/object_history.html",
132 "admin/%s/object_history.html" % app_label,
133 # Added:
134 "admin/%s/%s/object_history.html" % (base_app_label, base_opts.object_name.lower()),
135 "admin/%s/object_history.html" % base_app_label,
136 "admin/polymorphic/object_history.html",
137 "admin/object_history.html",
138 ]
140 def _get_parent_admin(self):
141 # this returns parent admin instance on which to call response_post_save methods
142 parent_model = self.model._meta.get_field("polymorphic_ctype").model
143 if parent_model == self.model:
144 # when parent_model is in among child_models, just return super instance
145 return super()
147 try:
148 return self.admin_site._registry[parent_model]
149 except KeyError:
150 # Admin is not registered for polymorphic_ctype model, but perhaps it's registered
151 # for a intermediate proxy model, between the parent_model and this model.
152 for klass in inspect.getmro(self.model):
153 if not issubclass(klass, parent_model):
154 continue # e.g. found a mixin.
156 # Fetch admin instance for model class, see if it's a possible candidate.
157 model_admin = self.admin_site._registry.get(klass)
158 if model_admin is not None and isinstance(
159 model_admin, PolymorphicParentModelAdmin
160 ):
161 return model_admin # Success!
163 # If we get this far without returning there is no admin available
164 raise ParentAdminNotRegistered(
165 f"No parent admin was registered for a '{parent_model}' model."
166 )
168 def response_post_save_add(self, request, obj):
169 return self._get_parent_admin().response_post_save_add(request, obj)
171 def response_post_save_change(self, request, obj):
172 return self._get_parent_admin().response_post_save_change(request, obj)
174 def render_change_form(self, request, context, add=False, change=False, form_url="", obj=None):
175 context.update({"base_opts": self.base_model._meta})
176 return super().render_change_form(
177 request, context, add=add, change=change, form_url=form_url, obj=obj
178 )
180 def delete_view(self, request, object_id, context=None):
181 extra_context = {"base_opts": self.base_model._meta}
182 return super().delete_view(request, object_id, extra_context)
184 def history_view(self, request, object_id, extra_context=None):
185 # Make sure the history view can also display polymorphic breadcrumbs
186 context = {"base_opts": self.base_model._meta}
187 if extra_context:
188 context.update(extra_context)
189 return super().history_view(request, object_id, extra_context=context)
191 # ---- Extra: improving the form/fieldset default display ----
193 def get_base_fieldsets(self, request, obj=None):
194 return self.base_fieldsets
196 def get_fieldsets(self, request, obj=None):
197 base_fieldsets = self.get_base_fieldsets(request, obj)
199 # If subclass declares fieldsets or fields, this is respected
200 if self.fieldsets or self.fields or not self.base_fieldsets:
201 return super().get_fieldsets(request, obj)
203 # Have a reasonable default fieldsets,
204 # where the subclass fields are automatically included.
205 other_fields = self.get_subclass_fields(request, obj)
207 if other_fields:
208 return (
209 base_fieldsets[0],
210 (self.extra_fieldset_title, {"fields": other_fields}),
211 ) + base_fieldsets[1:]
212 else:
213 return base_fieldsets
215 def get_subclass_fields(self, request, obj=None):
216 # Find out how many fields would really be on the form,
217 # if it weren't restricted by declared fields.
218 exclude = list(self.exclude or [])
219 exclude.extend(self.get_readonly_fields(request, obj))
221 # By not declaring the fields/form in the base class,
222 # get_form() will populate the form with all available fields.
223 form = self.get_form(request, obj, exclude=exclude)
224 subclass_fields = list(form.base_fields.keys()) + list(
225 self.get_readonly_fields(request, obj)
226 )
228 # Find which fields are not part of the common fields.
229 for fieldset in self.get_base_fieldsets(request, obj):
230 for field in fieldset[1]["fields"]:
231 try:
232 subclass_fields.remove(field)
233 except ValueError:
234 pass # field not found in form, Django will raise exception later.
235 return subclass_fields