Django Admin 中的动态字段

Posted

技术标签:

【中文标题】Django Admin 中的动态字段【英文标题】:Dynamic fields in Django Admin 【发布时间】:2011-12-21 20:49:52 【问题描述】:

我想要关于一个字段的值的附加字段。因此,我构建了一个自定义管理表单来添加一些新字段。

与 jacobian 1 的博文相关,这是我想出的:

class ProductAdminForm(forms.ModelForm):
    class Meta:
        model = Product

    def __init__(self, *args, **kwargs):
        super(ProductAdminForm, self).__init__(*args, **kwargs)
        self.fields['foo'] = forms.IntegerField(label="foo")

class ProductAdmin(admin.ModelAdmin):
    form = ProductAdminForm

admin.site.register(Product, ProductAdmin)

但附加字段“foo”没有显示在管理员中。如果我像这样添加字段,一切正常,但不像所需的那样动态,添加有关模型另一个字段的值的字段

class ProductAdminForm(forms.ModelForm):

    foo = forms.IntegerField(label="foo")

    class Meta:
        model = Product

class ProductAdmin(admin.ModelAdmin):
    form = ProductAdminForm

admin.site.register(Product, ProductAdmin)

那么有没有什么我必须再次触发才能使新字段工作的初始化方法?还是有其他尝试?

【问题讨论】:

【参考方案1】:

这是解决问题的方法。感谢 koniiiik 我试图通过扩展 *get_fieldsets* 方法来解决这个问题

class ProductAdmin(admin.ModelAdmin):
    def get_fieldsets(self, request, obj=None):
        fieldsets = super(ProductAdmin, self).get_fieldsets(request, obj)
        fieldsets[0][1]['fields'] += ['foo'] 
        return fieldsets

如果您使用多个字段集,请务必使用适当的索引将其添加到正确的字段集。

【讨论】:

Unknown field(s) (foo) specified for GlobalLabel. Check fields/fieldsets/exclude attributes of class GlobalLabelAdmin. 我收到了这个错误,我不知道为什么...你能帮帮我吗? @bhushya:你能解决这个问题吗?我也无法让它在 django 1.9.3 中工作,例如:django.core.exceptions.FieldError: Unknown field(s) (dynamicfield1, dynamicfield2) specified for MyModel @tehfink 看来你没有在你的模型中定义字段..你能在 pastebin.com 上发布你的模型结构并分享链接吗? @bhushya:你是对的;我的模型上没有定义字段(dynamicfield1 等)。就像在原始问题中一样,我想在ModelForm 中动态添加字段,而上面提到的get_fieldsets 覆盖似乎在Django 1.9.3 中不起作用 @bhushya:我找到了 Django 1.9.3 的潜在解决方案,发布在下面【参考方案2】:

上面接受的答案在旧版本的 django 中有效,我就是这样做的。这现在已经在以后的 django 版本中被打破了(我现在是 1.68,但现在即使是旧的)。

它现在被破坏的原因是因为您从ModelAdmin.get_fieldsets() 返回的字段集中的任何字段最终都作为fields=parameter 传递给modelform_factory(),这会给您一个错误,因为您列表中的字段不存在(并且在您的表单被实例化并调用其__init__ 之前不会存在)。

为了解决这个问题,我们必须覆盖 ModelAdmin.get_form() 并提供一个字段列表,其中不包含稍后将添加的任何额外字段。 get_form 的默认行为是调用 get_fieldsets() 获取此信息,我们必须防止这种情况发生:

# CHOOSE ONE
# newer versions of django use this
from django.contrib.admin.utils import flatten_fieldsets
# if above does not work, use this
from django.contrib.admin.util import flatten_fieldsets

class MyModelForm(ModelForm):
  def __init__(self, *args, **kwargs):
      super(MyModelForm, self).__init__(*args, **kwargs)
      # add your dynamic fields here..
      for fieldname in ('foo', 'bar', 'baz',):
          self.fields[fieldname] = form.CharField()

