2008-11-15 12 views
188

Sagen wir, ich habe folgendes in meinem models.py:Wie filtere ich ForeignKey-Auswahlen in einem Django ModelForm?

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) 

D.h. es gibt mehrere Companies, die jeweils einen Bereich von Rates und Clients haben. Jede Client sollte eine Basis Rate haben, die aus ihrer ursprünglichen , nicht anderen Company's Rates gewählt wird.

Wenn das Erstellen eines Formulars eine Client für das Hinzufügen, ich möchte die Company Auswahl entfernen (wie die bereits über einen „Add Client“ auf der Company Seite ausgewählt wurde) und begrenzen die Rate Entscheidungen zu diesem Company auch .

Wie gehe ich darüber in Django 1.0?

Meine aktuelle forms.py Datei ist nur vorformulierten im Moment:

from models import * 
from django.forms import ModelForm 

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

Und die views.py ist auch Grund:

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 Ich konnte dies etwas, indem Sie hacken in wie Folgendes vor dem Rendern der Vorlage:

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

ForeignKey.limit_choices_to scheint vielversprechend, aber ich weiß nicht, wie man the_company.id weitergeben und ich bin nicht klar, ob das sowieso außerhalb der Admin-Schnittstelle funktioniert.

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

Antwort

200

ForeignKey wird durch django.forms.ModelChoiceField repräsentiert, welches ein ChoiceField ist, dessen Auswahlmöglichkeiten ein Model QuerySet sind. Siehe die Referenz für ModelChoiceField.

Stellen Sie also ein QuerySet für das Feld queryset Attribut bereit. Hängt davon ab, wie Ihre Form aufgebaut ist. Wenn Sie ein explizites Formular erstellen, haben Sie direkt benannte Felder.

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

Wenn Sie die Standardmodelform Objekt nehmen, form.fields["rate"].queryset = ...

Diese explizit in der Ansicht durchgeführt wird. Kein hacken herum.

+0

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

+1

Ok, danke für die Erweiterung des Beispiels, aber ich scheine form.fields ["rate"] zu verwenden. Querieset um zu vermeiden, '' ClientForm 'Objekt hat kein Attribut' rate '", fehlt mir etwas? (und Ihr Beispiel sollte auch form.rate.queryset sein, um konsistent zu sein.) – Tom

+0

Ausgezeichnet, danke für die Klärung. Wenn Sie Ihre Antwort über einen Kommentar bearbeiten möchten, sollten Sie sie später zur Kenntnis nehmen, da Änderungen nicht auf der Registerkarte "Antworten" auf meiner Benutzerseite angezeigt werden. – Tom

115

Zusätzlich zu S.Lott's Antwort und als in Kommentaren kommentingGuru ist es möglich, die Filter des Abfragefiles durch Überschreiben der ModelForm.__init__ Funktion hinzuzufügen. (Dies könnte leicht für reguläre Formulare gelten.) Es kann bei der Wiederverwendung helfen und hält die Ansichtsfunktion sauber.

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 zur Wiederverwendung nützlich sein, sagen, wenn Sie bei vielen Modellen benötigt gemeinsame Filter haben (in der Regel ich eine abstrakte Form-Klasse deklarieren). 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') 

Ansonsten rede ich nur Django Blog-Material, von denen es viele gute da draußen gibt.

+0

In Ihrem ersten Code-Snippet ist ein Tippfehler enthalten. Sie definieren Argumente zweimal in __init __() anstelle von Argumenten und Kwargen. – tpk

+0

Prost, das ist aktualisiert – michael

+5

Ich mag diese Antwort besser, ich denke, es ist sauberer, die Formularinitialisierungslogik in der Formularklasse zu kapseln, anstatt in der Ansichtsmethode. Prost! – Symmetric

2

Also, ich habe wirklich versucht, das zu verstehen, aber es scheint, dass Django das immer noch nicht so 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 diese Art von Sache zu überschreiben, und jedes Beispiel, das ich finde, trifft nie vollständig auf die Admin-Ansichten zu.

