[Fixed]-Using 'django-filter' with CHOICES field – need "Any" option

8👍

As mentioned in the short but sweet ‘usage’ docs,

Filters also take any arbitrary keyword arguments which get passed onto the
django.forms.Field constructor.

This didn’t make a lot of sense until I looked a bit further. In the ./django-filter/docs/ref/ directory, there’s filters.txt which describes the Filter Fields and what Model Fields they interact with by default. (I think I’ve got the language right here, if not, correct me).

So, we can see that ChoiceFilter is used for any field “with choices”.

Hitting up the Django documentation, what’s important here is Form Fields, and how they interact with Models. (Using Django Forms). So we find ChoiceField (http://docs.djangoproject.com/en/dev/ref/forms/fields/#choicefield) which says

Takes one extra required argument:
ChoiceField.choices
An iterable (e.g., a list or tuple) of 2-tuples to use as choices for this field.

So you can pass it a list, not just the original Tuple choices set.

So this might not be pythonic or DRY, but here’s how I changed the model in question:

class TicketFilter(django_filters.FilterSet):
    class Meta:
        model = Ticket
        fields = ['assigned_to', 'priority', 'status']

    def __init__(self, *args, **kwargs):
        super(TicketFilter, self).__init__(*args, **kwargs)
        self.filters['priority'].extra.update(
            {
                'choices': CHOICES_FOR_PRIORITY_FILTER
            })

And above that, where my _CHOICES were defined, I defined this new one (mentioned above) and made sure to append the original choices to the end:

CHOICES_FOR_PRIORITY_FILTER = [
    ('', 'Any'),
]
CHOICES_FOR_PRIORITY_FILTER.extend(list(TICKET_PRIORITY_CHOICES))

I’m using list() here because the original Choices were set up in a tuple, so I want to turn that into a list. Also, if you’re getting a NoneType error, be sure you’re not attempting to assign the ‘return value’ of .extend(), because there isn’t one. I tripped on this, because I forgot that it was a method of a list, and not a function that returned a new list.

If you know of an easier, more DRY or “pythonic” way to do this, please let me know!

14👍

DRY’er would be to use the already defined ‘choices’ argument for ChoiceFilter.

So you could simply extend your FILTER_CHOICES to be your TICKET_STATUS_CHOICES plus an ‘any’ option with the empty string:

FILTER_CHOICES = (
    ('new', 'New'),
    ('accepted', 'Accepted'),
    ('assigned', 'Assigned'),
    ('reopened', 'Reopened'),
    ('closed', 'Closed'),
    ('', 'Any'),
)

And your TicketFilter would be:

class TicketFilter(django_filters.FilterSet):

   status = django_filters.ChoiceFilter(choices=FILTER_CHOICES)

   class Meta:
      model = Ticket
      fields = ['assigned_to']
👤mpaf

6👍

Even shorter is to use the empty label:

class TicketFilter(FilterSet):

    status = ChoiceFilter(choices=Ticket.CHOICES, empty_label=ugettext_lazy(u'Any'))

    class Meta:
        model = Ticket
        fields = ('status', )

3👍

Based on the work of anonymous coward, I did the following:

import django_filters

from .models import Experiment

EMPTY_CHOICE = ('', '---------'), # Don't forget the trailing comma

class ExperimentFilter(django_filters.FilterSet):
    class Meta:
        model = Experiment
        fields = ['method__year',]

    def __init__(self, *args, **kwargs):
        super(ExperimentFilter, self).__init__(*args, **kwargs)
        self.filters['method__year'].extra['choices'] = EMPTY_CHOICE + \
            self.filters['method__year'].extra['choices']

2👍

You can simply use null_label and null_value, e.g.

    my_field = MultipleChoiceFilter(choices=MY_CHOICES, null_label='Any', null_value='')

1👍

I combined all the methods above since I disliked specifying all the fields that need to be updated and ended up with something like this:

import django_filters

from django.db import models
from django_filters.filters import ChoiceFilter

EMPTY_CHOICE = ('', '---------')

class TicketFilter(django_filters.FilterSet):
    def __init__(self, *args, **kwargs):
        super(TicketFilter, self).__init__(*args, **kwargs)
        # add empty choice to all choice fields:
        choices = filter(
            lambda f: isinstance(self.filters[f], ChoiceFilter),
            self.filters)

        for field_name in choices:
            extended_choices = ((EMPTY_CHOICE,) +
                                self.filters[field_name].extra['choices'])
            self.filters[field_name].extra['choices'] = extended_choices

Which does the job.

Leave a comment