Better ArrayField admin widget?
Asked Answered
D

7

45

Is there any way to make ArrayField's admin widget allow adding and deleting objects? It seems that by default, it is instead displayed just a text field, and uses comma separation for its values.

Besides being inconvenient, AFAICT in the case the base field of the array is a Char/TextField, this doesn't allow any way of including commas in any of the texts in the array.

Dim answered 15/7, 2015 at 9:5 Comment(0)
P
16

For OP, or anyone out there looking, between these helpful bits you should be good to go:

Pyrophyllite answered 2/2, 2016 at 3:45 Comment(0)
D
43

I take no credit for this (original source), but if you are using PostgreSQL as the database and are happy to use the Postgres-specific ArrayField implementation there is an even easier option: subclass ArrayField on the model and override the default admin widget. A basic implementation follows (tested in 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)
Disserve answered 3/10, 2016 at 14:6 Comment(4)
Am I missing something? Where are you actually overriding the default admin widget? The snipped you quote from the weblog turns the rather limited ArrayField into something useful and works very well. However, it would have been nice to see what admin widget you found makes the best use of this. I might give Select2 a go.Erny
I have expanded the example to be more complete.Disserve
Solution works only for CharField. For other types check this gist.github.com/danni/f55c4ce19598b2b345ef#gistcomment-2058176Decaliter
The error for "TypeError: __init__() got an unexpected keyword argument 'base_field'" was fixed by checking the github link above. Here's the comment that helped gist.github.com/danni/…Iyeyasu
P
16

For OP, or anyone out there looking, between these helpful bits you should be good to go:

Pyrophyllite answered 2/2, 2016 at 3:45 Comment(0)
G
12

This is a better version of an already accepted solution. Using "CheckboxSelectMultiple" makes it more usable in the admin page.

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)
Gastight answered 5/2, 2021 at 8:2 Comment(0)
J
11

The Django better admin ArrayField package provides exactly this functionality. The advantage over the solutions above is that it allows you to add new entries dynamically instead of relying on pre-defined choices.

See the documentation here: django-better-admin-arrayfield

It has a drop-in replacement for the ArrayField and a simple mixin to add to the admin model.

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

This would show something like:

enter image description here

Jackijackie answered 1/3, 2021 at 14:3 Comment(3)
finally something that actually works! One thing thought: I can't seem to be able to add new fields with the add another button. How can I fix that?Whelm
Strange, do you get an error message? What happens exactly?Jackijackie
Thanks for answering! I finally fixed it. The problem was I forgot to run collectstatic so the new js and css files for the widget weren't exporiting to my staticfiles server :)Whelm
M
4

This is another version using the Django Admin M2M filter_horizontal widget, instead of the standard HTML select multiple.

enter image description here

We use Django forms only in the Admin site, and this works for us, but the admin widget FilteredSelectMultiple probably will break if used outside the Admin. An alternative would be overriding the ModelAdmin.get_form to instantiate the proper form class and widget for the array field. The ModelAdmin.formfields_overrides is not enough because you need to instantiate the widget setting the positional arguments as shown in the code snippet.

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)
Maller answered 23/12, 2020 at 11:36 Comment(2)
In python 3, you can simplify super(ArrayField, self) to just super()Afterdeck
Hi @bartaelterman, I initially thought the same, but the code didn't work. In this case, the super() is equivalent to super(ChoiceArrayField, self) but using super(ArrayField, self) we are intentionally skipping the ArrayField.formfield implementation in the inheritance chain. This code was just copied from previous responses to this question.Maller
B
3

django-select2 offers a way to render the ArrayField using Select2. In their documentation, the example is for ArrayField:

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

To render the already selected values:

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)

To add the widget to your form:

class MyForm(ModelForm):

    class Meta:
        fields = ['my_array_field']
        widgets = {
            'my_array_field': ArrayFieldWidget
        }
Bobbiebobbin answered 5/9, 2016 at 8:25 Comment(0)
L
0

write a form class for your model and use forms.MultipleChoiceField for ArrayField:

class ModelForm(forms.ModelForm):

    my_array_field = forms.MultipleChoiceField(
        choices=[1, 2, 3]
    )

    class Meta:
        exclude = ()
        model = Model

use ModelForm in your admin class:

class ModelAdmin(admin.ModelAdmin):
    form = ModelForm
    exclude = ()
    fields = (
        'my_array_field',
    )
Lepido answered 1/2, 2021 at 8:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.