Multiple Choice model field

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.

fromdjango.dbimportmodelsfromdjangoimportformsclassMultiSelectFormField(forms.MultipleChoiceField):widget=forms.CheckboxSelectMultipledef__init__(self,*args,**kwargs):self.max_choices=kwargs.pop('max_choices',0)super(MultiSelectFormField,self).__init__(*args,**kwargs)defclean(self,value):ifnotvalueandself.required:raiseforms.ValidationError(self.error_messages['required'])ifvalueandself.max_choicesandlen(value)>self.max_choices:raiseforms.ValidationError('You must select a maximum of %s choice%s.'%(apnumber(self.max_choices),pluralize(self.max_choices)))returnvalueclassMultiSelectField(models.Field):__metaclass__=models.SubfieldBasedefget_internal_type(self):return"CharField"defget_choices_default(self):returnself.get_choices(include_blank=False)def_get_FIELD_display(self,field):value=getattr(self,field.attname)choicedict=dict(field.choices)defformfield(self,**kwargs):# don't call super, as that overrides default widget if it has choicesdefaults={'required':notself.blank,'label':capfirst(self.verbose_name),'help_text':self.help_text,'choices':self.choices}ifself.has_default():defaults['initial']=self.get_default()defaults.update(kwargs)returnMultiSelectFormField(**defaults)defget_db_prep_value(self,value):ifisinstance(value,basestring):returnvalueelifisinstance(value,list):return",".join(value)defto_python(self,value):ifisinstance(value,list):returnvaluereturnvalue.split(",")defcontribute_to_class(self,cls,name):super(MultiSelectField,self).contribute_to_class(cls,name)ifself.choices:func=lambdaself,fieldname=name,choicedict=dict(self.choices):",".join([choicedict.get(value,value)forvalueingetattr(self,fieldname)])setattr(cls,'get_%s_display'%self.name,func)

As noted by danielfeng, since Django 1.2, get_db_prep_value expects a connection. Therefore chester's value_to_string method should pass a connection. Otherwise, when doing a dumpdata, you will get:

DeprecationWarning: get_db_prep_value has been called without providing a connection argument.

From my understanding, get_db_prep_value is meant for database backend specific preparations. This field's database preparations are very generic and therefore should be changed to get_prep_value. This solves the deprecation warning. Here are the 2 updated methods: