Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/polymorphic/formsets/models.py: 21%
148 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
1from collections import OrderedDict
3from django import forms
4from django.contrib.contenttypes.models import ContentType
5from django.core.exceptions import ImproperlyConfigured, ValidationError
6from django.forms.models import (
7 BaseInlineFormSet,
8 BaseModelFormSet,
9 ModelForm,
10 inlineformset_factory,
11 modelform_factory,
12 modelformset_factory,
13)
14from django.utils.functional import cached_property
16from polymorphic.models import PolymorphicModel
18from .utils import add_media
21class UnsupportedChildType(LookupError):
22 pass
25class PolymorphicFormSetChild:
26 """
27 Metadata to define the inline of a polymorphic child.
28 Provide this information in the :func:'polymorphic_inlineformset_factory' construction.
29 """
31 def __init__(
32 self,
33 model,
34 form=ModelForm,
35 fields=None,
36 exclude=None,
37 formfield_callback=None,
38 widgets=None,
39 localized_fields=None,
40 labels=None,
41 help_texts=None,
42 error_messages=None,
43 ):
45 self.model = model
47 # Instead of initializing the form here right away,
48 # the settings are saved so get_form() can receive additional exclude kwargs.
49 # This is mostly needed for the generic inline formsets
50 self._form_base = form
51 self.fields = fields
52 self.exclude = exclude or ()
53 self.formfield_callback = formfield_callback
54 self.widgets = widgets
55 self.localized_fields = localized_fields
56 self.labels = labels
57 self.help_texts = help_texts
58 self.error_messages = error_messages
60 @cached_property
61 def content_type(self):
62 """
63 Expose the ContentType that the child relates to.
64 This can be used for the ''polymorphic_ctype'' field.
65 """
66 return ContentType.objects.get_for_model(self.model, for_concrete_model=False)
68 def get_form(self, **kwargs):
69 """
70 Construct the form class for the formset child.
71 """
72 # Do what modelformset_factory() / inlineformset_factory() does to the 'form' argument;
73 # Construct the form with the given ModelFormOptions values
75 # Fields can be overwritten. To support the global 'polymorphic_child_forms_factory' kwargs,
76 # that doesn't completely replace all 'exclude' settings defined per child type,
77 # we allow to define things like 'extra_...' fields that are amended to the current child settings.
79 exclude = list(self.exclude)
80 extra_exclude = kwargs.pop("extra_exclude", None)
81 if extra_exclude:
82 exclude += list(extra_exclude)
84 defaults = {
85 "form": self._form_base,
86 "formfield_callback": self.formfield_callback,
87 "fields": self.fields,
88 "exclude": exclude,
89 # 'for_concrete_model': for_concrete_model,
90 "localized_fields": self.localized_fields,
91 "labels": self.labels,
92 "help_texts": self.help_texts,
93 "error_messages": self.error_messages,
94 "widgets": self.widgets,
95 # 'field_classes': field_classes,
96 }
97 defaults.update(kwargs)
99 return modelform_factory(self.model, **defaults)
102def polymorphic_child_forms_factory(formset_children, **kwargs):
103 """
104 Construct the forms for the formset children.
105 This is mostly used internally, and rarely needs to be used by external projects.
106 When using the factory methods (:func:'polymorphic_inlineformset_factory'),
107 this feature is called already for you.
108 """
109 child_forms = OrderedDict()
111 for formset_child in formset_children:
112 child_forms[formset_child.model] = formset_child.get_form(**kwargs)
114 return child_forms
117class BasePolymorphicModelFormSet(BaseModelFormSet):
118 """
119 A formset that can produce different forms depending on the object type.
121 Note that the 'add' feature is therefore more complex,
122 as all variations need ot be exposed somewhere.
124 When switching existing formsets to the polymorphic formset,
125 note that the ID field will no longer be named ''model_ptr'',
126 but just appear as ''id''.
127 """
129 # Assigned by the factory
130 child_forms = OrderedDict()
132 def __init__(self, *args, **kwargs):
133 super().__init__(*args, **kwargs)
134 self.queryset_data = self.get_queryset()
136 def _construct_form(self, i, **kwargs):
137 """
138 Create the form, depending on the model that's behind it.
139 """
140 # BaseModelFormSet logic
141 if self.is_bound and i < self.initial_form_count():
142 pk_key = f"{self.add_prefix(i)}-{self.model._meta.pk.name}"
143 pk = self.data[pk_key]
144 pk_field = self.model._meta.pk
145 to_python = self._get_to_python(pk_field)
146 pk = to_python(pk)
147 kwargs["instance"] = self._existing_object(pk)
148 if i < self.initial_form_count() and "instance" not in kwargs:
149 kwargs["instance"] = self.get_queryset()[i]
150 if i >= self.initial_form_count() and self.initial_extra:
151 # Set initial values for extra forms
152 try:
153 kwargs["initial"] = self.initial_extra[i - self.initial_form_count()]
154 except IndexError:
155 pass
157 # BaseFormSet logic, with custom formset_class
158 defaults = {
159 "auto_id": self.auto_id,
160 "prefix": self.add_prefix(i),
161 "error_class": self.error_class,
162 }
163 if self.is_bound:
164 defaults["data"] = self.data
165 defaults["files"] = self.files
166 if self.initial and "initial" not in kwargs:
167 try:
168 defaults["initial"] = self.initial[i]
169 except IndexError:
170 pass
171 # Allow extra forms to be empty, unless they're part of
172 # the minimum forms.
173 if i >= self.initial_form_count() and i >= self.min_num:
174 defaults["empty_permitted"] = True
175 defaults["use_required_attribute"] = False
176 defaults.update(kwargs)
178 # Need to find the model that will be displayed in this form.
179 # Hence, peeking in the self.queryset_data beforehand.
180 if self.is_bound:
181 if "instance" in defaults:
182 # Object is already bound to a model, won't change the content type
183 model = defaults["instance"].get_real_instance_class() # allow proxy models
184 else:
185 # Extra or empty form, use the provided type.
186 # Note this completely tru
187 prefix = defaults["prefix"]
188 try:
189 ct_id = int(self.data[f"{prefix}-polymorphic_ctype"])
190 except (KeyError, ValueError):
191 raise ValidationError(
192 "Formset row {} has no 'polymorphic_ctype' defined!".format(prefix)
193 )
195 model = ContentType.objects.get_for_id(ct_id).model_class()
196 if model not in self.child_forms:
197 # Perform basic validation, as we skip the ChoiceField here.
198 raise UnsupportedChildType(
199 f"Child model type {model} is not part of the formset"
200 )
201 else:
202 if "instance" in defaults:
203 model = defaults["instance"].get_real_instance_class() # allow proxy models
204 elif "polymorphic_ctype" in defaults.get("initial", {}):
205 model = defaults["initial"]["polymorphic_ctype"].model_class()
206 elif i < len(self.queryset_data):
207 model = self.queryset_data[i].__class__
208 else:
209 # Extra forms, cycle between all types
210 # TODO: take the 'extra' value of each child formset into account.
211 total_known = len(self.queryset_data)
212 child_models = list(self.child_forms.keys())
213 model = child_models[(i - total_known) % len(child_models)]
215 form_class = self.get_form_class(model)
216 form = form_class(**defaults)
217 self.add_fields(form, i)
218 return form
220 def add_fields(self, form, index):
221 """Add a hidden field for the content type."""
222 ct = ContentType.objects.get_for_model(form._meta.model, for_concrete_model=False)
223 choices = [(ct.pk, ct)] # Single choice, existing forms can't change the value.
224 form.fields["polymorphic_ctype"] = forms.TypedChoiceField(
225 choices=choices,
226 initial=ct.pk,
227 required=False,
228 widget=forms.HiddenInput,
229 coerce=int,
230 )
231 super().add_fields(form, index)
233 def get_form_class(self, model):
234 """
235 Return the proper form class for the given model.
236 """
237 if not self.child_forms:
238 raise ImproperlyConfigured(f"No 'child_forms' defined in {self.__class__.__name__}")
239 if not issubclass(model, PolymorphicModel):
240 raise TypeError(f"Expect polymorphic model type, not {model}")
242 try:
243 return self.child_forms[model]
244 except KeyError:
245 # This may happen when the query returns objects of a type that was not handled by the formset.
246 raise UnsupportedChildType(
247 "The '{}' found a '{}' model in the queryset, "
248 "but no form class is registered to display it.".format(
249 self.__class__.__name__, model.__name__
250 )
251 )
253 def is_multipart(self):
254 """
255 Returns True if the formset needs to be multipart, i.e. it
256 has FileInput. Otherwise, False.
257 """
258 return any(f.is_multipart() for f in self.empty_forms)
260 @property
261 def media(self):
262 # Include the media of all form types.
263 # The form media includes all form widget media
264 media = forms.Media()
265 for form in self.empty_forms:
266 add_media(media, form.media)
267 return media
269 @cached_property
270 def empty_forms(self):
271 """
272 Return all possible empty forms
273 """
274 forms = []
275 for model, form_class in self.child_forms.items():
276 kwargs = self.get_form_kwargs(None)
278 form = form_class(
279 auto_id=self.auto_id,
280 prefix=self.add_prefix("__prefix__"),
281 empty_permitted=True,
282 use_required_attribute=False,
283 **kwargs,
284 )
285 self.add_fields(form, None)
286 forms.append(form)
287 return forms
289 @property
290 def empty_form(self):
291 # TODO: make an exception when can_add_base is defined?
292 raise RuntimeError(
293 "'empty_form' is not used in polymorphic formsets, use 'empty_forms' instead."
294 )
297def polymorphic_modelformset_factory(
298 model,
299 formset_children,
300 formset=BasePolymorphicModelFormSet,
301 # Base field
302 # TODO: should these fields be removed in favor of creating
303 # the base form as a formset child too?
304 form=ModelForm,
305 fields=None,
306 exclude=None,
307 extra=1,
308 can_order=False,
309 can_delete=True,
310 max_num=None,
311 formfield_callback=None,
312 widgets=None,
313 validate_max=False,
314 localized_fields=None,
315 labels=None,
316 help_texts=None,
317 error_messages=None,
318 min_num=None,
319 validate_min=False,
320 field_classes=None,
321 child_form_kwargs=None,
322):
323 """
324 Construct the class for an polymorphic model formset.
326 All arguments are identical to :func:'~django.forms.models.modelformset_factory',
327 with the exception of the ''formset_children'' argument.
329 :param formset_children: A list of all child :class:'PolymorphicFormSetChild' objects
330 that tell the inline how to render the child model types.
331 :type formset_children: Iterable[PolymorphicFormSetChild]
332 :rtype: type
333 """
334 kwargs = {
335 "model": model,
336 "form": form,
337 "formfield_callback": formfield_callback,
338 "formset": formset,
339 "extra": extra,
340 "can_delete": can_delete,
341 "can_order": can_order,
342 "fields": fields,
343 "exclude": exclude,
344 "min_num": min_num,
345 "max_num": max_num,
346 "widgets": widgets,
347 "validate_min": validate_min,
348 "validate_max": validate_max,
349 "localized_fields": localized_fields,
350 "labels": labels,
351 "help_texts": help_texts,
352 "error_messages": error_messages,
353 "field_classes": field_classes,
354 }
355 FormSet = modelformset_factory(**kwargs)
357 child_kwargs = {
358 # 'exclude': exclude,
359 }
360 if child_form_kwargs:
361 child_kwargs.update(child_form_kwargs)
363 FormSet.child_forms = polymorphic_child_forms_factory(formset_children, **child_kwargs)
364 return FormSet
367class BasePolymorphicInlineFormSet(BaseInlineFormSet, BasePolymorphicModelFormSet):
368 """
369 Polymorphic formset variation for inline formsets
370 """
372 def _construct_form(self, i, **kwargs):
373 return super()._construct_form(i, **kwargs)
376def polymorphic_inlineformset_factory(
377 parent_model,
378 model,
379 formset_children,
380 formset=BasePolymorphicInlineFormSet,
381 fk_name=None,
382 # Base field
383 # TODO: should these fields be removed in favor of creating
384 # the base form as a formset child too?
385 form=ModelForm,
386 fields=None,
387 exclude=None,
388 extra=1,
389 can_order=False,
390 can_delete=True,
391 max_num=None,
392 formfield_callback=None,
393 widgets=None,
394 validate_max=False,
395 localized_fields=None,
396 labels=None,
397 help_texts=None,
398 error_messages=None,
399 min_num=None,
400 validate_min=False,
401 field_classes=None,
402 child_form_kwargs=None,
403):
404 """
405 Construct the class for an inline polymorphic formset.
407 All arguments are identical to :func:'~django.forms.models.inlineformset_factory',
408 with the exception of the ''formset_children'' argument.
410 :param formset_children: A list of all child :class:'PolymorphicFormSetChild' objects
411 that tell the inline how to render the child model types.
412 :type formset_children: Iterable[PolymorphicFormSetChild]
413 :rtype: type
414 """
415 kwargs = {
416 "parent_model": parent_model,
417 "model": model,
418 "form": form,
419 "formfield_callback": formfield_callback,
420 "formset": formset,
421 "fk_name": fk_name,
422 "extra": extra,
423 "can_delete": can_delete,
424 "can_order": can_order,
425 "fields": fields,
426 "exclude": exclude,
427 "min_num": min_num,
428 "max_num": max_num,
429 "widgets": widgets,
430 "validate_min": validate_min,
431 "validate_max": validate_max,
432 "localized_fields": localized_fields,
433 "labels": labels,
434 "help_texts": help_texts,
435 "error_messages": error_messages,
436 "field_classes": field_classes,
437 }
438 FormSet = inlineformset_factory(**kwargs)
440 child_kwargs = {
441 # 'exclude': exclude,
442 }
443 if child_form_kwargs:
444 child_kwargs.update(child_form_kwargs)
446 FormSet.child_forms = polymorphic_child_forms_factory(formset_children, **child_kwargs)
447 return FormSet