Header

Мы создаём веб сайты и веб приложения

Design is not just what it looks like and feels like

Design is how it works

Давайте сделаем что-то интересное!

воскресенье, 19 июня 2011 г.

Правый фильтр в django admin и ошибка типа "Filtering by property_values__value not allowed"

Переодически возникает задача когда нужно фильтровать спикок changelist модели по каким нибудь хитрым условиям, и стандартного функционала django admin не хватает.
Например есть такая структура:
class Product(models.Model):
 name = models.CharField(_(u"Name"), max_length=255, blank=True)
  
class PropertyOption(models.Model):
 name = models.CharField(_(u"Name"), max_length=255, blank=True)
 product = models.ForeignKey(Product, verbose_name=_(u"Product"), related_name="productoptions")
К списку товаров необходимо приделать правый фильтр по PropertyOption.
Сразу хочется отправиться в фаил admin и в классе ProductAdmin написать что-нибудь вроде list_filter = ('productoptions',), но к сожалению будет ошибка
'ProductAdmin.list_filter[0]' refers to field 'productoptions' that is missing from model 'Product'. 
У модели товара нету поля productoptions, есть только менеджер объектов с таким именем... в любом случае пока что это не прокатывает, поэтому нужно искать другой вариант. Например создать свой собственный фильтр, унаследовав его от класса ChoicesFilterSpec. В итоге получиться что-то вроде этого:
class PropertyOptionFilterSpec(ChoicesFilterSpec):
    def __init__(self, request, **kwargs):
        self.lookup_kwarg = 'productoptions__pk'
        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
        self.lookup_choices = PropertyOption.objects.all()

    def choices(self, cl):
        yield { 'selected': self.lookup_val is None,
                'query_string': cl.get_query_string({}, [self.lookup_kwarg,]),
                'display': u'Все'}
        for option in self.lookup_choices:
            yield  {'selected': smart_unicode(option.pk) == self.lookup_val,
                    'query_string': cl.get_query_string({self.lookup_kwarg: option.pk}),
                    'display': option.name }
                    
    # незабываем переопределить title иначе при рендеринге будет ошибка              
    def title(self):
        return u'параметрам'
Теперь возникает задача как этот фильтр применить. Т.к. нет поля к которому мы бы могли его прицепить, нужно использовать другой подход, а именно:
  • Переопределить метод changelist_view у ProductAdmin добавив туда эксземпляр нашего фильтра
  • Переопределить шаблон списка товаров, который рендерит changelist_view
ProductAdmin получается такой:
class ProductAdmin(admin.ModelAdmin):

    def changelist_view(self, request, extra_context=None):
        po_filter = PropertyOptionFilterSpec(request)
        return super(ProductAdmin, self).changelist_view(request, extra_context={ 'po_filter': po_filter, })
а шаблон для списка admin/-my_app_name-/product/change_list.html я сделал таким
{% extends "admin/change_list.html" %}
{% load admin_list i18n %}

{% block filters %}
      <div id="changelist-filter">
        <h2>{% trans 'Filter' %}</h2>
        {% if cl.has_filters %}
            {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
        {% endif %}
        {# Последним рендерим наш фильтр #}
        {% admin_list_filter cl po_filter %}
      </div>
{% endblock %}

На этом этапе можно попробвать фильтр в дейсвии. Заходим в список товаров, все отлично рендериться, но если попробовать воспользоваться фильтром выскочит ошибка:
SuspiciousOperation at /admin/ecko/product/
Filtering by productoptions__pk not allowed
Это происходит потому что в классе ModelAdmin есть такой специальный метод lookup_allowed, который занимается тем что проверяет позволено ли параметру в запросе учавстовать в обращении к базе или нет, в нашем случае я не стал заморачиваться и просто застваил его всегда возвращать true. В конечном итоге ProductAdmin стал выглядеть так:
class ProductAdmin(admin.ModelAdmin):

    def lookup_allowed(self, *args, **kwargs):
        return True

    def changelist_view(self, request, extra_context=None):
        po_filter = PropertyOptionFilterSpec(request)
        return super(ProductAdmin, self).changelist_view(request, extra_context={ 'po_filter': po_filter, })
После этого всё должно заработать.

Комментариев нет:

Отправить комментарий