Comment filtrer les choix ForeignKey dans un Django ModelForm?

227

Dites que j'ai ce qui suit dans mon 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)

C'est-à-dire qu'il y en a plusieurs Companies, chacun ayant une plage de Rateset Clients. Chacun Clientdoit avoir une base Ratechoisie parmi ses parents Company's Rates, pas une autre Company's Rates.

Lors de la création d'un formulaire pour ajouter un Client, je voudrais supprimer les Companychoix (car cela a déjà été sélectionné via un bouton "Ajouter un client" sur la Companypage) et limiter les Ratechoix à cela Companyégalement.

Comment procéder à ce sujet dans Django 1.0?

Mon forms.pydossier actuel est juste passe-partout pour le moment:

from models import *
from django.forms import ModelForm

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

Et views.pyc'est aussi basique:

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

Dans Django 0.96, j'ai pu pirater cela en faisant quelque chose comme ceci avant de rendre le modèle:

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

ForeignKey.limit_choices_tosemble prometteur mais je ne sais pas comment passer the_company.idet je ne sais pas si cela fonctionnera de toute façon en dehors de l'interface d'administration.

Merci. (Cela semble être une demande assez basique mais si je devais repenser quelque chose, je suis ouvert aux suggestions.)

À M
la source
Merci pour l'allusion à "limit_choices_to". Cela ne résout pas votre question, mais la mienne :-) Docs: docs.djangoproject.com/en/dev/ref/models/fields/…
guettli

Réponses:

243

ForeignKey est représenté par django.forms.ModelChoiceField, qui est un ChoiceField dont les choix sont un modèle QuerySet. Voir la référence pour ModelChoiceField .

Donc, fournissez un QuerySet à l' querysetattribut du champ . Cela dépend de la façon dont votre formulaire est créé. Si vous créez un formulaire explicite, vous aurez des champs nommés directement.

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

Si vous prenez l'objet ModelForm par défaut, form.fields["rate"].queryset = ...

Cela se fait explicitement dans la vue. Pas de piratage autour.

S.Lott
la source
Ok, cela semble prometteur. Comment accéder à l'objet Field correspondant? form.company.QuerySet = Rate.objects.filter (company_id = the_company.id)? ou via un dictionnaire?
Tom
1
Ok, merci d'avoir développé l'exemple, mais il me semble que je dois utiliser form.fields ["rate"]. Queryset pour éviter que "'ClientForm' ne possède aucun attribut 'rate'", est-ce que je manque quelque chose? (et votre exemple devrait également être form.rate.queryset pour être cohérent.)
Tom
8
Ne serait-il pas préférable de définir le jeu de requêtes des champs, dans la __init__méthode du formulaire ?
Lakshman Prasad
1
@SLott le dernier commentaire n'est pas correct (ou mon site ne devrait pas fonctionner :). Vous pouvez remplir les données de validation en utilisant l'appel super (...) .__ init__ dans votre méthode substituée. Si vous effectuez plusieurs de ces changements de requêtes, il est beaucoup plus élégant de les empaqueter en remplaçant la méthode init .
Michael
3
@Slott cheers, j'ai ajouté une réponse car il faudrait plus de 600 caractères pour l'expliquer. Même si cette question est ancienne, elle obtient un score Google élevé.
michael
135

En plus de la réponse de S.Lott et comme devenirGuru mentionné dans les commentaires, il est possible d'ajouter les filtres de jeu de requêtes en remplaçant la ModelForm.__init__fonction. (Cela pourrait facilement s'appliquer aux formulaires standard), il peut aider à la réutilisation et garder la fonction d'affichage bien rangée.

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

Cela peut être utile pour la réutilisation, par exemple si vous avez des filtres communs nécessaires sur de nombreux modèles (normalement je déclare une classe de formulaire abstraite). Par exemple

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

En dehors de cela, je ne fais que reformuler le contenu du blog Django, dont il existe de nombreux bons.

Michael
la source
Il y a une faute de frappe dans votre premier extrait de code, vous définissez deux fois les arguments dans __init __ () au lieu des arguments et des kwargs.
tpk
6
J'aime mieux cette réponse, je pense qu'il est plus propre d'encapsuler la logique d'initialisation du formulaire dans la classe du formulaire, plutôt que dans la méthode view. À votre santé!
symétrique
44

