Переодически возникает задача когда нужно фильтровать спикок 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, })
После этого всё должно заработать.