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.
@@ -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.manager import Manager
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
- "managers",
53
- "managers_map",
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.local_managers = []
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
- @cached_property
228
- def managers(self):
229
- managers = []
230
- seen_managers = set()
231
- bases = (b for b in self.model.mro() if hasattr(b, "_meta"))
232
- for depth, base in enumerate(bases):
233
- for manager in base._meta.local_managers:
234
- if manager.name in seen_managers:
235
- continue
236
-
237
- manager = copy.copy(manager)
238
- manager.model = self.model
239
- seen_managers.add(manager.name)
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
- if self.managers:
295
- return self.managers[0]
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
- attrs[CODE_STACKTRACE] = "".join(traceback.format_stack())
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, hints=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._base_manager.filter(id__in=self.values("id"))
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._base_manager.filter(id__in=other.values("id"))
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._base_manager.filter(id__in=self.values("id"))
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._base_manager.filter(id__in=other.values("id"))
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 manager in some cases.
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 manager should have given us a fresh
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
- manager = getattr(obj, to_attr)
2203
+ queryset = getattr(obj, to_attr)
2235
2204
  if leaf and lookup.queryset is not None:
2236
- qs = manager._apply_rel_filters(lookup.queryset)
2205
+ qs = queryset._apply_rel_filters(lookup.queryset)
2237
2206
  else:
2238
- qs = manager.get_queryset()
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.
@@ -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.objects.filter(id__in=Restaurant.objects.all()).
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,
@@ -98,20 +98,20 @@ class SQLCompiler:
98
98
  then it is correct".
99
99
  """
100
100
  # Some examples:
101
- # SomeModel.objects.annotate(Count('somecol'))
101
+ # SomeModel.query.annotate(Count('somecol'))
102
102
  # GROUP BY: all fields of the model
103
103
  #
104
- # SomeModel.objects.values('name').annotate(Count('somecol'))
104
+ # SomeModel.query.values('name').annotate(Count('somecol'))
105
105
  # GROUP BY: name
106
106
  #
107
- # SomeModel.objects.annotate(Count('somecol')).values('name')
107
+ # SomeModel.query.annotate(Count('somecol')).values('name')
108
108
  # GROUP BY: all cols of the model
109
109
  #
110
- # SomeModel.objects.values('name', 'id')
110
+ # SomeModel.query.values('name', 'id')
111
111
  # .annotate(Count('somecol')).values('id')
112
112
  # GROUP BY: name, id
113
113
  #
114
- # SomeModel.objects.values('name').annotate(Count('somecol')).values('id')
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.objects.all()). For example, the
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.objects.all() queryset's .model (Author also).
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.41.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
- - [Managers](#managers)
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.objects.create(
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.objects.filter(is_admin=True)
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 [`Manager`](./manager.py#Manager) interface:
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.objects.all()
103
+ all_users = User.query.all()
104
104
 
105
105
  # Filter users
106
- admin_users = User.objects.filter(is_admin=True)
107
- recent_users = User.objects.filter(created_at__gte=datetime.now() - timedelta(days=7))
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.objects.get(email="test@example.com")
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.objects.filter(
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.objects.order_by("-created_at")
119
+ users = User.query.order_by("-created_at")
120
120
 
121
121
  # Limiting results
122
- first_10_users = User.objects.all()[:10]
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
- ## Managers
225
+ ## Custom QuerySets
226
226
 
227
- [`Manager`](./manager.py#Manager) objects provide the interface for querying models:
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 PublishedManager(models.Manager):
231
- def get_queryset(self):
232
- return super().get_queryset().filter(status="published")
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
- # Default manager
239
- objects = models.Manager()
279
+ @classmethod
280
+ def published(cls):
281
+ return PublishedQuerySet(model=cls).published_only()
240
282
 
241
- # Custom manager
242
- published = PublishedManager()
283
+ @classmethod
284
+ def drafts(cls):
285
+ return PublishedQuerySet(model=cls).draft_only()
243
286
 
244
287
  # Usage
245
- all_articles = Article.objects.all()
246
- published_articles = Article.published.all()
288
+ published_articles = Article.published()
289
+ draft_articles = Article.drafts()
247
290
  ```
248
291
 
249
292
  ## Forms