Dies ist ein solcher gemeinsamer Umstand mit den Modellen, die ich machen, dass ich es schrecklich finden, dass es zu dieser keine offensichtliche Lösung ist ...

Ich habe diese Klassen bekam:

# 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 schafft Ein Problem bei der Einrichtung des Administrators für Unternehmen, da es Inlines für Vertrag und Standort sowie die m2m-Optionen für Standort für Contract enthält, die nicht ordnungsgemäß nach dem Unternehmen gefiltert werden, das Sie gerade bearbeiten.

Kurz gesagt, ich würde einig Admin-Option benötigen, 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ürde ich nicht egal, wenn der Filterprozess auf der Basis Company platziert wurde, oder ob es auf dem platziert Vertragsinline. (Es ist sinnvoller, es auf Inline zu platzieren, aber es macht es schwierig, den Basisvertrag als 'Selbst' zu bezeichnen.)

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 so etwas gemacht habe, wurde dies als grundlegende Funktionalität betrachtet! In der Tat war es immer automatisch und musste deaktiviert werden, wenn Sie es wirklich nicht wollten!

3

Wenn Sie nicht das Formular erstellt haben, und wollen die queryset ändern können Sie tun:

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

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

15

Um dies zu tun mit einer allgemeinen Ansicht, wie Create ...

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

den wichtigsten Teil davon ...

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

, read my post here

37

Dieses einfachen ist, und arbeitet 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 brauchen nicht diese in einer Formularklasse angeben, sondern kann es direkt in der Modeladmin tun, als Django bereits dieses eingebaut auf dem Modeladmin Verfahren enthält (aus der Dokumentation):

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 netterer Weg, dies zu tun (zum Beispiel beim Erstellen einer Frontend-Admin-Schnittstelle, auf die Benutzer zugreifen können), besteht darin, die ModelAdmin zu untergliedern und dann die folgenden Methoden zu ändern. Das Endergebnis ist eine Benutzerschnittstelle, die NUR ihnen Inhalt anzeigt, der mit ihnen verwandt ist, während es Ihnen (einem Superbenutzer) erlaubt, alles zu sehen.

Ich habe vier Methoden überschrieben, die ersten beiden machen es für einen Benutzer unmöglich, etwas zu löschen, und es entfernt auch die Schaltflächen zum Löschen von der Verwaltungsseite.

Die dritte Überschreibung filtert jede Abfrage, die einen Verweis auf (im Beispiel ‚Benutzer‘ oder ‚Stachelschwein‘ (nur als Illustration enthält).

Die letzte Überschreibung filtert jegliche ForeignKey Feld in dem Modell die filtern Die verfügbaren Optionen sind die gleichen wie die grundlegenden Abfrage-Sets

Auf diese Weise können Sie eine leicht zu verwaltende Front-Admin-Site präsentieren, die es Benutzern ermöglicht, sich mit ihren eigenen Objekten zu messen, und Sie müssen nicht mehr tippen die spezifischen ModelAdmin-Filter, ü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 'löschen' Buttons:

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

verhindert Erlaubnis

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

Filter Objekte löschen, die auf dem Server-Betreiber-Website eingesehen werden kann:

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 

Auswahl-Filter für alle foreignkey Felder auf die Verwaltungsseite:

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

Und ich sollte hinzufügen, dass dies gut als ein generisches benutzerdefiniertes Formular für mehrere Modeladmins mit ähnlichen Referenzfeldern von Interesse funktioniert. – nemesisfixx

+0

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

0

Ein öffentlicher Weg ist der Aufruf von get_form in Admin-Klassen. Es funktioniert auch für Nicht-Datenbankfelder. Zum Beispiel hier habe ich ein Feld ‚_terminal_list‘ auf dem Formular aufgerufen, die in besonderen Fällen für die Wahl mehrere Terminal Elemente aus get_list (Anfrage) verwendet werden kann, dann basierte Filterung auf 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