1"""
2Helper functions for creating Form classes from Plain models
3and database field objects.
4"""
5
6from itertools import chain
7
8from plain.exceptions import (
9 NON_FIELD_ERRORS,
10 FieldError,
11 ImproperlyConfigured,
12 ValidationError,
13)
14from plain.forms import fields
15from plain.forms.fields import ChoiceField, Field
16from plain.forms.forms import BaseForm, DeclarativeFieldsMetaclass
17
18__all__ = (
19 "ModelForm",
20 "BaseModelForm",
21 "model_to_dict",
22 "fields_for_model",
23 "ModelChoiceField",
24 "ModelMultipleChoiceField",
25)
26
27
28def construct_instance(form, instance, fields=None):
29 """
30 Construct and return a model instance from the bound ``form``'s
31 ``cleaned_data``, but do not save the returned instance to the database.
32 """
33 from plain import models
34
35 opts = instance._meta
36
37 cleaned_data = form.cleaned_data
38 file_field_list = []
39 for f in opts.fields:
40 if isinstance(f, models.AutoField) or f.name not in cleaned_data:
41 continue
42 if fields is not None and f.name not in fields:
43 continue
44 # Leave defaults for fields that aren't in POST data, except for
45 # checkbox inputs because they don't appear in POST data if not checked.
46 if (
47 f.has_default()
48 and form.add_prefix(f.name) not in form.data
49 and form.add_prefix(f.name) not in form.files
50 # and form[f.name].field.widget.value_omitted_from_data(
51 # form.data, form.files, form.add_prefix(f.name)
52 # )
53 and cleaned_data.get(f.name) in form[f.name].field.empty_values
54 ):
55 continue
56
57 f.save_form_data(instance, cleaned_data[f.name])
58
59 for f in file_field_list:
60 f.save_form_data(instance, cleaned_data[f.name])
61
62 return instance
63
64
65# ModelForms #################################################################
66
67
68def model_to_dict(instance, fields=None):
69 """
70 Return a dict containing the data in ``instance`` suitable for passing as
71 a Form's ``initial`` keyword argument.
72
73 ``fields`` is an optional list of field names. If provided, return only the
74 named.
75 """
76 opts = instance._meta
77 data = {}
78 for f in chain(opts.concrete_fields, opts.many_to_many):
79 if fields is not None and f.name not in fields:
80 continue
81 data[f.name] = f.value_from_object(instance)
82 return data
83
84
85def fields_for_model(
86 model,
87 fields=None,
88 formfield_callback=None,
89 error_messages=None,
90 field_classes=None,
91):
92 """
93 Return a dictionary containing form fields for the given model.
94
95 ``fields`` is an optional list of field names. If provided, return only the
96 named fields.
97
98 ``formfield_callback`` is a callable that takes a model field and returns
99 a form field.
100
101 ``error_messages`` is a dictionary of model field names mapped to a
102 dictionary of error messages.
103
104 ``field_classes`` is a dictionary of model field names mapped to a form
105 field class.
106 """
107 field_dict = {}
108 ignored = []
109 opts = model._meta
110
111 for f in sorted(chain(opts.concrete_fields, opts.many_to_many)):
112 if fields is not None and f.name not in fields:
113 continue
114
115 kwargs = {}
116 if error_messages and f.name in error_messages:
117 kwargs["error_messages"] = error_messages[f.name]
118 if field_classes and f.name in field_classes:
119 kwargs["form_class"] = field_classes[f.name]
120
121 if formfield_callback is None:
122 formfield = modelfield_to_formfield(f, **kwargs)
123 elif not callable(formfield_callback):
124 raise TypeError("formfield_callback must be a function or callable")
125 else:
126 formfield = formfield_callback(f, **kwargs)
127
128 if formfield:
129 field_dict[f.name] = formfield
130 else:
131 ignored.append(f.name)
132 if fields:
133 field_dict = {f: field_dict.get(f) for f in fields if f not in ignored}
134 return field_dict
135
136
137class ModelFormOptions:
138 def __init__(self, options=None):
139 self.model = getattr(options, "model", None)
140 self.fields = getattr(options, "fields", None)
141 self.error_messages = getattr(options, "error_messages", None)
142 self.field_classes = getattr(options, "field_classes", None)
143 self.formfield_callback = getattr(options, "formfield_callback", None)
144
145
146class ModelFormMetaclass(DeclarativeFieldsMetaclass):
147 def __new__(mcs, name, bases, attrs):
148 new_class = super().__new__(mcs, name, bases, attrs)
149
150 if bases == (BaseModelForm,):
151 return new_class
152
153 opts = new_class._meta = ModelFormOptions(getattr(new_class, "Meta", None))
154
155 # We check if a string was passed to `fields`,
156 # which is likely to be a mistake where the user typed ('foo') instead
157 # of ('foo',)
158 for opt in ["fields"]:
159 value = getattr(opts, opt)
160 if isinstance(value, str):
161 msg = (
162 f"{new_class.__name__}.Meta.{opt} cannot be a string. "
163 f"Did you mean to type: ('{value}',)?"
164 )
165 raise TypeError(msg)
166
167 if opts.model:
168 # If a model is defined, extract form fields from it.
169 if opts.fields is None:
170 raise ImproperlyConfigured(
171 "Creating a ModelForm without the 'fields' attribute "
172 f"is prohibited; form {name} "
173 "needs updating."
174 )
175
176 fields = fields_for_model(
177 opts.model,
178 opts.fields,
179 opts.formfield_callback,
180 opts.error_messages,
181 opts.field_classes,
182 )
183
184 # make sure opts.fields doesn't specify an invalid field
185 none_model_fields = {k for k, v in fields.items() if not v}
186 missing_fields = none_model_fields.difference(new_class.declared_fields)
187 if missing_fields:
188 message = "Unknown field(s) (%s) specified for %s"
189 message %= (", ".join(missing_fields), opts.model.__name__)
190 raise FieldError(message)
191 # Override default model fields with any custom declared ones
192 # (plus, include all the other declared fields).
193 fields.update(new_class.declared_fields)
194 else:
195 fields = new_class.declared_fields
196
197 new_class.base_fields = fields
198
199 return new_class
200
201
202class BaseModelForm(BaseForm):
203 def __init__(
204 self,
205 *,
206 request,
207 auto_id="id_%s",
208 prefix=None,
209 initial=None,
210 instance=None,
211 ):
212 opts = self._meta
213 if opts.model is None:
214 raise ValueError("ModelForm has no model class specified.")
215 if instance is None:
216 # if we didn't get an instance, instantiate a new one
217 self.instance = opts.model()
218 object_data = {}
219 else:
220 self.instance = instance
221 object_data = model_to_dict(instance, opts.fields)
222 # if initial was provided, it should override the values from instance
223 if initial is not None:
224 object_data.update(initial)
225 # self._validate_unique will be set to True by BaseModelForm.clean().
226 # It is False by default so overriding self.clean() and failing to call
227 # super will stop validate_unique from being called.
228 self._validate_unique = False
229 super().__init__(
230 request=request,
231 auto_id=auto_id,
232 prefix=prefix,
233 initial=object_data,
234 )
235
236 def _get_validation_exclusions(self):
237 """
238 For backwards-compatibility, exclude several types of fields from model
239 validation. See tickets #12507, #12521, #12553.
240 """
241 exclude = set()
242 # Build up a list of fields that should be excluded from model field
243 # validation and unique checks.
244 for f in self.instance._meta.fields:
245 field = f.name
246 # Exclude fields that aren't on the form. The developer may be
247 # adding these values to the model after form validation.
248 if field not in self.fields:
249 exclude.add(f.name)
250
251 # Don't perform model validation on fields that were defined
252 # manually on the form and excluded via the ModelForm's Meta
253 # class. See #12901.
254 elif self._meta.fields and field not in self._meta.fields:
255 exclude.add(f.name)
256
257 # Exclude fields that failed form validation. There's no need for
258 # the model fields to validate them as well.
259 elif field in self._errors:
260 exclude.add(f.name)
261
262 # Exclude empty fields that are not required by the form, if the
263 # underlying model field is required. This keeps the model field
264 # from raising a required error. Note: don't exclude the field from
265 # validation if the model field allows blanks. If it does, the blank
266 # value may be included in a unique check, so cannot be excluded
267 # from validation.
268 else:
269 form_field = self.fields[field]
270 field_value = self.cleaned_data.get(field)
271 if (
272 f.required
273 and not form_field.required
274 and field_value in form_field.empty_values
275 ):
276 exclude.add(f.name)
277 return exclude
278
279 def clean(self):
280 self._validate_unique = True
281 return self.cleaned_data
282
283 def _update_errors(self, errors):
284 # Override any validation error messages defined at the model level
285 # with those defined at the form level.
286 opts = self._meta
287
288 # Allow the model generated by construct_instance() to raise
289 # ValidationError and have them handled in the same way as others.
290 if hasattr(errors, "error_dict"):
291 error_dict = errors.error_dict
292 else:
293 error_dict = {NON_FIELD_ERRORS: errors}
294
295 for field, messages in error_dict.items():
296 if (
297 field == NON_FIELD_ERRORS
298 and opts.error_messages
299 and NON_FIELD_ERRORS in opts.error_messages
300 ):
301 error_messages = opts.error_messages[NON_FIELD_ERRORS]
302 elif field in self.fields:
303 error_messages = self.fields[field].error_messages
304 else:
305 continue
306
307 for message in messages:
308 if (
309 isinstance(message, ValidationError)
310 and message.code in error_messages
311 ):
312 message.message = error_messages[message.code]
313
314 self.add_error(None, errors)
315
316 def _post_clean(self):
317 opts = self._meta
318
319 exclude = self._get_validation_exclusions()
320
321 # Foreign Keys being used to represent inline relationships
322 # are excluded from basic field value validation. This is for two
323 # reasons: firstly, the value may not be supplied (#12507; the
324 # case of providing new values to the admin); secondly the
325 # object being referred to may not yet fully exist (#12749).
326 # However, these fields *must* be included in uniqueness checks,
327 # so this can't be part of _get_validation_exclusions().
328 for name, field in self.fields.items():
329 if isinstance(field, InlineForeignKeyField):
330 exclude.add(name)
331
332 try:
333 self.instance = construct_instance(self, self.instance, opts.fields)
334 except ValidationError as e:
335 self._update_errors(e)
336
337 try:
338 self.instance.full_clean(exclude=exclude, validate_unique=False)
339 except ValidationError as e:
340 self._update_errors(e)
341
342 # Validate uniqueness if needed.
343 if self._validate_unique:
344 self.validate_unique()
345
346 def validate_unique(self):
347 """
348 Call the instance's validate_unique() method and update the form's
349 validation errors if any were raised.
350 """
351 exclude = self._get_validation_exclusions()
352 try:
353 self.instance.validate_unique(exclude=exclude)
354 except ValidationError as e:
355 self._update_errors(e)
356
357 def _save_m2m(self):
358 """
359 Save the many-to-many fields and generic relations for this form.
360 """
361 cleaned_data = self.cleaned_data
362 fields = self._meta.fields
363 opts = self.instance._meta
364
365 for f in opts.many_to_many:
366 if not hasattr(f, "save_form_data"):
367 continue
368 if fields and f.name not in fields:
369 continue
370 if f.name in cleaned_data:
371 f.save_form_data(self.instance, cleaned_data[f.name])
372
373 def save(self, commit=True):
374 """
375 Save this form's self.instance object if commit=True. Otherwise, add
376 a save_m2m() method to the form which can be called after the instance
377 is saved manually at a later time. Return the model instance.
378 """
379 if self.errors:
380 raise ValueError(
381 "The {} could not be {} because the data didn't validate.".format(
382 self.instance._meta.object_name,
383 "created" if self.instance._state.adding else "changed",
384 )
385 )
386 if commit:
387 # If committing, save the instance and the m2m data immediately.
388 self.instance.save(clean_and_validate=False)
389 self._save_m2m()
390 else:
391 # If not committing, add a method to the form to allow deferred
392 # saving of m2m data.
393 self.save_m2m = self._save_m2m
394 return self.instance
395
396
397class ModelForm(BaseModelForm, metaclass=ModelFormMetaclass):
398 pass
399
400
401# Fields #####################################################################
402
403
404class InlineForeignKeyField(Field):
405 """
406 A basic integer field that deals with validating the given value to a
407 given parent instance in an inline.
408 """
409
410 default_error_messages = {
411 "invalid_choice": "The inline value did not match the parent instance.",
412 }
413
414 def __init__(self, parent_instance, *args, pk_field=False, to_field=None, **kwargs):
415 self.parent_instance = parent_instance
416 self.pk_field = pk_field
417 self.to_field = to_field
418 if self.parent_instance is not None:
419 if self.to_field:
420 kwargs["initial"] = getattr(self.parent_instance, self.to_field)
421 else:
422 kwargs["initial"] = self.parent_instance.pk
423 kwargs["required"] = False
424 super().__init__(*args, **kwargs)
425
426 def clean(self, value):
427 if value in self.empty_values:
428 if self.pk_field:
429 return None
430 # if there is no value act as we did before.
431 return self.parent_instance
432 # ensure the we compare the values as equal types.
433 if self.to_field:
434 orig = getattr(self.parent_instance, self.to_field)
435 else:
436 orig = self.parent_instance.pk
437 if str(value) != str(orig):
438 raise ValidationError(
439 self.error_messages["invalid_choice"], code="invalid_choice"
440 )
441 return self.parent_instance
442
443 def has_changed(self, initial, data):
444 return False
445
446
447class ModelChoiceIteratorValue:
448 def __init__(self, value, instance):
449 self.value = value
450 self.instance = instance
451
452 def __str__(self):
453 return str(self.value)
454
455 def __hash__(self):
456 return hash(self.value)
457
458 def __eq__(self, other):
459 if isinstance(other, ModelChoiceIteratorValue):
460 other = other.value
461 return self.value == other
462
463
464class ModelChoiceIterator:
465 def __init__(self, field):
466 self.field = field
467 self.queryset = field.queryset
468
469 def __iter__(self):
470 if self.field.empty_label is not None:
471 yield ("", self.field.empty_label)
472 queryset = self.queryset
473 # Can't use iterator() when queryset uses prefetch_related()
474 if not queryset._prefetch_related_lookups:
475 queryset = queryset.iterator()
476 for obj in queryset:
477 yield self.choice(obj)
478
479 def __len__(self):
480 # count() adds a query but uses less memory since the QuerySet results
481 # won't be cached. In most cases, the choices will only be iterated on,
482 # and __len__() won't be called.
483 return self.queryset.count() + (1 if self.field.empty_label is not None else 0)
484
485 def __bool__(self):
486 return self.field.empty_label is not None or self.queryset.exists()
487
488 def choice(self, obj):
489 return (
490 ModelChoiceIteratorValue(self.field.prepare_value(obj), obj),
491 str(obj),
492 )
493
494
495class ModelChoiceField(ChoiceField):
496 """A ChoiceField whose choices are a model QuerySet."""
497
498 # This class is a subclass of ChoiceField for purity, but it doesn't
499 # actually use any of ChoiceField's implementation.
500 default_error_messages = {
501 "invalid_choice": "Select a valid choice. That choice is not one of the available choices.",
502 }
503 iterator = ModelChoiceIterator
504
505 def __init__(
506 self,
507 queryset,
508 *,
509 empty_label="---------",
510 required=True,
511 initial=None,
512 to_field_name=None,
513 **kwargs,
514 ):
515 # Call Field instead of ChoiceField __init__() because we don't need
516 # ChoiceField.__init__().
517 Field.__init__(
518 self,
519 required=required,
520 initial=initial,
521 **kwargs,
522 )
523 if required and initial is not None:
524 self.empty_label = None
525 else:
526 self.empty_label = empty_label
527 self.queryset = queryset
528 self.to_field_name = to_field_name
529
530 def __deepcopy__(self, memo):
531 result = super(ChoiceField, self).__deepcopy__(memo)
532 # Need to force a new ModelChoiceIterator to be created, bug #11183
533 if self.queryset is not None:
534 result.queryset = self.queryset.all()
535 return result
536
537 def _get_queryset(self):
538 return self._queryset
539
540 def _set_queryset(self, queryset):
541 self._queryset = None if queryset is None else queryset.all()
542
543 queryset = property(_get_queryset, _set_queryset)
544
545 def _get_choices(self):
546 # If self._choices is set, then somebody must have manually set
547 # the property self.choices. In this case, just return self._choices.
548 if hasattr(self, "_choices"):
549 return self._choices
550
551 # Otherwise, execute the QuerySet in self.queryset to determine the
552 # choices dynamically. Return a fresh ModelChoiceIterator that has not been
553 # consumed. Note that we're instantiating a new ModelChoiceIterator *each*
554 # time _get_choices() is called (and, thus, each time self.choices is
555 # accessed) so that we can ensure the QuerySet has not been consumed. This
556 # construct might look complicated but it allows for lazy evaluation of
557 # the queryset.
558 return self.iterator(self)
559
560 choices = property(_get_choices, ChoiceField._set_choices)
561
562 def prepare_value(self, value):
563 if hasattr(value, "_meta"):
564 if self.to_field_name:
565 return value.serializable_value(self.to_field_name)
566 else:
567 return value.pk
568 return super().prepare_value(value)
569
570 def to_python(self, value):
571 if value in self.empty_values:
572 return None
573 try:
574 key = self.to_field_name or "pk"
575 if isinstance(value, self.queryset.model):
576 value = getattr(value, key)
577 value = self.queryset.get(**{key: value})
578 except (ValueError, TypeError, self.queryset.model.DoesNotExist):
579 raise ValidationError(
580 self.error_messages["invalid_choice"],
581 code="invalid_choice",
582 params={"value": value},
583 )
584 return value
585
586 def validate(self, value):
587 return Field.validate(self, value)
588
589 def has_changed(self, initial, data):
590 initial_value = initial if initial is not None else ""
591 data_value = data if data is not None else ""
592 return str(self.prepare_value(initial_value)) != str(data_value)
593
594
595class ModelMultipleChoiceField(ModelChoiceField):
596 """A MultipleChoiceField whose choices are a model QuerySet."""
597
598 default_error_messages = {
599 "invalid_list": "Enter a list of values.",
600 "invalid_choice": "Select a valid choice. %(value)s is not one of the available choices.",
601 "invalid_pk_value": "โ%(pk)sโ is not a valid value.",
602 }
603
604 def __init__(self, queryset, **kwargs):
605 super().__init__(queryset, empty_label=None, **kwargs)
606
607 def to_python(self, value):
608 if not value:
609 return []
610 return list(self._check_values(value))
611
612 def clean(self, value):
613 value = self.prepare_value(value)
614 if self.required and not value:
615 raise ValidationError(self.error_messages["required"], code="required")
616 elif not self.required and not value:
617 return self.queryset.none()
618 if not isinstance(value, list | tuple):
619 raise ValidationError(
620 self.error_messages["invalid_list"],
621 code="invalid_list",
622 )
623 qs = self._check_values(value)
624 # Since this overrides the inherited ModelChoiceField.clean
625 # we run custom validators here
626 self.run_validators(value)
627 return qs
628
629 def _check_values(self, value):
630 """
631 Given a list of possible PK values, return a QuerySet of the
632 corresponding objects. Raise a ValidationError if a given value is
633 invalid (not a valid PK, not in the queryset, etc.)
634 """
635 key = self.to_field_name or "pk"
636 # deduplicate given values to avoid creating many querysets or
637 # requiring the database backend deduplicate efficiently.
638 try:
639 value = frozenset(value)
640 except TypeError:
641 # list of lists isn't hashable, for example
642 raise ValidationError(
643 self.error_messages["invalid_list"],
644 code="invalid_list",
645 )
646 for pk in value:
647 try:
648 self.queryset.filter(**{key: pk})
649 except (ValueError, TypeError):
650 raise ValidationError(
651 self.error_messages["invalid_pk_value"],
652 code="invalid_pk_value",
653 params={"pk": pk},
654 )
655 qs = self.queryset.filter(**{f"{key}__in": value})
656 pks = {str(getattr(o, key)) for o in qs}
657 for val in value:
658 if str(val) not in pks:
659 raise ValidationError(
660 self.error_messages["invalid_choice"],
661 code="invalid_choice",
662 params={"value": val},
663 )
664 return qs
665
666 def prepare_value(self, value):
667 if (
668 hasattr(value, "__iter__")
669 and not isinstance(value, str)
670 and not hasattr(value, "_meta")
671 ):
672 prepare_value = super().prepare_value
673 return [prepare_value(v) for v in value]
674 return super().prepare_value(value)
675
676 def has_changed(self, initial, data):
677 if initial is None:
678 initial = []
679 if data is None:
680 data = []
681 if len(initial) != len(data):
682 return True
683 initial_set = {str(value) for value in self.prepare_value(initial)}
684 data_set = {str(value) for value in data}
685 return data_set != initial_set
686
687 def value_from_form_data(self, data, files, html_name):
688 return data.getlist(html_name)
689
690
691def modelfield_to_formfield(
692 modelfield, form_class=None, choices_form_class=None, **kwargs
693):
694 defaults = {
695 "required": modelfield.required,
696 }
697
698 if modelfield.has_default():
699 defaults["initial"] = modelfield.get_default()
700
701 if modelfield.choices is not None:
702 # Fields with choices get special treatment.
703 include_blank = not modelfield.required or not (
704 modelfield.has_default() or "initial" in kwargs
705 )
706 defaults["choices"] = modelfield.get_choices(include_blank=include_blank)
707 defaults["coerce"] = modelfield.to_python
708 if modelfield.allow_null:
709 defaults["empty_value"] = None
710 if choices_form_class is not None:
711 form_class = choices_form_class
712 else:
713 form_class = fields.TypedChoiceField
714 # Many of the subclass-specific formfield arguments (min_value,
715 # max_value) don't apply for choice fields, so be sure to only pass
716 # the values that TypedChoiceField will understand.
717 for k in list(kwargs):
718 if k not in (
719 "coerce",
720 "empty_value",
721 "choices",
722 "required",
723 "initial",
724 "error_messages",
725 ):
726 del kwargs[k]
727
728 defaults.update(kwargs)
729
730 if form_class is not None:
731 return form_class(**defaults)
732
733 # Avoid a circular import
734 from plain import models
735
736 # Auto fields aren't rendered by default
737 if isinstance(modelfield, models.AutoField) or issubclass(
738 modelfield.__class__, models.AutoField
739 ):
740 return None
741
742 if isinstance(modelfield, models.BooleanField):
743 form_class = (
744 fields.NullBooleanField if modelfield.allow_null else fields.BooleanField
745 )
746 # In HTML checkboxes, 'required' means "must be checked" which is
747 # different from the choices case ("must select some value").
748 # required=False allows unchecked checkboxes.
749 defaults["required"] = False
750 return form_class(**defaults)
751
752 if isinstance(modelfield, models.DecimalField):
753 return fields.DecimalField(
754 max_digits=modelfield.max_digits,
755 decimal_places=modelfield.decimal_places,
756 **defaults,
757 )
758
759 if issubclass(modelfield.__class__, models.fields.PositiveIntegerRelDbTypeMixin):
760 return fields.IntegerField(min_value=0, **defaults)
761
762 if isinstance(modelfield, models.TextField):
763 # Passing max_length to fields.CharField means that the value's length
764 # will be validated twice. This is considered acceptable since we want
765 # the value in the form field (to pass into widget for example).
766 return fields.CharField(max_length=modelfield.max_length, **defaults)
767
768 if isinstance(modelfield, models.CharField):
769 # Passing max_length to forms.CharField means that the value's length
770 # will be validated twice. This is considered acceptable since we want
771 # the value in the form field (to pass into widget for example).
772 # TODO: Handle multiple backends with different feature flags.
773 from plain.models.db import connection
774
775 if (
776 modelfield.allow_null
777 and not connection.features.interprets_empty_strings_as_nulls
778 ):
779 defaults["empty_value"] = None
780 return fields.CharField(
781 max_length=modelfield.max_length,
782 **defaults,
783 )
784
785 if isinstance(modelfield, models.JSONField):
786 return fields.JSONField(
787 encoder=modelfield.encoder, decoder=modelfield.decoder, **defaults
788 )
789
790 if isinstance(modelfield, models.ForeignKey):
791 return ModelChoiceField(
792 queryset=modelfield.remote_field.model._default_manager,
793 to_field_name=modelfield.remote_field.field_name,
794 **defaults,
795 )
796
797 # TODO related (OneToOne, m2m)
798
799 # If there's a form field of the exact same name, use it
800 # (models.URLField -> forms.URLField)
801 if hasattr(fields, modelfield.__class__.__name__):
802 form_class = getattr(fields, modelfield.__class__.__name__)
803 return form_class(**defaults)
804
805 # Default to CharField if we didn't find anything else
806 return fields.CharField(**defaults)