plain.models 0.41.0__py3-none-any.whl → 0.42.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 +65 -22
- plain/models/__init__.py +0 -2
- plain/models/base.py +12 -34
- plain/models/cli.py +16 -22
- plain/models/constraints.py +1 -1
- plain/models/deletion.py +1 -1
- plain/models/fields/__init__.py +1 -1
- plain/models/fields/related.py +1 -4
- plain/models/fields/related_descriptors.py +57 -53
- plain/models/fields/related_lookups.py +2 -2
- plain/models/fields/reverse_related.py +1 -1
- plain/models/forms.py +1 -1
- plain/models/migrations/autodetector.py +0 -18
- plain/models/migrations/operations/__init__.py +0 -2
- plain/models/migrations/operations/models.py +3 -57
- plain/models/migrations/operations/special.py +2 -8
- plain/models/migrations/recorder.py +1 -1
- plain/models/migrations/serializer.py +0 -12
- plain/models/migrations/state.py +1 -55
- plain/models/options.py +23 -86
- plain/models/otel.py +13 -1
- plain/models/query.py +10 -41
- plain/models/query_utils.py +1 -1
- plain/models/sql/compiler.py +5 -5
- plain/models/sql/query.py +2 -2
- {plain_models-0.41.0.dist-info → plain_models-0.42.0.dist-info}/METADATA +66 -23
- {plain_models-0.41.0.dist-info → plain_models-0.42.0.dist-info}/RECORD +31 -32
- plain/models/manager.py +0 -176
- {plain_models-0.41.0.dist-info → plain_models-0.42.0.dist-info}/WHEEL +0 -0
- {plain_models-0.41.0.dist-info → plain_models-0.42.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.41.0.dist-info → plain_models-0.42.0.dist-info}/licenses/LICENSE +0 -0
plain/models/migrations/state.py
CHANGED
@@ -12,7 +12,6 @@ from plain.models.options import DEFAULT_NAMES
|
|
12
12
|
from plain.models.registry import ModelsRegistry
|
13
13
|
from plain.models.registry import models_registry as global_models
|
14
14
|
from plain.packages import packages_registry
|
15
|
-
from plain.utils.module_loading import import_string
|
16
15
|
|
17
16
|
from .exceptions import InvalidBasesError
|
18
17
|
from .utils import resolve_relation
|
@@ -176,11 +175,6 @@ class ProjectState:
|
|
176
175
|
model_state.options.pop(key, False)
|
177
176
|
self.reload_model(package_label, model_name, delay=True)
|
178
177
|
|
179
|
-
def alter_model_managers(self, package_label, model_name, managers):
|
180
|
-
model_state = self.models[package_label, model_name]
|
181
|
-
model_state.managers = list(managers)
|
182
|
-
self.reload_model(package_label, model_name, delay=True)
|
183
|
-
|
184
178
|
def _append_option(self, package_label, model_name, option_name, obj):
|
185
179
|
model_state = self.models[package_label, model_name]
|
186
180
|
model_state.options[option_name] = [*model_state.options[option_name], obj]
|
@@ -598,9 +592,7 @@ class ModelState:
|
|
598
592
|
assign new ones, as these are not detached during a clone.
|
599
593
|
"""
|
600
594
|
|
601
|
-
def __init__(
|
602
|
-
self, package_label, name, fields, options=None, bases=None, managers=None
|
603
|
-
):
|
595
|
+
def __init__(self, package_label, name, fields, options=None, bases=None):
|
604
596
|
self.package_label = package_label
|
605
597
|
self.name = name
|
606
598
|
self.fields = dict(fields)
|
@@ -608,7 +600,6 @@ class ModelState:
|
|
608
600
|
self.options.setdefault("indexes", [])
|
609
601
|
self.options.setdefault("constraints", [])
|
610
602
|
self.bases = bases or (models.Model,)
|
611
|
-
self.managers = managers or []
|
612
603
|
for name, field in self.fields.items():
|
613
604
|
# Sanity-check that fields are NOT already bound to a model.
|
614
605
|
if hasattr(field, "model"):
|
@@ -711,33 +702,6 @@ class ModelState:
|
|
711
702
|
):
|
712
703
|
bases = (models.Model,)
|
713
704
|
|
714
|
-
managers = []
|
715
|
-
manager_names = set()
|
716
|
-
default_manager_shim = None
|
717
|
-
for manager in model._meta.managers:
|
718
|
-
if manager.name in manager_names:
|
719
|
-
# Skip overridden managers.
|
720
|
-
continue
|
721
|
-
elif manager.use_in_migrations:
|
722
|
-
# Copy managers usable in migrations.
|
723
|
-
new_manager = copy.copy(manager)
|
724
|
-
new_manager._set_creation_counter()
|
725
|
-
elif manager is model._base_manager or manager is model._default_manager:
|
726
|
-
# Shim custom managers used as default and base managers.
|
727
|
-
new_manager = models.Manager()
|
728
|
-
new_manager.model = manager.model
|
729
|
-
new_manager.name = manager.name
|
730
|
-
if manager is model._default_manager:
|
731
|
-
default_manager_shim = new_manager
|
732
|
-
else:
|
733
|
-
continue
|
734
|
-
manager_names.add(manager.name)
|
735
|
-
managers.append((manager.name, new_manager))
|
736
|
-
|
737
|
-
# Ignore a shimmed default manager called objects if it's the only one.
|
738
|
-
if managers == [("objects", default_manager_shim)]:
|
739
|
-
managers = []
|
740
|
-
|
741
705
|
# Construct the new ModelState
|
742
706
|
return cls(
|
743
707
|
model._meta.package_label,
|
@@ -745,22 +709,8 @@ class ModelState:
|
|
745
709
|
fields,
|
746
710
|
options,
|
747
711
|
bases,
|
748
|
-
managers,
|
749
712
|
)
|
750
713
|
|
751
|
-
def construct_managers(self):
|
752
|
-
"""Deep-clone the managers using deconstruction."""
|
753
|
-
# Sort all managers by their creation counter
|
754
|
-
sorted_managers = sorted(self.managers, key=lambda v: v[1].creation_counter)
|
755
|
-
for mgr_name, manager in sorted_managers:
|
756
|
-
as_manager, manager_path, qs_path, args, kwargs = manager.deconstruct()
|
757
|
-
if as_manager:
|
758
|
-
qs_class = import_string(qs_path)
|
759
|
-
yield mgr_name, qs_class.as_manager()
|
760
|
-
else:
|
761
|
-
manager_class = import_string(manager_path)
|
762
|
-
yield mgr_name, manager_class(*args, **kwargs)
|
763
|
-
|
764
714
|
def clone(self):
|
765
715
|
"""Return an exact copy of this ModelState."""
|
766
716
|
return self.__class__(
|
@@ -772,7 +722,6 @@ class ModelState:
|
|
772
722
|
# than mutating it.
|
773
723
|
options=dict(self.options),
|
774
724
|
bases=self.bases,
|
775
|
-
managers=list(self.managers),
|
776
725
|
)
|
777
726
|
|
778
727
|
def render(self, models_registry):
|
@@ -799,8 +748,6 @@ class ModelState:
|
|
799
748
|
body["Meta"] = meta
|
800
749
|
body["__module__"] = "__fake__"
|
801
750
|
|
802
|
-
# Restore managers
|
803
|
-
body.update(self.construct_managers())
|
804
751
|
# Then, make a Model object (models_registry.register_model is called in __new__)
|
805
752
|
model_class = type(self.name, bases, body)
|
806
753
|
from plain.models import register_model
|
@@ -840,5 +787,4 @@ class ModelState:
|
|
840
787
|
)
|
841
788
|
and (self.options == other.options)
|
842
789
|
and (self.bases == other.bases)
|
843
|
-
and (self.managers == other.managers)
|
844
790
|
)
|
plain/models/options.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import bisect
|
2
|
-
import copy
|
3
2
|
import inspect
|
4
3
|
from collections import defaultdict
|
5
4
|
from functools import cached_property
|
@@ -9,7 +8,7 @@ from plain.models import models_registry
|
|
9
8
|
from plain.models.constraints import UniqueConstraint
|
10
9
|
from plain.models.db import db_connection
|
11
10
|
from plain.models.fields import PrimaryKeyField
|
12
|
-
from plain.models.
|
11
|
+
from plain.models.query import QuerySet
|
13
12
|
from plain.utils.datastructures import ImmutableList
|
14
13
|
|
15
14
|
PROXY_PARENTS = object()
|
@@ -24,14 +23,12 @@ IMMUTABLE_WARNING = (
|
|
24
23
|
DEFAULT_NAMES = (
|
25
24
|
"db_table",
|
26
25
|
"db_table_comment",
|
26
|
+
"queryset_class",
|
27
27
|
"ordering",
|
28
28
|
"package_label",
|
29
29
|
"models_registry",
|
30
|
-
"default_related_name",
|
31
30
|
"required_db_features",
|
32
31
|
"required_db_vendor",
|
33
|
-
"base_manager_name",
|
34
|
-
"default_manager_name",
|
35
32
|
"indexes",
|
36
33
|
"constraints",
|
37
34
|
)
|
@@ -49,10 +46,8 @@ class Options:
|
|
49
46
|
"local_concrete_fields",
|
50
47
|
"_non_pk_concrete_field_names",
|
51
48
|
"_forward_fields_map",
|
52
|
-
"
|
53
|
-
"
|
54
|
-
"base_manager",
|
55
|
-
"default_manager",
|
49
|
+
"base_queryset",
|
50
|
+
"queryset",
|
56
51
|
}
|
57
52
|
REVERSE_PROPERTIES = {"related_objects", "fields_map", "_relation_tree"}
|
58
53
|
|
@@ -62,9 +57,7 @@ class Options:
|
|
62
57
|
self._get_fields_cache = {}
|
63
58
|
self.local_fields = []
|
64
59
|
self.local_many_to_many = []
|
65
|
-
self.
|
66
|
-
self.base_manager_name = None
|
67
|
-
self.default_manager_name = None
|
60
|
+
self.queryset_class = None
|
68
61
|
self.model_name = None
|
69
62
|
self.db_table = ""
|
70
63
|
self.db_table_comment = ""
|
@@ -89,8 +82,6 @@ class Options:
|
|
89
82
|
# A custom app registry to use, if you're making a separate model set.
|
90
83
|
self.models_registry = self.default_models_registry
|
91
84
|
|
92
|
-
self.default_related_name = None
|
93
|
-
|
94
85
|
@property
|
95
86
|
def label(self):
|
96
87
|
return f"{self.package_label}.{self.object_name}"
|
@@ -169,10 +160,6 @@ class Options:
|
|
169
160
|
if not any(f.name == "id" for f in self.local_fields):
|
170
161
|
model.add_to_class("id", PrimaryKeyField())
|
171
162
|
|
172
|
-
def add_manager(self, manager):
|
173
|
-
self.local_managers.append(manager)
|
174
|
-
self._expire_cache()
|
175
|
-
|
176
163
|
def add_field(self, field, private=False):
|
177
164
|
# Insert the given field in the order in which it was created, using
|
178
165
|
# the "creation_counter" attribute of the field.
|
@@ -224,75 +211,25 @@ class Options:
|
|
224
211
|
)
|
225
212
|
return True
|
226
213
|
|
227
|
-
@
|
228
|
-
def
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
managers.append((depth, manager.creation_counter, manager))
|
241
|
-
|
242
|
-
return make_immutable_fields_list(
|
243
|
-
"managers",
|
244
|
-
(m[2] for m in sorted(managers)),
|
245
|
-
)
|
246
|
-
|
247
|
-
@cached_property
|
248
|
-
def managers_map(self):
|
249
|
-
return {manager.name: manager for manager in self.managers}
|
250
|
-
|
251
|
-
@cached_property
|
252
|
-
def base_manager(self):
|
253
|
-
base_manager_name = self.base_manager_name
|
254
|
-
if not base_manager_name:
|
255
|
-
# Get the first parent's base_manager_name if there's one.
|
256
|
-
for parent in self.model.mro()[1:]:
|
257
|
-
if hasattr(parent, "_meta"):
|
258
|
-
if parent._base_manager.name != "_base_manager":
|
259
|
-
base_manager_name = parent._base_manager.name
|
260
|
-
break
|
261
|
-
|
262
|
-
if base_manager_name:
|
263
|
-
try:
|
264
|
-
return self.managers_map[base_manager_name]
|
265
|
-
except KeyError:
|
266
|
-
raise ValueError(
|
267
|
-
f"{self.object_name} has no manager named {base_manager_name!r}"
|
268
|
-
)
|
269
|
-
|
270
|
-
manager = Manager()
|
271
|
-
manager.name = "_base_manager"
|
272
|
-
manager.model = self.model
|
273
|
-
manager.auto_created = True
|
274
|
-
return manager
|
275
|
-
|
276
|
-
@cached_property
|
277
|
-
def default_manager(self):
|
278
|
-
default_manager_name = self.default_manager_name
|
279
|
-
if not default_manager_name and not self.local_managers:
|
280
|
-
# Get the first parent's default_manager_name if there's one.
|
281
|
-
for parent in self.model.mro()[1:]:
|
282
|
-
if hasattr(parent, "_meta"):
|
283
|
-
default_manager_name = parent._meta.default_manager_name
|
284
|
-
break
|
285
|
-
|
286
|
-
if default_manager_name:
|
287
|
-
try:
|
288
|
-
return self.managers_map[default_manager_name]
|
289
|
-
except KeyError:
|
290
|
-
raise ValueError(
|
291
|
-
f"{self.object_name} has no manager named {default_manager_name!r}"
|
292
|
-
)
|
214
|
+
@property
|
215
|
+
def base_queryset(self):
|
216
|
+
"""
|
217
|
+
The base queryset is used by Plain's internal operations like cascading
|
218
|
+
deletes, migrations, and related object lookups. It provides access to
|
219
|
+
all objects in the database without any filtering, ensuring Plain can
|
220
|
+
always see the complete dataset when performing framework operations.
|
221
|
+
|
222
|
+
Unlike user-defined querysets which may filter results (e.g. only active
|
223
|
+
objects), the base queryset must never filter out rows to prevent
|
224
|
+
incomplete results in related queries.
|
225
|
+
"""
|
226
|
+
return QuerySet(model=self.model)
|
293
227
|
|
294
|
-
|
295
|
-
|
228
|
+
@property
|
229
|
+
def queryset(self):
|
230
|
+
if self.queryset_class:
|
231
|
+
return self.queryset_class(model=self.model)
|
232
|
+
return QuerySet(model=self.model)
|
296
233
|
|
297
234
|
@cached_property
|
298
235
|
def fields(self):
|
plain/models/otel.py
CHANGED
@@ -218,7 +218,19 @@ def _get_code_attributes():
|
|
218
218
|
|
219
219
|
# Add full stack trace only in DEBUG mode (expensive)
|
220
220
|
if settings.DEBUG:
|
221
|
-
|
221
|
+
# Filter out internal frames from the stack trace
|
222
|
+
filtered_stack = []
|
223
|
+
for frame in stack:
|
224
|
+
filepath = frame.filename
|
225
|
+
if not filepath:
|
226
|
+
continue
|
227
|
+
if "/plain/models/" in filepath:
|
228
|
+
continue
|
229
|
+
if filepath.endswith("contextlib.py"):
|
230
|
+
continue
|
231
|
+
filtered_stack.append(frame)
|
232
|
+
|
233
|
+
attrs[CODE_STACKTRACE] = "".join(traceback.format_list(filtered_stack))
|
222
234
|
|
223
235
|
return attrs
|
224
236
|
|
plain/models/query.py
CHANGED
@@ -255,9 +255,8 @@ class FlatValuesListIterable(BaseIterable):
|
|
255
255
|
class QuerySet:
|
256
256
|
"""Represent a lazy database lookup for a set of objects."""
|
257
257
|
|
258
|
-
def __init__(self, model=None, query=None
|
258
|
+
def __init__(self, *, model=None, query=None):
|
259
259
|
self.model = model
|
260
|
-
self._hints = hints or {}
|
261
260
|
self._query = query or sql.Query(self.model)
|
262
261
|
self._result_cache = None
|
263
262
|
self._sticky_filter = False
|
@@ -284,17 +283,6 @@ class QuerySet:
|
|
284
283
|
self._iterable_class = ValuesIterable
|
285
284
|
self._query = value
|
286
285
|
|
287
|
-
def as_manager(cls):
|
288
|
-
# Address the circular dependency between `Queryset` and `Manager`.
|
289
|
-
from plain.models.manager import Manager
|
290
|
-
|
291
|
-
manager = Manager.from_queryset(cls)()
|
292
|
-
manager._built_with_as_manager = True
|
293
|
-
return manager
|
294
|
-
|
295
|
-
as_manager.queryset_only = True
|
296
|
-
as_manager = classmethod(as_manager)
|
297
|
-
|
298
286
|
########################
|
299
287
|
# PYTHON MAGIC METHODS #
|
300
288
|
########################
|
@@ -425,12 +413,12 @@ class QuerySet:
|
|
425
413
|
query = (
|
426
414
|
self
|
427
415
|
if self.query.can_filter()
|
428
|
-
else self.model.
|
416
|
+
else self.model._meta.base_queryset.filter(id__in=self.values("id"))
|
429
417
|
)
|
430
418
|
combined = query._chain()
|
431
419
|
combined._merge_known_related_objects(other)
|
432
420
|
if not other.query.can_filter():
|
433
|
-
other = other.model.
|
421
|
+
other = other.model._meta.base_queryset.filter(id__in=other.values("id"))
|
434
422
|
combined.query.combine(other.query, sql.OR)
|
435
423
|
return combined
|
436
424
|
|
@@ -444,12 +432,12 @@ class QuerySet:
|
|
444
432
|
query = (
|
445
433
|
self
|
446
434
|
if self.query.can_filter()
|
447
|
-
else self.model.
|
435
|
+
else self.model._meta.base_queryset.filter(id__in=self.values("id"))
|
448
436
|
)
|
449
437
|
combined = query._chain()
|
450
438
|
combined._merge_known_related_objects(other)
|
451
439
|
if not other.query.can_filter():
|
452
|
-
other = other.model.
|
440
|
+
other = other.model._meta.base_queryset.filter(id__in=other.values("id"))
|
453
441
|
combined.query.combine(other.query, sql.XOR)
|
454
442
|
return combined
|
455
443
|
|
@@ -957,8 +945,6 @@ class QuerySet:
|
|
957
945
|
self._result_cache = None
|
958
946
|
return deleted, _rows_count
|
959
947
|
|
960
|
-
delete.queryset_only = True
|
961
|
-
|
962
948
|
def _raw_delete(self):
|
963
949
|
"""
|
964
950
|
Delete objects found from the given queryset in single direct SQL
|
@@ -1027,8 +1013,6 @@ class QuerySet:
|
|
1027
1013
|
self._result_cache = None
|
1028
1014
|
return query.get_compiler().execute_sql(CURSOR)
|
1029
1015
|
|
1030
|
-
_update.queryset_only = False
|
1031
|
-
|
1032
1016
|
def exists(self):
|
1033
1017
|
"""
|
1034
1018
|
Return True if the QuerySet would have any results, False otherwise.
|
@@ -1201,7 +1185,7 @@ class QuerySet:
|
|
1201
1185
|
def all(self):
|
1202
1186
|
"""
|
1203
1187
|
Return a new QuerySet that is a copy of the current one. This allows a
|
1204
|
-
QuerySet to proxy for a model
|
1188
|
+
QuerySet to proxy for a model queryset in some cases.
|
1205
1189
|
"""
|
1206
1190
|
return self._chain()
|
1207
1191
|
|
@@ -1565,8 +1549,6 @@ class QuerySet:
|
|
1565
1549
|
query.insert_values(fields, objs, raw=raw)
|
1566
1550
|
return query.get_compiler().execute_sql(returning_fields)
|
1567
1551
|
|
1568
|
-
_insert.queryset_only = False
|
1569
|
-
|
1570
1552
|
def _batched_insert(
|
1571
1553
|
self,
|
1572
1554
|
objs,
|
@@ -1622,7 +1604,6 @@ class QuerySet:
|
|
1622
1604
|
c = self.__class__(
|
1623
1605
|
model=self.model,
|
1624
1606
|
query=self.query.chain(),
|
1625
|
-
hints=self._hints,
|
1626
1607
|
)
|
1627
1608
|
c._sticky_filter = self._sticky_filter
|
1628
1609
|
c._for_write = self._for_write
|
@@ -1678,15 +1659,6 @@ class QuerySet:
|
|
1678
1659
|
query = self.query.resolve_expression(*args, **kwargs)
|
1679
1660
|
return query
|
1680
1661
|
|
1681
|
-
resolve_expression.queryset_only = True
|
1682
|
-
|
1683
|
-
def _add_hints(self, **hints):
|
1684
|
-
"""
|
1685
|
-
Update hinting information for use by routers. Add new key/values or
|
1686
|
-
overwrite existing key/values.
|
1687
|
-
"""
|
1688
|
-
self._hints.update(hints)
|
1689
|
-
|
1690
1662
|
def _has_filters(self):
|
1691
1663
|
"""
|
1692
1664
|
Check if this QuerySet has any filtering going on. This isn't
|
@@ -1747,11 +1719,9 @@ class RawQuerySet:
|
|
1747
1719
|
query=None,
|
1748
1720
|
params=(),
|
1749
1721
|
translations=None,
|
1750
|
-
hints=None,
|
1751
1722
|
):
|
1752
1723
|
self.raw_query = raw_query
|
1753
1724
|
self.model = model
|
1754
|
-
self._hints = hints or {}
|
1755
1725
|
self.query = query or sql.RawQuery(sql=raw_query, params=params)
|
1756
1726
|
self.params = params
|
1757
1727
|
self.translations = translations or {}
|
@@ -1797,7 +1767,6 @@ class RawQuerySet:
|
|
1797
1767
|
query=self.query,
|
1798
1768
|
params=self.params,
|
1799
1769
|
translations=self.translations,
|
1800
|
-
hints=self._hints,
|
1801
1770
|
)
|
1802
1771
|
c._prefetch_related_lookups = self._prefetch_related_lookups[:]
|
1803
1772
|
return c
|
@@ -2180,7 +2149,7 @@ def prefetch_one_level(instances, prefetcher, lookup, level):
|
|
2180
2149
|
for additional_lookup in getattr(rel_qs, "_prefetch_related_lookups", ())
|
2181
2150
|
]
|
2182
2151
|
if additional_lookups:
|
2183
|
-
# Don't need to clone because the
|
2152
|
+
# Don't need to clone because the queryset should have given us a fresh
|
2184
2153
|
# instance, so we access an internal instead of using public interface
|
2185
2154
|
# for performance reasons.
|
2186
2155
|
rel_qs._prefetch_related_lookups = ()
|
@@ -2231,11 +2200,11 @@ def prefetch_one_level(instances, prefetcher, lookup, level):
|
|
2231
2200
|
if as_attr:
|
2232
2201
|
setattr(obj, to_attr, vals)
|
2233
2202
|
else:
|
2234
|
-
|
2203
|
+
queryset = getattr(obj, to_attr)
|
2235
2204
|
if leaf and lookup.queryset is not None:
|
2236
|
-
qs =
|
2205
|
+
qs = queryset._apply_rel_filters(lookup.queryset)
|
2237
2206
|
else:
|
2238
|
-
qs =
|
2207
|
+
qs = queryset.__class__(model=queryset.model)
|
2239
2208
|
qs._result_cache = vals
|
2240
2209
|
# We don't want the individual qs doing prefetch_related now,
|
2241
2210
|
# since we have merged this into the current work.
|
plain/models/query_utils.py
CHANGED
@@ -361,7 +361,7 @@ def check_rel_lookup_compatibility(model, target_opts, field):
|
|
361
361
|
# model is ok, too. Consider the case:
|
362
362
|
# class Restaurant(models.Model):
|
363
363
|
# place = OneToOneField(Place, primary_key=True):
|
364
|
-
# Restaurant.
|
364
|
+
# Restaurant.query.filter(id__in=Restaurant.query.all()).
|
365
365
|
# If we didn't have the primary key check, then id__in (== place__in) would
|
366
366
|
# give Place's opts as the target opts, but Restaurant isn't compatible
|
367
367
|
# with that. This logic applies only to primary keys, as when doing __in=qs,
|
plain/models/sql/compiler.py
CHANGED
@@ -98,20 +98,20 @@ class SQLCompiler:
|
|
98
98
|
then it is correct".
|
99
99
|
"""
|
100
100
|
# Some examples:
|
101
|
-
# SomeModel.
|
101
|
+
# SomeModel.query.annotate(Count('somecol'))
|
102
102
|
# GROUP BY: all fields of the model
|
103
103
|
#
|
104
|
-
# SomeModel.
|
104
|
+
# SomeModel.query.values('name').annotate(Count('somecol'))
|
105
105
|
# GROUP BY: name
|
106
106
|
#
|
107
|
-
# SomeModel.
|
107
|
+
# SomeModel.query.annotate(Count('somecol')).values('name')
|
108
108
|
# GROUP BY: all cols of the model
|
109
109
|
#
|
110
|
-
# SomeModel.
|
110
|
+
# SomeModel.query.values('name', 'id')
|
111
111
|
# .annotate(Count('somecol')).values('id')
|
112
112
|
# GROUP BY: name, id
|
113
113
|
#
|
114
|
-
# SomeModel.
|
114
|
+
# SomeModel.query.values('name').annotate(Count('somecol')).values('id')
|
115
115
|
# GROUP BY: name, id
|
116
116
|
#
|
117
117
|
# In fact, the self.query.group_by is the minimal set to GROUP BY. It
|
plain/models/sql/query.py
CHANGED
@@ -1174,9 +1174,9 @@ class Query(BaseExpression):
|
|
1174
1174
|
"""Check the type of object passed to query relations."""
|
1175
1175
|
if field.is_relation:
|
1176
1176
|
# Check that the field and the queryset use the same model in a
|
1177
|
-
# query like .filter(author=Author.
|
1177
|
+
# query like .filter(author=Author.query.all()). For example, the
|
1178
1178
|
# opts would be Author's (from the author field) and value.model
|
1179
|
-
# would be Author.
|
1179
|
+
# would be Author.query.all() queryset's .model (Author also).
|
1180
1180
|
# The field is the related field on the lhs side.
|
1181
1181
|
if (
|
1182
1182
|
isinstance(value, Query)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: plain.models
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.42.0
|
4
4
|
Summary: Model your data and store it in a database.
|
5
5
|
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
6
6
|
License-File: LICENSE
|
@@ -20,7 +20,7 @@ Description-Content-Type: text/markdown
|
|
20
20
|
- [Fields](#fields)
|
21
21
|
- [Validation](#validation)
|
22
22
|
- [Indexes and constraints](#indexes-and-constraints)
|
23
|
-
- [
|
23
|
+
- [Custom QuerySets](#custom-querysets)
|
24
24
|
- [Forms](#forms)
|
25
25
|
- [Sharing fields across models](#sharing-fields-across-models)
|
26
26
|
- [Installation](#installation)
|
@@ -54,7 +54,7 @@ from .models import User
|
|
54
54
|
|
55
55
|
|
56
56
|
# Create a new user
|
57
|
-
user = User.
|
57
|
+
user = User.query.create(
|
58
58
|
email="test@example.com",
|
59
59
|
password="password",
|
60
60
|
)
|
@@ -67,7 +67,7 @@ user.save()
|
|
67
67
|
user.delete()
|
68
68
|
|
69
69
|
# Query for users
|
70
|
-
admin_users = User.
|
70
|
+
admin_users = User.query.filter(is_admin=True)
|
71
71
|
```
|
72
72
|
|
73
73
|
## Database connection
|
@@ -96,30 +96,30 @@ Multiple backends are supported, including Postgres, MySQL, and SQLite.
|
|
96
96
|
|
97
97
|
## Querying
|
98
98
|
|
99
|
-
Models come with a powerful query API through their [`
|
99
|
+
Models come with a powerful query API through their [`QuerySet`](./query.py#QuerySet) interface:
|
100
100
|
|
101
101
|
```python
|
102
102
|
# Get all users
|
103
|
-
all_users = User.
|
103
|
+
all_users = User.query.all()
|
104
104
|
|
105
105
|
# Filter users
|
106
|
-
admin_users = User.
|
107
|
-
recent_users = User.
|
106
|
+
admin_users = User.query.filter(is_admin=True)
|
107
|
+
recent_users = User.query.filter(created_at__gte=datetime.now() - timedelta(days=7))
|
108
108
|
|
109
109
|
# Get a single user
|
110
|
-
user = User.
|
110
|
+
user = User.query.get(email="test@example.com")
|
111
111
|
|
112
112
|
# Complex queries with Q objects
|
113
113
|
from plain.models import Q
|
114
|
-
users = User.
|
114
|
+
users = User.query.filter(
|
115
115
|
Q(is_admin=True) | Q(email__endswith="@example.com")
|
116
116
|
)
|
117
117
|
|
118
118
|
# Ordering
|
119
|
-
users = User.
|
119
|
+
users = User.query.order_by("-created_at")
|
120
120
|
|
121
121
|
# Limiting results
|
122
|
-
first_10_users = User.
|
122
|
+
first_10_users = User.query.all()[:10]
|
123
123
|
```
|
124
124
|
|
125
125
|
For more advanced querying options, see the [`QuerySet`](./query.py#QuerySet) class.
|
@@ -222,28 +222,71 @@ class User(models.Model):
|
|
222
222
|
]
|
223
223
|
```
|
224
224
|
|
225
|
-
##
|
225
|
+
## Custom QuerySets
|
226
226
|
|
227
|
-
[`
|
227
|
+
With the Manager functionality now merged into QuerySet, you can customize [`QuerySet`](./query.py#QuerySet) classes to provide specialized query methods. There are several ways to use custom QuerySets:
|
228
|
+
|
229
|
+
### Setting a default QuerySet for a model
|
230
|
+
|
231
|
+
Use `Meta.queryset_class` to set a custom QuerySet that will be used by `Model.query`:
|
232
|
+
|
233
|
+
```python
|
234
|
+
class PublishedQuerySet(models.QuerySet):
|
235
|
+
def published_only(self):
|
236
|
+
return self.filter(status="published")
|
237
|
+
|
238
|
+
def draft_only(self):
|
239
|
+
return self.filter(status="draft")
|
240
|
+
|
241
|
+
@models.register_model
|
242
|
+
class Article(models.Model):
|
243
|
+
title = models.CharField(max_length=200)
|
244
|
+
status = models.CharField(max_length=20)
|
245
|
+
|
246
|
+
class Meta:
|
247
|
+
queryset_class = PublishedQuerySet
|
248
|
+
|
249
|
+
# Usage - all methods available on Article.objects
|
250
|
+
all_articles = Article.query.all()
|
251
|
+
published_articles = Article.query.published_only()
|
252
|
+
draft_articles = Article.query.draft_only()
|
253
|
+
```
|
254
|
+
|
255
|
+
### Using custom QuerySets without formal attachment
|
256
|
+
|
257
|
+
You can also use custom QuerySets manually without setting them as the default:
|
228
258
|
|
229
259
|
```python
|
230
|
-
class
|
231
|
-
def
|
232
|
-
return
|
260
|
+
class SpecialQuerySet(models.QuerySet):
|
261
|
+
def special_filter(self):
|
262
|
+
return self.filter(special=True)
|
233
263
|
|
264
|
+
# Create and use the QuerySet manually
|
265
|
+
special_qs = SpecialQuerySet(model=Article)
|
266
|
+
special_articles = special_qs.special_filter()
|
267
|
+
```
|
268
|
+
|
269
|
+
### Using classmethods for convenience
|
270
|
+
|
271
|
+
For even cleaner API, add classmethods to your model:
|
272
|
+
|
273
|
+
```python
|
274
|
+
@models.register_model
|
234
275
|
class Article(models.Model):
|
235
276
|
title = models.CharField(max_length=200)
|
236
277
|
status = models.CharField(max_length=20)
|
237
278
|
|
238
|
-
|
239
|
-
|
279
|
+
@classmethod
|
280
|
+
def published(cls):
|
281
|
+
return PublishedQuerySet(model=cls).published_only()
|
240
282
|
|
241
|
-
|
242
|
-
|
283
|
+
@classmethod
|
284
|
+
def drafts(cls):
|
285
|
+
return PublishedQuerySet(model=cls).draft_only()
|
243
286
|
|
244
287
|
# Usage
|
245
|
-
|
246
|
-
|
288
|
+
published_articles = Article.published()
|
289
|
+
draft_articles = Article.drafts()
|
247
290
|
```
|
248
291
|
|
249
292
|
## Forms
|