Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django admin filter using F() expressions

does someone know how to filter in admin based on comparison on model fields - F() expressions?

Let's assume we have following model:

class Transport(models.Model):
    start_area = models.ForeignKey(Area, related_name='starting_transports')
    finish_area = models.ForeignKey(Area, related_name='finishing_transports')

Now, what I would like to do is to make admin filter which allows for filtering of in-area and trans-area objects, where in-area are those, whose start_area and finish_area are the same and trans-area are the others.

I have tried to accomplish this by creating custom FilterSpec but there are two problems:

  • FilterSpec is bound to only one field.
  • FilterSpec doesn't support F() expressions and exclude.

The second problem might be solved by defining custom ChangeList class, but I see no way to solve the first one.

I also tried to "emulate" the filter straight in the ModelAdmin instance by overloading queryset method and sending extra context to the changelist template where the filter itself would be hard-coded and printed by hand. Unfortunately, there seems to be problem, that Django takes out my GET parameters (used in filter link) as they are unknown to the ModelAdmin instance and instead, it puts only ?e=1 which is supposed to signal some error.

Thanks anyone in advance.

EDIT: It seems that functionality, which would allow for this is planned for next Django release, see http://code.djangoproject.com/ticket/5833. Still, does someone have a clue how to accomplish that in Django 1.2?

like image 831
xaralis Avatar asked Nov 24 '10 10:11

xaralis


People also ask

What does F mean in Django?

In the Django QuerySet API, F() expressions are used to refer to model field values directly in the database.

What is the purpose of filter () method in Django?

The filter() method is used to filter you search, and allows you to return only the rows that matches the search term.

What is def __ str __( self in Django?

def str(self): is a python method which is called when we use print/str to convert object into a string. It is predefined , however can be customised.


2 Answers

it's not the best way*, but it should work

class TransportForm(forms.ModelForm):
    transports = Transport.objects.all()
    list = []
    for t in transports:
        if t.start_area.pk == t.finish_area.pk:
            list.append(t.pk)
    select = forms.ModelChoiceField(queryset=Page.objects.filter(pk__in=list))

    class Meta:
        model = Transport
like image 117
seler Avatar answered Oct 08 '22 17:10

seler


The solution involves adding your FilterSpec and as you said implementing your own ChangeList. As the filter name is validated, you must name your filter with a model field name. Below you will see a hack allowing to use the default filter for the same field.

You add your FilterSpec before the standard FilterSpecs.

Below is a working implementation running on Django 1.3

from django.contrib.admin.views.main import *
from django.contrib import admin
from django.db.models.fields import Field
from django.contrib.admin.filterspecs import FilterSpec
from django.db.models import F
from models import Transport, Area
from django.contrib.admin.util import get_fields_from_path
from django.utils.translation import ugettext as _


# Our filter spec
class InAreaFilterSpec(FilterSpec):

    def __init__(self, f, request, params, model, model_admin, field_path=None):
        super(InAreaFilterSpec, self).__init__(
            f, request, params, model, model_admin, field_path=field_path)
        self.lookup_val = request.GET.get('in_area', None)

    def title(self):
        return 'Area'

    def choices(self, cl):
        del self.field._in_area
        yield {'selected': self.lookup_val is None,
               'query_string': cl.get_query_string({}, ['in_area']),
               'display': _('All')}
        for pk_val, val in (('1', 'In Area'), ('0', 'Trans Area')):
            yield {'selected': self.lookup_val == pk_val,
                   'query_string': cl.get_query_string({'in_area' : pk_val}),
                   'display': val}

    def filter(self, params, qs):
        if 'in_area' in params:
            if params['in_area'] == '1':
                qs = qs.filter(start_area=F('finish_area'))
            else:
                qs = qs.exclude(start_area=F('finish_area'))
            del params['in_area']
        return qs

def in_area_test(field):
    # doing this so standard filters can be added with the same name
    if field.name == 'start_area' and not hasattr(field, '_in_area'):
        field._in_area = True
        return True    
    return False

# we add our special filter before standard ones
FilterSpec.filter_specs.insert(0, (in_area_test, InAreaFilterSpec))


# Defining my own change list for transport
class TransportChangeList(ChangeList):

    # Here we are doing our own initialization so the filters
    # are initialized when we request the data
    def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin):
        #super(TransportChangeList, self).__init__(request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin)
        self.model = model
        self.opts = model._meta
        self.lookup_opts = self.opts
        self.root_query_set = model_admin.queryset(request)
        self.list_display = list_display
        self.list_display_links = list_display_links
        self.list_filter = list_filter
        self.date_hierarchy = date_hierarchy
        self.search_fields = search_fields
        self.list_select_related = list_select_related
        self.list_per_page = list_per_page
        self.model_admin = model_admin

        # Get search parameters from the query string.
        try:
            self.page_num = int(request.GET.get(PAGE_VAR, 0))
        except ValueError:
            self.page_num = 0
        self.show_all = ALL_VAR in request.GET
        self.is_popup = IS_POPUP_VAR in request.GET
        self.to_field = request.GET.get(TO_FIELD_VAR)
        self.params = dict(request.GET.items())
        if PAGE_VAR in self.params:
            del self.params[PAGE_VAR]
        if TO_FIELD_VAR in self.params:
            del self.params[TO_FIELD_VAR]
        if ERROR_FLAG in self.params:
            del self.params[ERROR_FLAG]

        if self.is_popup:
            self.list_editable = ()
        else:
            self.list_editable = list_editable
        self.order_field, self.order_type = self.get_ordering()
        self.query = request.GET.get(SEARCH_VAR, '')
        self.filter_specs, self.has_filters = self.get_filters(request)
        self.query_set = self.get_query_set()
        self.get_results(request)
        self.title = (self.is_popup and ugettext('Select %s') % force_unicode(self.opts.verbose_name) or ugettext('Select %s to change') % force_unicode(self.opts.verbose_name))
        self.pk_attname = self.lookup_opts.pk.attname


    # To be able to do our own filter,
    # we need to override this
    def get_query_set(self):

        qs = self.root_query_set
        params = self.params.copy()

        # now we pass the parameters and the query set 
        # to each filter spec that may change it
        # The filter MUST delete a parameter that it uses
        if self.has_filters: 
            for filter_spec in self.filter_specs:
                if hasattr(filter_spec, 'filter'):
                    qs = filter_spec.filter(params, qs)

        # Now we call the parent get_query_set()
        # method to apply subsequent filters
        sav_qs = self.root_query_set
        sav_params = self.params

        self.root_query_set = qs
        self.params = params

        qs = super(TransportChangeList, self).get_query_set()

        self.root_query_set = sav_qs
        self.params = sav_params

        return qs


class TransportAdmin(admin.ModelAdmin):
    list_filter = ('start_area','start_area')

    def get_changelist(self, request, **kwargs):
        """
        Overriden from ModelAdmin
        """
        return TransportChangeList


admin.site.register(Transport, TransportAdmin)
admin.site.register(Area)
like image 42
Antoine Martin Avatar answered Oct 08 '22 18:10

Antoine Martin