Comment accéder à l'objet de requête ou à toute autre variable dans la méthode clean () d'un formulaire?

99

J'essaie de request.user pour la méthode propre d'un formulaire, mais comment puis-je accéder à l'objet de demande? Puis-je modifier la méthode de nettoyage pour autoriser l'entrée de variables?

nubela
la source

Réponses:

157

La réponse de Ber - la stocker dans des threadlocals - est une très mauvaise idée. Il n'y a absolument aucune raison de le faire de cette façon.

Une bien meilleure façon est de passer outre de la forme __init__de méthode pour prendre un argument mot - clé supplémentaire, request. Cela stocke la demande dans le formulaire , où elle est requise et à partir de laquelle vous pouvez y accéder dans votre méthode propre.

class MyForm(forms.Form):

    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop('request', None)
        super(MyForm, self).__init__(*args, **kwargs)


    def clean(self):
        ... access the request object via self.request ...

et à votre avis:

myform = MyForm(request.POST, request=request)
Daniel Roseman
la source
4
Vous avez raison dans ce cas. Cependant, il peut ne pas être souhaitable de modifier les formulaires / vues dans ce cas. En outre, il existe des cas d'utilisation pour le magasin local de threads où l'ajout de paramètres de méthode ou de variables d'instance est impossible. Pensez à l'argument appelable d'un filtre de requête qui a besoin d'un accès pour demander des données. Vous ne pouvez ni ajouter de paramètre à l'appel, ni aucune instance à référencer.
Ber
4
Cela n'est pas utile lorsque vous étendez un formulaire d'administration, car vous pouvez initier votre formulaire en passant la requête var. Une idée?
Mordi
13
Pourquoi dites-vous que l'utilisation du stockage local par thread est une très mauvaise idée? Cela évite d'avoir à abandonner le code pour passer la requête partout.
Michael Mior
9
Je ne passerais pas l'objet de requête lui-même au formulaire, mais plutôt les champs de requête dont vous avez besoin (c'est-à-dire l'utilisateur), sinon vous liez votre logique de formulaire au cycle de requête / réponse, ce qui rend les tests plus difficiles.
Andrew Ingram
2
Chris Pratt a également une bonne solution, pour le traitement des formulaires dans admin.ModelAdmin
radtek
34

MISE À JOUR 25/10/2011 : J'utilise maintenant ceci avec une classe créée dynamiquement au lieu d'une méthode, car Django 1.3 affiche une certaine bizarrerie autrement.

class MyModelAdmin(admin.ModelAdmin):
    form = MyCustomForm
    def get_form(self, request, obj=None, **kwargs):
        ModelForm = super(MyModelAdmin, self).get_form(request, obj, **kwargs)
        class ModelFormWithRequest(ModelForm):
            def __new__(cls, *args, **kwargs):
                kwargs['request'] = request
                return ModelForm(*args, **kwargs)
        return ModelFormWithRequest

Remplacez ensuite MyCustomForm.__init__comme suit:

class MyCustomForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop('request', None)
        super(MyCustomForm, self).__init__(*args, **kwargs)

Vous pouvez ensuite accéder à l'objet de requête à partir de n'importe quelle méthode de ModelFormavec self.request.

Chris Pratt
la source
1
Chris, que "def __init __ (self, request = None, * args, ** kwargs)" est mauvais, car il se terminera par une requête à la fois dans le premier argument positionnel et dans kwargs. Je l'ai changé en "def __init __ (self, * args, ** kwargs)" et cela fonctionne.
slinkp
1
Oups. C'était juste une erreur de ma part. J'ai négligé de mettre à jour cette partie du code lorsque j'ai effectué l'autre mise à jour. Merci pour la capture. Actualisé.
Chris Pratt
4
Est-ce vraiment une métaclasse? Je pense que c'est juste un remplacement normal, vous ajoutez une requête aux __new__kwargs de la classe qui plus tard sera transmise à la __init__méthode de la classe . Nommer la classe ModelFormWithRequestje pense que c'est beaucoup plus clair dans sa signification que ModelFormMetaClass.
k4ml
2
Ceci n'est PAS une métaclasse! Voir stackoverflow.com/questions/100003/…
frnhr
32

Pour ce que cela vaut, si vous utilisez des vues basées sur des classes , au lieu de vues basées sur des fonctions, remplacez-les get_form_kwargsdans votre vue d'édition. Exemple de code pour un CreateView personnalisé :

