Wie greife ich auf das Anforderungsobjekt oder eine andere Variable in der clean () -Methode eines Formulars zu?

99

Ich versuche, request.user für die Bereinigungsmethode eines Formulars anzufordern, aber wie kann ich auf das Anforderungsobjekt zugreifen? Kann ich die Bereinigungsmethode so ändern, dass Variablen eingegeben werden können?

Nubela
quelle

Antworten:

157

Die Antwort von Ber - es in Threadlocals zu speichern - ist eine sehr schlechte Idee. Es gibt absolut keinen Grund, dies so zu tun.

Eine viel bessere Möglichkeit besteht darin, die __init__Methode des Formulars zu überschreiben , um ein zusätzliches Schlüsselwortargument zu verwenden request. Dadurch wird die Anforderung im Formular gespeichert , wo sie benötigt wird und von wo aus Sie mit Ihrer Bereinigungsmethode darauf zugreifen können.

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

und aus Ihrer Sicht:

myform = MyForm(request.POST, request=request)
Daniel Roseman
quelle
4
Sie haben in diesem Fall Recht. Es kann jedoch nicht erwünscht sein, Formulare / Ansichten in diesem Fall zu ändern. Es gibt auch Anwendungsfälle für den lokalen Thread-Speicher, in denen das Hinzufügen von Methodenparametern oder Instanzvariablen nicht möglich ist. Denken Sie an ein aufrufbares Argument für einen Abfragefilter, der Zugriff auf Anforderungsdaten benötigt. Sie können dem Aufruf weder einen Parameter hinzufügen, noch gibt es eine Instanz, auf die verwiesen werden kann.
Ber
4
Dies ist nicht hilfreich, wenn Sie ein Administrationsformular erweitern, da Sie Ihr Formular initiieren können, indem Sie die Anforderungsvariable übergeben. Irgendeine Idee?
Mordi
13
Warum ist die Verwendung von thread-lokalem Speicher Ihrer Meinung nach eine sehr schlechte Idee? Es wird vermieden, dass der Code für die Weitergabe der Anforderung überall gelöscht werden muss.
Michael Mior
9
Ich würde das Anforderungsobjekt selbst nicht an das Formular übergeben, sondern an die Anforderungsfelder, die Sie benötigen (dh Benutzer), andernfalls verknüpfen Sie Ihre Formularlogik mit dem Anforderungs- / Antwortzyklus, was das Testen erschwert.
Andrew Ingram
2
Chris Pratt hat auch eine gute Lösung für den Umgang mit Formularen in admin.ModelAdmin
radtek
34

AKTUALISIERT am 25.10.2011 : Ich verwende dies jetzt mit einer dynamisch erstellten Klasse anstelle einer Methode, da Django 1.3 ansonsten etwas seltsam ist.

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

Dann überschreiben Sie MyCustomForm.__init__wie folgt:

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

Sie können dann von einer beliebigen Methode ModelFormmit auf das Anforderungsobjekt zugreifen self.request.

Chris Pratt
quelle
1
Chris, dass "def __init __ (self, request = None, * args, ** kwargs)" schlecht ist, weil es sowohl im ersten Positionsarg als auch in kwargs zu einer Anfrage kommt. Ich habe es in "def __init __ (self, * args, ** kwargs)" geändert und das funktioniert.
Slinkp
1
Hoppla. Das war nur ein Fehler von meiner Seite. Ich habe es versäumt, diesen Teil des Codes zu aktualisieren, als ich das andere Update durchgeführt habe. Danke für den Fang. Aktualisiert.
Chris Pratt
4
Ist das wirklich eine Metaklasse? Ich denke, es ist nur normales Überschreiben. Sie fügen den __new__kwargs eine Anfrage hinzu, die später an die __init__Methode der Klasse übergeben wird. ModelFormWithRequestIch nenne die Klasse viel klarer in ihrer Bedeutung als ModelFormMetaClass.
k4ml
2
Dies ist KEINE Metaklasse! Siehe stackoverflow.com/questions/100003/…
Uhr
32

Wenn Sie klassenbasierte Ansichten anstelle von funktionsbasierten Ansichten verwenden, überschreiben Sie diese get_form_kwargsin Ihrer Bearbeitungsansicht. Beispielcode für eine benutzerdefinierte CreateView :

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

Der obige Ansichtscode wird requestals eines der Schlüsselwortargumente für die __init__Konstruktorfunktion des Formulars verfügbar gemacht . Deshalb in Ihrem ModelFormdo:

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
quelle
1
Das hat bei mir funktioniert. Ich mache die Notiz, weil ich wegen der komplexen WizardForm-Logik sowieso get_form_kwargs verwendet habe. Keine andere Antwort, die ich gesehen habe, war für WizardForm verantwortlich.
Datum
2
Glaubt jemand außer mir, dass dies nur ein großes Durcheinander ist, um etwas zu tun, das für ein Webframework ziemlich rudimentär ist? Django ist großartig, aber deshalb möchte ich CBV überhaupt nicht verwenden.
trpt4him
1
IMHO überwiegen die Vorteile von CBVs bei weitem die Nachteile von FBVs, insbesondere wenn Sie an einem großen Projekt mit mehr als 25 Entwicklern arbeiten, die Code schreiben, der eine 100% ige Abdeckung von Unit-Tests anstrebt. Ich bin mir nicht sicher, ob neuere Versionen von Django dafür sorgen, dass das requestObjekt get_form_kwargsautomatisch enthalten ist.
Joseph Victor Zammit
Gibt es in ähnlicher Weise eine Möglichkeit, auf die ID der Objektinstanz in get_form_kwargs zuzugreifen?
Hassan Baig
1
@ HassanBaig Möglicherweise mit self.get_object? Das CreateViewerweitert die SingleObjectMixin. Ob dies funktioniert oder eine Ausnahme auslöst, hängt davon ab, ob Sie ein neues Objekt erstellen oder ein vorhandenes aktualisieren. dh beide Fälle testen (und natürlich löschen).
Joseph Victor Zammit
17

