42

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.

twampss
  • 1,287
  • 2
  • 12
  • 13
  • Can you post the full stack trace for the error you get on POSTing? – ars Apr 28 '10 at 04:32
  • Possible duplicate of [Django Model MultipleChoice](http://stackoverflow.com/questions/27440861/django-model-multiplechoice) – lechup Aug 16 '16 at 20:19

7 Answers7

40

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.

bns
  • 182
  • 2
  • 11
Brant
  • 5,721
  • 4
  • 36
  • 39
  • Thanks Brant. You were correct in pointing out that I needed to use a ManyToManyField relationship for my model type. – twampss Apr 29 '10 at 04:28
  • 5
    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 http://docs.djangoproject.com/en/dev/topics/forms/modelforms/#the-save-method – jakewins Nov 03 '10 at 19:24
  • I'm trying to implement this but I'm getting `NameError: name 'TheChoice' is not defined` – Daniel X Moore Aug 31 '11 at 19:20
  • Can the solution work for Choice as a tuple instead of a Choice model as in the original question? – user956424 Sep 22 '14 at 08:01
  • @brant maybe it is worth adding a suggestion, that postgresql users can use ArrayFields to achieve same functionality (with even better perfomance) – Jerzyk Jun 09 '16 at 18:32
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))
dreamer
  • 901
  • 2
  • 15
  • 38
twampss
  • 1,287
  • 2
  • 12
  • 13
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.

northben
  • 5,448
  • 4
  • 35
  • 47
Webthusiast
  • 986
  • 10
  • 16
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.

wyx
  • 3,334
  • 6
  • 24
  • 44
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',]
0

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 %}
juanifioren
  • 633
  • 5
  • 18
  • The `ArrayField` solution is nice, but bear in mind it only works if you are using Postgres for your DB. – Nexus Jun 30 '23 at 21:04
0

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)
    ...
Deloo
  • 96
  • 6