在 ModelForms 中继承 formfield_callback 的 Django 问题

Posted

技术标签:

【中文标题】在 ModelForms 中继承 formfield_callback 的 Django 问题【英文标题】:Django Problem inheriting formfield_callback in ModelForms 【发布时间】:2011-11-12 15:55:25 【问题描述】:

我现在才使用 Django 几个星期,所以我可能会遇到各种各样的错误,但是:

我有一个基本的 ModelForm,我在其中放入了一些样板文件,以使事情尽可能干燥,而我所有的实际 ModelForms 只是该基本表单的子类。这对error_css_class = 'error'required_css_class = 'required' 非常有用,但formfield_callback = add_css_classes 并没有像我期望的那样工作。

forms.py

# snippet I found
def add_css_classes(f, **kwargs):
    field = f.formfield(**kwargs)
    if field and 'class' not in field.widget.attrs:
        field.widget.attrs['class'] = '%s' % field.__class__.__name__.lower()
    return field

class BaseForm(forms.ModelForm):
    formfield_callback = add_css_classes  # not working

    error_css_class = 'error'
    required_css_class = 'required'
    class Meta:
        pass

class TimeLogForm(BaseForm):
    # I want the next line to be in the parent class
    # formfield_callback = add_css_classes
    class Meta(BaseForm.Meta):
        model = TimeLog

最终目标是在具有日期字段/时间字段/日期时间字段类的表单上添加一些 jquery 日期时间选择器。我希望应用程序中的所有日期时间字段都使用相同的小部件,因此我选择这样做,而不是为每个模型中的每个字段明确地这样做。在每个表单类中添加一个额外的行并不是什么大不了的事,但它只是让我烦恼,我无法弄清楚。在 django 源代码中挖掘表明这可能是在做一些我不理解的事情:

django.forms.models

class ModelFormMetaclass(type):
    def __new__(cls, name, bases, attrs):
        formfield_callback = attrs.pop('formfield_callback', None)

但我不知道__init____new__ 是如何交织在一起的。在 BaseForm 中,我尝试覆盖 __init__ 并在调用 super 之前和之后设置 formfield_callback,但我猜它需要位于 args 或 kwargs 中的某个位置。

【问题讨论】:

它不起作用,因为 formfield_callback 不是对象的字段。这只是一个变量。 那么有没有办法在运行时设置呢?还是formfield_callback = attrs.pop('formfield_callback', None) 杀死了ModelFormMetaclass 中的任何机会。 【参考方案1】:

__new__ 在对象构造之前调用。其实这是一个factory方法,返回一个新建对象的实例。

所以在ModelFormMetaclass中有3个关键行:

formfield_callback = attrs.pop('formfield_callback', None) #1
fields = fields_for_model(opts.model, opts.fields, 
                                      opts.exclude, opts.widgets, formfield_callback) #2
new_class.base_fields = fields #3

在类中,我们将 base_fields 附加到我们的 form

现在让我们看看 ModelForm 类

class ModelForm(BaseModelForm):
    __metaclass__ = ModelFormMetaclass

这意味着当我们创建一个ModelForm实例来改变未来实例的结构时,会调用ModelFormMetaclass.__new__(...)。并且 ModelFormMetaclass 中的 __new__ (def __new__(cls, name, bases, attrs)) 的属性是 所有属性 的字典ModelForm 类

因此决定为我们的案例创建新的 InheritedFormMetaclass(从 ModelFormMetaclass 继承它)。不要忘记在 InheritedFormMetaclass 中调用父级的 new。然后创建我们的 BaseForm 类 并说:

__metaclass__ = InheritedFormMetaclass

InheritedFormMetaclass 的 __new__(...) 实现中,我们可以为所欲为。

如果我的回答不够详细,请在 cmets 的帮助下告诉我。

【讨论】:

+1 表示整洁的东西,-1 表示我认为矫枉过正的东西。假设我理解这个问题,这可以通过覆盖 BaseForm.__init__ 来解决。我的答案中的详细信息。【参考方案2】:

您可以像这样设置小部件类:

class TimeLogForm(BaseForm):
# I want the next line to be in the parent class
# formfield_callback = add_css_classes
class Meta(BaseForm.Meta):
    model = TimeLog
    widgets = 
            'some_fields' : SomeWidgets(attrs='class' : 'myclass')
    

【讨论】:

我的想法是我不想在所有表单中重复编写相同的 Meta 类,而是只需正确一次,一切都会继承它。我们在使用 php 时受到了猛烈抨击,所以我还没有机会深入研究它,但我认为 s-zakharov 走在了正确的轨道上。【参考方案3】:

对于您要完成的工作,我认为您最好只遍历表单 init 上的字段。例如,

class BaseForm(forms.ModelForm):
  def __init__(self, *args, **kwargs):
    super(BaseForm, self).__init__(*args, **kwargs)
    for name, field in self.fields.items():
      field.widget.attrs['class'] = 'error'

显然,您需要针对您的具体案例进行更多逻辑处理。如果您想使用 sergzach 建议的方法(我认为对于您的特定问题过度杀伤),这里有一些代码可以在子类未定义的情况下在基类上调用 formfield_callback。

baseform_formfield_callback(field):
    # do some stuff
    return field.formfield()

class BaseModelFormMetaclass(forms.models.ModelFormMetaclass):
    def __new__(cls, name, bases, attrs):
        if not attrs.has_key('formfield_callback'):
            attrs['formfield_callback'] = baseform_formfield_callback
        new_class = super(BaseModelFormMetaclass, cls).__new__(
            cls, name, bases, attrs)
        return new_class

class BaseModelForm(forms.ModelForm):
    __metaclass__ = OrganizationModelFormMetaclass
    # other form stuff

最后,你可能想看看脆的形式:https://github.com/maraujop/django-crispy-forms

【讨论】:

【参考方案4】:

sergzach 是正确的,您必须使用元类;覆盖 __init__ 是不够的。原因是 ModelForm 的元类(将为所有 ModelForm 子类调用,除非您在子类中指定另一个元类)采用类定义,并使用 类定义 中的值创建一个类类属性。例如,META.fields 和我们的 formfield_callback 都用于创建具有各种选项(例如哪个小部件)的表单字段。

这意味着 AFAIU formfield_callback 是创建自定义模型表单类时使用的元类的一个参数,而不是在创建实际表单实例时在运行时使用的某个值。这使得将 formfield_callback 放在 __init__ 中毫无用处。

我用自定义元类解决了一个类似的问题,例如

from django.forms.models import ModelFormMetaclass

class MyModelFormMetaclass(ModelFormMetaclass):
    def __new__(cls,name,bases,attrs):
    attrs['formfield_callback']=my_callback_function
    return super(MyModelFormMetaclass,cls).__new__(cls,name,bases,attrs)

在我所有模型表单的基类中设置元类

class MyBaseModelForm(ModelForm):
    __metaclass__=MyModelFormMetaclass
    ...

可以像这样使用(至少在 Django 1.6 中)

class MyConcreteModelForm(MyBaseModelForm):
    # no need setting formfield_callback here
    ...

【讨论】:

以上是关于在 ModelForms 中继承 formfield_callback 的 Django 问题的主要内容,如果未能解决你的问题,请参考以下文章

在python中继承私有变量[重复]

私有成员是不是在 C# 中继承?

在 Python 中继承装饰函数?

如何在 Swift 中继承 UITableViewController

在mongodb中继承一个集合的属性[重复]

在 Mixin 中继承父类属性