Как отфильтровать выпадающий список в админке 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
объект, мы должны выбрать страну, а затем город. После выбора страны список доступных городов должен быть ограничен городами, которые находятся в выбранной стране.
принцип
Чтобы добиться этого, мы можем использовать кнопку "Сохранить и продолжить редактирование", которая (обычно) доступна в представлении "Добавить" или "Изменить" администратора. Основной принцип заключается в следующем:
- начальный вид добавления показывает только раскрывающийся список стран (и, возможно, некоторые другие, независимые поля)
- После выбора страны нажимаем "Сохранить и продолжить редактирование"
- теперь мы автоматически перенаправлены в представление изменений, которое показывает раскрывающийся список как страны (как readonly_field), так и раскрывающийся список городов
- в представлении изменений мы фильтруем набор запросов для
City
поле, в зависимости от выбранной страны (см., например, 1, 2)
реализация
Оригинальный пример использования "Сохранить и продолжить редактирование" находится в стандартной реализации администратора Django для contrib.auth.User
Модель: исходный код UserAdmin.
Базовая реализация для нашего случая включает в себя следующие шаги (подробности в примере ниже):
- переопределение
ModelAdmin.get_fields
а такжеModelAdmin.get_readonly_fields
(или наборы полей), поэтому они возвращают только страну (редактируемую), если мы находимся в представлении добавления (т.е. когдаobj is None
) - простираться
ModelAdmin.add_view
удалить ненужные кнопки сохранения (или переопределить их поведение, как вUserAdmin
) - простираться
ModelAdmin.save_model
сохранить ссылку на выбранную страну в пользовательском атрибуте - простираться
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))