plain.models 0.49.2__py3-none-any.whl → 0.51.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/models/CHANGELOG.md +27 -0
- plain/models/README.md +26 -42
- plain/models/__init__.py +2 -0
- plain/models/aggregates.py +42 -19
- plain/models/backends/base/base.py +125 -105
- plain/models/backends/base/client.py +11 -3
- plain/models/backends/base/creation.py +24 -14
- plain/models/backends/base/features.py +10 -4
- plain/models/backends/base/introspection.py +37 -20
- plain/models/backends/base/operations.py +187 -91
- plain/models/backends/base/schema.py +338 -218
- plain/models/backends/base/validation.py +13 -4
- plain/models/backends/ddl_references.py +85 -43
- plain/models/backends/mysql/base.py +29 -26
- plain/models/backends/mysql/client.py +7 -2
- plain/models/backends/mysql/compiler.py +13 -4
- plain/models/backends/mysql/creation.py +5 -2
- plain/models/backends/mysql/features.py +24 -22
- plain/models/backends/mysql/introspection.py +22 -13
- plain/models/backends/mysql/operations.py +107 -40
- plain/models/backends/mysql/schema.py +52 -28
- plain/models/backends/mysql/validation.py +13 -6
- plain/models/backends/postgresql/base.py +41 -34
- plain/models/backends/postgresql/client.py +7 -2
- plain/models/backends/postgresql/creation.py +10 -5
- plain/models/backends/postgresql/introspection.py +15 -8
- plain/models/backends/postgresql/operations.py +110 -43
- plain/models/backends/postgresql/schema.py +88 -49
- plain/models/backends/sqlite3/_functions.py +151 -115
- plain/models/backends/sqlite3/base.py +37 -23
- plain/models/backends/sqlite3/client.py +7 -1
- plain/models/backends/sqlite3/creation.py +9 -5
- plain/models/backends/sqlite3/features.py +5 -3
- plain/models/backends/sqlite3/introspection.py +32 -16
- plain/models/backends/sqlite3/operations.py +126 -43
- plain/models/backends/sqlite3/schema.py +127 -92
- plain/models/backends/utils.py +52 -29
- plain/models/backups/cli.py +8 -6
- plain/models/backups/clients.py +16 -7
- plain/models/backups/core.py +24 -13
- plain/models/base.py +221 -229
- plain/models/cli.py +98 -67
- plain/models/config.py +1 -1
- plain/models/connections.py +23 -7
- plain/models/constraints.py +79 -56
- plain/models/database_url.py +1 -1
- plain/models/db.py +6 -2
- plain/models/deletion.py +80 -56
- plain/models/entrypoints.py +1 -1
- plain/models/enums.py +22 -11
- plain/models/exceptions.py +23 -8
- plain/models/expressions.py +441 -258
- plain/models/fields/__init__.py +272 -217
- plain/models/fields/json.py +123 -57
- plain/models/fields/mixins.py +12 -8
- plain/models/fields/related.py +324 -290
- plain/models/fields/related_descriptors.py +33 -24
- plain/models/fields/related_lookups.py +24 -12
- plain/models/fields/related_managers.py +102 -79
- plain/models/fields/reverse_related.py +66 -63
- plain/models/forms.py +101 -75
- plain/models/functions/comparison.py +71 -18
- plain/models/functions/datetime.py +79 -29
- plain/models/functions/math.py +43 -10
- plain/models/functions/mixins.py +24 -7
- plain/models/functions/text.py +104 -25
- plain/models/functions/window.py +12 -6
- plain/models/indexes.py +57 -32
- plain/models/lookups.py +228 -153
- plain/models/meta.py +505 -0
- plain/models/migrations/autodetector.py +86 -43
- plain/models/migrations/exceptions.py +7 -3
- plain/models/migrations/executor.py +33 -7
- plain/models/migrations/graph.py +79 -50
- plain/models/migrations/loader.py +45 -22
- plain/models/migrations/migration.py +23 -18
- plain/models/migrations/operations/base.py +38 -20
- plain/models/migrations/operations/fields.py +95 -48
- plain/models/migrations/operations/models.py +246 -142
- plain/models/migrations/operations/special.py +82 -25
- plain/models/migrations/optimizer.py +7 -2
- plain/models/migrations/questioner.py +58 -31
- plain/models/migrations/recorder.py +27 -16
- plain/models/migrations/serializer.py +50 -39
- plain/models/migrations/state.py +232 -156
- plain/models/migrations/utils.py +30 -14
- plain/models/migrations/writer.py +17 -14
- plain/models/options.py +189 -518
- plain/models/otel.py +16 -6
- plain/models/preflight.py +42 -17
- plain/models/query.py +400 -251
- plain/models/query_utils.py +109 -69
- plain/models/registry.py +40 -21
- plain/models/sql/compiler.py +190 -127
- plain/models/sql/datastructures.py +38 -25
- plain/models/sql/query.py +320 -225
- plain/models/sql/subqueries.py +36 -25
- plain/models/sql/where.py +54 -29
- plain/models/test/pytest.py +15 -11
- plain/models/test/utils.py +4 -2
- plain/models/transaction.py +20 -7
- plain/models/utils.py +17 -6
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/METADATA +27 -43
- plain_models-0.51.0.dist-info/RECORD +123 -0
- plain_models-0.49.2.dist-info/RECORD +0 -122
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/WHEEL +0 -0
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/licenses/LICENSE +0 -0
plain/models/forms.py
CHANGED
@@ -3,7 +3,10 @@ Helper functions for creating Form classes from Plain models
|
|
3
3
|
and database field objects.
|
4
4
|
"""
|
5
5
|
|
6
|
+
from __future__ import annotations
|
7
|
+
|
6
8
|
from itertools import chain
|
9
|
+
from typing import TYPE_CHECKING, Any
|
7
10
|
|
8
11
|
from plain.exceptions import (
|
9
12
|
NON_FIELD_ERRORS,
|
@@ -15,6 +18,9 @@ from plain.forms.fields import ChoiceField, Field
|
|
15
18
|
from plain.forms.forms import BaseForm, DeclarativeFieldsMetaclass
|
16
19
|
from plain.models.exceptions import FieldError
|
17
20
|
|
21
|
+
if TYPE_CHECKING:
|
22
|
+
from plain.models.fields import Field as ModelField
|
23
|
+
|
18
24
|
__all__ = (
|
19
25
|
"ModelForm",
|
20
26
|
"BaseModelForm",
|
@@ -25,18 +31,22 @@ __all__ = (
|
|
25
31
|
)
|
26
32
|
|
27
33
|
|
28
|
-
def construct_instance(
|
34
|
+
def construct_instance(
|
35
|
+
form: BaseModelForm,
|
36
|
+
instance: Any,
|
37
|
+
fields: list[str] | tuple[str, ...] | None = None,
|
38
|
+
) -> Any:
|
29
39
|
"""
|
30
40
|
Construct and return a model instance from the bound ``form``'s
|
31
41
|
``cleaned_data``, but do not save the returned instance to the database.
|
32
42
|
"""
|
33
43
|
from plain import models
|
34
44
|
|
35
|
-
|
45
|
+
meta = instance._model_meta
|
36
46
|
|
37
47
|
cleaned_data = form.cleaned_data
|
38
48
|
file_field_list = []
|
39
|
-
for f in
|
49
|
+
for f in meta.fields:
|
40
50
|
if isinstance(f, models.PrimaryKeyField) or f.name not in cleaned_data:
|
41
51
|
continue
|
42
52
|
if fields is not None and f.name not in fields:
|
@@ -65,7 +75,9 @@ def construct_instance(form, instance, fields=None):
|
|
65
75
|
# ModelForms #################################################################
|
66
76
|
|
67
77
|
|
68
|
-
def model_to_dict(
|
78
|
+
def model_to_dict(
|
79
|
+
instance: Any, fields: list[str] | tuple[str, ...] | None = None
|
80
|
+
) -> dict[str, Any]:
|
69
81
|
"""
|
70
82
|
Return a dict containing the data in ``instance`` suitable for passing as
|
71
83
|
a Form's ``initial`` keyword argument.
|
@@ -73,9 +85,9 @@ def model_to_dict(instance, fields=None):
|
|
73
85
|
``fields`` is an optional list of field names. If provided, return only the
|
74
86
|
named.
|
75
87
|
"""
|
76
|
-
|
88
|
+
meta = instance._model_meta
|
77
89
|
data = {}
|
78
|
-
for f in chain(
|
90
|
+
for f in chain(meta.concrete_fields, meta.many_to_many):
|
79
91
|
if fields is not None and f.name not in fields:
|
80
92
|
continue
|
81
93
|
data[f.name] = f.value_from_object(instance)
|
@@ -83,12 +95,12 @@ def model_to_dict(instance, fields=None):
|
|
83
95
|
|
84
96
|
|
85
97
|
def fields_for_model(
|
86
|
-
model,
|
87
|
-
fields=None,
|
88
|
-
formfield_callback=None,
|
89
|
-
error_messages=None,
|
90
|
-
field_classes=None,
|
91
|
-
):
|
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]:
|
92
104
|
"""
|
93
105
|
Return a dictionary containing form fields for the given model.
|
94
106
|
|
@@ -106,9 +118,9 @@ def fields_for_model(
|
|
106
118
|
"""
|
107
119
|
field_dict = {}
|
108
120
|
ignored = []
|
109
|
-
|
121
|
+
meta = model._model_meta
|
110
122
|
|
111
|
-
for f in sorted(chain(
|
123
|
+
for f in sorted(chain(meta.concrete_fields, meta.many_to_many)):
|
112
124
|
if fields is not None and f.name not in fields:
|
113
125
|
continue
|
114
126
|
|
@@ -135,17 +147,28 @@ def fields_for_model(
|
|
135
147
|
|
136
148
|
|
137
149
|
class ModelFormOptions:
|
138
|
-
def __init__(self, options=None):
|
139
|
-
self.model = getattr(options, "model", None)
|
140
|
-
self.fields
|
141
|
-
|
142
|
-
|
143
|
-
self.
|
150
|
+
def __init__(self, options: Any = None) -> None:
|
151
|
+
self.model: type[Any] | None = getattr(options, "model", None)
|
152
|
+
self.fields: list[str] | tuple[str, ...] | None = getattr(
|
153
|
+
options, "fields", None
|
154
|
+
)
|
155
|
+
self.error_messages: dict[str, Any] | None = getattr(
|
156
|
+
options, "error_messages", None
|
157
|
+
)
|
158
|
+
self.field_classes: dict[str, type[Field]] | None = getattr(
|
159
|
+
options, "field_classes", None
|
160
|
+
)
|
161
|
+
self.formfield_callback: Any = getattr(options, "formfield_callback", None)
|
144
162
|
|
145
163
|
|
146
164
|
class ModelFormMetaclass(DeclarativeFieldsMetaclass):
|
147
|
-
def __new__(
|
148
|
-
|
165
|
+
def __new__(
|
166
|
+
mcs: type[ModelFormMetaclass],
|
167
|
+
name: str,
|
168
|
+
bases: tuple[type, ...],
|
169
|
+
attrs: dict[str, Any],
|
170
|
+
) -> type[BaseModelForm]:
|
171
|
+
new_class = super().__new__(mcs, name, bases, attrs) # type: ignore[invalid-super-argument]
|
149
172
|
|
150
173
|
if bases == (BaseModelForm,):
|
151
174
|
return new_class
|
@@ -203,12 +226,12 @@ class BaseModelForm(BaseForm):
|
|
203
226
|
def __init__(
|
204
227
|
self,
|
205
228
|
*,
|
206
|
-
request,
|
207
|
-
auto_id="id_%s",
|
208
|
-
prefix=None,
|
209
|
-
initial=None,
|
210
|
-
instance=None,
|
211
|
-
):
|
229
|
+
request: Any,
|
230
|
+
auto_id: str = "id_%s",
|
231
|
+
prefix: str | None = None,
|
232
|
+
initial: dict[str, Any] | None = None,
|
233
|
+
instance: Any = None,
|
234
|
+
) -> None:
|
212
235
|
opts = self._meta
|
213
236
|
if opts.model is None:
|
214
237
|
raise ValueError("ModelForm has no model class specified.")
|
@@ -233,7 +256,7 @@ class BaseModelForm(BaseForm):
|
|
233
256
|
initial=object_data,
|
234
257
|
)
|
235
258
|
|
236
|
-
def _get_validation_exclusions(self):
|
259
|
+
def _get_validation_exclusions(self) -> set[str]:
|
237
260
|
"""
|
238
261
|
For backwards-compatibility, exclude several types of fields from model
|
239
262
|
validation. See tickets #12507, #12521, #12553.
|
@@ -241,7 +264,7 @@ class BaseModelForm(BaseForm):
|
|
241
264
|
exclude = set()
|
242
265
|
# Build up a list of fields that should be excluded from model field
|
243
266
|
# validation and unique checks.
|
244
|
-
for f in self.instance.
|
267
|
+
for f in self.instance._model_meta.fields:
|
245
268
|
field = f.name
|
246
269
|
# Exclude fields that aren't on the form. The developer may be
|
247
270
|
# adding these values to the model after form validation.
|
@@ -276,11 +299,11 @@ class BaseModelForm(BaseForm):
|
|
276
299
|
exclude.add(f.name)
|
277
300
|
return exclude
|
278
301
|
|
279
|
-
def clean(self):
|
302
|
+
def clean(self) -> dict[str, Any]:
|
280
303
|
self._validate_unique = True
|
281
304
|
return self.cleaned_data
|
282
305
|
|
283
|
-
def _update_errors(self, errors):
|
306
|
+
def _update_errors(self, errors: ValidationError) -> None:
|
284
307
|
# Override any validation error messages defined at the model level
|
285
308
|
# with those defined at the form level.
|
286
309
|
opts = self._meta
|
@@ -313,7 +336,7 @@ class BaseModelForm(BaseForm):
|
|
313
336
|
|
314
337
|
self.add_error(None, errors)
|
315
338
|
|
316
|
-
def _post_clean(self):
|
339
|
+
def _post_clean(self) -> None:
|
317
340
|
opts = self._meta
|
318
341
|
|
319
342
|
exclude = self._get_validation_exclusions()
|
@@ -332,7 +355,7 @@ class BaseModelForm(BaseForm):
|
|
332
355
|
if self._validate_unique:
|
333
356
|
self.validate_unique()
|
334
357
|
|
335
|
-
def validate_unique(self):
|
358
|
+
def validate_unique(self) -> None:
|
336
359
|
"""
|
337
360
|
Call the instance's validate_unique() method and update the form's
|
338
361
|
validation errors if any were raised.
|
@@ -343,15 +366,15 @@ class BaseModelForm(BaseForm):
|
|
343
366
|
except ValidationError as e:
|
344
367
|
self._update_errors(e)
|
345
368
|
|
346
|
-
def _save_m2m(self):
|
369
|
+
def _save_m2m(self) -> None:
|
347
370
|
"""
|
348
371
|
Save the many-to-many fields and generic relations for this form.
|
349
372
|
"""
|
350
373
|
cleaned_data = self.cleaned_data
|
351
374
|
fields = self._meta.fields
|
352
|
-
|
375
|
+
meta = self.instance._model_meta
|
353
376
|
|
354
|
-
for f in
|
377
|
+
for f in meta.many_to_many:
|
355
378
|
if not hasattr(f, "save_form_data"):
|
356
379
|
continue
|
357
380
|
if fields and f.name not in fields:
|
@@ -359,7 +382,7 @@ class BaseModelForm(BaseForm):
|
|
359
382
|
if f.name in cleaned_data:
|
360
383
|
f.save_form_data(self.instance, cleaned_data[f.name])
|
361
384
|
|
362
|
-
def save(self, commit=True):
|
385
|
+
def save(self, commit: bool = True) -> Any:
|
363
386
|
"""
|
364
387
|
Save this form's self.instance object if commit=True. Otherwise, add
|
365
388
|
a save_m2m() method to the form which can be called after the instance
|
@@ -368,7 +391,7 @@ class BaseModelForm(BaseForm):
|
|
368
391
|
if self.errors:
|
369
392
|
raise ValueError(
|
370
393
|
"The {} could not be {} because the data didn't validate.".format(
|
371
|
-
self.instance.
|
394
|
+
self.instance.model_options.object_name,
|
372
395
|
"created" if self.instance._state.adding else "changed",
|
373
396
|
)
|
374
397
|
)
|
@@ -391,28 +414,28 @@ class ModelForm(BaseModelForm, metaclass=ModelFormMetaclass):
|
|
391
414
|
|
392
415
|
|
393
416
|
class ModelChoiceIteratorValue:
|
394
|
-
def __init__(self, value, instance):
|
417
|
+
def __init__(self, value: Any, instance: Any) -> None:
|
395
418
|
self.value = value
|
396
419
|
self.instance = instance
|
397
420
|
|
398
|
-
def __str__(self):
|
421
|
+
def __str__(self) -> str:
|
399
422
|
return str(self.value)
|
400
423
|
|
401
|
-
def __hash__(self):
|
424
|
+
def __hash__(self) -> int:
|
402
425
|
return hash(self.value)
|
403
426
|
|
404
|
-
def __eq__(self, other):
|
427
|
+
def __eq__(self, other: object) -> bool:
|
405
428
|
if isinstance(other, ModelChoiceIteratorValue):
|
406
429
|
other = other.value
|
407
430
|
return self.value == other
|
408
431
|
|
409
432
|
|
410
433
|
class ModelChoiceIterator:
|
411
|
-
def __init__(self, field):
|
434
|
+
def __init__(self, field: ModelChoiceField) -> None:
|
412
435
|
self.field = field
|
413
436
|
self.queryset = field.queryset
|
414
437
|
|
415
|
-
def __iter__(self):
|
438
|
+
def __iter__(self) -> Any:
|
416
439
|
if self.field.empty_label is not None:
|
417
440
|
yield ("", self.field.empty_label)
|
418
441
|
queryset = self.queryset
|
@@ -422,16 +445,16 @@ class ModelChoiceIterator:
|
|
422
445
|
for obj in queryset:
|
423
446
|
yield self.choice(obj)
|
424
447
|
|
425
|
-
def __len__(self):
|
448
|
+
def __len__(self) -> int:
|
426
449
|
# count() adds a query but uses less memory since the QuerySet results
|
427
450
|
# won't be cached. In most cases, the choices will only be iterated on,
|
428
451
|
# and __len__() won't be called.
|
429
452
|
return self.queryset.count() + (1 if self.field.empty_label is not None else 0)
|
430
453
|
|
431
|
-
def __bool__(self):
|
454
|
+
def __bool__(self) -> bool:
|
432
455
|
return self.field.empty_label is not None or self.queryset.exists()
|
433
456
|
|
434
|
-
def choice(self, obj):
|
457
|
+
def choice(self, obj: Any) -> tuple[ModelChoiceIteratorValue, str]:
|
435
458
|
return (
|
436
459
|
ModelChoiceIteratorValue(self.field.prepare_value(obj), obj),
|
437
460
|
str(obj),
|
@@ -450,13 +473,13 @@ class ModelChoiceField(ChoiceField):
|
|
450
473
|
|
451
474
|
def __init__(
|
452
475
|
self,
|
453
|
-
queryset,
|
476
|
+
queryset: Any,
|
454
477
|
*,
|
455
|
-
empty_label="---------",
|
456
|
-
required=True,
|
457
|
-
initial=None,
|
458
|
-
**kwargs,
|
459
|
-
):
|
478
|
+
empty_label: str | None = "---------",
|
479
|
+
required: bool = True,
|
480
|
+
initial: Any = None,
|
481
|
+
**kwargs: Any,
|
482
|
+
) -> None:
|
460
483
|
# Call Field instead of ChoiceField __init__() because we don't need
|
461
484
|
# ChoiceField.__init__().
|
462
485
|
Field.__init__(
|
@@ -471,22 +494,22 @@ class ModelChoiceField(ChoiceField):
|
|
471
494
|
self.empty_label = empty_label
|
472
495
|
self.queryset = queryset
|
473
496
|
|
474
|
-
def __deepcopy__(self, memo):
|
497
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> ModelChoiceField:
|
475
498
|
result = super(ChoiceField, self).__deepcopy__(memo)
|
476
499
|
# Need to force a new ModelChoiceIterator to be created, bug #11183
|
477
500
|
if self.queryset is not None:
|
478
501
|
result.queryset = self.queryset.all()
|
479
502
|
return result
|
480
503
|
|
481
|
-
def _get_queryset(self):
|
504
|
+
def _get_queryset(self) -> Any:
|
482
505
|
return self._queryset
|
483
506
|
|
484
|
-
def _set_queryset(self, queryset):
|
507
|
+
def _set_queryset(self, queryset: Any) -> None:
|
485
508
|
self._queryset = None if queryset is None else queryset.all()
|
486
509
|
|
487
510
|
queryset = property(_get_queryset, _set_queryset)
|
488
511
|
|
489
|
-
def _get_choices(self):
|
512
|
+
def _get_choices(self) -> ModelChoiceIterator:
|
490
513
|
# If self._choices is set, then somebody must have manually set
|
491
514
|
# the property self.choices. In this case, just return self._choices.
|
492
515
|
if hasattr(self, "_choices"):
|
@@ -503,12 +526,12 @@ class ModelChoiceField(ChoiceField):
|
|
503
526
|
|
504
527
|
choices = property(_get_choices, ChoiceField._set_choices)
|
505
528
|
|
506
|
-
def prepare_value(self, value):
|
507
|
-
if hasattr(value, "
|
529
|
+
def prepare_value(self, value: Any) -> Any:
|
530
|
+
if hasattr(value, "_model_meta"):
|
508
531
|
return value.id
|
509
532
|
return super().prepare_value(value)
|
510
533
|
|
511
|
-
def to_python(self, value):
|
534
|
+
def to_python(self, value: Any) -> Any:
|
512
535
|
if value in self.empty_values:
|
513
536
|
return None
|
514
537
|
try:
|
@@ -524,10 +547,10 @@ class ModelChoiceField(ChoiceField):
|
|
524
547
|
)
|
525
548
|
return value
|
526
549
|
|
527
|
-
def validate(self, value):
|
550
|
+
def validate(self, value: Any) -> None:
|
528
551
|
return Field.validate(self, value)
|
529
552
|
|
530
|
-
def has_changed(self, initial, data):
|
553
|
+
def has_changed(self, initial: Any, data: Any) -> bool:
|
531
554
|
initial_value = initial if initial is not None else ""
|
532
555
|
data_value = data if data is not None else ""
|
533
556
|
return str(self.prepare_value(initial_value)) != str(data_value)
|
@@ -542,15 +565,15 @@ class ModelMultipleChoiceField(ModelChoiceField):
|
|
542
565
|
"invalid_id_value": "'%(id)s' is not a valid value.",
|
543
566
|
}
|
544
567
|
|
545
|
-
def __init__(self, queryset, **kwargs):
|
568
|
+
def __init__(self, queryset: Any, **kwargs: Any) -> None:
|
546
569
|
super().__init__(queryset, empty_label=None, **kwargs)
|
547
570
|
|
548
|
-
def to_python(self, value):
|
571
|
+
def to_python(self, value: Any) -> list[Any]:
|
549
572
|
if not value:
|
550
573
|
return []
|
551
574
|
return list(self._check_values(value))
|
552
575
|
|
553
|
-
def clean(self, value):
|
576
|
+
def clean(self, value: Any) -> Any:
|
554
577
|
value = self.prepare_value(value)
|
555
578
|
if self.required and not value:
|
556
579
|
raise ValidationError(self.error_messages["required"], code="required")
|
@@ -567,7 +590,7 @@ class ModelMultipleChoiceField(ModelChoiceField):
|
|
567
590
|
self.run_validators(value)
|
568
591
|
return qs
|
569
592
|
|
570
|
-
def _check_values(self, value):
|
593
|
+
def _check_values(self, value: Any) -> Any:
|
571
594
|
"""
|
572
595
|
Given a list of possible PK values, return a QuerySet of the
|
573
596
|
corresponding objects. Raise a ValidationError if a given value is
|
@@ -603,17 +626,17 @@ class ModelMultipleChoiceField(ModelChoiceField):
|
|
603
626
|
)
|
604
627
|
return qs
|
605
628
|
|
606
|
-
def prepare_value(self, value):
|
629
|
+
def prepare_value(self, value: Any) -> Any:
|
607
630
|
if (
|
608
631
|
hasattr(value, "__iter__")
|
609
632
|
and not isinstance(value, str)
|
610
|
-
and not hasattr(value, "
|
633
|
+
and not hasattr(value, "_model_meta")
|
611
634
|
):
|
612
635
|
prepare_value = super().prepare_value
|
613
636
|
return [prepare_value(v) for v in value]
|
614
637
|
return super().prepare_value(value)
|
615
638
|
|
616
|
-
def has_changed(self, initial, data):
|
639
|
+
def has_changed(self, initial: Any, data: Any) -> bool:
|
617
640
|
if initial is None:
|
618
641
|
initial = []
|
619
642
|
if data is None:
|
@@ -624,13 +647,16 @@ class ModelMultipleChoiceField(ModelChoiceField):
|
|
624
647
|
data_set = {str(value) for value in data}
|
625
648
|
return data_set != initial_set
|
626
649
|
|
627
|
-
def value_from_form_data(self, data, files, html_name):
|
650
|
+
def value_from_form_data(self, data: Any, files: Any, html_name: str) -> Any:
|
628
651
|
return data.getlist(html_name)
|
629
652
|
|
630
653
|
|
631
654
|
def modelfield_to_formfield(
|
632
|
-
modelfield
|
633
|
-
|
655
|
+
modelfield: ModelField,
|
656
|
+
form_class: type[Field] | None = None,
|
657
|
+
choices_form_class: type[Field] | None = None,
|
658
|
+
**kwargs: Any,
|
659
|
+
) -> Field | None:
|
634
660
|
defaults = {
|
635
661
|
"required": modelfield.required,
|
636
662
|
}
|
@@ -694,7 +720,7 @@ def modelfield_to_formfield(
|
|
694
720
|
**defaults,
|
695
721
|
)
|
696
722
|
|
697
|
-
if issubclass(modelfield.__class__, models.fields.PositiveIntegerRelDbTypeMixin):
|
723
|
+
if issubclass(modelfield.__class__, models.fields.PositiveIntegerRelDbTypeMixin): # type: ignore[attr-defined]
|
698
724
|
return fields.IntegerField(min_value=0, **defaults)
|
699
725
|
|
700
726
|
if isinstance(modelfield, models.TextField):
|
@@ -721,7 +747,7 @@ def modelfield_to_formfield(
|
|
721
747
|
|
722
748
|
if isinstance(modelfield, models.ForeignKey):
|
723
749
|
return ModelChoiceField(
|
724
|
-
queryset=modelfield.remote_field.model.query,
|
750
|
+
queryset=modelfield.remote_field.model.query, # type: ignore[attr-defined]
|
725
751
|
**defaults,
|
726
752
|
)
|
727
753
|
|
@@ -1,11 +1,19 @@
|
|
1
1
|
"""Database functions that do comparisons or type conversions."""
|
2
2
|
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import TYPE_CHECKING, Any
|
6
|
+
|
3
7
|
from plain.models.db import NotSupportedError
|
4
8
|
from plain.models.expressions import Func, Value
|
5
|
-
from plain.models.fields import TextField
|
9
|
+
from plain.models.fields import Field, TextField
|
6
10
|
from plain.models.fields.json import JSONField
|
7
11
|
from plain.utils.regex_helper import _lazy_re_compile
|
8
12
|
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from plain.models.backends.base.base import BaseDatabaseWrapper
|
15
|
+
from plain.models.sql.compiler import SQLCompiler
|
16
|
+
|
9
17
|
|
10
18
|
class Cast(Func):
|
11
19
|
"""Coerce an expression to a new field type."""
|
@@ -13,14 +21,24 @@ class Cast(Func):
|
|
13
21
|
function = "CAST"
|
14
22
|
template = "%(function)s(%(expressions)s AS %(db_type)s)"
|
15
23
|
|
16
|
-
def __init__(self, expression, output_field):
|
24
|
+
def __init__(self, expression: Any, output_field: Field) -> None:
|
17
25
|
super().__init__(expression, output_field=output_field)
|
18
26
|
|
19
|
-
def as_sql(
|
27
|
+
def as_sql(
|
28
|
+
self,
|
29
|
+
compiler: SQLCompiler,
|
30
|
+
connection: BaseDatabaseWrapper,
|
31
|
+
**extra_context: Any,
|
32
|
+
) -> tuple[str, tuple[Any, ...]]:
|
20
33
|
extra_context["db_type"] = self.output_field.cast_db_type(connection)
|
21
34
|
return super().as_sql(compiler, connection, **extra_context)
|
22
35
|
|
23
|
-
def as_sqlite(
|
36
|
+
def as_sqlite(
|
37
|
+
self,
|
38
|
+
compiler: SQLCompiler,
|
39
|
+
connection: BaseDatabaseWrapper,
|
40
|
+
**extra_context: Any,
|
41
|
+
) -> tuple[str, tuple[Any, ...]]:
|
24
42
|
db_type = self.output_field.db_type(connection)
|
25
43
|
if db_type in {"datetime", "time"}:
|
26
44
|
# Use strftime as datetime/time don't keep fractional seconds.
|
@@ -38,18 +56,28 @@ class Cast(Func):
|
|
38
56
|
)
|
39
57
|
return self.as_sql(compiler, connection, **extra_context)
|
40
58
|
|
41
|
-
def as_mysql(
|
59
|
+
def as_mysql(
|
60
|
+
self,
|
61
|
+
compiler: SQLCompiler,
|
62
|
+
connection: BaseDatabaseWrapper,
|
63
|
+
**extra_context: Any,
|
64
|
+
) -> tuple[str, tuple[Any, ...]]:
|
42
65
|
template = None
|
43
66
|
output_type = self.output_field.get_internal_type()
|
44
67
|
# MySQL doesn't support explicit cast to float.
|
45
68
|
if output_type == "FloatField":
|
46
69
|
template = "(%(expressions)s + 0.0)"
|
47
70
|
# MariaDB doesn't support explicit cast to JSON.
|
48
|
-
elif output_type == "JSONField" and connection.mysql_is_mariadb:
|
71
|
+
elif output_type == "JSONField" and connection.mysql_is_mariadb: # type: ignore[attr-defined]
|
49
72
|
template = "JSON_EXTRACT(%(expressions)s, '$')"
|
50
73
|
return self.as_sql(compiler, connection, template=template, **extra_context)
|
51
74
|
|
52
|
-
def as_postgresql(
|
75
|
+
def as_postgresql(
|
76
|
+
self,
|
77
|
+
compiler: SQLCompiler,
|
78
|
+
connection: BaseDatabaseWrapper,
|
79
|
+
**extra_context: Any,
|
80
|
+
) -> tuple[str, tuple[Any, ...]]:
|
53
81
|
# CAST would be valid too, but the :: shortcut syntax is more readable.
|
54
82
|
# 'expressions' is wrapped in parentheses in case it's a complex
|
55
83
|
# expression.
|
@@ -66,13 +94,13 @@ class Coalesce(Func):
|
|
66
94
|
|
67
95
|
function = "COALESCE"
|
68
96
|
|
69
|
-
def __init__(self, *expressions, **extra):
|
97
|
+
def __init__(self, *expressions: Any, **extra: Any) -> None:
|
70
98
|
if len(expressions) < 2:
|
71
99
|
raise ValueError("Coalesce must take at least two expressions")
|
72
100
|
super().__init__(*expressions, **extra)
|
73
101
|
|
74
102
|
@property
|
75
|
-
def empty_result_set_value(self):
|
103
|
+
def empty_result_set_value(self) -> Any:
|
76
104
|
for expression in self.get_source_expressions():
|
77
105
|
result = expression.empty_result_set_value
|
78
106
|
if result is NotImplemented or result is not None:
|
@@ -87,13 +115,18 @@ class Collate(Func):
|
|
87
115
|
# https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
|
88
116
|
collation_re = _lazy_re_compile(r"^[\w\-]+$")
|
89
117
|
|
90
|
-
def __init__(self, expression, collation):
|
118
|
+
def __init__(self, expression: Any, collation: str) -> None:
|
91
119
|
if not (collation and self.collation_re.match(collation)):
|
92
120
|
raise ValueError(f"Invalid collation name: {collation!r}.")
|
93
121
|
self.collation = collation
|
94
122
|
super().__init__(expression)
|
95
123
|
|
96
|
-
def as_sql(
|
124
|
+
def as_sql(
|
125
|
+
self,
|
126
|
+
compiler: SQLCompiler,
|
127
|
+
connection: BaseDatabaseWrapper,
|
128
|
+
**extra_context: Any,
|
129
|
+
) -> tuple[str, tuple[Any, ...]]:
|
97
130
|
extra_context.setdefault("collation", connection.ops.quote_name(self.collation))
|
98
131
|
return super().as_sql(compiler, connection, **extra_context)
|
99
132
|
|
@@ -109,12 +142,17 @@ class Greatest(Func):
|
|
109
142
|
|
110
143
|
function = "GREATEST"
|
111
144
|
|
112
|
-
def __init__(self, *expressions, **extra):
|
145
|
+
def __init__(self, *expressions: Any, **extra: Any) -> None:
|
113
146
|
if len(expressions) < 2:
|
114
147
|
raise ValueError("Greatest must take at least two expressions")
|
115
148
|
super().__init__(*expressions, **extra)
|
116
149
|
|
117
|
-
def as_sqlite(
|
150
|
+
def as_sqlite(
|
151
|
+
self,
|
152
|
+
compiler: SQLCompiler,
|
153
|
+
connection: BaseDatabaseWrapper,
|
154
|
+
**extra_context: Any,
|
155
|
+
) -> tuple[str, tuple[Any, ...]]:
|
118
156
|
"""Use the MAX function on SQLite."""
|
119
157
|
return super().as_sqlite(compiler, connection, function="MAX", **extra_context)
|
120
158
|
|
@@ -123,20 +161,30 @@ class JSONObject(Func):
|
|
123
161
|
function = "JSON_OBJECT"
|
124
162
|
output_field = JSONField()
|
125
163
|
|
126
|
-
def __init__(self, **fields):
|
164
|
+
def __init__(self, **fields: Any) -> None:
|
127
165
|
expressions = []
|
128
166
|
for key, value in fields.items():
|
129
167
|
expressions.extend((Value(key), value))
|
130
168
|
super().__init__(*expressions)
|
131
169
|
|
132
|
-
def as_sql(
|
170
|
+
def as_sql(
|
171
|
+
self,
|
172
|
+
compiler: SQLCompiler,
|
173
|
+
connection: BaseDatabaseWrapper,
|
174
|
+
**extra_context: Any,
|
175
|
+
) -> tuple[str, tuple[Any, ...]]:
|
133
176
|
if not connection.features.has_json_object_function:
|
134
177
|
raise NotSupportedError(
|
135
178
|
"JSONObject() is not supported on this database backend."
|
136
179
|
)
|
137
180
|
return super().as_sql(compiler, connection, **extra_context)
|
138
181
|
|
139
|
-
def as_postgresql(
|
182
|
+
def as_postgresql(
|
183
|
+
self,
|
184
|
+
compiler: SQLCompiler,
|
185
|
+
connection: BaseDatabaseWrapper,
|
186
|
+
**extra_context: Any,
|
187
|
+
) -> tuple[str, tuple[Any, ...]]:
|
140
188
|
copy = self.copy()
|
141
189
|
copy.set_source_expressions(
|
142
190
|
[
|
@@ -163,12 +211,17 @@ class Least(Func):
|
|
163
211
|
|
164
212
|
function = "LEAST"
|
165
213
|
|
166
|
-
def __init__(self, *expressions, **extra):
|
214
|
+
def __init__(self, *expressions: Any, **extra: Any) -> None:
|
167
215
|
if len(expressions) < 2:
|
168
216
|
raise ValueError("Least must take at least two expressions")
|
169
217
|
super().__init__(*expressions, **extra)
|
170
218
|
|
171
|
-
def as_sqlite(
|
219
|
+
def as_sqlite(
|
220
|
+
self,
|
221
|
+
compiler: SQLCompiler,
|
222
|
+
connection: BaseDatabaseWrapper,
|
223
|
+
**extra_context: Any,
|
224
|
+
) -> tuple[str, tuple[Any, ...]]:
|
172
225
|
"""Use the MIN function on SQLite."""
|
173
226
|
return super().as_sqlite(compiler, connection, function="MIN", **extra_context)
|
174
227
|
|