from django import forms from django.utils.safestring import mark_safe class ChoiceWithOtherRenderer(forms.RadioSelect.renderer): """RadioFieldRenderer that renders its last choice with a placeholder.""" def __init__(self, *args, **kwargs): super(ChoiceWithOtherRenderer, self).__init__(*args, **kwargs) self.choices, self.other = self.choices[:-1], self.choices[-1] def __iter__(self): for input in super(ChoiceWithOtherRenderer, self).__iter__(): yield input id = '%s_%s' % (self.attrs['id'], self.other[0]) if 'id' in self.attrs else '' label_for = ' for="%s"' % id if id else '' checked = '' if not self.other[0] == self.value else 'checked="true" ' yield ' %%s' % ( label_for, id, self.other[0], self.name, checked, self.other[1]) class ChoiceWithOtherWidget(forms.MultiWidget): """MultiWidget for use with ChoiceWithOtherField.""" def __init__(self, choices): widgets = [ forms.RadioSelect(choices=choices, renderer=ChoiceWithOtherRenderer), forms.TextInput ] super(ChoiceWithOtherWidget, self).__init__(widgets) def decompress(self, value): if not value: return [None, None] return value def format_output(self, rendered_widgets): """Format the output by substituting the "other" choice into the first widget.""" return rendered_widgets[0] % rendered_widgets[1] class ChoiceWithOtherField(forms.MultiValueField): """ ChoiceField with an option for a user-submitted "other" value. The last item in the choices array passed to __init__ is expected to be a choice for "other". This field's cleaned data is a tuple consisting of the choice the user made, and the "other" field typed in if the choice made was the last one. >>> class AgeForm(forms.Form): ... age = ChoiceWithOtherField(choices=[ ... (0, '15-29'), ... (1, '30-44'), ... (2, '45-60'), ... (3, 'Other, please specify:') ... ]) ... >>> # rendered as a RadioSelect choice field whose last choice has a text input ... print AgeForm()['age']