twampss
twampss

Reputation: 1297

Django Multiple Choice Field / Checkbox Select Multiple

I have a Django application and want to display multiple choice checkboxes in a user's profile. They will then be able to select multiple items.

This is a simplified version of my models.py:

from profiles.choices import SAMPLE_CHOICES

class Profile(models.Model):
    user = models.ForeignKey(User, unique=True, verbose_name_('user'))
    choice_field = models.CharField(_('Some choices...'), choices=SAMPLE_CHOICES, max_length=50)

And my form class:

class ProfileForm(forms.ModelForm):
    choice_field = forms.MultipleChoiceField(choices=SAMPLE_CHOICES, widget=forms.CheckboxSelectMultiple)

    class Meta:
        model = Profile

And my views.py:

if request.method == "POST":
    profile_form = form_class(request.POST, instance=profile)
    if profile_form.is_valid():
        ...
        profile.save()
return render_to_response(template_name, {"profile_form": profile_form,}, context_instance=RequestContext(request))

I can see that the POST is only sending one value:

choice_field u'choice_three' 

And the local vars params is sending a list:

[u'choice_one', u'choice_two', u'choice_three']

All of the form fields display correct, but when I submit a POST, I get an error

Error binding parameter 7 - probably unsupported type.

Do I need to process the multiple choice field further in the view? Is the model field type correct? Any help or references would be greatly appreciated.

Upvotes: 43

Views: 129227

Answers (7)

Deloo
Deloo

Reputation: 116

I've implemented a very simple form field that converts from and to a CharField:

from django.forms import ModelForm, MultipleChoiceField, ValidationError, CheckboxSelectMultiple
from spellbook.models import MyModel


class CheckboxSelectMultipleAsCharField(CheckboxSelectMultiple):
    def format_value(self, value):
        if value is not None and isinstance(value, str):
            value = list(value)
        return super().format_value(value)


class MultipleChoiceFieldAsCharField(MultipleChoiceField):
    widget = CheckboxSelectMultipleAsCharField
    def to_python(self, value):
        return ''.join(super().to_python(value))

    def validate(self, value):
        super().validate(value)
        if len(value) > len(self.choices):
            raise ValidationError('Too many choices.')



class MyModelForm(ModelForm):
    my_char_field = MultipleChoiceFieldAsCharField(choices=MyModel.OptionsEnum.choices, required=True)
    ...

Upvotes: 1

juanifioren
juanifioren

Reputation: 683

You can easily achieve this using ArrayField:

# in my models...
tags = ArrayField(models.CharField(null=True, blank=True, max_length=100, choices=SECTORS_TAGS_CHOICES), blank=True, default=list)

# in my forms...
class MyForm(forms.ModelForm):

    class Meta:
        model = ModelClass
        fields = [..., 'tags', ...]

I use tagsinput JS library to render my tags but you can use whatever you like: This my template for this widget:

{% if not hidelabel and field.label %}<label for="{{ field.id_for_label }}">{{ field.label }}</label>{% endif %}
<input id="{{ field.id_for_label }}" type="text" name="{{ field.name }}" data-provide="tagsinput"{% if field.value %} value="{{ field.value }}"{% endif %}{% if field.field.disabled %} disabled{% endif %}>
{% if field.help_text %}<small id="{{ field.name }}-help-text" class="form-text text-muted">{{ field.help_text | safe }}</small>{% endif %}

Upvotes: 1

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',]

Upvotes: 0

wyx
wyx

Reputation: 3514

ManyToManyField isn`t a good choice.You can use some snippets to implement MultipleChoiceField.You can be inspired by MultiSelectField with comma separated values (Field + FormField) But it has some bug in it.And you can install django-multiselectfield.This is more prefect.

Upvotes: 3

twampss
twampss

Reputation: 1297

Brant's solution is absolutely correct, but I needed to modify it to make it work with multiple select checkboxes and commit=false. Here is my solution:

models.py

class Choices(models.Model):
    description = models.CharField(max_length=300)

class Profile(models.Model):
   user = models.ForeignKey(User, blank=True, unique=True, verbose_name_('user'))
   the_choices = models.ManyToManyField(Choices)

forms.py

class ProfileForm(forms.ModelForm):
    the_choices = forms.ModelMultipleChoiceField(queryset=Choices.objects.all(), required=False, widget=forms.CheckboxSelectMultiple)

    class Meta:
        model = Profile
        exclude = ['user']

views.py

if request.method=='POST':
    form = ProfileForm(request.POST)
    if form.is_valid():
        profile = form.save(commit=False)
        profile.user = request.user
        profile.save()
        form.save_m2m() # needed since using commit=False
    else:
        form = ProfileForm()

return render_to_response(template_name, {"profile_form": form}, context_instance=RequestContext(request))

Upvotes: 19

Brant
Brant

Reputation: 6011

The profile choices need to be setup as a ManyToManyField for this to work correctly.

So... your model should be like this:

class Choices(models.Model):
  description = models.CharField(max_length=300)

class Profile(models.Model):
  user = models.ForeignKey(User, blank=True, unique=True, verbose_name='user')
  choices = models.ManyToManyField(Choices)

Then, sync the database and load up Choices with the various options you want available.

Now, the ModelForm will build itself...

class ProfileForm(forms.ModelForm):
  Meta:
    model = Profile
    exclude = ['user']

And finally, the view:

if request.method=='POST':
  form = ProfileForm(request.POST)
  if form.is_valid():
    profile = form.save(commit=False)
    profile.user = request.user
    profile.save()
else:
  form = ProfileForm()

return render_to_response(template_name, {"profile_form": form}, context_instance=RequestContext(request))

It should be mentioned that you could setup a profile in a couple different ways, including inheritance. That said, this should work for you as well.

Good luck.

Upvotes: 41

Webthusiast
Webthusiast

Reputation: 1026

The models.CharField is a CharField representation of one of the choices. What you want is a set of choices. This doesn't seem to be implemented in django (yet).

You could use a many to many field for it, but that has the disadvantage that the choices have to be put in a database. If you want to use hard coded choices, this is probably not what you want.

There is a django snippet at http://djangosnippets.org/snippets/1200/ that does seem to solve your problem, by implementing a ModelField MultipleChoiceField.

Upvotes: 13

Related Questions