使用F()表达式进行Django管理过滤

12
有人知道如何基于模型字段 - F()表达式进行管理员筛选吗?
假设我们有以下模型:
class Transport(models.Model):
    start_area = models.ForeignKey(Area, related_name='starting_transports')
    finish_area = models.ForeignKey(Area, related_name='finishing_transports')

现在,我想做的是创建一个管理员筛选器,允许筛选区域内和跨区域的对象,其中区域内的对象是其起始区域和终止区域相同的对象,而跨区域的对象则不同。

我尝试通过创建自定义FilterSpec来实现这一点,但存在两个问题:

  • FilterSpec仅绑定到一个字段。
  • FilterSpec不支持F()表达式和排除。

第二个问题可以通过定义自定义ChangeList类来解决,但我看不到解决第一个问题的方法。

我还尝试在ModelAdmin实例中直接“模拟”筛选器,通过重载queryset方法并向changelist模板发送额外的上下文,在那里将过滤器本身硬编码并手动打印。不幸的是,似乎存在问题,Django会将我的GET参数(用于筛选链接)取出,因为它们对ModelAdmin实例是未知的,而代替它们放入?e=1,这应该是某种错误信号。

提前感谢任何人的帮助。

编辑:似乎下一个 Django 发布版本计划中会包含允许此功能的功能,请参见 http://code.djangoproject.com/ticket/5833。不过,有人知道如何在 Django 1.2 中实现吗?

没有什么特别的 ;) 它应该是数据库中所有Transport对象的实例,其start_area与finish_area不同。 - xaralis
1
好吧,似乎没有人有答案。到目前为止,我想到的唯一解决方案非常丑陋:向Transport模型添加另一个字段,在保存时更新以保存运输是否在区域内或跨区域的信息:( 我讨厌那些多余的字段:( - xaralis
3个回答

3

这不是最好的方法,但它应该能够工作

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

1
该解决方案涉及添加您的FilterSpec并实现自己的ChangeList,正如您所说。由于过滤器名称已经过验证,因此必须使用模型字段名称命名您的过滤器。下面您将看到一个技巧,允许使用相同字段的默认过滤器。
在标准FilterSpecs之前添加您的FilterSpec。
以下是在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)

谢谢。不幸的是,这似乎是不必要的,我宁愿选择添加一个冗余字段的方式。希望 Django 开发人员在下一个版本中会修复这个问题(http://code.djangoproject.com/ticket/5833)。 - xaralis

0

很遗憾,目前Django中的FilterSpecs非常有限。简单来说,它们并没有考虑到定制化。

但值得庆幸的是,许多人一直在为FilterSpec修补程序工作了很长时间。它错过了1.3里程碑,但看起来现在终于进入了主干,并将在下一个版本中发布。

#5833 (自定义FilterSpecs)

如果你想在主干上运行你的项目,现在就可以利用它,或者你可能能够修补你当前的安装。否则,你就得等待,但至少它即将到来。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接