Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/polymorphic/admin/inlines.py: 31%
92 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"""
2Django Admin support for polymorphic inlines.
4Each row in the inline can correspond with a different subclass.
5"""
6from functools import partial
8from django.conf import settings
9from django.contrib.admin.options import InlineModelAdmin
10from django.contrib.admin.utils import flatten_fieldsets
11from django.core.exceptions import ImproperlyConfigured
12from django.forms import Media
14from polymorphic.formsets import (
15 BasePolymorphicInlineFormSet,
16 PolymorphicFormSetChild,
17 UnsupportedChildType,
18 polymorphic_child_forms_factory,
19)
20from polymorphic.formsets.utils import add_media
22from .helpers import PolymorphicInlineSupportMixin
25class PolymorphicInlineModelAdmin(InlineModelAdmin):
26 """
27 A polymorphic inline, where each formset row can be a different form.
29 Note that:
31 * Permissions are only checked on the base model.
32 * The child inlines can't override the base model fields, only this parent inline can do that.
33 """
35 formset = BasePolymorphicInlineFormSet
37 #: The extra media to add for the polymorphic inlines effect.
38 #: This can be redefined for subclasses.
39 polymorphic_media = Media(
40 js=(
41 "admin/js/vendor/jquery/jquery{}.js".format("" if settings.DEBUG else ".min"),
42 "admin/js/jquery.init.js",
43 "polymorphic/js/polymorphic_inlines.js",
44 ),
45 css={"all": ("polymorphic/css/polymorphic_inlines.css",)},
46 )
48 #: The extra forms to show
49 #: By default there are no 'extra' forms as the desired type is unknown.
50 #: Instead, add each new item using JavaScript that first offers a type-selection.
51 extra = 0
53 #: Inlines for all model sub types that can be displayed in this inline.
54 #: Each row is a :class:`PolymorphicInlineModelAdmin.Child`
55 child_inlines = ()
57 def __init__(self, parent_model, admin_site):
58 super().__init__(parent_model, admin_site)
60 # Extra check to avoid confusion
61 # While we could monkeypatch the admin here, better stay explicit.
62 parent_admin = admin_site._registry.get(parent_model, None)
63 if parent_admin is not None: # Can be None during check
64 if not isinstance(parent_admin, PolymorphicInlineSupportMixin):
65 raise ImproperlyConfigured(
66 "To use polymorphic inlines, add the `PolymorphicInlineSupportMixin` mixin "
67 "to the ModelAdmin that hosts the inline."
68 )
70 # While the inline is created per request, the 'request' object is not known here.
71 # Hence, creating all child inlines unconditionally, without checking permissions.
72 self.child_inline_instances = self.get_child_inline_instances()
74 # Create a lookup table
75 self._child_inlines_lookup = {}
76 for child_inline in self.child_inline_instances:
77 self._child_inlines_lookup[child_inline.model] = child_inline
79 def get_child_inline_instances(self):
80 """
81 :rtype List[PolymorphicInlineModelAdmin.Child]
82 """
83 instances = []
84 for ChildInlineType in self.child_inlines:
85 instances.append(ChildInlineType(parent_inline=self))
86 return instances
88 def get_child_inline_instance(self, model):
89 """
90 Find the child inline for a given model.
92 :rtype: PolymorphicInlineModelAdmin.Child
93 """
94 try:
95 return self._child_inlines_lookup[model]
96 except KeyError:
97 raise UnsupportedChildType(f"Model '{model.__name__}' not found in child_inlines")
99 def get_formset(self, request, obj=None, **kwargs):
100 """
101 Construct the inline formset class.
103 This passes all class attributes to the formset.
105 :rtype: type
106 """
107 # Construct the FormSet class
108 FormSet = super().get_formset(request, obj=obj, **kwargs)
110 # Instead of completely redefining super().get_formset(), we use
111 # the regular inlineformset_factory(), and amend that with our extra bits.
112 # This code line is the essence of what polymorphic_inlineformset_factory() does.
113 FormSet.child_forms = polymorphic_child_forms_factory(
114 formset_children=self.get_formset_children(request, obj=obj)
115 )
116 return FormSet
118 def get_formset_children(self, request, obj=None):
119 """
120 The formset 'children' provide the details for all child models that are part of this formset.
121 It provides a stripped version of the modelform/formset factory methods.
122 """
123 formset_children = []
124 for child_inline in self.child_inline_instances:
125 # TODO: the children can be limited here per request based on permissions.
126 formset_children.append(child_inline.get_formset_child(request, obj=obj))
127 return formset_children
129 def get_fieldsets(self, request, obj=None):
130 """
131 Hook for specifying fieldsets.
132 """
133 if self.fieldsets:
134 return self.fieldsets
135 else:
136 return [] # Avoid exposing fields to the child
138 def get_fields(self, request, obj=None):
139 if self.fields:
140 return self.fields
141 else:
142 return [] # Avoid exposing fields to the child
144 @property
145 def media(self):
146 # The media of the inline focuses on the admin settings,
147 # whether to expose the scripts for filter_horizontal etc..
148 # The admin helper exposes the inline + formset media.
149 base_media = super().media
150 all_media = Media()
151 add_media(all_media, base_media)
153 # Add all media of the child inline instances
154 for child_instance in self.child_inline_instances:
155 child_media = child_instance.media
157 # Avoid adding the same media object again and again
158 if child_media._css != base_media._css and child_media._js != base_media._js:
159 add_media(all_media, child_media)
161 add_media(all_media, self.polymorphic_media)
163 return all_media
165 class Child(InlineModelAdmin):
166 """
167 The child inline; which allows configuring the admin options
168 for the child appearance.
170 Note that not all options will be honored by the parent, notably the formset options:
171 * :attr:`extra`
172 * :attr:`min_num`
173 * :attr:`max_num`
175 The model form options however, will all be read.
176 """
178 formset_child = PolymorphicFormSetChild
179 extra = 0 # TODO: currently unused for the children.
181 def __init__(self, parent_inline):
182 self.parent_inline = parent_inline
183 super(PolymorphicInlineModelAdmin.Child, self).__init__(
184 parent_inline.parent_model, parent_inline.admin_site
185 )
187 def get_formset(self, request, obj=None, **kwargs):
188 # The child inline is only used to construct the form,
189 # and allow to override the form field attributes.
190 # The formset is created by the parent inline.
191 raise RuntimeError("The child get_formset() is not used.")
193 def get_fields(self, request, obj=None):
194 if self.fields:
195 return self.fields
197 # Standard Django logic, use the form to determine the fields.
198 # The form needs to pass through all factory logic so all 'excludes' are set as well.
199 # Default Django does: form = self.get_formset(request, obj, fields=None).form
200 # Use 'fields=None' avoids recursion in the field autodetection.
201 form = self.get_formset_child(request, obj, fields=None).get_form()
202 return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
204 def get_formset_child(self, request, obj=None, **kwargs):
205 """
206 Return the formset child that the parent inline can use to represent us.
208 :rtype: PolymorphicFormSetChild
209 """
210 # Similar to the normal get_formset(), the caller may pass fields to override the defaults settings
211 # in the inline. In Django's GenericInlineModelAdmin.get_formset() this is also used in the same way,
212 # to make sure the 'exclude' also contains the GFK fields.
213 #
214 # Hence this code is almost identical to InlineModelAdmin.get_formset()
215 # and GenericInlineModelAdmin.get_formset()
216 #
217 # Transfer the local inline attributes to the formset child,
218 # this allows overriding settings.
219 if "fields" in kwargs:
220 fields = kwargs.pop("fields")
221 else:
222 fields = flatten_fieldsets(self.get_fieldsets(request, obj))
224 if self.exclude is None:
225 exclude = []
226 else:
227 exclude = list(self.exclude)
229 exclude.extend(self.get_readonly_fields(request, obj))
230 # Add forcefully, as Django 1.10 doesn't include readonly fields.
231 exclude.append("polymorphic_ctype")
233 if self.exclude is None and hasattr(self.form, "_meta") and self.form._meta.exclude:
234 # Take the custom ModelForm's Meta.exclude into account only if the
235 # InlineModelAdmin doesn't define its own.
236 exclude.extend(self.form._meta.exclude)
238 # can_delete = self.can_delete and self.has_delete_permission(request, obj)
239 defaults = {
240 "form": self.form,
241 "fields": fields,
242 "exclude": exclude or None,
243 "formfield_callback": partial(self.formfield_for_dbfield, request=request),
244 }
245 defaults.update(kwargs)
247 # This goes through the same logic that get_formset() calls
248 # by passing the inline class attributes to modelform_factory()
249 FormSetChildClass = self.formset_child
250 return FormSetChildClass(self.model, **defaults)
253class StackedPolymorphicInline(PolymorphicInlineModelAdmin):
254 """
255 Stacked inline for django-polymorphic models.
256 Since tabular doesn't make much sense with changed fields, just offer this one.
257 """
259 #: The default template to use.
260 template = "admin/polymorphic/edit_inline/stacked.html"