From c624d7a301a6644f6faf33eb88000582855abe1f Mon Sep 17 00:00:00 2001 From: Paul McLanahan Date: Tue, 8 Mar 2011 13:52:32 -0500 Subject: [PATCH 1/3] Added PassThroughManager to managers. Added me to authors. --- AUTHORS.rst | 1 + model_utils/managers.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index a5da196..ae3eecf 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -2,3 +2,4 @@ Carl Meyer Jannis Leidel Gregor Müllegger Jeff Elmore +Paul McLanahan diff --git a/model_utils/managers.py b/model_utils/managers.py index 7b764dd..47e7fbb 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -81,6 +81,42 @@ class QueryManager(models.Manager): return qs +class PassThroughManager(models.Manager): + ''' + Inherit from this Manager to enable you to call any methods from your + custom QuerySet class from your manager. Simply define your QuerySet + class, and return an instance of it from your manager's `get_query_set` + method. + + Alternately, if you don't need any extra methods on your manager that + aren't on your QuerySet, then just pass your QuerySet class to this + class' constructer. + + class PostQuerySet(QuerySet): + def enabled(self): + return self.filter(disabled=False) + + class Post(models.Model): + objects = PassThroughManager(PostQuerySet) + ''' + # pickling causes recursion errors + _deny_methods = ['__getstate__', '__setstate__'] + + def __init__(self, queryset_cls=None): + self._queryset_cls = queryset_cls + super(PassthroughManager, self).__init__() + + def __getattr__(self, name): + if name in self._deny_methods: + raise AttributeError(name) + return getattr(self.get_query_set(), name) + + def get_query_set(self): + if self._queryset_cls is not None: + return self._queryset_cls(self.model, using=self._db) + return super(PassThroughManager, self).get_query_set() + + def manager_from(*mixins, **kwds): ''' Returns a Manager instance with extra methods, also available and From a2c8e4122573d6f438ac0d15995d2f0ad6ca3018 Mon Sep 17 00:00:00 2001 From: Paul McLanahan Date: Wed, 9 Mar 2011 13:47:44 -0500 Subject: [PATCH 2/3] Added usage info for PassThroughManager to the README. --- README.rst | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/README.rst b/README.rst index c3e9857..beed371 100644 --- a/README.rst +++ b/README.rst @@ -392,3 +392,86 @@ same additional methods:: .. _created by George Sakkis: http://djangosnippets.org/snippets/2117/ +PassThroughManager +================== + +The ``PassThroughManager`` class (`contributed by Paul McLanahan`_) solves +the same problem as the above ``manager_from`` function. This class, however, +accomplishes it in a different way. The reason it exsts is that the dynamically +generated ``QuerySet`` classes created by the ``manager_from`` function are +not picklable. It's probably not often that a ``QuerySet`` is pickled, but +it is a documented feature of the Django ``QuerySet`` class, and this method +maintains that functionality. + +``PassThroughManager`` is a subclass of ``django.db.models.manager.Manager``, +so all that is required is that you change your custom managers to inherit from +``PassThroughManager`` instead of Django's built-in ``Manager`` class. Once you +do this, create your custom ``QuerySet`` class, and have your manager's +``get_query_set`` method return instances of said class, then all of the +methods you add to your custom ``QuerySet`` class will be avilable from your +manager as well:: + + from datetime import datetime + from django.db import models + from django.db.models.query import QuerySet + + class PostQuerySet(QuerySet): + def by_author(self, user): + return self.filter(user=user) + + def published(self): + return self.filter(published__lte=datetime.now()) + + def unpublished(self): + return self.filter(published__gte=datetime.now()) + + class PostManager(PassThroughManager): + def get_query_set(self): + PostQuerySet(self.model, using=self._db) + + def get_stats(self): + return { + 'published_count': self.published().count(), + 'unpublished_count': self.unpublished().count(), + } + + class Post(models.Model): + user = models.ForeignKey(User) + published = models.DateTimeField() + + objects = PostManager() + + Post.objects.get_stats() + Post.objects.published() + Post.objects.by_author(user=request.user).unpublished() + +Alternatively, if you don't need any methods on your manager that shouldn't also +be on your queryset, a shortcut is available. ``PassThroughManager``'s +constructor takes an optional argument. If you pass it a ``QuerySet`` subclass +it will automatically use that class when creating querysets for the manager:: + + from datetime import datetime + from django.db import models + from django.db.models.query import QuerySet + + class PostQuerySet(QuerySet): + def by_author(self, user): + return self.filter(user=user) + + 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 = PassThroughManager(PostQuerySet) + + Post.objects.published() + Post.objects.by_author(user=request.user).unpublished() + +.. _contributed by Paul McLanahan: http://paulm.us/post/3717466639/passthroughmanager-for-django From 04c848e8c729e190f12cc1bd943de9d28fbecf1d Mon Sep 17 00:00:00 2001 From: Paul McLanahan Date: Wed, 9 Mar 2011 14:44:10 -0500 Subject: [PATCH 3/3] Added tests for PassThroughManager. --- model_utils/managers.py | 2 +- model_utils/tests/models.py | 33 ++++++++++++++++++++++++++++- model_utils/tests/tests.py | 42 ++++++++++++++++++++++++++++++++++++- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/model_utils/managers.py b/model_utils/managers.py index 47e7fbb..1f51d9d 100644 --- a/model_utils/managers.py +++ b/model_utils/managers.py @@ -104,7 +104,7 @@ class PassThroughManager(models.Manager): def __init__(self, queryset_cls=None): self._queryset_cls = queryset_cls - super(PassthroughManager, self).__init__() + super(PassThroughManager, self).__init__() def __getattr__(self, name): if name in self._deny_methods: diff --git a/model_utils/tests/models.py b/model_utils/tests/models.py index 5e63597..a84e1cc 100644 --- a/model_utils/tests/models.py +++ b/model_utils/tests/models.py @@ -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, manager_from, InheritanceManager +from model_utils.managers import QueryManager, manager_from, InheritanceManager, PassThroughManager from model_utils.fields import SplitField, MonitorField from model_utils import Choices @@ -115,3 +115,34 @@ class Entry(models.Model): featured = manager_from(PublishedMixin, manager_cls=FeaturedManager, queryset_cls=ByAuthorQuerySet) + +class DudeQuerySet(models.query.QuerySet): + def abiding(self): + return self.filter(abides=True) + + def rug_positive(self): + return self.filter(has_rug=True) + + def rug_negative(self): + return self.filter(has_rug=False) + + def by_name(self, name): + return self.filter(name__iexact=name) + +class AbidingManager(PassThroughManager): + def get_query_set(self): + return DudeQuerySet(self.model, using=self._db).abiding() + + def get_stats(self): + return { + 'abiding_count': self.count(), + 'rug_count': self.rug_positive().count(), + } + +class Dude(models.Model): + abides = models.BooleanField(default=True) + name = models.CharField(max_length=20) + has_rug = models.BooleanField() + + objects = PassThroughManager(DudeQuerySet) + abiders = AbidingManager() diff --git a/model_utils/tests/tests.py b/model_utils/tests/tests.py index d073f64..d2733f3 100644 --- a/model_utils/tests/tests.py +++ b/model_utils/tests/tests.py @@ -1,3 +1,8 @@ +try: + import cPickle as pickle +except ImportError: + import pickle + from datetime import datetime, timedelta from django.test import TestCase @@ -16,7 +21,7 @@ from model_utils.tests.models import ( InheritParent, InheritChild, InheritChild2, InheritanceManagerTestParent, InheritanceManagerTestChild1, InheritanceManagerTestChild2, TimeStamp, Post, Article, Status, StatusPlainTuple, TimeFrame, Monitored, - StatusManagerAdded, TimeFrameManagerAdded, Entry) + StatusManagerAdded, TimeFrameManagerAdded, Entry, Dude) class GetExcerptTests(TestCase): @@ -408,6 +413,7 @@ if 'south' in settings.INSTALLED_APPS: NoRendered._meta.get_field, '_body_excerpt') + class ManagerFromTests(TestCase): def setUp(self): Entry.objects.create(author='George', published=True) @@ -429,3 +435,37 @@ class ManagerFromTests(TestCase): def test_cant_reconcile_qs_class(self): self.assertRaises(TypeError, Entry.broken.all) + + def test_queryset_pickling_fails(self): + qs = Entry.objects.all() + def dump_load(): + pqs = pickle.dumps(qs) + upqs = pickle.loads(pqs) + self.assertRaises(pickle.PicklingError, dump_load) + + +class PassThroughManagerTests(TestCase): + def setUp(self): + Dude.objects.create(name='The Dude', abides=True, has_rug=False) + Dude.objects.create(name='His Dudeness', abides=False, has_rug=True) + Dude.objects.create(name='Duder', abides=False, has_rug=False) + Dude.objects.create(name='El Duderino', abides=True, has_rug=True) + + def test_chaining(self): + self.assertEqual(Dude.objects.by_name('Duder').count(), 1) + self.assertEqual(Dude.objects.all().by_name('Duder').count(), 1) + self.assertEqual(Dude.abiders.rug_positive().count(), 1) + self.assertEqual(Dude.abiders.all().rug_positive().count(), 1) + + def test_manager_only_methods(self): + stats = Dude.abiders.get_stats() + self.assertEqual(stats['rug_count'], 1) + def notonqs(): + Dude.abiders.all().get_stats() + self.assertRaises(AttributeError, notonqs) + + def test_queryset_pickling(self): + qs = Dude.objects.all() + saltyqs = pickle.dumps(qs) + unqs = pickle.loads(saltyqs) + self.assertEqual(unqs.by_name('The Dude').count(), 1)