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?