C'est simple et fonctionne avec 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
    ....

Vous n'avez pas besoin de le spécifier dans une classe de formulaire, mais vous pouvez le faire directement dans ModelAdmin, car Django inclut déjà cette méthode intégrée sur ModelAdmin (à partir des documents):

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)

Une manière encore plus astucieuse de le faire (par exemple, en créant une interface d'administration frontale accessible aux utilisateurs) consiste à sous-classer le ModelAdmin, puis à modifier les méthodes ci-dessous. Le résultat net est une interface utilisateur qui leur montre UNIQUEMENT du contenu qui leur est lié, tout en vous permettant (à un super-utilisateur) de tout voir.

J'ai remplacé quatre méthodes, les deux premières empêchent un utilisateur de supprimer quoi que ce soit, et il supprime également les boutons de suppression du site d'administration.

Le troisième remplacement filtre toute requête qui contient une référence à (dans l'exemple «utilisateur» ou «porc-épic» (juste à titre d'illustration).

La dernière substitution filtre tout champ de clé étrangère dans le modèle pour filtrer les choix disponibles de la même manière que l'ensemble de requêtes de base.

De cette façon, vous pouvez présenter un site d'administration frontal facile à gérer qui permet aux utilisateurs de jouer avec leurs propres objets, et vous n'avez pas à vous rappeler de taper les filtres ModelAdmin spécifiques dont nous avons parlé ci-dessus.

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)

supprimer les boutons «supprimer»:

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

empêche l'autorisation de suppression

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

filtre les objets visibles sur le site d'administration:

    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

filtre les choix pour tous les champs de clé étrangère sur le site d'administration:

    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
la source
1
Et je dois ajouter que cela fonctionne bien en tant que formulaire personnalisé générique pour plusieurs administrateurs de modèle avec des champs de référence similaires.
nemesisfixx
C'est la meilleure réponse si vous utilisez Django 1.4+
Rick Westera
16

Pour ce faire avec une vue générique, comme 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 partie la plus importante de cela ...

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

, lisez mon article ici

teewuane
la source
4

Si vous n'avez pas créé le formulaire et souhaitez modifier l'ensemble de requêtes, vous pouvez le faire:

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

C'est assez utile lorsque vous utilisez des vues génériques!

Hassek
la source
2

J'ai donc vraiment essayé de comprendre cela, mais il semble que Django ne rend toujours pas cela très simple. Je ne suis pas du tout stupide, mais je ne vois aucune solution (quelque peu) simple.

Je trouve généralement assez moche de devoir remplacer les vues Admin pour ce genre de choses, et chaque exemple que je trouve ne s'applique jamais entièrement aux vues Admin.

C'est une circonstance tellement courante avec les modèles que je fabrique que je trouve épouvantable qu'il n'y ait pas de solution évidente à cela ...

J'ai ces cours:

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

Cela crée un problème lors de la configuration de l'administrateur pour la société, car il comporte des lignes pour le contrat et l'emplacement, et les options m2m du contrat pour l'emplacement ne sont pas correctement filtrées selon la société que vous modifiez actuellement.

En bref, j'aurais besoin d'une option d'administration pour faire quelque chose comme ceci:

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

En fin de compte, je me fiche de savoir si le processus de filtrage a été placé sur la base CompanyAdmin, ou s'il a été placé sur ContractInline. (Le placer sur l'inline est plus logique, mais il est difficile de référencer le contrat de base en tant que «soi».)

Y a-t-il quelqu'un qui sait quelque chose d'aussi simple que ce raccourci si nécessaire? À l'époque où j'ai créé des administrateurs PHP pour ce genre de choses, cela était considéré comme une fonctionnalité de base! En fait, il était toujours automatique et devait être désactivé si vous ne le vouliez vraiment pas!

Tim
la source
0

Une manière plus publique consiste à appeler get_form dans les classes Admin. Cela fonctionne également pour les champs non-base de données. Par exemple, ici, j'ai un champ appelé '_terminal_list' sur le formulaire qui peut être utilisé dans des cas spéciaux pour choisir plusieurs éléments de terminal dans get_list (request), puis filtrer en fonction de 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
la source