Como acesso o objeto de solicitação ou qualquer outra variável no método clean () de um formulário?

99

Estou tentando request.user para um método limpo de formulário, mas como posso acessar o objeto de solicitação? Posso modificar o método de limpeza para permitir a entrada de variáveis?

Nubela
fonte

Respostas:

157

A resposta de Ber - armazená-lo em locais de discussão - é uma ideia muito ruim. Não há absolutamente nenhuma razão para fazer dessa maneira.

Uma maneira muito melhor é sobrescrever o __init__método do formulário para obter um argumento de palavra-chave extra request,. Isso armazena a solicitação no formulário , onde é necessária e de onde você pode acessá-la em seu método de limpeza.

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

e na sua opinião:

myform = MyForm(request.POST, request=request)
Daniel Roseman
fonte
4
Você está certo neste caso. No entanto, pode não ser desejável modificar Formulários / Visualizações neste era. Além disso, há casos de uso para armazenamento local de thread em que adicionar parâmetros de método ou variáveis ​​de instância é impossível. Pense em um argumento que pode ser chamado para um filtro de consulta que precisa de acesso para solicitar dados. Você não pode adicionar um parâmetro à chamada, nem há qualquer instância para fazer referência.
Ber
4
Não é útil quando você está estendendo um formulário de administração, porque você pode iniciar seu formulário passando a solicitação var. Qualquer ideia?
Mordi
13
Por que você diz que usar armazenamento local de thread é uma ideia muito ruim? Isso evita ter que descartar o código para passar a solicitação em todos os lugares.
Michael Mior
9
Eu não passaria o objeto de solicitação em si para o formulário, mas sim os campos de solicitação que você precisa (ou seja, usuário), caso contrário, você vincula a lógica do formulário ao ciclo de solicitação / resposta, o que torna o teste mais difícil.
Andrew Ingram
2
Chris Pratt também tem uma boa solução para lidar com formulários em admin.ModelAdmin
radtek
34

ATUALIZADO em 25/10/2011 : Agora estou usando isso com uma classe criada dinamicamente em vez do método, já que o Django 1.3 exibe algumas estranhezas de outra forma.

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

Em seguida, substitua da MyCustomForm.__init__seguinte forma:

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

Você pode acessar o objeto de solicitação de qualquer método de ModelFormcom self.request.

Chris Pratt
fonte
1
Chris, esse "def __init __ (self, request = None, * args, ** kwargs)" é ruim, porque terminará com request no primeiro arg posicional e nos kwargs. Eu mudei para "def __init __ (self, * args, ** kwargs)" e isso funciona.
Slinkp
1
Opa. Isso foi apenas um erro da minha parte. Eu negligenciei a atualização dessa parte do código quando fiz a outra atualização. Obrigado pela pegadinha. Atualizada.
Chris Pratt
4
Isso é realmente uma metaclasse? Eu acho que é apenas uma substituição normal, você adiciona uma solicitação aos __new__kwargs de que mais tarde será passada para o __init__método da classe . Nomeando a classe ModelFormWithRequest, acho muito mais claro em seu significado do que ModelFormMetaClass.
k4ml
2
Isso NÃO é uma metaclasse! Consulte stackoverflow.com/questions/100003/…
frnhr
32

Por que vale a pena, se você estiver usando visualizações baseadas em classe , em vez de visualizações baseadas em função, substitua get_form_kwargsem sua visualização de edição. Código de exemplo para um personalizado 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

O código de exibição acima será requestdisponibilizado como um dos argumentos de palavra-chave para a __init__função de construtor do formulário . Portanto, em seu ModelFormfazer:

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
fonte
1
Isso funcionou para mim. Eu faço a observação porque eu estava usando get_form_kwargs de qualquer maneira devido à complexa lógica WizardForm. Nenhuma outra resposta que eu vi responsável por WizardForm.
datakid
2
Alguém além de mim acha que isso é uma grande bagunça para fazer algo que é bastante rudimentar para um framework web? Django é ótimo, mas isso me faz não querer usar CBV, nunca.
trpt4him
1
IMHO, os benefícios dos CBVs superam as desvantagens dos FBVs de longe, especialmente se você trabalhar em um grande projeto com mais de 25 desenvolvedores escrevendo código que visa 100% de cobertura de teste de unidade. Não tenho certeza se as versões mais recentes do Django permitem ter o requestobjeto get_form_kwargsautomaticamente.
Joseph Victor Zammit
Na mesma linha, existe alguma maneira de acessar o ID da instância do objeto em get_form_kwargs?
Hassan Baig
1
@HassanBaig Possivelmente usando self.get_object? O CreateViewestende o SingleObjectMixin. Mas se isso funciona ou gera uma exceção depende se você está criando um novo objeto ou atualizando um existente; ou seja, teste ambos os casos (e exclusão, é claro).
Joseph Victor Zammit,
17

