Plain is headed towards 1.0! Subscribe for development updates →

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