from braces.views import LoginRequiredMixin

class MyModelCreateView(LoginRequiredMixin, CreateView):
    template_name = 'example/create.html'
    model = MyModel
    form_class = MyModelForm
    success_message = "%(my_object)s added to your site."

    def get_form_kwargs(self):
        kw = super(MyModelCreateView, self).get_form_kwargs()
        kw['request'] = self.request # the trick!
        return kw

    def form_valid(self):
        # do something

Le code de vue ci-dessus rendra requestdisponible comme l'un des arguments de mot-clé de la __init__fonction constructeur du formulaire . Par conséquent dans votre ModelFormfaire:

class MyModelForm(forms.ModelForm):
    class Meta:
        model = MyModel

    def __init__(self, *args, **kwargs):
        # important to "pop" added kwarg before call to parent's constructor
        self.request = kwargs.pop('request')
        super(MyModelForm, self).__init__(*args, **kwargs)
Joseph Victor Zammit
la source
1
Cela a fonctionné pour moi. Je fais la note parce que j'utilisais de toute façon get_form_kwargs en raison de la logique complexe de WizardForm. Aucune autre réponse que j'ai vue ne représentait WizardForm.
datakid
2
Quelqu'un d'autre que moi pense-t-il que ce n'est qu'un gros gâchis de faire quelque chose d'assez rudimentaire pour un framework Web? Django est génial mais cela me donne envie de ne pas du tout utiliser CBV.
trpt4him
1
À mon humble avis, les avantages des CBV l'emportent de loin sur les inconvénients des FBV, surtout si vous travaillez sur un grand projet avec plus de 25 développeurs qui écrivent du code qui vise une couverture de test unitaire à 100%. Je ne sais pas si les versions plus récentes de Django permettent d'avoir l' requestobjet get_form_kwargsautomatiquement.
Joseph Victor Zammit
Dans la même veine, existe-t-il un moyen d'accéder à l'ID de l'instance d'objet dans get_form_kwargs?
Hassan Baig
1
@HassanBaig Utilisation possible self.get_object? Le CreateViewprolonge le SingleObjectMixin. Mais si cela fonctionne ou déclenche une exception, cela dépend si vous créez un nouvel objet ou mettez à jour un objet existant; c'est à dire tester les deux cas (et suppression bien sûr).
Joseph Victor Zammit
17

L'approche habituelle est de stocker l'objet de requête dans une référence locale de thread à l'aide d'un middleware. Ensuite, vous pouvez y accéder depuis n'importe où dans votre application, y compris la méthode Form.clean ().

Changer la signature de la méthode Form.clean () signifie que vous avez votre propre version modifiée de Django, ce qui n'est peut-être pas ce que vous voulez.

Merci, le nombre de middleware ressemble à ceci:

import threading
_thread_locals = threading.local()

def get_current_request():
    return getattr(_thread_locals, 'request', None)

class ThreadLocals(object):
    """
    Middleware that gets various objects from the
    request object and saves them in thread local storage.
    """
    def process_request(self, request):
        _thread_locals.request = request

Enregistrez ce middleware comme décrit dans la documentation Django

Ber
la source
2
Malgré les commentaires ci-dessus, cette méthode fonctionne alors que l'autre méthode ne fonctionne pas. La définition d'un attribut de l'objet de formulaire dans init ne se répercute pas de manière fiable sur les méthodes de nettoyage, tandis que la définition des locals de thread permet le transfert de ces données.
rplevy
4
@rplevy avez-vous réellement passé l'objet de requête lorsque vous créez une instance du formulaire? Au cas où vous ne l'auriez pas remarqué, il utilise des arguments de mots clés **kwargs, ce qui signifie que vous devrez transmettre l'objet de requête en tant que MyForm(request.POST, request=request).
unode
13

Pour l'administrateur Django, dans Django 1.8

class MyModelAdmin(admin.ModelAdmin):
    ...
    form = RedirectForm

    def get_form(self, request, obj=None, **kwargs):
        form = super(MyModelAdmin, self).get_form(request, obj=obj, **kwargs)
        form.request = request
        return form
François Constant
la source
1
La méthode la mieux notée ci-dessus semble en effet avoir cessé de fonctionner quelque part entre Django 1.6 et 1.9. Celui-ci fonctionne et est beaucoup plus court. Merci!
Raik
9

