Jak filtrować wybory ForeignKey w formularzu modelu Django?

227

Powiedz, że mam w sobie następujące 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)

Tzn. Jest ich wiele Companies, z których każdy ma zakres Ratesi Clients. Każdy Clientpowinien mieć bazę Ratewybraną spośród jego rodzica Company's Rates, a nie inną Company's Rates.

Tworząc formularz dodawania a Client, chciałbym usunąć Companyopcje (ponieważ zostały one już wybrane za pomocą przycisku „Dodaj klienta” na Companystronie) i ograniczyć również te Rateopcje Company.

Jak to zrobić w Django 1.0?

Mój aktualny forms.pyplik jest w tej chwili tylko szablonem:

from models import *
from django.forms import ModelForm

class ClientForm(ModelForm):
    class Meta:
        model = Client

I to views.pyjest również podstawowe:

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})

W Django 0.96 byłem w stanie włamać się do tego, wykonując coś takiego przed renderowaniem szablonu:

manipulator.fields[0].choices = [(r.id,r.name) for r in Rate.objects.filter(company_id=the_company.id)]

ForeignKey.limit_choices_to wydaje się obiecujące, ale nie wiem, jak się poddać the_company.id i nie jestem i tak zadziała poza interfejsem administratora.

Dzięki. (To wydaje się być dość prostą prośbą, ale jeśli powinienem przeprojektować coś, jestem otwarty na sugestie).

Tomek
źródło
Dziękujemy za podpowiedź do „limit_choices_to”. To nie rozwiązuje twojego pytania, ale moje :-) Dokumenty: docs.djangoproject.com/en/dev/ref/models/fields/…
guettli

Odpowiedzi:

243

ForeignKey jest reprezentowany przez django.forms.ModelChoiceField, czyli ChoiceField, którego wybory to model QuerySet. Zobacz odniesienie do ModelChoiceField .

Zatem podaj querysetatrybut QuerySet dla atrybutu pola . Zależy od sposobu zbudowania formularza. Jeśli zbudujesz jawny formularz, będziesz mieć pola nazwane bezpośrednio.

form.rate.queryset = Rate.objects.filter(company_id=the_company.id)

Jeśli weźmiesz domyślny obiekt ModelForm, form.fields["rate"].queryset = ...

Odbywa się to wyraźnie w widoku. Żadnego hakowania.

S.Lott
źródło
Ok, to brzmi obiecująco. Jak uzyskać dostęp do odpowiedniego obiektu Field? form.company.QuerySet = Rate.objects.filter (company_id = the_company.id)? lub przez słownik?
Tom
1
Ok, dziękuję za rozwinięcie przykładu, ale wydaje mi się, że muszę użyć form.fields [„oceń”]. Zestaw zapytań, aby uniknąć obiektu „ClientForm” nie ma atrybutu „oceń”, czy coś mi brakuje? (a twój przykład powinien być również form.rate.queryset, aby zachować spójność.)
Tom
8
Czy nie byłoby lepiej ustawić zestaw zapytań pól w __init__metodzie formularza ?
Lakshman Prasad
1
@SLott ostatni komentarz jest nieprawidłowy (lub moja strona nie powinna działać :). Dane dotyczące sprawdzania poprawności można wypełnić, wykonując wywołanie super (...) .__ init__ w przesłoniętej metodzie. Jeśli wprowadzasz kilka z tych zestawów zapytań, znacznie bardziej elegancko jest je spakować, zastępując metodę init .
Michael
3
@ Slott wiwatuje, dodałem odpowiedź, ponieważ wyjaśnienie zajęłoby ponad 600 znaków. Nawet jeśli to pytanie jest stare, ma wysoki wynik w Google.
Michael
135

Oprócz odpowiedzi S.Lott i gdy staje sięGuru wspomnianym w komentarzach, możliwe jest dodanie filtrów zestawu zapytań przez przesłonięcie ModelForm.__init__funkcji. (Może to łatwo dotyczyć zwykłych formularzy), może pomóc w ponownym użyciu i utrzymuje porządek w funkcji widoku.

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})

Może to być przydatne do ponownego użycia, powiedzmy, jeśli potrzebujesz wspólnych filtrów w wielu modelach (zwykle deklaruję abstrakcyjną klasę Form). Na przykład

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')

Poza tym po prostu powtarzam materiały na blogu Django, z których jest wiele dobrych.

Michael
źródło
W pierwszym fragmencie kodu jest literówka, dwa razy definiujesz argumenty w __init __ () zamiast args i kwargs.
tpk
6
Bardziej podoba mi się ta odpowiedź, myślę, że bardziej przejrzyste jest enkapsulowanie logiki inicjalizacji formularza w klasie form niż w metodzie view. Twoje zdrowie!
Symetryczny
44

