Fixed #20625 -- Chainable Manager/QuerySet methods.
Additionally this patch solves the orthogonal problem that specialized `QuerySet` like `ValuesQuerySet` didn't inherit from the current `QuerySet` type. This wasn't an issue until now because we didn't officially support custom `QuerySet` but it became necessary with the introduction of this new feature. Thanks aaugustin, akaariai, carljm, charettes, mjtamlyn, shaib and timgraham for the reviews.
This commit is contained in:
parent
8f3aefdec3
commit
31fadc1202
|
@ -2,7 +2,7 @@ from functools import wraps
|
|||
|
||||
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
|
||||
from django.db.models.loading import get_apps, get_app_path, get_app_paths, get_app, get_models, get_model, register_models, UnavailableApp
|
||||
from django.db.models.query import Q
|
||||
from django.db.models.query import Q, QuerySet
|
||||
from django.db.models.expressions import F
|
||||
from django.db.models.manager import Manager
|
||||
from django.db.models.base import Model
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import copy
|
||||
import inspect
|
||||
|
||||
from django.db import router
|
||||
from django.db.models.query import QuerySet, insert_query, RawQuerySet
|
||||
from django.db.models import signals
|
||||
|
@ -56,17 +58,51 @@ class RenameManagerMethods(RenameMethodsBase):
|
|||
)
|
||||
|
||||
|
||||
class Manager(six.with_metaclass(RenameManagerMethods)):
|
||||
class BaseManager(six.with_metaclass(RenameManagerMethods)):
|
||||
# Tracks each time a Manager instance is created. Used to retain order.
|
||||
creation_counter = 0
|
||||
|
||||
def __init__(self):
|
||||
super(Manager, self).__init__()
|
||||
super(BaseManager, self).__init__()
|
||||
self._set_creation_counter()
|
||||
self.model = None
|
||||
self._inherited = False
|
||||
self._db = None
|
||||
|
||||
@classmethod
|
||||
def _get_queryset_methods(cls, queryset_class):
|
||||
def create_method(name, method):
|
||||
def manager_method(self, *args, **kwargs):
|
||||
return getattr(self.get_queryset(), name)(*args, **kwargs)
|
||||
manager_method.__name__ = method.__name__
|
||||
manager_method.__doc__ = method.__doc__
|
||||
return manager_method
|
||||
|
||||
new_methods = {}
|
||||
# Refs http://bugs.python.org/issue1785.
|
||||
predicate = inspect.isfunction if six.PY3 else inspect.ismethod
|
||||
for name, method in inspect.getmembers(queryset_class, predicate=predicate):
|
||||
# Only copy missing methods.
|
||||
if hasattr(cls, name):
|
||||
continue
|
||||
# Only copy public methods or methods with the attribute `queryset_only=False`.
|
||||
queryset_only = getattr(method, 'queryset_only', None)
|
||||
if queryset_only or (queryset_only is None and name.startswith('_')):
|
||||
continue
|
||||
# Copy the method onto the manager.
|
||||
new_methods[name] = create_method(name, method)
|
||||
return new_methods
|
||||
|
||||
@classmethod
|
||||
def from_queryset(cls, queryset_class, class_name=None):
|
||||
if class_name is None:
|
||||
class_name = '%sFrom%s' % (cls.__name__, queryset_class.__name__)
|
||||
class_dict = {
|
||||
'_queryset_class': queryset_class,
|
||||
}
|
||||
class_dict.update(cls._get_queryset_methods(queryset_class))
|
||||
return type(class_name, (cls,), class_dict)
|
||||
|
||||
def contribute_to_class(self, model, name):
|
||||
# TODO: Use weakref because of possible memory leak / circular reference.
|
||||
self.model = model
|
||||
|
@ -92,8 +128,8 @@ class Manager(six.with_metaclass(RenameManagerMethods)):
|
|||
Sets the creation counter value for this instance and increments the
|
||||
class-level copy.
|
||||
"""
|
||||
self.creation_counter = Manager.creation_counter
|
||||
Manager.creation_counter += 1
|
||||
self.creation_counter = BaseManager.creation_counter
|
||||
BaseManager.creation_counter += 1
|
||||
|
||||
def _copy_to_model(self, model):
|
||||
"""
|
||||
|
@ -117,130 +153,30 @@ class Manager(six.with_metaclass(RenameManagerMethods)):
|
|||
def db(self):
|
||||
return self._db or router.db_for_read(self.model)
|
||||
|
||||
#######################
|
||||
# PROXIES TO QUERYSET #
|
||||
#######################
|
||||
|
||||
def get_queryset(self):
|
||||
"""Returns a new QuerySet object. Subclasses can override this method
|
||||
to easily customize the behavior of the Manager.
|
||||
"""
|
||||
return QuerySet(self.model, using=self._db)
|
||||
|
||||
def none(self):
|
||||
return self.get_queryset().none()
|
||||
Returns a new QuerySet object. Subclasses can override this method to
|
||||
easily customize the behavior of the Manager.
|
||||
"""
|
||||
return self._queryset_class(self.model, using=self._db)
|
||||
|
||||
def all(self):
|
||||
# We can't proxy this method through the `QuerySet` like we do for the
|
||||
# rest of the `QuerySet` methods. This is because `QuerySet.all()`
|
||||
# works by creating a "copy" of the current queryset and in making said
|
||||
# copy, all the cached `prefetch_related` lookups are lost. See the
|
||||
# implementation of `RelatedManager.get_queryset()` for a better
|
||||
# understanding of how this comes into play.
|
||||
return self.get_queryset()
|
||||
|
||||
def count(self):
|
||||
return self.get_queryset().count()
|
||||
|
||||
def dates(self, *args, **kwargs):
|
||||
return self.get_queryset().dates(*args, **kwargs)
|
||||
|
||||
def datetimes(self, *args, **kwargs):
|
||||
return self.get_queryset().datetimes(*args, **kwargs)
|
||||
|
||||
def distinct(self, *args, **kwargs):
|
||||
return self.get_queryset().distinct(*args, **kwargs)
|
||||
|
||||
def extra(self, *args, **kwargs):
|
||||
return self.get_queryset().extra(*args, **kwargs)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.get_queryset().get(*args, **kwargs)
|
||||
|
||||
def get_or_create(self, **kwargs):
|
||||
return self.get_queryset().get_or_create(**kwargs)
|
||||
|
||||
def update_or_create(self, **kwargs):
|
||||
return self.get_queryset().update_or_create(**kwargs)
|
||||
|
||||
def create(self, **kwargs):
|
||||
return self.get_queryset().create(**kwargs)
|
||||
|
||||
def bulk_create(self, *args, **kwargs):
|
||||
return self.get_queryset().bulk_create(*args, **kwargs)
|
||||
|
||||
def filter(self, *args, **kwargs):
|
||||
return self.get_queryset().filter(*args, **kwargs)
|
||||
|
||||
def aggregate(self, *args, **kwargs):
|
||||
return self.get_queryset().aggregate(*args, **kwargs)
|
||||
|
||||
def annotate(self, *args, **kwargs):
|
||||
return self.get_queryset().annotate(*args, **kwargs)
|
||||
|
||||
def complex_filter(self, *args, **kwargs):
|
||||
return self.get_queryset().complex_filter(*args, **kwargs)
|
||||
|
||||
def exclude(self, *args, **kwargs):
|
||||
return self.get_queryset().exclude(*args, **kwargs)
|
||||
|
||||
def in_bulk(self, *args, **kwargs):
|
||||
return self.get_queryset().in_bulk(*args, **kwargs)
|
||||
|
||||
def iterator(self, *args, **kwargs):
|
||||
return self.get_queryset().iterator(*args, **kwargs)
|
||||
|
||||
def earliest(self, *args, **kwargs):
|
||||
return self.get_queryset().earliest(*args, **kwargs)
|
||||
|
||||
def latest(self, *args, **kwargs):
|
||||
return self.get_queryset().latest(*args, **kwargs)
|
||||
|
||||
def first(self):
|
||||
return self.get_queryset().first()
|
||||
|
||||
def last(self):
|
||||
return self.get_queryset().last()
|
||||
|
||||
def order_by(self, *args, **kwargs):
|
||||
return self.get_queryset().order_by(*args, **kwargs)
|
||||
|
||||
def select_for_update(self, *args, **kwargs):
|
||||
return self.get_queryset().select_for_update(*args, **kwargs)
|
||||
|
||||
def select_related(self, *args, **kwargs):
|
||||
return self.get_queryset().select_related(*args, **kwargs)
|
||||
|
||||
def prefetch_related(self, *args, **kwargs):
|
||||
return self.get_queryset().prefetch_related(*args, **kwargs)
|
||||
|
||||
def values(self, *args, **kwargs):
|
||||
return self.get_queryset().values(*args, **kwargs)
|
||||
|
||||
def values_list(self, *args, **kwargs):
|
||||
return self.get_queryset().values_list(*args, **kwargs)
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
return self.get_queryset().update(*args, **kwargs)
|
||||
|
||||
def reverse(self, *args, **kwargs):
|
||||
return self.get_queryset().reverse(*args, **kwargs)
|
||||
|
||||
def defer(self, *args, **kwargs):
|
||||
return self.get_queryset().defer(*args, **kwargs)
|
||||
|
||||
def only(self, *args, **kwargs):
|
||||
return self.get_queryset().only(*args, **kwargs)
|
||||
|
||||
def using(self, *args, **kwargs):
|
||||
return self.get_queryset().using(*args, **kwargs)
|
||||
|
||||
def exists(self, *args, **kwargs):
|
||||
return self.get_queryset().exists(*args, **kwargs)
|
||||
|
||||
def _insert(self, objs, fields, **kwargs):
|
||||
return insert_query(self.model, objs, fields, **kwargs)
|
||||
|
||||
def _update(self, values, **kwargs):
|
||||
return self.get_queryset()._update(values, **kwargs)
|
||||
|
||||
def raw(self, raw_query, params=None, *args, **kwargs):
|
||||
return RawQuerySet(raw_query=raw_query, model=self.model, params=params, using=self._db, *args, **kwargs)
|
||||
|
||||
Manager = BaseManager.from_queryset(QuerySet, class_name='Manager')
|
||||
|
||||
|
||||
class ManagerDescriptor(object):
|
||||
# This class ensures managers aren't accessible via model instances.
|
||||
|
|
|
@ -10,7 +10,7 @@ from django.conf import settings
|
|||
from django.core import exceptions
|
||||
from django.db import connections, router, transaction, DatabaseError, IntegrityError
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.fields import AutoField
|
||||
from django.db.models.fields import AutoField, Empty
|
||||
from django.db.models.query_utils import (Q, select_related_descend,
|
||||
deferred_class_factory, InvalidQuery)
|
||||
from django.db.models.deletion import Collector
|
||||
|
@ -30,10 +30,23 @@ REPR_OUTPUT_SIZE = 20
|
|||
EmptyResultSet = sql.EmptyResultSet
|
||||
|
||||
|
||||
def _pickle_queryset(class_bases, class_dict):
|
||||
"""
|
||||
Used by `__reduce__` to create the initial version of the `QuerySet` class
|
||||
onto which the output of `__getstate__` will be applied.
|
||||
|
||||
See `__reduce__` for more details.
|
||||
"""
|
||||
new = Empty()
|
||||
new.__class__ = type(class_bases[0].__name__, class_bases, class_dict)
|
||||
return new
|
||||
|
||||
|
||||
class QuerySet(object):
|
||||
"""
|
||||
Represents a lazy database lookup for a set of objects.
|
||||
"""
|
||||
|
||||
def __init__(self, model=None, query=None, using=None):
|
||||
self.model = model
|
||||
self._db = using
|
||||
|
@ -45,6 +58,13 @@ class QuerySet(object):
|
|||
self._prefetch_done = False
|
||||
self._known_related_objects = {} # {rel_field, {pk: rel_obj}}
|
||||
|
||||
def as_manager(cls):
|
||||
# Address the circular dependency between `Queryset` and `Manager`.
|
||||
from django.db.models.manager import Manager
|
||||
return Manager.from_queryset(cls)()
|
||||
as_manager.queryset_only = True
|
||||
as_manager = classmethod(as_manager)
|
||||
|
||||
########################
|
||||
# PYTHON MAGIC METHODS #
|
||||
########################
|
||||
|
@ -70,6 +90,26 @@ class QuerySet(object):
|
|||
obj_dict = self.__dict__.copy()
|
||||
return obj_dict
|
||||
|
||||
def __reduce__(self):
|
||||
"""
|
||||
Used by pickle to deal with the types that we create dynamically when
|
||||
specialized queryset such as `ValuesQuerySet` are used in conjunction
|
||||
with querysets that are *subclasses* of `QuerySet`.
|
||||
|
||||
See `_clone` implementation for more details.
|
||||
"""
|
||||
if hasattr(self, '_specialized_queryset_class'):
|
||||
class_bases = (
|
||||
self._specialized_queryset_class,
|
||||
self._base_queryset_class,
|
||||
)
|
||||
class_dict = {
|
||||
'_specialized_queryset_class': self._specialized_queryset_class,
|
||||
'_base_queryset_class': self._base_queryset_class,
|
||||
}
|
||||
return _pickle_queryset, (class_bases, class_dict), self.__getstate__()
|
||||
return super(QuerySet, self).__reduce__()
|
||||
|
||||
def __repr__(self):
|
||||
data = list(self[:REPR_OUTPUT_SIZE + 1])
|
||||
if len(data) > REPR_OUTPUT_SIZE:
|
||||
|
@ -528,6 +568,7 @@ class QuerySet(object):
|
|||
# Clear the result cache, in case this QuerySet gets reused.
|
||||
self._result_cache = None
|
||||
delete.alters_data = True
|
||||
delete.queryset_only = True
|
||||
|
||||
def _raw_delete(self, using):
|
||||
"""
|
||||
|
@ -567,6 +608,7 @@ class QuerySet(object):
|
|||
self._result_cache = None
|
||||
return query.get_compiler(self.db).execute_sql(None)
|
||||
_update.alters_data = True
|
||||
_update.queryset_only = False
|
||||
|
||||
def exists(self):
|
||||
if self._result_cache is None:
|
||||
|
@ -886,6 +928,15 @@ class QuerySet(object):
|
|||
def _clone(self, klass=None, setup=False, **kwargs):
|
||||
if klass is None:
|
||||
klass = self.__class__
|
||||
elif not issubclass(self.__class__, klass):
|
||||
base_queryset_class = getattr(self, '_base_queryset_class', self.__class__)
|
||||
class_bases = (klass, base_queryset_class)
|
||||
class_dict = {
|
||||
'_base_queryset_class': base_queryset_class,
|
||||
'_specialized_queryset_class': klass,
|
||||
}
|
||||
klass = type(klass.__name__, class_bases, class_dict)
|
||||
|
||||
query = self.query.clone()
|
||||
if self._sticky_filter:
|
||||
query.filter_is_sticky = True
|
||||
|
|
|
@ -121,9 +121,7 @@ described here.
|
|||
QuerySet API
|
||||
============
|
||||
|
||||
Though you usually won't create one manually — you'll go through a
|
||||
:class:`~django.db.models.Manager` — here's the formal declaration of a
|
||||
``QuerySet``:
|
||||
Here's the formal declaration of a ``QuerySet``:
|
||||
|
||||
.. class:: QuerySet([model=None, query=None, using=None])
|
||||
|
||||
|
@ -1866,6 +1864,17 @@ DO_NOTHING do not prevent taking the fast-path in deletion.
|
|||
Note that the queries generated in object deletion is an implementation
|
||||
detail subject to change.
|
||||
|
||||
as_manager
|
||||
~~~~~~~~~~
|
||||
|
||||
.. classmethod:: as_manager()
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Class method that returns an instance of :class:`~django.db.models.Manager`
|
||||
with a copy of the ``QuerySet``'s methods. See
|
||||
:ref:`create-manager-with-queryset-methods` for more details.
|
||||
|
||||
.. _field-lookups:
|
||||
|
||||
Field lookups
|
||||
|
|
|
@ -30,6 +30,13 @@ security support until the release of Django 1.8.
|
|||
What's new in Django 1.7
|
||||
========================
|
||||
|
||||
Calling custom ``QuerySet`` methods from the ``Manager``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The :meth:`QuerySet.as_manager() <django.db.models.query.QuerySet.as_manager>`
|
||||
class method has been added to :ref:`create Manager with QuerySet methods
|
||||
<create-manager-with-queryset-methods>`.
|
||||
|
||||
Admin shortcuts support time zones
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -201,6 +201,125 @@ attribute on the manager class. This is documented fully below_.
|
|||
|
||||
.. _below: manager-types_
|
||||
|
||||
.. _calling-custom-queryset-methods-from-manager:
|
||||
|
||||
Calling custom ``QuerySet`` methods from the ``Manager``
|
||||
--------------------------------------------------------
|
||||
|
||||
While most methods from the standard ``QuerySet`` are accessible directly from
|
||||
the ``Manager``, this is only the case for the extra methods defined on a
|
||||
custom ``QuerySet`` if you also implement them on the ``Manager``::
|
||||
|
||||
class PersonQuerySet(models.QuerySet):
|
||||
def male(self):
|
||||
return self.filter(sex='M')
|
||||
|
||||
def female(self):
|
||||
return self.filter(sex='F')
|
||||
|
||||
class PersonManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return PersonQuerySet()
|
||||
|
||||
def male(self):
|
||||
return self.get_queryset().male()
|
||||
|
||||
def female(self):
|
||||
return self.get_queryset().female()
|
||||
|
||||
class Person(models.Model):
|
||||
first_name = models.CharField(max_length=50)
|
||||
last_name = models.CharField(max_length=50)
|
||||
sex = models.CharField(max_length=1, choices=(('M', 'Male'), ('F', 'Female')))
|
||||
people = PersonManager()
|
||||
|
||||
This example allows you to call both ``male()`` and ``female()`` directly from
|
||||
the manager ``Person.people``.
|
||||
|
||||
.. _create-manager-with-queryset-methods:
|
||||
|
||||
Creating ``Manager`` with ``QuerySet`` methods
|
||||
----------------------------------------------
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
In lieu of the above approach which requires duplicating methods on both the
|
||||
``QuerySet`` and the ``Manager``, :meth:`QuerySet.as_manager()
|
||||
<django.db.models.query.QuerySet.as_manager>` can be used to create an instance
|
||||
of ``Manager`` with a copy of a custom ``QuerySet``'s methods::
|
||||
|
||||
class Person(models.Model):
|
||||
...
|
||||
people = PersonQuerySet.as_manager()
|
||||
|
||||
The ``Manager`` instance created by :meth:`QuerySet.as_manager()
|
||||
<django.db.models.query.QuerySet.as_manager>` will be virtually
|
||||
identical to the ``PersonManager`` from the previous example.
|
||||
|
||||
Not every ``QuerySet`` method makes sense at the ``Manager`` level; for
|
||||
instance we intentionally prevent the :meth:`QuerySet.delete()
|
||||
<django.db.models.query.QuerySet.delete>` method from being copied onto
|
||||
the ``Manager`` class.
|
||||
|
||||
Methods are copied according to the following rules:
|
||||
|
||||
- Public methods are copied by default.
|
||||
- Private methods (starting with an underscore) are not copied by default.
|
||||
- Methods with a `queryset_only` attribute set to `False` are always copied.
|
||||
- Methods with a `queryset_only` attribute set to `True` are never copied.
|
||||
|
||||
For example::
|
||||
|
||||
class CustomQuerySet(models.QuerySet):
|
||||
# Available on both Manager and QuerySet.
|
||||
def public_method(self):
|
||||
return
|
||||
|
||||
# Available only on QuerySet.
|
||||
def _private_method(self):
|
||||
return
|
||||
|
||||
# Available only on QuerySet.
|
||||
def opted_out_public_method(self):
|
||||
return
|
||||
opted_out_public_method.queryset_only = True
|
||||
|
||||
# Available on both Manager and QuerySet.
|
||||
def _opted_in_private_method(self):
|
||||
return
|
||||
_opted_in_private_method.queryset_only = False
|
||||
|
||||
from_queryset
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
.. classmethod:: from_queryset(queryset_class)
|
||||
|
||||
For advance usage you might want both a custom ``Manager`` and a custom
|
||||
``QuerySet``. You can do that by calling ``Manager.from_queryset()`` which
|
||||
returns a *subclass* of your base ``Manager`` with a copy of the custom
|
||||
``QuerySet`` methods::
|
||||
|
||||
class BaseManager(models.Manager):
|
||||
def __init__(self, *args, **kwargs):
|
||||
...
|
||||
|
||||
def manager_only_method(self):
|
||||
return
|
||||
|
||||
class CustomQuerySet(models.QuerySet):
|
||||
def manager_and_queryset_method(self):
|
||||
return
|
||||
|
||||
class MyModel(models.Model):
|
||||
objects = BaseManager.from_queryset(CustomQueryset)(*args, **kwargs)
|
||||
|
||||
You may also store the generated class into a variable::
|
||||
|
||||
CustomManager = BaseManager.from_queryset(CustomQueryset)
|
||||
|
||||
class MyModel(models.Model):
|
||||
objects = CustomManager(*args, **kwargs)
|
||||
|
||||
.. _custom-managers-and-inheritance:
|
||||
|
||||
Custom managers and model inheritance
|
||||
|
|
|
@ -6,6 +6,7 @@ import threading
|
|||
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
|
||||
from django.db import connections, DEFAULT_DB_ALIAS
|
||||
from django.db.models.fields import Field, FieldDoesNotExist
|
||||
from django.db.models.manager import BaseManager
|
||||
from django.db.models.query import QuerySet, EmptyQuerySet, ValuesListQuerySet, MAX_GET_RESULTS
|
||||
from django.test import TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature
|
||||
from django.utils import six
|
||||
|
@ -734,3 +735,57 @@ class ConcurrentSaveTests(TransactionTestCase):
|
|||
t.join()
|
||||
a.save()
|
||||
self.assertEqual(Article.objects.get(pk=a.pk).headline, 'foo')
|
||||
|
||||
|
||||
class ManagerTest(TestCase):
|
||||
QUERYSET_PROXY_METHODS = [
|
||||
'none',
|
||||
'count',
|
||||
'dates',
|
||||
'datetimes',
|
||||
'distinct',
|
||||
'extra',
|
||||
'get',
|
||||
'get_or_create',
|
||||
'update_or_create',
|
||||
'create',
|
||||
'bulk_create',
|
||||
'filter',
|
||||
'aggregate',
|
||||
'annotate',
|
||||
'complex_filter',
|
||||
'exclude',
|
||||
'in_bulk',
|
||||
'iterator',
|
||||
'earliest',
|
||||
'latest',
|
||||
'first',
|
||||
'last',
|
||||
'order_by',
|
||||
'select_for_update',
|
||||
'select_related',
|
||||
'prefetch_related',
|
||||
'values',
|
||||
'values_list',
|
||||
'update',
|
||||
'reverse',
|
||||
'defer',
|
||||
'only',
|
||||
'using',
|
||||
'exists',
|
||||
'_update',
|
||||
]
|
||||
|
||||
def test_manager_methods(self):
|
||||
"""
|
||||
This test ensures that the correct set of methods from `QuerySet`
|
||||
are copied onto `Manager`.
|
||||
|
||||
It's particularly useful to prevent accidentally leaking new methods
|
||||
into `Manager`. New `QuerySet` methods that should also be copied onto
|
||||
`Manager` will need to be added to `ManagerTest.QUERYSET_PROXY_METHODS`.
|
||||
"""
|
||||
self.assertEqual(
|
||||
sorted(BaseManager._get_queryset_methods(QuerySet).keys()),
|
||||
sorted(self.QUERYSET_PROXY_METHODS),
|
||||
)
|
||||
|
|
|
@ -20,6 +20,49 @@ class PersonManager(models.Manager):
|
|||
def get_fun_people(self):
|
||||
return self.filter(fun=True)
|
||||
|
||||
# An example of a custom manager that sets get_queryset().
|
||||
|
||||
class PublishedBookManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super(PublishedBookManager, self).get_queryset().filter(is_published=True)
|
||||
|
||||
# An example of a custom queryset that copies its methods onto the manager.
|
||||
|
||||
class CustomQuerySet(models.QuerySet):
|
||||
def filter(self, *args, **kwargs):
|
||||
queryset = super(CustomQuerySet, self).filter(fun=True)
|
||||
queryset._filter_CustomQuerySet = True
|
||||
return queryset
|
||||
|
||||
def public_method(self, *args, **kwargs):
|
||||
return self.all()
|
||||
|
||||
def _private_method(self, *args, **kwargs):
|
||||
return self.all()
|
||||
|
||||
def optout_public_method(self, *args, **kwargs):
|
||||
return self.all()
|
||||
optout_public_method.queryset_only = True
|
||||
|
||||
def _optin_private_method(self, *args, **kwargs):
|
||||
return self.all()
|
||||
_optin_private_method.queryset_only = False
|
||||
|
||||
class BaseCustomManager(models.Manager):
|
||||
def __init__(self, arg):
|
||||
super(BaseCustomManager, self).__init__()
|
||||
self.init_arg = arg
|
||||
|
||||
def filter(self, *args, **kwargs):
|
||||
queryset = super(BaseCustomManager, self).filter(fun=True)
|
||||
queryset._filter_CustomManager = True
|
||||
return queryset
|
||||
|
||||
def manager_only(self):
|
||||
return self.all()
|
||||
|
||||
CustomManager = BaseCustomManager.from_queryset(CustomQuerySet)
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Person(models.Model):
|
||||
first_name = models.CharField(max_length=30)
|
||||
|
@ -27,15 +70,12 @@ class Person(models.Model):
|
|||
fun = models.BooleanField()
|
||||
objects = PersonManager()
|
||||
|
||||
custom_queryset_default_manager = CustomQuerySet.as_manager()
|
||||
custom_queryset_custom_manager = CustomManager('hello')
|
||||
|
||||
def __str__(self):
|
||||
return "%s %s" % (self.first_name, self.last_name)
|
||||
|
||||
# An example of a custom manager that sets get_queryset().
|
||||
|
||||
class PublishedBookManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super(PublishedBookManager, self).get_queryset().filter(is_published=True)
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Book(models.Model):
|
||||
title = models.CharField(max_length=50)
|
||||
|
|
|
@ -11,12 +11,54 @@ class CustomManagerTests(TestCase):
|
|||
p1 = Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True)
|
||||
p2 = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False)
|
||||
|
||||
# Test a custom `Manager` method.
|
||||
self.assertQuerysetEqual(
|
||||
Person.objects.get_fun_people(), [
|
||||
"Bugs Bunny"
|
||||
],
|
||||
six.text_type
|
||||
)
|
||||
|
||||
# Test that the methods of a custom `QuerySet` are properly
|
||||
# copied onto the default `Manager`.
|
||||
for manager in ['custom_queryset_default_manager',
|
||||
'custom_queryset_custom_manager']:
|
||||
manager = getattr(Person, manager)
|
||||
|
||||
# Copy public methods.
|
||||
manager.public_method()
|
||||
# Don't copy private methods.
|
||||
with self.assertRaises(AttributeError):
|
||||
manager._private_method()
|
||||
# Copy methods with `manager=True` even if they are private.
|
||||
manager._optin_private_method()
|
||||
# Don't copy methods with `manager=False` even if they are public.
|
||||
with self.assertRaises(AttributeError):
|
||||
manager.optout_public_method()
|
||||
|
||||
# Test that the overriden method is called.
|
||||
queryset = manager.filter()
|
||||
self.assertQuerysetEqual(queryset, ["Bugs Bunny"], six.text_type)
|
||||
self.assertEqual(queryset._filter_CustomQuerySet, True)
|
||||
|
||||
# Test that specialized querysets inherit from our custom queryset.
|
||||
queryset = manager.values_list('first_name', flat=True).filter()
|
||||
self.assertEqual(list(queryset), [six.text_type("Bugs")])
|
||||
self.assertEqual(queryset._filter_CustomQuerySet, True)
|
||||
|
||||
# Test that the custom manager `__init__()` argument has been set.
|
||||
self.assertEqual(Person.custom_queryset_custom_manager.init_arg, 'hello')
|
||||
|
||||
# Test that the custom manager method is only available on the manager.
|
||||
Person.custom_queryset_custom_manager.manager_only()
|
||||
with self.assertRaises(AttributeError):
|
||||
Person.custom_queryset_custom_manager.all().manager_only()
|
||||
|
||||
# Test that the queryset method doesn't override the custom manager method.
|
||||
queryset = Person.custom_queryset_custom_manager.filter()
|
||||
self.assertQuerysetEqual(queryset, ["Bugs Bunny"], six.text_type)
|
||||
self.assertEqual(queryset._filter_CustomManager, True)
|
||||
|
||||
# The RelatedManager used on the 'books' descriptor extends the default
|
||||
# manager
|
||||
self.assertIsInstance(p2.books, PublishedBookManager)
|
||||
|
|
|
@ -90,3 +90,7 @@ class PickleabilityTestCase(TestCase):
|
|||
reloaded = pickle.loads(dumped)
|
||||
self.assertEqual(original, reloaded)
|
||||
self.assertIs(reloaded.__class__, dynclass)
|
||||
|
||||
def test_specialized_queryset(self):
|
||||
self.assert_pickles(Happening.objects.values('name'))
|
||||
self.assert_pickles(Happening.objects.values('name').dates('when', 'year'))
|
||||
|
|
Loading…
Reference in New Issue