Custom Widget – My First Experience

I hadn’t needed to write a custom widget before today, but I needed to have disabled options included in a <select> list. After a little research and a LOT of trial and error, this is what I came up with.

from django.forms.widgets import Select, force_unicode, flatatt, escape, mark_safe, chain, conditional_escape

class SelectDisabled(Select):
    def __init__(self, attrs=None, choices=(), disabled=[]):
        super(SelectDisabled, self).__init__(attrs, choices)
        self.disabled = list(disabled)

    def render(self, name, value, attrs=None, choices=()):
        if value is None: value = ''
        final_attrs = self.build_attrs(attrs, name=name)
        output = [u'<select%s>' % flatatt(final_attrs)]
        options = self.render_options(choices, [value], self.disabled)
        if options:
            output.append(options)
        output.append('</select>')
        return mark_safe(u'\n'.join(output))

    def render_options(self, choices, selected_choices, disabled_choices):
        def render_option(option_value, option_label):
            option_value = force_unicode(option_value)
            option_label = (option_value in disabled_choices) and (force_unicode(option_label) + ' - SOLD OUT') or force_unicode(option_label)
            selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
            disabled_html = (option_value in disabled_choices) and u' disabled="disabled"' or ''
            return u'<option value="%s"%s%s>%s</option>' % (
                escape(option_value), selected_html, disabled_html,
                conditional_escape(option_label))
        # Normalize to strings.
        selected_choices = set([force_unicode(v) for v in selected_choices])
        disabled_choices = set([force_unicode(v) for v in disabled_choices])
        output = []
        for option_value, option_label in chain(self.choices, choices):
            if isinstance(option_label, (list, tuple)):
                output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value)))
                for option in option_label:
                    output.append(render_option(*option))
                output.append(u'</optgroup>')
            else:
                output.append(render_option(option_value, option_label))
        return u'\n'.join(output)

And here’s how I used it in the form:

class B2CRegistrationStartForm(Form): 
    def __init__(self, disabled, *args, **kwargs):
        super(B2CRegistrationStartForm, self).__init__(*args, **kwargs)
        if disabled:
            self.fields['event'].widget.disabled = disabled
    event = ModelChoiceField(widget=SelectDisabled(),queryset=Event.b2c_web_reg.all(),empty_label=None)  

And here is the form creation in views.py:

    disabled = []
    for e in Event.b2c_sold_out.values_list('id'):
        disabled.append(e[0])
    form = B2CRegistrationStartForm( data=request.POST or None, disabled=disabled)

(The list of disabled IDs needs to be in a list, not a list of tuples)

It works fine, and I learned about custom widgets, but I don’t feel like I was very Object Oriented in my approach. The __init__() method uses super() well, but the other two basically replace the methods in the parent class. When I update to a newer version of Django (from the current 1.0.4), I’ll have to check this carefully and probably redo it.

Anyone have suggestions on how I could better implement this feature?

Comments are closed.