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
Fields
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)