AlvaroAV
AlvaroAV

Reputation: 10553

Django Model MultipleChoice

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

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.

Upvotes: 107

Views: 166739

Answers (9)

tzot
tzot

Reputation: 95921

In my case:

  • I have a model field which is a Postgres ArrayField of IntegerField.
  • The values in the ArrayField should be constrained by choices.
  • We want the admin interface to present a sequence of checkboxes.

After much swearing, I have this (working minimal snippet):

# admin.py
from django import forms
from django.contrib import admin

class SomeAdmin(admin.ModelAdmin):
    CHOICES = [(1, 'One'), (2, 'Two'), (3, 'Three')]

    def formfield_for_dbfield(self, db_field, request, **kw):
        if db.field_name == 'the_multiple_choice_field':
            return forms.TypedMultipleChoiceField(
                coerce=int,
                choices=self.CHOICES,
                widget=forms.CheckboxSelectMultiple
            )
        return super().formfield_for_dbfield(db_field, request, **kw)

Upvotes: 0

Amir Heshmati
Amir Heshmati

Reputation: 648

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

Upvotes: 3

nitely
nitely

Reputation: 2288

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.

Upvotes: 4

Desert Camel
Desert Camel

Reputation: 137

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

Upvotes: 4

younesfmgtc
younesfmgtc

Reputation: 19

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

Upvotes: -2

spookylukey
spookylukey

Reputation: 6576

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

Upvotes: 85

Risadinha
Risadinha

Reputation: 16666

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.

Upvotes: 1

lechup
lechup

Reputation: 3150

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),
    )

Upvotes: 91

Matias Herranz
Matias Herranz

Reputation: 151

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

Upvotes: 14

Related Questions