Wie filtere ich ForeignKey-Auswahlmöglichkeiten in einer Django ModelForm?

Lesezeit: 11 Minuten

Angenommen, ich habe Folgendes in meinem 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)

Dh es gibt mehrere Companiesjeweils mit einer Reihe von Rates und Clients. Jeder Client sollte eine Basis haben Rate das von seinem Elternteil ausgewählt wird Company's Ratesnicht noch einen Company's Rates.

Beim Erstellen eines Formulars zum Hinzufügen eines Clientich möchte die entfernen Company Auswahlmöglichkeiten (da diese bereits über eine Schaltfläche “Client hinzufügen” auf der ausgewählt wurde Company Seite) und begrenzen die Rate Entscheidungen dazu Company auch.

Wie mache ich das in Django 1.0?

Meine jetzige forms.py Datei ist im Moment nur ein Boilerplate:

from models import *
from django.forms import ModelForm

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

Und die views.py ist auch grundlegend:

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

In Django 0.96 konnte ich dies hacken, indem ich vor dem Rendern der Vorlage Folgendes tat:

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

ForeignKey.limit_choices_to sieht vielversprechend aus, aber ich weiß nicht, wie ich weitermachen soll the_company.id und mir ist nicht klar, ob das sowieso außerhalb der Admin-Oberfläche funktionieren wird.

Vielen Dank. (Das scheint eine ziemlich einfache Anfrage zu sein, aber wenn ich etwas umgestalten sollte, bin ich offen für Vorschläge.)

  • Danke für den Hinweis auf “limit_choices_to”. Es löst nicht Ihre Frage, aber meine 🙂 Docs: docs.djangoproject.com/en/dev/ref/models/fields/…

    – Güttli

    7. Juni 2019 um 9:21 Uhr


  • Wenn Sie heutzutage die generischen Bearbeitungsansichten (CreateView usw.) verwenden, besteht meine bevorzugte Methode zum Filtern von ForeignKey-Auswahlmöglichkeiten in einer ModelForm darin, get_form_class() in der Ansicht zu überschreiben. Sie können dann base_fields festlegen[‘my_field_name’].limit_choices_to – siehe zB stackoverflow.com/questions/70399761

    – Martin CR

    18. Dezember 2021 um 11:58 Uhr

Benutzer-Avatar
S. Lott

ForeignKey wird durch django.forms.ModelChoiceField dargestellt, bei dem es sich um ein ChoiceField handelt, dessen Auswahlmöglichkeiten ein Modell-QuerySet sind. Siehe die Referenz für ModelChoiceField.

Stellen Sie also ein QuerySet für die Felder bereit queryset Attribut. Hängt davon ab, wie Ihr Formular aufgebaut ist. Wenn Sie ein explizites Formular erstellen, werden Felder direkt benannt.

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

Wenn Sie das standardmäßige ModelForm-Objekt verwenden, form.fields["rate"].queryset = ...

Dies geschieht explizit in der Ansicht. Kein Herumhacken.

  • Ok, das klingt vielversprechend. Wie greife ich auf das relevante Field-Objekt zu? form.company.QuerySet = Rate.objects.filter(company_id=the_company.id) ? oder über ein Wörterbuch?

    – Tom

    15. November 2008 um 2:43 Uhr

  • Ok, danke für die Erweiterung des Beispiels, aber ich muss anscheinend form.fields verwenden[“rate”].queryset, um zu vermeiden, dass das Objekt ‘ClientForm’ kein Attribut ‘Rate’ hat, fehlt mir etwas? (und Ihr Beispiel sollte form.rate.queryset sein, um auch konsistent zu sein.)

    – Tom

    15. November 2008 um 3:36 Uhr

  • Wäre es nicht besser, den Abfragesatz der Felder in den Formularen festzulegen __init__ Methode?

    – lprsd

    3. August 2009 um 21:03 Uhr

  • @SLott der letzte Kommentar ist nicht korrekt (oder meine Seite sollte nicht funktionieren :). Sie können die Validierungsdaten füllen, indem Sie den Aufruf super(…).__init__ in Ihrer überschriebenen Methode verwenden. Wenn Sie mehrere dieser Abfragesatzänderungen vornehmen, ist es viel eleganter, sie zu packen, indem Sie die überschreiben drin Methode.

    – michael

    7. August 2009 um 4:53 Uhr

  • @Slott Prost, ich habe eine Antwort hinzugefügt, da zur Erklärung mehr als 600 Zeichen erforderlich wären. Auch wenn diese Frage alt ist, bekommt sie einen hohen Google-Score.

    – michael

    7. August 2009 um 13:19 Uhr

