Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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)
@ropable

This comment has been minimized.

Copy link

@ropable ropable commented Oct 3, 2016

Excellent snippet, thanks!

@Marakai

This comment has been minimized.

Copy link

@Marakai Marakai commented Nov 7, 2016

Bless you! after slamming my head against a wall for nearly an entire day, your snippet gave me a breakthrough! Now, if you could add an example of using it with a field widget you found useful, this here backend guy thrown into doing frontend code would be eternally grateful!

@aniruddha-adhikary

This comment has been minimized.

Copy link

@aniruddha-adhikary aniruddha-adhikary commented Nov 9, 2016

Thanks a lot @danni! You're the hero.

@Proper-Job

This comment has been minimized.

Copy link

@Proper-Job Proper-Job commented Feb 28, 2017

@danni That's kind of problematic with the base_field being anything but a CharField. Wouldn't it be better to use a TypedMultipleChoiceField thus?

def formfield(self, **kwargs):
        defaults = {
            'form_class': forms.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(ArrayField, self).formfield(**defaults)
@Proper-Job

This comment has been minimized.

Copy link

@Proper-Job Proper-Job commented Mar 30, 2017

Actually starting on Django > 1.10.2 the custom field also needs a custom widget like below. Otherwise an empty selection won't be written back to the model.

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


class ArraySelectMultiple(SelectMultiple):

    def value_omitted_from_data(self, data, files, name):
        return False


class ChoiceArrayField(ArrayField):

    def formfield(self, **kwargs):
        defaults = {
            'form_class': forms.TypedMultipleChoiceField,
            'choices': self.base_field.choices,
            'coerce': self.base_field.to_python,
            'widget': ArraySelectMultiple
        }
        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)
@kholidfu

This comment has been minimized.

Copy link

@kholidfu kholidfu commented Apr 13, 2017

Hi @danni, @Proper-Job

Using Django 1.11, this is what I have:

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


class FacilitiesArrayField(ArrayField):

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

I've succeeded entering the data (checked via ./manage.py shell), but when take a look back my django-admin, none checked..

Any ideas?

@Allan-Nava

This comment has been minimized.

Copy link

@Allan-Nava Allan-Nava commented Jan 17, 2018

Good snippet!

@vyruss

This comment has been minimized.

Copy link

@vyruss vyruss commented Apr 2, 2018

Thanks very much for this @danni & @Proper-Job.

@igerko

This comment has been minimized.

Copy link

@igerko igerko commented Dec 19, 2018

If you are using base field IntegerField or other than CharField, you will need to convert values in list to python.

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,
            'widget': ArraySelectMultiple,
        }
        defaults.update(kwargs)
        return super(ArrayField, self).formfield(**defaults)

    def to_python(self, value):
        res = super().to_python(value)
        if isinstance(res, list):
            value = [self.base_field.to_python(val) for val in res]
        return value
@gghildyal

This comment has been minimized.

Copy link

@gghildyal gghildyal commented Apr 29, 2019

Thanks for this snippet.

@hyeonsook95

This comment has been minimized.

Copy link

@hyeonsook95 hyeonsook95 commented Jun 19, 2020

Thanks for this!

@pcraciunoiu

This comment has been minimized.

Copy link

@pcraciunoiu pcraciunoiu commented Aug 7, 2020

I had to overwrite the validation as it was complaining for multiple values:

django.core.exceptions.ValidationError: {'field_name': ["Value ['ONE, 'TWO', 'THREE'] is not a valid choice."]}
class ChoiceArrayField(ArrayField):
    """
    A postgres ArrayField that supports the choices property.

    Ref. https://gist.github.com/danni/f55c4ce19598b2b345ef.
    """

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

    def to_python(self, value):
        res = super().to_python(value)
        if isinstance(res, list):
            value = [self.base_field.to_python(val) for val in res]
        return value

    def validate(self, value, model_instance):
        if not self.editable:
            # Skip validation for non-editable fields.
            return

        if self.choices is not None and value not in self.empty_values:
            if set(value).issubset({option_key for option_key, _ in self.choices}):
                return
            raise exceptions.ValidationError(
                self.error_messages["invalid_choice"],
                code="invalid_choice",
                params={"value": value},
            )

        if value is None and not self.null:
            raise exceptions.ValidationError(self.error_messages["null"], code="null")

        if not self.blank and value in self.empty_values:
            raise exceptions.ValidationError(self.error_messages["blank"], code="blank")
@Jorgeley

This comment has been minimized.

Copy link

@Jorgeley Jorgeley commented Dec 1, 2020

give this guy an Oscar right now! such an elegant solution, thank you soooo much!!!

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