Как отфильтровать выпадающий список в админке Django при выборе другого выпадающего списка

У меня есть два выпадающих списка на сайте администратора Django. Например, у меня есть SelectCountry и SelectRegion. Регион имеет отношение к стране. Как я могу гарантировать, что при выборе страны регионы будут отфильтрованы по этой стране?

NB: я использую django-grappelli для своего админ-бэкенда.

Любые идеи будут оценены. Благодарю.

1 ответ

Как намекает DrMeers, используйте django-smart-select. Нет смысла заново изобретать колесо.

ТЛ; др

Используйте стандартную кнопку "Сохранить и продолжить редактирование". Собственный исходный код UserAdmin в Django показывает, как это реализовать.

вступление

Вот способ добиться простого поведения dropdown-filtering*, используя чистую стандартную функциональность администратора Django, то есть без использования сторонних приложений, Ajax или JavaScript.

С другой стороны, это решение использует стандартную функциональность, и весь код содержится в одном месте, а именно. ваш обычай ModelAdmin,

С другой стороны, это решение не является по-настоящему "динамическим", поскольку требует от пользователя нажатия кнопки "Сохранить". Это хорошо работает для простых случаев, подобных описанному ниже, но для более сложных случаев может потребоваться другой подход.

(* так называемые зависимые выпадающие списки, цепочечные выпадающие списки, цепочки выбора, зависимые поля выбора, связанные списки, связанные поля внешних ключей и т. д.)

предпосылка

Предположим, у нас есть Address модель, которая имеет ForeignKey поля для Country а также City, City модель имеет ForeignKey поле для Country,

желаемое поведение

Создать новый Address объект, мы должны выбрать страну, а затем город. После выбора страны список доступных городов должен быть ограничен городами, которые находятся в выбранной стране.

принцип

Чтобы добиться этого, мы можем использовать кнопку "Сохранить и продолжить редактирование", которая (обычно) доступна в представлении "Добавить" или "Изменить" администратора. Основной принцип заключается в следующем:

  1. начальный вид добавления показывает только раскрывающийся список стран (и, возможно, некоторые другие, независимые поля)
  2. После выбора страны нажимаем "Сохранить и продолжить редактирование"
  3. теперь мы автоматически перенаправлены в представление изменений, которое показывает раскрывающийся список как страны (как readonly_field), так и раскрывающийся список городов
  4. в представлении изменений мы фильтруем набор запросов для City поле, в зависимости от выбранной страны (см., например, 1, 2)

реализация

Оригинальный пример использования "Сохранить и продолжить редактирование" находится в стандартной реализации администратора Django для contrib.auth.User Модель: исходный код UserAdmin.

Базовая реализация для нашего случая включает в себя следующие шаги (подробности в примере ниже):

  1. переопределение ModelAdmin.get_fields а также ModelAdmin.get_readonly_fields (или наборы полей), поэтому они возвращают только страну (редактируемую), если мы находимся в представлении добавления (т.е. когда obj is None)
  2. простираться ModelAdmin.add_view удалить ненужные кнопки сохранения (или переопределить их поведение, как в UserAdmin)
  3. простираться ModelAdmin.save_model сохранить ссылку на выбранную страну в пользовательском атрибуте
  4. простираться ModelAdmin.get_field_queryset (или, например, formfield_for_foreignkey) отфильтровать набор запросов для City поле

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

пример кода

Минимальный пример может выглядеть так (немного грубо по краям):

models.py

from django.db import models

class Country(models.Model):
    name = models.CharField(max_length=30, default='', null=False, blank=False)

class City(models.Model):
    name = models.CharField(max_length=30, default='', null=False, blank=False)
    country = models.ForeignKey(to=Country, on_delete=models.CASCADE,
                                null=False, blank=False)

class Address(models.Model):
    country = models.ForeignKey(to=Country, on_delete=models.CASCADE,
                                null=False, blank=False)
    city = models.ForeignKey(to=City, on_delete=models.CASCADE,
                             null=True, blank=True)

admin.py

from django.contrib import admin
from admindropdowns.models import Country, City, Address

class AddressAdmin(admin.ModelAdmin):
    # standard attributes (used in "change" view)
    fields = ['country', 'city']
    readonly_fields = ['country']

    # custom attributes (used in "add" view)
    initial_fields = ['country']
    initial_readonly_fields = []
    selected_country = None

    def get_fields(self, request, obj=None):
        """ show initial fields in add view, show all fields in change view """
        fields = super().get_fields(request, obj)
        if obj is None:
            fields = self.initial_fields
        return fields

    def get_readonly_fields(self, request, obj=None):
        """ set the initial field readonly in the change view """
        readonly_fields = super().get_readonly_fields(request, obj)
        if obj is None:
            readonly_fields = self.initial_readonly_fields
        return readonly_fields

    def add_view(self, request, form_url='', extra_context=None):
        """ remove the save button from the "add" view """
        extra_context = dict(show_save=False)
        return super().add_view(request, form_url, extra_context)

    def save_model(self, request, obj, form, change):
        """ store the select country for use in get_field_queryset """
        self.selected_country = obj.country
        return super().save_model(request, obj, form, change)

    def get_field_queryset(self, db, db_field, request):
        """ filter the City queryset by selected country """
        queryset = super().get_field_queryset(db, db_field, request)
        if db_field.name == 'city':
            if queryset is None:
                # If "ordering" is not set on the City admin, get_field_queryset returns
                # None, so we have to get it ourselves. See original source:
                # github.com/django/django/blob/2.1.5/django/contrib/admin/options.py#L209
                queryset = City.objects.all()
            # Filter by country
            queryset = queryset.filter(country=self.selected_country)
        return queryset

admin.site.register(Country)
admin.site.register(City, CityAdmin)
admin.site.register(Address, AddressAdmin)

(протестировано с Python 3.6, Django 2.1)

альтернатива

В качестве альтернативы фильтрации можно также просто забыть вышеупомянутое и вместо этого полагаться на расширенный Model.clean метод ( docs), который предупреждает пользователя, если поля не совпадают.

Например (с использованием models.py, определенного выше):

...

from django.core.exceptions import ValidationError

...

class Address(models.Model):
    ...

    def clean(self):
        """ warn if selected city is not in selected country """
        if (self.country_id and self.city_id and self.country != self.city.country):
            raise ValidationError(message='%(city)s is not in %(country)s',
                                  code='wrong_country',
                                  params=dict(city=self.city.name, 
                                              country=self.country.name))
Другие вопросы по тегам