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)