Der übliche Ansatz besteht darin, das Anforderungsobjekt mithilfe einer Middleware in einer threadlokalen Referenz zu speichern. Dann können Sie von überall in Ihrer App darauf zugreifen, einschließlich der Form.clean () -Methode.

Wenn Sie die Signatur der Form.clean () -Methode ändern, haben Sie eine eigene, geänderte Version von Django, die möglicherweise nicht Ihren Wünschen entspricht.

Vielen Dank, dass die Anzahl der Middleware ungefähr so ​​aussieht:

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

Registrieren Sie diese Middleware wie in den Django-Dokumenten beschrieben

Ber
quelle
2
Trotz der obigen Kommentare funktioniert diese Methode, die andere nicht. Das Festlegen eines Attributs des Formularobjekts in init überträgt sich nicht zuverlässig auf saubere Methoden, während das Festlegen der Thread-Lokalitäten das Übertragen dieser Daten ermöglicht.
Rplevy
4
@rplevy Haben Sie das Anforderungsobjekt tatsächlich übergeben, als Sie eine Instanz des Formulars erstellt haben? Falls Sie es nicht bemerkt haben, werden Schlüsselwortargumente verwendet. Dies **kwargsbedeutet, dass Sie das Anforderungsobjekt als übergeben müssen MyForm(request.POST, request=request).
Unode
13

Für Django-Administrator in 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
quelle
1
Die oben am besten bewertete Methode scheint tatsächlich irgendwo zwischen Django 1.6 und 1.9 nicht mehr zu funktionieren. Dieser funktioniert und ist viel kürzer. Vielen Dank!
Raik
9

Ich bin beim Anpassen des Administrators auf dieses spezielle Problem gestoßen. Ich wollte, dass ein bestimmtes Feld basierend auf den Anmeldeinformationen des jeweiligen Administrators überprüft wird.

Da ich die Ansicht nicht ändern wollte, um die Anforderung als Argument an das Formular zu übergeben, habe ich Folgendes getan:

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
quelle
Dank dafür. Schneller Tippfehler: obj=objnicht obj=Nonein Zeile 11.
François Constant
Wirklich schöne Antwort, ich liebe es!
Luke Dupin
Django 1.9 liefert : 'function' object has no attribute 'base_fields'. Die einfachere (ohne Abschluss) @ François-Antwort funktioniert jedoch reibungslos.
Raratiru
5

Sie können diese Methode nicht immer verwenden (und es ist wahrscheinlich eine schlechte Vorgehensweise), aber wenn Sie das Formular nur in einer Ansicht verwenden, können Sie es innerhalb der Ansichtsmethode selbst erfassen.

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
quelle
Dies ist manchmal eine wirklich schöne Lösung: Ich mache dies oft in einer CBV- get_form_classMethode, wenn ich weiß, dass ich mit der Anfrage viele Dinge tun muss. Das wiederholte Erstellen der Klasse kann mit einem gewissen Aufwand verbunden sein, der jedoch nur von der Importzeit zur Laufzeit verschoben wird.
Matthew Schinckel
5

Die Antwort von Daniel Roseman ist immer noch die beste. Ich würde jedoch aus einigen Gründen das erste Positionsargument für die Anforderung anstelle des Schlüsselwortarguments verwenden:

  1. Sie laufen nicht Gefahr, einen Kwarg mit demselben Namen zu überschreiben
  2. Die Anfrage ist optional, was nicht richtig ist. Das Anforderungsattribut sollte in diesem Zusammenhang niemals None sein.
  3. Sie können die Argumente und Warnungen sauber an die übergeordnete Klasse übergeben, ohne sie ändern zu müssen.

Zuletzt würde ich einen eindeutigeren Namen verwenden, um zu vermeiden, dass eine vorhandene Variable überschrieben wird. Meine modifizierte Antwort sieht also so aus:

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
quelle
3

Ich habe eine andere Antwort auf diese Frage gemäß Ihrer Anforderung, dass Sie auf den Benutzer über die saubere Methode des Formulars zugreifen möchten. Sie können dies versuchen. 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)

Jetzt können Sie in jeder sauberen Methode in form.py auf die self.instance zugreifen

Nishant Kashyap
quelle
0

Wenn Sie über "vorbereitete" Django-Klassenansichten darauf zugreifen möchten, CreateViewist ein kleiner Trick zu kennen (= die offizielle Lösung funktioniert nicht sofort). In Ihrem eigenen müssen CreateView Sie Code wie folgt hinzufügen:

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

= Kurz gesagt, dies ist die Lösung, die requestSie mit den Ansichten "Erstellen / Aktualisieren" von Django an Ihr Formular übergeben können.

Olivier Pons
quelle