开发者

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 = mo开发者_StackOverflowdels.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?


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


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)


Unfortunately, FilterSpecs are very limited currently in Django. Simply, they weren't created with customization in mind.

Thankfully, though, many have been working on a patch to FilterSpec for a long time. It missed the 1.3 milestone, but it looks like it's now finally in trunk, and should hit with the next release.

#5833 (Custom FilterSpecs)

If you want to run your project on trunk, you can take advantage of it now, or you might be able to patch your current installation. Otherwise, you'll have to wait, but at least it's coming soon.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