Как отфильтровать выборки 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__ метод модельной формы.

https://docs.djangoproject.com/en/3.1/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey

      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
Другие вопросы по тегам