17

I made a custom manager that has to randomize my query:

class RandomManager(models.Manager):

    def randomize(self):        
        count = self.aggregate(count=Count('id'))['count']
        random_index = random.randint(0, count - 1)
        return self.all()[random_index]

When I use the method defined in my manager in the first place, it's works ok:

>>> PostPages.random_objects.randomize()
>>> <PostPages: post 3>

I need to randomize the already filtered query. When I tried to use the manager and the method in chain I got an error:

PostPages.random_objects.filter(image_gallary__isnull=False).randomize()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/home/i159/workspace/shivaroot/shivablog/<ipython-input-9-98f654c77896> in <module>()
----> 1 PostPages.random_objects.filter(image_gallary__isnull=False).randomize()

AttributeError: 'QuerySet' object has no attribute 'randomize'

Result of filtering is not an instance of model class, but it's django.db.models.query.QuerySet, so that it does not have my manager and method, respectively. Is there a way to use custom manager in chain query?

I159
  • 29,741
  • 31
  • 97
  • 132

5 Answers5

31

This is how you chain custom methods on custom manager ie: Post.objects.by_author(user=request.user).published()

from django.db.models.query import QuerySet

class PostMixin(object):
    def by_author(self, user):
        return self.filter(user=user)

    def published(self):
        return self.filter(published__lte=datetime.now())

class PostQuerySet(QuerySet, PostMixin):
    pass

class PostManager(models.Manager, PostMixin):
    def get_query_set(self):
        return PostQuerySet(self.model, using=self._db)
wieczorek1990
  • 7,403
  • 1
  • 25
  • 19
zzart
  • 11,207
  • 5
  • 52
  • 47
  • This is really neat. You have all your custom methods accessible within `Manager` and `Queryset` and they are all defined in one class. To push it even further, one could define decorator to make codebase smaller. Although, I don't know how to implement it. – Tomas Tomecek Sep 10 '13 at 07:22
  • very nice way to do it – mpaf Dec 10 '13 at 08:20
  • 3
    +1 for the Django 1.7 way to do it, but shouldn't it be QuerySet.as_manager()? This should be also marked as correct answer in my opinion.. – tbolender Feb 24 '15 at 00:59
  • Shouldn't the mixin classes be on the left of the base class when specifying inheritance? – nivcaner Jul 25 '16 at 10:13
15

Just a code example using the new as_manager() method (see update information from @zzart.

class MyQuerySet(models.query.QuerySet):
    def randomize(self):        
        count = self.aggregate(count=Count('id'))['count']
        random_index = random.randint(0, count - 1)
        return self.all()[random_index]

class MyModel(models.Model):
    .....
    .....
    objects = MyQuerySet.as_manager()
    .....
    .....

And then you will be able to use something like this in your code:

MyModel.objects.filter(age__gt=16).randomize()

As you can see, the new as_manager() is really neat:)

Zhe Li
  • 1,103
  • 1
  • 14
  • 20
13

Looks like this snippet provides a solution to your situation: Custom managers with chainable filters.

arie
  • 18,737
  • 5
  • 70
  • 76
2

Given that you have an existing models.Manager and you don't want to expose some of the manager method to a chainable queryset, you can use Manager.from_queryset(QuerySet)().

So, you could still place all your chainable queryset method inside the QuerySet and your manager method independently.

Example given in the official site.

Snippet from Django Docs

class BaseManager(models.Manager):
    # Available only on Manager.
    def manager_only_method(self):
        return

class CustomQuerySet(models.QuerySet):
    # Available on both Manager and QuerySet.
    def manager_and_queryset_method(self):
        return

    # Available only on QuerySet.
    def _private_method(self):
        return

CustomManager = BaseManager.from_queryset(CustomQuerySet)

class MyModel(models.Model):
    objects = CustomManager()
Yeo
  • 11,416
  • 6
  • 63
  • 90
0

How about something like below which creates the custom QuerySet dynamically and allows us to 'transplant' our custom queries onto the returned QuerySet instance:

class OfferManager(models.Manager):
    """
    Additional methods / constants to Offer's objects manager
    """
    ### Model (db table) wide constants - we put these and 
    ### not in model definition to avoid circular imports.
    ### One can access these constants through like
    <foo>.objects.STATUS_DISABLED or ImageManager.STATUS_DISABLED

    STATUS_DISABLED = 0
    ...
    STATUS_CHOICES = (
        (STATUS_DISABLED, "Disabled"),
        (STATUS_ENABLED, "Enabled"),
        (STATUS_NEGOTIATED, "Negotiated"),
        (STATUS_ARCHIVED, "Archived"),
    )
    ...

    # we keep status and filters naming a little different as
    # it is not one-to-one mapping in all situations
    QUERYSET_PUBLIC_KWARGS = {'status__gte': STATUS_ENABLED}
    QUERYSET_ACTIVE_KWARGS = {'status': STATUS_ENABLED}

    def get_query_set(self):
        """ our customized method which transpalats manager methods
        as per get_query_set.<method_name> = <method> definitions """
        CustomizedQuerySet = QuerySet
        for name, function in self.get_query_set.__dict__.items():
            setattr(CustomizedQuerySet, name, function)
        return CustomizedQuerySet(self.model, using=self._db)

    def public(self):
        """ Returns all entries accessible through front end site"""
        return self.all().filter(**OfferManager.QUERYSET_PUBLIC_KWARGS)
    get_query_set.public = public # will tranplat the function onto the 
                                  # returned QuerySet instance which 
                                  # means 'self' changes depending on context.

    def active(self):
        """ returns offers that are open to negotiation """
        return self.public().filter(**OfferManager.QUERYSET_ACTIVE_KWARGS)
    get_query_set.active = active
    ...

More polished version of this method and django ticket here: https://code.djangoproject.com/ticket/20625.

Daniel Sokolowski
  • 11,982
  • 4
  • 69
  • 55