Подтвердить что ты не робот

Фильтр admin Django с использованием выражений F()

Кто-нибудь знает, как фильтровать в admin на основе сравнения по полям модели - выражения F()?

Предположим, что мы имеем следующую модель:

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

Теперь, что я хотел бы сделать, это сделать админ-фильтр, который позволяет фильтровать объекты внутри зоны и транс-области, где in-area - это те, чьи start_area и finish_area являются одинаковыми, а trans-area - другие.

Я попытался это сделать, создав собственный фильтр FilterSpec, но есть две проблемы:

  • FilterSpec привязан только к одному полю.
  • FilterSpec не поддерживает выражения F() и исключает.

Вторая проблема может быть решена путем определения пользовательского класса ChangeList, но я не вижу способа решить первую.

Я также попытался "эмулировать" фильтр прямо в экземпляре ModelAdmin, перегружая метод запроса и отправляя дополнительный контекст в шаблон списка изменений, где сам фильтр будет жестко закодирован и распечатан вручную. К сожалению, кажется, что проблема заключается в том, что Django извлекает мои параметры GET (используемые в канале фильтра), поскольку они неизвестны экземпляру ModelAdmin, и вместо этого он помещает только? E = 1, который должен сигнализировать об ошибке.

Спасибо всем заблаговременно.

РЕДАКТИРОВАТЬ. Похоже, что функциональность, которая позволила бы это сделать, запланирована для следующего выпуска Django, см. http://code.djangoproject.com/ticket/5833. Тем не менее, кто-то знает, как это сделать в Django 1.2?

4b9b3361

Ответ 1

Решение включает в себя добавление фильтра FilterSpec и, как вы сказали, внедрение собственного ChangeList. Когда имя фильтра будет проверено, вы должны указать свой фильтр с именем поля модели. Ниже вы увидите хак, позволяющий использовать фильтр по умолчанию для того же поля.

Вы добавляете свой фильтрSpec перед стандартными фильтрами.

Ниже приведена рабочая версия, запущенная на 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)

Ответ 2

это не лучший способ *, но он должен работать

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

Ответ 3

К сожалению, FilterSpecs в настоящее время очень ограничены в Django. Просто они не были созданы с учетом настроек.

К счастью, многие давно работали над патчем FilterSpec. Он пропустил 1.3-этап, но теперь он выглядит, наконец, в багажнике и должен ударить со следующей версией.

# 5833 (Custom FilterSpecs)

Если вы хотите запустить проект на соединительной линии, вы можете воспользоваться его сейчас или у вас есть возможность исправить вашу текущую установку. В противном случае вам придется подождать, но, по крайней мере, скоро.