Examples

Using a Simple Filter

Here Filternaut pulls a username from user_data and filters a User queryset with it. Filternaut is overkill for such a simple job, but hey, it’s the first example.

from filternaut import Filter

user_data = {'username': 'nostromo'}
filters = Filter('username')
filters.parse(user_data)
print(User.objects.filter(filters.Q).query)
SELECT "auth_user"."id", ...
WHERE "auth_user"."username" = nostromo

Using Lookups

It’s common to require comparisons such as greater than, less than, etc. against one field. You can provide a lookups argument to specify these.

from filternaut.filters import Filter

filters = Filter('username', lookups=['icontains', 'contains'])
filters.parse({'username__icontains': 'nostromo'})
print(User.objects.filter(filters.Q).query)
SELECT "auth_user"."id", ...
WHERE "auth_user"."username" LIKE %nostromo%...

The default comparison is ‘exact’, which is the equivalent of using no comparison affix when filtering with Django’s ORM. To keep the ‘exact’ comparison when explicitly listing lookups, you must add 'exact' to the list:

filters = Filter('last_login', lookups=['year', 'exact'])

If you provide only one lookup, the source data can optionally omit the lookup from the name of the key:

from filternaut.filters import Filter

filters = Filter('email', lookups=['iexact'])

# the lookup can be omitted...
filters.parse({'email': 'Sentence@Case.COM'})

# ...or included
filters.parse({'email__iexact': 'Sentence@Case.COM'})

Combining Several Filters

Now Filternaut is used to filter Users with either an email, a username, or a (first name, last name) pair.

from filternaut import Filter

user_data = {'email': 'user3@example.org', 'username': 'user3'}
filters = (
    Filter('email') |
    Filter('username') |
    (Filter('first_name') & Filter('last_name')))
filters.parse(user_data)
print(User.objects.filter(filters.Q).query)
SELECT "auth_user"."id", ...
WHERE ("auth_user"."email" = user3@example.org OR
       "auth_user"."username" = user3)

The same filters generate result in different SQL when given different input data:

user_data = {'first_name': 'Art', 'last_name': 'Vandelay'}
filters.parse(user_data)
print(User.objects.filter(filters.Q).query)
SELECT "auth_user"."id", ...
WHERE ("auth_user"."first_name" = Art AND
       "auth_user"."last_name" = Vandelay)

Filtering with Multiple Values

Using the lookup __in triggers the collection of multiple values from the source. If this is the case, the source must provide a getlist method. Django’s QueryDict provides such a method.

from filternaut.filters import Filter
from django.utils.datastructures import MultiValueDict

filters = Filter(source='groups', dest='groups__name', lookups=['in'])
user_data = MultiValueDict({'groups': ['foo', 'bar']})
filters.parse(user_data)
print(User.objects.filter(filters.Q).query)
SELECT "auth_user"."id", ...
WHERE "auth_group"."name" IN (foo, bar)

If the source does not provide a getlist method Filternaut will fall back to a single value, but still deliver it as a list.

Mapping a Different Public API onto your Schema.

In this example, the source data’s last_transaction value filters on the value of a field across a distant relationship. This allows you to simplify or hide the details of your schema, and to later change them without changing the names you expose.

from filternaut import Filter
filters = Filter(
    source='last_payment',
    dest='order__transaction__created_date',
    lookups=['lt', 'lte', 'gt', 'gte'])

Default Values for Filters

Filters can be given default values.

from filternaut import Filter
filters = Filter('is_active', default=True)
filters.parse({})  # no 'is_active'
print(User.objects.filter(filters.Q).query)
SELECT "auth_user"."id", ...
WHERE "auth_user"."is_active" = True

When a default value is used, lookups are ignored. Most combinations of lookups are mutually exclusive when comparing the same value. For example, filtering by score__lt=3 and score__gt=3 does not make any sense. Instead, a lookup of exact is used. default_lookup may be used to override this.

from datetime import datetime
from filternaut import Filter
filters = Filter('last_login', lookups=['lte', 'lt', 'gt', 'gte'],
                 default=datetime.now(), default_lookup='lte')
filters.parse({})  # no 'last_login'
print(User.objects.filter(filters.Q).query)
SELECT "auth_user"."id", ...
WHERE "auth_user"."last_login" <= ...

Requiring Certain Filters

If it’s mandatory to provide certain filtering values, you can use the required argument. By default, filters are not required.

from filternaut import Filter

filters = Filter('username', required=True)
filters.parse({})  # no 'username'