class MyAdmin(ModelAdmin): 
   form = MyModelForm

    fieldsets = [
       # here you put the list of fieldsets you want displayed.. only
       # including the ones that are not dynamic
    ]

    def get_form(self, request, obj=None, **kwargs):
        # By passing 'fields', we prevent ModelAdmin.get_form from
        # looking up the fields itself by calling self.get_fieldsets()
        # If you do not do this you will get an error from 
        # modelform_factory complaining about non-existent fields.

        # use this line only for django before 1.9 (but after 1.5??)
        kwargs['fields'] =  flatten_fieldsets(self.declared_fieldsets)
        # use this line only for django 1.9 and later 
        kwargs['fields'] =  flatten_fieldsets(self.fieldsets)

        return super(MyAdmin, self).get_form(request, obj, **kwargs)

    def get_fieldsets(self, request, obj=None):
        fieldsets = super(MyAdmin, self).get_fieldsets(request, obj)

        newfieldsets = list(fieldsets)
        fields = ['foo', 'bar', 'baz']
        newfieldsets.append(['Dynamic Fields',  'fields': fields ])

        return newfieldsets

【讨论】:

不幸的是,Django 1.9 中的 ModelAdmin.declared_fieldsets has been removed 嗯.. 好吧,我想当我将服务器升级到 1.9 时,我会有一些工作要做;)但幸运的是,我已经在我的应用程序的其他地方复制了大部分管理功能.. . 另外django.contrib.admin.util 现在是django.contrib.admin.utils 谢谢,否则我的回答是否仍然有效?如果是这样,我会纠正它。 嘿,django 2.5 怎么样。 kwargs['fields'] = flatten_fieldsets(self.fieldsets) flatten_fieldsets for name, opts in fieldsets: TypeError: 'NoneType' object is not iterable 我有一些问题【参考方案3】:

这适用于在 Django 1.9.3 中添加动态字段,仅使用 ModelAdmin 类(无 ModelForm)并覆盖 get_fields。我还不知道它有多强大:

class MyModelAdmin(admin.ModelAdmin):

    fields = [('title','status', ), 'description', 'contact_person',]
    exclude = ['material']

    def get_fields(self, request, obj=None):
        gf = super(MyModelAdmin, self).get_fields(request, obj)

        new_dynamic_fields = [
            ('test1', forms.CharField()),
            ('test2', forms.ModelMultipleChoiceField(MyModel.objects.all(), widget=forms.CheckboxSelectMultiple)),
        ]

        #without updating get_fields, the admin form will display w/o any new fields
        #without updating base_fields or declared_fields, django will throw an error: django.core.exceptions.FieldError: Unknown field(s) (test) specified for MyModel. Check fields/fieldsets/exclude attributes of class MyModelAdmin.

        for f in new_dynamic_fields:
            #`gf.append(f[0])` results in multiple instances of the new fields
            gf = gf + [f[0]]
            #updating base_fields seems to have the same effect
            self.form.declared_fields.update(f[0]:f[1])
        return gf

【讨论】:

【参考方案4】:

虽然 Jacob 的帖子可能适用于普通的 ModelForms(即使它已经超过一年半了),但管理员是另一回事。

所有定义模型、表单 ModelAdmins 和诸如此类的声明方式都大量使用元类和类自省。与管理员相同 - 当您告诉 ModelAdmin 使用特定表单而不是创建默认表单时,它会内省 。它从类本身获取字段列表和其他内容,而不实例化它。

然而,您的自定义类没有在类级别定义额外的表单字段,而是在实例化之后动态添加一个 - 这对于ModelAdmin 来说已经太迟了,无法识别这种变化.

解决问题的一种方法可能是将ModelAdmin 子类化并覆盖其get_fieldsets 方法以实际实例化ModelForm 类并从实例而不是类中获取字段列表。不过,您必须记住,这可能比默认实现要慢一些。

【讨论】:

【参考方案5】:

您可以使用表单元类创建动态字段和字段集。示例代码如下。根据您的要求添加循环逻辑。

class CustomAdminFormMetaClass(ModelFormMetaclass):
    """
    Metaclass for custom admin form with dynamic field
    """
    def __new__(cls, name, bases, attrs):
        for field in get_dynamic_fields: #add logic to get the fields
            attrs[field] = forms.CharField(max_length=30) #add logic to the form field
        return super(CustomAdminFormMetaClass, cls).__new__(cls, name, bases, attrs)


class CustomAdminForm(six.with_metaclass(CustomAdminFormMetaClass, forms.ModelForm)):
    """
    Custom admin form
    """

    class Meta:
        model = ModelName
        fields = "__all__" 


