Django将自定义表单参数传递给Formset


150

这在Django 1.9中用form_kwargs修复

我有一个看起来像这样的Django表单:

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)

我称这种形式是这样的:

form = ServiceForm(affiliate=request.affiliate)

request.affiliate登录用户在哪里。这按预期工作。

我的问题是我现在想将此单一表单转换为表单集。我不知道的是在创建表单集时如何将会员信息传递给各个表单。根据文档来制作一个表单集,我需要做这样的事情:

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

然后我需要这样创建它:

formset = ServiceFormSet()

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

Answers:


105

我会用functools.partialfunctools.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)

我认为这是最干净的方法,并且不会以任何方式影响ServiceForm(即,使子类难以继承)。


它对我不起作用。我得到的错误:AttributeError的:“_curriedFormSet”对象有没有属性“得到”
保罗Bergantino

我无法重复此错误。这也是一个奇怪的问题,因为表单集通常没有'get'属性,因此您似乎在代码中做了一些奇怪的事情。(此外,我用一种消除“ _curriedFormSet”之类的奇怪方式更新了答案)。
卡尔·迈耶

我正在重新讨论此问题,因为我想让您的解决方案正常工作。我可以声明该formset很好,但是如果我尝试打印它{{formset}},这是当我收到“没有属性'get'”错误时。您提供的任何一种解决方案都会发生这种情况。如果我遍历表单集并将表单打印为{{form}},则会再次出现错误。例如,如果我循环并打印为{{form.as_table}},则会得到空的表格表,即。没有打印任何字段。有任何想法吗?
Paolo Bergantino,

您说得对,对不起。我之前的测试还远远不够。我对此进行了跟踪,并且由于FormSet在内部工作的方式有些奇怪而使其中断。有一种方法可以解决该问题,但是它开始失去本来的优雅……
Carl Meyer

5
如果这里的注释线程没有意义,那是因为我只是编辑答案以使用Python functools.partial而不是Django的答案django.utils.functional.curry。它们执行相同的操作,除了functools.partial返回一个独特的可调用类型而不是常规的Python函数,并且该partial类型不作为实例方法进行绑定,这巧妙地解决了此注释线程主要用于调试的问题。
卡尔·梅耶

81

正式文件方式

Django 2.0:

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

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


8
这应该是现在正确的方法。公认的答案有效且不错,但很
容易

绝对是最好的答案和正确的方法。
yaniv14


46

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

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

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

编辑:

为了回应评论,您可以在使用类名的任何地方调用此函数:

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

谢谢-可行。我推迟将其标记为已接受,因为我有点希望有一个更清洁的选择,因为这样做绝对感觉很时髦。
Paolo Bergantino,2009年

标记为已接受,因为显然这是最好的方法。感觉很奇怪,但是能解决问题。:) 谢谢。
Paolo Bergantino,

我认为,卡尔·迈耶(Carl Meyer)拥有您寻找的更清洁的方式。
Jarret Hardie

我正在Django ModelForms中使用此方法。
Chefsmart

我喜欢这种解决方案,但不确定如何在像表单集的视图中使用它。您是否有很好的示例说明如何在视图中使用它?任何建议表示赞赏。
乔·J

16

这是对我有效的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):

希望它能对某人有所帮助,花了我足够长的时间才能弄清楚;)


1
您能告诉我为什么staticmethod在这里需要吗?
fpghost

9

我喜欢闭包解决方案,因为它更“干净”,并且使用了更多的Python语言(因此对+1表示响应),但Django表单也具有可用于过滤表单集中的查询集的回调机制。

它也没有记录,我认为这表明Django开发人员可能不太喜欢它。

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

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

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

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

这应该给您大致的想法。使回调成为这样的对象方法要稍微复杂一些,但是与执行简单的函数回调相比,它具有更多的灵活性。


1
谢谢你的回答。我现在正在使用mmarshall的解决方案,因为您同意它更像Python(我不知道,因为这是我的第一个Python项目),我想我会坚持下去。不过,了解回调绝对是一件好事。再次感谢。
Paolo Bergantino,

