Пока Правильное Решение проблемы страдает в джанговском Trac'е уже два с половиной года от смены майнтейнеров и архитектурной астронавтики, мне как раз потребовалось это сегодня для Афиши. Оказывается, никого ждать не надо , и это вполне решаемо уже сейчас, хоть и с небольшим хаком процесса регистрации.
Задача
У нас есть моделька City, в которой страна задана целым числом — ID региона из яндексовой геобазы. Название в модели не хранится, а при необходимости вычисляется из ID запросом в геобазу:
class City(models.Model):
country_region_id = models.IntegerField()
def country(self):
return get_name_from_geobase(self.country_region_id)
Если в админке сделать фильтр по полю country_region_id
, получится некрасиво:
Хочется заставить админку выводить не цифры, а то, что получается из country()
.
FilterSpec
В админке за выдачу данных фильтров овечает несколько классов, определённых в django.contrib.admin.filterspecs. Каждый класс отвечает за то, чтобы из модели и типа поля вытащить данные для фильтра и определяет правила составления запроса. Стандартные FilterSpec'и умеют строить фильтры для полей разного типа:
- Для ForeignKey выбираются все данные из модели, на которую он ссылается.
- Для полей с choice'ами фильтрами являются сами choice'ы.
- Для BooleanField'ов предопределённый набор из двух значений: True и False.
- Для всех остальных полей делается distinct-выборка всех значений, которые есть в модели.
Для того, чтобы выдавать названия стран, я и написал своего наследника от последнего варианта. Он тоже берёт все страны, которые есть в данных модели City, а при выводе данных переделывает целые цифры в названия стран:
class CountryFilterSpec(AllValuesFilterSpec):
def title(self):
return u'Страна'
def choices(self, cl):
choices = super(CountryFilterSpec, self).choices(cl)
yield choices.next() # специальное значение "Все" оставляем без изменений
while True:
choice = choices.next()
choice['display'] = get_name_from_geobase(int(choice['display']))
yield choice
Класс этот определён у меня прямо в admin.py, рядом с админкой городов.
Регистрация
Осталось научить админку, чтобы она этот класс использовала для поля country. В базовом классе FilterSpec есть специальная функция регистрации фильтров, которая запоминает в некий список классы фильтров, а также условия, по которым они применяются к полям (то есть "если это поле — ForeignKey, тогда для него работает RelatedFilterSpec").
Казалось бы, надо всего лишь сделать так:
# если поле называется country_region_id, использовать CountryFilterSpec
FilterSpec.register(lambda f: f.name == 'country_region_id', CountryFilterSpec)
Но это не работает. Эти пары "условие-класс" хранятся в списке, через который последовательно прогоняется каждое поле, пока для него не подойдёт какой-нибудь из фильтров. Проблема в том, что сама джанговская админка сразу забивает этот список своими фильтрами, последний из которых такой:
FilterSpec.register(lambda f: True, AllValuesFilterSpec)
Он, как видно, применяется безусловно для любого поля, для которого не нашлось более подходящего списка. Следовательно, до любых пользовательских фильтров, добавленных в регистрацию позднее, очередь просто никогда не дойдёт. В этом, кстати, состоит изначальная суть бага 5833.
Чтобы это обойти, я воспользовался не стандартной функцией, а просто записываю свой фильтр предпоследним прямо в тот список:
FilterSpec.filter_specs.insert(-2, (lambda f: f.name == 'country_region_id', CountryFilterSpec))
В итоге вот:
P.S. Ах да, вчера вышла Django 1.1 :-)
Комментарии: 16
Fine hack, thanks. Bookmarked for future reference. :)
Как раз то, что надо, спасибо! Удручают конечно заголовки типа «По Дата создания» и «Выберите пользователь для изменения»... Для себя-то оно ладно, а вот клиенту такое не покажешь. Извиняюсь, что не в тему немного.
А что choices в country_region_id, чтобы get_country_region_id_display выдавал в фильтре имена вместо значений, задать слабо?
Когда-то давным-давно(больше года назад) я на коленке написал статейку посвященную этой же теме - http://tilarids.blogspot.com/2008/03/django-custom-filterspecs.html . Может, кого тоже заинтересует.
Они устарели бы при добавлении городов с новыми странами в админке. Сейчас это возможно делать без участия программистов.
P.S. Довольно странно предлагать решения задачи, не имея никаких других данных, кроме примера из поста в блоге. Может быть миллион причин, почему там не было choices, которые я не упомянул, потому что они не имеют отношения к делу. Отвыкайте домысливать.
Почитал, интересно.
Есть два вопроса:
1. Куда нужно вставить FilterSpec.register, в admin.py ?
2. А почему бы н вместо models.IntegerField() не поставить models.ForeignKey(Model) ?
В любое место после описания класса FilterSpec.
А у нас нет никакой модели для страны. Все данные лежат воа внешнем для нас сервисе — геобазе.
Стесняюсь спросить: а где создается объект FilterSpec или его надо просто импортировать from django.contrib.admin.filterspecs import AllValuesFilterSpec, FilterSpec.
Если класс этого объекта пришется самостоятельно, то приведите, пожалуйста, хотя бы какой-то вариант написания.
У меня не получалось добавить фильтр для колонки потому, что она была ForeignKey. Для других колонок все прошло гладко.
Привет, Иван!
А есть ли причина, почему не вставить кастомный фильтр самым первым? :) Тогда уж наверняка :) Ведь это все равно специфический фильтр, и он заведомо должен перекрыть все остальные.
Это бы сработало, насколько я понимаю, во всех случаях, в том числе в случае Дмитрия (пред. коммент):
Можно и первым в этом случае... Не принципиально.
Как один из вариантов - можно обойтись без средств джанго и заменить коды стран на названия - джаваскриптом на body.onload
Спасибо, Ивану за статью. Воспользовался, очень благодарен.
Есть модель, в которой поле типа дата и еще несколько целочисленных полей. Как можно реализовать фильтр в админке за период по дате?
Доп.условия:
Если подобное через FilterSpec не решить, то подскажите пожалуйста в какую сторону смотреть.