Ana Balica

Hi, I'm Ana.

I'm a software developer. I mostly do Python. This blog is about my adventures with code, travel experiences and relevant life events. I try not to take myself too seriously.

Here's what I'm doing now.

Occasionally I give talks.

Please don't take my words for granted, because Internet is full of bad advice, and I might be part of it inadvertently.

How to filter by DateRange with django-filters

Django offers a few fields that are exclusive to Postgres. I recently had to use DateRangeField which is an interesting alternative to doing the usual start_date and end_date.

If you’re using Django Rest Framework, you are probably familiar with django-filters. It’s a generic system for filtering Django QuerySets, though I’ve mostly used it in combination with DRF.

Timestamped

Tested with following versions:

  • Django 1.11 (confident this will work with Django 2.1)
  • django-filters 2.0.0 (won’t work with previous versions, because 2.0 release includes a reworked version of RangeWidget)

Requirements

Ranges are composed of lower and upper bounds that operate as a whole value. I want to be able to specify both bounds when filtering and get the objects that satisfy these exact values. By default, Django stores DateRangeField in its canonical form [) - lower inclusive and upper exclusive. In this case, I’m fine to specify and filter the bounds only in their canonical form. But it should be fairly easy to extend this to support different range types.

From the filters list I spot DateRangeFilter and DateFromToRangeFilter. DateRangeFilter defines a list of choices to select from (like today, yesterday past week, etc), not exactly what I’m looking for. DateFromToRangeFilter looks more appealing, as it allows filtering using the _after and _before suffixes. It will conveniently convert the passed strings to DateField and do gte and lte lookups on the field. Still not exactly what I need.

Show me the code

import django_filters
from psycopg2.extras import DateRange


class DateExactRangeWidget(django_filters.widgets.DateRangeWidget):
    """Date widget to help filter by *_start and *_end."""
    suffixes = ['start', 'end']


class DateExactRangeField(django_filters.fields.DateRangeField):
    widget = DateExactRangeWidget

    def compress(self, data_list):
        if data_list:
            start_date, stop_date = data_list
            return DateRange(start_date, stop_date)


class DateExactRangeFilter(django_filters.Filter):
    """
    Filter to be used for Postgres specific Django field - DateRangeField.
    https://docs.djangoproject.com/en/2.1/ref/contrib/postgres/fields/#daterangefield
    """
    field_class = DateExactRangeField

In your app:

import django_filters

from .filters import DateExactRangeFilter
from .models import Event


class EventFilter(django_filters.rest_framework.FilterSet):
    event_dates = DateExactRangeFilter()

    class Meta:
        model = Event
        fields = [
            'event_dates',
        ]

Explanation

Let’s start from the bottom - DateExactRangeFilter. Since we are doing a regular lookup, we are inheriting directly from Filter. The field_class here is important, as it converts the raw values to a format and type we need, in this case DateRange composed of start and end dates. The DateExactRangeWidget lists the available suffixes.

Now you can do this /api/events?event_dates_start=2018-08-07&event_dates_end=2018-08-12 to filter all events that start and end exactly on these dates.

Warning

This solution is not ideal, because none of this documented. Django-filters doesn’t provide a guide on how to extend existing classes to add your own filters, fields or widgets. The details of how these objects behave might change with time, hence use this at your own risk.