Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using 'django-filter' with CHOICES field - need "Any" option

I'm using the very cool django-filter (via: http://github.com/alex/django-filter) and either can't seem to wrap my head around the docs, or maybe just need a little boost.

When I show the filter form on an object list page, for a FK field I get the drop down that includes a "-----" which results in an "any" type filter. But I have some choices set to a field on that model, and I'd like to get the same "any" type option. Here's a relevant example portion from models.py:

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

class Ticket(models.Model):
    assigned_to = models.ForeignKey(User, null=True, blank=True)
    status = models.CharField(max_length=20,
choices=TICKET_STATUS_CHOICES, default='new')

import django_filters

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

When I display the filter form, 'assigned_to' gets an 'any' option, as well as listing the available users. The 'status' field, however, is limited to only the options listed in the actual '_CHOICES'.

How do I add an 'any' option to the fields based on _CHOICES?

like image 251
anonymous coward Avatar asked Sep 21 '09 16:09

anonymous coward


People also ask

How does Django-filter work?

Django-filter is a generic, reusable application to alleviate writing some of the more mundane bits of view code. Specifically, it allows users to filter down a queryset based on a model's fields, displaying the form to let them do this.

What is the difference between filter and get method in Django?

Basically use get() when you want to get a single unique object, and filter() when you want to get all objects that match your lookup parameters. Show activity on this post. you will get: Pol does not exist. I.e. If you want to run some code depending on whether a single object can be found, use filter.

Can you filter by property Django?

Nope. Django filters operate at the database level, generating SQL. To filter based on Python properties, you have to load the object into Python to evaluate the property--and at that point, you've already done all the work to load it.

What does filter return in Django?

Django provides a filter() method which returns a subset of data. It accepts field names as keyword arguments and returns a QuerySet object. As database has only one record where name is 'tom' , the QuerySet object contains only a single record.


6 Answers

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']
like image 81
mpaf Avatar answered Sep 28 '22 14:09

mpaf


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!

like image 28
anonymous coward Avatar answered Sep 28 '22 15:09

anonymous coward


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', )
like image 39
Paul Bormans Avatar answered Sep 28 '22 15:09

Paul Bormans


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']
like image 26
Wim Feijen Avatar answered Sep 28 '22 13:09

Wim Feijen


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.

like image 30
Visgean Skeloru Avatar answered Sep 28 '22 13:09

Visgean Skeloru


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

    my_field = MultipleChoiceFilter(choices=MY_CHOICES, null_label='Any', null_value='')
like image 40
Shahar Gino Avatar answered Sep 28 '22 14:09

Shahar Gino