diff options
Diffstat (limited to 'django/forms/models.py')
| -rw-r--r-- | django/forms/models.py | 727 |
1 files changed, 476 insertions, 251 deletions
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 ) |
