Django Passing Custom Form Parameters to Formset

2019-01-01 07:52发布

问题:

This was fixed in Django 1.9 with form_kwargs.

I have a Django Form that looks like this:

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)

I call this form with something like this:

form = ServiceForm(affiliate=request.affiliate)

Where request.affiliate is the logged in user. This works as intended.

My problem is that I now want to turn this single form into a formset. What I can\'t figure out is how I can pass the affiliate information to the individual forms when creating the formset. According to the docs to make a formset out of this I need to do something like this:

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

And then I need to create it like this:

formset = ServiceFormSet()

Now how can I pass affiliate=request.affiliate to the individual forms this way?

回答1:

I would use functools.partial and 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)

I think this is the cleanest approach, and doesn\'t affect ServiceForm in any way (i.e. by making it difficult to subclass).



回答2:

I would build the form class dynamically in a function, so that it has access to the affiliate via closure:

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

As a bonus, you don\'t have to rewrite the queryset in the option field. The downside is that subclassing is a little funky. (Any subclass has to be made in a similar way.)

edit:

In response to a comment, you can call this function about any place you would use the class name:

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


回答3:

Official Document 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



回答4:

This is what worked for me, 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):

Hope it helps someone, took me long enough to figure it out ;)



回答5:

I like the closure solution for being \"cleaner\" and more Pythonic (so +1 to mmarshall answer) but Django forms also have a callback mechanism you can use for filtering querysets in formsets.

It\'s also not documented, which I think is an indicator the Django devs might not like it as much.

So you basically create your formset the same but add the callback:

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

This is creating an instance of a class that looks like this:

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

This should give you the general idea. It\'s a little more complex making the callback an object method like this, but gives you a little more flexibility as opposed to doing a simple function callback.



回答6:

I wanted to place this as a comment to Carl Meyers answer, but since that requires points I just placed it here. This took me 2 hours to figure out so I hope it will help someone.

A note about using the inlineformset_factory.

I used that solution my self and it worked perfect, until I tried it with the inlineformset_factory. I was running Django 1.0.2 and got some strange KeyError exception. I upgraded to latest trunk and it worked direct.

I can now use it similar to this:

BookFormSet = inlineformset_factory(Author, Book, form=BookForm)
BookFormSet.form = staticmethod(curry(BookForm, user=request.user))


回答7:

As of commit e091c18f50266097f648efc7cac2503968e9d217 on Tue Aug 14 23:44:46 2012 +0200 the accepted solution can\'t work anymore.

The current version of django.forms.models.modelform_factory() function uses a \"type construction technique\", calling the type() function on the passed form to get the metaclass type, then using the result to construct a class-object of its type on the fly::

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

This means even a curryed or partial object passed instead of a form \"causes the duck to bite you\" so to speak: it\'ll call a function with the construction parameters of a ModelFormClass object, returning the error message::

function() argument 1 must be code, not str

To work around this I wrote a generator function that uses a closure to return a subclass of any class specified as first parameter, that then calls super.__init__ after updateing the kwargs with the ones supplied on the generator function\'s call::

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

Then in your code you\'ll call the form factory as::

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

caveats:

  • this received very little testing, at least for now
  • supplied parameters could clash and overwrite those used by whatever code will use the object returned by the constructor


回答8:

Carl Meyer\'s solution looks very elegant. I tried implementing it for modelformsets. I was under the impression that I could not call staticmethods within a class, but the following inexplicably works:

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

In my view, if I do something like this:

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

Then the \"request\" keyword gets propagated to all of the member forms of my formset. I\'m pleased, but I have no idea why this is working - it seems wrong. Any suggestions?



回答9:

I spent some time trying to figure out this problem before I saw this posting.

The solution I came up with was the closure solution (and it is a solution I\'ve used before with Django model forms).

I tried the curry() method as described above, but I just couldn\'t get it to work with Django 1.0 so in the end I reverted to the closure method.

The closure method is very neat and the only slight oddness is that the class definition is nested inside the view or another function. I think the fact that this looks odd to me is a hangup from my previous programming experience and I think someone with a background in more dynamic languages wouldn\'t bat an eyelid!



回答10:

I had to do a similar thing. This is similar to the curry solution:

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


回答11:

I\'m a newbie here so I can\'t add comment. I hope this code will work too:

ServiceFormSet = formset_factory(ServiceForm, extra=3)

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

as for adding additional parameters to the formset\'s BaseFormSet instead of form.



回答12:

based on this answer I found more clear solution:

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

And run it in view like

formset_factory(form=ServiceForm.make_service_form(affiliate))