plain.postgres 0.84.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. plain/postgres/CHANGELOG.md +1028 -0
  2. plain/postgres/README.md +925 -0
  3. plain/postgres/__init__.py +120 -0
  4. plain/postgres/agents/.claude/rules/plain-postgres.md +78 -0
  5. plain/postgres/aggregates.py +236 -0
  6. plain/postgres/backups/__init__.py +0 -0
  7. plain/postgres/backups/cli.py +148 -0
  8. plain/postgres/backups/clients.py +94 -0
  9. plain/postgres/backups/core.py +172 -0
  10. plain/postgres/base.py +1415 -0
  11. plain/postgres/cli/__init__.py +3 -0
  12. plain/postgres/cli/db.py +142 -0
  13. plain/postgres/cli/migrations.py +1085 -0
  14. plain/postgres/config.py +18 -0
  15. plain/postgres/connection.py +1331 -0
  16. plain/postgres/connections.py +77 -0
  17. plain/postgres/constants.py +13 -0
  18. plain/postgres/constraints.py +495 -0
  19. plain/postgres/database_url.py +94 -0
  20. plain/postgres/db.py +59 -0
  21. plain/postgres/default_settings.py +38 -0
  22. plain/postgres/deletion.py +475 -0
  23. plain/postgres/dialect.py +640 -0
  24. plain/postgres/entrypoints.py +4 -0
  25. plain/postgres/enums.py +103 -0
  26. plain/postgres/exceptions.py +217 -0
  27. plain/postgres/expressions.py +1912 -0
  28. plain/postgres/fields/__init__.py +2118 -0
  29. plain/postgres/fields/encrypted.py +354 -0
  30. plain/postgres/fields/json.py +413 -0
  31. plain/postgres/fields/mixins.py +30 -0
  32. plain/postgres/fields/related.py +1192 -0
  33. plain/postgres/fields/related_descriptors.py +290 -0
  34. plain/postgres/fields/related_lookups.py +223 -0
  35. plain/postgres/fields/related_managers.py +661 -0
  36. plain/postgres/fields/reverse_descriptors.py +229 -0
  37. plain/postgres/fields/reverse_related.py +328 -0
  38. plain/postgres/fields/timezones.py +143 -0
  39. plain/postgres/forms.py +773 -0
  40. plain/postgres/functions/__init__.py +189 -0
  41. plain/postgres/functions/comparison.py +127 -0
  42. plain/postgres/functions/datetime.py +454 -0
  43. plain/postgres/functions/math.py +140 -0
  44. plain/postgres/functions/mixins.py +59 -0
  45. plain/postgres/functions/text.py +282 -0
  46. plain/postgres/functions/window.py +125 -0
  47. plain/postgres/indexes.py +286 -0
  48. plain/postgres/lookups.py +758 -0
  49. plain/postgres/meta.py +584 -0
  50. plain/postgres/migrations/__init__.py +53 -0
  51. plain/postgres/migrations/autodetector.py +1379 -0
  52. plain/postgres/migrations/exceptions.py +54 -0
  53. plain/postgres/migrations/executor.py +188 -0
  54. plain/postgres/migrations/graph.py +364 -0
  55. plain/postgres/migrations/loader.py +377 -0
  56. plain/postgres/migrations/migration.py +180 -0
  57. plain/postgres/migrations/operations/__init__.py +34 -0
  58. plain/postgres/migrations/operations/base.py +139 -0
  59. plain/postgres/migrations/operations/fields.py +373 -0
  60. plain/postgres/migrations/operations/models.py +798 -0
  61. plain/postgres/migrations/operations/special.py +184 -0
  62. plain/postgres/migrations/optimizer.py +74 -0
  63. plain/postgres/migrations/questioner.py +340 -0
  64. plain/postgres/migrations/recorder.py +119 -0
  65. plain/postgres/migrations/serializer.py +378 -0
  66. plain/postgres/migrations/state.py +882 -0
  67. plain/postgres/migrations/utils.py +147 -0
  68. plain/postgres/migrations/writer.py +302 -0
  69. plain/postgres/options.py +207 -0
  70. plain/postgres/otel.py +231 -0
  71. plain/postgres/preflight.py +336 -0
  72. plain/postgres/query.py +2242 -0
  73. plain/postgres/query_utils.py +456 -0
  74. plain/postgres/registry.py +217 -0
  75. plain/postgres/schema.py +1885 -0
  76. plain/postgres/sql/__init__.py +40 -0
  77. plain/postgres/sql/compiler.py +1869 -0
  78. plain/postgres/sql/constants.py +22 -0
  79. plain/postgres/sql/datastructures.py +222 -0
  80. plain/postgres/sql/query.py +2947 -0
  81. plain/postgres/sql/where.py +374 -0
  82. plain/postgres/test/__init__.py +0 -0
  83. plain/postgres/test/pytest.py +117 -0
  84. plain/postgres/test/utils.py +18 -0
  85. plain/postgres/transaction.py +222 -0
  86. plain/postgres/types.py +92 -0
  87. plain/postgres/types.pyi +751 -0
  88. plain/postgres/utils.py +345 -0
  89. plain_postgres-0.84.0.dist-info/METADATA +937 -0
  90. plain_postgres-0.84.0.dist-info/RECORD +93 -0
  91. plain_postgres-0.84.0.dist-info/WHEEL +4 -0
  92. plain_postgres-0.84.0.dist-info/entry_points.txt +5 -0
  93. plain_postgres-0.84.0.dist-info/licenses/LICENSE +61 -0
