diff options
Diffstat (limited to 'django/forms')
| -rw-r--r-- | django/forms/boundfield.py | 113 | ||||
| -rw-r--r-- | django/forms/fields.py | 437 | ||||
| -rw-r--r-- | django/forms/forms.py | 175 | ||||
| -rw-r--r-- | django/forms/formsets.py | 238 | ||||
| -rw-r--r-- | django/forms/models.py | 727 | ||||
| -rw-r--r-- | django/forms/renderers.py | 20 | ||||
| -rw-r--r-- | django/forms/utils.py | 79 | ||||
| -rw-r--r-- | django/forms/widgets.py | 514 |
8 files changed, 1414 insertions, 889 deletions
diff --git a/django/forms/boundfield.py b/django/forms/boundfield.py index 62d1823d57..e83160645e 100644 --- a/django/forms/boundfield.py +++ b/django/forms/boundfield.py @@ -7,12 +7,13 @@ from django.utils.functional import cached_property from django.utils.html import format_html, html_safe from django.utils.translation import gettext_lazy as _ -__all__ = ('BoundField',) +__all__ = ("BoundField",) @html_safe class BoundField: "A Field plus data" + def __init__(self, form, field, name): self.form = form self.field = field @@ -24,7 +25,7 @@ class BoundField: self.label = pretty_name(name) else: self.label = self.field.label - self.help_text = field.help_text or '' + self.help_text = field.help_text or "" def __str__(self): """Render this field as an HTML widget.""" @@ -41,12 +42,14 @@ class BoundField: This property is cached so that only one database query occurs when rendering ModelChoiceFields. """ - id_ = self.field.widget.attrs.get('id') or self.auto_id - attrs = {'id': id_} if id_ else {} + id_ = self.field.widget.attrs.get("id") or self.auto_id + attrs = {"id": id_} if id_ else {} attrs = self.build_widget_attrs(attrs) return [ BoundWidget(self.field.widget, widget, self.form.renderer) - for widget in self.field.widget.subwidgets(self.html_name, self.value(), attrs=attrs) + for widget in self.field.widget.subwidgets( + self.html_name, self.value(), attrs=attrs + ) ] def __bool__(self): @@ -64,7 +67,7 @@ class BoundField: # from templates. if not isinstance(idx, (int, slice)): raise TypeError( - 'BoundField indices must be integers or slices, not %s.' + "BoundField indices must be integers or slices, not %s." % type(idx).__name__ ) return self.subwidgets[idx] @@ -74,7 +77,9 @@ class BoundField: """ Return an ErrorList (empty if there are no errors) for this field. """ - return self.form.errors.get(self.name, self.form.error_class(renderer=self.form.renderer)) + return self.form.errors.get( + self.name, self.form.error_class(renderer=self.form.renderer) + ) def as_widget(self, widget=None, attrs=None, only_initial=False): """ @@ -87,8 +92,10 @@ class BoundField: widget.is_localized = True attrs = attrs or {} attrs = self.build_widget_attrs(attrs, widget) - if self.auto_id and 'id' not in widget.attrs: - attrs.setdefault('id', self.html_initial_id if only_initial else self.auto_id) + if self.auto_id and "id" not in widget.attrs: + attrs.setdefault( + "id", self.html_initial_id if only_initial else self.auto_id + ) return widget.render( name=self.html_initial_name if only_initial else self.html_name, value=self.value(), @@ -134,7 +141,8 @@ class BoundField: if field.show_hidden_initial: hidden_widget = field.hidden_widget() initial_value = self.form._widget_data_value( - hidden_widget, self.html_initial_name, + hidden_widget, + self.html_initial_name, ) try: initial_value = field.to_python(initial_value) @@ -157,31 +165,34 @@ class BoundField: """ contents = contents or self.label if label_suffix is None: - label_suffix = (self.field.label_suffix if self.field.label_suffix is not None - else self.form.label_suffix) + label_suffix = ( + self.field.label_suffix + if self.field.label_suffix is not None + else self.form.label_suffix + ) # Only add the suffix if the label does not end in punctuation. # Translators: If found as last label character, these punctuation # characters will prevent the default label_suffix to be appended to the label - if label_suffix and contents and contents[-1] not in _(':?.!'): - contents = format_html('{}{}', contents, label_suffix) + if label_suffix and contents and contents[-1] not in _(":?.!"): + contents = format_html("{}{}", contents, label_suffix) widget = self.field.widget - id_ = widget.attrs.get('id') or self.auto_id + id_ = widget.attrs.get("id") or self.auto_id if id_: id_for_label = widget.id_for_label(id_) if id_for_label: - attrs = {**(attrs or {}), 'for': id_for_label} - if self.field.required and hasattr(self.form, 'required_css_class'): + attrs = {**(attrs or {}), "for": id_for_label} + if self.field.required and hasattr(self.form, "required_css_class"): attrs = attrs or {} - if 'class' in attrs: - attrs['class'] += ' ' + self.form.required_css_class + if "class" in attrs: + attrs["class"] += " " + self.form.required_css_class else: - attrs['class'] = self.form.required_css_class + attrs["class"] = self.form.required_css_class context = { - 'field': self, - 'label': contents, - 'attrs': attrs, - 'use_tag': bool(id_), - 'tag': tag or 'label', + "field": self, + "label": contents, + "attrs": attrs, + "use_tag": bool(id_), + "tag": tag or "label", } return self.form.render(self.form.template_name_label, context) @@ -195,20 +206,20 @@ class BoundField: label_suffix overrides the form's label_suffix. """ - return self.label_tag(contents, attrs, label_suffix, tag='legend') + return self.label_tag(contents, attrs, label_suffix, tag="legend") def css_classes(self, extra_classes=None): """ Return a string of space-separated CSS classes for this field. """ - if hasattr(extra_classes, 'split'): + if hasattr(extra_classes, "split"): extra_classes = extra_classes.split() extra_classes = set(extra_classes or []) - if self.errors and hasattr(self.form, 'error_css_class'): + if self.errors and hasattr(self.form, "error_css_class"): extra_classes.add(self.form.error_css_class) - if self.field.required and hasattr(self.form, 'required_css_class'): + if self.field.required and hasattr(self.form, "required_css_class"): extra_classes.add(self.form.required_css_class) - return ' '.join(extra_classes) + return " ".join(extra_classes) @property def is_hidden(self): @@ -222,11 +233,11 @@ class BoundField: associated Form has specified auto_id. Return an empty string otherwise. """ auto_id = self.form.auto_id # Boolean or string - if auto_id and '%s' in str(auto_id): + if auto_id and "%s" in str(auto_id): return auto_id % self.html_name elif auto_id: return self.html_name - return '' + return "" @property def id_for_label(self): @@ -236,7 +247,7 @@ class BoundField: it has a single widget or a MultiWidget. """ widget = self.field.widget - id_ = widget.attrs.get('id') or self.auto_id + id_ = widget.attrs.get("id") or self.auto_id return widget.id_for_label(id_) @cached_property @@ -246,25 +257,34 @@ class BoundField: def build_widget_attrs(self, attrs, widget=None): widget = widget or self.field.widget attrs = dict(attrs) # Copy attrs to avoid modifying the argument. - if widget.use_required_attribute(self.initial) and self.field.required and self.form.use_required_attribute: + if ( + widget.use_required_attribute(self.initial) + and self.field.required + and self.form.use_required_attribute + ): # MultiValueField has require_all_fields: if False, fall back # on subfields. if ( - hasattr(self.field, 'require_all_fields') and - not self.field.require_all_fields and - isinstance(self.field.widget, MultiWidget) + hasattr(self.field, "require_all_fields") + and not self.field.require_all_fields + and isinstance(self.field.widget, MultiWidget) ): for subfield, subwidget in zip(self.field.fields, widget.widgets): - subwidget.attrs['required'] = subwidget.use_required_attribute(self.initial) and subfield.required + subwidget.attrs["required"] = ( + subwidget.use_required_attribute(self.initial) + and subfield.required + ) else: - attrs['required'] = True + attrs["required"] = True if self.field.disabled: - attrs['disabled'] = True + attrs["disabled"] = True return attrs @property def widget_type(self): - return re.sub(r'widget$|input$', '', self.field.widget.__class__.__name__.lower()) + return re.sub( + r"widget$|input$", "", self.field.widget.__class__.__name__.lower() + ) @html_safe @@ -281,6 +301,7 @@ class BoundWidget: </label> {% endfor %} """ + def __init__(self, parent_widget, data, renderer): self.parent_widget = parent_widget self.data = data @@ -290,19 +311,19 @@ class BoundWidget: return self.tag(wrap_label=True) def tag(self, wrap_label=False): - context = {'widget': {**self.data, 'wrap_label': wrap_label}} + context = {"widget": {**self.data, "wrap_label": wrap_label}} return self.parent_widget._render(self.template_name, context, self.renderer) @property def template_name(self): - if 'template_name' in self.data: - return self.data['template_name'] + if "template_name" in self.data: + return self.data["template_name"] return self.parent_widget.template_name @property def id_for_label(self): - return self.data['attrs'].get('id') + return self.data["attrs"].get("id") @property def choice_label(self): - return self.data['label'] + return self.data["label"] diff --git a/django/forms/fields.py b/django/forms/fields.py index 65d6a9ec82..a7031936dd 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -19,45 +19,94 @@ from django.core.exceptions import ValidationError from django.forms.boundfield import BoundField from django.forms.utils import from_current_timezone, to_current_timezone from django.forms.widgets import ( - FILE_INPUT_CONTRADICTION, CheckboxInput, ClearableFileInput, DateInput, - DateTimeInput, EmailInput, FileInput, HiddenInput, MultipleHiddenInput, - NullBooleanSelect, NumberInput, Select, SelectMultiple, - SplitDateTimeWidget, SplitHiddenDateTimeWidget, Textarea, TextInput, - TimeInput, URLInput, + FILE_INPUT_CONTRADICTION, + CheckboxInput, + ClearableFileInput, + DateInput, + DateTimeInput, + EmailInput, + FileInput, + HiddenInput, + MultipleHiddenInput, + NullBooleanSelect, + NumberInput, + Select, + SelectMultiple, + SplitDateTimeWidget, + SplitHiddenDateTimeWidget, + Textarea, + TextInput, + TimeInput, + URLInput, ) from django.utils import formats from django.utils.dateparse import parse_datetime, parse_duration from django.utils.duration import duration_string from django.utils.ipv6 import clean_ipv6_address from django.utils.regex_helper import _lazy_re_compile -from django.utils.translation import gettext_lazy as _, ngettext_lazy +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import ngettext_lazy __all__ = ( - 'Field', 'CharField', 'IntegerField', - 'DateField', 'TimeField', 'DateTimeField', 'DurationField', - 'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField', - 'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField', - 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', - 'SplitDateTimeField', 'GenericIPAddressField', 'FilePathField', - 'JSONField', 'SlugField', 'TypedChoiceField', 'TypedMultipleChoiceField', - 'UUIDField', + "Field", + "CharField", + "IntegerField", + "DateField", + "TimeField", + "DateTimeField", + "DurationField", + "RegexField", + "EmailField", + "FileField", + "ImageField", + "URLField", + "BooleanField", + "NullBooleanField", + "ChoiceField", + "MultipleChoiceField", + "ComboField", + "MultiValueField", + "FloatField", + "DecimalField", + "SplitDateTimeField", + "GenericIPAddressField", + "FilePathField", + "JSONField", + "SlugField", + "TypedChoiceField", + "TypedMultipleChoiceField", + "UUIDField", ) class Field: widget = TextInput # Default widget to use when rendering this type of Field. - hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden". + hidden_widget = ( + HiddenInput # Default widget to use when rendering this as "hidden". + ) default_validators = [] # Default set of validators # Add an 'invalid' entry to default_error_message if you want a specific # field error message not raised by the field validators. default_error_messages = { - 'required': _('This field is required.'), + "required": _("This field is required."), } empty_values = list(validators.EMPTY_VALUES) - def __init__(self, *, required=True, widget=None, label=None, initial=None, - help_text='', error_messages=None, show_hidden_initial=False, - validators=(), localize=False, disabled=False, label_suffix=None): + def __init__( + self, + *, + required=True, + widget=None, + label=None, + initial=None, + help_text="", + error_messages=None, + show_hidden_initial=False, + validators=(), + localize=False, + disabled=False, + label_suffix=None, + ): # required -- Boolean that specifies whether the field is required. # True by default. # widget -- A Widget class, or instance of a Widget class, that should @@ -109,7 +158,7 @@ class Field: messages = {} for c in reversed(self.__class__.__mro__): - messages.update(getattr(c, 'default_error_messages', {})) + messages.update(getattr(c, "default_error_messages", {})) messages.update(error_messages or {}) self.error_messages = messages @@ -125,7 +174,7 @@ class Field: def validate(self, value): if value in self.empty_values and self.required: - raise ValidationError(self.error_messages['required'], code='required') + raise ValidationError(self.error_messages["required"], code="required") def run_validators(self, value): if value in self.empty_values: @@ -135,7 +184,7 @@ class Field: try: v(value) except ValidationError as e: - if hasattr(e, 'code') and e.code in self.error_messages: + if hasattr(e, "code") and e.code in self.error_messages: e.message = self.error_messages[e.code] errors.extend(e.error_list) if errors: @@ -180,15 +229,15 @@ class Field: return False try: data = self.to_python(data) - if hasattr(self, '_coerce'): + if hasattr(self, "_coerce"): return self._coerce(data) != self._coerce(initial) except ValidationError: return True # For purposes of seeing whether something has changed, None is # the same as an empty string, if the data or initial value we get # is None, replace it with ''. - initial_value = initial if initial is not None else '' - data_value = data if data is not None else '' + initial_value = initial if initial is not None else "" + data_value = data if data is not None else "" return initial_value != data_value def get_bound_field(self, form, field_name): @@ -208,7 +257,9 @@ class Field: class CharField(Field): - def __init__(self, *, max_length=None, min_length=None, strip=True, empty_value='', **kwargs): + def __init__( + self, *, max_length=None, min_length=None, strip=True, empty_value="", **kwargs + ): self.max_length = max_length self.min_length = min_length self.strip = strip @@ -234,25 +285,25 @@ class CharField(Field): attrs = super().widget_attrs(widget) if self.max_length is not None and not widget.is_hidden: # The HTML attribute is maxlength, not max_length. - attrs['maxlength'] = str(self.max_length) + attrs["maxlength"] = str(self.max_length) if self.min_length is not None and not widget.is_hidden: # The HTML attribute is minlength, not min_length. - attrs['minlength'] = str(self.min_length) + attrs["minlength"] = str(self.min_length) return attrs class IntegerField(Field): widget = NumberInput default_error_messages = { - 'invalid': _('Enter a whole number.'), + "invalid": _("Enter a whole number."), } - re_decimal = _lazy_re_compile(r'\.0*\s*$') + re_decimal = _lazy_re_compile(r"\.0*\s*$") def __init__(self, *, max_value=None, min_value=None, **kwargs): self.max_value, self.min_value = max_value, min_value - if kwargs.get('localize') and self.widget == NumberInput: + if kwargs.get("localize") and self.widget == NumberInput: # Localized number input is not well supported on most browsers - kwargs.setdefault('widget', super().widget) + kwargs.setdefault("widget", super().widget) super().__init__(**kwargs) if max_value is not None: @@ -272,24 +323,24 @@ class IntegerField(Field): value = formats.sanitize_separators(value) # Strip trailing decimal and zeros. try: - value = int(self.re_decimal.sub('', str(value))) + value = int(self.re_decimal.sub("", str(value))) except (ValueError, TypeError): - raise ValidationError(self.error_messages['invalid'], code='invalid') + raise ValidationError(self.error_messages["invalid"], code="invalid") return value def widget_attrs(self, widget): attrs = super().widget_attrs(widget) if isinstance(widget, NumberInput): if self.min_value is not None: - attrs['min'] = self.min_value + attrs["min"] = self.min_value if self.max_value is not None: - attrs['max'] = self.max_value + attrs["max"] = self.max_value return attrs class FloatField(IntegerField): default_error_messages = { - 'invalid': _('Enter a number.'), + "invalid": _("Enter a number."), } def to_python(self, value): @@ -305,7 +356,7 @@ class FloatField(IntegerField): try: value = float(value) except (ValueError, TypeError): - raise ValidationError(self.error_messages['invalid'], code='invalid') + raise ValidationError(self.error_messages["invalid"], code="invalid") return value def validate(self, value): @@ -313,21 +364,29 @@ class FloatField(IntegerField): if value in self.empty_values: return if not math.isfinite(value): - raise ValidationError(self.error_messages['invalid'], code='invalid') + raise ValidationError(self.error_messages["invalid"], code="invalid") def widget_attrs(self, widget): attrs = super().widget_attrs(widget) - if isinstance(widget, NumberInput) and 'step' not in widget.attrs: - attrs.setdefault('step', 'any') + if isinstance(widget, NumberInput) and "step" not in widget.attrs: + attrs.setdefault("step", "any") return attrs class DecimalField(IntegerField): default_error_messages = { - 'invalid': _('Enter a number.'), + "invalid": _("Enter a number."), } - def __init__(self, *, max_value=None, min_value=None, max_digits=None, decimal_places=None, **kwargs): + def __init__( + self, + *, + max_value=None, + min_value=None, + max_digits=None, + decimal_places=None, + **kwargs, + ): self.max_digits, self.decimal_places = max_digits, decimal_places super().__init__(max_value=max_value, min_value=min_value, **kwargs) self.validators.append(validators.DecimalValidator(max_digits, decimal_places)) @@ -346,7 +405,7 @@ class DecimalField(IntegerField): try: value = Decimal(str(value)) except DecimalException: - raise ValidationError(self.error_messages['invalid'], code='invalid') + raise ValidationError(self.error_messages["invalid"], code="invalid") return value def validate(self, value): @@ -355,26 +414,25 @@ class DecimalField(IntegerField): return if not value.is_finite(): raise ValidationError( - self.error_messages['invalid'], - code='invalid', - params={'value': value}, + self.error_messages["invalid"], + code="invalid", + params={"value": value}, ) def widget_attrs(self, widget): attrs = super().widget_attrs(widget) - if isinstance(widget, NumberInput) and 'step' not in widget.attrs: + if isinstance(widget, NumberInput) and "step" not in widget.attrs: if self.decimal_places is not None: # Use exponential notation for small values since they might # be parsed as 0 otherwise. ref #20765 step = str(Decimal(1).scaleb(-self.decimal_places)).lower() else: - step = 'any' - attrs.setdefault('step', step) + step = "any" + attrs.setdefault("step", step) return attrs class BaseTemporalField(Field): - def __init__(self, *, input_formats=None, **kwargs): super().__init__(**kwargs) if input_formats is not None: @@ -388,17 +446,17 @@ class BaseTemporalField(Field): return self.strptime(value, format) except (ValueError, TypeError): continue - raise ValidationError(self.error_messages['invalid'], code='invalid') + raise ValidationError(self.error_messages["invalid"], code="invalid") def strptime(self, value, format): - raise NotImplementedError('Subclasses must define this method.') + raise NotImplementedError("Subclasses must define this method.") class DateField(BaseTemporalField): widget = DateInput - input_formats = formats.get_format_lazy('DATE_INPUT_FORMATS') + input_formats = formats.get_format_lazy("DATE_INPUT_FORMATS") default_error_messages = { - 'invalid': _('Enter a valid date.'), + "invalid": _("Enter a valid date."), } def to_python(self, value): @@ -420,10 +478,8 @@ class DateField(BaseTemporalField): class TimeField(BaseTemporalField): widget = TimeInput - input_formats = formats.get_format_lazy('TIME_INPUT_FORMATS') - default_error_messages = { - 'invalid': _('Enter a valid time.') - } + input_formats = formats.get_format_lazy("TIME_INPUT_FORMATS") + default_error_messages = {"invalid": _("Enter a valid time.")} def to_python(self, value): """ @@ -442,15 +498,15 @@ class TimeField(BaseTemporalField): class DateTimeFormatsIterator: def __iter__(self): - yield from formats.get_format('DATETIME_INPUT_FORMATS') - yield from formats.get_format('DATE_INPUT_FORMATS') + yield from formats.get_format("DATETIME_INPUT_FORMATS") + yield from formats.get_format("DATE_INPUT_FORMATS") class DateTimeField(BaseTemporalField): widget = DateTimeInput input_formats = DateTimeFormatsIterator() default_error_messages = { - 'invalid': _('Enter a valid date/time.'), + "invalid": _("Enter a valid date/time."), } def prepare_value(self, value): @@ -473,7 +529,7 @@ class DateTimeField(BaseTemporalField): try: result = parse_datetime(value.strip()) except ValueError: - raise ValidationError(self.error_messages['invalid'], code='invalid') + raise ValidationError(self.error_messages["invalid"], code="invalid") if not result: result = super().to_python(value) return from_current_timezone(result) @@ -484,8 +540,8 @@ class DateTimeField(BaseTemporalField): class DurationField(Field): default_error_messages = { - 'invalid': _('Enter a valid duration.'), - 'overflow': _('The number of days must be between {min_days} and {max_days}.') + "invalid": _("Enter a valid duration."), + "overflow": _("The number of days must be between {min_days} and {max_days}."), } def prepare_value(self, value): @@ -501,12 +557,15 @@ class DurationField(Field): try: value = parse_duration(str(value)) except OverflowError: - raise ValidationError(self.error_messages['overflow'].format( - min_days=datetime.timedelta.min.days, - max_days=datetime.timedelta.max.days, - ), code='overflow') + raise ValidationError( + self.error_messages["overflow"].format( + min_days=datetime.timedelta.min.days, + max_days=datetime.timedelta.max.days, + ), + code="overflow", + ) if value is None: - raise ValidationError(self.error_messages['invalid'], code='invalid') + raise ValidationError(self.error_messages["invalid"], code="invalid") return value @@ -515,7 +574,7 @@ class RegexField(CharField): """ regex can be either a string or a compiled regular expression object. """ - kwargs.setdefault('strip', False) + kwargs.setdefault("strip", False) super().__init__(**kwargs) self._set_regex(regex) @@ -526,7 +585,10 @@ class RegexField(CharField): if isinstance(regex, str): regex = re.compile(regex) self._regex = regex - if hasattr(self, '_regex_validator') and self._regex_validator in self.validators: + if ( + hasattr(self, "_regex_validator") + and self._regex_validator in self.validators + ): self.validators.remove(self._regex_validator) self._regex_validator = validators.RegexValidator(regex=regex) self.validators.append(self._regex_validator) @@ -545,14 +607,17 @@ class EmailField(CharField): class FileField(Field): widget = ClearableFileInput default_error_messages = { - 'invalid': _("No file was submitted. Check the encoding type on the form."), - 'missing': _("No file was submitted."), - 'empty': _("The submitted file is empty."), - 'max_length': ngettext_lazy( - 'Ensure this filename has at most %(max)d character (it has %(length)d).', - 'Ensure this filename has at most %(max)d characters (it has %(length)d).', - 'max'), - 'contradiction': _('Please either submit a file or check the clear checkbox, not both.') + "invalid": _("No file was submitted. Check the encoding type on the form."), + "missing": _("No file was submitted."), + "empty": _("The submitted file is empty."), + "max_length": ngettext_lazy( + "Ensure this filename has at most %(max)d character (it has %(length)d).", + "Ensure this filename has at most %(max)d characters (it has %(length)d).", + "max", + ), + "contradiction": _( + "Please either submit a file or check the clear checkbox, not both." + ), } def __init__(self, *, max_length=None, allow_empty_file=False, **kwargs): @@ -569,22 +634,26 @@ class FileField(Field): file_name = data.name file_size = data.size except AttributeError: - raise ValidationError(self.error_messages['invalid'], code='invalid') + raise ValidationError(self.error_messages["invalid"], code="invalid") if self.max_length is not None and len(file_name) > self.max_length: - params = {'max': self.max_length, 'length': len(file_name)} - raise ValidationError(self.error_messages['max_length'], code='max_length', params=params) + params = {"max": self.max_length, "length": len(file_name)} + raise ValidationError( + self.error_messages["max_length"], code="max_length", params=params + ) if not file_name: - raise ValidationError(self.error_messages['invalid'], code='invalid') + raise ValidationError(self.error_messages["invalid"], code="invalid") if not self.allow_empty_file and not file_size: - raise ValidationError(self.error_messages['empty'], code='empty') + raise ValidationError(self.error_messages["empty"], code="empty") return data def clean(self, data, initial=None): # If the widget got contradictory inputs, we raise a validation error if data is FILE_INPUT_CONTRADICTION: - raise ValidationError(self.error_messages['contradiction'], code='contradiction') + raise ValidationError( + self.error_messages["contradiction"], code="contradiction" + ) # False means the field value should be cleared; further validation is # not needed. if data is False: @@ -612,7 +681,7 @@ class FileField(Field): class ImageField(FileField): default_validators = [validators.validate_image_file_extension] default_error_messages = { - 'invalid_image': _( + "invalid_image": _( "Upload a valid image. The file you uploaded was either not an " "image or a corrupted image." ), @@ -631,13 +700,13 @@ class ImageField(FileField): # We need to get a file object for Pillow. We might have a path or we might # have to read the data into memory. - if hasattr(data, 'temporary_file_path'): + if hasattr(data, "temporary_file_path"): file = data.temporary_file_path() else: - if hasattr(data, 'read'): + if hasattr(data, "read"): file = BytesIO(data.read()) else: - file = BytesIO(data['content']) + file = BytesIO(data["content"]) try: # load() could spot a truncated JPEG, but it loads the entire @@ -654,24 +723,24 @@ class ImageField(FileField): except Exception as exc: # Pillow doesn't recognize it as an image. raise ValidationError( - self.error_messages['invalid_image'], - code='invalid_image', + self.error_messages["invalid_image"], + code="invalid_image", ) from exc - if hasattr(f, 'seek') and callable(f.seek): + if hasattr(f, "seek") and callable(f.seek): f.seek(0) return f def widget_attrs(self, widget): attrs = super().widget_attrs(widget) - if isinstance(widget, FileInput) and 'accept' not in widget.attrs: - attrs.setdefault('accept', 'image/*') + if isinstance(widget, FileInput) and "accept" not in widget.attrs: + attrs.setdefault("accept", "image/*") return attrs class URLField(CharField): widget = URLInput default_error_messages = { - 'invalid': _('Enter a valid URL.'), + "invalid": _("Enter a valid URL."), } default_validators = [validators.URLValidator()] @@ -679,7 +748,6 @@ class URLField(CharField): super().__init__(strip=True, **kwargs) def to_python(self, value): - def split_url(url): """ Return a list of url parts via urlparse.urlsplit(), or raise @@ -690,19 +758,19 @@ class URLField(CharField): except ValueError: # urlparse.urlsplit can raise a ValueError with some # misformatted URLs. - raise ValidationError(self.error_messages['invalid'], code='invalid') + raise ValidationError(self.error_messages["invalid"], code="invalid") value = super().to_python(value) if value: url_fields = split_url(value) if not url_fields[0]: # If no URL scheme given, assume http:// - url_fields[0] = 'http' + url_fields[0] = "http" if not url_fields[1]: # Assume that if no domain is provided, that the path segment # contains the domain. url_fields[1] = url_fields[2] - url_fields[2] = '' + url_fields[2] = "" # Rebuild the url_fields list, since the domain segment may now # contain the path too. url_fields = split_url(urlunsplit(url_fields)) @@ -719,7 +787,7 @@ class BooleanField(Field): # will submit for False. Also check for '0', since this is what # RadioSelect will provide. Because bool("True") == bool('1') == True, # we don't need to handle that explicitly. - if isinstance(value, str) and value.lower() in ('false', '0'): + if isinstance(value, str) and value.lower() in ("false", "0"): value = False else: value = bool(value) @@ -727,7 +795,7 @@ class BooleanField(Field): def validate(self, value): if not value and self.required: - raise ValidationError(self.error_messages['required'], code='required') + raise ValidationError(self.error_messages["required"], code="required") def has_changed(self, initial, data): if self.disabled: @@ -742,6 +810,7 @@ class NullBooleanField(BooleanField): A field whose valid values are None, True, and False. Clean invalid values to None. """ + widget = NullBooleanSelect def to_python(self, value): @@ -753,9 +822,9 @@ class NullBooleanField(BooleanField): the Booleanfield, this field must check for True because it doesn't use the bool() function. """ - if value in (True, 'True', 'true', '1'): + if value in (True, "True", "true", "1"): return True - elif value in (False, 'False', 'false', '0'): + elif value in (False, "False", "false", "0"): return False else: return None @@ -775,7 +844,9 @@ class CallableChoiceIterator: class ChoiceField(Field): widget = Select default_error_messages = { - 'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'), + "invalid_choice": _( + "Select a valid choice. %(value)s is not one of the available choices." + ), } def __init__(self, *, choices=(), **kwargs): @@ -806,7 +877,7 @@ class ChoiceField(Field): def to_python(self, value): """Return a string.""" if value in self.empty_values: - return '' + return "" return str(value) def validate(self, value): @@ -814,9 +885,9 @@ class ChoiceField(Field): super().validate(value) if value and not self.valid_value(value): raise ValidationError( - self.error_messages['invalid_choice'], - code='invalid_choice', - params={'value': value}, + self.error_messages["invalid_choice"], + code="invalid_choice", + params={"value": value}, ) def valid_value(self, value): @@ -835,7 +906,7 @@ class ChoiceField(Field): class TypedChoiceField(ChoiceField): - def __init__(self, *, coerce=lambda val: val, empty_value='', **kwargs): + def __init__(self, *, coerce=lambda val: val, empty_value="", **kwargs): self.coerce = coerce self.empty_value = empty_value super().__init__(**kwargs) @@ -850,9 +921,9 @@ class TypedChoiceField(ChoiceField): value = self.coerce(value) except (ValueError, TypeError, ValidationError): raise ValidationError( - self.error_messages['invalid_choice'], - code='invalid_choice', - params={'value': value}, + self.error_messages["invalid_choice"], + code="invalid_choice", + params={"value": value}, ) return value @@ -865,28 +936,32 @@ class MultipleChoiceField(ChoiceField): hidden_widget = MultipleHiddenInput widget = SelectMultiple default_error_messages = { - 'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'), - 'invalid_list': _('Enter a list of values.'), + "invalid_choice": _( + "Select a valid choice. %(value)s is not one of the available choices." + ), + "invalid_list": _("Enter a list of values."), } def to_python(self, value): if not value: return [] elif not isinstance(value, (list, tuple)): - raise ValidationError(self.error_messages['invalid_list'], code='invalid_list') + raise ValidationError( + self.error_messages["invalid_list"], code="invalid_list" + ) return [str(val) for val in value] def validate(self, value): """Validate that the input is a list or tuple.""" if self.required and not value: - raise ValidationError(self.error_messages['required'], code='required') + raise ValidationError(self.error_messages["required"], code="required") # Validate that each value in the value list is in self.choices. for val in value: if not self.valid_value(val): raise ValidationError( - self.error_messages['invalid_choice'], - code='invalid_choice', - params={'value': val}, + self.error_messages["invalid_choice"], + code="invalid_choice", + params={"value": val}, ) def has_changed(self, initial, data): @@ -906,7 +981,7 @@ class MultipleChoiceField(ChoiceField): class TypedMultipleChoiceField(MultipleChoiceField): def __init__(self, *, coerce=lambda val: val, **kwargs): self.coerce = coerce - self.empty_value = kwargs.pop('empty_value', []) + self.empty_value = kwargs.pop("empty_value", []) super().__init__(**kwargs) def _coerce(self, value): @@ -922,9 +997,9 @@ class TypedMultipleChoiceField(MultipleChoiceField): new_value.append(self.coerce(choice)) except (ValueError, TypeError, ValidationError): raise ValidationError( - self.error_messages['invalid_choice'], - code='invalid_choice', - params={'value': choice}, + self.error_messages["invalid_choice"], + code="invalid_choice", + params={"value": choice}, ) return new_value @@ -936,13 +1011,14 @@ class TypedMultipleChoiceField(MultipleChoiceField): if value != self.empty_value: super().validate(value) elif self.required: - raise ValidationError(self.error_messages['required'], code='required') + raise ValidationError(self.error_messages["required"], code="required") class ComboField(Field): """ A Field whose clean() method calls multiple Field clean() methods. """ + def __init__(self, fields, **kwargs): super().__init__(**kwargs) # Set 'required' to False on the individual fields, because the @@ -980,17 +1056,17 @@ class MultiValueField(Field): You'll probably want to use this with MultiWidget. """ + default_error_messages = { - 'invalid': _('Enter a list of values.'), - 'incomplete': _('Enter a complete value.'), + "invalid": _("Enter a list of values."), + "incomplete": _("Enter a complete value."), } def __init__(self, fields, *, require_all_fields=True, **kwargs): self.require_all_fields = require_all_fields super().__init__(**kwargs) for f in fields: - f.error_messages.setdefault('incomplete', - self.error_messages['incomplete']) + f.error_messages.setdefault("incomplete", self.error_messages["incomplete"]) if self.disabled: f.disabled = True if self.require_all_fields: @@ -1024,11 +1100,13 @@ class MultiValueField(Field): if not value or isinstance(value, (list, tuple)): if not value or not [v for v in value if v not in self.empty_values]: if self.required: - raise ValidationError(self.error_messages['required'], code='required') + raise ValidationError( + self.error_messages["required"], code="required" + ) else: return self.compress([]) else: - raise ValidationError(self.error_messages['invalid'], code='invalid') + raise ValidationError(self.error_messages["invalid"], code="invalid") for i, field in enumerate(self.fields): try: field_value = value[i] @@ -1039,13 +1117,15 @@ class MultiValueField(Field): # Raise a 'required' error if the MultiValueField is # required and any field is empty. if self.required: - raise ValidationError(self.error_messages['required'], code='required') + raise ValidationError( + self.error_messages["required"], code="required" + ) elif field.required: # Otherwise, add an 'incomplete' error to the list of # collected errors and skip field cleaning, if a required # field is empty. - if field.error_messages['incomplete'] not in errors: - errors.append(field.error_messages['incomplete']) + if field.error_messages["incomplete"] not in errors: + errors.append(field.error_messages["incomplete"]) continue try: clean_data.append(field.clean(field_value)) @@ -1071,13 +1151,13 @@ class MultiValueField(Field): fields=(DateField(), TimeField()), this might return a datetime object created by combining the date and time in data_list. """ - raise NotImplementedError('Subclasses must implement this method.') + raise NotImplementedError("Subclasses must implement this method.") def has_changed(self, initial, data): if self.disabled: return False if initial is None: - initial = ['' for x in range(0, len(data))] + initial = ["" for x in range(0, len(data))] else: if not isinstance(initial, list): initial = self.widget.decompress(initial) @@ -1092,8 +1172,16 @@ class MultiValueField(Field): class FilePathField(ChoiceField): - def __init__(self, path, *, match=None, recursive=False, allow_files=True, - allow_folders=False, **kwargs): + def __init__( + self, + path, + *, + match=None, + recursive=False, + allow_files=True, + allow_folders=False, + **kwargs, + ): self.path, self.match, self.recursive = path, match, recursive self.allow_files, self.allow_folders = allow_files, allow_folders super().__init__(choices=(), **kwargs) @@ -1115,7 +1203,7 @@ class FilePathField(ChoiceField): self.choices.append((f, f.replace(path, "", 1))) if self.allow_folders: for f in sorted(dirs): - if f == '__pycache__': + if f == "__pycache__": continue if self.match is None or self.match_re.search(f): f = os.path.join(root, f) @@ -1124,12 +1212,12 @@ class FilePathField(ChoiceField): choices = [] with os.scandir(self.path) as entries: for f in entries: - if f.name == '__pycache__': + if f.name == "__pycache__": continue - if (( - (self.allow_files and f.is_file()) or - (self.allow_folders and f.is_dir()) - ) and (self.match is None or self.match_re.search(f.name))): + if ( + (self.allow_files and f.is_file()) + or (self.allow_folders and f.is_dir()) + ) and (self.match is None or self.match_re.search(f.name)): choices.append((f.path, f.name)) choices.sort(key=operator.itemgetter(1)) self.choices.extend(choices) @@ -1141,22 +1229,26 @@ class SplitDateTimeField(MultiValueField): widget = SplitDateTimeWidget hidden_widget = SplitHiddenDateTimeWidget default_error_messages = { - 'invalid_date': _('Enter a valid date.'), - 'invalid_time': _('Enter a valid time.'), + "invalid_date": _("Enter a valid date."), + "invalid_time": _("Enter a valid time."), } def __init__(self, *, input_date_formats=None, input_time_formats=None, **kwargs): errors = self.default_error_messages.copy() - if 'error_messages' in kwargs: - errors.update(kwargs['error_messages']) - localize = kwargs.get('localize', False) + if "error_messages" in kwargs: + errors.update(kwargs["error_messages"]) + localize = kwargs.get("localize", False) fields = ( - DateField(input_formats=input_date_formats, - error_messages={'invalid': errors['invalid_date']}, - localize=localize), - TimeField(input_formats=input_time_formats, - error_messages={'invalid': errors['invalid_time']}, - localize=localize), + DateField( + input_formats=input_date_formats, + error_messages={"invalid": errors["invalid_date"]}, + localize=localize, + ), + TimeField( + input_formats=input_time_formats, + error_messages={"invalid": errors["invalid_time"]}, + localize=localize, + ), ) super().__init__(fields, **kwargs) @@ -1165,25 +1257,31 @@ class SplitDateTimeField(MultiValueField): # Raise a validation error if time or date is empty # (possible if SplitDateTimeField has required=False). if data_list[0] in self.empty_values: - raise ValidationError(self.error_messages['invalid_date'], code='invalid_date') + raise ValidationError( + self.error_messages["invalid_date"], code="invalid_date" + ) if data_list[1] in self.empty_values: - raise ValidationError(self.error_messages['invalid_time'], code='invalid_time') + raise ValidationError( + self.error_messages["invalid_time"], code="invalid_time" + ) result = datetime.datetime.combine(*data_list) return from_current_timezone(result) return None class GenericIPAddressField(CharField): - def __init__(self, *, protocol='both', unpack_ipv4=False, **kwargs): + def __init__(self, *, protocol="both", unpack_ipv4=False, **kwargs): self.unpack_ipv4 = unpack_ipv4 - self.default_validators = validators.ip_address_validators(protocol, unpack_ipv4)[0] + self.default_validators = validators.ip_address_validators( + protocol, unpack_ipv4 + )[0] super().__init__(**kwargs) def to_python(self, value): if value in self.empty_values: - return '' + return "" value = value.strip() - if value and ':' in value: + if value and ":" in value: return clean_ipv6_address(value, self.unpack_ipv4) return value @@ -1200,7 +1298,7 @@ class SlugField(CharField): class UUIDField(CharField): default_error_messages = { - 'invalid': _('Enter a valid UUID.'), + "invalid": _("Enter a valid UUID."), } def prepare_value(self, value): @@ -1216,7 +1314,7 @@ class UUIDField(CharField): try: value = uuid.UUID(value) except ValueError: - raise ValidationError(self.error_messages['invalid'], code='invalid') + raise ValidationError(self.error_messages["invalid"], code="invalid") return value @@ -1230,7 +1328,7 @@ class JSONString(str): class JSONField(CharField): default_error_messages = { - 'invalid': _('Enter a valid JSON.'), + "invalid": _("Enter a valid JSON."), } widget = Textarea @@ -1250,9 +1348,9 @@ class JSONField(CharField): converted = json.loads(value, cls=self.decoder) except json.JSONDecodeError: raise ValidationError( - self.error_messages['invalid'], - code='invalid', - params={'value': value}, + self.error_messages["invalid"], + code="invalid", + params={"value": value}, ) if isinstance(converted, str): return JSONString(converted) @@ -1279,7 +1377,6 @@ class JSONField(CharField): return True # For purposes of seeing whether something has changed, True isn't the # same as 1 and the order of keys doesn't matter. - return ( - json.dumps(initial, sort_keys=True, cls=self.encoder) != - json.dumps(self.to_python(data), sort_keys=True, cls=self.encoder) + return json.dumps(initial, sort_keys=True, cls=self.encoder) != json.dumps( + self.to_python(data), sort_keys=True, cls=self.encoder ) diff --git a/django/forms/forms.py b/django/forms/forms.py index 589b4693fd..952b974130 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -19,15 +19,17 @@ from django.utils.translation import gettext as _ from .renderers import get_default_renderer -__all__ = ('BaseForm', 'Form') +__all__ = ("BaseForm", "Form") class DeclarativeFieldsMetaclass(MediaDefiningClass): """Collect Fields declared on the base classes.""" + def __new__(mcs, name, bases, attrs): # Collect fields from current class and remove them from attrs. - attrs['declared_fields'] = { - key: attrs.pop(key) for key, value in list(attrs.items()) + attrs["declared_fields"] = { + key: attrs.pop(key) + for key, value in list(attrs.items()) if isinstance(value, Field) } @@ -37,7 +39,7 @@ class DeclarativeFieldsMetaclass(MediaDefiningClass): declared_fields = {} for base in reversed(new_class.__mro__): # Collect fields from base class. - if hasattr(base, 'declared_fields'): + if hasattr(base, "declared_fields"): declared_fields.update(base.declared_fields) # Field shadowing. @@ -58,20 +60,32 @@ class BaseForm(RenderableFormMixin): improvements to the form API should be made to this class, not to the Form class. """ + default_renderer = None field_order = None prefix = None use_required_attribute = True - template_name = 'django/forms/default.html' - template_name_p = 'django/forms/p.html' - template_name_table = 'django/forms/table.html' - template_name_ul = 'django/forms/ul.html' - template_name_label = 'django/forms/label.html' + template_name = "django/forms/default.html" + template_name_p = "django/forms/p.html" + template_name_table = "django/forms/table.html" + template_name_ul = "django/forms/ul.html" + template_name_label = "django/forms/label.html" - def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, - initial=None, error_class=ErrorList, label_suffix=None, - empty_permitted=False, field_order=None, use_required_attribute=None, renderer=None): + def __init__( + self, + data=None, + files=None, + auto_id="id_%s", + prefix=None, + initial=None, + error_class=ErrorList, + label_suffix=None, + empty_permitted=False, + field_order=None, + use_required_attribute=None, + renderer=None, + ): self.is_bound = data is not None or files is not None self.data = MultiValueDict() if data is None else data self.files = MultiValueDict() if files is None else files @@ -81,7 +95,7 @@ class BaseForm(RenderableFormMixin): self.initial = initial or {} self.error_class = error_class # Translators: This is the default suffix added to form field labels - self.label_suffix = label_suffix if label_suffix is not None else _(':') + self.label_suffix = label_suffix if label_suffix is not None else _(":") self.empty_permitted = empty_permitted self._errors = None # Stores the errors after clean() has been called. @@ -99,8 +113,8 @@ class BaseForm(RenderableFormMixin): if self.empty_permitted and self.use_required_attribute: raise ValueError( - 'The empty_permitted and use_required_attribute arguments may ' - 'not both be True.' + "The empty_permitted and use_required_attribute arguments may " + "not both be True." ) # Initialize form renderer. Use a global default if not specified @@ -141,11 +155,11 @@ class BaseForm(RenderableFormMixin): is_valid = "Unknown" else: is_valid = self.is_bound and not self._errors - return '<%(cls)s bound=%(bound)s, valid=%(valid)s, fields=(%(fields)s)>' % { - 'cls': self.__class__.__name__, - 'bound': self.is_bound, - 'valid': is_valid, - 'fields': ';'.join(self.fields), + return "<%(cls)s bound=%(bound)s, valid=%(valid)s, fields=(%(fields)s)>" % { + "cls": self.__class__.__name__, + "bound": self.is_bound, + "valid": is_valid, + "fields": ";".join(self.fields), } def _bound_items(self): @@ -168,10 +182,11 @@ class BaseForm(RenderableFormMixin): field = self.fields[name] except KeyError: raise KeyError( - "Key '%s' not found in '%s'. Choices are: %s." % ( + "Key '%s' not found in '%s'. Choices are: %s." + % ( name, self.__class__.__name__, - ', '.join(sorted(self.fields)), + ", ".join(sorted(self.fields)), ) ) bound_field = field.get_bound_field(self, name) @@ -196,11 +211,11 @@ class BaseForm(RenderableFormMixin): Subclasses may wish to override. """ - return '%s-%s' % (self.prefix, field_name) if self.prefix else field_name + return "%s-%s" % (self.prefix, field_name) if self.prefix else field_name def add_initial_prefix(self, field_name): """Add an 'initial' prefix for checking dynamic initial values.""" - return 'initial-%s' % self.add_prefix(field_name) + return "initial-%s" % self.add_prefix(field_name) def _widget_data_value(self, widget, html_name): # value_from_datadict() gets the data from the data dictionaries. @@ -208,11 +223,13 @@ class BaseForm(RenderableFormMixin): # widgets split data over several HTML fields. return widget.value_from_datadict(self.data, self.files, html_name) - def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row): + def _html_output( + self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row + ): "Output HTML. Used by as_table(), as_ul(), as_p()." warnings.warn( - 'django.forms.BaseForm._html_output() is deprecated. ' - 'Please use .render() and .get_context() instead.', + "django.forms.BaseForm._html_output() is deprecated. " + "Please use .render() and .get_context() instead.", RemovedInDjango50Warning, stacklevel=2, ) @@ -222,13 +239,17 @@ class BaseForm(RenderableFormMixin): for name, bf in self._bound_items(): field = bf.field - html_class_attr = '' + html_class_attr = "" bf_errors = self.error_class(bf.errors) if bf.is_hidden: if bf_errors: top_errors.extend( - [_('(Hidden field %(name)s) %(error)s') % {'name': name, 'error': str(e)} - for e in bf_errors]) + [ + _("(Hidden field %(name)s) %(error)s") + % {"name": name, "error": str(e)} + for e in bf_errors + ] + ) hidden_fields.append(str(bf)) else: # Create a 'class="..."' attribute if the row should have any @@ -242,30 +263,33 @@ class BaseForm(RenderableFormMixin): if bf.label: label = conditional_escape(bf.label) - label = bf.label_tag(label) or '' + label = bf.label_tag(label) or "" else: - label = '' + label = "" if field.help_text: help_text = help_text_html % field.help_text else: - help_text = '' + help_text = "" - output.append(normal_row % { - 'errors': bf_errors, - 'label': label, - 'field': bf, - 'help_text': help_text, - 'html_class_attr': html_class_attr, - 'css_classes': css_classes, - 'field_name': bf.html_name, - }) + output.append( + normal_row + % { + "errors": bf_errors, + "label": label, + "field": bf, + "help_text": help_text, + "html_class_attr": html_class_attr, + "css_classes": css_classes, + "field_name": bf.html_name, + } + ) if top_errors: output.insert(0, error_row % top_errors) if hidden_fields: # Insert any hidden fields in the last row. - str_hidden = ''.join(hidden_fields) + str_hidden = "".join(hidden_fields) if output: last_row = output[-1] # Chop off the trailing row_ender (e.g. '</td></tr>') and @@ -275,22 +299,22 @@ class BaseForm(RenderableFormMixin): # that users write): if there are only top errors, we may # not be able to conscript the last row for our purposes, # so insert a new, empty row. - last_row = (normal_row % { - 'errors': '', - 'label': '', - 'field': '', - 'help_text': '', - 'html_class_attr': html_class_attr, - 'css_classes': '', - 'field_name': '', - }) + last_row = normal_row % { + "errors": "", + "label": "", + "field": "", + "help_text": "", + "html_class_attr": html_class_attr, + "css_classes": "", + "field_name": "", + } output.append(last_row) - output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender + output[-1] = last_row[: -len(row_ender)] + str_hidden + row_ender else: # If there aren't any rows in the output, just append the # hidden fields. output.append(str_hidden) - return mark_safe('\n'.join(output)) + return mark_safe("\n".join(output)) def get_context(self): fields = [] @@ -301,7 +325,8 @@ class BaseForm(RenderableFormMixin): if bf.is_hidden: if bf_errors: top_errors += [ - _('(Hidden field %(name)s) %(error)s') % {'name': name, 'error': str(e)} + _("(Hidden field %(name)s) %(error)s") + % {"name": name, "error": str(e)} for e in bf_errors ] hidden_fields.append(bf) @@ -310,18 +335,18 @@ class BaseForm(RenderableFormMixin): # RemovedInDjango50Warning. if not isinstance(errors_str, SafeString): warnings.warn( - f'Returning a plain string from ' - f'{self.error_class.__name__} is deprecated. Please ' - f'customize via the template system instead.', + f"Returning a plain string from " + f"{self.error_class.__name__} is deprecated. Please " + f"customize via the template system instead.", RemovedInDjango50Warning, ) errors_str = mark_safe(errors_str) fields.append((bf, errors_str)) return { - 'form': self, - 'fields': fields, - 'hidden_fields': hidden_fields, - 'errors': top_errors, + "form": self, + "fields": fields, + "hidden_fields": hidden_fields, + "errors": top_errors, } def non_field_errors(self): @@ -332,7 +357,7 @@ class BaseForm(RenderableFormMixin): """ return self.errors.get( NON_FIELD_ERRORS, - self.error_class(error_class='nonfield', renderer=self.renderer), + self.error_class(error_class="nonfield", renderer=self.renderer), ) def add_error(self, field, error): @@ -358,7 +383,7 @@ class BaseForm(RenderableFormMixin): # do the hard work of making sense of the input. error = ValidationError(error) - if hasattr(error, 'error_dict'): + if hasattr(error, "error_dict"): if field is not None: raise TypeError( "The argument `field` must be `None` when the `error` " @@ -373,9 +398,13 @@ class BaseForm(RenderableFormMixin): if field not in self.errors: if field != NON_FIELD_ERRORS and field not in self.fields: raise ValueError( - "'%s' has no field named '%s'." % (self.__class__.__name__, field)) + "'%s' has no field named '%s'." + % (self.__class__.__name__, field) + ) if field == NON_FIELD_ERRORS: - self._errors[field] = self.error_class(error_class='nonfield', renderer=self.renderer) + self._errors[field] = self.error_class( + error_class="nonfield", renderer=self.renderer + ) else: self._errors[field] = self.error_class(renderer=self.renderer) self._errors[field].extend(error_list) @@ -384,8 +413,8 @@ class BaseForm(RenderableFormMixin): def has_error(self, field, code=None): return field in self.errors and ( - code is None or - any(error.code == code for error in self.errors.as_data()[field]) + code is None + or any(error.code == code for error in self.errors.as_data()[field]) ) def full_clean(self): @@ -415,8 +444,8 @@ class BaseForm(RenderableFormMixin): else: value = field.clean(value) self.cleaned_data[name] = value - if hasattr(self, 'clean_%s' % name): - value = getattr(self, 'clean_%s' % name)() + if hasattr(self, "clean_%s" % name): + value = getattr(self, "clean_%s" % name)() self.cleaned_data[name] = value except ValidationError as e: self.add_error(name, e) @@ -493,8 +522,10 @@ class BaseForm(RenderableFormMixin): value = value() # If this is an auto-generated default date, nix the microseconds # for standardized handling. See #22502. - if (isinstance(value, (datetime.datetime, datetime.time)) and - not field.widget.supports_microseconds): + if ( + isinstance(value, (datetime.datetime, datetime.time)) + and not field.widget.supports_microseconds + ): value = value.replace(microsecond=0) return value diff --git a/django/forms/formsets.py b/django/forms/formsets.py index 75b0646512..e5807e8688 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -5,17 +5,18 @@ from django.forms.renderers import get_default_renderer from django.forms.utils import ErrorList, RenderableFormMixin from django.forms.widgets import CheckboxInput, HiddenInput, NumberInput from django.utils.functional import cached_property -from django.utils.translation import gettext_lazy as _, ngettext +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import ngettext -__all__ = ('BaseFormSet', 'formset_factory', 'all_valid') +__all__ = ("BaseFormSet", "formset_factory", "all_valid") # special field names -TOTAL_FORM_COUNT = 'TOTAL_FORMS' -INITIAL_FORM_COUNT = 'INITIAL_FORMS' -MIN_NUM_FORM_COUNT = 'MIN_NUM_FORMS' -MAX_NUM_FORM_COUNT = 'MAX_NUM_FORMS' -ORDERING_FIELD_NAME = 'ORDER' -DELETION_FIELD_NAME = 'DELETE' +TOTAL_FORM_COUNT = "TOTAL_FORMS" +INITIAL_FORM_COUNT = "INITIAL_FORMS" +MIN_NUM_FORM_COUNT = "MIN_NUM_FORMS" +MAX_NUM_FORM_COUNT = "MAX_NUM_FORMS" +ORDERING_FIELD_NAME = "ORDER" +DELETION_FIELD_NAME = "DELETE" # default minimum number of forms in a formset DEFAULT_MIN_NUM = 0 @@ -30,6 +31,7 @@ class ManagementForm(Form): new forms via JavaScript, you should increment the count field of this form as well. """ + TOTAL_FORMS = IntegerField(widget=HiddenInput) INITIAL_FORMS = IntegerField(widget=HiddenInput) # MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT are output with the rest of the @@ -51,22 +53,31 @@ class BaseFormSet(RenderableFormMixin): """ A collection of instances of the same Form class. """ + deletion_widget = CheckboxInput ordering_widget = NumberInput default_error_messages = { - 'missing_management_form': _( - 'ManagementForm data is missing or has been tampered with. Missing fields: ' - '%(field_names)s. You may need to file a bug report if the issue persists.' + "missing_management_form": _( + "ManagementForm data is missing or has been tampered with. Missing fields: " + "%(field_names)s. You may need to file a bug report if the issue persists." ), } - template_name = 'django/forms/formsets/default.html' - template_name_p = 'django/forms/formsets/p.html' - template_name_table = 'django/forms/formsets/table.html' - template_name_ul = 'django/forms/formsets/ul.html' + template_name = "django/forms/formsets/default.html" + template_name_p = "django/forms/formsets/p.html" + template_name_table = "django/forms/formsets/table.html" + template_name_ul = "django/forms/formsets/ul.html" - def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, - initial=None, error_class=ErrorList, form_kwargs=None, - error_messages=None): + def __init__( + self, + data=None, + files=None, + auto_id="id_%s", + prefix=None, + initial=None, + error_class=ErrorList, + form_kwargs=None, + error_messages=None, + ): self.is_bound = data is not None or files is not None self.prefix = prefix or self.get_default_prefix() self.auto_id = auto_id @@ -80,7 +91,7 @@ class BaseFormSet(RenderableFormMixin): messages = {} for cls in reversed(type(self).__mro__): - messages.update(getattr(cls, 'default_error_messages', {})) + messages.update(getattr(cls, "default_error_messages", {})) if error_messages is not None: messages.update(error_messages) self.error_messages = messages @@ -105,14 +116,14 @@ class BaseFormSet(RenderableFormMixin): def __repr__(self): if self._errors is None: - is_valid = 'Unknown' + is_valid = "Unknown" else: is_valid = ( - self.is_bound and - not self._non_form_errors and - not any(form_errors for form_errors in self._errors) + self.is_bound + and not self._non_form_errors + and not any(form_errors for form_errors in self._errors) ) - return '<%s: bound=%s valid=%s total_forms=%s>' % ( + return "<%s: bound=%s valid=%s total_forms=%s>" % ( self.__class__.__qualname__, self.is_bound, is_valid, @@ -123,7 +134,12 @@ class BaseFormSet(RenderableFormMixin): def management_form(self): """Return the ManagementForm instance for this FormSet.""" if self.is_bound: - form = ManagementForm(self.data, auto_id=self.auto_id, prefix=self.prefix, renderer=self.renderer) + form = ManagementForm( + self.data, + auto_id=self.auto_id, + prefix=self.prefix, + renderer=self.renderer, + ) form.full_clean() else: form = ManagementForm( @@ -146,7 +162,9 @@ class BaseFormSet(RenderableFormMixin): # count in the data; this is DoS protection to prevent clients # from forcing the server to instantiate arbitrary numbers of # forms - return min(self.management_form.cleaned_data[TOTAL_FORM_COUNT], self.absolute_max) + return min( + self.management_form.cleaned_data[TOTAL_FORM_COUNT], self.absolute_max + ) else: initial_forms = self.initial_form_count() total_forms = max(initial_forms, self.min_num) + self.extra @@ -188,27 +206,27 @@ class BaseFormSet(RenderableFormMixin): def _construct_form(self, i, **kwargs): """Instantiate and return the i-th form instance in a formset.""" defaults = { - 'auto_id': self.auto_id, - 'prefix': self.add_prefix(i), - 'error_class': self.error_class, + "auto_id": self.auto_id, + "prefix": self.add_prefix(i), + "error_class": self.error_class, # Don't render the HTML 'required' attribute as it may cause # incorrect validation for extra, optional, and deleted # forms in the formset. - 'use_required_attribute': False, - 'renderer': self.renderer, + "use_required_attribute": False, + "renderer": self.renderer, } if self.is_bound: - defaults['data'] = self.data - defaults['files'] = self.files - if self.initial and 'initial' not in kwargs: + defaults["data"] = self.data + defaults["files"] = self.files + if self.initial and "initial" not in kwargs: try: - defaults['initial'] = self.initial[i] + defaults["initial"] = self.initial[i] except IndexError: pass # Allow extra forms to be empty, unless they're part of # the minimum forms. if i >= self.initial_form_count() and i >= self.min_num: - defaults['empty_permitted'] = True + defaults["empty_permitted"] = True defaults.update(kwargs) form = self.form(**defaults) self.add_fields(form, i) @@ -217,18 +235,18 @@ class BaseFormSet(RenderableFormMixin): @property def initial_forms(self): """Return a list of all the initial forms in this formset.""" - return self.forms[:self.initial_form_count()] + return self.forms[: self.initial_form_count()] @property def extra_forms(self): """Return a list of all the extra forms in this formset.""" - return self.forms[self.initial_form_count():] + return self.forms[self.initial_form_count() :] @property def empty_form(self): form = self.form( auto_id=self.auto_id, - prefix=self.add_prefix('__prefix__'), + prefix=self.add_prefix("__prefix__"), empty_permitted=True, use_required_attribute=False, **self.get_form_kwargs(None), @@ -243,7 +261,9 @@ class BaseFormSet(RenderableFormMixin): Return a list of form.cleaned_data dicts for every form in self.forms. """ if not self.is_valid(): - raise AttributeError("'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__) + raise AttributeError( + "'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__ + ) return [form.cleaned_data for form in self.forms] @property @@ -253,7 +273,7 @@ class BaseFormSet(RenderableFormMixin): return [] # construct _deleted_form_indexes which is just a list of form indexes # that have had their deletion widget set to True - if not hasattr(self, '_deleted_form_indexes'): + if not hasattr(self, "_deleted_form_indexes"): self._deleted_form_indexes = [] for i, form in enumerate(self.forms): # if this is an extra form and hasn't changed, don't consider it @@ -270,12 +290,14 @@ class BaseFormSet(RenderableFormMixin): Raise an AttributeError if ordering is not allowed. """ if not self.is_valid() or not self.can_order: - raise AttributeError("'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__) + raise AttributeError( + "'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__ + ) # Construct _ordering, which is a list of (form_index, order_field_value) # tuples. After constructing this list, we'll sort it by order_field_value # so we have a way to get to the form indexes in the order specified # by the form data. - if not hasattr(self, '_ordering'): + if not hasattr(self, "_ordering"): self._ordering = [] for i, form in enumerate(self.forms): # if this is an extra form and hasn't changed, don't consider it @@ -295,6 +317,7 @@ class BaseFormSet(RenderableFormMixin): if k[1] is None: return (1, 0) # +infinity, larger than any number return (0, k[1]) + self._ordering.sort(key=compare_ordering_key) # Return a list of form.cleaned_data dicts in the order specified by # the form data. @@ -302,7 +325,7 @@ class BaseFormSet(RenderableFormMixin): @classmethod def get_default_prefix(cls): - return 'form' + return "form" @classmethod def get_deletion_widget(cls): @@ -331,8 +354,9 @@ class BaseFormSet(RenderableFormMixin): def total_error_count(self): """Return the number of errors across all forms in the formset.""" - return len(self.non_form_errors()) +\ - sum(len(form_errors) for form_errors in self.errors) + return len(self.non_form_errors()) + sum( + len(form_errors) for form_errors in self.errors + ) def _should_delete_form(self, form): """Return whether or not the form was marked for deletion.""" @@ -346,10 +370,13 @@ class BaseFormSet(RenderableFormMixin): self.errors # List comprehension ensures is_valid() is called for all forms. # Forms due to be deleted shouldn't cause the formset to be invalid. - forms_valid = all([ - form.is_valid() for form in self.forms - if not (self.can_delete and self._should_delete_form(form)) - ]) + forms_valid = all( + [ + form.is_valid() + for form in self.forms + if not (self.can_delete and self._should_delete_form(form)) + ] + ) return forms_valid and not self.non_form_errors() def full_clean(self): @@ -358,7 +385,9 @@ class BaseFormSet(RenderableFormMixin): self._non_form_errors. """ self._errors = [] - self._non_form_errors = self.error_class(error_class='nonform', renderer=self.renderer) + self._non_form_errors = self.error_class( + error_class="nonform", renderer=self.renderer + ) empty_forms_count = 0 if not self.is_bound: # Stop further processing. @@ -366,14 +395,14 @@ class BaseFormSet(RenderableFormMixin): if not self.management_form.is_valid(): error = ValidationError( - self.error_messages['missing_management_form'], + self.error_messages["missing_management_form"], params={ - 'field_names': ', '.join( + "field_names": ", ".join( self.management_form.add_prefix(field_name) for field_name in self.management_form.errors ), }, - code='missing_management_form', + code="missing_management_form", ) self._non_form_errors.append(error) @@ -388,26 +417,43 @@ class BaseFormSet(RenderableFormMixin): continue self._errors.append(form_errors) try: - if (self.validate_max and - self.total_form_count() - len(self.deleted_forms) > self.max_num) or \ - self.management_form.cleaned_data[TOTAL_FORM_COUNT] > self.absolute_max: - raise ValidationError(ngettext( - "Please submit at most %d form.", - "Please submit at most %d forms.", self.max_num) % self.max_num, - code='too_many_forms', + if ( + self.validate_max + and self.total_form_count() - len(self.deleted_forms) > self.max_num + ) or self.management_form.cleaned_data[ + TOTAL_FORM_COUNT + ] > self.absolute_max: + raise ValidationError( + ngettext( + "Please submit at most %d form.", + "Please submit at most %d forms.", + self.max_num, + ) + % self.max_num, + code="too_many_forms", + ) + if ( + self.validate_min + and self.total_form_count() + - len(self.deleted_forms) + - empty_forms_count + < self.min_num + ): + raise ValidationError( + ngettext( + "Please submit at least %d form.", + "Please submit at least %d forms.", + self.min_num, + ) + % self.min_num, + code="too_few_forms", ) - if (self.validate_min and - self.total_form_count() - len(self.deleted_forms) - empty_forms_count < self.min_num): - raise ValidationError(ngettext( - "Please submit at least %d form.", - "Please submit at least %d forms.", self.min_num) % self.min_num, - code='too_few_forms') # Give self.clean() a chance to do cross-form validation. self.clean() except ValidationError as e: self._non_form_errors = self.error_class( e.error_list, - error_class='nonform', + error_class="nonform", renderer=self.renderer, ) @@ -431,26 +477,26 @@ class BaseFormSet(RenderableFormMixin): # Only pre-fill the ordering field for initial forms. if index is not None and index < initial_form_count: form.fields[ORDERING_FIELD_NAME] = IntegerField( - label=_('Order'), + label=_("Order"), initial=index + 1, required=False, widget=self.get_ordering_widget(), ) else: form.fields[ORDERING_FIELD_NAME] = IntegerField( - label=_('Order'), + label=_("Order"), required=False, widget=self.get_ordering_widget(), ) if self.can_delete and (self.can_delete_extra or index < initial_form_count): form.fields[DELETION_FIELD_NAME] = BooleanField( - label=_('Delete'), + label=_("Delete"), required=False, widget=self.get_deletion_widget(), ) def add_prefix(self, index): - return '%s-%s' % (self.prefix, index) + return "%s-%s" % (self.prefix, index) def is_multipart(self): """ @@ -472,13 +518,23 @@ class BaseFormSet(RenderableFormMixin): return self.empty_form.media def get_context(self): - return {'formset': self} + return {"formset": self} -def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, - can_delete=False, max_num=None, validate_max=False, - min_num=None, validate_min=False, absolute_max=None, - can_delete_extra=True, renderer=None): +def formset_factory( + form, + formset=BaseFormSet, + extra=1, + can_order=False, + can_delete=False, + max_num=None, + validate_max=False, + min_num=None, + validate_min=False, + absolute_max=None, + can_delete_extra=True, + renderer=None, +): """Return a FormSet for the given form class.""" if min_num is None: min_num = DEFAULT_MIN_NUM @@ -490,23 +546,21 @@ def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, if absolute_max is None: absolute_max = max_num + DEFAULT_MAX_NUM if max_num > absolute_max: - raise ValueError( - "'absolute_max' must be greater or equal to 'max_num'." - ) + raise ValueError("'absolute_max' must be greater or equal to 'max_num'.") attrs = { - 'form': form, - 'extra': extra, - 'can_order': can_order, - 'can_delete': can_delete, - 'can_delete_extra': can_delete_extra, - 'min_num': min_num, - 'max_num': max_num, - 'absolute_max': absolute_max, - 'validate_min': validate_min, - 'validate_max': validate_max, - 'renderer': renderer or get_default_renderer(), + "form": form, + "extra": extra, + "can_order": can_order, + "can_delete": can_delete, + "can_delete_extra": can_delete_extra, + "min_num": min_num, + "max_num": max_num, + "absolute_max": absolute_max, + "validate_min": validate_min, + "validate_max": validate_max, + "renderer": renderer or get_default_renderer(), } - return type(form.__name__ + 'FormSet', (formset,), attrs) + return type(form.__name__ + "FormSet", (formset,), attrs) def all_valid(formsets): diff --git a/django/forms/models.py b/django/forms/models.py index 19a5cb142a..a55af8eeb6 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -5,26 +5,41 @@ and database field objects. from itertools import chain from django.core.exceptions import ( - NON_FIELD_ERRORS, FieldError, ImproperlyConfigured, ValidationError, + NON_FIELD_ERRORS, + FieldError, + ImproperlyConfigured, + ValidationError, ) from django.forms.fields import ChoiceField, Field from django.forms.forms import BaseForm, DeclarativeFieldsMetaclass from django.forms.formsets import BaseFormSet, formset_factory from django.forms.utils import ErrorList from django.forms.widgets import ( - HiddenInput, MultipleHiddenInput, RadioSelect, SelectMultiple, + HiddenInput, + MultipleHiddenInput, + RadioSelect, + SelectMultiple, ) from django.utils.text import capfirst, get_text_list -from django.utils.translation import gettext, gettext_lazy as _ +from django.utils.translation import gettext +from django.utils.translation import gettext_lazy as _ __all__ = ( - 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', - 'ModelChoiceField', 'ModelMultipleChoiceField', 'ALL_FIELDS', - 'BaseModelFormSet', 'modelformset_factory', 'BaseInlineFormSet', - 'inlineformset_factory', 'modelform_factory', + "ModelForm", + "BaseModelForm", + "model_to_dict", + "fields_for_model", + "ModelChoiceField", + "ModelMultipleChoiceField", + "ALL_FIELDS", + "BaseModelFormSet", + "modelformset_factory", + "BaseInlineFormSet", + "inlineformset_factory", + "modelform_factory", ) -ALL_FIELDS = '__all__' +ALL_FIELDS = "__all__" def construct_instance(form, instance, fields=None, exclude=None): @@ -33,13 +48,17 @@ def construct_instance(form, instance, fields=None, exclude=None): ``cleaned_data``, but do not save the returned instance to the database. """ from django.db import models + opts = instance._meta cleaned_data = form.cleaned_data file_field_list = [] for f in opts.fields: - if not f.editable or isinstance(f, models.AutoField) \ - or f.name not in cleaned_data: + if ( + not f.editable + or isinstance(f, models.AutoField) + or f.name not in cleaned_data + ): continue if fields is not None and f.name not in fields: continue @@ -48,9 +67,11 @@ def construct_instance(form, instance, fields=None, exclude=None): # Leave defaults for fields that aren't in POST data, except for # checkbox inputs because they don't appear in POST data if not checked. if ( - f.has_default() and - form[f.name].field.widget.value_omitted_from_data(form.data, form.files, form.add_prefix(f.name)) and - cleaned_data.get(f.name) in form[f.name].field.empty_values + f.has_default() + and form[f.name].field.widget.value_omitted_from_data( + form.data, form.files, form.add_prefix(f.name) + ) + and cleaned_data.get(f.name) in form[f.name].field.empty_values ): continue # Defer saving file-type fields until after the other fields, so a @@ -68,6 +89,7 @@ def construct_instance(form, instance, fields=None, exclude=None): # ModelForms ################################################################# + def model_to_dict(instance, fields=None, exclude=None): """ Return a dict containing the data in ``instance`` suitable for passing as @@ -83,7 +105,7 @@ def model_to_dict(instance, fields=None, exclude=None): opts = instance._meta data = {} for f in chain(opts.concrete_fields, opts.private_fields, opts.many_to_many): - if not getattr(f, 'editable', False): + if not getattr(f, "editable", False): continue if fields is not None and f.name not in fields: continue @@ -96,23 +118,34 @@ def model_to_dict(instance, fields=None, exclude=None): def apply_limit_choices_to_to_formfield(formfield): """Apply limit_choices_to to the formfield's queryset if needed.""" from django.db.models import Exists, OuterRef, Q - if hasattr(formfield, 'queryset') and hasattr(formfield, 'get_limit_choices_to'): + + if hasattr(formfield, "queryset") and hasattr(formfield, "get_limit_choices_to"): limit_choices_to = formfield.get_limit_choices_to() if limit_choices_to: complex_filter = limit_choices_to if not isinstance(complex_filter, Q): complex_filter = Q(**limit_choices_to) - complex_filter &= Q(pk=OuterRef('pk')) + complex_filter &= Q(pk=OuterRef("pk")) # Use Exists() to avoid potential duplicates. formfield.queryset = formfield.queryset.filter( Exists(formfield.queryset.model._base_manager.filter(complex_filter)), ) -def fields_for_model(model, fields=None, exclude=None, widgets=None, - formfield_callback=None, localized_fields=None, - labels=None, help_texts=None, error_messages=None, - field_classes=None, *, apply_limit_choices_to=True): +def fields_for_model( + model, + fields=None, + exclude=None, + widgets=None, + formfield_callback=None, + localized_fields=None, + labels=None, + help_texts=None, + error_messages=None, + field_classes=None, + *, + apply_limit_choices_to=True, +): """ Return a dictionary containing form fields for the given model. @@ -148,14 +181,22 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, opts = model._meta # Avoid circular import from django.db.models import Field as ModelField - sortable_private_fields = [f for f in opts.private_fields if isinstance(f, ModelField)] - for f in sorted(chain(opts.concrete_fields, sortable_private_fields, opts.many_to_many)): - if not getattr(f, 'editable', False): - if (fields is not None and f.name in fields and - (exclude is None or f.name not in exclude)): + + sortable_private_fields = [ + f for f in opts.private_fields if isinstance(f, ModelField) + ] + for f in sorted( + chain(opts.concrete_fields, sortable_private_fields, opts.many_to_many) + ): + if not getattr(f, "editable", False): + if ( + fields is not None + and f.name in fields + and (exclude is None or f.name not in exclude) + ): raise FieldError( - "'%s' cannot be specified for %s model form as it is a non-editable field" % ( - f.name, model.__name__) + "'%s' cannot be specified for %s model form as it is a non-editable field" + % (f.name, model.__name__) ) continue if fields is not None and f.name not in fields: @@ -165,22 +206,24 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, kwargs = {} if widgets and f.name in widgets: - kwargs['widget'] = widgets[f.name] - if localized_fields == ALL_FIELDS or (localized_fields and f.name in localized_fields): - kwargs['localize'] = True + kwargs["widget"] = widgets[f.name] + if localized_fields == ALL_FIELDS or ( + localized_fields and f.name in localized_fields + ): + kwargs["localize"] = True if labels and f.name in labels: - kwargs['label'] = labels[f.name] + kwargs["label"] = labels[f.name] if help_texts and f.name in help_texts: - kwargs['help_text'] = help_texts[f.name] + kwargs["help_text"] = help_texts[f.name] if error_messages and f.name in error_messages: - kwargs['error_messages'] = error_messages[f.name] + kwargs["error_messages"] = error_messages[f.name] if field_classes and f.name in field_classes: - kwargs['form_class'] = field_classes[f.name] + kwargs["form_class"] = field_classes[f.name] if formfield_callback is None: formfield = f.formfield(**kwargs) elif not callable(formfield_callback): - raise TypeError('formfield_callback must be a function or callable') + raise TypeError("formfield_callback must be a function or callable") else: formfield = formfield_callback(f, **kwargs) @@ -192,7 +235,8 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, ignored.append(f.name) if fields: field_dict = { - f: field_dict.get(f) for f in fields + f: field_dict.get(f) + for f in fields if (not exclude or f not in exclude) and f not in ignored } return field_dict @@ -200,46 +244,49 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, class ModelFormOptions: def __init__(self, options=None): - self.model = getattr(options, 'model', None) - self.fields = getattr(options, 'fields', None) - self.exclude = getattr(options, 'exclude', None) - self.widgets = getattr(options, 'widgets', None) - self.localized_fields = getattr(options, 'localized_fields', None) - self.labels = getattr(options, 'labels', None) - self.help_texts = getattr(options, 'help_texts', None) - self.error_messages = getattr(options, 'error_messages', None) - self.field_classes = getattr(options, 'field_classes', None) + self.model = getattr(options, "model", None) + self.fields = getattr(options, "fields", None) + self.exclude = getattr(options, "exclude", None) + self.widgets = getattr(options, "widgets", None) + self.localized_fields = getattr(options, "localized_fields", None) + self.labels = getattr(options, "labels", None) + self.help_texts = getattr(options, "help_texts", None) + self.error_messages = getattr(options, "error_messages", None) + self.field_classes = getattr(options, "field_classes", None) class ModelFormMetaclass(DeclarativeFieldsMetaclass): def __new__(mcs, name, bases, attrs): base_formfield_callback = None for b in bases: - if hasattr(b, 'Meta') and hasattr(b.Meta, 'formfield_callback'): + if hasattr(b, "Meta") and hasattr(b.Meta, "formfield_callback"): base_formfield_callback = b.Meta.formfield_callback break - formfield_callback = attrs.pop('formfield_callback', base_formfield_callback) + formfield_callback = attrs.pop("formfield_callback", base_formfield_callback) new_class = super().__new__(mcs, name, bases, attrs) if bases == (BaseModelForm,): return new_class - opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None)) + opts = new_class._meta = ModelFormOptions(getattr(new_class, "Meta", None)) # We check if a string was passed to `fields` or `exclude`, # which is likely to be a mistake where the user typed ('foo') instead # of ('foo',) - for opt in ['fields', 'exclude', 'localized_fields']: + for opt in ["fields", "exclude", "localized_fields"]: value = getattr(opts, opt) if isinstance(value, str) and value != ALL_FIELDS: - msg = ("%(model)s.Meta.%(opt)s cannot be a string. " - "Did you mean to type: ('%(value)s',)?" % { - 'model': new_class.__name__, - 'opt': opt, - 'value': value, - }) + msg = ( + "%(model)s.Meta.%(opt)s cannot be a string. " + "Did you mean to type: ('%(value)s',)?" + % { + "model": new_class.__name__, + "opt": opt, + "value": value, + } + ) raise TypeError(msg) if opts.model: @@ -257,9 +304,16 @@ class ModelFormMetaclass(DeclarativeFieldsMetaclass): opts.fields = None fields = fields_for_model( - opts.model, opts.fields, opts.exclude, opts.widgets, - formfield_callback, opts.localized_fields, opts.labels, - opts.help_texts, opts.error_messages, opts.field_classes, + opts.model, + opts.fields, + opts.exclude, + opts.widgets, + formfield_callback, + opts.localized_fields, + opts.labels, + opts.help_texts, + opts.error_messages, + opts.field_classes, # limit_choices_to will be applied during ModelForm.__init__(). apply_limit_choices_to=False, ) @@ -268,9 +322,8 @@ class ModelFormMetaclass(DeclarativeFieldsMetaclass): none_model_fields = {k for k, v in fields.items() if not v} missing_fields = none_model_fields.difference(new_class.declared_fields) if missing_fields: - message = 'Unknown field(s) (%s) specified for %s' - message = message % (', '.join(missing_fields), - opts.model.__name__) + message = "Unknown field(s) (%s) specified for %s" + message = message % (", ".join(missing_fields), opts.model.__name__) raise FieldError(message) # Override default model fields with any custom declared ones # (plus, include all the other declared fields). @@ -284,13 +337,23 @@ class ModelFormMetaclass(DeclarativeFieldsMetaclass): class BaseModelForm(BaseForm): - def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, - initial=None, error_class=ErrorList, label_suffix=None, - empty_permitted=False, instance=None, use_required_attribute=None, - renderer=None): + def __init__( + self, + data=None, + files=None, + auto_id="id_%s", + prefix=None, + initial=None, + error_class=ErrorList, + label_suffix=None, + empty_permitted=False, + instance=None, + use_required_attribute=None, + renderer=None, + ): opts = self._meta if opts.model is None: - raise ValueError('ModelForm has no model class specified.') + raise ValueError("ModelForm has no model class specified.") if instance is None: # if we didn't get an instance, instantiate a new one self.instance = opts.model() @@ -306,8 +369,15 @@ class BaseModelForm(BaseForm): # super will stop validate_unique from being called. self._validate_unique = False super().__init__( - data, files, auto_id, prefix, object_data, error_class, - label_suffix, empty_permitted, use_required_attribute=use_required_attribute, + data, + files, + auto_id, + prefix, + object_data, + error_class, + label_suffix, + empty_permitted, + use_required_attribute=use_required_attribute, renderer=renderer, ) for formfield in self.fields.values(): @@ -350,7 +420,11 @@ class BaseModelForm(BaseForm): else: form_field = self.fields[field] field_value = self.cleaned_data.get(field) - if not f.blank and not form_field.required and field_value in form_field.empty_values: + if ( + not f.blank + and not form_field.required + and field_value in form_field.empty_values + ): exclude.append(f.name) return exclude @@ -365,14 +439,17 @@ class BaseModelForm(BaseForm): # Allow the model generated by construct_instance() to raise # ValidationError and have them handled in the same way as others. - if hasattr(errors, 'error_dict'): + if hasattr(errors, "error_dict"): error_dict = errors.error_dict else: error_dict = {NON_FIELD_ERRORS: errors} for field, messages in error_dict.items(): - if (field == NON_FIELD_ERRORS and opts.error_messages and - NON_FIELD_ERRORS in opts.error_messages): + if ( + field == NON_FIELD_ERRORS + and opts.error_messages + and NON_FIELD_ERRORS in opts.error_messages + ): error_messages = opts.error_messages[NON_FIELD_ERRORS] elif field in self.fields: error_messages = self.fields[field].error_messages @@ -380,8 +457,10 @@ class BaseModelForm(BaseForm): continue for message in messages: - if (isinstance(message, ValidationError) and - message.code in error_messages): + if ( + isinstance(message, ValidationError) + and message.code in error_messages + ): message.message = error_messages[message.code] self.add_error(None, errors) @@ -403,7 +482,9 @@ class BaseModelForm(BaseForm): exclude.append(name) try: - self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude) + self.instance = construct_instance( + self, self.instance, opts.fields, opts.exclude + ) except ValidationError as e: self._update_errors(e) @@ -439,7 +520,7 @@ class BaseModelForm(BaseForm): # private_fields here. (GenericRelation was previously a fake # m2m field). for f in chain(opts.many_to_many, opts.private_fields): - if not hasattr(f, 'save_form_data'): + if not hasattr(f, "save_form_data"): continue if fields and f.name not in fields: continue @@ -456,9 +537,10 @@ class BaseModelForm(BaseForm): """ if self.errors: raise ValueError( - "The %s could not be %s because the data didn't validate." % ( + "The %s could not be %s because the data didn't validate." + % ( self.instance._meta.object_name, - 'created' if self.instance._state.adding else 'changed', + "created" if self.instance._state.adding else "changed", ) ) if commit: @@ -478,10 +560,19 @@ class ModelForm(BaseModelForm, metaclass=ModelFormMetaclass): pass -def modelform_factory(model, form=ModelForm, fields=None, exclude=None, - formfield_callback=None, widgets=None, localized_fields=None, - labels=None, help_texts=None, error_messages=None, - field_classes=None): +def modelform_factory( + model, + form=ModelForm, + fields=None, + exclude=None, + formfield_callback=None, + widgets=None, + localized_fields=None, + labels=None, + help_texts=None, + error_messages=None, + field_classes=None, +): """ Return a ModelForm containing form fields for the given model. You can optionally pass a `form` argument to use as a starting point for @@ -517,41 +608,37 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None, # inner class. # Build up a list of attributes that the Meta object will have. - attrs = {'model': model} + attrs = {"model": model} if fields is not None: - attrs['fields'] = fields + attrs["fields"] = fields if exclude is not None: - attrs['exclude'] = exclude + attrs["exclude"] = exclude if widgets is not None: - attrs['widgets'] = widgets + attrs["widgets"] = widgets if localized_fields is not None: - attrs['localized_fields'] = localized_fields + attrs["localized_fields"] = localized_fields if labels is not None: - attrs['labels'] = labels + attrs["labels"] = labels if help_texts is not None: - attrs['help_texts'] = help_texts + attrs["help_texts"] = help_texts if error_messages is not None: - attrs['error_messages'] = error_messages + attrs["error_messages"] = error_messages if field_classes is not None: - attrs['field_classes'] = field_classes + attrs["field_classes"] = field_classes # If parent form class already has an inner Meta, the Meta we're # creating needs to inherit from the parent's inner meta. - bases = (form.Meta,) if hasattr(form, 'Meta') else () - Meta = type('Meta', bases, attrs) + bases = (form.Meta,) if hasattr(form, "Meta") else () + Meta = type("Meta", bases, attrs) if formfield_callback: Meta.formfield_callback = staticmethod(formfield_callback) # Give this new form class a reasonable name. - class_name = model.__name__ + 'Form' + class_name = model.__name__ + "Form" # Class attributes for the new form class. - form_class_attrs = { - 'Meta': Meta, - 'formfield_callback': formfield_callback - } + form_class_attrs = {"Meta": Meta, "formfield_callback": formfield_callback} - if (getattr(Meta, 'fields', None) is None and - getattr(Meta, 'exclude', None) is None): + if getattr(Meta, "fields", None) is None and getattr(Meta, "exclude", None) is None: raise ImproperlyConfigured( "Calling modelform_factory without defining 'fields' or " "'exclude' explicitly is prohibited." @@ -563,20 +650,39 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None, # ModelFormSets ############################################################## + class BaseModelFormSet(BaseFormSet): """ A ``FormSet`` for editing a queryset and/or adding new objects to it. """ + model = None # Set of fields that must be unique among forms of this set. unique_fields = set() - def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, - queryset=None, *, initial=None, **kwargs): + def __init__( + self, + data=None, + files=None, + auto_id="id_%s", + prefix=None, + queryset=None, + *, + initial=None, + **kwargs, + ): self.queryset = queryset self.initial_extra = initial - super().__init__(**{'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix, **kwargs}) + super().__init__( + **{ + "data": data, + "files": files, + "auto_id": auto_id, + "prefix": prefix, + **kwargs, + } + ) def initial_form_count(self): """Return the number of forms that are required in this FormSet.""" @@ -585,7 +691,7 @@ class BaseModelFormSet(BaseFormSet): return super().initial_form_count() def _existing_object(self, pk): - if not hasattr(self, '_object_dict'): + if not hasattr(self, "_object_dict"): self._object_dict = {o.pk: o for o in self.get_queryset()} return self._object_dict.get(pk) @@ -602,7 +708,7 @@ class BaseModelFormSet(BaseFormSet): pk_required = i < self.initial_form_count() if pk_required: if self.is_bound: - pk_key = '%s-%s' % (self.add_prefix(i), self.model._meta.pk.name) + pk_key = "%s-%s" % (self.add_prefix(i), self.model._meta.pk.name) try: pk = self.data[pk_key] except KeyError: @@ -618,13 +724,13 @@ class BaseModelFormSet(BaseFormSet): # user may have tampered with POST data. pass else: - kwargs['instance'] = self._existing_object(pk) + kwargs["instance"] = self._existing_object(pk) else: - kwargs['instance'] = self.get_queryset()[i] + kwargs["instance"] = self.get_queryset()[i] elif self.initial_extra: # Set initial values for extra forms try: - kwargs['initial'] = self.initial_extra[i - self.initial_form_count()] + kwargs["initial"] = self.initial_extra[i - self.initial_form_count()] except IndexError: pass form = super()._construct_form(i, **kwargs) @@ -633,7 +739,7 @@ class BaseModelFormSet(BaseFormSet): return form def get_queryset(self): - if not hasattr(self, '_queryset'): + if not hasattr(self, "_queryset"): if self.queryset is not None: qs = self.queryset else: @@ -675,6 +781,7 @@ class BaseModelFormSet(BaseFormSet): def save_m2m(): for form in self.saved_forms: form.save_m2m() + self.save_m2m = save_m2m if self.edit_only: return self.save_existing_objects(commit) @@ -691,10 +798,16 @@ class BaseModelFormSet(BaseFormSet): all_unique_checks = set() all_date_checks = set() forms_to_delete = self.deleted_forms - valid_forms = [form for form in self.forms if form.is_valid() and form not in forms_to_delete] + valid_forms = [ + form + for form in self.forms + if form.is_valid() and form not in forms_to_delete + ] for form in valid_forms: exclude = form._get_validation_exclusions() - unique_checks, date_checks = form.instance._get_unique_checks(exclude=exclude) + unique_checks, date_checks = form.instance._get_unique_checks( + exclude=exclude + ) all_unique_checks.update(unique_checks) all_date_checks.update(date_checks) @@ -706,14 +819,15 @@ class BaseModelFormSet(BaseFormSet): # Get the data for the set of fields that must be unique among the forms. row_data = ( field if field in self.unique_fields else form.cleaned_data[field] - for field in unique_check if field in form.cleaned_data + for field in unique_check + if field in form.cleaned_data ) # Reduce Model instances to their primary key values row_data = tuple( - d._get_pk_val() if hasattr(d, '_get_pk_val') + d._get_pk_val() if hasattr(d, "_get_pk_val") # Prevent "unhashable type: list" errors later on. - else tuple(d) if isinstance(d, list) - else d for d in row_data + else tuple(d) if isinstance(d, list) else d + for d in row_data ) if row_data and None not in row_data: # if we've already seen it then we have a uniqueness failure @@ -737,10 +851,13 @@ class BaseModelFormSet(BaseFormSet): uclass, lookup, field, unique_for = date_check for form in valid_forms: # see if we have data for both fields - if (form.cleaned_data and form.cleaned_data[field] is not None and - form.cleaned_data[unique_for] is not None): + if ( + form.cleaned_data + and form.cleaned_data[field] is not None + and form.cleaned_data[unique_for] is not None + ): # if it's a date lookup we need to get the data for all the fields - if lookup == 'date': + if lookup == "date": date = form.cleaned_data[unique_for] date_data = (date.year, date.month, date.day) # otherwise it's just the attribute on the date/datetime @@ -771,7 +888,9 @@ class BaseModelFormSet(BaseFormSet): "field": unique_check[0], } else: - return gettext("Please correct the duplicate data for %(field)s, which must be unique.") % { + return gettext( + "Please correct the duplicate data for %(field)s, which must be unique." + ) % { "field": get_text_list(unique_check, _("and")), } @@ -780,9 +899,9 @@ class BaseModelFormSet(BaseFormSet): "Please correct the duplicate data for %(field_name)s " "which must be unique for the %(lookup)s in %(date_field)s." ) % { - 'field_name': date_check[2], - 'date_field': date_check[3], - 'lookup': str(date_check[1]), + "field_name": date_check[2], + "date_field": date_check[3], + "lookup": str(date_check[1]), } def get_form_error(self): @@ -831,6 +950,7 @@ class BaseModelFormSet(BaseFormSet): def add_fields(self, form, index): """Add a hidden field for the object's primary key.""" from django.db.models import AutoField, ForeignKey, OneToOneField + self._pk_field = pk = self.model._meta.pk # If a pk isn't editable, then it won't be on the form, so we need to # add it here so we can tell which object is which when we get the @@ -840,11 +960,15 @@ class BaseModelFormSet(BaseFormSet): def pk_is_not_editable(pk): return ( - (not pk.editable) or (pk.auto_created or isinstance(pk, AutoField)) or ( - pk.remote_field and pk.remote_field.parent_link and - pk_is_not_editable(pk.remote_field.model._meta.pk) + (not pk.editable) + or (pk.auto_created or isinstance(pk, AutoField)) + or ( + pk.remote_field + and pk.remote_field.parent_link + and pk_is_not_editable(pk.remote_field.model._meta.pk) ) ) + if pk_is_not_editable(pk) or pk.name not in form.fields: if form.is_bound: # If we're adding the related instance, ignore its primary key @@ -868,37 +992,75 @@ class BaseModelFormSet(BaseFormSet): widget = form._meta.widgets.get(self._pk_field.name, HiddenInput) else: widget = HiddenInput - form.fields[self._pk_field.name] = ModelChoiceField(qs, initial=pk_value, required=False, widget=widget) + form.fields[self._pk_field.name] = ModelChoiceField( + qs, initial=pk_value, required=False, widget=widget + ) super().add_fields(form, index) -def modelformset_factory(model, form=ModelForm, formfield_callback=None, - formset=BaseModelFormSet, extra=1, can_delete=False, - can_order=False, max_num=None, fields=None, exclude=None, - widgets=None, validate_max=False, localized_fields=None, - labels=None, help_texts=None, error_messages=None, - min_num=None, validate_min=False, field_classes=None, - absolute_max=None, can_delete_extra=True, renderer=None, - edit_only=False): +def modelformset_factory( + model, + form=ModelForm, + formfield_callback=None, + formset=BaseModelFormSet, + extra=1, + can_delete=False, + can_order=False, + max_num=None, + fields=None, + exclude=None, + widgets=None, + validate_max=False, + localized_fields=None, + labels=None, + help_texts=None, + error_messages=None, + min_num=None, + validate_min=False, + field_classes=None, + absolute_max=None, + can_delete_extra=True, + renderer=None, + edit_only=False, +): """Return a FormSet class for the given Django model class.""" - meta = getattr(form, 'Meta', None) - if (getattr(meta, 'fields', fields) is None and - getattr(meta, 'exclude', exclude) is None): + meta = getattr(form, "Meta", None) + if ( + getattr(meta, "fields", fields) is None + and getattr(meta, "exclude", exclude) is None + ): raise ImproperlyConfigured( "Calling modelformset_factory without defining 'fields' or " "'exclude' explicitly is prohibited." ) - form = modelform_factory(model, form=form, fields=fields, exclude=exclude, - formfield_callback=formfield_callback, - widgets=widgets, localized_fields=localized_fields, - labels=labels, help_texts=help_texts, - error_messages=error_messages, field_classes=field_classes) - FormSet = formset_factory(form, formset, extra=extra, min_num=min_num, max_num=max_num, - can_order=can_order, can_delete=can_delete, - validate_min=validate_min, validate_max=validate_max, - absolute_max=absolute_max, can_delete_extra=can_delete_extra, - renderer=renderer) + form = modelform_factory( + model, + form=form, + fields=fields, + exclude=exclude, + formfield_callback=formfield_callback, + widgets=widgets, + localized_fields=localized_fields, + labels=labels, + help_texts=help_texts, + error_messages=error_messages, + field_classes=field_classes, + ) + FormSet = formset_factory( + form, + formset, + extra=extra, + min_num=min_num, + max_num=max_num, + can_order=can_order, + can_delete=can_delete, + validate_min=validate_min, + validate_max=validate_max, + absolute_max=absolute_max, + can_delete_extra=can_delete_extra, + renderer=renderer, + ) FormSet.model = model FormSet.edit_only = edit_only return FormSet @@ -906,10 +1068,20 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None, # InlineFormSets ############################################################# + class BaseInlineFormSet(BaseModelFormSet): """A formset for child objects related to a parent.""" - def __init__(self, data=None, files=None, instance=None, - save_as_new=False, prefix=None, queryset=None, **kwargs): + + def __init__( + self, + data=None, + files=None, + instance=None, + save_as_new=False, + prefix=None, + queryset=None, + **kwargs, + ): if instance is None: self.instance = self.fk.remote_field.model() else: @@ -939,7 +1111,7 @@ class BaseInlineFormSet(BaseModelFormSet): def _construct_form(self, i, **kwargs): form = super()._construct_form(i, **kwargs) if self.save_as_new: - mutable = getattr(form.data, '_mutable', None) + mutable = getattr(form.data, "_mutable", None) # Allow modifying an immutable QueryDict. if mutable is not None: form.data._mutable = True @@ -955,13 +1127,13 @@ class BaseInlineFormSet(BaseModelFormSet): fk_value = self.instance.pk if self.fk.remote_field.field_name != self.fk.remote_field.model._meta.pk.name: fk_value = getattr(self.instance, self.fk.remote_field.field_name) - fk_value = getattr(fk_value, 'pk', fk_value) + fk_value = getattr(fk_value, "pk", fk_value) setattr(form.instance, self.fk.get_attname(), fk_value) return form @classmethod def get_default_prefix(cls): - return cls.fk.remote_field.get_accessor_name(model=cls.model).replace('+', '') + return cls.fk.remote_field.get_accessor_name(model=cls.model).replace("+", "") def save_new(self, form, commit=True): # Ensure the latest copy of the related instance is present on each @@ -974,26 +1146,28 @@ class BaseInlineFormSet(BaseModelFormSet): super().add_fields(form, index) if self._pk_field == self.fk: name = self._pk_field.name - kwargs = {'pk_field': True} + kwargs = {"pk_field": True} else: # The foreign key field might not be on the form, so we poke at the # Model field to get the label, since we need that for error messages. name = self.fk.name kwargs = { - 'label': getattr(form.fields.get(name), 'label', capfirst(self.fk.verbose_name)) + "label": getattr( + form.fields.get(name), "label", capfirst(self.fk.verbose_name) + ) } # The InlineForeignKeyField assumes that the foreign key relation is # based on the parent model's pk. If this isn't the case, set to_field # to correctly resolve the initial form value. if self.fk.remote_field.field_name != self.fk.remote_field.model._meta.pk.name: - kwargs['to_field'] = self.fk.remote_field.field_name + kwargs["to_field"] = self.fk.remote_field.field_name # If we're adding a new object, ignore a parent's auto-generated key # as it will be regenerated on the save request. if self.instance._state.adding: - if kwargs.get('to_field') is not None: - to_field = self.instance._meta.get_field(kwargs['to_field']) + if kwargs.get("to_field") is not None: + to_field = self.instance._meta.get_field(kwargs["to_field"]) else: to_field = self.instance._meta.pk if to_field.has_default(): @@ -1016,24 +1190,30 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False): """ # avoid circular import from django.db.models import ForeignKey + opts = model._meta if fk_name: fks_to_parent = [f for f in opts.fields if f.name == fk_name] if len(fks_to_parent) == 1: fk = fks_to_parent[0] parent_list = parent_model._meta.get_parent_list() - if not isinstance(fk, ForeignKey) or ( - # ForeignKey to proxy models. - fk.remote_field.model._meta.proxy and - fk.remote_field.model._meta.proxy_for_model not in parent_list - ) or ( - # ForeignKey to concrete models. - not fk.remote_field.model._meta.proxy and - fk.remote_field.model != parent_model and - fk.remote_field.model not in parent_list + if ( + not isinstance(fk, ForeignKey) + or ( + # ForeignKey to proxy models. + fk.remote_field.model._meta.proxy + and fk.remote_field.model._meta.proxy_for_model not in parent_list + ) + or ( + # ForeignKey to concrete models. + not fk.remote_field.model._meta.proxy + and fk.remote_field.model != parent_model + and fk.remote_field.model not in parent_list + ) ): raise ValueError( - "fk_name '%s' is not a ForeignKey to '%s'." % (fk_name, parent_model._meta.label) + "fk_name '%s' is not a ForeignKey to '%s'." + % (fk_name, parent_model._meta.label) ) elif not fks_to_parent: raise ValueError( @@ -1043,12 +1223,15 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False): # Try to discover what the ForeignKey from model to parent_model is parent_list = parent_model._meta.get_parent_list() fks_to_parent = [ - f for f in opts.fields - if isinstance(f, ForeignKey) and ( - f.remote_field.model == parent_model or - f.remote_field.model in parent_list or ( - f.remote_field.model._meta.proxy and - f.remote_field.model._meta.proxy_for_model in parent_list + f + for f in opts.fields + if isinstance(f, ForeignKey) + and ( + f.remote_field.model == parent_model + or f.remote_field.model in parent_list + or ( + f.remote_field.model._meta.proxy + and f.remote_field.model._meta.proxy_for_model in parent_list ) ) ] @@ -1058,7 +1241,8 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False): if can_fail: return raise ValueError( - "'%s' has no ForeignKey to '%s'." % ( + "'%s' has no ForeignKey to '%s'." + % ( model._meta.label, parent_model._meta.label, ) @@ -1066,7 +1250,8 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False): else: raise ValueError( "'%s' has more than one ForeignKey to '%s'. You must specify " - "a 'fk_name' attribute." % ( + "a 'fk_name' attribute." + % ( model._meta.label, parent_model._meta.label, ) @@ -1074,15 +1259,33 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False): return fk -def inlineformset_factory(parent_model, model, form=ModelForm, - formset=BaseInlineFormSet, fk_name=None, - fields=None, exclude=None, extra=3, can_order=False, - can_delete=True, max_num=None, formfield_callback=None, - widgets=None, validate_max=False, localized_fields=None, - labels=None, help_texts=None, error_messages=None, - min_num=None, validate_min=False, field_classes=None, - absolute_max=None, can_delete_extra=True, renderer=None, - edit_only=False): +def inlineformset_factory( + parent_model, + model, + form=ModelForm, + formset=BaseInlineFormSet, + fk_name=None, + fields=None, + exclude=None, + extra=3, + can_order=False, + can_delete=True, + max_num=None, + formfield_callback=None, + widgets=None, + validate_max=False, + localized_fields=None, + labels=None, + help_texts=None, + error_messages=None, + min_num=None, + validate_min=False, + field_classes=None, + absolute_max=None, + can_delete_extra=True, + renderer=None, + edit_only=False, +): """ Return an ``InlineFormSet`` for the given kwargs. @@ -1094,28 +1297,28 @@ def inlineformset_factory(parent_model, model, form=ModelForm, if fk.unique: max_num = 1 kwargs = { - 'form': form, - 'formfield_callback': formfield_callback, - 'formset': formset, - 'extra': extra, - 'can_delete': can_delete, - 'can_order': can_order, - 'fields': fields, - 'exclude': exclude, - 'min_num': min_num, - 'max_num': max_num, - 'widgets': widgets, - 'validate_min': validate_min, - 'validate_max': validate_max, - 'localized_fields': localized_fields, - 'labels': labels, - 'help_texts': help_texts, - 'error_messages': error_messages, - 'field_classes': field_classes, - 'absolute_max': absolute_max, - 'can_delete_extra': can_delete_extra, - 'renderer': renderer, - 'edit_only': edit_only, + "form": form, + "formfield_callback": formfield_callback, + "formset": formset, + "extra": extra, + "can_delete": can_delete, + "can_order": can_order, + "fields": fields, + "exclude": exclude, + "min_num": min_num, + "max_num": max_num, + "widgets": widgets, + "validate_min": validate_min, + "validate_max": validate_max, + "localized_fields": localized_fields, + "labels": labels, + "help_texts": help_texts, + "error_messages": error_messages, + "field_classes": field_classes, + "absolute_max": absolute_max, + "can_delete_extra": can_delete_extra, + "renderer": renderer, + "edit_only": edit_only, } FormSet = modelformset_factory(model, **kwargs) FormSet.fk = fk @@ -1124,14 +1327,16 @@ def inlineformset_factory(parent_model, model, form=ModelForm, # Fields ##################################################################### + class InlineForeignKeyField(Field): """ A basic integer field that deals with validating the given value to a given parent instance in an inline. """ + widget = HiddenInput default_error_messages = { - 'invalid_choice': _('The inline value did not match the parent instance.'), + "invalid_choice": _("The inline value did not match the parent instance."), } def __init__(self, parent_instance, *args, pk_field=False, to_field=None, **kwargs): @@ -1158,7 +1363,9 @@ class InlineForeignKeyField(Field): else: orig = self.parent_instance.pk if str(value) != str(orig): - raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice') + raise ValidationError( + self.error_messages["invalid_choice"], code="invalid_choice" + ) return self.parent_instance def has_changed(self, initial, data): @@ -1215,34 +1422,50 @@ class ModelChoiceIterator: class ModelChoiceField(ChoiceField): """A ChoiceField whose choices are a model QuerySet.""" + # This class is a subclass of ChoiceField for purity, but it doesn't # actually use any of ChoiceField's implementation. default_error_messages = { - 'invalid_choice': _( - 'Select a valid choice. That choice is not one of the available choices.' + "invalid_choice": _( + "Select a valid choice. That choice is not one of the available choices." ), } iterator = ModelChoiceIterator - def __init__(self, queryset, *, empty_label="---------", - required=True, widget=None, label=None, initial=None, - help_text='', to_field_name=None, limit_choices_to=None, - blank=False, **kwargs): + def __init__( + self, + queryset, + *, + empty_label="---------", + required=True, + widget=None, + label=None, + initial=None, + help_text="", + to_field_name=None, + limit_choices_to=None, + blank=False, + **kwargs, + ): # Call Field instead of ChoiceField __init__() because we don't need # ChoiceField.__init__(). Field.__init__( - self, required=required, widget=widget, label=label, - initial=initial, help_text=help_text, **kwargs + self, + required=required, + widget=widget, + label=label, + initial=initial, + help_text=help_text, + **kwargs, ) - if ( - (required and initial is not None) or - (isinstance(self.widget, RadioSelect) and not blank) + if (required and initial is not None) or ( + isinstance(self.widget, RadioSelect) and not blank ): self.empty_label = None else: self.empty_label = empty_label self.queryset = queryset - self.limit_choices_to = limit_choices_to # limit the queryset later. + self.limit_choices_to = limit_choices_to # limit the queryset later. self.to_field_name = to_field_name def get_limit_choices_to(self): @@ -1284,7 +1507,7 @@ class ModelChoiceField(ChoiceField): def _get_choices(self): # If self._choices is set, then somebody must have manually set # the property self.choices. In this case, just return self._choices. - if hasattr(self, '_choices'): + if hasattr(self, "_choices"): return self._choices # Otherwise, execute the QuerySet in self.queryset to determine the @@ -1299,7 +1522,7 @@ class ModelChoiceField(ChoiceField): choices = property(_get_choices, ChoiceField._set_choices) def prepare_value(self, value): - if hasattr(value, '_meta'): + if hasattr(value, "_meta"): if self.to_field_name: return value.serializable_value(self.to_field_name) else: @@ -1310,15 +1533,15 @@ class ModelChoiceField(ChoiceField): if value in self.empty_values: return None try: - key = self.to_field_name or 'pk' + key = self.to_field_name or "pk" if isinstance(value, self.queryset.model): value = getattr(value, key) value = self.queryset.get(**{key: value}) except (ValueError, TypeError, self.queryset.model.DoesNotExist): raise ValidationError( - self.error_messages['invalid_choice'], - code='invalid_choice', - params={'value': value}, + self.error_messages["invalid_choice"], + code="invalid_choice", + params={"value": value}, ) return value @@ -1328,21 +1551,22 @@ class ModelChoiceField(ChoiceField): def has_changed(self, initial, data): if self.disabled: return False - initial_value = initial if initial is not None else '' - data_value = data if data is not None else '' + initial_value = initial if initial is not None else "" + data_value = data if data is not None else "" return str(self.prepare_value(initial_value)) != str(data_value) class ModelMultipleChoiceField(ModelChoiceField): """A MultipleChoiceField whose choices are a model QuerySet.""" + widget = SelectMultiple hidden_widget = MultipleHiddenInput default_error_messages = { - 'invalid_list': _('Enter a list of values.'), - 'invalid_choice': _( - 'Select a valid choice. %(value)s is not one of the available choices.' + "invalid_list": _("Enter a list of values."), + "invalid_choice": _( + "Select a valid choice. %(value)s is not one of the available choices." ), - 'invalid_pk_value': _('“%(pk)s” is not a valid value.') + "invalid_pk_value": _("“%(pk)s” is not a valid value."), } def __init__(self, queryset, **kwargs): @@ -1356,13 +1580,13 @@ class ModelMultipleChoiceField(ModelChoiceField): def clean(self, value): value = self.prepare_value(value) if self.required and not value: - raise ValidationError(self.error_messages['required'], code='required') + raise ValidationError(self.error_messages["required"], code="required") elif not self.required and not value: return self.queryset.none() if not isinstance(value, (list, tuple)): raise ValidationError( - self.error_messages['invalid_list'], - code='invalid_list', + self.error_messages["invalid_list"], + code="invalid_list", ) qs = self._check_values(value) # Since this overrides the inherited ModelChoiceField.clean @@ -1376,7 +1600,7 @@ class ModelMultipleChoiceField(ModelChoiceField): corresponding objects. Raise a ValidationError if a given value is invalid (not a valid PK, not in the queryset, etc.) """ - key = self.to_field_name or 'pk' + key = self.to_field_name or "pk" # deduplicate given values to avoid creating many querysets or # requiring the database backend deduplicate efficiently. try: @@ -1384,33 +1608,35 @@ class ModelMultipleChoiceField(ModelChoiceField): except TypeError: # list of lists isn't hashable, for example raise ValidationError( - self.error_messages['invalid_list'], - code='invalid_list', + self.error_messages["invalid_list"], + code="invalid_list", ) for pk in value: try: self.queryset.filter(**{key: pk}) except (ValueError, TypeError): raise ValidationError( - self.error_messages['invalid_pk_value'], - code='invalid_pk_value', - params={'pk': pk}, + self.error_messages["invalid_pk_value"], + code="invalid_pk_value", + params={"pk": pk}, ) - qs = self.queryset.filter(**{'%s__in' % key: value}) + qs = self.queryset.filter(**{"%s__in" % key: value}) pks = {str(getattr(o, key)) for o in qs} for val in value: if str(val) not in pks: raise ValidationError( - self.error_messages['invalid_choice'], - code='invalid_choice', - params={'value': val}, + self.error_messages["invalid_choice"], + code="invalid_choice", + params={"value": val}, ) return qs def prepare_value(self, value): - if (hasattr(value, '__iter__') and - not isinstance(value, str) and - not hasattr(value, '_meta')): + if ( + hasattr(value, "__iter__") + and not isinstance(value, str) + and not hasattr(value, "_meta") + ): prepare_value = super().prepare_value return [prepare_value(v) for v in value] return super().prepare_value(value) @@ -1430,7 +1656,6 @@ class ModelMultipleChoiceField(ModelChoiceField): def modelform_defines_fields(form_class): - return hasattr(form_class, '_meta') and ( - form_class._meta.fields is not None or - form_class._meta.exclude is not None + return hasattr(form_class, "_meta") and ( + form_class._meta.fields is not None or form_class._meta.exclude is not None ) diff --git a/django/forms/renderers.py b/django/forms/renderers.py index ffb61600c2..88cf504653 100644 --- a/django/forms/renderers.py +++ b/django/forms/renderers.py @@ -16,7 +16,7 @@ def get_default_renderer(): class BaseRenderer: def get_template(self, template_name): - raise NotImplementedError('subclasses must implement get_template()') + raise NotImplementedError("subclasses must implement get_template()") def render(self, template_name, context, request=None): template = self.get_template(template_name) @@ -29,12 +29,14 @@ class EngineMixin: @cached_property def engine(self): - return self.backend({ - 'APP_DIRS': True, - 'DIRS': [Path(__file__).parent / self.backend.app_dirname], - 'NAME': 'djangoforms', - 'OPTIONS': {}, - }) + return self.backend( + { + "APP_DIRS": True, + "DIRS": [Path(__file__).parent / self.backend.app_dirname], + "NAME": "djangoforms", + "OPTIONS": {}, + } + ) class DjangoTemplates(EngineMixin, BaseRenderer): @@ -42,6 +44,7 @@ class DjangoTemplates(EngineMixin, BaseRenderer): Load Django templates from the built-in widget templates in django/forms/templates and from apps' 'templates' directory. """ + backend = DjangoTemplates @@ -50,9 +53,11 @@ class Jinja2(EngineMixin, BaseRenderer): Load Jinja2 templates from the built-in widget templates in django/forms/jinja2 and from apps' 'jinja2' directory. """ + @cached_property def backend(self): from django.template.backends.jinja2 import Jinja2 + return Jinja2 @@ -61,5 +66,6 @@ class TemplatesSetting(BaseRenderer): Load templates using template.loader.get_template() which is configured based on settings.TEMPLATES. """ + def get_template(self, template_name): return get_template(template_name) diff --git a/django/forms/utils.py b/django/forms/utils.py index 4af57e8586..7d3bb5ad48 100644 --- a/django/forms/utils.py +++ b/django/forms/utils.py @@ -13,8 +13,8 @@ from django.utils.translation import gettext_lazy as _ def pretty_name(name): """Convert 'first_name' to 'First name'.""" if not name: - return '' - return name.replace('_', ' ').capitalize() + return "" + return name.replace("_", " ").capitalize() def flatatt(attrs): @@ -37,23 +37,24 @@ def flatatt(attrs): elif value is not None: key_value_attrs.append((attr, value)) - return ( - format_html_join('', ' {}="{}"', sorted(key_value_attrs)) + - format_html_join('', ' {}', sorted(boolean_attrs)) + return format_html_join("", ' {}="{}"', sorted(key_value_attrs)) + format_html_join( + "", " {}", sorted(boolean_attrs) ) class RenderableMixin: def get_context(self): raise NotImplementedError( - 'Subclasses of RenderableMixin must provide a get_context() method.' + "Subclasses of RenderableMixin must provide a get_context() method." ) def render(self, template_name=None, context=None, renderer=None): - return mark_safe((renderer or self.renderer).render( - template_name or self.template_name, - context or self.get_context(), - )) + return mark_safe( + (renderer or self.renderer).render( + template_name or self.template_name, + context or self.get_context(), + ) + ) __str__ = render __html__ = render @@ -90,9 +91,10 @@ class ErrorDict(dict, RenderableErrorMixin): The dictionary keys are the field names, and the values are the errors. """ - template_name = 'django/forms/errors/dict/default.html' - template_name_text = 'django/forms/errors/dict/text.txt' - template_name_ul = 'django/forms/errors/dict/ul.html' + + template_name = "django/forms/errors/dict/default.html" + template_name_text = "django/forms/errors/dict/text.txt" + template_name_ul = "django/forms/errors/dict/ul.html" def __init__(self, *args, renderer=None, **kwargs): super().__init__(*args, **kwargs) @@ -106,8 +108,8 @@ class ErrorDict(dict, RenderableErrorMixin): def get_context(self): return { - 'errors': self.items(), - 'error_class': 'errorlist', + "errors": self.items(), + "error_class": "errorlist", } @@ -115,17 +117,18 @@ class ErrorList(UserList, list, RenderableErrorMixin): """ A collection of errors that knows how to display itself in various formats. """ - template_name = 'django/forms/errors/list/default.html' - template_name_text = 'django/forms/errors/list/text.txt' - template_name_ul = 'django/forms/errors/list/ul.html' + + template_name = "django/forms/errors/list/default.html" + template_name_text = "django/forms/errors/list/text.txt" + template_name_ul = "django/forms/errors/list/ul.html" def __init__(self, initlist=None, error_class=None, renderer=None): super().__init__(initlist) if error_class is None: - self.error_class = 'errorlist' + self.error_class = "errorlist" else: - self.error_class = 'errorlist {}'.format(error_class) + self.error_class = "errorlist {}".format(error_class) self.renderer = renderer or get_default_renderer() def as_data(self): @@ -140,16 +143,18 @@ class ErrorList(UserList, list, RenderableErrorMixin): errors = [] for error in self.as_data(): message = next(iter(error)) - errors.append({ - 'message': escape(message) if escape_html else message, - 'code': error.code or '', - }) + errors.append( + { + "message": escape(message) if escape_html else message, + "code": error.code or "", + } + ) return errors def get_context(self): return { - 'errors': self, - 'error_class': self.error_class, + "errors": self, + "error_class": self.error_class, } def __repr__(self): @@ -179,6 +184,7 @@ class ErrorList(UserList, list, RenderableErrorMixin): # Utilities for time zone support in DateTimeField et al. + def from_current_timezone(value): """ When time zone support is enabled, convert naive datetimes @@ -187,19 +193,20 @@ def from_current_timezone(value): if settings.USE_TZ and value is not None and timezone.is_naive(value): current_timezone = timezone.get_current_timezone() try: - if ( - not timezone._is_pytz_zone(current_timezone) and - timezone._datetime_ambiguous_or_imaginary(value, current_timezone) - ): - raise ValueError('Ambiguous or non-existent time.') + if not timezone._is_pytz_zone( + current_timezone + ) and timezone._datetime_ambiguous_or_imaginary(value, current_timezone): + raise ValueError("Ambiguous or non-existent time.") return timezone.make_aware(value, current_timezone) except Exception as exc: raise ValidationError( - _('%(datetime)s couldn’t be interpreted ' - 'in time zone %(current_timezone)s; it ' - 'may be ambiguous or it may not exist.'), - code='ambiguous_timezone', - params={'datetime': value, 'current_timezone': current_timezone} + _( + "%(datetime)s couldn’t be interpreted " + "in time zone %(current_timezone)s; it " + "may be ambiguous or it may not exist." + ), + code="ambiguous_timezone", + params={"datetime": value, "current_timezone": current_timezone}, ) from exc return value diff --git a/django/forms/widgets.py b/django/forms/widgets.py index 05667f8e44..8c5122ad1d 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -17,24 +17,41 @@ from django.utils.formats import get_format from django.utils.html import format_html, html_safe from django.utils.regex_helper import _lazy_re_compile from django.utils.safestring import mark_safe -from django.utils.topological_sort import ( - CyclicDependencyError, stable_topological_sort, -) +from django.utils.topological_sort import CyclicDependencyError, stable_topological_sort from django.utils.translation import gettext_lazy as _ from .renderers import get_default_renderer __all__ = ( - 'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'NumberInput', - 'EmailInput', 'URLInput', 'PasswordInput', 'HiddenInput', - 'MultipleHiddenInput', 'FileInput', 'ClearableFileInput', 'Textarea', - 'DateInput', 'DateTimeInput', 'TimeInput', 'CheckboxInput', 'Select', - 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', - 'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget', - 'SplitHiddenDateTimeWidget', 'SelectDateWidget', + "Media", + "MediaDefiningClass", + "Widget", + "TextInput", + "NumberInput", + "EmailInput", + "URLInput", + "PasswordInput", + "HiddenInput", + "MultipleHiddenInput", + "FileInput", + "ClearableFileInput", + "Textarea", + "DateInput", + "DateTimeInput", + "TimeInput", + "CheckboxInput", + "Select", + "NullBooleanSelect", + "SelectMultiple", + "RadioSelect", + "CheckboxSelectMultiple", + "MultiWidget", + "SplitDateTimeWidget", + "SplitHiddenDateTimeWidget", + "SelectDateWidget", ) -MEDIA_TYPES = ('css', 'js') +MEDIA_TYPES = ("css", "js") class MediaOrderConflictWarning(RuntimeWarning): @@ -45,8 +62,8 @@ class MediaOrderConflictWarning(RuntimeWarning): class Media: def __init__(self, media=None, css=None, js=None): if media is not None: - css = getattr(media, 'css', {}) - js = getattr(media, 'js', []) + css = getattr(media, "css", {}) + js = getattr(media, "js", []) else: if css is None: css = {} @@ -56,7 +73,7 @@ class Media: self._js_lists = [js] def __repr__(self): - return 'Media(css=%r, js=%r)' % (self._css, self._js) + return "Media(css=%r, js=%r)" % (self._css, self._js) def __str__(self): return self.render() @@ -74,26 +91,35 @@ class Media: return self.merge(*self._js_lists) def render(self): - return mark_safe('\n'.join(chain.from_iterable(getattr(self, 'render_' + name)() for name in MEDIA_TYPES))) + return mark_safe( + "\n".join( + chain.from_iterable( + getattr(self, "render_" + name)() for name in MEDIA_TYPES + ) + ) + ) def render_js(self): return [ - format_html( - '<script src="{}"></script>', - self.absolute_path(path) - ) for path in self._js + format_html('<script src="{}"></script>', self.absolute_path(path)) + for path in self._js ] def render_css(self): # To keep rendering order consistent, we can't just iterate over items(). # We need to sort the keys, and iterate over the sorted list. media = sorted(self._css) - return chain.from_iterable([ - format_html( - '<link href="{}" media="{}" rel="stylesheet">', - self.absolute_path(path), medium - ) for path in self._css[medium] - ] for medium in media) + return chain.from_iterable( + [ + format_html( + '<link href="{}" media="{}" rel="stylesheet">', + self.absolute_path(path), + medium, + ) + for path in self._css[medium] + ] + for medium in media + ) def absolute_path(self, path): """ @@ -101,14 +127,14 @@ class Media: path. An absolute path will be returned unchanged while a relative path will be passed to django.templatetags.static.static(). """ - if path.startswith(('http://', 'https://', '/')): + if path.startswith(("http://", "https://", "/")): return path return static(path) def __getitem__(self, name): """Return a Media object that only contains media of the given type.""" if name in MEDIA_TYPES: - return Media(**{str(name): getattr(self, '_' + name)}) + return Media(**{str(name): getattr(self, "_" + name)}) raise KeyError('Unknown media type "%s"' % name) @staticmethod @@ -138,9 +164,10 @@ class Media: return stable_topological_sort(all_items, dependency_graph) except CyclicDependencyError: warnings.warn( - 'Detected duplicate Media files in an opposite order: {}'.format( - ', '.join(repr(list_) for list_ in lists) - ), MediaOrderConflictWarning, + "Detected duplicate Media files in an opposite order: {}".format( + ", ".join(repr(list_) for list_ in lists) + ), + MediaOrderConflictWarning, ) return list(all_items) @@ -167,9 +194,9 @@ def media_property(cls): base = Media() # Get the media definition for this class - definition = getattr(cls, 'Media', None) + definition = getattr(cls, "Media", None) if definition: - extend = getattr(definition, 'extend', True) + extend = getattr(definition, "extend", True) if extend: if extend is True: m = base @@ -180,6 +207,7 @@ def media_property(cls): return m + Media(definition) return Media(definition) return base + return property(_media) @@ -187,10 +215,11 @@ class MediaDefiningClass(type): """ Metaclass for classes that can have media definitions. """ + def __new__(mcs, name, bases, attrs): new_class = super().__new__(mcs, name, bases, attrs) - if 'media' not in attrs: + if "media" not in attrs: new_class.media = media_property(new_class) return new_class @@ -213,17 +242,17 @@ class Widget(metaclass=MediaDefiningClass): @property def is_hidden(self): - return self.input_type == 'hidden' if hasattr(self, 'input_type') else False + return self.input_type == "hidden" if hasattr(self, "input_type") else False def subwidgets(self, name, value, attrs=None): context = self.get_context(name, value, attrs) - yield context['widget'] + yield context["widget"] def format_value(self, value): """ Return a value as it should appear when rendered in a template. """ - if value == '' or value is None: + if value == "" or value is None: return None if self.is_localized: return formats.localize_input(value) @@ -231,13 +260,13 @@ class Widget(metaclass=MediaDefiningClass): def get_context(self, name, value, attrs): return { - 'widget': { - 'name': name, - 'is_hidden': self.is_hidden, - 'required': self.is_required, - 'value': self.format_value(value), - 'attrs': self.build_attrs(self.attrs, attrs), - 'template_name': self.template_name, + "widget": { + "name": name, + "is_hidden": self.is_hidden, + "required": self.is_required, + "value": self.format_value(value), + "attrs": self.build_attrs(self.attrs, attrs), + "template_name": self.template_name, }, } @@ -285,44 +314,45 @@ class Input(Widget): """ Base class for all <input> widgets. """ + input_type = None # Subclasses must define this. - template_name = 'django/forms/widgets/input.html' + template_name = "django/forms/widgets/input.html" def __init__(self, attrs=None): if attrs is not None: attrs = attrs.copy() - self.input_type = attrs.pop('type', self.input_type) + self.input_type = attrs.pop("type", self.input_type) super().__init__(attrs) def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) - context['widget']['type'] = self.input_type + context["widget"]["type"] = self.input_type return context class TextInput(Input): - input_type = 'text' - template_name = 'django/forms/widgets/text.html' + input_type = "text" + template_name = "django/forms/widgets/text.html" class NumberInput(Input): - input_type = 'number' - template_name = 'django/forms/widgets/number.html' + input_type = "number" + template_name = "django/forms/widgets/number.html" class EmailInput(Input): - input_type = 'email' - template_name = 'django/forms/widgets/email.html' + input_type = "email" + template_name = "django/forms/widgets/email.html" class URLInput(Input): - input_type = 'url' - template_name = 'django/forms/widgets/url.html' + input_type = "url" + template_name = "django/forms/widgets/url.html" class PasswordInput(Input): - input_type = 'password' - template_name = 'django/forms/widgets/password.html' + input_type = "password" + template_name = "django/forms/widgets/password.html" def __init__(self, attrs=None, render_value=False): super().__init__(attrs) @@ -335,8 +365,8 @@ class PasswordInput(Input): class HiddenInput(Input): - input_type = 'hidden' - template_name = 'django/forms/widgets/hidden.html' + input_type = "hidden" + template_name = "django/forms/widgets/hidden.html" class MultipleHiddenInput(HiddenInput): @@ -344,25 +374,26 @@ class MultipleHiddenInput(HiddenInput): Handle <input type="hidden"> for fields that have a list of values. """ - template_name = 'django/forms/widgets/multiple_hidden.html' + + template_name = "django/forms/widgets/multiple_hidden.html" def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) - final_attrs = context['widget']['attrs'] - id_ = context['widget']['attrs'].get('id') + final_attrs = context["widget"]["attrs"] + id_ = context["widget"]["attrs"].get("id") subwidgets = [] - for index, value_ in enumerate(context['widget']['value']): + for index, value_ in enumerate(context["widget"]["value"]): widget_attrs = final_attrs.copy() if id_: # An ID attribute was given. Add a numeric index as a suffix # so that the inputs don't all have the same ID attribute. - widget_attrs['id'] = '%s_%s' % (id_, index) + widget_attrs["id"] = "%s_%s" % (id_, index) widget = HiddenInput() widget.is_required = self.is_required - subwidgets.append(widget.get_context(name, value_, widget_attrs)['widget']) + subwidgets.append(widget.get_context(name, value_, widget_attrs)["widget"]) - context['widget']['subwidgets'] = subwidgets + context["widget"]["subwidgets"] = subwidgets return context def value_from_datadict(self, data, files, name): @@ -377,9 +408,9 @@ class MultipleHiddenInput(HiddenInput): class FileInput(Input): - input_type = 'file' + input_type = "file" needs_multipart_form = True - template_name = 'django/forms/widgets/file.html' + template_name = "django/forms/widgets/file.html" def format_value(self, value): """File input never renders a value.""" @@ -400,29 +431,29 @@ FILE_INPUT_CONTRADICTION = object() class ClearableFileInput(FileInput): - clear_checkbox_label = _('Clear') - initial_text = _('Currently') - input_text = _('Change') - template_name = 'django/forms/widgets/clearable_file_input.html' + clear_checkbox_label = _("Clear") + initial_text = _("Currently") + input_text = _("Change") + template_name = "django/forms/widgets/clearable_file_input.html" def clear_checkbox_name(self, name): """ Given the name of the file input, return the name of the clear checkbox input. """ - return name + '-clear' + return name + "-clear" def clear_checkbox_id(self, name): """ Given the name of the clear checkbox input, return the HTML id for it. """ - return name + '_id' + return name + "_id" def is_initial(self, value): """ Return whether value is considered to be initial value. """ - return bool(value and getattr(value, 'url', False)) + return bool(value and getattr(value, "url", False)) def format_value(self, value): """ @@ -435,20 +466,23 @@ class ClearableFileInput(FileInput): context = super().get_context(name, value, attrs) checkbox_name = self.clear_checkbox_name(name) checkbox_id = self.clear_checkbox_id(checkbox_name) - context['widget'].update({ - 'checkbox_name': checkbox_name, - 'checkbox_id': checkbox_id, - 'is_initial': self.is_initial(value), - 'input_text': self.input_text, - 'initial_text': self.initial_text, - 'clear_checkbox_label': self.clear_checkbox_label, - }) + context["widget"].update( + { + "checkbox_name": checkbox_name, + "checkbox_id": checkbox_id, + "is_initial": self.is_initial(value), + "input_text": self.input_text, + "initial_text": self.initial_text, + "clear_checkbox_label": self.clear_checkbox_label, + } + ) return context def value_from_datadict(self, data, files, name): upload = super().value_from_datadict(data, files, name) if not self.is_required and CheckboxInput().value_from_datadict( - data, files, self.clear_checkbox_name(name)): + data, files, self.clear_checkbox_name(name) + ): if upload: # If the user contradicts themselves (uploads a new file AND @@ -461,24 +495,24 @@ class ClearableFileInput(FileInput): def value_omitted_from_data(self, data, files, name): return ( - super().value_omitted_from_data(data, files, name) and - self.clear_checkbox_name(name) not in data + super().value_omitted_from_data(data, files, name) + and self.clear_checkbox_name(name) not in data ) class Textarea(Widget): - template_name = 'django/forms/widgets/textarea.html' + template_name = "django/forms/widgets/textarea.html" def __init__(self, attrs=None): # Use slightly better defaults than HTML's 20x2 box - default_attrs = {'cols': '40', 'rows': '10'} + default_attrs = {"cols": "40", "rows": "10"} if attrs: default_attrs.update(attrs) super().__init__(default_attrs) class DateTimeBaseInput(TextInput): - format_key = '' + format_key = "" supports_microseconds = False def __init__(self, attrs=None, format=None): @@ -486,32 +520,34 @@ class DateTimeBaseInput(TextInput): self.format = format or None def format_value(self, value): - return formats.localize_input(value, self.format or formats.get_format(self.format_key)[0]) + return formats.localize_input( + value, self.format or formats.get_format(self.format_key)[0] + ) class DateInput(DateTimeBaseInput): - format_key = 'DATE_INPUT_FORMATS' - template_name = 'django/forms/widgets/date.html' + format_key = "DATE_INPUT_FORMATS" + template_name = "django/forms/widgets/date.html" class DateTimeInput(DateTimeBaseInput): - format_key = 'DATETIME_INPUT_FORMATS' - template_name = 'django/forms/widgets/datetime.html' + format_key = "DATETIME_INPUT_FORMATS" + template_name = "django/forms/widgets/datetime.html" class TimeInput(DateTimeBaseInput): - format_key = 'TIME_INPUT_FORMATS' - template_name = 'django/forms/widgets/time.html' + format_key = "TIME_INPUT_FORMATS" + template_name = "django/forms/widgets/time.html" # Defined at module level so that CheckboxInput is picklable (#17976) def boolean_check(v): - return not (v is False or v is None or v == '') + return not (v is False or v is None or v == "") class CheckboxInput(Input): - input_type = 'checkbox' - template_name = 'django/forms/widgets/checkbox.html' + input_type = "checkbox" + template_name = "django/forms/widgets/checkbox.html" def __init__(self, attrs=None, check_test=None): super().__init__(attrs) @@ -521,13 +557,13 @@ class CheckboxInput(Input): def format_value(self, value): """Only return the 'value' attribute if value isn't empty.""" - if value is True or value is False or value is None or value == '': + if value is True or value is False or value is None or value == "": return return str(value) def get_context(self, name, value, attrs): if self.check_test(value): - attrs = {**(attrs or {}), 'checked': True} + attrs = {**(attrs or {}), "checked": True} return super().get_context(name, value, attrs) def value_from_datadict(self, data, files, name): @@ -537,7 +573,7 @@ class CheckboxInput(Input): return False value = data.get(name) # Translate true and false strings to boolean values. - values = {'true': True, 'false': False} + values = {"true": True, "false": False} if isinstance(value, str): value = values.get(value.lower(), value) return bool(value) @@ -554,7 +590,7 @@ class ChoiceWidget(Widget): template_name = None option_template_name = None add_id_index = True - checked_attribute = {'checked': True} + checked_attribute = {"checked": True} option_inherits_attrs = True def __init__(self, attrs=None, choices=()): @@ -591,7 +627,7 @@ class ChoiceWidget(Widget): for index, (option_value, option_label) in enumerate(self.choices): if option_value is None: - option_value = '' + option_value = "" subgroup = [] if isinstance(option_label, (list, tuple)): @@ -605,50 +641,62 @@ class ChoiceWidget(Widget): groups.append((group_name, subgroup, index)) for subvalue, sublabel in choices: - selected = ( - (not has_selected or self.allow_multiple_selected) and - str(subvalue) in value - ) + selected = (not has_selected or self.allow_multiple_selected) and str( + subvalue + ) in value has_selected |= selected - subgroup.append(self.create_option( - name, subvalue, sublabel, selected, index, - subindex=subindex, attrs=attrs, - )) + subgroup.append( + self.create_option( + name, + subvalue, + sublabel, + selected, + index, + subindex=subindex, + attrs=attrs, + ) + ) if subindex is not None: subindex += 1 return groups - def create_option(self, name, value, label, selected, index, subindex=None, attrs=None): + def create_option( + self, name, value, label, selected, index, subindex=None, attrs=None + ): index = str(index) if subindex is None else "%s_%s" % (index, subindex) - option_attrs = self.build_attrs(self.attrs, attrs) if self.option_inherits_attrs else {} + option_attrs = ( + self.build_attrs(self.attrs, attrs) if self.option_inherits_attrs else {} + ) if selected: option_attrs.update(self.checked_attribute) - if 'id' in option_attrs: - option_attrs['id'] = self.id_for_label(option_attrs['id'], index) + if "id" in option_attrs: + option_attrs["id"] = self.id_for_label(option_attrs["id"], index) return { - 'name': name, - 'value': value, - 'label': label, - 'selected': selected, - 'index': index, - 'attrs': option_attrs, - 'type': self.input_type, - 'template_name': self.option_template_name, - 'wrap_label': True, + "name": name, + "value": value, + "label": label, + "selected": selected, + "index": index, + "attrs": option_attrs, + "type": self.input_type, + "template_name": self.option_template_name, + "wrap_label": True, } def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) - context['widget']['optgroups'] = self.optgroups(name, context['widget']['value'], attrs) + context["widget"]["optgroups"] = self.optgroups( + name, context["widget"]["value"], attrs + ) return context - def id_for_label(self, id_, index='0'): + def id_for_label(self, id_, index="0"): """ Use an incremented id for each option where the main widget references the zero index. """ if id_ and self.add_id_index: - id_ = '%s_%s' % (id_, index) + id_ = "%s_%s" % (id_, index) return id_ def value_from_datadict(self, data, files, name): @@ -666,28 +714,28 @@ class ChoiceWidget(Widget): return [] if not isinstance(value, (tuple, list)): value = [value] - return [str(v) if v is not None else '' for v in value] + return [str(v) if v is not None else "" for v in value] class Select(ChoiceWidget): - input_type = 'select' - template_name = 'django/forms/widgets/select.html' - option_template_name = 'django/forms/widgets/select_option.html' + input_type = "select" + template_name = "django/forms/widgets/select.html" + option_template_name = "django/forms/widgets/select_option.html" add_id_index = False - checked_attribute = {'selected': True} + checked_attribute = {"selected": True} option_inherits_attrs = False def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) if self.allow_multiple_selected: - context['widget']['attrs']['multiple'] = True + context["widget"]["attrs"]["multiple"] = True return context @staticmethod def _choice_has_empty_value(choice): """Return True if the choice's value is empty string or None.""" value, _ = choice - return value is None or value == '' + return value is None or value == "" def use_required_attribute(self, initial): """ @@ -700,44 +748,52 @@ class Select(ChoiceWidget): return use_required_attribute first_choice = next(iter(self.choices), None) - return use_required_attribute and first_choice is not None and self._choice_has_empty_value(first_choice) + return ( + use_required_attribute + and first_choice is not None + and self._choice_has_empty_value(first_choice) + ) class NullBooleanSelect(Select): """ A Select Widget intended to be used with NullBooleanField. """ + def __init__(self, attrs=None): choices = ( - ('unknown', _('Unknown')), - ('true', _('Yes')), - ('false', _('No')), + ("unknown", _("Unknown")), + ("true", _("Yes")), + ("false", _("No")), ) super().__init__(attrs, choices) def format_value(self, value): try: return { - True: 'true', False: 'false', - 'true': 'true', 'false': 'false', + True: "true", + False: "false", + "true": "true", + "false": "false", # For backwards compatibility with Django < 2.2. - '2': 'true', '3': 'false', + "2": "true", + "3": "false", }[value] except KeyError: - return 'unknown' + return "unknown" def value_from_datadict(self, data, files, name): value = data.get(name) return { True: True, - 'True': True, - 'False': False, + "True": True, + "False": False, False: False, - 'true': True, - 'false': False, + "true": True, + "false": False, # For backwards compatibility with Django < 2.2. - '2': True, - '3': False, + "2": True, + "3": False, }.get(value) @@ -758,9 +814,9 @@ class SelectMultiple(Select): class RadioSelect(ChoiceWidget): - input_type = 'radio' - template_name = 'django/forms/widgets/radio.html' - option_template_name = 'django/forms/widgets/radio_option.html' + input_type = "radio" + template_name = "django/forms/widgets/radio.html" + option_template_name = "django/forms/widgets/radio_option.html" def id_for_label(self, id_, index=None): """ @@ -769,15 +825,15 @@ class RadioSelect(ChoiceWidget): the first input. """ if index is None: - return '' + return "" return super().id_for_label(id_, index) class CheckboxSelectMultiple(RadioSelect): allow_multiple_selected = True - input_type = 'checkbox' - template_name = 'django/forms/widgets/checkbox_select.html' - option_template_name = 'django/forms/widgets/checkbox_option.html' + input_type = "checkbox" + template_name = "django/forms/widgets/checkbox_select.html" + option_template_name = "django/forms/widgets/checkbox_option.html" def use_required_attribute(self, initial): # Don't use the 'required' attribute because browser validation would @@ -800,16 +856,15 @@ class MultiWidget(Widget): You'll probably want to use this class with MultiValueField. """ - template_name = 'django/forms/widgets/multiwidget.html' + + template_name = "django/forms/widgets/multiwidget.html" def __init__(self, widgets, attrs=None): if isinstance(widgets, dict): - self.widgets_names = [ - ('_%s' % name) if name else '' for name in widgets - ] + self.widgets_names = [("_%s" % name) if name else "" for name in widgets] widgets = widgets.values() else: - self.widgets_names = ['_%s' % i for i in range(len(widgets))] + self.widgets_names = ["_%s" % i for i in range(len(widgets))] self.widgets = [w() if isinstance(w, type) else w for w in widgets] super().__init__(attrs) @@ -827,11 +882,13 @@ class MultiWidget(Widget): if not isinstance(value, list): value = self.decompress(value) - final_attrs = context['widget']['attrs'] - input_type = final_attrs.pop('type', None) - id_ = final_attrs.get('id') + final_attrs = context["widget"]["attrs"] + input_type = final_attrs.pop("type", None) + id_ = final_attrs.get("id") subwidgets = [] - for i, (widget_name, widget) in enumerate(zip(self.widgets_names, self.widgets)): + for i, (widget_name, widget) in enumerate( + zip(self.widgets_names, self.widgets) + ): if input_type is not None: widget.input_type = input_type widget_name = name + widget_name @@ -841,15 +898,17 @@ class MultiWidget(Widget): widget_value = None if id_: widget_attrs = final_attrs.copy() - widget_attrs['id'] = '%s_%s' % (id_, i) + widget_attrs["id"] = "%s_%s" % (id_, i) else: widget_attrs = final_attrs - subwidgets.append(widget.get_context(widget_name, widget_value, widget_attrs)['widget']) - context['widget']['subwidgets'] = subwidgets + subwidgets.append( + widget.get_context(widget_name, widget_value, widget_attrs)["widget"] + ) + context["widget"]["subwidgets"] = subwidgets return context def id_for_label(self, id_): - return '' + return "" def value_from_datadict(self, data, files, name): return [ @@ -869,7 +928,7 @@ class MultiWidget(Widget): The given value can be assumed to be valid, but not necessarily non-empty. """ - raise NotImplementedError('Subclasses must implement this method.') + raise NotImplementedError("Subclasses must implement this method.") def _get_media(self): """ @@ -880,6 +939,7 @@ class MultiWidget(Widget): for w in self.widgets: media = media + w.media return media + media = property(_get_media) def __deepcopy__(self, memo): @@ -896,10 +956,18 @@ class SplitDateTimeWidget(MultiWidget): """ A widget that splits datetime input into two <input type="text"> boxes. """ + supports_microseconds = False - template_name = 'django/forms/widgets/splitdatetime.html' + template_name = "django/forms/widgets/splitdatetime.html" - def __init__(self, attrs=None, date_format=None, time_format=None, date_attrs=None, time_attrs=None): + def __init__( + self, + attrs=None, + date_format=None, + time_format=None, + date_attrs=None, + time_attrs=None, + ): widgets = ( DateInput( attrs=attrs if date_attrs is None else date_attrs, @@ -923,12 +991,20 @@ class SplitHiddenDateTimeWidget(SplitDateTimeWidget): """ A widget that splits datetime input into two <input type="hidden"> inputs. """ - template_name = 'django/forms/widgets/splithiddendatetime.html' - def __init__(self, attrs=None, date_format=None, time_format=None, date_attrs=None, time_attrs=None): + template_name = "django/forms/widgets/splithiddendatetime.html" + + def __init__( + self, + attrs=None, + date_format=None, + time_format=None, + date_attrs=None, + time_attrs=None, + ): super().__init__(attrs, date_format, time_format, date_attrs, time_attrs) for widget in self.widgets: - widget.input_type = 'hidden' + widget.input_type = "hidden" class SelectDateWidget(Widget): @@ -938,14 +1014,15 @@ class SelectDateWidget(Widget): This also serves as an example of a Widget that has more than one HTML element and hence implements value_from_datadict. """ - none_value = ('', '---') - month_field = '%s_month' - day_field = '%s_day' - year_field = '%s_year' - template_name = 'django/forms/widgets/select_date.html' - input_type = 'select' + + none_value = ("", "---") + month_field = "%s_month" + day_field = "%s_day" + year_field = "%s_year" + template_name = "django/forms/widgets/select_date.html" + input_type = "select" select_widget = Select - date_re = _lazy_re_compile(r'(\d{4}|0)-(\d\d?)-(\d\d?)$') + date_re = _lazy_re_compile(r"(\d{4}|0)-(\d\d?)-(\d\d?)$") def __init__(self, attrs=None, years=None, months=None, empty_label=None): self.attrs = attrs or {} @@ -966,14 +1043,14 @@ class SelectDateWidget(Widget): # Optional string, list, or tuple to use as empty_label. if isinstance(empty_label, (list, tuple)): if not len(empty_label) == 3: - raise ValueError('empty_label list/tuple must have 3 elements.') + raise ValueError("empty_label list/tuple must have 3 elements.") - self.year_none_value = ('', empty_label[0]) - self.month_none_value = ('', empty_label[1]) - self.day_none_value = ('', empty_label[2]) + self.year_none_value = ("", empty_label[0]) + self.month_none_value = ("", empty_label[1]) + self.day_none_value = ("", empty_label[2]) else: if empty_label is not None: - self.none_value = ('', empty_label) + self.none_value = ("", empty_label) self.year_none_value = self.none_value self.month_none_value = self.none_value @@ -986,33 +1063,40 @@ class SelectDateWidget(Widget): if not self.is_required: year_choices.insert(0, self.year_none_value) year_name = self.year_field % name - date_context['year'] = self.select_widget(attrs, choices=year_choices).get_context( + date_context["year"] = self.select_widget( + attrs, choices=year_choices + ).get_context( name=year_name, - value=context['widget']['value']['year'], - attrs={**context['widget']['attrs'], 'id': 'id_%s' % year_name}, + value=context["widget"]["value"]["year"], + attrs={**context["widget"]["attrs"], "id": "id_%s" % year_name}, ) month_choices = list(self.months.items()) if not self.is_required: month_choices.insert(0, self.month_none_value) month_name = self.month_field % name - date_context['month'] = self.select_widget(attrs, choices=month_choices).get_context( + date_context["month"] = self.select_widget( + attrs, choices=month_choices + ).get_context( name=month_name, - value=context['widget']['value']['month'], - attrs={**context['widget']['attrs'], 'id': 'id_%s' % month_name}, + value=context["widget"]["value"]["month"], + attrs={**context["widget"]["attrs"], "id": "id_%s" % month_name}, ) day_choices = [(i, i) for i in range(1, 32)] if not self.is_required: day_choices.insert(0, self.day_none_value) day_name = self.day_field % name - date_context['day'] = self.select_widget(attrs, choices=day_choices,).get_context( + date_context["day"] = self.select_widget( + attrs, + choices=day_choices, + ).get_context( name=day_name, - value=context['widget']['value']['day'], - attrs={**context['widget']['attrs'], 'id': 'id_%s' % day_name}, + value=context["widget"]["value"]["day"], + attrs={**context["widget"]["attrs"], "id": "id_%s" % day_name}, ) subwidgets = [] for field in self._parse_date_fmt(): - subwidgets.append(date_context[field]['widget']) - context['widget']['subwidgets'] = subwidgets + subwidgets.append(date_context[field]["widget"]) + context["widget"]["subwidgets"] = subwidgets return context def format_value(self, value): @@ -1029,58 +1113,58 @@ class SelectDateWidget(Widget): if match: # Convert any zeros in the date to empty strings to match the # empty option value. - year, month, day = [int(val) or '' for val in match.groups()] + year, month, day = [int(val) or "" for val in match.groups()] else: - input_format = get_format('DATE_INPUT_FORMATS')[0] + input_format = get_format("DATE_INPUT_FORMATS")[0] try: d = datetime.datetime.strptime(value, input_format) except ValueError: pass else: year, month, day = d.year, d.month, d.day - return {'year': year, 'month': month, 'day': day} + return {"year": year, "month": month, "day": day} @staticmethod def _parse_date_fmt(): - fmt = get_format('DATE_FORMAT') + fmt = get_format("DATE_FORMAT") escaped = False for char in fmt: if escaped: escaped = False - elif char == '\\': + elif char == "\\": escaped = True - elif char in 'Yy': - yield 'year' - elif char in 'bEFMmNn': - yield 'month' - elif char in 'dj': - yield 'day' + elif char in "Yy": + yield "year" + elif char in "bEFMmNn": + yield "month" + elif char in "dj": + yield "day" def id_for_label(self, id_): for first_select in self._parse_date_fmt(): - return '%s_%s' % (id_, first_select) - return '%s_month' % id_ + return "%s_%s" % (id_, first_select) + return "%s_month" % id_ def value_from_datadict(self, data, files, name): y = data.get(self.year_field % name) m = data.get(self.month_field % name) d = data.get(self.day_field % name) - if y == m == d == '': + if y == m == d == "": return None if y is not None and m is not None and d is not None: - input_format = get_format('DATE_INPUT_FORMATS')[0] + input_format = get_format("DATE_INPUT_FORMATS")[0] input_format = formats.sanitize_strftime_format(input_format) try: date_value = datetime.date(int(y), int(m), int(d)) except ValueError: # Return pseudo-ISO dates with zeros for any unselected values, # e.g. '2017-0-23'. - return '%s-%s-%s' % (y or 0, m or 0, d or 0) + return "%s-%s-%s" % (y or 0, m or 0, d or 0) return date_value.strftime(input_format) return data.get(name) def value_omitted_from_data(self, data, files, name): return not any( - ('{}_{}'.format(name, interval) in data) - for interval in ('year', 'month', 'day') + ("{}_{}".format(name, interval) in data) + for interval in ("year", "month", "day") ) |
