web-dev-qa-db-fra.com

Comment filtrer les choix de ForeignKey dans un Django ModelForm?

Dites 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. il y a plusieurs Companies, chacun ayant une plage de Rates et Clients. Chaque Client devrait avoir une base Rate choisie dans son parent Company's Rates, pas un autre Company's Rates.

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

Comment puis-je m'y prendre dans Django 1.0?

Mon actuel forms.py Le fichier n'est que passe-partout pour le moment:

from models import *
from Django.forms import ModelForm

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

Et le views.py 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 ceci en faisant quelque chose comme ce qui suit 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_to semble prometteur mais je ne sais pas comment passer à the_company.id et 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 simple, mais si je devais repenser quelque chose, je suis ouvert aux suggestions.)

214
Tom

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

Donc, fournissez un QuerySet à l'attribut queryset du champ. Cela dépend de la façon dont votre formulaire est construit. 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 = ...

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

229
S.Lott

En plus de la réponse de S.Lott et de la mention de Guru mentionnée dans les commentaires, il est possible d’ajouter les filtres de jeu de requêtes en remplaçant le ModelForm.__init__ une fonction. (Cela pourrait facilement s'appliquer à des formulaires ordinaires). Cela 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 besoin de filtres communs 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 répète simplement Django) des articles de blog parmi lesquels il y en a beaucoup de bons.

127
michael

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 spécifier cela dans une classe de formulaire, mais vous pouvez le faire directement dans ModelAdmin, car Django inclut déjà cette méthode intégrée dans ModelAdmin (à partir de 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 simple de procéder (par exemple, lors de la création d'une interface d'administration frontale accessible aux utilisateurs) consiste à sous-classer ModelAdmin, puis à modifier les méthodes ci-dessous. Le résultat net est une interface utilisateur qui leur montre UNIQUEMENT le 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 supprime également les boutons de suppression du site d'administration.

La troisième substitue toute requête contenant une référence à (dans l'exemple 'utilisateur' ou 'porcupine' (à titre d'illustration).

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

De cette manière, vous pouvez présenter un site d’administration frontale facile à gérer qui permet aux utilisateurs de manipuler leurs propres objets, sans avoir à oublier de taper les filtres ModelAdmin spécifiques 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 supprimer

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

filtre les objets pouvant ê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 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)
42
neil.millikin

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 post ici

16
teewuane

Si vous n'avez pas créé le formulaire et souhaitez modifier le jeu de requête, vous pouvez procéder comme suit:

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

Ceci est très utile lorsque vous utilisez des vues génériques!

4
Hassek

Donc, j'ai vraiment essayé de comprendre cela, mais il semble que Django ne rend toujours pas cela très simple. Je ne suis pas si bête que ça, mais je ne vois rien solution (un peu) simple.

Je trouve généralement assez moche de devoir redéfinir les vues Admin pour ce genre de chose, et chaque exemple que je trouve ne s'applique jamais pleinement aux vues Admin.

C'est une situation si courante avec les modèles que je fais 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 contient des éléments en ligne pour le contrat et l'emplacement, et les options m2m du contrat pour l'emplacement ne sont pas correctement filtrées en fonction de la société en cours de modification.

En bref, il me faudrait une option d’administrateur 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, peu m'importe que le processus de filtrage ait été placé sur le CompanyAdmin de base ou sur ContractInline. (Il est plus logique de le placer sur l’inline, mais il est difficile de faire référence au 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ù je faisais PHP administrateurs pour ce genre de chose, c'était considéré comme une fonctionnalité de base! En fait, c'était toujours automatique, et il fallait le désactiver si vous ne le vouliez vraiment pas!

2
Tim

Une méthode plus publique consiste à appeler get_form dans les classes d'administration. Cela fonctionne également pour les champs non-base de données aussi. Par exemple ici, j'ai un champ appelé '_terminal_list' sur le formulaire qui peut être utilisé dans des cas particuliers pour choisir plusieurs éléments de terminal de get_list (request), puis pour 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
0
F.Tamy