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

227

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 Companies, die jeweils einen Bereich von Ratesund haben Clients. Jeder Clientsollte eine Basis haben Rate, die von seinem Elternteil ausgewählt wird Company's Rates, nicht eine andere Company's Rates.

Beim Erstellen eines Formulars zum Hinzufügen eines Clientmöchte ich die CompanyAuswahlmöglichkeiten entfernen (da dies bereits über die Schaltfläche "Client hinzufügen " auf der CompanySeite ausgewählt wurde) und die RateAuswahlmöglichkeiten Companyauch darauf beschränken.

Wie gehe ich in Django 1.0 vor?

Meine aktuelle forms.pyDatei ist im Moment nur Boilerplate:

from models import *
from django.forms import ModelForm

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

Und das views.pyist 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_toscheint vielversprechend, aber ich weiß nicht, wie the_company.idich weitergeben soll, und ich bin mir nicht sicher , ob das außerhalb der Admin-Oberfläche trotzdem funktioniert.

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

Tom
quelle
Vielen Dank für den Hinweis auf "limit_choices_to". Es löst nicht Ihre Frage, aber meine :-) Docs: docs.djangoproject.com/de/dev/ref/models/fields/…
guettli

Antworten:

243

ForeignKey wird durch django.forms.ModelChoiceField dargestellt, ein ChoiceField, dessen Auswahl ein QuerySet-Modell ist. Siehe die Referenz für ModelChoiceField .

Geben Sie also ein QuerySet für das Feldattribut an queryset. Hängt davon ab, wie Ihr Formular erstellt wird. Wenn Sie ein explizites Formular erstellen, werden Felder direkt benannt.

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

Wenn Sie das Standard-ModelForm-Objekt verwenden, form.fields["rate"].queryset = ...

Dies erfolgt explizit in der Ansicht. Kein herumhacken.

S.Lott
quelle
Ok, das klingt vielversprechend. Wie greife ich auf das entsprechende Feldobjekt zu? form.company.QuerySet = Rate.objects.filter (company_id = the_company.id)? oder über ein Wörterbuch?
Tom
1
Ok, danke für die Erweiterung des Beispiels, aber ich muss anscheinend form.fields ["rate"]. Queryset verwenden, 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
8
Wäre es nicht besser, das Abfrageset der Felder in der __init__Methode des Formulars festzulegen ?
Lakshman Prasad
1
@SLott der letzte Kommentar ist nicht korrekt (oder meine Seite sollte nicht funktionieren :). Sie können die Validierungsdaten mit dem Aufruf super (...) .__ init__ in Ihrer überschriebenen Methode füllen. Wenn Sie mehrere dieser Abfragesatzänderungen vornehmen, ist es viel eleganter, sie durch Überschreiben der init- Methode zu verpacken .
Michael
3
@Slott Prost, ich habe eine Antwort hinzugefügt, da es mehr als 600 Zeichen dauern würde, um zu erklären. Selbst wenn diese Frage alt ist, erhält sie einen hohen Google-Score.
Michael
135

Zusätzlich zu S.Lotts Antwort und wie in Kommentaren erwähnt wird Guru, ist es möglich, die Abfragesatzfilter durch Überschreiben der ModelForm.__init__Funktion hinzuzufügen . (Dies kann leicht für normale Formulare gelten.) Es kann bei der Wiederverwendung hilfreich sein und die Ansichtsfunktion aufgeräumt halten.

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 nützlich sein, wenn Sie beispielsweise für viele Modelle gemeinsame Filter benötigen (normalerweise deklariere ich eine abstrakte Formularklasse). 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 wiederhole ich nur das Django-Blog-Material, von dem es viele gute gibt.

Michael
quelle
Ihr erstes Code-Snippet enthält einen Tippfehler. Sie definieren Argumente zweimal in __init __ () anstelle von Argumenten und Warnungen.
tpk
6
Diese Antwort gefällt mir besser, ich denke, es ist sauberer, die Formularinitialisierungslogik in der Formularklasse zu kapseln, als in der Ansichtsmethode. Prost!
Symmetrisch
44

Dies 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, können dies jedoch 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)

Eine noch raffiniertere Möglichkeit, dies zu tun (z. B. beim Erstellen einer Front-End-Administrationsoberfläche, auf die Benutzer zugreifen können), besteht darin, ModelAdmin in Unterklassen zu unterteilen und anschließend die folgenden Methoden zu ändern. Das Nettoergebnis ist eine Benutzeroberfläche, die NUR Inhalte anzeigt, 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 das grundlegende Abfrageset.

Auf diese Weise können Sie eine einfach zu verwaltende, nach vorne gerichtete Administrationssite präsentieren, auf der Benutzer mit ihren eigenen Objekten herumspielen können, und Sie müssen nicht daran denken, die oben genannten spezifischen ModelAdmin-Filter einzugeben.

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)

Schaltflächen zum Löschen entfernen:

    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 die Auswahlmöglichkeiten für alle Fremdschlüsselfelder 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)
neil.millikin
quelle
1
Und ich sollte hinzufügen, dass dies gut als generisches benutzerdefiniertes Formular für mehrere Modeladmins mit ähnlichen Referenzfeldern von Interesse funktioniert.
Nemesisfixx
Dies ist die beste Antwort, wenn Sie Django 1.4+
Rick Westera
16

Um dies mit einer generischen Ansicht wie CreateView zu tun ...

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

der wichtigste Teil davon ...

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

, lies meinen Beitrag hier

teewuane
quelle
4

Wenn Sie das Formular nicht erstellt haben und das Abfrageset ändern möchten, haben Sie folgende Möglichkeiten:

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

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

Hassek
quelle
2

Ich habe wirklich versucht, das zu verstehen, aber es scheint, dass Django dies immer noch nicht sehr einfach macht. Ich bin nicht so dumm, aber ich kann einfach keine (etwas) einfache Lösung sehen.

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, gilt nie vollständig für die Admin-Ansichten.

Dies ist ein so häufiger Umstand bei den Modellen, die ich herstelle, dass ich es entsetzlich finde, dass es keine offensichtliche Lösung dafür 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 Administrators für das Unternehmen, da es Inlines für Vertrag und Standort enthält und die m2m-Optionen des Vertrags für den Standort nicht ordnungsgemäß nach dem Unternehmen gefiltert werden, das Sie gerade bearbeiten.

Kurz gesagt, ich würde eine Admin-Option benötigen, 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 der ContractInline platziert wurde. (Es ist sinnvoller, es in die Inline zu setzen, aber es macht es schwierig, den Basisvertrag als "Selbst" zu bezeichnen.)

Gibt es jemanden da draußen, der etwas weiß, das so einfach ist wie diese dringend benötigte Abkürzung? Als ich PHP-Administratoren für diese Art von Dingen machte, galt dies als Grundfunktionalität! Tatsächlich war es immer automatisch und musste deaktiviert werden, wenn Sie es wirklich nicht wollten!

Tim
quelle
0

Ein öffentlicherer Weg ist das Aufrufen von get_form in Admin-Klassen. Es funktioniert auch für Nicht-Datenbankfelder. Zum Beispiel habe ich hier ein Feld namens '_terminal_list' auf dem Formular, das in besonderen Fällen verwendet werden kann, um mehrere Terminalelemente 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
F.Tamy
quelle