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.
精彩评论