@@ -0,0 +1,773 @@
1
+ """
2
+ Helper functions for creating Form classes from Plain models
3
+ and database field objects.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from itertools import chain
9
+ from typing import TYPE_CHECKING, Any, cast
10
+
11
+ from plain.exceptions import (
12
+ NON_FIELD_ERRORS,
13
+ ImproperlyConfigured,
14
+ ValidationError,
15
+ )
16
+ from plain.forms import fields
17
+ from plain.forms.fields import ChoiceField, Field
18
+ from plain.forms.forms import BaseForm, DeclarativeFieldsMetaclass
19
+ from plain.postgres.exceptions import FieldError
20
+
21
+ if TYPE_CHECKING:
22
+ from plain.postgres.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
+
34
+ def 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 postgres
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, postgres.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
+
78
+ def 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
+
97
+ def 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(
124
+ chain(meta.concrete_fields, meta.many_to_many), key=lambda f: f.name
125
+ ):
126
+ if fields is not None and f.name not in fields:
127
+ continue
128
+
129
+ kwargs = {}
130
+ if error_messages and f.name in error_messages:
131
+ kwargs["error_messages"] = error_messages[f.name]
132
+ if field_classes and f.name in field_classes:
133
+ kwargs["form_class"] = field_classes[f.name]
134
+
135
+ if formfield_callback is None:
136
+ formfield = modelfield_to_formfield(f, **kwargs)
137
+ elif not callable(formfield_callback):
138
+ raise TypeError("formfield_callback must be a function or callable")
139
+ else:
140
+ formfield = formfield_callback(f, **kwargs)
141
+
142
+ if formfield:
143
+ field_dict[f.name] = formfield
144
+ else:
145
+ ignored.append(f.name)
146
+ if fields:
147
+ field_dict = {f: field_dict.get(f) for f in fields if f not in ignored}
148
+ return field_dict
149
+
150
+
151
+ class ModelFormOptions:
152
+ def __init__(self, options: Any = None) -> None:
153
+ self.model: type[Any] | None = getattr(options, "model", None)
154
+ self.fields: list[str] | tuple[str, ...] | None = getattr(
155
+ options, "fields", None
156
+ )
157
+ self.error_messages: dict[str, Any] | None = getattr(
158
+ options, "error_messages", None
159
+ )
160
+ self.field_classes: dict[str, type[Field]] | None = getattr(
161
+ options, "field_classes", None
162
+ )
163
+ self.formfield_callback: Any = getattr(options, "formfield_callback", None)
164
+
165
+
166
+ class ModelFormMetaclass(DeclarativeFieldsMetaclass):
167
+ def __new__(
168
+ mcs: type[ModelFormMetaclass],
169
+ name: str,
170
+ bases: tuple[type, ...],
171
+ attrs: dict[str, Any],
172
+ ) -> type[BaseModelForm]:
173
+ # Metaclass __new__ returns a type, specifically type[BaseModelForm]
174
+ new_class = cast(type[BaseModelForm], super().__new__(mcs, name, bases, attrs))
175
+
176
+ if bases == (BaseModelForm,):
177
+ return new_class
178
+
179
+ opts = new_class._meta = ModelFormOptions(getattr(new_class, "Meta", None))
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)
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)
220
+ else:
221
+ fields = new_class.declared_fields
222
+
223
+ # After validation and update, all fields should be non-None
224
+ new_class.base_fields = cast(dict[str, Field], fields)
225
+
226
+ return new_class
227
+
228
+
229
+ class BaseModelForm(BaseForm):
230
+ # Set by DeclarativeFieldsMetaclass
231
+ declared_fields: dict[str, Field]
232
+ # Set by ModelFormMetaclass
233
+ _meta: ModelFormOptions
234
+
235
+ def __init__(
236
+ self,
237
+ *,
238
+ request: Any,
239
+ auto_id: str = "id_%s",
240
+ prefix: str | None = None,
241
+ initial: dict[str, Any] | None = None,
242
+ instance: Any = None,
243
+ ) -> None:
244
+ opts = self._meta
245
+ if opts.model is None:
246
+ raise ValueError("ModelForm has no model class specified.")
247
+ if instance is None:
248
+ # if we didn't get an instance, instantiate a new one
249
+ self.instance = opts.model()
250
+ object_data = {}
251
+ else:
252
+ self.instance = instance
253
+ object_data = model_to_dict(instance, opts.fields)
254
+ # if initial was provided, it should override the values from instance
255
+ if initial is not None:
256
+ object_data.update(initial)
257
+ # self._validate_unique will be set to True by BaseModelForm.clean().
258
+ # It is False by default so overriding self.clean() and failing to call
259
+ # super will stop validate_unique from being called.
260
+ self._validate_unique = False
261
+ super().__init__(
262
+ request=request,
263
+ auto_id=auto_id,
264
+ prefix=prefix,
265
+ initial=object_data,
266
+ )
267
+
268
+ def _get_validation_exclusions(self) -> set[str]:
269
+ """
270
+ For backwards-compatibility, exclude several types of fields from model
271
+ validation. See tickets #12507, #12521, #12553.
272
+ """
273
+ exclude = set()
274
+ # Build up a list of fields that should be excluded from model field
275
+ # validation and unique checks.
276
+ for f in self.instance._model_meta.fields:
277
+ field = f.name
278
+ # Exclude fields that aren't on the form. The developer may be
279
+ # adding these values to the model after form validation.
280
+ if field not in self.fields:
281
+ exclude.add(f.name)
282
+
283
+ # Don't perform model validation on fields that were defined
284
+ # manually on the form and excluded via the ModelForm's Meta
285
+ # class. See #12901.
286
+ elif self._meta.fields and field not in self._meta.fields:
287
+ exclude.add(f.name)
288
+
289
+ # Exclude fields that failed form validation. There's no need for
290
+ # the model fields to validate them as well.
291
+ elif self._errors and field in self._errors:
292
+ exclude.add(f.name)
293
+
294
+ # Exclude empty fields that are not required by the form, if the
295
+ # underlying model field is required. This keeps the model field
296
+ # from raising a required error. Note: don't exclude the field from
297
+ # validation if the model field allows blanks. If it does, the blank
298
+ # value may be included in a unique check, so cannot be excluded
299
+ # from validation.
300
+ else:
301
+ form_field = self.fields[field]
302
+ field_value = self.cleaned_data.get(field)
303
+ if (
304
+ f.required
305
+ and not form_field.required
306
+ and field_value in form_field.empty_values
307
+ ):
308
+ exclude.add(f.name)
309
+ return exclude
310
+
311
+ def clean(self) -> dict[str, Any]:
312
+ self._validate_unique = True
313
+ return self.cleaned_data
314
+
315
+ def _update_errors(self, errors: ValidationError) -> None:
316
+ # Override any validation error messages defined at the model level
317
+ # with those defined at the form level.
318
+ opts = self._meta
319
+
320
+ # Allow the model generated by construct_instance() to raise
321
+ # ValidationError and have them handled in the same way as others.
322
+ if hasattr(errors, "error_dict"):
323
+ error_dict = errors.error_dict
324
+ else:
325
+ error_dict = {NON_FIELD_ERRORS: errors}
326
+
327
+ for field, messages in error_dict.items():
328
+ if (
329
+ field == NON_FIELD_ERRORS
330
+ and opts.error_messages
331
+ and NON_FIELD_ERRORS in opts.error_messages
332
+ ):
333
+ error_messages = opts.error_messages[NON_FIELD_ERRORS]
334
+ elif field in self.fields:
335
+ error_messages = self.fields[field].error_messages
336
+ else:
337
+ continue
338
+
339
+ for message in messages:
340
+ if (
341
+ isinstance(message, ValidationError)
342
+ and message.code in error_messages
343
+ ):
344
+ message.message = error_messages[message.code]
345
+
346
+ self.add_error(None, errors)
347
+
348
+ def _post_clean(self) -> None:
349
+ opts = self._meta
350
+
351
+ exclude = self._get_validation_exclusions()
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) -> None:
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) -> None:
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
+ meta = self.instance._model_meta
385
+
386
+ for f in meta.many_to_many:
387
+ if not hasattr(f, "save_form_data"):
388
+ continue
389
+ if fields and f.name not in fields:
390
+ continue
391
+ if f.name in cleaned_data:
392
+ f.save_form_data(self.instance, cleaned_data[f.name])
393
+
394
+ def save(self, commit: bool = True) -> Any:
395
+ """
396
+ Save this form's self.instance object if commit=True. Otherwise, add
397
+ a save_m2m() method to the form which can be called after the instance
398
+ is saved manually at a later time. Return the model instance.
399
+ """
400
+ if self.errors:
401
+ raise ValueError(
402
+ "The {} could not be {} because the data didn't validate.".format(
403
+ self.instance.model_options.object_name,
404
+ "created" if self.instance._state.adding else "changed",
405
+ )
406
+ )
407
+ if commit:
408
+ # If committing, save the instance and the m2m data immediately.
409
+ self.instance.save(clean_and_validate=False)
410
+ self._save_m2m()
411
+ else:
412
+ # If not committing, add a method to the form to allow deferred
413
+ # saving of m2m data.
414
+ self.save_m2m = self._save_m2m
415
+ return self.instance
416
+
417
+
418
+ class ModelForm(BaseModelForm, metaclass=ModelFormMetaclass):
419
+ pass
420
+
421
+
422
+ # Fields #####################################################################
423
+
424
+
425
+ class ModelChoiceIteratorValue:
426
+ def __init__(self, value: Any, instance: Any) -> None:
427
+ self.value = value
428
+ self.instance = instance
429
+
430
+ def __str__(self) -> str:
431
+ return str(self.value)
432
+
433
+ def __hash__(self) -> int:
434
+ return hash(self.value)
435
+
436
+ def __eq__(self, other: object) -> bool:
437
+ if isinstance(other, ModelChoiceIteratorValue):
438
+ other = other.value
439
+ return self.value == other
440
+
441
+
442
+ class ModelChoiceIterator:
443
+ def __init__(self, field: ModelChoiceField) -> None:
444
+ self.field = field
445
+ self.queryset = field.queryset
446
+
447
+ def __iter__(self) -> Any:
448
+ if self.field.empty_label is not None:
449
+ yield ("", self.field.empty_label)
450
+ queryset = self.queryset
451
+ # Can't use iterator() when queryset uses prefetch_related()
452
+ if not queryset._prefetch_related_lookups:
453
+ queryset = queryset.iterator()
454
+ for obj in queryset:
455
+ yield self.choice(obj)
456
+
457
+ def __len__(self) -> int:
458
+ # count() adds a query but uses less memory since the QuerySet results
459
+ # won't be cached. In most cases, the choices will only be iterated on,
460
+ # and __len__() won't be called.
461
+ return self.queryset.count() + (1 if self.field.empty_label is not None else 0)
462
+
463
+ def __bool__(self) -> bool:
464
+ return self.field.empty_label is not None or self.queryset.exists()
465
+
466
+ def choice(self, obj: Any) -> tuple[ModelChoiceIteratorValue, str]:
467
+ return (
468
+ ModelChoiceIteratorValue(self.field.prepare_value(obj), obj),
469
+ str(obj),
470
+ )
471
+
472
+
473
+ class ModelChoiceField(ChoiceField):
474
+ """A ChoiceField whose choices are a model QuerySet."""
475
+
476
+ # This class is a subclass of ChoiceField for purity, but it doesn't
477
+ # actually use any of ChoiceField's implementation.
478
+ default_error_messages = {
479
+ "invalid_choice": "Select a valid choice. That choice is not one of the available choices.",
480
+ }
481
+ iterator = ModelChoiceIterator
482
+
483
+ def __init__(
484
+ self,
485
+ queryset: Any,
486
+ *,
487
+ empty_label: str | None = "---------",
488
+ required: bool = True,
489
+ initial: Any = None,
490
+ **kwargs: Any,
491
+ ) -> None:
492
+ # Call Field instead of ChoiceField __init__() because we don't need
493
+ # ChoiceField.__init__().
494
+ Field.__init__(
495
+ self,
496
+ required=required,
497
+ initial=initial,
498
+ **kwargs,
499
+ )
500
+ if required and initial is not None:
501
+ self.empty_label = None
502
+ else:
503
+ self.empty_label = empty_label
504
+ self.queryset = queryset
505
+
506
+ def __deepcopy__(self, memo: dict[int, Any]) -> ModelChoiceField:
507
+ result = super(ChoiceField, self).__deepcopy__(memo)
508
+ # Need to force a new ModelChoiceIterator to be created, bug #11183
509
+ if self.queryset is not None:
510
+ result.queryset = self.queryset.all()
511
+ return result
512
+
513
+ def _get_queryset(self) -> Any:
514
+ return self._queryset
515
+
516
+ def _set_queryset(self, queryset: Any) -> None:
517
+ self._queryset = None if queryset is None else queryset.all()
518
+
519
+ queryset = property(_get_queryset, _set_queryset)
520
+
521
+ def _get_choices(self) -> ModelChoiceIterator:
522
+ # If self._choices is set, then somebody must have manually set
523
+ # the property self.choices. In this case, just return self._choices.
524
+ if hasattr(self, "_choices"):
525
+ # After checking hasattr, we know _choices exists and is ModelChoiceIterator
526
+ return cast(ModelChoiceIterator, self._choices)
527
+
528
+ # Otherwise, execute the QuerySet in self.queryset to determine the
529
+ # choices dynamically. Return a fresh ModelChoiceIterator that has not been
530
+ # consumed. Note that we're instantiating a new ModelChoiceIterator *each*
531
+ # time _get_choices() is called (and, thus, each time self.choices is
532
+ # accessed) so that we can ensure the QuerySet has not been consumed. This
533
+ # construct might look complicated but it allows for lazy evaluation of
534
+ # the queryset.
535
+ return self.iterator(self)
536
+
537
+ choices = property(_get_choices, ChoiceField._set_choices)
538
+
539
+ def prepare_value(self, value: Any) -> Any:
540
+ if hasattr(value, "_model_meta"):
541
+ return value.id
542
+ return super().prepare_value(value)
543
+
544
+ def to_python(self, value: Any) -> Any:
545
+ if value in self.empty_values:
546
+ return None
547
+ try:
548
+ key = "id"
549
+ if isinstance(value, self.queryset.model):
550
+ value = getattr(value, key)
551
+ value = self.queryset.get(**{key: value})
552
+ except (ValueError, TypeError, self.queryset.model.DoesNotExist):
553
+ raise ValidationError(
554
+ self.error_messages["invalid_choice"],
555
+ code="invalid_choice",
556
+ params={"value": value},
557
+ )
558
+ return value
559
+
560
+ def validate(self, value: Any) -> None:
561
+ return Field.validate(self, value)
562
+
563
+ def has_changed(self, initial: Any, data: Any) -> bool:
564
+ initial_value = initial if initial is not None else ""
565
+ data_value = data if data is not None else ""
566
+ return str(self.prepare_value(initial_value)) != str(data_value)
567
+
568
+
569
+ class ModelMultipleChoiceField(ModelChoiceField):
570
+ """A MultipleChoiceField whose choices are a model QuerySet."""
571
+
572
+ default_error_messages = {
573
+ "invalid_list": "Enter a list of values.",
574
+ "invalid_choice": "Select a valid choice. %(value)s is not one of the available choices.",
575
+ "invalid_id_value": "'%(id)s' is not a valid value.",
576
+ }
577
+
578
+ def __init__(self, queryset: Any, **kwargs: Any) -> None:
579
+ super().__init__(queryset, empty_label=None, **kwargs)
580
+
581
+ def to_python(self, value: Any) -> list[Any]: # type: ignore[override]
582
+ if not value:
583
+ return []
584
+ return list(self._check_values(value))
585
+
586
+ def clean(self, value: Any) -> Any:
587
+ value = self.prepare_value(value)
588
+ if self.required and not value:
589
+ raise ValidationError(self.error_messages["required"], code="required")
590
+ elif not self.required and not value:
591
+ return self.queryset.none()
592
+ if not isinstance(value, list | tuple):
593
+ raise ValidationError(
594
+ self.error_messages["invalid_list"],
595
+ code="invalid_list",
596
+ )
597
+ qs = self._check_values(value)
598
+ # Since this overrides the inherited ModelChoiceField.clean
599
+ # we run custom validators here
600
+ self.run_validators(value)
601
+ return qs
602
+
603
+ def _check_values(self, value: Any) -> Any:
604
+ """
605
+ Given a list of possible PK values, return a QuerySet of the
606
+ corresponding objects. Raise a ValidationError if a given value is
607
+ invalid (not a valid PK, not in the queryset, etc.)
608
+ """
609
+ # deduplicate given values to avoid creating many querysets or
610
+ # requiring the database backend deduplicate efficiently.
611
+ try:
612
+ value = frozenset(value)
613
+ except TypeError:
614
+ # list of lists isn't hashable, for example
615
+ raise ValidationError(
616
+ self.error_messages["invalid_list"],
617
+ code="invalid_list",
618
+ )
619
+ for id_val in value:
620
+ try:
621
+ self.queryset.filter(id=id_val)
622
+ except (ValueError, TypeError):
623
+ raise ValidationError(
624
+ self.error_messages["invalid_id_value"],
625
+ code="invalid_id_value",
626
+ params={"id": id_val},
627
+ )
628
+ qs = self.queryset.filter(id__in=value)
629
+ ids = {str(o.id) for o in qs}
630
+ for val in value:
631
+ if str(val) not in ids:
632
+ raise ValidationError(
633
+ self.error_messages["invalid_choice"],
634
+ code="invalid_choice",
635
+ params={"value": val},
636
+ )
637
+ return qs
638
+
639
+ def prepare_value(self, value: Any) -> Any:
640
+ if (
641
+ hasattr(value, "__iter__")
642
+ and not isinstance(value, str)
643
+ and not hasattr(value, "_model_meta")
644
+ ):
645
+ prepare_value = super().prepare_value
646
+ return [prepare_value(v) for v in value]
647
+ return super().prepare_value(value)
648
+
649
+ def has_changed(self, initial: Any, data: Any) -> bool:
650
+ if initial is None:
651
+ initial = []
652
+ if data is None:
653
+ data = []
654
+ if len(initial) != len(data):
655
+ return True
656
+ initial_set = {str(value) for value in self.prepare_value(initial)}
657
+ data_set = {str(value) for value in data}
658
+ return data_set != initial_set
659
+
660
+ def value_from_form_data(self, data: Any, files: Any, html_name: str) -> Any:
661
+ return data.getlist(html_name)
662
+
663
+
664
+ def modelfield_to_formfield(
665
+ modelfield: ModelField,
666
+ form_class: type[Field] | None = None,
667
+ choices_form_class: type[Field] | None = None,
668
+ **kwargs: Any,
669
+ ) -> Field | None:
670
+ defaults: dict[str, Any] = {
671
+ "required": modelfield.required,
672
+ }
673
+
674
+ if modelfield.has_default():
675
+ defaults["initial"] = modelfield.get_default()
676
+
677
+ if modelfield.choices is not None:
678
+ # Fields with choices get special treatment.
679
+ include_blank = not modelfield.required or not (
680
+ modelfield.has_default() or "initial" in kwargs
681
+ )
682
+ defaults["choices"] = modelfield.get_choices(include_blank=include_blank)
683
+ defaults["coerce"] = modelfield.to_python
684
+ if modelfield.allow_null:
685
+ defaults["empty_value"] = None
686
+ if choices_form_class is not None:
687
+ form_class = choices_form_class
688
+ else:
689
+ form_class = fields.TypedChoiceField
690
+ # Many of the subclass-specific formfield arguments (min_value,
691
+ # max_value) don't apply for choice fields, so be sure to only pass
692
+ # the values that TypedChoiceField will understand.
693
+ for k in list(kwargs):
694
+ if k not in (
695
+ "coerce",
696
+ "empty_value",
697
+ "choices",
698
+ "required",
699
+ "initial",
700
+ "error_messages",
701
+ ):
702
+ del kwargs[k]
703
+
704
+ defaults.update(kwargs)
705
+
706
+ if form_class is not None:
707
+ return form_class(**defaults)
708
+
709
+ # Avoid a circular import
710
+ from plain import postgres
711
+
712
+ # Primary key fields aren't rendered by default
713
+ if isinstance(modelfield, postgres.PrimaryKeyField):
714
+ return None
715
+
716
+ if isinstance(modelfield, postgres.BooleanField):
717
+ form_class = (
718
+ fields.NullBooleanField if modelfield.allow_null else fields.BooleanField
719
+ )
720
+ # In HTML checkboxes, 'required' means "must be checked" which is
721
+ # different from the choices case ("must select some value").
722
+ # required=False allows unchecked checkboxes.
723
+ defaults["required"] = False
724
+ return form_class(**defaults)
725
+
726
+ if isinstance(modelfield, postgres.DecimalField):
727
+ return fields.DecimalField(
728
+ max_digits=modelfield.max_digits,
729
+ decimal_places=modelfield.decimal_places,
730
+ **defaults,
731
+ )
732
+
733
+ if issubclass(modelfield.__class__, postgres.fields.PositiveIntegerRelDbTypeMixin):
734
+ return fields.IntegerField(min_value=0, **defaults)
735
+
736
+ if isinstance(modelfield, postgres.TextField):
737
+ # Passing max_length to fields.CharField means that the value's length
738
+ # will be validated twice. This is considered acceptable since we want
739
+ # the value in the form field (to pass into widget for example).
740
+ return fields.CharField(max_length=modelfield.max_length, **defaults)
741
+
742
+ if isinstance(modelfield, postgres.CharField):
743
+ # Passing max_length to forms.CharField means that the value's length
744
+ # will be validated twice. This is considered acceptable since we want
745
+ # the value in the form field (to pass into widget for example).
746
+ if modelfield.allow_null:
747
+ defaults["empty_value"] = None
748
+ return fields.CharField(
749
+ max_length=modelfield.max_length,
750
+ **defaults,
751
+ )
752
+
753
+ if isinstance(modelfield, postgres.JSONField):
754
+ return fields.JSONField(
755
+ encoder=modelfield.encoder, decoder=modelfield.decoder, **defaults
756
+ )
757
+
758
+ if isinstance(modelfield, postgres.ForeignKeyField):
759
+ return ModelChoiceField(
760
+ queryset=modelfield.remote_field.model.query,
761
+ **defaults,
762
+ )
763
+
764
+ # TODO related (OneToOne, m2m)
765
+
766
+ # If there's a form field of the exact same name, use it
767
+ # (models.URLField -> forms.URLField)
768
+ if hasattr(fields, modelfield.__class__.__name__):
769
+ form_class = getattr(fields, modelfield.__class__.__name__)
770
+ return form_class(**defaults)
771
+
772
+ # Default to CharField if we didn't find anything else
773
+ return fields.CharField(**defaults)