Как мне отфильтровать варианты ForeignKey в Django ModelForm?
-
08-07-2019 - |
Вопрос
Допустим, у меня есть следующее в моем 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
и мне все равно не ясно, будет ли это работать вне интерфейса администратора.
Спасибо.(Это кажется довольно простым запросом, но если мне следует что-то переделать, я открыт для предложений.)
Решение
ForeignKey представлен django.forms.ModelChoiceField, который является полем выбора, выбор которого является набором запросов модели.Смотрите ссылку для Поле выбора модели.
Итак, предоставьте набор запросов к полю queryset
атрибут.Зависит от того, как построена ваша форма.Если вы создадите явную форму, у вас будут поля с прямыми именами.
form.rate.queryset = Rate.objects.filter(company_id=the_company.id)
Если вы возьмете объект ModelForm по умолчанию, form.fields["rate"].queryset = ...
Это делается явно в представлении.Никакого взлома вокруг да около.
Другие советы
В дополнение к ответу С. Лотта и, как упоминалось в комментариях becomingGuru, можно добавить фильтры набора запросов, переопределив 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
....
Вам не нужно указывать это в классе form, но вы можете сделать это непосредственно в 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, а затем изменить приведенные ниже методы.Конечным результатом является пользовательский интерфейс, который показывает им ТОЛЬКО контент, связанный с ними, позволяя вам (суперпользователю) видеть все.
Я переопределил четыре метода, первые два делают невозможным удаление пользователем чего-либо, а также удаляют кнопки удаления с сайта администратора.
Третье переопределение фильтрует любой запрос, содержащий ссылку на (в примере 'user' или 'porcupine' (просто в качестве иллюстрации).
Последнее переопределение фильтрует любое поле foreignkey в модели, чтобы фильтровать доступные варианты так же, как в базовом наборе запросов.
Таким образом, вы можете представить простой в управлении интерфейсный сайт администратора, который позволяет пользователям работать со своими собственными объектами, и вам не нужно забывать вводить конкретные фильтры 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(...)
Это очень полезно, когда вы используете общие представления!
Итак, я действительно пытался понять это, но, похоже, Django все еще не делает это очень простым.Я не такой уж тупой, но я просто не вижу никакого (в какой-то степени) простого решения.
Я нахожу, что, как правило, довольно некрасиво переопределять представления администратора для такого рода вещей, и каждый пример, который я нахожу, никогда полностью не применим к представлениям администратора.
Это настолько распространенное обстоятельство с моделями, которые я создаю, что я нахожу ужасающим отсутствие очевидного решения этой проблемы...
У меня есть эти занятия:
# 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)
Это создает проблему при настройке Администратора для Компании, поскольку в нем есть строки как для Контракта, так и для местоположения, а параметры m2m контракта для местоположения неправильно фильтруются в соответствии с Компанией, которую вы в данный момент редактируете.
Короче говоря, мне понадобился бы какой-нибудь параметр администратора, чтобы сделать что-то подобное:
# 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 .(Размещение его во встроенном имеет больше смысла, но это затрудняет ссылку на базовый контракт как на "self".)
Есть ли кто-нибудь, кто знает о чем-то столь же простом, как этот крайне необходимый короткий путь?Раньше, когда я создавал PHP-администраторов для подобных вещей, это считалось базовой функциональностью!На самом деле, это всегда было автоматически, и его приходилось отключать, если вы действительно этого не хотели!
Более общедоступный способ - вызвать get_form в классах администратора.Это также работает и для полей, не относящихся к базе данных.Например, здесь у меня есть поле с именем '_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