Opened 3 months ago

Closed 3 months ago

Last modified 2 months ago

#33714 closed New feature (duplicate)

Better admin support for ArrayFields where the base_field has choices

Reported by: Jaap Roes Owned by: nobody
Component: contrib.admin Version: dev
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Currently the admin doesn't really work well when registering a model that has a ArrayField of which the base_field has choices e.g.:

class Post(model.Model):
    category = ArrayField(
        models.CharField(choices=[
            ('FASHION', 'Fashion'),
            ('STYLE', 'Style'),
            ('SPORTS', 'Sports'),
            ('FUN', 'Fun'),
        ], max_length=8),
        size=2,
        blank=True,
        default=list
    )

For example, the edit form uses a simple text input to edit the choices instead of a MultipleChoiceField. Adding the field to list_display will show the raw values, not the display names. Adding the field to list_filter will only show filter options of combinations used in the database (stringified arrays).

Change History (4)

comment:1 Changed 3 months ago by Jaap Roes

We have some workarounds in place to make it work in our projects.

Instead of ArrayField we use:

from django import forms
from django.contrib.postgres.fields import ArrayField


class ChoiceArrayField(ArrayField):
    def formfield(self, **kwargs):
        defaults = {
            "form_class": forms.MultipleChoiceField,
            "choices": self.base_field.choices,
            **kwargs,
        }

        # Bypass the ArrayField's formfield, because we don't want it to pass the unexpected
        # base_field to our selected form_class.
        return super(ArrayField, self).formfield(**defaults)

For list filters we use:

class ChoiceArrayFieldListFilter(admin.SimpleListFilter):
    field_name = NotImplemented  # Set by subclasses (ChoiceArrayFieldListFilter.for_field)

    def __init__(self, request, params, model, model_admin):
        field = model._meta.get_field(self.field_name)
        self.parameter_name = field.name
        self.title = field.verbose_name
        self._choices = field.base_field.choices
        super().__init__(request, params, model, model_admin)

    def lookups(self, request, model_admin):
        return self._choices

    def queryset(self, request, queryset):
        if value := self.value():
            queryset = queryset.filter(**{
                f'{self.field_name}__contains': [value]
            })
        return queryset

    @classmethod
    def for_field(cls, field_name):
        return type(f'{field_name.title()}ListFilter', (ChoiceArrayFieldListFilter,), {
            'field_name': field_name
        })

and for list display purposes we define methods on the admin class that call this little helper function:

def _get_array_field_display(obj, field_name):
    field = obj._meta.get_field(field_name)
    choice_lookup = dict(field.base_field.flatchoices)
    return "; ".join(str(choice_lookup.get(value)) for value in getattr(obj, field_name))

It would be nice if we can get rid of these workarounds and have this working in Django Admin without any extra code.

comment:2 Changed 3 months ago by Mariusz Felisiak

Resolution: duplicate
Status: newclosed

I think we can treat this as a duplicate of #32328.

comment:3 Changed 2 months ago by Jaap Roes

I don't agree that this is a duplicate. This is about a much narrower case where an ArrayField is used to wrap another field with choices. Basically using the ArrayField as a M2M where the "other" side is a fixed set of options, instead of a database table. The most similar ticket I can find is #24858, which has been resolved but for a much more esoteric case (imho) where the ArrayField itself has choices.

comment:4 in reply to:  3 Changed 2 months ago by Mariusz Felisiak

Replying to Jaap Roes:

I don't agree that this is a duplicate. This is about a much narrower case where an ArrayField is used to wrap another field with choices. Basically using the ArrayField as a M2M where the "other" side is a fixed set of options, instead of a database table. The most similar ticket I can find is #24858, which has been resolved but for a much more esoteric case (imho) where the ArrayField itself has choices.

My understanding is that we could have a better widget. I don't think it's worth improving only this rather niche case.

Note: See TracTickets for help on using tickets.
Back to Top