Как отфильтровать выборки ForeignKey в Django ModelForm?
Скажем, у меня есть следующее в моем models.py
:
class Company(models.Model):
name = ...
class Rate(models.Model):
company = models.ForeignKey(Company)
name = ...
class Client(models.Model):
name = ...
company = models.ForeignKey(Company)
base_rate = models.ForeignKey(Rate)
Т.е. есть несколько Companies
каждый из которых имеет диапазон Rates
а также Clients
, каждый Client
должен иметь базу Rate
который выбран из его родителя Company's Rates
, не другой Company's Rates
,
При создании формы для добавления Client
Я хотел бы удалить Company
варианты (поскольку это уже было выбрано с помощью кнопки "Добавить клиента" на Company
страница) и ограничить Rate
выбор к этому Company
также.
Как мне это сделать в Django 1.0?
Мой текущий forms.py
На данный момент файл просто шаблонный:
from models import *
from django.forms import ModelForm
class ClientForm(ModelForm):
class Meta:
model = Client
И views.py
также является основным:
from django.shortcuts import render_to_response, get_object_or_404
from models import *
from forms import *
def addclient(request, company_id):
the_company = get_object_or_404(Company, id=company_id)
if request.POST:
form = ClientForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(the_company.get_clients_url())
else:
form = ClientForm()
return render_to_response('addclient.html', {'form': form, 'the_company':the_company})
В Django 0.96 я смог взломать это, выполнив что-то вроде следующего перед рендерингом шаблона:
manipulator.fields[0].choices = [(r.id,r.name) for r in Rate.objects.filter(company_id=the_company.id)]
ForeignKey.limit_choices_to
кажется многообещающим, но я не знаю, как пройти в the_company.id
и я не уверен, будет ли это работать вне интерфейса администратора в любом случае.
Благодарю. (Это кажется довольно простой просьбой, но если мне нужно что-то изменить, я открыт для предложений.)
9 ответов
ForeignKey представлен django.forms.ModelChoiceField, который является ChoiceField, чьи выборы являются моделью QuerySet. Смотрите ссылку для ModelChoiceField.
Итак, предоставьте QuerySet для поля queryset
приписывать. Зависит от того, как строится ваша форма. Если вы создадите явную форму, у вас будут поля с именами напрямую.
form.rate.queryset = Rate.objects.filter(company_id=the_company.id)
Если вы берете объект ModelForm по умолчанию, form.fields["rate"].queryset = ...
Это сделано явно в представлении. Нет взлома вокруг.
В дополнение к ответу С. Лотта и упоминанию в комментариях Гуру, можно добавить фильтры набора запросов, переопределив ModelForm.__init__
функция. (Это может быть легко применимо к обычным формам), это может помочь с повторным использованием и поддерживает функцию просмотра в чистоте.
class ClientForm(forms.ModelForm):
def __init__(self,company,*args,**kwargs):
super (ClientForm,self ).__init__(*args,**kwargs) # populates the post
self.fields['rate'].queryset = Rate.objects.filter(company=company)
self.fields['client'].queryset = Client.objects.filter(company=company)
class Meta:
model = Client
def addclient(request, company_id):
the_company = get_object_or_404(Company, id=company_id)
if request.POST:
form = ClientForm(the_company,request.POST) #<-- Note the extra arg
if form.is_valid():
form.save()
return HttpResponseRedirect(the_company.get_clients_url())
else:
form = ClientForm(the_company)
return render_to_response('addclient.html',
{'form': form, 'the_company':the_company})
Это может быть полезно для повторного использования, например, если у вас есть общие фильтры, необходимые для многих моделей (обычно я объявляю абстрактный класс Form). Например
class UberClientForm(ClientForm):
class Meta:
model = UberClient
def view(request):
...
form = UberClientForm(company)
...
#or even extend the existing custom init
class PITAClient(ClientForm):
def __init__(company, *args, **args):
super (PITAClient,self ).__init__(company,*args,**kwargs)
self.fields['support_staff'].queryset = User.objects.exclude(user='michael')
Кроме этого, я просто перевожу материал блога Django, из которого есть много хороших.
Это просто и работает с Django 1.4:
class ClientAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ClientAdminForm, self).__init__(*args, **kwargs)
# access object through self.instance...
self.fields['base_rate'].queryset = Rate.objects.filter(company=self.instance.company)
class ClientAdmin(admin.ModelAdmin):
form = ClientAdminForm
....
Вам не нужно указывать это в классе формы, но вы можете сделать это непосредственно в ModelAdmin, так как Django уже включает этот встроенный метод в ModelAdmin (из документации):
ModelAdmin.formfield_for_foreignkey(self, db_field, request, **kwargs)¶
'''The formfield_for_foreignkey method on a ModelAdmin allows you to
override the default formfield for a foreign keys field. For example,
to return a subset of objects for this foreign key field based on the
user:'''
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "car":
kwargs["queryset"] = Car.objects.filter(owner=request.user)
return super(MyModelAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
Еще более изящный способ сделать это (например, создать интерфейсный интерфейс администратора, к которому могут обращаться пользователи) - создать подкласс ModelAdmin, а затем изменить методы, приведенные ниже. Конечным результатом является пользовательский интерфейс, который показывает ТОЛЬКО контент, связанный с ними, позволяя вам (суперпользователю) видеть все.
Я переопределил четыре метода, первые два не позволяют пользователю что-либо удалить, а также удаляют кнопки удаления с сайта администратора.
Третье переопределение фильтрует любой запрос, который содержит ссылку (в примере "пользователь" или "дикобраз" (просто в качестве иллюстрации).
Последнее переопределение фильтрует любое поле постороннего ключа в модели, чтобы отфильтровать доступные варианты, такие же, как базовый набор запросов.
Таким образом, вы можете представить простой в управлении фронтальный сайт администратора, который позволяет пользователям связываться с их собственными объектами, и вам не нужно вводить конкретные фильтры ModelAdmin, о которых мы говорили выше.
class FrontEndAdmin(models.ModelAdmin):
def __init__(self, model, admin_site):
self.model = model
self.opts = model._meta
self.admin_site = admin_site
super(FrontEndAdmin, self).__init__(model, admin_site)
удалить кнопки "удалить":
def get_actions(self, request):
actions = super(FrontEndAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
предотвращает удаление разрешения
def has_delete_permission(self, request, obj=None):
return False
Фильтрует объекты, которые можно просмотреть на сайте администратора:
def get_queryset(self, request):
if request.user.is_superuser:
try:
qs = self.model.objects.all()
except AttributeError:
qs = self.model._default_manager.get_queryset()
return qs
else:
try:
qs = self.model.objects.all()
except AttributeError:
qs = self.model._default_manager.get_queryset()
if hasattr(self.model, ‘user’):
return qs.filter(user=request.user)
if hasattr(self.model, ‘porcupine’):
return qs.filter(porcupine=request.user.porcupine)
else:
return qs
Фильтрует выбор для всех полей Foreignkey на сайте администратора:
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if request.employee.is_superuser:
return super(FrontEndAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
else:
if hasattr(db_field.rel.to, 'user'):
kwargs["queryset"] = db_field.rel.to.objects.filter(user=request.user)
if hasattr(db_field.rel.to, 'porcupine'):
kwargs["queryset"] = db_field.rel.to.objects.filter(porcupine=request.user.porcupine)
return super(ModelAdminFront, self).formfield_for_foreignkey(db_field, request, **kwargs)
Для этого используется общий вид, например CreateView...
class AddPhotoToProject(CreateView):
"""
a view where a user can associate a photo with a project
"""
model = Connection
form_class = CreateConnectionForm
def get_context_data(self, **kwargs):
context = super(AddPhotoToProject, self).get_context_data(**kwargs)
context['photo'] = self.kwargs['pk']
context['form'].fields['project'].queryset = Project.objects.for_user(self.request.user)
return context
def form_valid(self, form):
pobj = Photo.objects.get(pk=self.kwargs['pk'])
obj = form.save(commit=False)
obj.photo = pobj
obj.save()
return_json = {'success': True}
if self.request.is_ajax():
final_response = json.dumps(return_json)
return HttpResponse(final_response)
else:
messages.success(self.request, 'photo was added to project!')
return HttpResponseRedirect(reverse('MyPhotos'))
самая важная часть этого...
context['form'].fields['project'].queryset = Project.objects.for_user(self.request.user)
Если вы еще не создали форму и хотите изменить набор запросов, вы можете сделать следующее:
formmodel.base_fields['myfield'].queryset = MyModel.objects.filter(...)
Это очень полезно, когда вы используете общие представления!
Итак, я действительно пытался понять это, но кажется, что Джанго все еще не делает это очень просто. Я не настолько глуп, но просто не вижу ни одного (несколько) простого решения.
Я нахожу довольно уродливым переопределение представлений Admin для такого рода вещей, и каждый найденный мной пример никогда полностью не относится к представлениям Admin.
Это настолько распространенное обстоятельство, с моделями, которые я делаю, и это ужасно, потому что нет очевидного решения этого...
У меня есть эти классы:
# models.py
class Company(models.Model):
# ...
class Contract(models.Model):
company = models.ForeignKey(Company)
locations = models.ManyToManyField('Location')
class Location(models.Model):
company = models.ForeignKey(Company)
Это создает проблему при настройке Администратора для Компании, поскольку в нем есть встроенные строки для Контракта и Местоположения, а параметры Контейнера в м2м для Местоположения не фильтруются должным образом в соответствии с Компанией, которую вы в настоящее время редактируете.
Короче, мне понадобится опция администратора, чтобы сделать что-то вроде этого:
# admin.py
class LocationInline(admin.TabularInline):
model = Location
class ContractInline(admin.TabularInline):
model = Contract
class CompanyAdmin(admin.ModelAdmin):
inlines = (ContractInline, LocationInline)
inline_filter = dict(Location__company='self')
В конечном счете, мне было бы все равно, был ли процесс фильтрации размещен в базе CompanyAdmin или в ContractInline. (Размещение его в строке имеет больше смысла, но затрудняет ссылаться на базовый контракт как на "себя".)
Есть ли кто-нибудь, кто знает что-то столь же простое, как этот крайне необходимый ярлык? Еще когда я делал PHP администраторов для такого рода вещей, это считалось основной функциональностью! На самом деле, он всегда был автоматическим и должен был быть отключен, если вы действительно этого не хотели!
Более публичным способом является вызов get_form в классах Admin. Это также работает для полей без базы данных. Например, здесь у меня есть поле с именем '_terminal_list' в форме, которое можно использовать в особых случаях для выбора нескольких элементов терминала из get_list(запрос), а затем фильтрации на основе request.user:
class ChangeKeyValueForm(forms.ModelForm):
_terminal_list = forms.ModelMultipleChoiceField(
queryset=Terminal.objects.all() )
class Meta:
model = ChangeKeyValue
fields = ['_terminal_list', 'param_path', 'param_value', 'scheduled_time', ]
class ChangeKeyValueAdmin(admin.ModelAdmin):
form = ChangeKeyValueForm
list_display = ('terminal','task_list', 'plugin','last_update_time')
list_per_page =16
def get_form(self, request, obj = None, **kwargs):
form = super(ChangeKeyValueAdmin, self).get_form(request, **kwargs)
qs, filterargs = Terminal.get_list(request)
form.base_fields['_terminal_list'].queryset = qs
return form
Хороший способ ограничить выбор поля ForeignKey в ModelForm во время выполнения (например, в CreateView) — установить
limit_choices_to
за
base_fields['field_name']
путем переопределения
get_form_class()
в представлении.
Например, при создании Клиента, чтобы ограничить выбор Ставки теми для Компании, которые указаны в URL-адресе:
class ClientCreateView(LoginRequired, CreateView):
model = Client
fields = '__all__'
def get_form_class(self):
modelform = super().get_form_class()
modelform.base_fields['rate'].limit_choices_to = {'company': self.kwargs['company']}
return modelform
Согласно документации Django, вы можете применять фильтры к вашему набору запросов по умолчанию, используя
__init__
метод модельной формы.
class CountryAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['capital'].queryset = self.instance.cities.all()
class CountryAdmin(admin.ModelAdmin):
form = CountryAdminForm