J'ai rencontré ce problème particulier lors de la personnalisation de l'administrateur. Je voulais qu'un certain champ soit validé en fonction des informations d'identification de l'administrateur particulier.

Puisque je ne voulais pas modifier la vue pour transmettre la requête en tant qu'argument au formulaire, voici ce que j'ai fait:

class MyCustomForm(forms.ModelForm):
    class Meta:
        model = MyModel

    def clean(self):
        # make use of self.request here

class MyModelAdmin(admin.ModelAdmin):
    form = MyCustomForm
    def get_form(self, request, obj=None, **kwargs):
        ModelForm = super(MyModelAdmin, self).get_form(request, obj=obj, **kwargs)
        def form_wrapper(*args, **kwargs):
            a = ModelForm(*args, **kwargs)
            a.request = request
            return a
    return form_wrapper
entropie
la source
Merci pour ça. Petite faute de frappe: obj=objpas obj=Nonesur la ligne 11.
François Constant
Vraiment belle réponse, j'adore!
Luke Dupin
Django 1.9 offre: 'function' object has no attribute 'base_fields'. Cependant, la réponse la plus simple (sans fermeture) @ François fonctionne bien.
raratiru
5

Vous ne pouvez pas toujours utiliser cette méthode (et c'est probablement une mauvaise pratique), mais si vous n'utilisez le formulaire que dans une vue, vous pouvez l'étendre à l'intérieur de la méthode de vue elle-même.

def my_view(request):

    class ResetForm(forms.Form):
        password = forms.CharField(required=True, widget=forms.PasswordInput())

        def clean_password(self):
            data = self.cleaned_data['password']
            if not request.user.check_password(data):
                raise forms.ValidationError("The password entered does not match your account password.")
            return data

    if request.method == 'POST':
        form = ResetForm(request.POST, request.FILES)
        if form.is_valid():

            return HttpResponseRedirect("/")
    else:
        form = ResetForm()

    return render_to_response(request, "reset.html")
Chris
la source
C'est parfois une très bonne solution: je le fais souvent dans une get_form_classméthode CBV , si je sais que j'ai besoin de faire beaucoup de choses avec la requête. La création répétée de la classe peut entraîner une surcharge, mais cela ne fait que la déplacer du moment de l'importation au moment de l'exécution.
Matthew Schinckel
5

La réponse de Daniel Roseman est toujours la meilleure. Cependant, j'utiliserais le premier argument positionnel pour la requête au lieu de l'argument mot-clé pour plusieurs raisons:

  1. Vous ne courez pas le risque de remplacer un kwarg du même nom
  2. La demande est facultative, ce qui n'est pas correct. L'attribut request ne doit jamais être None dans ce contexte.
  3. Vous pouvez passer proprement les arguments et kwargs à la classe parente sans avoir à les modifier.

Enfin, j'utiliserais un nom plus unique pour éviter de remplacer une variable existante. Ainsi, ma réponse modifiée ressemble à:

class MyForm(forms.Form):

  def __init__(self, request, *args, **kwargs):
      self._my_request = request
      super(MyForm, self).__init__(*args, **kwargs)


  def clean(self):
      ... access the request object via self._my_request ...
Andres Restrepo
la source
3

J'ai une autre réponse à cette question selon votre condition que vous souhaitez accéder à l'utilisateur dans la méthode propre du formulaire. Vous pouvez essayer ceci. View.py

person=User.objects.get(id=person_id)
form=MyForm(request.POST,instance=person)

forms.py

def __init__(self,*arg,**kwargs):
    self.instance=kwargs.get('instance',None)
    if kwargs['instance'] is not None:
        del kwargs['instance']
    super(Myform, self).__init__(*args, **kwargs)

Vous pouvez maintenant accéder à self.instance dans n'importe quelle méthode propre dans form.py

Nishant Kashyap
la source
0

Lorsque vous voulez y accéder via des vues de classe Django "préparées", comme CreateViewil y a une petite astuce à connaître (= la solution officielle ne fonctionne pas hors de la boîte). Dans le vôtre, CreateView vous devrez ajouter un code comme celui-ci:

class MyCreateView(LoginRequiredMixin, CreateView):
    form_class = MyOwnForm
    template_name = 'my_sample_create.html'

    def get_form_kwargs(self):
        result = super().get_form_kwargs()
        result['request'] = self.request
        return result

= en bref c'est la solution à passer requestà votre formulaire avec les vues Créer / Mettre à jour de Django.

Olivier Pons
la source