Filter by custom QuerySet of matching model in Django

Let's say I have two models: Book

andAuthor

class Author(models.Model):
    name = models.CharField()
    country = models.CharField()
    approved = models.BooleanField()


class Book(models.Model):
    title = models.CharField()
    approved = models.BooleanField()
    author = models.ForeignKey(Author)

      

Each of the two models has an attribute approved

that shows or hides the object from the website. If Book

not approved, it is hidden. If Author

not approved, all his books are hidden.

To define these criteria in a dry manner, creating a custom QuerySet looks like the ideal solution:

class AuthorQuerySet(models.query.QuerySet):
    def for_site():
        return self.filter(approved=True)

class BookQuerySet(models.query.QuerySet):
    def for_site():
        reuturn self.filter(approved=True).filter(author__approved=True)

      

Once these QuerysSets are connected to the appropriate models, they can be queried like this Book.objects.for_site()

, without having to hard-code all filtering every time.


However, this solution is still not perfect. Later I may decide to add another filter to authors:

class AuthorQuerySet(models.query.QuerySet):
    def for_site():
        return self.filter(approved=True).exclude(country='Problematic Country')

      

but this new filter will only work in Author.objects.for_site()

, but not in Book.objects.for_site()

, as it is hardcoded there.


So my questions are, it is possible to apply a custom queryset of a related model while filtering on another model so that it looks something like this:

class BookQuerySet(models.query.QuerySet):
    def for_site():
        reuturn self.filter(approved=True).filter(author__for_site=True)

      

where for_site

is the custom QuerySet of the model Author

.

+3


source to share


1 answer


I think I came up with a solution based on objects Q

that are described in the official documentation. This is definitely not the most elegant solution you can think of, but it works. See code below.

from django.db import models
from django.db.models import Q


######## Custom querysets
class QuerySetRelated(models.query.QuerySet):
    """Queryset that can be applied in filters on related models"""

    @classmethod
    def _qq(cls, q, related_name):
        """Returns a Q object or a QuerySet filtered with the Q object, prepending fields with the related_name if specified"""
        if not related_name:
            # Returning Q object without changes
            return q
        # Recursively updating keywords in this and nested Q objects
        for i_child in range(len(q.children)):
            child = q.children[i_child]
            if isinstance(child, Q):
                q.children[i_child] = cls._qq(child, related_name)
            else:
                q.children[i_child] = ('__'.join([related_name, child[0]]), child[1])
        return q


class AuthorQuerySet(QuerySetRelated):

    @classmethod
    def for_site_q(cls, q_prefix=None):
        q = Q(approved=True)
        q = q & ~Q(country='Problematic Country')
        return cls._qq(q, q_prefix)


    def for_site(self):
        return self.filter(self.for_site_q())


class BookQuerySet(QuerySetRelated):

    @classmethod
    def for_site_q(cls, q_prefix=None):
        q = Q(approved=True) & AuthorQuerySet.for_site_q('author')
        return cls._qq(q, q_prefix)


    def for_site(self):
        return self.filter(self.for_site_q())



######## Models
class Author(models.Model):
    name = models.CharField(max_length=255)
    country = models.CharField(max_length=255)
    approved = models.BooleanField()

    objects = AuthorQuerySet.as_manager()


class Book(models.Model):
    title = models.CharField(max_length=255)
    approved = models.BooleanField()
    author = models.ForeignKey(Author)

    objects = BookQuerySet.as_manager()

      

This way, whenever the method AuthorQuerySet.for_site_q()

changes, it will be automatically reflected in the method BookQuerySet.for_site()

.

Here custom classes QuerySet

perform class-level selection by combining different objects Q

instead of using methods filter()

or exclude()

at the object level. Having an object Q

allows three different ways to use it:

  • put it inside the call filter()

    to filter the set of queries in place;
  • combine it with other objects Q

    using operators & (AND)

    or | (OR)

    ;
  • dynamically change the names of keywords used in objects Q

    by referring to its attribute children

    , which is defined in the superclassdjango.utils.tree.Node

The method _qq()

defined in each custom class QuerySet

takes care of adding the specified related_name

to all filter keys.

If we have an object q = Q(approved=True)

, we can have the following outputs:

  • self._qq(q)

    - equivalent self.filter(approved=True)

    ;
  • self._qq(q, 'author')

    - equivalent self.filter(author__approved=True)




This solution still has serious drawbacks:

  • you must import and call your own class of the QuerySet

    corresponding model explicitly
  • for each filtering method, two methods filter_q

    (class method) and filter

    (instance method) must be defined ;

UPDATE: The disadvantage 2.

can be partially mitigated by creating dynamic filtering methods:

# in class QuerySetRelated
    @classmethod
    def add_filters(cls, names):
        for name in names:
            method_q = getattr(cls, '{0:s}_q'.format(name))
            def function(self, *args, **kwargs):
                return self.filter(method_q(*args, **kwargs))
            setattr(cls, name, function)

AuthorQuerySet.add_filters(['for_site'])
BookQuerySet.add_filters(['for_site'])

      


So if anyone comes up with a more elegant solution, please suggest one. It would be very grateful.

0


source







All Articles