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