Index: widgets.py
===================================================================
--- widgets.py	(revision 4557)
+++ widgets.py	(working copy)
@@ -2,11 +2,14 @@
 Extra HTML Widget classes
 """
 
+from django.utils.html import escape
+from itertools import chain
+from django.newforms.util import flatatt, smart_unicode
 from django.newforms.widgets import Widget, Select
 from django.utils.dates import MONTHS
 import datetime
 
-__all__ = ('SelectDateWidget',)
+__all__ = ('SelectDateWidget', 'GroupedSelect')
 
 class SelectDateWidget(Widget):
     """
@@ -57,3 +60,33 @@
         if y and m and d:
             return '%s-%s-%s' % (y, m, d)
         return None
+ 
+class GroupedSelect(Select):
+    def __init__(self, attrs=None, groups=(), choices=()):
+        super(GroupedSelect, self).__init__(attrs, choices)
+        # groups maps from 'group name' to a list of values in that group
+        # to preserve ordering, it's a list-of-tuples instead of a dict
+        self.groups = groups
+
+    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)]
+        str_value = smart_unicode(value) # Normalize to string.
+        allchoices = dict([(smart_unicode(k), smart_unicode(v))
+            for k, v in chain(self.choices, choices)])
+        for group_label, members in self.groups:
+            group_label = smart_unicode(group_label)
+            output.append(u'<optgroup label="%s">' % escape(group_label))
+            for option_value in members:
+                option_value = smart_unicode(option_value)
+                try:
+                    option_label = allchoices[option_value]
+                except KeyError:
+                    continue # silently ignore missing elements from the group
+                selected_html = (option_value == str_value) and u' selected="selected"' or ''
+                output.append(u'<option value="%s"%s>%s</option>' % (escape(option_value), selected_html, escape(option_label)))
+            output.append(u'</optgroup>')
+        output.append(u'</select>')
+        return u'\n'.join(output)
+
