sghael
sghael

Reputation: 1287

Custom Filter in Django Admin on Django 1.3 or below

How can I add a custom filter to django admin (the filters that appear on the right side of a model dashboard)? I know its easy to include a filter based on a field of that model, but what about a "calculated" field like this:

class NewsItem(models.Model):
    headline = models.CharField(max_length=4096, blank=False)
    byline_1 = models.CharField(max_length=4096, blank=True)
    dateline = models.DateTimeField(help_text=_("date/time that appears on article"))
    body_copy = models.TextField(blank=False)

    when_to_publish = models.DateTimeField(verbose_name="When to publish",  blank=True, null=True)

    # HOW CAN I HAVE "is_live" as part of the admin filter?  It's a calculated state!!
    def is_live(self):
        if self.when_to_publish is not None:
            if ( self.when_to_publish < datetime.now() ):
                return """ <img alt="True" src="/media/img/admin/icon-yes.gif"/> """
        else:
            return """ <img alt="False" src="/media/img/admin/icon-no.gif"/> """      

    is_live.allow_tags = True

class NewsItemAdmin(admin.ModelAdmin):
    form = NewsItemAdminForm
    list_display = ('headline', 'id', 'is_live')
    list_filter = ('is_live')  #  how can i make this work??

Upvotes: 73

Views: 48923

Answers (8)

Jay Dave
Jay Dave

Reputation: 147

Here is the answer and implemented the custom filter as simple as possible this might help

Django admin date range filter

Upvotes: 1

Dmitrii Sutiagin
Dmitrii Sutiagin

Reputation: 571

Not an optimal way (CPU-wise) but simple and will work, so I do it this way (for my small database). My Django version is 1.6.

In admin.py:

class IsLiveFilter(admin.SimpleListFilter):
    title = 'Live'
    parameter_name = 'islive'
    def lookups(self, request, model_admin):
        return (
            ('1', 'islive'),
        )
    def queryset(self, request, queryset):
        if self.value():
            array = []
            for element in queryset:
                if element.is_live.__call__() == True:
                    q_array.append(element.id)
            return queryset.filter(pk__in=q_array)

...

class NewsItemAdmin(admin.ModelAdmin):
    form = NewsItemAdminForm
    list_display = ('headline', 'id', 'is_live')
    list_filter = (IsLiveFilter)

Key idea here is to access custom fields in a QuerySet via __call__() function.

Upvotes: 3

matley
matley

Reputation: 341

In current django development version there is the support for custom filters: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter

Upvotes: 9

gklka
gklka

Reputation: 2574

Just a sidenote: You can use the deafult ticks on Django admin more easily like this:

def is_live(self):
    if self.when_to_publish is not None:
        if ( self.when_to_publish < datetime.now() ):
            return True
    else:
        return False

is_live.boolean = True

Upvotes: 3

peter2108
peter2108

Reputation: 6120

The user supplies goods to some countries postage free. I wanted to filter those countries:

All - all countries, Yes - postage free, No - charged postage.

The main answer for this question did not work for me (Django 1.3) I think because there was no field_path parameter provided in the __init__ method. Also it subclassed DateFieldFilterSpec. The postage field is a FloatField

from django.contrib.admin.filterspecs import FilterSpec

class IsFreePostage(FilterSpec):

    def __init__(self, f, request, params, model, model_admin, field_path=None):
        super(IsFreePostage, self).__init__(f, request, params, model,
            model_admin, field_path)

        self.removes = {
            'Yes': ['postage__gt'],
            'No': ['postage__exact'],
            'All': ['postage__exact', 'postage__gt'] }

        self.links = (
            ('All', {}),
            ('Yes', {'postage__exact': 0}),
            ('No', {'postage__gt': 0}))

        if request.GET.has_key('postage__exact'):
            self.ttl = 'Yes'
        elif request.GET.has_key('postage__gt'):
            self.ttl = 'No'
        else:
            self.ttl = 'All'

    def choices(self, cl):
        for title, param_dict in self.links:
            yield {'selected': title == self.ttl,
                   'query_string': cl.get_query_string(param_dict,
                       self.removes[title]),
                   'display': title}
    def title(self):
        return 'Free Postage'

FilterSpec.filter_specs.insert(0,
    (lambda f: getattr(f, 'free_postage', False), IsFreePostage))

In self.links we supply dicts. used to construct HTTP query strings like ?postage__exact=0 for each of the possible filters. Filters I think are cumulative so if there was a previous request for 'No' and now we have a request for 'Yes' we have to remove the 'No' query. self.removes specifies what needs to be removed for each query. The choices method constructs the query strings, says which filter has been selected and sets the displayed name of the filter.

Upvotes: 2

Mark Ellul
Mark Ellul

Reputation: 1906

Thanks to gpilotino for giving me the push into the right direction for implementing this.

I noticed the question's code is using a datetime to figure out when its live . So I used the DateFieldFilterSpec and subclassed it.

from django.db import models
from django.contrib.admin.filterspecs import FilterSpec, ChoicesFilterSpec,DateFieldFilterSpec
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext as _
from datetime import datetime

class IsLiveFilterSpec(DateFieldFilterSpec):
    """
    Adds filtering by future and previous values in the admin
    filter sidebar. Set the is_live_filter filter in the model field attribute
    'is_live_filter'.    my_model_field.is_live_filter = True
    """

    def __init__(self, f, request, params, model, model_admin):
        super(IsLiveFilterSpec, self).__init__(f, request, params, model,
                                               model_admin)
        today = datetime.now()
        self.links = (
            (_('Any'), {}),
            (_('Yes'), {'%s__lte' % self.field.name: str(today),
                       }),
            (_('No'), {'%s__gte' % self.field.name: str(today),
                    }),

        )


    def title(self):
        return "Is Live"

# registering the filter
FilterSpec.filter_specs.insert(0, (lambda f: getattr(f, 'is_live_filter', False),
                               IsLiveFilterSpec))

To use you can put the above code into a filters.py, and import it in the model you want to add the filter to

Upvotes: 58

gpilotino
gpilotino

Reputation: 13312

you have to write a custom FilterSpec (not documentend anywhere). Look here for an example:

http://www.djangosnippets.org/snippets/1051/

Upvotes: 23

Daniel Roseman
Daniel Roseman

Reputation: 599956

You can't, unfortunately. Currently non-field items can not be used as list_filter entries.

Note that your admin class wouldn't have worked even if it was a field, as a single-item tuple needs a comma: ('is_live',)

Upvotes: 3

Related Questions