Come filtrare le scelte di ForeignKey in un Django ModelForm?


227

Di 'che ho nel mio 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)

Vale a dire ce ne sono molti Companies, ognuno con un intervallo di Ratese Clients. Ognuno Clientdovrebbe avere una base Ratescelta dal proprio genitore Company's Rates, non un'altra Company's Rates.

Quando creo un modulo per aggiungere un Client, vorrei rimuovere le Companyscelte (dato che è già stato selezionato tramite un pulsante "Aggiungi client" sulla Companypagina) e limitare le Ratescelte anche a quello Company.

Come posso procedere in questo in Django 1.0?

Il mio forms.pyfile attuale è solo boilerplate al momento:

from models import *
from django.forms import ModelForm

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

Ed views.pyè anche di base:

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 sono stato in grado di hackerare questo facendo qualcosa di simile prima di eseguire il rendering del modello:

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

ForeignKey.limit_choices_tosembra promettente ma non so come passare the_company.ide non sono chiaro se funzionerà comunque al di fuori dell'interfaccia di amministrazione.

Grazie. (Questa sembra una richiesta piuttosto semplice ma se dovessi riprogettare qualcosa sono aperto ai suggerimenti.)


Grazie per il suggerimento di "limit_choices_to". Non risolve la tua domanda, ma la mia :-) Documenti: docs.djangoproject.com/en/dev/ref/models/fields/…
guettli

Risposte:


243

ForeignKey è rappresentato da django.forms.ModelChoiceField, che è ChoiceField le cui scelte sono un QuerySet modello. Vedere il riferimento per ModelChoiceField .

Quindi, fornire un QuerySet all'attributo del campo queryset. Dipende da come viene costruito il modulo. Se compili un modulo esplicito, avrai campi chiamati direttamente.

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

Se si prende l'oggetto ModelForm predefinito, form.fields["rate"].queryset = ...

Questo viene fatto esplicitamente nella vista. Nessun hack in giro.


Ok, sembra promettente. Come accedo all'oggetto Field pertinente? form.company.QuerySet = Rate.objects.filter (company_id = the_company.id)? o tramite un dizionario?
Tom,

1
Ok, grazie per l'espansione dell'esempio, ma mi sembra di dover usare form.fields ["rate"]. Queryset per evitare "l'oggetto" ClientForm "non ha attributo" rate "", mi sto perdendo qualcosa? (e il tuo esempio dovrebbe essere form.rate.queryset anche per essere coerente.)
Tom

8
Non sarebbe meglio impostare il queryset dei campi, nel __init__metodo del form ?
Lakshman Prasad,

1
@SLott l'ultimo commento non è corretto (o il mio sito non dovrebbe funzionare :). È possibile popolare i dati di convalida effettuando la chiamata super (...) .__ init__ nel metodo sovrascritto. Se stai apportando alcune di queste modifiche al queryset è molto più elegante impacchettarle sovrascrivendo il metodo init .
michael

3
@Slott evviva, ho aggiunto una risposta in quanto occorrerebbero più di 600 caratteri per spiegare. Anche se questa domanda è vecchia sta ottenendo un punteggio elevato di Google.
michael,

135

Oltre alla risposta di S.Lott e come diventandoGuru menzionato nei commenti, è possibile aggiungere i filtri del queryset ignorando la ModelForm.__init__funzione. (Questo potrebbe facilmente applicarsi ai moduli regolari) può aiutare con il riutilizzo e mantiene in ordine la funzione di visualizzazione.

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

Questo può essere utile per il riutilizzo, ad esempio se hai molti filtri comuni necessari su molti modelli (normalmente dichiaro una classe Form astratta). Per esempio

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

A parte questo, sto solo riformulando il materiale del blog Django di cui ce ne sono molti di buoni là fuori.


C'è un refuso nel tuo primo frammento di codice, stai definendo args due volte in __init __ () invece di args e kwargs.
TPK