A abordagem usual é armazenar o objeto de solicitação em uma referência de thread local usando um middleware. Em seguida, você pode acessar isso de qualquer lugar em seu aplicativo, incluindo o método Form.clean ().

Mudar a assinatura do método Form.clean () significa que você possui sua própria versão modificada do Django, que pode não ser o que você deseja.

Obrigado, a contagem de middleware parece algo assim:

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

Registre este middleware conforme descrito na documentação do Django

Ber
fonte
2
Apesar dos comentários acima, este método funciona enquanto o outro método não. Definir um atributo do objeto de formulário no init não é transportado de forma confiável para métodos de limpeza, ao passo que definir os locais de thread permite que esses dados sejam transportados.
rplevy
4
@rplevy você realmente passou o objeto de solicitação ao criar uma instância do formulário? Caso você não tenha notado, ele usa argumentos de palavra-chave **kwargs, o que significa que você terá que passar o objeto de solicitação como MyForm(request.POST, request=request).
unode
13

Para Django admin, em 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
fonte
1
O método mais bem avaliado acima, de fato, parece ter parado de funcionar em algum lugar entre Django 1.6 e 1.9. Este funciona e é muito mais curto. Obrigado!
Raik
9

Corri para este problema específico ao personalizar o administrador. Eu queria que um determinado campo fosse validado com base nas credenciais do administrador específico.

Como não queria modificar a visualização para passar a solicitação como um argumento para o formulário, fiz o seguinte:

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
entropia
fonte
Obrigado por isso. Erro de digitação rápido: obj=objnão está obj=Nonena linha 11.
François Constant
Resposta muito boa, adorei!
Luke Dupin
Django 1.9 oferece: 'function' object has no attribute 'base_fields'. No entanto, a resposta mais simples (sem encerramento) @ François funciona sem problemas.
raratiru
5

Você nem sempre pode usar este método (e provavelmente é uma prática ruim), mas se você estiver usando o formulário em apenas uma visão, você pode escopo dentro do próprio método de visão.

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
fonte
Às vezes, essa é uma solução muito boa: geralmente faço isso em um get_form_classmétodo CBV , se sei que preciso fazer muitas coisas com a solicitação. Pode haver alguma sobrecarga na criação repetida da classe, mas isso apenas a move do tempo de importação para o tempo de execução.
Matthew Schinckel
5

A resposta de Daniel Roseman ainda é a melhor. No entanto, eu usaria o primeiro argumento posicional para a solicitação em vez do argumento de palavra-chave por alguns motivos:

  1. Você não corre o risco de substituir um kwarg com o mesmo nome
  2. O pedido é opcional, o que não está certo. O atributo de solicitação nunca deve ser Nenhum neste contexto.
  3. Você pode passar de forma limpa os args e kwargs para a classe pai sem precisar modificá-los.

Por último, eu usaria um nome mais exclusivo para evitar a substituição de uma variável existente. Assim, Minha resposta modificada se parece com:

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

Eu tenho outra resposta a esta pergunta de acordo com sua exigência de que você deseja acessar o usuário no método limpo do formulário. Você pode tentar isso. 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)

Agora você pode acessar o self.instance em qualquer método limpo em form.py

Nishant Kashyap
fonte
0

Quando você deseja acessá-lo por meio de visualizações de classe "preparadas" do Django, como se CreateViewhouvesse um pequeno truque para saber (= a solução oficial não funciona fora da caixa). Por conta própria, CreateView você terá que adicionar um código como este:

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

= resumidamente, esta é a solução para passar requestpara o seu formulário com as visualizações Criar / Atualizar do Django.

Olivier Pons
fonte