Django-filter and custom querysets

Django-filter is a powerful tool, but the documentation is a little sparse. If you want to see examples of custom Filters you have to dive into the source code.

I recently wanted to add a filter for methods on a custom QuerySet. Unlike custom managers, custom QuerySets allow you to chain methods. You can read this introduction or refer to the official documentation (at the time of this writing 1.7 wasn't released yet). Here is a short example of what's possible:

Product.products.in_stock().price_below(100).has_color('red')

You get the idea, it's just a convenient way to write shorter code.

So I had my methods and wanted to use them with django-filter, but it took a while to figure out how. After some digging I took the DateRangeField class as a blueprint (0.7 source) and came up with this filter:

class QuerySetFilter(django_filters.ChoiceFilter):
    def __init__(self, options, *args, **kwargs):
        self.options = options
        kwargs['choices'] = [
            (key, value[0]) for key, value in six.iteritems(self.options)]
        super(QuerySetFilter, self).__init__(*args, **kwargs)

    def filter(self, qs, value):
        config = self.options[value][1]
        method = config.get('method', '')
        args = config.get('args', ())
        kwargs = config.get('kwargs', {})
        if method == '':
            return qs
        elif not hasattr(qs, method):
            raise Exception("Improperly configured", "Unknown QuerySet method %s" % method)
        return getattr(qs, method)(*args, **kwargs)

To use this QuerySetFilter you can create a FilterSet that is configured like this:

class ProductFilterSet(django_filters.FilterSet):
    product_choices = {
        '': ('---------', ''),
        'fresh': ('Fresh', {
            'method': 'age_group'
            'args': ('fresh', ),
        }),
        'regular': ('Regular', {
            'method': 'age_group',
            'args': ('regular', ),
        }),
        'old': ('Old', {
            'method': 'age_group',
            'args': ('old', ),
        }),
    }
    product_group = QuerySetFilter(product_choices, label='Product age choices')

    class Meta:
        model = Product
        fields = ['product_group', ]

The configuration dict is made of key-choice pairs. Each choice is a two-tuple of label-queryconfig. The queryconfig should define a method key, which is the QuerySet method that will be used. Optional args and kwargs values are passed to the method call.

This can be quite useful, but keep in mind that you can often accomplish what you need with the Filters built into django-filter. However, this can be a useful approach if you aready have your query logic and want to avoid code duplication. In some cases you can also avoid having to create complex forms on the frontend, which was my initial motivation.

4 comments

  1. avatar
    wrote this comment on
    (key, value[0]) for key, value in six.iteritems(self.options)]

    What is "six"? in six.iteritems?
  2. avatar
    wrote this comment on
    https://pythonhosted.org/six/
  3. avatar
    wrote this comment on
    Thanks a lot, it was very helpful
  4. avatar
    wrote this comment on
    it is possible to filter with to model at the same time ?

Reply

Cancel reply
Markdown. Syntax highlighting with <code lang="php"><?php echo "Hello, world!"; ?></code> etc.
DjangoPythonBitcoinTuxDebianHTML5 badgeSaltStackUpset confused bugMoneyHackerUpset confused bugX.OrggitFirefoxWindowMakerBashIs it worth the time?i3 window managerWagtailContainerIrssiNginxSilenceUse a maskWorldInternet securityPianoFontGnuPGThunderbirdJenkinshome-assistant-logo