102

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.

Community
  • 1
  • 1
AlvaroAV
  • 10,335
  • 12
  • 60
  • 91
  • 1
    What you describe ought to be a many to many relationship between two models, at least if you have any care for your db schema sanity. And it will be faster to implement than writing a custom field and widget... – bruno desthuilliers Dec 12 '14 at 10:48
  • 1
    @brunodesthuilliers No, I'm not interested in link 2 models, in fact, there is no model for the multiple options (I never said that in my question). I need the options to be like a choices, a tuple . – AlvaroAV Dec 12 '14 at 10:51
  • 3
    possible duplicate of [Is there a Django ModelField that allows for multiple choices, aside from ManyToMany?](http://stackoverflow.com/questions/2267332/is-there-a-django-modelfield-that-allows-for-multiple-choices-aside-from-manyto) – spookylukey Dec 12 '14 at 11:37
  • 3
    @Liarez: in a relational schema, fields should be atomic. This is why the technically sane and correct solution is to create a second model with your options and use a many to many relationship. Note that it takes about 10 minutes to get the whole thing working. – bruno desthuilliers Dec 12 '14 at 11:48
  • 1
    @brunodesthuilliers again, maybe I'm explaining bad, I'm not english native, I need to avoid using models because of the project itself, I know that the option you're saying is the correct one, is the first one anybody would think about, but I **cannot** do it like that (because of the project structure but mostly because of the clients) – AlvaroAV Dec 12 '14 at 11:50
  • @Liarez: AOK. If you know what you're doing then spookylukey's suggestions are probably your best bets (as you already found out). As a general suggestion: when you know what you're asking for is not the obvious solution but you have other reasons, then mention it clearly, this will save you from pedantic comments like mine ;) – bruno desthuilliers Dec 12 '14 at 11:54

8 Answers8

85

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

nuts
  • 715
  • 8
  • 22
spookylukey
  • 6,380
  • 1
  • 31
  • 34
  • 3
    django-multiselectfield is exactly what I was looking for. I didn't see the other guy question ! Thanks you so much for this information !! – AlvaroAV Dec 12 '14 at 11:46
  • 4
    I come from the future thanking you for that answer! multiselectfield works like charm! – Mostafa Elgayar Dec 14 '16 at 16:22
  • Can you use multiselectfield and maintain a ManyToMany relationship? – BigMonkey89WithaLeg Jun 22 '17 at 18:13
  • Pypi was down for maintenance yesterday, the links are working again. But I updated them to the modern style anyway. – spookylukey Nov 11 '18 at 16:51
  • @BigMonkey89WithaLeg did you find out if it can? – Kwaku Biney Apr 01 '21 at 15:06
  • django-multiselectfield doesnt have option of disabled=True in form – Aseem Apr 20 '21 at 06:10
  • normally you would create a dimension table with all your options with an auto incrementing primary key, when inserting this value onto your fact table you would select the text value and use the primary key as a foreign key, _storing repetitive strings in a table is bad design_ , I'm not skilled enough in Django but a many to one relationship would work. I personally just wrote a procedure in PSQL to populate my table after creating a class. – Umar.H Jun 21 '21 at 21:55
84

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),
    )
lechup
  • 3,031
  • 27
  • 26
  • The above does not work with a nice dropdown like in a single field. Correct? In that case is there a way of getting dropdowns to appear (with the ability add additional instances each with their own dropdown as needed) – dnk8n Jun 22 '20 at 17:51
  • 5
    @DeanKayton You need a custom form for the admin that assigns TypedMultipleChoiceField to the field `class UserForm(forms.ModelForm): roles = TypedMultipleChoiceField(choices=User.ROLES)` and then set `form=UserForm` on your admin – Gordon Wrigley Jun 26 '20 at 13:56
  • Thank you @GordonWrigley. In the end we decided it was simpler to reference ManyToMany relationships and use admin.py (admin.ModelAdmin) autocomplete_fields Would have been great if autocomplete_fields could simply also work for hardcoded choices. – dnk8n Jun 28 '20 at 11:40
14

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.-

Matias Herranz
  • 151
  • 1
  • 3
  • 1
    Hello, welcome to SO. This is not a forum, it's a Q&A site. I recommend to take a [tour] of the site. You've posted what should be an answer to the original question, but it isn't; it's rather a comment on spookylukey's answer. Or, if it really is an answer, and it is simply based on another one, it should be stated clearly. So please [edit] it. Thank you! – Fabio says Reinstate Monica Jul 28 '16 at 01:04
  • 48
    Perhaps true, it anyway contributes to the subject on the table. Given that I have not more than 50 points of reputation, I cannot reply to spookylukey's comment, which was my original intention. If you believe that contributing to open source and sharing it here is not worth it, then please, go ahead and ban my comment. Have a nice day! – Matias Herranz Jul 29 '16 at 18:22
4

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

Desert Camel
  • 127
  • 11
  • This is crazy, but works perfectly fine. I have been using it since 2010. I did not find any any errors yet. just need make sure that user selects only one field from UI. But drawback is, it's not handy when you have countable static values Salutations,Genders etc. in such cases I prefer putting them on to the UI and handle with JS instead of creating a model – trex Aug 15 '20 at 04:58
  • It is okay, But it differs when you want to leave the field to the client to add whatever he wants from choices. Is that available with JS in the UI? – Desert Camel Aug 15 '20 at 19:06
  • 1
    -Yes, of course. you can do anything with JavaScript. Take a look at django admin's `ManyToManyField` option by logging to admin ui, they're doing it already there. They handled it through django itself. It could have been more flexible and and user friendly if you use jquery, anugular or any javascript. – trex Aug 16 '20 at 05:48
3

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.

nitely
  • 2,208
  • 1
  • 22
  • 23
2

Postgres only.

Quite late but for those who come across this based on @lechup answer I came across this gist (please also take a look at its comments).

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 ended up here. :)

Amir Heshmati
  • 550
  • 1
  • 8
  • 19
1

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.

Risadinha
  • 16,058
  • 2
  • 88
  • 91
-2

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()
            ...