Jest to proste i działa z 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
    ....

Nie musisz tego określać w klasie formularza, ale możesz to zrobić bezpośrednio w ModelAdmin, ponieważ Django już zawiera tę wbudowaną metodę w ModelAdmin (z dokumentacji):

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)

Jeszcze szybszym sposobem na to (na przykład w tworzeniu interfejsu administracyjnego frontonu, do którego użytkownicy mogą uzyskać dostęp) jest podklasa ModelAdmin, a następnie zmiana poniższych metod. Rezultatem netto jest interfejs użytkownika, który TYLKO pokazuje im powiązaną z nimi treść, jednocześnie pozwalając Tobie (superużytkownikowi) zobaczyć wszystko.

Zastąpiłem cztery metody, dwie pierwsze uniemożliwiają użytkownikowi usunięcie czegokolwiek, a także usuwa przyciski usuwania z witryny administratora.

Trzecie przesłonięcie filtruje każde zapytanie zawierające odwołanie do (w przykładzie „użytkownik” lub „jeżozwierz” (tylko jako ilustracja).

Ostatnie przesłonięcie filtruje dowolne pole klucza obcego w modelu, aby filtrować dostępne opcje tak samo, jak podstawowy zestaw zapytań.

W ten sposób możesz zaprezentować łatwą w zarządzaniu frontową stronę administracyjną, która pozwala użytkownikom na bałagan z własnymi obiektami i nie musisz pamiętać o wpisywaniu określonych filtrów ModelAdmin, o których mówiliśmy powyżej.

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)

usuń przyciski „usuń”:

    def get_actions(self, request):
        actions = super(FrontEndAdmin, self).get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions

uniemożliwia usunięcie uprawnienia

    def has_delete_permission(self, request, obj=None):
        return False

filtruje obiekty, które można oglądać na stronie administratora:

    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

filtruje opcje dla wszystkich pól kluczy obcych w witrynie administratora:

    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)
neil.millikin
źródło
1
I powinienem dodać, że działa to dobrze jako ogólny formularz niestandardowy dla wielu administratorów modeli o podobnych polach zainteresowania.
nemesisfixx
To najlepsza odpowiedź, jeśli używasz Django 1.4+
Rick Westera
16

Aby to zrobić za pomocą widoku ogólnego, takiego jak 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'))

najważniejsza część tego ...

    context['form'].fields['project'].queryset = Project.objects.for_user(self.request.user)

, przeczytaj mój post tutaj

teewuane
źródło
4

Jeśli nie utworzyłeś formularza i chcesz zmienić zestaw zapytań, możesz:

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

Jest to bardzo przydatne, gdy używasz widoków ogólnych!

Hassek
źródło
2

Naprawdę próbowałem to zrozumieć, ale wygląda na to, że Django wciąż nie czyni tego bardzo prostym. Nie jestem aż tak głupi, ale po prostu nie widzę żadnego (nieco) prostego rozwiązania.

Uważam, że generalnie dość brzydka jest konieczność przesłonięcia widoków administratora dla tego rodzaju rzeczy, a każdy przykład, który uważam, nigdy nie dotyczy w pełni widoków administratora.

Jest to tak powszechna okoliczność w przypadku modeli, które sprawiają, że przerażam to, że nie ma oczywistego rozwiązania tego ...

Mam te zajęcia:

# 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)

Stwarza to problem podczas konfigurowania Administratora dla firmy, ponieważ ma on wstawki zarówno dla Kontraktu, jak i Lokalizacji, a opcje m2m Kontraktu dla Lokalizacji nie są odpowiednio filtrowane według Firmy, którą aktualnie edytujesz.

Krótko mówiąc, potrzebuję opcji administratora, aby zrobić coś takiego:

# 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')

Ostatecznie nie dbam o to, czy proces filtrowania został umieszczony na podstawowym CompanyAdmin, czy też na ContractInline. (Umieszczenie go w linii jest bardziej sensowne, ale utrudnia odniesienie do podstawowej umowy jako „ja”).

Czy jest ktoś, kto wie o czymś tak prostym, jak ten tak potrzebny skrót? Kiedy tworzyłem administratorów PHP dla tego typu rzeczy, uważano to za podstawową funkcjonalność! W rzeczywistości zawsze był automatyczny i musiał być wyłączony, jeśli naprawdę tego nie chciałeś!

Tim
źródło
0

Bardziej publicznym sposobem jest wywołanie get_form w klasach administracyjnych. Działa również w przypadku pól innych niż bazy danych. Na przykład tutaj mam pole o nazwie „_terminal_list” w formularzu, które może być używane w szczególnych przypadkach do wybierania kilku pozycji końcowych z get_list (żądanie), a następnie filtrowania na podstawie 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
F.Tamy
źródło