Пока Правильное Решение проблемы страдает в джанговском 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'и умеют строить фильтры для полей разного типа:

Для того, чтобы выдавать названия стран, я и написал своего наследника от последнего варианта. Он тоже берёт все страны, которые есть в данных модели 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

  1. zgoda

    Fine hack, thanks. Bookmarked for future reference. :)

  2. Миша

    Как раз то, что надо, спасибо! Удручают конечно заголовки типа «По Дата создания» и «Выберите пользователь для изменения»... Для себя-то оно ладно, а вот клиенту такое не покажешь. Извиняюсь, что не в тему немного.

  3. Serge Matveenko

    А что choices в country_region_id, чтобы get_country_region_id_display выдавал в фильтре имена вместо значений, задать слабо?

  4. Sergey Kishchenko

    Когда-то давным-давно(больше года назад) я на коленке написал статейку посвященную этой же теме - http://tilarids.blogspot.com/2008/03/django-custom-filterspecs.html . Может, кого тоже заинтересует.

  5. Ivan Sagalaev

    А что choices в country_region_id

    Они устарели бы при добавлении городов с новыми странами в админке. Сейчас это возможно делать без участия программистов.

    P.S. Довольно странно предлагать решения задачи, не имея никаких других данных, кроме примера из поста в блоге. Может быть миллион причин, почему там не было choices, которые я не упомянул, потому что они не имеют отношения к делу. Отвыкайте домысливать.

  6. Михаил

    Почитал, интересно.
    Есть два вопроса:
    1. Куда нужно вставить FilterSpec.register, в admin.py ?
    2. А почему бы н вместо models.IntegerField() не поставить models.ForeignKey(Model) ?

  7. Ivan Sagalaev
    1. Куда нужно вставить FilterSpec.register, в admin.py ?

    В любое место после описания класса FilterSpec.

    1. А почему бы н вместо models.IntegerField() не поставить models.ForeignKey(Model) ?

    А у нас нет никакой модели для страны. Все данные лежат воа внешнем для нас сервисе — геобазе.

  8. Дмитрий

    Стесняюсь спросить: а где создается объект FilterSpec или его надо просто импортировать from django.contrib.admin.filterspecs import AllValuesFilterSpec, FilterSpec.
    Если класс этого объекта пришется самостоятельно, то приведите, пожалуйста, хотя бы какой-то вариант написания.

  9. Дмитрий

    У меня не получалось добавить фильтр для колонки потому, что она была ForeignKey. Для других колонок все прошло гладко.

  10. rudy

    Привет, Иван!

    А есть ли причина, почему не вставить кастомный фильтр самым первым? :) Тогда уж наверняка :) Ведь это все равно специфический фильтр, и он заведомо должен перекрыть все остальные.

    Это бы сработало, насколько я понимаю, во всех случаях, в том числе в случае Дмитрия (пред. коммент):

    У меня не получалось добавить фильтр для колонки потому, что она была ForeignKey. Для других колонок все прошло гладко.

  11. Ivan Sagalaev

    Можно и первым в этом случае... Не принципиально.

  12. почитайте статью Ивана http://softwaremaniacs.org/blog/2009/07/30/django-admin-custom-filterspecs/ Возможно она вас натолкнет на правильные мысли

  13. Висилий

    Как один из вариантов - можно обойтись без средств джанго и заменить коды стран на названия - джаваскриптом на body.onload

  14. chukreev-alexey

    Спасибо, Ивану за статью. Воспользовался, очень благодарен.

  15. Собственные фильтры полей в админке Django.

  16. Евгений

    Есть модель, в которой поле типа дата и еще несколько целочисленных полей. Как можно реализовать фильтр в админке за период по дате?

    Доп.условия:

    • В результате должна получиться одна строка.
    • Значения целочисленных полей должны быть соответственно суммированы.
    • Период должен указывать пользователь выбирая дату начала и дату окончания.

    Если подобное через FilterSpec не решить, то подскажите пожалуйста в какую сторону смотреть.

Добавить комментарий