Login

Custom managers with chainable filters

Author:
itavor
Posted:
January 23, 2008
Language:
Python
Version:
.96
Score:
10 (after 10 ratings)

The Django docs show us how to give models a custom manager. Unfortunately, filter methods defined this way cannot be chained to each other or to standard queryset filters. Try it:

class NewsManager(models.Manager):
    def live(self):
        return self.filter(state='published')

    def interesting(self):
        return self.filter(interesting=True)

>>> NewsManager().live().interesting()
AttributeError: '_QuerySet' object has no attribute 'interesting'

So, instead of adding our new filters to the custom manager, we add them to a custom queryset. But we still want to be able to access them as methods of the manager. We could add stub methods on the manager for each new filter, calling the corresponding method on the queryset - but that would be a blatant DRY violation. A custom __getattr__ method on the manager takes care of that problem.

And now we can do:

>>> NewsManager().live().interesting()
[<NewsItem: ...>]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from django.db import models

class NewsQuerySet(models.query.QuerySet):
    def live(self):
        return self.filter(state='published')

    def interesting(self):
        return self.filter(interesting=True)

class NewsManager(models.Manager):
    def get_query_set(self): 
        model = models.get_model('news', 'NewsItem')
        return NewsQuerySet(model)

    def __getattr__(self, attr, *args):
        try:
            return getattr(self.__class__, attr, *args)
        except AttributeError:
            return getattr(self.get_query_set(), attr, *args)

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
  5. Help text hyperlinks by sa2812 1 year, 8 months ago

Comments

ep (on January 26, 2008):

Thanks, looks excellent!

I think you can even avoid repeating the Manager code for each of your classes by using something like:

class QuerySetManager(models.Manager):
    def __init__(self, qs_class):
        self.queryset_class = qs_class
    def get_query_set(self):
        return self.queryset_class(self.model)
    def __getattr__(self, attr, *args):
        try:
            return getattr(self.__class__, attr, *args)
        except AttributeError:
            return getattr(self.get_query_set(), attr, *args)

and then setting the objects attribute like this:

class NewsItem(models.Model):
    objects = QuerySetManager(NewsQuerySet)

#

itavor (on February 1, 2008):

Thanks, ep! I like your improvement. Will do it that way in my own code from now on.

#

herion (on April 20, 2008):

I tried using this. In ep's approach Manager's init is overriden and it expects an additional argument compared to Django core's Manager. However, in Django code Manager's are called without arguments. So when using object.delete() and object.someothermodel_set.* methods the system tries to call the Manager without arguments and it throws an error. I don't know however, why Django is calling Manager via the Manager model and not thourgh models Manager instance (so it doesn't help if you make argument optional as class can't find the queryset then).. I found ep's approach much too hazard although you could hack it here and there..

#

whiteinge (on July 13, 2008):

herion, just use a default for the qs_class:

def __init__(self, qs_class=models.query.QuerySet):
    self.queryset_class = qs_class

#

leovitch (on December 1, 2008):

Thanks for this snippet, it makes for really clean extensions to the Query API.

ep, don't you need to be calling super's __init__ in your improvement?

#

marcoslhc (on February 11, 2010):

Excellent. Althougt leovitch is right. you need to call the super init. The code will look like this:

class CustomManager(models.Manager):
def __init__(self, qs_class=models.query.QuerySet):
    super(CustomManager,self).__init__()
    self.queryset_class = qs_class

def get_query_set(self):
    return self.queryset_class(self.model)

def __getattr__(self, attr, *args):
    try:
        return getattr(self.__class__, attr, *args)
    except AttributeError:
        return getattr(self.get_query_set(), attr, *args)

I recommend this piece since is reusable and complies better with DRY

#

anentropic (on March 12, 2010):

There's something horribly wrong if you try and inherit of the CustomManager though...

I get errors like: site-packages/django/db/models/options.py", line 489, in pk_index return self.fields.index(self.pk)

ValueError: list.index(x): x not in list

#

anentropic (on March 12, 2010):

I find this works fine though, hopefully I won't burn in hell for the __class__ trick!

class BaseManager(models.Manager):
    def __init__(self, qs_class=None):
        super(BaseManager, self).__init__()
        self.queryset_class = qs_class

    def get_query_set(self):
        qs = super(BaseManager, self).get_query_set()
        if self.queryset_class:
            qs.__class__ = self.queryset_class
        return qs

class SpecificCustomManager(BaseManager):
    def get_query_set(self):
        qs = super(SpecificCustomManager, self).get_query_set()
        return qs.filter(some custom filters)

class MyModel(Model):
    objects = SpecificCustomManager(CustomQuerySet)

#

pkoch (on March 23, 2010):

My solution:

def generate_chainer_manager(qs_class):
    class ChainerManager(models.Manager):
        def __init__(self):
            super(ChainerManager,self).__init__()
            self.queryset_class = qs_class

        def get_query_set(self):
            return self.queryset_class(self.model)

        def __getattr__(self, attr, *args):
            try:
                return getattr(self.__class__, attr, *args)
            except AttributeError:
                return getattr(self.get_query_set(), attr, *args)

    return ChainerManager()

class MyModel(Model):
    objects = generate_chainer_manager(CustomQuerySet)

Doesn't use the kwarg hack and it's still very self contained.

#

glic3rinu (on June 8, 2011):

pkoch, your solution seems the best for me since it allows use the chained manager even with reverse relations like:

mymodel.myothermodel_set.mycustomfilter

Thanks :)

#

fission6 (on May 3, 2012):

here is a full gist with an example https://gist.github.com/2587518

#

jwgcarlson (on August 25, 2012):

The solutions listed here didn't work for me when using a custom manager on an abstract base model. This is what I'm using instead:

from django.db import models
class CustomQuerySetManager(models.Manager):
    use_for_related_fields = True

    def __init__(self, qs_class=models.query.QuerySet):
        super(CustomQuerySetManager, self).__init__()
        self.queryset_class = qs_class
        self.custom_methods = [a for a in qs_class.__dict__ if not a.startswith('_')]

    def get_query_set(self):
        return self.queryset_class(self.model)

    def __getattr__(self, attr, *args):
        if attr in self.custom_methods:
            return getattr(self.get_query_set(), attr, *args)
        else:
            return getattr(self.__class__, attr, *args)

Usage is exactly like the others:

class MyQuerySet(models.query.QuerySet):
    def upcoming(self):
        return self.filter(date__gt=datetime.now())

class MyModel(models.Model):
    date = models.DateTimeField()
    objects = CustomQuerySetManager(MyQuerySet)

This has the added bonus that it only proxies the methods you add in your custom QuerySet class (in the example only the 'upcoming' method). So (for instance) MyModel.objects.delete() fails as it's supposed to.

#

Please login first before commenting.