Custom filters for Django objects
Using mixins to add fluid filters to Django model managers.
There are plenty of samples on the web showing how to create custom Django
QuerySet and Manager classes to create fluid manager interfaces; here's
another one.
We have a lot of shared fields across models, that we filter on in similar
ways, irrespective of the model. A good example is state
- we use the django-fsm app, and several of our models have a state attribute that is used to manage workflow. Our codebase is then littered with .filter(state='xyz')
model manager method calls.
In order to consolidate some of these common filters, we use custom mixins that encapsulate the specifics. The key (and the thing you will see in all of the posts on the web - search for 'custom django model manager'), is how to combine the Manager and QuerySet in such a way that they are both chainable. What does this mean?
Django QuerySets are chainable - you can do the following:
>>> objs = MyModel.objects.all().filter(x=y).filter(a=b).exclude(k=j)
If you create a custom QuerySet, you can add your own filters and give them more explicit names (yes, it's a fatuous example, but it's just an example):
>>> objs = MyModel.objects.all().x_is_y().a_is_b()
The problem is that pesky all()
- the filters are on the queryset, not the manager, and so if we want to have the x_is_y()
method available on the
manager, we need to replicate the method on both Manager and QuerySet.
In order to get around this, we have a base mixin class that is used to return a reference to the underlying QuerySet when implemented by either a Manager or a QuerySet:
class FilterMixinBase():
"Base class used by Filter mixins."
def _get_query_set(self):
"Returns the underlying queryset from Manager or QuerySet object."
if issubclass(type(self), models.query.QuerySet):
return self
elif issubclass(type(self), models.Manager):
return self.get_query_set()
else:
raise Exception(
u"Object %s of type <%s> does not contain queryset."
% (self, type(self)))
This mixin is then implemented by the specific filter class, in this case a StateFilterMixin that provides a neater in_state()
filter:
class StateFilterMixin(FilterMixinBase):
"""
Mixin for custom QuerySet and Manager classes to filter on state.
This mixin should be used to provide the `in_state` and `in_states`
fluid filters to models that have a state attribute.
"""
def in_state(self, state):
"Filter requests by a state."
return self._get_query_set().filter(state=state)
def in_states(self, states):
"Filter requests by a list of states."
if states is None or states == []:
return self._get_query_set()
else:
return self._get_query_set().filter(state__in=states)
This StateFilterMixin can be implemented by both Manager and QuerySet classes to add the in_state
and in_states
methods. The only remaining task is to declare the custom classes, and to bind the Manager to the correct QuerySet:
from django.db import models
import StateFilterMixin
class MyModelQuerySet(StateFilterMixin, models.query.QuerySet):
pass
class MyModelManager(StateFilterMixin, models.Manager):
def get_query_set(self):
return MyModelQuerySet(self, using=self._db)
class MyModel(models.Model):
state = models.CharField(default='new')
objects = MyModelManager()
We can now use the custom methods without the all()
:
>>> models = MyModel.objects.all()
>>> new_models = MyModel.objects.in_state('new')
>>> old_models = MyModel.objects.in_state('old')
We've rolled this across a number of models and it's proving very helpful (assuming you think fluid interfaces are a good thing) - here's a slightly more complex example that we used to filter on the date a model was created, which for various reasons is modelled as either added
or created_at
attributes (implementation details removed for brevity - there's nothing very complicated about them):
class AddedSinceFilterMixin(FilterMixinBase):
"""
Mixin used to filter according to 'added' or 'created_at' attr.
"""
def added_today(self):
"Filter entities added today."
...
def added_yesterday(self):
"Filter entities added yesterday."
...
def added_this_week(self):
"Filter entities added this week."
...
def added_on_date(self, date):
"Filter entities added on a given date."
...
def added_after(self, date, inclusive=True):
"Filter entities added since given date."
...
def added_before(self, date, inclusive=True):
"Filter entities before a given date."
...
If we now have to filter objects created between two dates in a particular state, we have something that looks like:
>>> matches = (
... MyModel.objects
... .added_after(start_date)
... .added_before(end_date)
... .in_state(state_required)
... )
And we can added these filters to any new (or existing) models that have the requisite attributes in just a few lines of code. Simples.
Making Freelance Work
Posted in: django