print(filters.valid)
print(filters.errors)
False
{'username': ['This field is required']}

Conditional Requirements

Sometimes a field is required only when another is present. For example, you may say that a value for last_name must be accompanied by a value for first_name, whilst also allowing neither. Additionally, you may say that a value for middle_name requires values for first_name and last_name, but not vice versa. That example is illustrated here:

from filternaut import Filter, Optional

filters = (
   Optional(
      Filter('first_name', required=True),
      Filter('middle_name'),
      Filter('last_name', required=True)) &
   Filter('badgers_defeated'))

filters.parse({'first_name': 'Nostromo'})
print(filters.errors['__all__'])
print(filters.errors['last_name'])
['If any of first_name, last_name, middle_name are provided,
  all must be provided']
['This field is required']

Though the middle name filter isn’t required itself, when present it triggers the requirement of the filters that are.

filters.parse({'middle_name': 'Boone'})
print(filters.errors['__all__'])
print(filters.errors['first_name'])
print(filters.errors['last_name'])
['If any of first_name, last_name, middle_name are provided,
  all must be provided']
['This field is required']
['This field is required']

When all required filters in an Optional group are present, the filters as a whole are valid.

filters.parse({
   'first_name': 'Nostromo',
   'last_name': 'Cheradenine'})
assert filters.valid

Similarly, when none of the filters in an Optional group are present, the filters as a whole are valid.

filters.parse({})
assert filters.valid

Validating and Transforming Source Data

Filters can be combined with django.forms.fields.Field instances to validate and transform source data.

from django.forms import DateTimeField
from filternaut.filters import FieldFilter

filters = FieldFilter('signup_date', field=DateTimeField())
filters.parse({'signup_date': 'potato'})

print(filters.valid)
print(filters.errors)
False
{'signup_date': ['Enter a valid date/time.']}

Instead of making you provide your own field argument, Filternaut pairs most of Django’s Field subclasses with Filters. They can be used like so:

from filternaut.filters import ChoiceFilter

difficulties = [(4, 'Torment I'), (5, 'Torment II')]
filters = ChoiceFilter('difficulty', choices=difficulties)
filters.parse({'difficulty': 'foo'})

print(filters.valid)
print(filters.errors)
False
{'difficulty': ['Select a valid choice. foo is not ...']}

Filters wrapping fields which require special arguments to instantiate (e.g. choices in the example above) also require those arguments. That is, because ChoiceField needs choices, so does ChoiceFilter.

The full list of field-specific filter classes is:

  • BooleanFilter
  • CharFilter
  • ChoiceFilter
  • ComboFilter
  • DateFilter
  • DateTimeFilter
  • DecimalFilter
  • EmailFilter
  • FilePathFilter
  • FloatFilter
  • GenericIPAddressFilter (Django 1.4 and greater)
  • IPAddressFilter
  • ImageFilter
  • FieldFilter
  • IntegerFilter
  • MultiValueFilter
  • MultipleChoiceFilter
  • NullBooleanFilter
  • RegexFilter
  • SlugFilter
  • SplitDateTimeFilter
  • TimeFilter
  • TypedChoiceFilter
  • TypedMultipleChoiceFilter (Django 1.4 and greater)
  • URLFilter

Django REST Framework

Using Filternaut with Django REST Framework is no more complicated than normal; simply connect, for example, a request’s query parameters to a view’s queryset:

from filternaut.filters import CharFilter, EmailFilter
from rest_framework import generics

class UserListView(generics.ListAPIView):
    model = User

    def filter_queryset(self, queryset):
        filters = CharFilter('username') | EmailFilter('email')
        filters.parse(self.request.query_params)
        queryset = super(UserListView, self).filter_queryset(queryset)
        return queryset.filter(filters.Q)

Filternaut also provides a Django REST Framework-compatible filter backend:

from filternaut.drf import FilternautBackend
from filternaut.filters import CharFilter, EmailFilter
from rest_framework import views

class MyView(views.APIView):
    filter_backends = (FilternautBackend, )
    filternaut_filters = CharFilter('username') | EmailFilter('email')

The attribute filternaut_filters should contain one or more Filter instances. Instead of an attribute, it can also be a callable which returns a list of filters, allowing the filters to vary on the current request:

from rest_framework import views

class MyView(views.APIView):
    filter_backends = (FilternautBackend, )

    def filternaut_filters(self, request):
        choices = ['guest', 'developer']
        if request.user.is_staff:
            choices.append('manager')
        return ChoiceFilter('account_type', choices=enumerate(choices))