python - example - django formulaire dynamique




Comment filtrer les choix ForeignKey dans un ModelForm Django? (5)

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 existe plusieurs Companies , chacune ayant une gamme de Rates et de Clients . Chaque Client doit avoir un Rate base choisi parmi Company's Rates mère et non ceux d'une autre Company's Rates .

Lors de la création d'un formulaire pour l'ajout d'un Client , je souhaite supprimer les choix de la Company (ceux-ci ayant déjà été sélectionnés via un bouton "Ajouter un client" sur la page Company ) et limiter les choix de Rate à cette Company .

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

Mon fichier forms.py actuel est tout simplement très répandu en ce moment:

from models import *
from django.forms import ModelForm

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

Et le views.py est également 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 été capable de pirater ceci en faisant quelque chose comme ce qui suit avant de rendre le template:

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 dans the_company.id et je ne sais pas si cela fonctionnera en dehors de l'interface d'administration.

Merci. (Cela semble être une requête assez simple mais si je devais refaire quelque chose, je suis ouvert aux suggestions.)


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 sur ModelAdmin (à partir des 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 simple de le faire (par exemple en créant une interface d'administration frontale à laquelle les utilisateurs peuvent accéder) consiste à sous-classer ModelAdmin et ensuite à modifier les méthodes ci-dessous. Le résultat net est une interface utilisateur qui affiche SEULEMENT le contenu qui leur est lié, tout en vous permettant (un super-utilisateur) de tout voir.

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

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

Le dernier remplacement filtre n'importe quel champ de clé étrangère dans le modèle pour filtrer les choix disponibles de la même manière que le jeu de requête de base.

De cette façon, vous pouvez présenter un site d'administration facile à gérer qui permet aux utilisateurs de manipuler leurs propres objets, et vous n'avez pas à vous souvenir 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 suppression

    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)

Donc, j'ai vraiment essayé de comprendre cela, mais il semble que Django ne rende toujours pas cela très simple. Je ne suis pas tout à fait stupide, mais je ne vois pas de solution (un peu) simple.

Je trouve généralement assez moche d'avoir à remplacer les vues d'administration pour ce genre de chose, et chaque exemple que je trouve ne s'applique jamais entièrement aux vues d'administration.

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

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 configuration de l'Admin for Company, car il contient des inlines pour Contract et Location, et les options m2m de Contract pour Location ne sont pas correctement filtrées en fonction de la Société que vous êtes en train d'éditer.

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 m'inquiéterais pas si le processus de filtrage a été placé sur le CompanyAdmin de base, ou s'il a été placé sur ContractInline. (Le placer sur l'inline a plus de sens, mais cela rend difficile la référence au contrat de base en tant que 'soi'.)

Y at-il quelqu'un là-bas qui sait quelque chose d'aussi simple que ce raccourci très nécessaire? À l'époque où j'ai fait des administrateurs PHP pour ce genre de chose, cela a été 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!


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 .

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.


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


Une manière plus publique consiste à appeler get_form dans les classes Admin. Il fonctionne également pour les champs autres que les bases 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 terminaux de get_list (request), puis filtrer basé sur 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




django-forms