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.
- plain/postgres/CHANGELOG.md +1028 -0
- plain/postgres/README.md +925 -0
- plain/postgres/__init__.py +120 -0
- plain/postgres/agents/.claude/rules/plain-postgres.md +78 -0
- plain/postgres/aggregates.py +236 -0
- plain/postgres/backups/__init__.py +0 -0
- plain/postgres/backups/cli.py +148 -0
- plain/postgres/backups/clients.py +94 -0
- plain/postgres/backups/core.py +172 -0
- plain/postgres/base.py +1415 -0
- plain/postgres/cli/__init__.py +3 -0
- plain/postgres/cli/db.py +142 -0
- plain/postgres/cli/migrations.py +1085 -0
- plain/postgres/config.py +18 -0
- plain/postgres/connection.py +1331 -0
- plain/postgres/connections.py +77 -0
- plain/postgres/constants.py +13 -0
- plain/postgres/constraints.py +495 -0
- plain/postgres/database_url.py +94 -0
- plain/postgres/db.py +59 -0
- plain/postgres/default_settings.py +38 -0
- plain/postgres/deletion.py +475 -0
- plain/postgres/dialect.py +640 -0
- plain/postgres/entrypoints.py +4 -0
- plain/postgres/enums.py +103 -0
- plain/postgres/exceptions.py +217 -0
- plain/postgres/expressions.py +1912 -0
- plain/postgres/fields/__init__.py +2118 -0
- plain/postgres/fields/encrypted.py +354 -0
- plain/postgres/fields/json.py +413 -0
- plain/postgres/fields/mixins.py +30 -0
- plain/postgres/fields/related.py +1192 -0
- plain/postgres/fields/related_descriptors.py +290 -0
- plain/postgres/fields/related_lookups.py +223 -0
- plain/postgres/fields/related_managers.py +661 -0
- plain/postgres/fields/reverse_descriptors.py +229 -0
- plain/postgres/fields/reverse_related.py +328 -0
- plain/postgres/fields/timezones.py +143 -0
- plain/postgres/forms.py +773 -0
- plain/postgres/functions/__init__.py +189 -0
- plain/postgres/functions/comparison.py +127 -0
- plain/postgres/functions/datetime.py +454 -0
- plain/postgres/functions/math.py +140 -0
- plain/postgres/functions/mixins.py +59 -0
- plain/postgres/functions/text.py +282 -0
- plain/postgres/functions/window.py +125 -0
- plain/postgres/indexes.py +286 -0
- plain/postgres/lookups.py +758 -0
- plain/postgres/meta.py +584 -0
- plain/postgres/migrations/__init__.py +53 -0
- plain/postgres/migrations/autodetector.py +1379 -0
- plain/postgres/migrations/exceptions.py +54 -0
- plain/postgres/migrations/executor.py +188 -0
- plain/postgres/migrations/graph.py +364 -0
- plain/postgres/migrations/loader.py +377 -0
- plain/postgres/migrations/migration.py +180 -0
- plain/postgres/migrations/operations/__init__.py +34 -0
- plain/postgres/migrations/operations/base.py +139 -0
- plain/postgres/migrations/operations/fields.py +373 -0
- plain/postgres/migrations/operations/models.py +798 -0
- plain/postgres/migrations/operations/special.py +184 -0
- plain/postgres/migrations/optimizer.py +74 -0
- plain/postgres/migrations/questioner.py +340 -0
- plain/postgres/migrations/recorder.py +119 -0
- plain/postgres/migrations/serializer.py +378 -0
- plain/postgres/migrations/state.py +882 -0
- plain/postgres/migrations/utils.py +147 -0
- plain/postgres/migrations/writer.py +302 -0
- plain/postgres/options.py +207 -0
- plain/postgres/otel.py +231 -0
- plain/postgres/preflight.py +336 -0
- plain/postgres/query.py +2242 -0
- plain/postgres/query_utils.py +456 -0
- plain/postgres/registry.py +217 -0
- plain/postgres/schema.py +1885 -0
- plain/postgres/sql/__init__.py +40 -0
- plain/postgres/sql/compiler.py +1869 -0
- plain/postgres/sql/constants.py +22 -0
- plain/postgres/sql/datastructures.py +222 -0
- plain/postgres/sql/query.py +2947 -0
- plain/postgres/sql/where.py +374 -0
- plain/postgres/test/__init__.py +0 -0
- plain/postgres/test/pytest.py +117 -0
- plain/postgres/test/utils.py +18 -0
- plain/postgres/transaction.py +222 -0
- plain/postgres/types.py +92 -0
- plain/postgres/types.pyi +751 -0
- plain/postgres/utils.py +345 -0
- plain_postgres-0.84.0.dist-info/METADATA +937 -0
- plain_postgres-0.84.0.dist-info/RECORD +93 -0
- plain_postgres-0.84.0.dist-info/WHEEL +4 -0
- plain_postgres-0.84.0.dist-info/entry_points.txt +5 -0
- plain_postgres-0.84.0.dist-info/licenses/LICENSE +61 -0
plain/postgres/forms.py
ADDED
|
@@ -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)
|