1
谢谢。这种方式非常适合modelformset_factory。我无法正确地使用其他方式使用modelformsets,但是这种方式非常简单。

curry函数本质上会创建一个关闭,不是吗?为什么您说@mmarshall的解决方案更像Pythonic?顺便说一句,谢谢您的回答。我喜欢这种方法。
2012年

9

我想将其作为对卡尔·迈耶斯答案的评论,但由于这需要要点,因此我将其放在此处。我花了2个小时才弄清楚,所以希望对您有所帮助。

有关使用inlineformset_factory的注释。

我自己使用了该解决方案,并且效果很好,直到我使用inlineformset_factory对其进行了尝试。我正在运行Django 1.0.2,并遇到了一些奇怪的KeyError异常。我升级到最新的后备箱,并且可以直接使用。

我现在可以像这样使用它:

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

同样的道理modelformset_factory。感谢您的回答!
thnee 2014年

9

从2012年8月14日星期二23:44:46 +0200提交e091c18f50266097f648efc7cac2503968e9d217开始,已接受的解决方案不再起作用。

当前版本的django.forms.models.modelform_factory()函数使用“类型构造技术”,在传递的表单上调用type()函数以获取元类类型,然后使用结果来构造其类对象即时输入::

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

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

function() argument 1 must be code, not str

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

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

然后在您的代码中将表单工厂称为::

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

注意事项:

  • 至少到目前为止,这很少接受测试
  • 提供的参数可能会冲突并覆盖那些将使用构造函数返回的对象的代码所使用的参数

谢谢,似乎在Django 1.10.1中效果很好,这与此处的其他一些解决方案不同。
fpghost

1
@fpghost请记住,至少要达到1.9(出于多种原因,我仍然不在1.10上),如果您需要做的就是更改构造表单的QuerySet,则可以在在使用MyFormSet之前,通过更改其.queryset属性返回MyFormSet。灵活性不如此方法,但更易于阅读/理解。
RobM 2016年

3

卡尔·迈耶的解决方案看起来非常优雅。我尝试为modelformsets实现它。我的印象是我无法在类中调用静态方法,但是以下操作莫名其妙地起作用:

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

在我看来,如果我做这样的事情:

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

然后,“ request”关键字传播到我的表单集中的所有成员表单。我很高兴,但我不知道为什么这样做有效-似乎错了。有什么建议?


嗯...现在,如果我尝试访问MyFormSet实例的form属性,它将(正确)返回<function _curried>而不是<MyForm>。但是,有关如何访问实际表格的任何建议?我试过了MyFormSet.form.Meta.model
trubliphone'2

糟糕...我必须调用 curried函数才能访问表单。 MyFormSet.form().Meta.model。确实很明显。
trubliphone'2

我一直在尝试将您的解决方案应用于我的问题,但我想我还不太了解您的全部答案。有什么想法可以将您的方法应用于我的问题吗?stackoverflow.com/questions/14176265/...
finspin

1

在看到这篇文章之前,我花了一些时间试图解决这个问题。

我想到的解决方案是闭包解决方案(这是我之前在Django模型表单中使用的解决方案)。

我如上所述尝试了curry()方法,但是我无法使它与Django 1.0一起使用,因此最终我恢复为闭包方法。

闭合方法非常简洁,唯一的奇怪之处是类定义嵌套在视图或其他函数中。我认为这对我来说似乎很奇怪,这是我以前的编程经验的遗忘,而且我认为具有动态语言背景的人也不会蒙上双眼!


1

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

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

1

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

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

并像这样运行它

formset_factory(form=ServiceForm.make_service_form(affiliate))

6
Django 1.9使所有这些都不是必须的,请改用form_kwargs。
Paolo Bergantino,

在我目前的工作中,我们需要使用旧版django 1.7((
alexey_efimov

0

我是这里的新手,因此无法添加评论。我希望这段代码也能工作:

ServiceFormSet = formset_factory(ServiceForm, extra=3)

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

至于向BaseFormSet表单集(而不是表单)添加其他参数。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.