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)