43

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.

2
  • Can you post the full stack trace for the error you get on POSTing? Commented Apr 28, 2010 at 4:32
  • Possible duplicate of Django Model MultipleChoice Commented Aug 16, 2016 at 20:19

7 Answers 7

42

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks Brant. You were correct in pointing out that I needed to use a ManyToManyField relationship for my model type.
Note that you need to call save_m2m() on the form instance when you use save(commit=false), or changes to the relations will not be saved. See docs.djangoproject.com/en/dev/topics/forms/modelforms/…
I'm trying to implement this but I'm getting NameError: name 'TheChoice' is not defined
Can the solution work for Choice as a tuple instead of a Choice model as in the original question?
@brant maybe it is worth adding a suggestion, that postgresql users can use ArrayFields to achieve same functionality (with even better perfomance)
19

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

2 Comments

any advice on how to render the multi choice field in the django template?
Ensure to define __str__() function inside Choices model, so that description text is rendered as checkbox options in the template.
13

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.

Comments

3

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.

Comments

1

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 %}

1 Comment

The ArrayField solution is nice, but bear in mind it only works if you are using Postgres for your DB.
1

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

Comments

0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.