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}
django, python

Join my free newsletter and receive updates directly to your inbox.