6
Mi piace meglio questa risposta, penso che sia più pulito incapsulare la logica di inizializzazione del modulo nella classe del modulo, piuttosto che nel metodo di visualizzazione. Saluti!
Simmetrico,

44

Questo è semplice e funziona con 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
    ....

Non è necessario specificarlo in una classe di modulo, ma è possibile farlo direttamente in ModelAdmin, poiché Django include già questo metodo integrato in ModelAdmin (dai documenti):

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)

Un modo ancora più efficace per farlo (ad esempio nella creazione di un'interfaccia di amministrazione front-end a cui gli utenti possono accedere) è quello di sottoclassare ModelAdmin e quindi modificare i metodi di seguito. Il risultato netto è un'interfaccia utente che mostra SOLO il contenuto a loro correlato, consentendo allo stesso tempo (un superutente) di vedere tutto.

Ho ignorato quattro metodi, i primi due rendono impossibile per un utente eliminare qualsiasi cosa e rimuove anche i pulsanti di eliminazione dal sito di amministrazione.

La terza sostituzione filtra qualsiasi query che contenga un riferimento (nell'esempio "utente" o "istrice" (solo come illustrazione).

L'ultima sostituzione filtra qualsiasi campo chiave esterna nel modello per filtrare le scelte disponibili allo stesso modo del set di query di base.

In questo modo, puoi presentare un sito di amministrazione frontale di facile gestione che consente agli utenti di pasticciare con i propri oggetti e non devi ricordare di digitare i filtri specifici di ModelAdmin di cui abbiamo parlato sopra.

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)

rimuovere i pulsanti "elimina":

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

impedisce l'autorizzazione all'eliminazione

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

filtra gli oggetti che possono essere visualizzati sul sito di amministrazione:

    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

filtra le scelte per tutti i campi di chiave straniera sul sito di amministrazione:

    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
E dovrei aggiungere che questo funziona bene come un modulo personalizzato generico per più amministratori di modello con campi di riferimento simili di interesse.
nemesisfixx,

Questa è la risposta migliore se usi Django 1.4+
Rick Westera il

16

Per fare ciò con una vista generica, come 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'))

la parte più importante di quello ...

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

, leggi il mio post qui


4

Se non hai creato il modulo e desideri modificare il set di query, puoi fare:

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

Questo è molto utile quando si usano viste generiche!


2

Quindi, ho davvero provato a capirlo, ma sembra che Django non lo renda ancora molto semplice. Non sono così stupido, ma non riesco proprio a vedere alcuna (in qualche modo) soluzione semplice.

Trovo generalmente piuttosto brutto dover sovrascrivere le viste dell'amministratore per questo genere di cose e ogni esempio che trovo non si applica mai completamente alle viste dell'amministratore.

Questa è una circostanza così comune con i modelli che faccio che trovo spaventoso che non ci sia una soluzione ovvia a questo ...

Ho queste lezioni:

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

Ciò crea un problema durante la configurazione dell'amministratore per la società, poiché ha inline sia per il contratto che per la posizione e le opzioni m2m del contratto per la posizione non vengono filtrate correttamente in base alla società che si sta attualmente modificando.

In breve, avrei bisogno di alcune opzioni di amministrazione per fare qualcosa del genere:

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

In definitiva, non mi importerebbe se il processo di filtraggio fosse inserito nell'Admin Company di base o se fosse inserito nel ContractInline. (Inserirlo in linea ha più senso, ma rende difficile fare riferimento al Contratto di base come 'sé'.)

C'è qualcuno là fuori che sa qualcosa di così semplice come questa scorciatoia tanto necessaria? Quando ho creato amministratori PHP per questo genere di cose, questa era considerata una funzionalità di base! In effetti, era sempre automatico e doveva essere disabilitato se davvero non lo volevi!


0

Un modo più pubblico è chiamando get_form nelle classi Admin. Funziona anche per campi non di database. Ad esempio qui ho un campo chiamato '_terminal_list' sul modulo che può essere utilizzato in casi speciali per la scelta di diversi elementi terminali da get_list (richiesta), quindi il filtro in base a 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.