-
-
Save danni/f55c4ce19598b2b345ef to your computer and use it in GitHub Desktop.
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) |
Thank you @pcraciunoiu
@pcraciunoiu , I am getting:
TypeError
__init__() got an unexpected keyword argument 'base_field'
Relevant traceback:
/usr/local/lib/python3.8/site-packages/django/forms/fields.py, line 772, in __init__
class ChoiceField(Field):
widget = Select
default_error_messages = {
'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'),
}
def __init__(self, *, choices=(), **kwargs):
super().__init__(**kwargs) …
self.choices = choices
def __deepcopy__(self, memo):
result = super().__deepcopy__(memo)
result._choices = copy.deepcopy(self._choices, memo)
return result
@niccolomineo our project still uses this exactly as I pasted it, and it works on Django 3.1.7 with postgres. Are you sure you're on a version that's valid? Should be at least Django 2.2 I think.
Also this is a model field, not a form field. You might be using it in the wrong place.
Yep, I am using it as an ArrayField
replacement. I am on Django 3.2.
@pcraciunoiu Outstanding solution. Works on Django 3.2/Python 3.8.
Why doesn't this work in Django 4? I keep getting AttributeError: 'ChoiceArrayField' object has no attribute 'get_bound_field'
@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.
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}
}
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)
Thanks everybody. using @anyidea 's solution. works nicely
Thanks this solution works very well
give this guy an Oscar right now! such an elegant solution, thank you soooo much!!!