Як я фільтрую варіанти ForeignKey у Django ModelForm?


227

Скажіть, у мене є таке 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 і мені не зрозуміло, чи буде це працювати поза інтерфейсом адміністратора.

Дякую. (Це здається досить базовим запитом, але якщо я повинен щось переробити, я відкритий для пропозицій.)


Дякую за натяк на "limit_choices_to". Це не вирішує ваше запитання, але моє :-) Документи: docs.djangoproject.com/en/dev/ref/models/fields/…
guettli

Відповіді:


243

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 = ...

Це робиться явно на виду. Жодного злому навколо.


Гаразд, це звучить багатообіцяюче. Як отримати доступ до відповідного об'єкта Field? form.company.QuerySet = Rate.objects.filter (company_id = the_company.id)? чи через словник?
Том

1
Добре, дякую за розширення прикладу, але мені здається, що я повинен використовувати form.fields ["rate"]. Набір запитів, щоб уникнути "" ClientForm 'об'єкт не має атрибута "rate" ", я щось пропускаю? (і ваш приклад має бути form.rate.queryset, щоб бути також послідовним.)
Том

8
Чи не було б краще встановити набір запитів полів у __init__методі форми ?
Лакшман Прасад

1
@SLott останній коментар невірний (або мій сайт не повинен працювати :). Ви можете заповнити дані перевірки, скориставшись викликом super (...) .__ init__ у вашому переосмисленому методі. Якщо ви робите декілька цих наборів запитів, це набагато більш елегантно упакувати їх, замінивши метод init .
Майкл

3
@Slott привіт, я додав відповідь, оскільки для пояснення потрібно більше 600 символів. Навіть якщо це питання старе, воно отримує високий бал google.
Майкл

135

На додаток до відповіді С.Лотта і про те, як Гугур згадується в коментарях, можна додати фільтри набору запитів шляхом зміни 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, якого є багато хороших.


У вашому першому фрагменті коду є помилка друку, ви визначаєте двічі аргументи в __init __ () замість args та kwargs.
tpk

6
Мені ця відповідь більше подобається, я вважаю, що складніше інкапсулювати логіку ініціалізації форми у класі форми, а не у способі перегляду. Ура!
Симетричний

44

Це просто і працює з 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

фільтрує вибір усіх полів іноземних ключів на веб-сайті адміністратора:

    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)

1
І я хочу додати, що це добре працює як загальна спеціальна форма для декількох моделадмінів з подібними референтними полями, що цікавлять.
nemesisfixx

Це найкраща відповідь, якщо ви використовуєте Django 1.4+
Rick Westera

16

Для цього потрібно зробити загальний вигляд, як-от 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)

, читайте тут мій пост


4

Якщо ви ще не створили форму і хочете змінити набір запитів, ви можете зробити:

formmodel.base_fields['myfield'].queryset = MyModel.objects.filter(...)

Це дуже корисно, коли ви використовуєте загальні представлення!


2

Отже, я справді намагався це зрозуміти, але здається, що Джанго все ще не робить цього просто. Я не все так глухо, але просто не можу побачити жодного (дещо) простого рішення.

Мені здається, що це дуже некрасиво для того, щоб переглядати адміністративні погляди для подібних речей, і кожен приклад, який я вважаю, ніколи повністю не стосується поглядів адміністратора.

Це настільки поширена обставина з моделями, які я вважаю, що мені здається жахливим, що для цього немає очевидного рішення ...

У мене такі заняття:

# 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. (Розміщення його на вбудованому рядку має більше сенсу, але важко посилатися на базовий договір як на "власне".)

Чи є хтось там, хто знає щось таке прямо, як цей вкрай потрібний ярлик? Коли я робив адміністраторів PHP для подібних речей, це вважалося базовим функціоналом! Насправді це було завжди автоматично, і його потрібно було відключити, якщо ви насправді цього не хотіли!


0

Більш відкритий спосіб - зателефонувавши 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.