Skip to content

Instantly share code, notes, and snippets.

@danni
Created March 8, 2016 08:52
Show Gist options
  • Select an option

  • Save danni/f55c4ce19598b2b345ef to your computer and use it in GitHub Desktop.

Select an option

Save danni/f55c4ce19598b2b345ef to your computer and use it in GitHub Desktop.
Multi Choice Django Array Field
from django import forms
from django.contrib.postgres.fields import ArrayField
class ChoiceArrayField(ArrayField):
"""
A field that allows us to store an array of choices.
Uses Django 1.9's postgres ArrayField
and a MultipleChoiceField for its formfield.
Usage:
choices = ChoiceArrayField(models.CharField(max_length=...,
choices=(...,)),
default=[...])
"""
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)
@niccolomineo

niccolomineo commented Sep 9, 2021 •

Copy link
Copy Markdown

Yep, I am using it as an ArrayField replacement. I am on Django 3.2.

@DanielSwain

Copy link
Copy Markdown

@pcraciunoiu Outstanding solution. Works on Django 3.2/Python 3.8.

@peterchibunna

Copy link
Copy Markdown

Why doesn't this work in Django 4? I keep getting AttributeError: 'ChoiceArrayField' object has no attribute 'get_bound_field'

@bo5o

bo5o commented Aug 7, 2022

Copy link
Copy Markdown

@niccolomineo I got the same error on Django 3.2 and Python 3.9. In my model definition I had

my_field = ChoiceArrayField(
    base_field=models.CharField(choices=my_choices, max_length=3),
    default=list,
    blank=True,
)

I got errors like you for base_field and also max_length (?).

Without diving too deep into the issue, I did a dirty fix by overriding forms.MultipleChoiceField.__init__ like so

class _MultipleChoiceField(forms.MultipleChoiceField):
    def __init__(self, *args, **kwargs):
        kwargs.pop("base_field", None)
        kwargs.pop("max_length", None)
        super().__init__(*args, **kwargs)

and use that as form_class in the implementation from @pcraciunoiu.

It solved the issue for me.

@willywongi

Copy link
Copy Markdown

Building on all the previous answers, this is my setup working with Django 4 (4.0.3).

Form field, model field, choices and model (models.py)

from django.contrib.postgres.fields import ArrayField
from django.db.models import Model
from django.db.models.enums import TextChoices
from django.db.models.fields import CharField
from django.forms.fields import MultipleChoiceField


# Contribution by @cbows 
class _MultipleChoiceField(MultipleChoiceField):
    def __init__(self, *args, **kwargs):
        kwargs.pop("base_field", None)
        kwargs.pop("max_length", None)
        super().__init__(*args, **kwargs)


# Original contribution by @danni 
# slightly rewrited to match Django writing code style
class ChoiceArrayField(ArrayField):
    def formfield(self, **kwargs):
        return super().formfield(**{"form_class": _MultipleChoiceField,
                                    "choices": self.base_field.choices,
                                    **kwargs})

class MyOption(TextChoices):
    OPTION1 = 'OPTION1', "Option 1"
    OPTION2 = 'OPTION2', "Option 2"
    OPTION3 = 'OPTION3', "Option 3"


class MyModel(Model):
    # All your fields...
    options = ChoiceArrayField(CharField(max_length=24, choices=MyOption.choices), default=list)

Django admin configuration (admin.py)

from django.contrib.admin import register
from django.contrib.admin.options import ModelAdmin
from django.forms.widgets import CheckboxSelectMultiple

from yourdjangoapplication.admin import site
from .models import MyModel, ChoiceArrayField


@register(MyModel, site=site)
class MyModelAdmin(ModelAdmin):
    fields = (
        # all your fields...
        "options", )
    # The default widget is a <select multiple>;
    # use this configuration for a group of checkboxes.
    formfield_overrides = {
        ChoiceArrayField: {'widget': CheckboxSelectMultiple}
    }

@anyidea

anyidea commented Aug 10, 2023 •

Copy link
Copy Markdown

After combining all the previous answers, below works for me, Django 4.2, Python 3.11

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


class _TypedMultipleChoiceField(forms.TypedMultipleChoiceField):
    def __init__(self, *args, **kwargs):
        kwargs.pop("base_field", None)
        kwargs.pop("max_length", None)
        super().__init__(*args, **kwargs)


class ChoiceArrayField(ArrayField):
    """
    A field that allows us to store an array of choices.

    Uses Django 4.2's postgres ArrayField
    and a TypeMultipleChoiceField for its formfield.

    Usage:

        choices = ChoiceArrayField(
            models.CharField(max_length=..., choices=(...,)), blank=[...], default=[...]
        )
    """
    def formfield(self, **kwargs):
        defaults = {
            'form_class': _TypedMultipleChoiceField,
            'choices': self.base_field.choices,
            'coerce': self.base_field.to_python,
        }
        defaults.update(kwargs)
        # Skip our parent's formfield implementation completely as we don't care for it.
        # pylint:disable=bad-super-call
        return super().formfield(**defaults)

@andreasnuesslein

Copy link
Copy Markdown

Thanks everybody. using @anyidea 's solution. works nicely

@sam-ghosh

Copy link
Copy Markdown

Thanks this solution works very well

@arnaud-penet

Copy link
Copy Markdown

Thanks you 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment