Django and Custom CheckboxSelectMultipleField
Note: as of Django version 0.95, this solution no longer works. But thanks to the newforms module, it is no longer necessary. Explanation of how it works can be found in this post
The problem: I have been using Django in a project for some weeks. The application needs to display a series of checkboxes on the webpage, but the HTML rendering of the formfields.CheckboxSelectMultipleField class does not offer the precise placement of each checkbox that is needed on the webpage.
formfields.CheckboxSelectMultipleField is a descendant of SelectMultipleField and does not provide the ability to iterate through each of the possible choices, as formfields.RadioSelectField derived objects do:
<!-- choices is an formfields.RadioSelectField object
This is not possible with formfields.CheckboxSelectMultipleField derived objects.
-->
<div class="form-row">
<table><tr>
{% for i in form.choices.field_list %}
<td>;{{ i.field }}</td><td>{{i.name}}</td>
{% endfor %}
</tr>
</table>
</div>
The Solution: a custom CheckboxesMultipleSelectField based on the code from formfields.RadioSelectField. CustomCheckboxSelectMultipleField is intended to be used in a custom Manipulator.
The code for the custom CheckboxSelectMultiple
from django.core import formfields
class CustomCheckboxSelectMultipleField(formfields.FormField):
def __init__(self, field_name, choices=[], ul_class='', is_required=False, validator_list=[], member_name=None):
self.field_name = field_name
# choices is a list of (value, human-readable key) tuples because order matters
self.choices, self.is_required = choices, is_required
self.validator_list = [self.isValidChoice] + validator_list
self.ul_class = ul_class
if member_name != None:
self.member_name = member_name
def render(self, data):
"""
Returns a special object that is iterable *and*
has a default str() rendered output.
This allows for flexible use in templates. You can just use the default
rendering:
{{ field_name }}
...which will output the radio buttons in an unordered list.
Or, you can manually traverse each radio option for special layout:
{% for option in field_name.field_list %}
{{ option.field }} {{ option.label }}<br />
{% endfor %}
"""
class CustomCheckBoxSelectMultipleRenderer:
def __init__(self, datalist, ul_class):
self.datalist, self.ul_class = datalist, ul_class
def __str__(self):
"Default str() output for this radio field -- a <ul>"
output = ['</ul><ul %s>' % (self.ul_class and ' class="%s"' % self.ul_class or '')]
output.extend(['<li>%s %s</li>' % (d['field'], d['label']) for d in self.datalist])
output.append('</ul>')
return ''.join(output)
def __iter__(self):
for d in self.datalist:
yield d
def __len__(self):
return len(self.datalist)
datalist = []
str_data_list = map(str, data) # normalize to string
for i, (value, display_name) in enumerate(self.choices):
checked_html = ''
if str(value) in str_data_list:
checked_html = 'checked="checked"'
datalist.append({
'value': value, 'checked': checked_html,
'name': display_name,
'field': '<input type="checkbox" id="%s" name="%s" value="%s"%s/>' % \
(self.get_id() + '_' + str(i), self.field_name, value, checked_html),
'label': '<label for="%s">%s</label>' % \
(self.get_id() + '_' + str(i), display_name),
})
return CustomCheckBoxSelectMultipleRenderer(datalist, self.ul_class)
def isValidChoice(self, data, form):
str_data = str(data)
str_choices = [str(item[0]) for item in self.choices]
if str_data not in str_choices:
raise validators.ValidationError, _("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data':str_data, 'choices':str_choices}