Django passando parâmetros de formulário personalizado para Formset

150

Isso foi corrigido no Django 1.9 com form_kwargs .

Eu tenho um formulário do Django que se parece com isso:

class ServiceForm(forms.Form):
    option = forms.ModelChoiceField(queryset=ServiceOption.objects.none())
    rate = forms.DecimalField(widget=custom_widgets.SmallField())
    units = forms.IntegerField(min_value=1, widget=custom_widgets.SmallField())

    def __init__(self, *args, **kwargs):
        affiliate = kwargs.pop('affiliate')
        super(ServiceForm, self).__init__(*args, **kwargs)
        self.fields["option"].queryset = ServiceOption.objects.filter(affiliate=affiliate)

Eu chamo este formulário com algo assim:

form = ServiceForm(affiliate=request.affiliate)

Onde request.affiliateestá o usuário conectado. Isso funciona como pretendido.

Meu problema é que agora quero transformar esse formulário único em um formset. O que não consigo descobrir é como posso passar as informações do afiliado para os formulários individuais ao criar o conjunto de formulários. De acordo com os documentos para criar um formset, preciso fazer algo assim:

ServiceFormSet = forms.formsets.formset_factory(ServiceForm, extra=3)

E então eu preciso criá-lo assim:

formset = ServiceFormSet()

Agora, como posso passar o afiliado = request.affiliate para os formulários individuais dessa maneira?

Paolo Bergantino
fonte

Respostas:

105

Eu usaria functools.partial e functools.wraps :

from functools import partial, wraps
from django.forms.formsets import formset_factory

ServiceFormSet = formset_factory(wraps(ServiceForm)(partial(ServiceForm, affiliate=request.affiliate)), extra=3)

Eu acho que essa é a abordagem mais limpa e não afeta o ServiceForm de forma alguma (ou seja, dificultando a subclasse).

Carl Meyer
fonte
Não está funcionando para mim. Eu recebo o erro: AttributeError: objeto '_curriedFormSet' tem nenhum atributo 'get'
Paolo Bergantino
Não consigo duplicar este erro. Também é estranho, porque um conjunto de formulários geralmente não tem um atributo 'get', portanto parece que você está fazendo algo estranho no seu código. (Além disso, atualizei a resposta com uma maneira de me livrar de curiosidades como '_curriedFormSet').
Carl Meyer
Estou revisitando isso porque gostaria que sua solução funcionasse. Posso declarar o formset como bom, mas se eu tentar imprimi-lo, o {{formset}} será quando eu receber o erro "has no attribute 'get'". Isso acontece com qualquer solução que você forneceu. Se eu percorrer o formset e imprimir os formulários como {{form}}, recebo o erro novamente. Se eu fizer um loop e imprimir como {{form.as_table}} por exemplo, recebo tabelas de formulários vazias, ou seja. nenhum campo é impresso. Alguma ideia?
21411 Paolo Bergantino
Você está certo, me desculpe; meus testes anteriores não foram suficientemente longe. Eu rastreei isso, e ele quebra devido a algumas esquisitices na forma como o FormSets funciona internamente. Existe uma maneira de contornar o problema, mas ele começa a perder a elegância original ...
Carl Meyer
5
Se o tópico de comentário aqui não faz sentido, é porque eu apenas editei a resposta para usar o Python em functools.partialvez do Django django.utils.functional.curry. Eles fazem a mesma coisa, exceto que functools.partialretorna um tipo de chamada distinto, em vez de uma função Python regular, e o partialtipo não é vinculado como um método de instância, que resolve perfeitamente o problema que esse segmento de comentário foi amplamente dedicado à depuração.
Carl Meyer
81

Documento Oficial Way

Django 2.0:

ArticleFormSet = formset_factory(MyArticleForm)
formset = ArticleFormSet(form_kwargs={'user': request.user})

https://docs.djangoproject.com/en/2.0/topics/forms/formsets/#passing-custom-parameters-to-formset-forms

sergi0
fonte
8
essa deve ser a maneira correta de fazer isso agora. a resposta aceita funciona e é bom, mas é um hack
Junchao Gu
definitivamente a melhor resposta e a maneira correta de fazê-lo.
yaniv14 08/09/19
Também funciona no Django 1.11 docs.djangoproject.com/en/1.11/topics/forms/formsets/…
ruohola
46

Eu criaria a classe de formulário dinamicamente em uma função, para que ela tenha acesso ao afiliado por meio do fechamento:

def make_service_form(affiliate):
    class ServiceForm(forms.Form):
        option = forms.ModelChoiceField(
                queryset=ServiceOption.objects.filter(affiliate=affiliate))
        rate = forms.DecimalField(widget=custom_widgets.SmallField())
        units = forms.IntegerField(min_value=1, 
                widget=custom_widgets.SmallField())
    return ServiceForm

Como bônus, você não precisa reescrever o conjunto de consultas no campo de opções. A desvantagem é que a subclasse é um pouco descolada. (Qualquer subclasse deve ser feita de maneira semelhante.)

editar:

Em resposta a um comentário, você pode chamar esta função sobre qualquer lugar em que usaria o nome da classe:

def view(request):
    affiliate = get_object_or_404(id=request.GET.get('id'))
    formset_cls = formset_factory(make_service_form(affiliate))
    formset = formset_cls(request.POST)
    ...
Matthew Marshall
fonte
Obrigado - funcionou. Estou adiando marcar isso como aceito, porque espero que exista uma opção mais limpa, pois fazê-lo dessa maneira definitivamente parece divertido.
Paolo Bergantino
Marcar como aceito, pois aparentemente essa é a melhor maneira de fazê-lo. Parece estranho, mas faz o truque. :) Obrigado.
Paolo Bergantino
Carl Meyer tem, eu acho, a maneira mais limpa que você estava procurando.
Jarret Hardie
Eu estou usando esse método com o Django ModelForms.
chefsmart 20/10/09
Eu gosto dessa solução, mas não sei como usá-la em uma exibição como um formset. Você tem bons exemplos de como usar isso em uma exibição? Todas as sugestões são apreciadas.
Joe J
16

Isto é o que funcionou para mim, Django 1.7:

from django.utils.functional import curry    

lols = {'lols':'lols'}
formset = modelformset_factory(MyModel, form=myForm, extra=0)
formset.form = staticmethod(curry(MyForm, lols=lols))
return formset

#form.py
class MyForm(forms.ModelForm):

    def __init__(self, lols, *args, **kwargs):

Espero que ajude alguém, demorei o suficiente para descobrir;)

rix
fonte
1
Você poderia me explicar por que staticmethodé necessário aqui?
precisa saber é
9

Eu gosto da solução de fechamento por ser "mais limpa" e mais Pythonic (então marque com +1 para responder à mmarshall), mas os formulários do Django também têm um mecanismo de retorno de chamada que você pode usar para filtrar conjuntos de consultas em conjuntos de formulários.

Também não está documentado, o que eu acho que é um indicador que os desenvolvedores do Django podem não gostar tanto.

Então, você basicamente cria o seu formset da mesma forma, mas adiciona o retorno de chamada:

ServiceFormSet = forms.formsets.formset_factory(
    ServiceForm, extra=3, formfield_callback=Callback('option', affiliate).cb)

Isso está criando uma instância de uma classe que se parece com isso:

class Callback(object):
    def __init__(self, field_name, aff):
        self._field_name = field_name
        self._aff = aff
    def cb(self, field, **kwargs):
        nf = field.formfield(**kwargs)
        if field.name == self._field_name:  # this is 'options' field
            nf.queryset = ServiceOption.objects.filter(affiliate=self._aff)
        return nf

Isso deve lhe dar uma idéia geral. É um pouco mais complexo transformar o retorno de chamada em um método de objeto como esse, mas oferece um pouco mais de flexibilidade em vez de executar um retorno de chamada de função simples.

Van Gale
fonte
1
Obrigado pela resposta. Estou usando a solução de mmarshall agora e, como você concorda, é mais Pythonic (algo que eu não saberia, pois esse é meu primeiro projeto em Python). Acho que estou mantendo isso. É definitivamente bom saber sobre o retorno de chamada. Obrigado novamente.
Paolo Bergantino
1
Obrigado. Dessa forma, funciona muito bem com modelformset_factory. Não consegui entender as outras maneiras de trabalhar adequadamente com os modelformsets, mas dessa maneira foi muito simples.
Spike
O curry funcional essencialmente cria um fechamento, não é? Por que você diz que a solução da @ mmarshall é mais pitônica? Btw, obrigado pela sua resposta. Eu gosto dessa abordagem.
23412 Josh
9

Eu queria colocar isso como um comentário na resposta de Carl Meyers, mas como isso exige pontos, eu apenas o coloquei aqui. Levei duas horas para descobrir, então espero que ajude alguém.

Uma observação sobre o uso do inlineformset_factory.

Usei essa solução sozinho e funcionou perfeitamente, até que tentei com o inlineformset_factory. Eu estava executando o Django 1.0.2 e recebi alguma exceção estranha do KeyError. Atualizei para o tronco mais recente e funcionou diretamente.

Agora posso usá-lo semelhante a este:

BookFormSet = inlineformset_factory(Author, Book, form=BookForm)
BookFormSet.form = staticmethod(curry(BookForm, user=request.user))
Johan Berg Nilsson
fonte
A mesma coisa vale para modelformset_factory. Obrigado por esta resposta!
thnee
9

A partir da confirmação e091c18f50266097f648efc7cac2503968e9d217 em terça-feira, 14 de agosto às 23:44:46 2012 +0200, a solução aceita não pode mais funcionar.

A versão atual da função django.forms.models.modelform_factory () usa uma "técnica de construção de tipo", chamando a função type () no formulário passado para obter o tipo de metaclasse e, em seguida, usando o resultado para construir um objeto de classe digite em tempo real ::

# Instatiate type(form) in order to use the same metaclass as form.
return type(form)(class_name, (form,), form_class_attrs)

Isso significa que mesmo um curryed ou partialobjeto passado em vez de um formulário "faz com que o pato o morde" por assim dizer: chamará uma função com os parâmetros de construção de um ModelFormClassobjeto, retornando a mensagem de erro:

function() argument 1 must be code, not str

Para contornar esse eu escrevi uma função de gerador que usa um fecho para retornar uma subclasse de qualquer classe especificada como primeiro parâmetro, que em seguida, chama super.__init__após updateing os kwargs com as fornecidas na chamada da função gerador ::

def class_gen_with_kwarg(cls, **additionalkwargs):
  """class generator for subclasses with additional 'stored' parameters (in a closure)
     This is required to use a formset_factory with a form that need additional 
     initialization parameters (see http://stackoverflow.com/questions/622982/django-passing-custom-form-parameters-to-formset)
  """
  class ClassWithKwargs(cls):
      def __init__(self, *args, **kwargs):
          kwargs.update(additionalkwargs)
          super(ClassWithKwargs, self).__init__(*args, **kwargs)
  return ClassWithKwargs

Em seu código, você chamará a fábrica de formulários como:

MyFormSet = inlineformset_factory(ParentModel, Model,form = class_gen_with_kwarg(MyForm, user=self.request.user))

ressalvas:

  • isso recebeu muito pouco teste, pelo menos por enquanto
  • parâmetros fornecidos podem colidir e sobrescrever aqueles usados ​​por qualquer código que usará o objeto retornado pelo construtor
RobM
fonte
Obrigado, parece funcionar muito bem no Django 1.10.1, ao contrário de algumas das outras soluções aqui.
Fpghost 14/09/16
1
@fpghost lembre-se de que, pelo menos até 1,9 (ainda não estou na 1,10 por vários motivos), se você só precisa alterar o QuerySet no qual o formulário é construído, é possível atualizá-lo no retornou MyFormSet alterando seu atributo .queryset antes de usá-lo. Menos flexível que esse método, mas muito mais simples de ler / entender.
RobM 16/09/16
3

A solução de Carl Meyer parece muito elegante. Eu tentei implementá-lo para modelformsets. Fiquei com a impressão de que não podia chamar métodos estáticos em uma classe, mas o seguinte funciona inexplicavelmente:

class MyModel(models.Model):
  myField = models.CharField(max_length=10)

class MyForm(ModelForm):
  _request = None
  class Meta:
    model = MyModel

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

class MyFormsetBase(BaseModelFormSet):
  _request = None

def __init__(self,*args,**kwargs):
  self._request = kwargs.pop('request', None)
  subFormClass = self.form
  self.form = curry(subFormClass,request=self._request)
  super(MyFormsetBase,self).__init__(*args,**kwargs)

MyFormset =  modelformset_factory(MyModel,formset=MyFormsetBase,extra=1,max_num=10,can_delete=True)
MyFormset.form = staticmethod(curry(MyForm,request=MyFormsetBase._request))

Na minha opinião, se eu fizer algo assim:

formset = MyFormset(request.POST,queryset=MyModel.objects.all(),request=request)

Em seguida, a palavra-chave "request" é propagada para todos os formulários membros do meu formset. Estou satisfeito, mas não tenho ideia de por que isso está funcionando - parece errado. Alguma sugestão?

trubliphone
fonte
Hmmm ... Agora, se eu tentar acessar o atributo form de uma instância de MyFormSet, ele (corretamente) retornará <function _curried> em vez de <MyForm>. Alguma sugestão de como acessar o formulário atual? Eu tentei MyFormSet.form.Meta.model.
trubliphone
Opa ... Eu tenho que chamar a função ao curry para acessar o formulário. MyFormSet.form().Meta.model. Óbvio mesmo.
trubliphone
Eu tenho tentado aplicar sua solução ao meu problema, mas acho que não entendo completamente toda a sua resposta. Alguma idéia se sua abordagem puder ser aplicada ao meu problema aqui? stackoverflow.com/questions/14176265/…
finspin
1

Passei algum tempo tentando descobrir esse problema antes de ver esta postagem.

A solução que eu encontrei foi a solução de fechamento (e é uma solução que eu usei antes com os formulários de modelo do Django).

Eu tentei o método curry () como descrito acima, mas simplesmente não consegui fazê-lo funcionar com o Django 1.0, então, no final, voltei ao método de fechamento.

O método de fechamento é muito elegante e a única pequena estranheza é que a definição de classe está aninhada dentro da visualização ou de outra função. Eu acho que o fato de isso parecer estranho para mim é uma dificuldade da minha experiência anterior em programação e acho que alguém com experiência em linguagens mais dinâmicas não se incomodaria!

Nick Craig-Wood
fonte
1

Eu tive que fazer uma coisa semelhante. Isso é semelhante à currysolução:

def form_with_my_variable(myvar):
   class MyForm(ServiceForm):
     def __init__(self, myvar=myvar, *args, **kwargs):
       super(SeriveForm, self).__init__(myvar=myvar, *args, **kwargs)
   return MyForm

factory = inlineformset_factory(..., form=form_with_my_variable(myvar), ... )
Rory
fonte
1

Com base nesta resposta , encontrei uma solução mais clara:

class ServiceForm(forms.Form):
    option = forms.ModelChoiceField(
            queryset=ServiceOption.objects.filter(affiliate=self.affiliate))
    rate = forms.DecimalField(widget=custom_widgets.SmallField())
    units = forms.IntegerField(min_value=1, 
            widget=custom_widgets.SmallField())

    @staticmethod
    def make_service_form(affiliate):
        self.affiliate = affiliate
        return ServiceForm

E execute-o como

formset_factory(form=ServiceForm.make_service_form(affiliate))
alexey_efimov
fonte
6
O Django 1.9 tornou isso desnecessário, use form_kwargs.
Paolo Bergantino
No meu trabalho atual, precisamos usar o django legado 1.7 (((
alexey_efimov 31/08/16
0

Eu sou um novato aqui, então não posso adicionar comentários. Espero que este código funcione também:

ServiceFormSet = formset_factory(ServiceForm, extra=3)

ServiceFormSet.formset = staticmethod(curry(ServiceForm, affiliate=request.affiliate))

como adicionar parâmetros adicionais ao formset em BaseFormSetvez de ao form.

Philamer Sune
fonte