249 votes

Comment filtrer les choix de clés étrangères dans un formulaire modèle Django ?

Disons que j'ai les éléments suivants 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 a plusieurs Companies chacun ayant une gamme de Rates y Clients . Chaque Client devrait avoir une base Rate qui est choisi parmi ses parents Company's Rates et non un autre Company's Rates .

Lors de la création d'un formulaire pour ajouter un Client je voudrais supprimer le Company (puisqu'il a déjà été sélectionné via un bouton "Ajouter un client" sur le site de la Commission européenne). Company page) et limiter les Rate les choix qui en découlent Company également.

Comment dois-je procéder dans Django 1.0 ?

Mon actuel forms.py n'est pour l'instant que du réchauffé :

from models import *
from django.forms import ModelForm

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

Et le views.py est également de 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})

Dans la version 0.96 de Django, j'ai été en mesure d'y remédier en faisant quelque chose comme ce qui suit avant d'effectuer le rendu du modèle :

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

ForeignKey.limit_choices_to semble prometteuse mais je ne sais pas comment passer en the_company.id et je ne suis pas sûr que cela fonctionne en dehors de l'interface d'administration de toute façon.

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

0 votes

Merci pour l'indication de "limit_choices_to". Cela ne résout pas votre question, mais la mienne :-) Docs : docs.djangoproject.com/fr/dev/ref/models/fields/

256voto

S.Lott Points 207588

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

Il faut donc fournir un QuerySet au champ queryset attribut. Cela dépend de la façon dont votre formulaire est construit. Si vous construisez un formulaire explicite, les champs seront 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 = ...

Ceci est fait explicitement dans la vue. Pas de piratage.

0 votes

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 ?

2 votes

Ok, merci d'avoir développé l'exemple, mais il semble que je doive utiliser form.fields["rate"].queryset pour éviter "'ClientForm' object has no attribute 'rate'", ai-je manqué quelque chose ? (et votre exemple devrait être form.rate.queryset pour être cohérent aussi).

0 votes

Excellent, merci d'avoir clarifié. À l'avenir, il serait bon de noter que lorsque vous modifiez votre réponse dans un commentaire, les modifications n'apparaissent pas dans l'onglet des réponses de ma page d'utilisateur.

144voto

michael Points 5990

En plus de la réponse de S.Lott et comme becomingGuru l'a mentionné dans les commentaires, il est possible d'ajouter les filtres queryset en surchargeant la fonction ModelForm.__init__ fonction. (Cela pourrait facilement s'appliquer aux formulaires réguliers). Cela peut aider à la réutilisation et permet de garder la fonction d'affichage ordonné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 abstraite Form). 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')

Pour le reste, je ne fais que reprendre les articles du blog de Django, qui sont nombreux et de qualité.

0 votes

Il y a une faute de frappe dans votre premier extrait de code, vous définissez args deux fois dans __init__() au lieu de args et kwargs.

8 votes

Je préfère 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 de la vue. Merci !

48voto

neil.millikin Points 215

C'est simple, et cela 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 le ModelAdmin, car Django inclut déjà cette méthode intégrée dans le ModelAdmin (dans la documentation) :

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 façon encore plus astucieuse de procéder (par exemple pour créer une interface d'administration frontale à laquelle les utilisateurs peuvent accéder) consiste à sous-classer ModelAdmin et à modifier les méthodes ci-dessous. Le résultat net est une interface utilisateur qui leur montre UNIQUEMENT le contenu qui les concerne, 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 supprime également les boutons de suppression du site d'administration.

La troisième dérogation filtre toute requête qui contient une référence à (dans l'exemple 'user' ou 'porc-épic' (juste à titre d'illustration).

La dernière surcharge filtre tout champ de clé étrangère dans le modèle pour filtrer les choix disponibles de la même manière que le queryset de base.

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

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 qui peuvent être visualisés 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 à 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)

1 votes

Et je devrais ajouter que cela fonctionne bien comme un formulaire personnalisé générique pour plusieurs administrateurs de modèles avec des champs de référence similaires d'intérêt.

0 votes

C'est la meilleure réponse si vous utilisez Django 1.4+.

25voto

teewuane Points 1383

Pour faire cela 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 tout ça...

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

, lire mon article ici

0 votes

J'ai d'abord placé l'affectation de queryset dans le formulaire, mais j'obtenais une erreur de formulaire. En déplaçant l'affectation du queryset dans la vue, plus d'erreur.

2voto

Tim Points 111

J'ai vraiment essayé de comprendre, mais il semble que Django ne rende pas les choses très simples. Je ne suis pas si bête, mais je ne vois pas de solution (un peu) simple.

Je trouve qu'il est généralement assez laid de devoir remplacer les vues administratives pour ce genre de choses, et tous les exemples que je trouve ne s'appliquent jamais complètement aux vues administratives.

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

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 pose un problème lors de la configuration de l'administration de la société, car elle comporte des lignes pour le contrat et le lieu, et les options m2m du contrat pour le lieu ne sont pas correctement filtrées en fonction de la société que vous êtes en train de modifier.

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

# 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 ne me soucierais pas de savoir si le processus de filtrage est placé sur le CompanyAdmin de base, ou s'il est placé sur le ContractInline. (Le placer sur l'inline a plus de sens, mais cela rend difficile de référencer le contrat de base comme "self").

Y a-t-il quelqu'un qui connaisse quelque chose d'aussi simple que ce raccourci dont on a cruellement besoin ? A l'époque où je créais des admins PHP pour ce genre de choses, c'était considéré comme une fonctionnalité de base ! En fait, c'était toujours automatique, et devait être désactivé si vous ne le vouliez vraiment pas !

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X