Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/forms/formsets.py: 23%
242 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 django.core.exceptions import ValidationError
2from django.forms import Form
3from django.forms.fields import BooleanField, IntegerField
4from django.forms.renderers import get_default_renderer
5from django.forms.utils import ErrorList, RenderableFormMixin
6from django.forms.widgets import CheckboxInput, HiddenInput, NumberInput
7from django.utils.functional import cached_property
8from django.utils.translation import gettext_lazy as _
9from django.utils.translation import ngettext
11__all__ = ("BaseFormSet", "formset_factory", "all_valid")
13# special field names
14TOTAL_FORM_COUNT = "TOTAL_FORMS"
15INITIAL_FORM_COUNT = "INITIAL_FORMS"
16MIN_NUM_FORM_COUNT = "MIN_NUM_FORMS"
17MAX_NUM_FORM_COUNT = "MAX_NUM_FORMS"
18ORDERING_FIELD_NAME = "ORDER"
19DELETION_FIELD_NAME = "DELETE"
21# default minimum number of forms in a formset
22DEFAULT_MIN_NUM = 0
24# default maximum number of forms in a formset, to prevent memory exhaustion
25DEFAULT_MAX_NUM = 1000
28class ManagementForm(Form):
29 """
30 Keep track of how many form instances are displayed on the page. If adding
31 new forms via JavaScript, you should increment the count field of this form
32 as well.
33 """
35 def __init__(self, *args, **kwargs):
36 self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
37 self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
38 # MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT are output with the rest of
39 # the management form, but only for the convenience of client-side
40 # code. The POST value of them returned from the client is not checked.
41 self.base_fields[MIN_NUM_FORM_COUNT] = IntegerField(
42 required=False, widget=HiddenInput
43 )
44 self.base_fields[MAX_NUM_FORM_COUNT] = IntegerField(
45 required=False, widget=HiddenInput
46 )
47 super().__init__(*args, **kwargs)
49 def clean(self):
50 cleaned_data = super().clean()
51 # When the management form is invalid, we don't know how many forms
52 # were submitted.
53 cleaned_data.setdefault(TOTAL_FORM_COUNT, 0)
54 cleaned_data.setdefault(INITIAL_FORM_COUNT, 0)
55 return cleaned_data
58class BaseFormSet(RenderableFormMixin):
59 """
60 A collection of instances of the same Form class.
61 """
63 deletion_widget = CheckboxInput
64 ordering_widget = NumberInput
65 default_error_messages = {
66 "missing_management_form": _(
67 "ManagementForm data is missing or has been tampered with. Missing fields: "
68 "%(field_names)s. You may need to file a bug report if the issue persists."
69 ),
70 }
71 template_name = "django/forms/formsets/default.html"
72 template_name_p = "django/forms/formsets/p.html"
73 template_name_table = "django/forms/formsets/table.html"
74 template_name_ul = "django/forms/formsets/ul.html"
76 def __init__(
77 self,
78 data=None,
79 files=None,
80 auto_id="id_%s",
81 prefix=None,
82 initial=None,
83 error_class=ErrorList,
84 form_kwargs=None,
85 error_messages=None,
86 ):
87 self.is_bound = data is not None or files is not None
88 self.prefix = prefix or self.get_default_prefix()
89 self.auto_id = auto_id
90 self.data = data or {}
91 self.files = files or {}
92 self.initial = initial
93 self.form_kwargs = form_kwargs or {}
94 self.error_class = error_class
95 self._errors = None
96 self._non_form_errors = None
98 messages = {}
99 for cls in reversed(type(self).__mro__):
100 messages.update(getattr(cls, "default_error_messages", {}))
101 if error_messages is not None:
102 messages.update(error_messages)
103 self.error_messages = messages
105 def __iter__(self):
106 """Yield the forms in the order they should be rendered."""
107 return iter(self.forms)
109 def __getitem__(self, index):
110 """Return the form at the given index, based on the rendering order."""
111 return self.forms[index]
113 def __len__(self):
114 return len(self.forms)
116 def __bool__(self):
117 """
118 Return True since all formsets have a management form which is not
119 included in the length.
120 """
121 return True
123 @cached_property
124 def management_form(self):
125 """Return the ManagementForm instance for this FormSet."""
126 if self.is_bound:
127 form = ManagementForm(
128 self.data,
129 auto_id=self.auto_id,
130 prefix=self.prefix,
131 renderer=self.renderer,
132 )
133 form.full_clean()
134 else:
135 form = ManagementForm(
136 auto_id=self.auto_id,
137 prefix=self.prefix,
138 initial={
139 TOTAL_FORM_COUNT: self.total_form_count(),
140 INITIAL_FORM_COUNT: self.initial_form_count(),
141 MIN_NUM_FORM_COUNT: self.min_num,
142 MAX_NUM_FORM_COUNT: self.max_num,
143 },
144 renderer=self.renderer,
145 )
146 return form
148 def total_form_count(self):
149 """Return the total number of forms in this FormSet."""
150 if self.is_bound:
151 # return absolute_max if it is lower than the actual total form
152 # count in the data; this is DoS protection to prevent clients
153 # from forcing the server to instantiate arbitrary numbers of
154 # forms
155 return min(
156 self.management_form.cleaned_data[TOTAL_FORM_COUNT], self.absolute_max
157 )
158 else:
159 initial_forms = self.initial_form_count()
160 total_forms = max(initial_forms, self.min_num) + self.extra
161 # Allow all existing related objects/inlines to be displayed,
162 # but don't allow extra beyond max_num.
163 if initial_forms > self.max_num >= 0:
164 total_forms = initial_forms
165 elif total_forms > self.max_num >= 0:
166 total_forms = self.max_num
167 return total_forms
169 def initial_form_count(self):
170 """Return the number of forms that are required in this FormSet."""
171 if self.is_bound:
172 return self.management_form.cleaned_data[INITIAL_FORM_COUNT]
173 else:
174 # Use the length of the initial data if it's there, 0 otherwise.
175 initial_forms = len(self.initial) if self.initial else 0
176 return initial_forms
178 @cached_property
179 def forms(self):
180 """Instantiate forms at first property access."""
181 # DoS protection is included in total_form_count()
182 return [
183 self._construct_form(i, **self.get_form_kwargs(i))
184 for i in range(self.total_form_count())
185 ]
187 def get_form_kwargs(self, index):
188 """
189 Return additional keyword arguments for each individual formset form.
191 index will be None if the form being constructed is a new empty
192 form.
193 """
194 return self.form_kwargs.copy()
196 def _construct_form(self, i, **kwargs):
197 """Instantiate and return the i-th form instance in a formset."""
198 defaults = {
199 "auto_id": self.auto_id,
200 "prefix": self.add_prefix(i),
201 "error_class": self.error_class,
202 # Don't render the HTML 'required' attribute as it may cause
203 # incorrect validation for extra, optional, and deleted
204 # forms in the formset.
205 "use_required_attribute": False,
206 "renderer": self.renderer,
207 }
208 if self.is_bound:
209 defaults["data"] = self.data
210 defaults["files"] = self.files
211 if self.initial and "initial" not in kwargs:
212 try:
213 defaults["initial"] = self.initial[i]
214 except IndexError:
215 pass
216 # Allow extra forms to be empty, unless they're part of
217 # the minimum forms.
218 if i >= self.initial_form_count() and i >= self.min_num:
219 defaults["empty_permitted"] = True
220 defaults.update(kwargs)
221 form = self.form(**defaults)
222 self.add_fields(form, i)
223 return form
225 @property
226 def initial_forms(self):
227 """Return a list of all the initial forms in this formset."""
228 return self.forms[: self.initial_form_count()]
230 @property
231 def extra_forms(self):
232 """Return a list of all the extra forms in this formset."""
233 return self.forms[self.initial_form_count() :]
235 @property
236 def empty_form(self):
237 form = self.form(
238 auto_id=self.auto_id,
239 prefix=self.add_prefix("__prefix__"),
240 empty_permitted=True,
241 use_required_attribute=False,
242 **self.get_form_kwargs(None),
243 renderer=self.renderer,
244 )
245 self.add_fields(form, None)
246 return form
248 @property
249 def cleaned_data(self):
250 """
251 Return a list of form.cleaned_data dicts for every form in self.forms.
252 """
253 if not self.is_valid():
254 raise AttributeError(
255 "'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__
256 )
257 return [form.cleaned_data for form in self.forms]
259 @property
260 def deleted_forms(self):
261 """Return a list of forms that have been marked for deletion."""
262 if not self.is_valid() or not self.can_delete:
263 return []
264 # construct _deleted_form_indexes which is just a list of form indexes
265 # that have had their deletion widget set to True
266 if not hasattr(self, "_deleted_form_indexes"):
267 self._deleted_form_indexes = []
268 for i, form in enumerate(self.forms):
269 # if this is an extra form and hasn't changed, don't consider it
270 if i >= self.initial_form_count() and not form.has_changed():
271 continue
272 if self._should_delete_form(form):
273 self._deleted_form_indexes.append(i)
274 return [self.forms[i] for i in self._deleted_form_indexes]
276 @property
277 def ordered_forms(self):
278 """
279 Return a list of form in the order specified by the incoming data.
280 Raise an AttributeError if ordering is not allowed.
281 """
282 if not self.is_valid() or not self.can_order:
283 raise AttributeError(
284 "'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__
285 )
286 # Construct _ordering, which is a list of (form_index, order_field_value)
287 # tuples. After constructing this list, we'll sort it by order_field_value
288 # so we have a way to get to the form indexes in the order specified
289 # by the form data.
290 if not hasattr(self, "_ordering"):
291 self._ordering = []
292 for i, form in enumerate(self.forms):
293 # if this is an extra form and hasn't changed, don't consider it
294 if i >= self.initial_form_count() and not form.has_changed():
295 continue
296 # don't add data marked for deletion to self.ordered_data
297 if self.can_delete and self._should_delete_form(form):
298 continue
299 self._ordering.append((i, form.cleaned_data[ORDERING_FIELD_NAME]))
300 # After we're done populating self._ordering, sort it.
301 # A sort function to order things numerically ascending, but
302 # None should be sorted below anything else. Allowing None as
303 # a comparison value makes it so we can leave ordering fields
304 # blank.
306 def compare_ordering_key(k):
307 if k[1] is None:
308 return (1, 0) # +infinity, larger than any number
309 return (0, k[1])
311 self._ordering.sort(key=compare_ordering_key)
312 # Return a list of form.cleaned_data dicts in the order specified by
313 # the form data.
314 return [self.forms[i[0]] for i in self._ordering]
316 @classmethod
317 def get_default_prefix(cls):
318 return "form"
320 @classmethod
321 def get_deletion_widget(cls):
322 return cls.deletion_widget
324 @classmethod
325 def get_ordering_widget(cls):
326 return cls.ordering_widget
328 def non_form_errors(self):
329 """
330 Return an ErrorList of errors that aren't associated with a particular
331 form -- i.e., from formset.clean(). Return an empty ErrorList if there
332 are none.
333 """
334 if self._non_form_errors is None:
335 self.full_clean()
336 return self._non_form_errors
338 @property
339 def errors(self):
340 """Return a list of form.errors for every form in self.forms."""
341 if self._errors is None:
342 self.full_clean()
343 return self._errors
345 def total_error_count(self):
346 """Return the number of errors across all forms in the formset."""
347 return len(self.non_form_errors()) + sum(
348 len(form_errors) for form_errors in self.errors
349 )
351 def _should_delete_form(self, form):
352 """Return whether or not the form was marked for deletion."""
353 return form.cleaned_data.get(DELETION_FIELD_NAME, False)
355 def is_valid(self):
356 """Return True if every form in self.forms is valid."""
357 if not self.is_bound:
358 return False
359 # Accessing errors triggers a full clean the first time only.
360 self.errors
361 # List comprehension ensures is_valid() is called for all forms.
362 # Forms due to be deleted shouldn't cause the formset to be invalid.
363 forms_valid = all(
364 [
365 form.is_valid()
366 for form in self.forms
367 if not (self.can_delete and self._should_delete_form(form))
368 ]
369 )
370 return forms_valid and not self.non_form_errors()
372 def full_clean(self):
373 """
374 Clean all of self.data and populate self._errors and
375 self._non_form_errors.
376 """
377 self._errors = []
378 self._non_form_errors = self.error_class(
379 error_class="nonform", renderer=self.renderer
380 )
381 empty_forms_count = 0
383 if not self.is_bound: # Stop further processing.
384 return
386 if not self.management_form.is_valid():
387 error = ValidationError(
388 self.error_messages["missing_management_form"],
389 params={
390 "field_names": ", ".join(
391 self.management_form.add_prefix(field_name)
392 for field_name in self.management_form.errors
393 ),
394 },
395 code="missing_management_form",
396 )
397 self._non_form_errors.append(error)
399 for i, form in enumerate(self.forms):
400 # Empty forms are unchanged forms beyond those with initial data.
401 if not form.has_changed() and i >= self.initial_form_count():
402 empty_forms_count += 1
403 # Accessing errors calls full_clean() if necessary.
404 # _should_delete_form() requires cleaned_data.
405 form_errors = form.errors
406 if self.can_delete and self._should_delete_form(form):
407 continue
408 self._errors.append(form_errors)
409 try:
410 if (
411 self.validate_max
412 and self.total_form_count() - len(self.deleted_forms) > self.max_num
413 ) or self.management_form.cleaned_data[
414 TOTAL_FORM_COUNT
415 ] > self.absolute_max:
416 raise ValidationError(
417 ngettext(
418 "Please submit at most %d form.",
419 "Please submit at most %d forms.",
420 self.max_num,
421 )
422 % self.max_num,
423 code="too_many_forms",
424 )
425 if (
426 self.validate_min
427 and self.total_form_count()
428 - len(self.deleted_forms)
429 - empty_forms_count
430 < self.min_num
431 ):
432 raise ValidationError(
433 ngettext(
434 "Please submit at least %d form.",
435 "Please submit at least %d forms.",
436 self.min_num,
437 )
438 % self.min_num,
439 code="too_few_forms",
440 )
441 # Give self.clean() a chance to do cross-form validation.
442 self.clean()
443 except ValidationError as e:
444 self._non_form_errors = self.error_class(
445 e.error_list,
446 error_class="nonform",
447 renderer=self.renderer,
448 )
450 def clean(self):
451 """
452 Hook for doing any extra formset-wide cleaning after Form.clean() has
453 been called on every form. Any ValidationError raised by this method
454 will not be associated with a particular form; it will be accessible
455 via formset.non_form_errors()
456 """
457 pass
459 def has_changed(self):
460 """Return True if data in any form differs from initial."""
461 return any(form.has_changed() for form in self)
463 def add_fields(self, form, index):
464 """A hook for adding extra fields on to each form instance."""
465 initial_form_count = self.initial_form_count()
466 if self.can_order:
467 # Only pre-fill the ordering field for initial forms.
468 if index is not None and index < initial_form_count:
469 form.fields[ORDERING_FIELD_NAME] = IntegerField(
470 label=_("Order"),
471 initial=index + 1,
472 required=False,
473 widget=self.get_ordering_widget(),
474 )
475 else:
476 form.fields[ORDERING_FIELD_NAME] = IntegerField(
477 label=_("Order"),
478 required=False,
479 widget=self.get_ordering_widget(),
480 )
481 if self.can_delete and (self.can_delete_extra or index < initial_form_count):
482 form.fields[DELETION_FIELD_NAME] = BooleanField(
483 label=_("Delete"),
484 required=False,
485 widget=self.get_deletion_widget(),
486 )
488 def add_prefix(self, index):
489 return "%s-%s" % (self.prefix, index)
491 def is_multipart(self):
492 """
493 Return True if the formset needs to be multipart, i.e. it
494 has FileInput, or False otherwise.
495 """
496 if self.forms:
497 return self.forms[0].is_multipart()
498 else:
499 return self.empty_form.is_multipart()
501 @property
502 def media(self):
503 # All the forms on a FormSet are the same, so you only need to
504 # interrogate the first form for media.
505 if self.forms:
506 return self.forms[0].media
507 else:
508 return self.empty_form.media
510 def get_context(self):
511 return {"formset": self}
514def formset_factory(
515 form,
516 formset=BaseFormSet,
517 extra=1,
518 can_order=False,
519 can_delete=False,
520 max_num=None,
521 validate_max=False,
522 min_num=None,
523 validate_min=False,
524 absolute_max=None,
525 can_delete_extra=True,
526 renderer=None,
527):
528 """Return a FormSet for the given form class."""
529 if min_num is None:
530 min_num = DEFAULT_MIN_NUM
531 if max_num is None:
532 max_num = DEFAULT_MAX_NUM
533 # absolute_max is a hard limit on forms instantiated, to prevent
534 # memory-exhaustion attacks. Default to max_num + DEFAULT_MAX_NUM
535 # (which is 2 * DEFAULT_MAX_NUM if max_num is None in the first place).
536 if absolute_max is None:
537 absolute_max = max_num + DEFAULT_MAX_NUM
538 if max_num > absolute_max:
539 raise ValueError("'absolute_max' must be greater or equal to 'max_num'.")
540 attrs = {
541 "form": form,
542 "extra": extra,
543 "can_order": can_order,
544 "can_delete": can_delete,
545 "can_delete_extra": can_delete_extra,
546 "min_num": min_num,
547 "max_num": max_num,
548 "absolute_max": absolute_max,
549 "validate_min": validate_min,
550 "validate_max": validate_max,
551 "renderer": renderer or get_default_renderer(),
552 }
553 return type(form.__name__ + "FormSet", (formset,), attrs)
556def all_valid(formsets):
557 """Validate every formset and return True if all are valid."""
558 # List comprehension ensures is_valid() is called for all formsets.
559 return all([formset.is_valid() for formset in formsets])