【问题标题】:Better ArrayField admin widget?更好的 ArrayField 管理小部件?
【发布时间】:2015-10-04 05:37:39
【问题描述】:

有没有办法让 ArrayField 的管理小部件允许添加和删除对象?似乎默认情况下,它只显示一个文本字段,并使用逗号分隔其值。

除了不方便之外,如果数组的基本字段是 Char/TextField,则 AFAICT 不允许在数组中的任何文本中包含逗号。

【问题讨论】:

    标签: django django-admin


    【解决方案1】:

    我对此不以为然 (original source),但是如果您使用 PostgreSQL 作为数据库并且乐于使用 Postgres 特定的 ArrayField 实现,那么还有一个更简单的选择:模型上的子类 ArrayField 和覆盖默认的管理小部件。下面是一个基本实现(在 Django 1.9、1.10、1.11、2.0、2.1 和 2.2 中测试):

    models.py

    from django import forms
    from django.db import models
    from django.contrib.postgres.fields import ArrayField
    
    
    class ChoiceArrayField(ArrayField):
        """
        A field that allows us to store an array of choices.
        Uses Django's Postgres ArrayField
        and a MultipleChoiceField for its formfield.
        """
    
        def formfield(self, **kwargs):
            defaults = {
                'form_class': forms.MultipleChoiceField,
                'choices': self.base_field.choices,
            }
            defaults.update(kwargs)
            # Skip our parent's formfield implementation completely as we don't
            # care for it.
            # pylint:disable=bad-super-call
            return super(ArrayField, self).formfield(**defaults)
    
    
    FUNCTION_CHOICES = (
        ('0', 'Planning'),
        ('1', 'Operation'),
        ('2', 'Reporting'),
    )
    
    
    class FunctionModel(models.Model):
        name = models.CharField(max_length=128, unique=True)
        function = ChoiceArrayField(
            base_field=models.CharField(max_length=256, choices=FUNCTION_CHOICES),
            default=list)
    

    【讨论】:

    • 我错过了什么吗?您实际上在哪里覆盖了默认的管理小部件?您从博客中引用的片段将相当有限的 ArrayField 变成了有用的东西并且效果很好。但是,很高兴看到您找到的哪个管理小部件充分利用了这一点。我可以试试 Select2。
    • 我已将示例扩展为更完整。
    • 解决方案仅适用于 CharField。对于其他类型,请查看gist.github.com/danni/f55c4ce19598b2b345ef#gistcomment-2058176
    【解决方案2】:

    【讨论】:

      【解决方案3】:

      Django 更好的管理 ArrayField 包正好提供了这个功能。与上述解决方案相比,优势在于它允许您动态添加新条目,而不是依赖于预定义的选择。

      在此处查看文档:django-better-admin-arrayfield

      它有一个替换 ArrayField 和一个简单的 mixin 来添加到管理模型中。

      # models.py
      from django_better_admin_arrayfield.models.fields import ArrayField
      
      class MyModel(models.Model):
          my_array_field = ArrayField(models.IntegerField(), null=True, blank=True)
      
      
      # admin.py
      from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
      
      @admin.register(MyModel)
      class MyModelAdmin(admin.ModelAdmin, DynamicArrayMixin):
          ...
      

      这将显示如下内容:

      【讨论】:

      • 终于找到了真正有用的东西!一件事想到:我似乎无法使用添加另一个按钮添加新字段。我该如何解决?
      • 奇怪,你收到错误信息了吗?究竟会发生什么?
      • 感谢您的回答!我终于修好了。问题是我忘记运行 collectstatic 所以小部件的新 js 和 css 文件没有导出到我的静态文件服务器:)
      【解决方案4】:

      django-select2 提供了一种使用Select2 呈现ArrayField 的方法。在他们的文档中,该示例适用于ArrayField

      http://django-select2.readthedocs.io/en/latest/django_select2.html#django_select2.forms.Select2TagWidget

      渲染已经选择的值:

      class ArrayFieldWidget(Select2TagWidget):
      
          def render_options(self, *args, **kwargs):
              try:
                  selected_choices, = args
              except ValueError:  # Signature contained `choices` prior to Django 1.10
                  choices, selected_choices = args
              output = ['<option></option>' if not self.is_required and not self.allow_multiple_selected else '']
              selected_choices = {force_text(v) for v in selected_choices.split(',')}
              choices = {(v, v) for v in selected_choices}
              for option_value, option_label in choices:
                  output.append(self.render_option(selected_choices, option_value, option_label))
              return '\n'.join(output)
      
          def value_from_datadict(self, data, files, name):
              values = super().value_from_datadict(data, files, name)
              return ",".join(values)
      

      要将小部件添加到表单中:

      class MyForm(ModelForm):
      
          class Meta:
              fields = ['my_array_field']
              widgets = {
                  'my_array_field': ArrayFieldWidget
              }
      

      【讨论】:

        【解决方案5】:

        这是已被接受的解决方案的更好版本。使用“CheckboxSelectMultiple”使其在管理页面中更有用。

        class ChoiceArrayField(ArrayField):
        
            def formfield(self, **kwargs):
                defaults = {
                    'form_class': forms.TypedMultipleChoiceField,
                    'choices': self.base_field.choices,
                    'coerce': self.base_field.to_python,
                    'widget': forms.CheckboxSelectMultiple,
                }
                defaults.update(kwargs)
        
                return super(ArrayField, self).formfield(**defaults)
        

        【讨论】:

        • 简洁优雅。
        【解决方案6】:

        这是另一个使用 Django Admin M2M filter_horizontal 小部件的版本,而不是标准的 HTML 多选。

        我们仅在 Admin 站点中使用 Django 表单,这对我们有用,但如果在 Admin 之外使用管理小部件 FilteredSelectMultiple 可能会损坏。另一种方法是覆盖ModelAdmin.get_form 来为数组字段实例化正确的表单类和小部件。 ModelAdmin.formfields_overrides 是不够的,因为您需要实例化设置位置参数的小部件,如代码 sn-p 所示。

        from django.contrib.admin.widgets import FilteredSelectMultiple
        from django.contrib.postgres.fields import ArrayField
        from django.forms import MultipleChoiceField
        
        
        class ChoiceArrayField(ArrayField):
            """
            A choices ArrayField that uses the `horizontal_filter` style of an M2M in the Admin
        
            Usage::
        
                class MyModel(models.Model):
                    tags = ChoiceArrayField(
                        models.TextField(choices=TAG_CHOICES),
                        verbose_name="Tags",
                        help_text="Some tags help",
                        blank=True,
                        default=list,
                    )
            """
        
            def formfield(self, **kwargs):
                widget = FilteredSelectMultiple(self.verbose_name, False)
                defaults = {
                    "form_class": MultipleChoiceField,
                    "widget": widget,
                    "choices": self.base_field.choices,
                }
                defaults.update(kwargs)
                # Skip our parent's formfield implementation completely as we don't
                # care for it.
                return super(ArrayField, self).formfield(**defaults)
        

        【讨论】:

        • 在 python 3 中,您可以将 super(ArrayField, self) 简化为 super()
        • 嗨@bartaelterman,我最初也是这么想的,但是代码不起作用。在这种情况下,super() 等价于super(ChoiceArrayField, self),但使用super(ArrayField, self) 我们有意跳过继承链中的ArrayField.formfield 实现。这段代码是从之前对该问题的回复中复制而来的。
        【解决方案7】:

        为您的模型编写一个表单类并将 forms.MultipleChoiceField 用于 ArrayField:

        class ModelForm(forms.ModelForm):
        
            my_array_field = forms.MultipleChoiceField(
                choices=[1, 2, 3]
            )
        
            class Meta:
                exclude = ()
                model = Model
        

        在您的管理类中使用 ModelForm:

        class ModelAdmin(admin.ModelAdmin):
            form = ModelForm
            exclude = ()
            fields = (
                'my_array_field',
            )
        

        【讨论】: