Django Model MultipleChoice
Question:
I know there isn’t MultipleChoiceField
for a Model, you can only use it on Forms.
Today I face an issue when analyzing a new project related with Multiple Choices.
I would like to have a field like a CharField
with choices
with the option of multiple choice.
I solved this issue other times by creating a CharField
and managed the multiple choices in the form with a forms.MultipleChoiceField
and store the choices separated by commas.
In this project, due to configuration, I cannot do it as I mention above, I need to do it in the Models, and I prefer NOT to edit the Django admin form neither use forms. I need a Model Field with multiple choices option
- Have someone solved anything like this via Models ?
Maybe overriding some of the models function or using a custom widget… I don’t know, I’m kinda lost here.
Edit
I’m aware off simple choices, I would like to have something like:
class MODEL(models.Model):
MY_CHOICES = (
('a', 'Hola'),
('b', 'Hello'),
('c', 'Bonjour'),
('d', 'Boas'),
)
...
...
my_field = models.CharField(max_length=1, choices=MY_CHOICES)
...
but with the capability of saving multiple choices not only 1 choice.
Answers:
You need to think about how you are going to store the data at a database level. This will dictate your solution.
Presumably, you want a single column in a table that is storing multiple values. This will also force you to think about how you will serialize – for example, you can’t simply do comma separated if you need to store strings that might contain commas.
However, you are probably best off using a solution like django-multiselectfield
From the two, https://pypi.python.org/pypi/django-select-multiple-field/ looks more well rounded and complete. It even has a nice set of unittests.
The problem I found is that it throws a Django 1.10 deprecation warning in the class that implements the model field.
I fixed this and sent a PR. The latest code, until they merge my PR (if they ever decide to hehe) is in my fork of the repo, here: https://github.com/matiasherranz/django-select-multiple-field
Cheers!
M.-
In case You are using Postgres consider using ArrayField.
from django.db import models
from django.contrib.postgres.fields import ArrayField
class WhateverModel(models.Model):
WHATEVER_CHOICE = u'1'
SAMPLE_CHOICES = (
(WHATEVER_CHOICE, u'one'),
)
choices = ArrayField(
models.CharField(choices=SAMPLE_CHOICES, max_length=2, blank=True, default=WHATEVER_CHOICE),
)
If you want the widget to look like a text input and still be able to allow selecting several options from suggestions, you might be looking for Select2. There is also django-select2 that integrates it with Django Forms and Admin.
The easiest way I found (just I use eval() to convert string gotten from input to tuple to read again for form instance or other place)
This trick works very well
#model.py
class ClassName(models.Model):
field_name = models.CharField(max_length=100)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.field_name:
self.field_name= eval(self.field_name)
#form.py
CHOICES = [('pi', 'PI'), ('ci', 'CI')]
class ClassNameForm(forms.ModelForm):
field_name = forms.MultipleChoiceField(choices=CHOICES)
class Meta:
model = ClassName
fields = ['field_name',]
#view.py
def viewfunction(request, pk):
ins = ClassName.objects.get(pk=pk)
form = ClassNameForm(instance=ins)
if request.method == 'POST':
form = form (request.POST, instance=ins)
if form.is_valid():
form.save()
...
In Your Case, I used ManyToManyField
It Will be something like that:
class MY_CHOICES(models.Model)
choice = models.CharField(max_length=154, unique=True)
class MODEL(models.Model):
...
...
my_field = models.ManyToManyField(MY_CHOICES)
So, now you can select multiple choices
You can use an IntegerField
for the model and powers of two for the choices (a bitmap field). I’m not sure why Django doesn’t have this already built-in.
class MyModel(models.Model):
A = 1
B = 2
C = 4
MY_CHOICES = ((A, "foo"), (B, "bar"), (C, "baz"))
my_field = models.IntegerField(default=0)
from functools import reduce
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
# it can be set to required=True if needed
my_multi_field = forms.TypedMultipleChoiceField(
coerce=int, choices=MyModel.MY_CHOICES, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['my_multi_field'].initial = [
c for c, _ in MyModel.MY_CHOICES
if self.instance.my_field & c
]
def save(self, *args, **kwargs):
self.instance.my_field = reduce(
lambda x, y: x | y,
self.cleaned_data.get('my_multi_field', []),
0)
return super().save(*args, **kwargs)
It can be queried like this: MyModel.objects.filter(my_field=MyModel.A | MyModel.C)
to get all records with A and C set.
Postgres only.
Quite late but for those who come across this
based on @lechup answer i came across this gist.
Take a look at that gist there more improved versions there
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)
Which then i saw it in another production code in one of my other projects.. it worked so well that i thought it was from Django’s default fields. I was googling just to find the Django docs that i came here. 🙂
I know there isn’t MultipleChoiceField
for a Model, you can only use it on Forms.
Today I face an issue when analyzing a new project related with Multiple Choices.
I would like to have a field like a CharField
with choices
with the option of multiple choice.
I solved this issue other times by creating a CharField
and managed the multiple choices in the form with a forms.MultipleChoiceField
and store the choices separated by commas.
In this project, due to configuration, I cannot do it as I mention above, I need to do it in the Models, and I prefer NOT to edit the Django admin form neither use forms. I need a Model Field with multiple choices option
- Have someone solved anything like this via Models ?
Maybe overriding some of the models function or using a custom widget… I don’t know, I’m kinda lost here.
Edit
I’m aware off simple choices, I would like to have something like:
class MODEL(models.Model):
MY_CHOICES = (
('a', 'Hola'),
('b', 'Hello'),
('c', 'Bonjour'),
('d', 'Boas'),
)
...
...
my_field = models.CharField(max_length=1, choices=MY_CHOICES)
...
but with the capability of saving multiple choices not only 1 choice.
You need to think about how you are going to store the data at a database level. This will dictate your solution.
Presumably, you want a single column in a table that is storing multiple values. This will also force you to think about how you will serialize – for example, you can’t simply do comma separated if you need to store strings that might contain commas.
However, you are probably best off using a solution like django-multiselectfield
From the two, https://pypi.python.org/pypi/django-select-multiple-field/ looks more well rounded and complete. It even has a nice set of unittests.
The problem I found is that it throws a Django 1.10 deprecation warning in the class that implements the model field.
I fixed this and sent a PR. The latest code, until they merge my PR (if they ever decide to hehe) is in my fork of the repo, here: https://github.com/matiasherranz/django-select-multiple-field
Cheers!
M.-
In case You are using Postgres consider using ArrayField.
from django.db import models
from django.contrib.postgres.fields import ArrayField
class WhateverModel(models.Model):
WHATEVER_CHOICE = u'1'
SAMPLE_CHOICES = (
(WHATEVER_CHOICE, u'one'),
)
choices = ArrayField(
models.CharField(choices=SAMPLE_CHOICES, max_length=2, blank=True, default=WHATEVER_CHOICE),
)
If you want the widget to look like a text input and still be able to allow selecting several options from suggestions, you might be looking for Select2. There is also django-select2 that integrates it with Django Forms and Admin.
The easiest way I found (just I use eval() to convert string gotten from input to tuple to read again for form instance or other place)
This trick works very well
#model.py
class ClassName(models.Model):
field_name = models.CharField(max_length=100)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.field_name:
self.field_name= eval(self.field_name)
#form.py
CHOICES = [('pi', 'PI'), ('ci', 'CI')]
class ClassNameForm(forms.ModelForm):
field_name = forms.MultipleChoiceField(choices=CHOICES)
class Meta:
model = ClassName
fields = ['field_name',]
#view.py
def viewfunction(request, pk):
ins = ClassName.objects.get(pk=pk)
form = ClassNameForm(instance=ins)
if request.method == 'POST':
form = form (request.POST, instance=ins)
if form.is_valid():
form.save()
...
In Your Case, I used ManyToManyField
It Will be something like that:
class MY_CHOICES(models.Model)
choice = models.CharField(max_length=154, unique=True)
class MODEL(models.Model):
...
...
my_field = models.ManyToManyField(MY_CHOICES)
So, now you can select multiple choices
You can use an IntegerField
for the model and powers of two for the choices (a bitmap field). I’m not sure why Django doesn’t have this already built-in.
class MyModel(models.Model):
A = 1
B = 2
C = 4
MY_CHOICES = ((A, "foo"), (B, "bar"), (C, "baz"))
my_field = models.IntegerField(default=0)
from functools import reduce
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
# it can be set to required=True if needed
my_multi_field = forms.TypedMultipleChoiceField(
coerce=int, choices=MyModel.MY_CHOICES, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['my_multi_field'].initial = [
c for c, _ in MyModel.MY_CHOICES
if self.instance.my_field & c
]
def save(self, *args, **kwargs):
self.instance.my_field = reduce(
lambda x, y: x | y,
self.cleaned_data.get('my_multi_field', []),
0)
return super().save(*args, **kwargs)
It can be queried like this: MyModel.objects.filter(my_field=MyModel.A | MyModel.C)
to get all records with A and C set.
Postgres only.
Quite late but for those who come across this
based on @lechup answer i came across this gist.
Take a look at that gist there more improved versions there
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)
Which then i saw it in another production code in one of my other projects.. it worked so well that i thought it was from Django’s default fields. I was googling just to find the Django docs that i came here. 🙂