Benutzer-Avatar
michael

Zusätzlich zu der Antwort von S.Lott und wie in den Kommentaren erwähnt, ist es möglich, die Abfragesatzfilter durch Überschreiben von hinzuzufügen ModelForm.__init__ Funktion. (Dies könnte leicht auf normale Formulare zutreffen) es kann bei der Wiederverwendung helfen und hält die Ansichtsfunktion aufgeräumt.

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

Dies kann für die Wiederverwendung nützlich sein, wenn Sie beispielsweise allgemeine Filter für viele Modelle benötigen (normalerweise deklariere ich eine abstrakte Form-Klasse). Z.B

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

Abgesehen davon stelle ich nur Django-Blog-Material wieder her, von dem es viele gute gibt.

  • Es gibt einen Tippfehler in Ihrem ersten Code-Snippet, Sie definieren Argumente zweimal in __init__() anstelle von Args und Kwargs.

    – tpk

    24. Januar 2010 um 17:55 Uhr

  • Ich mag diese Antwort besser, ich denke, es ist sauberer, die Formularinitialisierungslogik in der Formularklasse und nicht in der Ansichtsmethode zu kapseln. Prost!

    – Symmetrisch

    13. Oktober 2012 um 19:49 Uhr

Benutzer-Avatar
neil.millikin

Das ist einfach und funktioniert mit 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
    ....

Sie müssen dies nicht in einer Formularklasse angeben, sondern können dies direkt in ModelAdmin tun, da Django diese integrierte Methode bereits in ModelAdmin enthält (aus den Dokumenten):

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)

Ein noch raffinierterer Weg, dies zu tun (z. B. beim Erstellen einer Front-End-Verwaltungsschnittstelle, auf die Benutzer zugreifen können), besteht darin, ModelAdmin zu unterteilen und dann die folgenden Methoden zu ändern. Das Endergebnis ist eine Benutzeroberfläche, die ihnen NUR Inhalte zeigt, die sich auf sie beziehen, während Sie (ein Superuser) alles sehen können.

Ich habe vier Methoden überschrieben, die ersten beiden machen es einem Benutzer unmöglich, etwas zu löschen, und es entfernt auch die Löschschaltflächen von der Admin-Site.

Die dritte Überschreibung filtert alle Abfragen, die einen Verweis auf enthalten (im Beispiel „Benutzer“ oder „Stachelschwein“ (nur zur Veranschaulichung).

Die letzte Überschreibung filtert jedes Fremdschlüsselfeld im Modell, um die verfügbaren Auswahlmöglichkeiten genauso zu filtern wie der grundlegende Abfragesatz.

Auf diese Weise können Sie eine einfach zu verwaltende Front-Admin-Site präsentieren, die es Benutzern ermöglicht, mit ihren eigenen Objekten herumzuspielen, und Sie müssen nicht daran denken, die spezifischen ModelAdmin-Filter einzugeben, über die wir oben gesprochen haben.

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)

Entfernen Sie die Schaltflächen “Löschen”:

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

verhindert die Löschberechtigung

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

filtert Objekte, die auf der Admin-Site angezeigt werden können:

    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

filtert Auswahlmöglichkeiten für alle Foreignkey-Felder auf der Admin-Site:

    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)

  • Und ich sollte hinzufügen, dass dies gut als generisches benutzerdefiniertes Formular für mehrere Modelladministratoren mit ähnlichen Interessengebieten funktioniert.

    – JWL

    27. Juli 2013 um 10:39 Uhr

  • Dies ist die beste Antwort, wenn Sie Django 1.4+ verwenden

    – Rick Westera

    26. März 2015 um 0:49 Uhr


Benutzer-Avatar
Hassek

Wenn Sie das Formular nicht erstellt haben und den Abfragesatz ändern möchten, können Sie Folgendes tun:

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

Dies ist ziemlich nützlich, wenn Sie generische Ansichten verwenden!

  • Ich habe zuerst die Queryset-Zuweisung in das Formular eingefügt, aber ich habe einen Formularfehler erhalten. Durch das Verschieben der Queryset-Zuweisung in die Ansicht, kein Fehler mehr.

    – Rick Gräber

    15. August 2021 um 23:10 Uhr

Also, ich habe wirklich versucht, das zu verstehen, aber es scheint, dass Django das immer noch nicht sehr einfach macht. Ich bin nicht ganz so dumm, aber ich sehe einfach keine (ziemlich) einfache Lösung.

Ich finde es im Allgemeinen ziemlich hässlich, die Admin-Ansichten für solche Dinge überschreiben zu müssen, und jedes Beispiel, das ich finde, trifft nie vollständig auf die Admin-Ansichten zu.

Dies ist ein so häufiger Umstand bei den Modellen, die ich mache, dass ich es entsetzlich finde, dass es dafür keine offensichtliche Lösung gibt …

Ich habe diese Klassen:

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

Dies führt zu einem Problem beim Einrichten des Admin für das Unternehmen, da es Inlines für Vertrag und Standort hat und die m2m-Optionen des Vertrags für den Standort nicht richtig nach dem Unternehmen gefiltert werden, das Sie gerade bearbeiten.

Kurz gesagt, ich bräuchte eine Admin-Option, um so etwas zu tun:

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

Letztendlich wäre es mir egal, ob der Filterprozess auf der Basis CompanyAdmin oder auf ContractInline platziert wurde. (Das Platzieren in der Inline ist sinnvoller, macht es jedoch schwierig, den Basisvertrag als „selbst“ zu referenzieren.)

Gibt es jemanden da draußen, der etwas so Einfaches wie diese dringend benötigte Abkürzung kennt? Damals, als ich PHP-Admins für solche Dinge erstellte, galt dies als grundlegende Funktionalität! Tatsächlich war es immer automatisch und musste deaktiviert werden, wenn Sie es wirklich nicht wollten!

Benutzer-Avatar
Schwimmende Kiwi

Ein öffentlicherer Weg ist der Aufruf von get_form in Admin-Klassen. Es funktioniert auch für Nicht-Datenbankfelder. Hier habe ich zum Beispiel ein Feld namens ‘_terminal_list’ auf dem Formular, das in besonderen Fällen verwendet werden kann, um mehrere Terminal-Elemente aus get_list(request) auszuwählen und dann basierend auf request.user zu filtern:

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

Benutzer-Avatar
Martin CR

Eine gute Möglichkeit, die Auswahlmöglichkeiten für ein ForeignKey-Feld einer ModelForm zur Laufzeit einzuschränken (z. B. in einer CreateView), ist set limit_choices_to zum base_fields['field_name'] durch Überschreiben get_form_class() in der Ansicht.

Wenn Sie beispielsweise einen Kunden erstellen, um die Tarifauswahl auf diejenigen für das Unternehmen zu beschränken, das in der URL angegeben ist:

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

1061350cookie-checkWie filtere ich ForeignKey-Auswahlmöglichkeiten in einer Django ModelForm?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy