Django pasa parámetros de formulario personalizados al formulario

150

Esto se solucionó en Django 1.9 con form_kwargs .

Tengo un formulario de Django que se ve así:

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)

Llamo a este formulario con algo como esto:

form = ServiceForm(affiliate=request.affiliate)

¿Dónde request.affiliateestá el usuario conectado? Esto funciona según lo previsto.

Mi problema es que ahora quiero convertir este formulario único en un conjunto de formularios. Lo que no puedo entender es cómo puedo pasar la información del afiliado a los formularios individuales al crear el conjunto de formularios. De acuerdo con los documentos para hacer un formulario de esto, necesito hacer algo como esto:

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

Y luego necesito crearlo así:

formset = ServiceFormSet()

Ahora, ¿cómo puedo pasar afiliado = request.affiliate a los formularios individuales de esta manera?

Paolo Bergantino
fuente

Respuestas:

105

Me gustaría utilizar functools.partial y 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)

Creo que este es el enfoque más limpio y no afecta a ServiceForm de ninguna manera (es decir, al dificultar la subclase).

Carl Meyer
fuente
No está funcionando para mi. Me sale el error: AttributeError: el objeto '_curriedFormSet' no tiene el atributo 'get'
Paolo Bergantino
No puedo duplicar este error. También es extraño porque un formulario generalmente no tiene un atributo 'get', por lo que parece que podría estar haciendo algo extraño en su código. (Además, actualicé la respuesta con una forma de deshacerme de rarezas como '_curriedFormSet').
Carl Meyer
Estoy revisando esto porque me gustaría que tu solución funcione. Puedo declarar bien el conjunto de formularios, pero si intento imprimirlo haciendo {{formset}} es cuando obtengo el error "no tiene atributo 'get'". Sucede con cualquiera de las soluciones que proporcionó. Si recorro el conjunto de formularios e imprimo los formularios como {{form}}, obtengo el error nuevamente. Si hago un bucle e imprimo como {{form.as_table}}, por ejemplo, obtengo tablas de formularios vacías, es decir. No se imprimen campos. ¿Algunas ideas?
Paolo Bergantino
Tienes razón, lo siento; mis pruebas anteriores no fueron lo suficientemente lejos. Rastreé esto, y se rompe debido a algunas rarezas en la forma en que FormSets funciona internamente. Hay una manera de solucionar el problema, pero empieza a perder la elegancia original ...
Carl Meyer
55
Si el hilo de comentarios aquí no tiene sentido, es porque acabo de editar la respuesta para usar Python en functools.partiallugar de Django django.utils.functional.curry. Hacen lo mismo, excepto que functools.partialdevuelve un tipo invocable distinto en lugar de una función Python normal, y el partialtipo no se vincula como un método de instancia, que resuelve perfectamente el problema que este hilo de comentarios se dedicó en gran medida a la depuración.
Carl Meyer
81

Manera oficial del documento

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
fuente
8
Esta debería ser la forma correcta de hacerlo ahora. la respuesta aceptada funciona y es agradable pero es un truco
Junchao Gu
Definitivamente la mejor respuesta y la forma correcta de hacerlo.
yaniv14
También funciona en Django 1.11 docs.djangoproject.com/en/1.11/topics/forms/formsets/…
ruohola
46

Construiría la clase de formulario dinámicamente en una función, para que tenga acceso al afiliado a través del cierre:

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 beneficio adicional, no tiene que volver a escribir el conjunto de consultas en el campo de opción. La desventaja es que la subclase es un poco funky. (Cualquier subclase debe hacerse de manera similar).

editar:

En respuesta a un comentario, puede llamar a esta función sobre cualquier lugar donde usaría el nombre de la clase:

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
fuente
Gracias, eso funcionó. Espero marcar esto como aceptado porque espero que haya una opción más limpia, ya que hacerlo de esta manera definitivamente se siente raro.
Paolo Bergantino
Marcar como aceptado ya que aparentemente esta es la mejor manera de hacerlo. Se siente raro, pero hace el truco. :) Gracias.
Paolo Bergantino
Carl Meyer tiene, creo, la forma más limpia que estabas buscando.
Jarret Hardie
Estoy usando este método con Django ModelForms.
chefsmart
Me gusta esta solución, pero no estoy seguro de cómo usarla en una vista como un conjunto de formularios. ¿Tiene algún buen ejemplo de cómo usar esto en una vista? Cualquier sugerencia es apreciada.
Joe J
16

Esto es lo que funcionó para mí, 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 ayude a alguien, me tomó el tiempo suficiente para resolverlo;)

rix
fuente
1
¿Podría explicarme por qué staticmethodse necesita aquí?
fpghost
9

Me gusta la solución de cierre por ser "más limpia" y más Pythonic (por lo tanto, +1 a la respuesta de mmarshall) pero los formularios de Django también tienen un mecanismo de devolución de llamada que puede usar para filtrar conjuntos de consultas en conjuntos de formularios.

Tampoco está documentado, lo que creo que es un indicador de que a los desarrolladores de Django no les gustará tanto.

Entonces, básicamente, crea su conjunto de formularios de la misma manera pero agrega la devolución de llamada:

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

Esto está creando una instancia de una clase que se ve así:

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

Esto debería darte la idea general. Es un poco más complejo hacer que la devolución de llamada sea un método de objeto como este, pero le brinda un poco más de flexibilidad en lugar de hacer una devolución de llamada de función simple.

Van Gale
fuente
1
Gracias por la respuesta. Estoy usando la solución de mmarshall en este momento y, dado que está de acuerdo, es más Pythonic (algo que no sabría, ya que este es mi primer proyecto de Python) Creo que me estoy quedando con eso. Sin embargo, definitivamente es bueno saber acerca de la devolución de llamada. Gracias de nuevo.
Paolo Bergantino
1
Gracias. De esta manera funciona muy bien con modelformset_factory. No pude encontrar las otras formas de trabajar con conjuntos de modelos de forma adecuada, pero esta forma fue muy sencilla.
Spike
El curry funcional esencialmente crea un cierre, ¿no? ¿Por qué dices que la solución de @mmarshall es más Pythonic? Por cierto, gracias por tu respuesta. Me gusta este enfoque.
Josh el
9

Quería colocar esto como un comentario a la respuesta de Carl Meyers, pero como eso requiere puntos, simplemente lo coloqué aquí. Me llevó 2 horas entenderlo, así que espero que ayude a alguien.

Una nota sobre el uso de inlineformset_factory.

Utilicé esa solución por mi cuenta y funcionó perfectamente, hasta que la probé con inlineformset_factory. Estaba ejecutando Django 1.0.2 y obtuve una extraña excepción KeyError. Actualicé a la última troncal y funcionó directamente.

Ahora puedo usarlo de manera similar a esto:

BookFormSet = inlineformset_factory(Author, Book, form=BookForm)
BookFormSet.form = staticmethod(curry(BookForm, user=request.user))
Johan Berg Nilsson
fuente
Lo mismo vale para modelformset_factory. Gracias por esta respuesta!
Thnee
9

A partir de commit e091c18f50266097f648efc7cac2503968e9d217 el martes 14 de agosto 23:44:46 2012 +0200 la solución aceptada ya no puede funcionar.

La versión actual de la función django.forms.models.modelform_factory () utiliza una "técnica de construcción de tipos", que llama a la función type () en el formulario pasado para obtener el tipo de metaclase, luego usa el resultado para construir un objeto de clase de su escriba sobre la marcha ::

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

Esto significa incluso un currypartial objeto ed o pasado en lugar de una forma "hace que el pato te muerda", por así decirlo: llamará a una función con los parámetros de construcción de un ModelFormClassobjeto, devolviendo el mensaje de error ::

function() argument 1 must be code, not str

Para evitar esto, escribí una función de generador que usa un cierre para devolver una subclase de cualquier clase especificada como primer parámetro, que luego llama super.__init__después updatede enviar los kwargs con los que se proporcionan en la llamada de la función del generador ::

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

Luego, en su código, llamará a la fábrica de formularios como ::

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

advertencias:

  • esto recibió muy pocas pruebas, al menos por ahora
  • los parámetros proporcionados podrían entrar en conflicto y sobrescribir los utilizados por cualquier código que use el objeto devuelto por el constructor
RobM
fuente
Gracias, parece funcionar muy bien en Django 1.10.1 a diferencia de algunas de las otras soluciones aquí.
fpghost
1
@fpghost tenga en cuenta que, al menos hasta 1.9 (todavía no estoy en 1.10 por varias razones) si todo lo que necesita hacer es cambiar el QuerySet sobre el que se construye el formulario, puede actualizarlo en el devolvió MyFormSet cambiando su atributo .queryset antes de usarlo. Menos flexible que este método, pero mucho más simple de leer / comprender.
RobM
3

La solución de Carl Meyer se ve muy elegante. Intenté implementarlo para modelformsets. Tenía la impresión de que no podía llamar a métodos estáticos dentro de una clase, pero lo siguiente funciona inexplicablemente:

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

En mi opinión, si hago algo como esto:

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

Luego, la palabra clave "solicitud" se propaga a todos los formularios de miembros de mi conjunto de formularios. Estoy contento, pero no tengo idea de por qué está funcionando, parece estar mal. ¿Alguna sugerencia?

trubliphone
fuente
Hmmm ... Ahora, si intento acceder al atributo de formulario de una instancia de MyFormSet, (correctamente) devuelve <function _curried> en lugar de <MyForm>. ¿Alguna sugerencia sobre cómo acceder al formulario real? He tratado MyFormSet.form.Meta.model.
trubliphone
Whoops ... Tengo que llamar a la función curry para acceder al formulario. MyFormSet.form().Meta.model. Obvio realmente.
Trubliphone
He estado tratando de aplicar su solución a mi problema, pero creo que no entiendo completamente su respuesta. ¿Alguna idea de si su enfoque se puede aplicar a mi problema aquí? stackoverflow.com/questions/14176265/…
finspin
1

Pasé algún tiempo tratando de resolver este problema antes de ver esta publicación.

La solución que se me ocurrió fue la solución de cierre (y es una solución que he usado antes con los formularios modelo Django).

Probé el método curry () como se describió anteriormente, pero no pude hacerlo funcionar con Django 1.0, así que al final volví al método de cierre.

El método de cierre es muy claro y la única rareza es que la definición de clase está anidada dentro de la vista u otra función. ¡Creo que el hecho de que esto me parezca extraño es un problema de mi experiencia previa en programación y creo que alguien con experiencia en lenguajes más dinámicos no pestañearía!

Nick Craig-Wood
fuente
1

Tuve que hacer algo similar. Esto es similar a la currysolución:

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
fuente
1

En base a esta respuesta , encontré una solución más 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

Y ejecutarlo a la vista como

formset_factory(form=ServiceForm.make_service_form(affiliate))
alexey_efimov
fuente
66
Django 1.9 hizo que todo esto fuera innecesario, use form_kwargs en su lugar.
Paolo Bergantino
En mi trabajo actual, necesitamos usar el legado django 1.7 ((
alexey_efimov el
0

Soy un novato aquí, así que no puedo agregar comentarios. Espero que este código también funcione:

ServiceFormSet = formset_factory(ServiceForm, extra=3)

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

en cuanto a agregar parámetros adicionales al conjunto BaseFormSetde formularios en lugar del formulario.

Philamer Sune
fuente