class CustomAdmin(admin.ModelAdmin):
    """
    Custom admin 
    """

    fieldsets = None
    form = CustomAdminForm

    def get_fieldsets(self, request, obj=None):
        """
        Different fieldset for the admin form
        """
        self.fieldsets = self.dynamic_fieldset(). #add logic to add the dynamic fieldset with fields
        return super(CustomAdmin, self).get_fieldsets(request, obj)

    def dynamic_fieldset(self):
        """
        get the dynamic field sets
        """
        fieldsets = []
        for group in get_field_set_groups: #logic to get the field set group
            fields = []
            for field in get_group_fields: #logic to get the group fields
                fields.append(field)

            fieldset_values = "fields": tuple(fields), "classes": ['collapse']
            fieldsets.append((group, fieldset_values))

        fieldsets = tuple(fieldsets)

        return fieldsets

【讨论】:

【参考方案6】:

也许我有点晚了...但是,我使用的是 Django 3.0,并且还想根据请求动态地将一些自定义字段添加到表单中。

我最终得到了一个类似于@tehfink 结合@little_birdie 描述的解决方案。

但是,仅按照建议更新 self.form.declared_fields 并没有帮助。此过程的结果是,self.form.declared_fields 中定义的自定义字段列表总是随着请求而增长。

我首先通过初始化这个字典来解决这个问题:

class ModelAdminGetCustomFieldsMixin(object):
    def get_fields(self, request, obj=None):
        fields = super().get_fields(request, obj=None)
        self.form.declared_fields = 
        if obj:
            for custom_attribute in custom_attribute_list:
                self.form.declared_fields.update(custom_attribute.name: custom_attribute.field)
        return fields

其中custom_attribute.field 是一个表单字段实例。

此外,还需要定义一个 ModelForm,其中在初始化期间,自定义字段也已动态添加:

class SomeModelForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for custom_attribute in custom_attribute_list:
            self.fields[custom_attribute.name] = custom_attribute.field

并在 ModelAdmin 中使用此 ModelForm。

之后,新定义的属性可以用于例如字段集。

【讨论】:

嘿,感谢 Django 3.0 的更新,我是框架的新手。你是如何使用 SomeModelForm 实现第一类 ModelAdminGetCustomFieldsMixin 的。我希望根据下拉菜单中的选择在 Django admin 中显示此动态表单,该表单在将使用此动态表单的同一模型中选择 嗨弗朗西斯科,反之亦然:SomeModelFormModelAdmin 中用作formModelAdminGetCustomFieldsMixin 是一个mixin,它需要作为一个附加类来继承来自例如SomeModelAdmin(ModelAdminGetCustomFieldsMixin, ModelAdmin)。页面本身是静态的。表格不会动态变化。您需要重新加载页面以更改表单或一堆 js。【参考方案7】:

Stephan 的回答很优雅,但是当我在 dj1.6 中使用时,它要求该字段是一个元组。 完整的解决方案如下所示:

class ProductForm(ModelForm):
    foo = CharField(label='foo')


class ProductAdmin(admin.ModelAdmin):
    form = ProductForm
    def get_fieldsets(self, request, obj=None):
        fieldsets = super(ProductAdmin, self).get_fieldsets(request, obj)
        fieldsets[0][1]['fields'] += ('foo', ) 
        return fieldsets

【讨论】:

【参考方案8】:

不确定为什么这不起作用,但可能的解决方法是静态定义字段(在表单上),然后在 __init__ 中覆盖它?

【讨论】:

【参考方案9】:

我很长时间都无法解决动态添加字段的问题。 解决方案“little_birdie”确实有效。谢谢小鸟)) 唯一的细微差别是: “Self.declared_fieldsets”应替换为“self.fieldsets”。

#kwargs['fields'] =  flatten_fieldsets(self.declared_fieldsets)
kwargs['fields'] =  flatten_fieldsets(self.fieldsets)

我使用的是 1.10 版。也许有些事情发生了变化。

如果有人找到更简单、更优雅的解决方案,请在此处展示。

谢谢大家)))

【讨论】:

以上是关于Django Admin 中的动态字段的主要内容,如果未能解决你的问题,请参考以下文章

Django Admin中的动态只读字段

Django - Admin 中的 ForeignKey 字段初始值定义

模型形式中的 django auto slug,例如 django admin 中的预填充字段

如何隐藏 django-admin 中的某些字段?

django admin中的表单字段描述

Django Admin 根据其他选择动态禁用字段