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

162

使用 form_kwargs 在 Django 1.9 中已经修复了这个问题。

我有一个长得像这样的 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 传递给各个表单?

12个回答

110

14
现在这应该是正确的做法。所接受的答案可行且不错,但有些取巧。 - Junchao Gu
2
绝对是最佳答案和正确的做法。 - yaniv14
3
同样适用于 Django 1.11 版本。https://docs.djangoproject.com/en/1.11/topics/forms/formsets/#passing-custom-parameters-to-formset-forms - ruohola

106

我会使用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(例如通过使其难以进行子类化)。

不要忘记 from django.forms.formsets import formset_factory - Ashley
6
如果这里的评论串不太通顺,那是因为我刚刚编辑了答案,使用了 Python 的 functools.partial 替代了 Django 的 django.utils.functional.curry。它们的作用是相同的,只是 functools.partial 返回一个不同的可调用类型,而不是普通的 Python 函数,并且 partial 类型不会绑定为实例方法,这样就很好地解决了这个评论串主要致力于调试的问题。 - Carl Meyer
@boldnik 你有喜欢Ajax的理由吗?这个解决方案对我来说没有太多意义。 - Carl Meyer
@Carl Meyer 我的观点是代码应该清晰易读,容易理解。这就是为什么使用django.utils.functional.curry可能不明显的原因。但最近我在每个表单中实现了一个自定义参数(reauest.user)在表单集中。我通过创建CustomFormset(BaseModelFormSet)来实现它。在__init__中,它会pop一个附加参数的字典,并将其附加到表单集的主体每个表单的_construct_form方法中。使用setattr,这个CustomFormSet可以在任何地方使用。但我同意这也不是一种明显的方式... - boldnik
2
这是一个hack!使用roach的答案,它遵循官方文档的方式。 - Bigair
显示剩余13条评论

47

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

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
标记为已接受,因为显然这是最好的方法。感觉有点奇怪,但确实有效。:)谢谢。 - Paolo Bergantino
Carl Meyer有你在寻找的更简洁的方法。 - Jarret Hardie
我正在使用Django的ModelForms方法。 - chefsmart
我喜欢这个解决方案,但是我不确定如何在视图中像表单集一样使用它。你有任何好的例子可以展示如何在视图中使用吗?任何建议都将不胜感激。 - Joe 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

10

我原本想将这个放在Carl Meyers答案的评论中,但是因为那需要积分,所以我就把它放在这里了。我花了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

10

我喜欢使用闭包的解决方案,因为它更加"干净"和符合Pythonic方式(所以对mmarshall的回答点赞+1)但是Django表单也有一个回调机制,你可以用它来过滤formsets中的queryset。

这个机制在文档中没有提到,我认为这表明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的解决方案,既然你也认为它更符合Pythonic(这是我第一个Python项目,我不知道),我想我会坚持使用它。不过了解回调函数肯定是很好的。再次感谢。 - Paolo Bergantino
1
谢谢。这种方法与modelformset_factory非常配合。我无法正确地使用其他方式来处理modelformsets,但这种方式非常直接。 - Spike
柯里化函数本质上创建了一个闭包,不是吗?你为什么说@mmarshall的解决方案更符合Python风格?顺便说一句,谢谢你的回答。我喜欢这种方法。 - Josh

10

从提交的e091c18f50266097f648efc7cac2503968e9d217开始,在2012年8月14日23:44:46 +0200时,原先被接受的解决方案已经无法使用。

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)
这意味着即使传递了一个被柯里化或部分应用的对象而不是一个表单,"鸭子"也会咬你,换句话说:它将使用ModelFormClass对象的构造参数调用一个函数,返回错误信息。
function() argument 1 must be code, not str
为了解决这个问题,我编写了一个生成器函数。该函数使用闭包来返回指定为第一个参数的任何类的子类,并在使用生成器函数调用时将kwargs与提供的kwargs合并后,调用super.__init__
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 https://dev59.com/i3RB5IYBdhLWcg3wcm2d)
  """
  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,你可以在使用之前通过更改其.queryset属性来更新返回的MyFormSet。虽然不如这种方法灵活,但更易于阅读/理解。 - RobM
@RobM - 我遇到了一个错误 _NameError: name 'self' is not defined_。 - shaan
@shaan 在哪一行?是调用 super() 的那一行(如果你正在使用 Python 3,你可以简单地写成 super().init(*args, **kwargs)),还是调用 inlineformset_factory 的那一行?如果是调用工厂函数的那一行,你需要将 self.request.user 替换为在你的代码中包含用户的变量。你可能没有使用基于类的视图,所以你没有 self,但是有 request 作为参数:在这种情况下,它是 request.user。 - RobM

3

Carl Meyer的解决方案看起来非常优雅。我尝试将其应用于modelformsets。我曾经认为在类内部不能调用staticmethods,但是以下代码不知道为什么可以正常工作:

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实例的表单属性,它(正确地)返回<function _curried>而不是<MyForm>。有没有关于如何访问实际表单的建议?我已经尝试过MyFormSet.form.Meta.model了。 - trubliphone
糟糕...我必须调用柯里化函数才能访问表格。MyFormSet.form().Meta.model。显然是这样的。 - trubliphone
我一直在尝试将您的解决方案应用于我的问题,但我认为我并没有完全理解您的整个答案。您有什么想法,是否可以将您的方法应用于我在这里遇到的问题?http://stackoverflow.com/questions/14176265/limit-values-in-the-modelformset-field - finspin

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

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

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

我尝试了上面描述的curry()方法,但是我无法让它与Django 1.0配合使用,所以最终我回归到了闭包方法。

闭包方法非常简洁,唯一的奇怪之处就是类定义嵌套在视图或另一个函数中。我认为这看起来很奇怪是因为我以前的编程经验,而有更多动态语言背景的人可能不会感到惊讶!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接