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