Login

MultiSelectField with comma separated values (Field + FormField)

Author:
quinode
Posted:
May 17, 2012
Language:
Python
Version:
1.4
Score:
6 (after 6 ratings)

Daniel Roseman's snippet, updated will all fixes mentioned in the comments of the first version + some other things to make it work under Django 1.4.
South, and dumpdata are working.

There's an ugly int(....) at the validate function in order to cast each value as an integer before comparing it to default choices : I needed this, but if you're storing strings values, just remove the int(......) wrapper.


Orginal readme

Usually you want to store multiple choices as a manytomany link to another table. Sometimes however it is useful to store them in the model itself. This field implements a model field and an accompanying formfield to store multiple choices as a comma-separated list of values, using the normal CHOICES attribute.

You'll need to set maxlength long enough to cope with the maximum number of choices, plus a comma for each.

The normal get_FOO_display() method returns a comma-delimited string of the expanded values of the selected choices.

The formfield takes an optional max_choices parameter to validate a maximum number of choices.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
# New version of this snippet http://djangosnippets.org/snippets/1200/
# tested with Django 1.4

from django import forms
from django.db import models
from django.utils.text import capfirst
from django.core import exceptions


class MultiSelectFormField(forms.MultipleChoiceField):
    widget = forms.CheckboxSelectMultiple
 
    def __init__(self, *args, **kwargs):
        self.max_choices = kwargs.pop('max_choices', 0)
        super(MultiSelectFormField, self).__init__(*args, **kwargs)
 
    def clean(self, value):
        if not value and self.required:
            raise forms.ValidationError(self.error_messages['required'])
        # if value and self.max_choices and len(value) > self.max_choices:
        #     raise forms.ValidationError('You must select a maximum of %s choice%s.'
        #             % (apnumber(self.max_choices), pluralize(self.max_choices)))
        return value

 
class MultiSelectField(models.Field):
    __metaclass__ = models.SubfieldBase
 
    def get_internal_type(self):
        return "CharField"
 
    def get_choices_default(self):
        return self.get_choices(include_blank=False)
 
    def _get_FIELD_display(self, field):
        value = getattr(self, field.attname)
        choicedict = dict(field.choices)
 
    def formfield(self, **kwargs):
        # don't call super, as that overrides default widget if it has choices
        defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name),
                    'help_text': self.help_text, 'choices': self.choices}
        if self.has_default():
            defaults['initial'] = self.get_default()
        defaults.update(kwargs)
        return MultiSelectFormField(**defaults)

    def get_prep_value(self, value):
        return value

    def get_db_prep_value(self, value, connection=None, prepared=False):
        if isinstance(value, basestring):
            return value
        elif isinstance(value, list):
            return ",".join(value)
 
    def to_python(self, value):
        if value is not None:
            return value if isinstance(value, list) else value.split(',')
        return ''

    def contribute_to_class(self, cls, name):
        super(MultiSelectField, self).contribute_to_class(cls, name)
        if self.choices:
            func = lambda self, fieldname = name, choicedict = dict(self.choices): ",".join([choicedict.get(value, value) for value in getattr(self, fieldname)])
            setattr(cls, 'get_%s_display' % self.name, func)
 
    def validate(self, value, model_instance):
        arr_choices = self.get_choices_selected(self.get_choices_default())
        for opt_select in value:
            if (int(opt_select) not in arr_choices):  # the int() here is for comparing with integer choices
                raise exceptions.ValidationError(self.error_messages['invalid_choice'] % value)  
        return
 
    def get_choices_selected(self, arr_choices=''):
        if not arr_choices:
            return False
        list = []
        for choice_selected in arr_choices:
            list.append(choice_selected[0])
        return list
 
    def value_to_string(self, obj):
        value = self._get_val_from_obj(obj)
        return self.get_db_prep_value(value)


# needed for South compatibility

from south.modelsinspector import add_introspection_rules  
add_introspection_rules([], ["^coop\.utils\.fields\.MultiSelectField"])


# Example Model

TYPES = (1, 'Product'),
        (2, 'Service'),
        (3, 'Skill'),
        (4, 'Partnership')),
        (5, 'Question'),
)

class Exchange(models.Model):
    types = MultiSelectField(max_length=250, blank=True, choices=TYPES)
    ...

# Example Form

class ExchangeForm(forms.Form):
    types = MultiSelectFormField(choices=TYPES)
    ...

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

yeago (on June 14, 2012):

Thanks for the snippet. Some thoughts:

-Inherit from CharField or else south won't properly inspect. -Also above, max_length missing error will be obscure -Remove checkbox widget as default

#

danny_adair (on August 2, 2012):

Thanks for the update. Suggestion: change the base from CharField to TextField and the delimiter from comma to newline (\n).

That way you can use commas in your values, don't have to care about max_length, and it's also more readable when looking at raw data.

(I allow for dynamic choices which get configured in a "Choice" model's TextField attribute - one line per choice - that goes together well and makes it easy to edit values through the admin)

#

Rhakyr (on September 10, 2012):

Whenever I save a form with this field in it, it is in the form.changed_data list. How would I stop this from happening?

#

gorazio (on September 18, 2012):

Can you help me? How i can store value of multiple select as "1;0;0;1;0", and not as now "1;4"?

#

aludue (on July 21, 2013):

Does this snippet work for 1.5, i'm currently having problems with the getattr line within _get_FIELD_display. any thoughts?

#

Goin (on October 16, 2013):

I created a package with this code, and I did some improvements:

https://github.com/goinnn/django-multiselectfield/blob/master/CHANGES.rst

#

professorDante (on November 20, 2013):

Such a useful snippet - no need for m2m model just to store static data for your field choices. One improvement - use list comprehension for get_choices_selected, faster:

    if not arr_choices:
        return False
    return [choice_selected[0] for choice_selected in arr_choices]

#

Please login first before commenting.