added manager_from (thanks George Sakkis)

This commit is contained in:
Carl Meyer 2010-07-30 22:09:46 -04:00
parent 726f0de1dd
commit aed583f763
5 changed files with 118 additions and 3 deletions

View file

@ -4,6 +4,7 @@ CHANGES
tip (unreleased)
----------------
- added manager_from (thanks George Sakkis)
- added StatusField, MonitorField, TimeFramedModel, and StatusModel
(thanks Jannis Leidel)
- deprecated ChoiceEnum and replaced with Choices

View file

@ -290,3 +290,46 @@ set the ordering of the ``QuerySet`` returned by the ``QueryManager``
by chaining a call to ``.order_by()`` on the ``QueryManager`` (this is
not required).
manager_from
============
A common "gotcha" when defining methods on a custom manager class is
that those same methods are not automatically also available on the
QuerySet used by that model, so are not "chainable". This can be
counterintuitive, as most of the public QuerySet API is also available
on managers. It is possible to create a custom Manager that returns
QuerySets that have the same additional methods, but this requires
boilerplate code.
The ``manager_from`` function (`created by George Sakkis`_ and
included here by permission) solves this problem with zero
boilerplate. It creates and returns a Manager subclass with additional
behavior defined by mixin subclasses or functions you pass it, and the
returned Manager will return instances of a custom QuerySet with those
same additional methods::
from datetime import datetime
from django.db import models
class AuthorMixin(object):
def by_author(self, user):
return self.filter(user=user)
class PublishedMixin(object):
def published(self):
return self.filter(published__lte=datetime.now())
def unpublished(self):
return self.filter(published__gte=datetime.now())
class Post(models.Model):
user = models.ForeignKey(User)
published = models.DateTimeField()
objects = manager_from(AuthorMixin, PublishedMixin, unpublished)
Post.objects.published()
Post.objects.by_author(user=request.user).unpublished()
.. _created by George Sakkis: http://djangosnippets.org/snippets/2117/

View file

@ -1,4 +1,8 @@
from types import ClassType
from django.db import models
from django.db.models.manager import Manager
from django.db.models.query import QuerySet
class QueryManager(models.Manager):
def __init__(self, *args, **kwargs):
@ -17,4 +21,41 @@ class QueryManager(models.Manager):
if hasattr(self, '_order_by'):
return qs.order_by(*self._order_by)
return qs
def manager_from(*mixins, **kwds):
'''
Returns a Manager instance with extra methods, also available and
chainable on generated querysets.
(By George Sakkis, originally posted at
http://djangosnippets.org/snippets/2117/)
:param mixins: Each ``mixin`` can be either a class or a function. The
generated manager and associated queryset subclasses extend the mixin
classes and include the mixin functions (as methods).
:keyword queryset_cls: The base queryset class to extend from
(``django.db.models.query.QuerySet`` by default).
:keyword manager_cls: The base manager class to extend from
(``django.db.models.manager.Manager`` by default).
'''
bases = [kwds.get('queryset_cls', QuerySet)]
attrs = {}
for mixin in mixins:
if isinstance(mixin, (ClassType, type)):
bases.append(mixin)
else:
try: attrs[mixin.__name__] = mixin
except AttributeError:
raise TypeError('Mixin must be class or function, not %s' %
mixin.__class__)
# create the QuerySet subclass
id = hash(mixins + tuple(kwds.iteritems()))
qset_cls = type('Queryset_%d' % id, tuple(bases), attrs)
# create the Manager subclass
bases[0] = kwds.get('manager_cls', Manager)
attrs['get_query_set'] = lambda self: qset_cls(self.model, using=self._db)
manager_cls = type('Manager_%d' % id, tuple(bases), attrs)
return manager_cls()

View file

@ -2,7 +2,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from model_utils.models import InheritanceCastModel, TimeStampedModel, StatusModel, TimeFramedModel
from model_utils.managers import QueryManager
from model_utils.managers import QueryManager, manager_from
from model_utils.fields import SplitField, MonitorField
from model_utils import Choices
@ -74,3 +74,20 @@ class NoRendered(models.Model):
"""
body = SplitField(no_excerpt_field=True)
class AuthorMixin(object):
def by_author(self, name):
return self.filter(author=name)
class PublishedMixin(object):
def published(self):
return self.filter(published=True)
def unpublished(self):
return self.filter(published=False)
class Entry(models.Model):
author = models.CharField(max_length=20)
published = models.BooleanField()
objects = manager_from(AuthorMixin, PublishedMixin, unpublished)

View file

@ -10,7 +10,7 @@ from model_utils.fields import get_excerpt
from model_utils.managers import QueryManager
from model_utils.tests.models import (InheritParent, InheritChild, TimeStamp,
Post, Article, Status, StatusPlainTuple, TimeFrame, Monitored,
StatusManagerAdded, TimeFrameManagerAdded)
StatusManagerAdded, TimeFrameManagerAdded, Entry)
class GetExcerptTests(TestCase):
@ -318,3 +318,16 @@ if 'south' in settings.INSTALLED_APPS:
self.assertRaises(FieldDoesNotExist,
NoRendered._meta.get_field,
'_body_excerpt')
class ManagerFromTests(TestCase):
def setUp(self):
Entry.objects.create(author='George', published=True)
Entry.objects.create(author='George', published=False)
Entry.objects.create(author='Paul', published=True)
def test_chaining(self):
self.assertEqual(Entry.objects.by_author('George').published().count(),
1)
def test_function(self):
self.assertEqual(Entry.objects.unpublished().count(), 1)