关于python:Django将自定义表单参数传递给Formset

Django Passing Custom Form Parameters to Formset

This was fixed in Django 1.9 with form_kwargs.

我有张像这样的django表格:

1
2
3
4
5
6
7
8
9
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)

我将此表单命名为:

1
form = ServiceForm(affiliate=request.affiliate)

其中,request.affiliate是登录用户。这是按预期工作的。

我的问题是我现在想把这个单一的表单变成一个表单集。我无法理解的是,在创建表单集时,如何将关联信息传递给各个表单。根据文档,我需要做如下的事情:

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

然后我需要这样创建它:

1
formset = ServiceFormSet()

现在,如何通过这种方式将affiliate=request.affiliate传递给各个表单?


我将使用functools.partial和functools.wrapps:

1
2
3
4
from functools import partial, wraps
from django.forms.formsets import formset_factory

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

我认为这是最干净的方法,不会以任何方式影响ServiceForm(也就是说,通过使子类化变得困难)。


公文方式

Django 2:

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

https://docs.djangoproject.com/en/2.0/topics/forms/formsets/向formset表单传递自定义参数


我将在一个函数中动态地构建表单类,这样它就可以通过闭包访问关联:

1
2
3
4
5
6
7
8
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

另外,您不必在选项字段中重写查询集。缺点是子类化有点奇怪。(任何子类都必须以类似的方式制作。)

编辑:

作为对注释的响应,您可以对将使用类名的任何位置调用此函数:

1
2
3
4
5
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)
    ...


这就是我的工作,Django 1.7:

1
2
3
4
5
6
7
8
9
10
11
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):

希望它能帮助别人,花了我足够长的时间才弄明白;)


截止到2012年8月14日星期二23:44:46+0200提交e091c18f50266097f648efc7cac2503968e9d217,已接受的解决方案将无法再工作。

django.forms.models.modelForm_Factory()函数的当前版本使用"类型构造技术",在传递的表单上调用type()函数以获取元类类型,然后使用结果动态构造其类型的类对象:

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

这意味着,即使传递了一个curryed或partial对象,而不是一个"导致鸭子咬你"的形式,也可以这么说:它将调用一个具有ModelFormClass对象构造参数的函数,返回错误消息:

1
function() argument 1 must be code, not str

为了解决这个问题,我编写了一个生成器函数,它使用一个闭包返回指定为第一个参数的任何类的子类,然后在update使用生成器函数调用上提供的闭包调用wargs之后调用super.__init__

1
2
3
4
5
6
7
8
9
10
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

然后在您的代码中,您将调用表单工厂作为:

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

注意事项:

  • 至少目前为止,这还没有得到多少测试。
  • 提供的参数可能会冲突并覆盖由任何代码使用构造函数返回的对象所使用的参数。


我想把它作为对卡尔·迈耶斯答案的评论,但因为这需要点,所以我把它放在这里。我花了2个小时才弄明白,所以我希望它能帮助别人。

关于使用inlineformset_工厂的说明。

我用这个方法解决了我自己的问题,而且效果很好,直到我在inlineformset工厂尝试过。我在运行django 1.0.2,得到了一些奇怪的keyerror异常。我升级到最新的行李箱,它直接工作。

我现在可以这样使用它:

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


我喜欢闭包解决方案,因为它"更干净"和更多的pythonic(so+1到mmarshall答案),但是django表单也有一个回调机制,可以用来过滤表单集中的查询集。

它也没有文档记录,我认为这是一个指标,Django开发人员可能不太喜欢它。

所以您基本上创建了相同的表单集,但添加了回调:

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

这将创建如下所示的类的实例:

1
2
3
4
5
6
7
8
9
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

这应该给你一个大概的想法。使回调成为这样的对象方法有点复杂,但与执行简单的函数回调相比,它给了您更多的灵活性。


卡尔·迈耶的解决方案看起来很优雅。我尝试对模型集实现它。我觉得我不能在类中调用staticmethods,但是下面的方法确实有效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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))

在我看来,如果我这样做:

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

然后"request"关键字被传播到我的表单集的所有成员表单。我很高兴,但我不知道为什么这是有效的-这似乎是错误的。有什么建议吗?


我也不得不做类似的事情。这与curry解决方案类似:

1
2
3
4
5
6
7
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), ... )

在看到这篇文章之前,我花了一些时间想弄清楚这个问题。

我提出的解决方案是闭包解决方案(这是我以前在Django模型表单中使用过的解决方案)。

我尝试了上面描述的curry()方法,但是我无法让它与django 1.0一起工作,所以最后我恢复到闭包方法。

闭包方法非常整洁,唯一有点奇怪的是类定义嵌套在视图或其他函数中。我认为这对我来说很奇怪,这是我以前编程经验的一个障碍,我认为有更动态语言背景的人是不会眨眼的!


基于这个答案,我找到了更清晰的解决方案:

1
2
3
4
5
6
7
8
9
10
11
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

在视图中运行它

1
formset_factory(form=ServiceForm.make_service_form(affiliate))


我是这里的新手,所以我不能添加评论。我希望这个代码也能工作:

1
2
3
ServiceFormSet = formset_factory(ServiceForm, extra=3)

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

至于在表单集的BaseFormSet中添加其他参数,而不是表单。