Plain is headed towards 1.0! Subscribe for development updates →

plain.models

Model your data and store it in a database.

# app/users/models.py
from plain import models
from plain.passwords.models import PasswordField


class User(models.Model):
    email = models.EmailField(unique=True)
    password = PasswordField()
    is_staff = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.email

Create, update, and delete instances of your models:

from .models import User


# Create a new user
user = User.objects.create(
    email="[email protected]",
    password="password",
)

# Update a user
user.email = "[email protected]"
user.save()

# Delete a user
user.delete()

# Query for users
staff_users = User.objects.filter(is_staff=True)

Installation

# app/settings.py
INSTALLED_PACKAGES = [
    ...
    "plain.models",
]

To connect to a database, you can provide a DATABASE_URL environment variable.

DATABASE_URL=postgresql://user:password@localhost:5432/dbname

Or you can manually define the DATABASES setting.

# app/settings.py
DATABASES = {
    "default": {
        "ENGINE": "plain.models.backends.postgresql",
        "NAME": "dbname",
        "USER": "user",
        "PASSWORD": "password",
        "HOST": "localhost",
        "PORT": "5432",
    }
}

Multiple backends are supported, including Postgres, MySQL, and SQLite.

Querying

Migrations

Migration docs

Fields

Field docs

Validation

Indexes and constraints

Managers

Forms

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