Comment filtrer les choix de ForeignKey dans un Modèledjango?

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

i. e. il y a plusieurs Companies , chacun ayant une gamme de Rates et Clients . Chaque Client devrait avoir une base Rate qui est choisi de 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 (car cela a déjà été sélectionné via un bouton " Ajouter Client "sur la page Company ) et limiter les choix Rate à celui Company ainsi.

Comment faire avec Django 1.0?

Mon forms.py fichier est juste réutilisable à l'instant:

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 Django 0.96 j'ai pu pirater ceci en faisant quelque chose comme ce qui suit avant rendant 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 cela semble prometteur mais je ne sais pas comment passer dans the_company.id et je ne suis pas sûr que cela fonctionnera en dehors de l'interface D'administration de toute façon.

Merci. (Cela semble assez simple demande, mais si je dois redessiner quelque chose je suis ouvert aux suggestions.)

196
demandé sur Tom 2008-11-15 04:21:33

7 réponses

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

ainsi, fournir un QuerySet à l'attribut queryset du champ. Dépend de la façon dont votre forme est construite. Si vous construisez 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 autour.

212
répondu S.Lott 2010-02-12 22:14:23

en plus de la réponse de S. Lott et comme devenant gourou mentionné dans les commentaires, il est possible d'ajouter les filtres queryset en remplaçant la fonction ModelForm.__init__ . (Cela pourrait facilement s'appliquer à des formes régulières) il peut aider à la réutilisation et garde la fonction de vue 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 disons si vous avez des filtres communs nécessaires sur de nombreux modèles (normalement je déclare une classe de forme 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')

autres que que je suis juste en train de reformuler le Blog de Django dont il y a beaucoup de bons là-bas.

120
répondu michael 2010-11-30 06:00:29

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 forme, mais vous pouvez le faire directement dans le ModelAdmin, puisque Django inclut déjà cette méthode intégrée sur le ModelAdmin (à partir du docs):

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 intelligente de faire ceci (par exemple en créant une interface d'administration frontale à laquelle les utilisateurs peuvent accéder) est de sous-classe le ModelAdmin et puis de modifier les méthodes ci-dessous. Filet résultat est une interface utilisateur qui ne leur montre que le contenu qui leur est lié, tout en vous permettant (un super-utilisateur) de tout voir.

j'ai dépassé quatre méthodes, les deux premières rendent impossible pour un utilisateur de supprimer quoi que ce soit, et il supprime également les boutons supprimer du site administrateur.

le troisième outrepasser filtre toute requête contenant une référence à (dans l'exemple 'user' ou 'porcupine' (juste comme une illustration).

le dernier override filtre n'importe quel champ foreignkey dans le model 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 admin face à l'avant facile à gérer qui permet aux utilisateurs de jouer avec leurs propres objets, et vous n'avez pas à vous rappeler de taper dans 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 supprimer la permission

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

filtre les objets qui peuvent être consultés sur le site administrateur:

    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 des choix pour tous les champs foreignkey sur le site admin:

    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)
40
répondu neil.millikin 2016-01-14 02:55:36

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...

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

, lire mon post ici

15
répondu teewuane 2012-04-20 19:07:11

si vous n'avez pas créé le formulaire et que vous voulez changer le queryset, vous pouvez le faire:

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

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

4
répondu Hassek 2015-10-12 09:24:58

donc, j'ai vraiment essayé de comprendre cela, mais il semble que Django ne rend pas cela très simple. Je ne suis pas si bête, mais je ne vois pas de solution simple.

je trouve qu'il est généralement assez laid d'avoir à outrepasser 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 courante avec les modèles que je fais que je trouve consternant qu'il n'y a pas de solution évidente à ce...

j'ai ces classes:

# 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 mise en place de L'administrateur pour la société, parce qu'il dispose d'inlines pour le contrat et L'emplacement, et les options M2M du contrat pour L'emplacement ne sont pas correctement filtré selon la société que vous êtes en train d'éditer.

bref, j'aurais besoin d'une option Administrateur 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')

finalement, je ne me soucierais pas si le processus de filtrage a été placé sur la base CompanyAdmin, ou s'il a été placé sur la ContractInline. (Le placer sur la ligne a plus de sens, mais il est difficile de faire référence au contrat de base en tant que "soi".)

y a-t-il quelqu'un qui connaît quelque chose d'aussi simple que ce raccourci dont on a tant besoin? Quand J'ai créé des administrateurs PHP pour ce genre de chose, c'était considéré comme une fonctionnalité de base! En fait, il était toujours automatique, et a dû être désactivé si vous n'avez vraiment pas envie!

2
répondu Tim 2009-10-23 00:45:59

une façon plus publique est d'appeler get_form dans les classes D'administration. Il fonctionne également pour les champs non-base de données aussi. Par exemple, j'ai ici un champ appelé '_terminal_list' sur le formulaire qui peut être utilisé dans des cas spéciaux pour choisir plusieurs éléments de terminal à partir de get_list(request), puis filtrer sur la base de request.utilisateur:

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
répondu F.Tamy 2016-08-17 06:30:12