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