Index: django/forms/extras/widgets.py
===================================================================
--- django/forms/extras/widgets.py	(revision 9337)
+++ django/forms/extras/widgets.py	(working copy)
@@ -6,74 +6,239 @@
 import re
 
 from django.forms.widgets import Widget, Select
-from django.utils.dates import MONTHS
+from django.utils.dates import MONTHS, MONTHS_AP, MONTHS_3
 from django.utils.safestring import mark_safe
+from django.conf import settings
 
-__all__ = ('SelectDateWidget',)
+__all__ = ('SelectDateWidget', 'SelectTimeWidget',)
 
-RE_DATE = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$')
 
-class SelectDateWidget(Widget):
+class SelectDateWidgetBase(Widget):
     """
-    A Widget that splits date input into three <select> boxes.
+    Base class for SelectDateWidget, SelectDateTimeWidget and
+    SelectTimeWidget.
+    """
+    def __init__(self, attrs=None, format=None):
+        if attrs is None:
+            attrs = {}
+        self.attrs = attrs
+        self.values = {}
+        self.format = self.parse_format(format)
 
-    This also serves as an example of a Widget that has more than one HTML
-    element and hence implements value_from_datadict.
+    def render(self, name, value, attrs=None):
+        """
+        Return the html code of the widget.
+        """
+        if 'id' in self.attrs:
+            id_ = self.attrs['id']
+        else:
+            id_ = 'id_%s' % name
+        local_attrs = self.build_attrs()
+        self.values = self.parse_value(value)
+        output = []
+        for (n, fmt) in self.format:
+            select_name = '%s_%s' % (name, n)
+            local_attrs['id'] = '%s_%s' % (id_, n)
+            if hasattr(self, '%s_choices' % n):
+                select = Select(choices=getattr(self, '%s_choices' % n)(fmt))
+                html = select.render(select_name, self.values[n], local_attrs)
+                output.append(html)
+        return mark_safe(u'\n'.join(output))
+
+    def id_for_label(self, id_):
+        return '%s_%s' % (self.format[0][1], id_)
+    id_for_label = classmethod(id_for_label)
+
+    def value_from_datadict(self, data, files, name):
+        raise NotImplementedError('SelectDateWidgetBase::value_from_datadict()\
+                is abstract and must be implemented in child classes')
+
+    def parse_format(self, fmt):
+        raise NotImplementedError('SelectDateWidgetBase::parse_format() is \
+                abstract and must be implemented in child classes')
+
+    def parse_value(self, fmt):
+        raise NotImplementedError('SelectDateWidgetBase::parse_value() is \
+                abstract and must be implemented in child classes')
+
+
+class SelectDateWidget(SelectDateWidgetBase):
     """
-    month_field = '%s_month'
-    day_field = '%s_day'
-    year_field = '%s_year'
-
-    def __init__(self, attrs=None, years=None):
+    A Widget that splits date input into three <select> boxes.
+    """
+    def __init__(self, attrs=None, years=None, format=None):
         # years is an optional list/tuple of years to use in the "year" select box.
-        self.attrs = attrs or {}
         if years:
             self.years = years
         else:
             this_year = datetime.date.today().year
             self.years = range(this_year, this_year+10)
+        super(SelectDateWidget, self).__init__(attrs, format)
 
-    def render(self, name, value, attrs=None):
-        try:
-            year_val, month_val, day_val = value.year, value.month, value.day
-        except AttributeError:
-            year_val = month_val = day_val = None
-            if isinstance(value, basestring):
-                match = RE_DATE.match(value)
-                if match:
-                    year_val, month_val, day_val = [int(v) for v in match.groups()]
+    def value_from_datadict(self, data, files, name):
+        vals = []
+        y = data.get('%s_year' % name)
+        m = data.get('%s_month' % name)
+        d = data.get('%s_day' % name)
+        if y and m and d:
+            return u'-'.join([y, m, d])
+        return data.get(name, None)
 
-        output = []
-
-        if 'id' in self.attrs:
-            id_ = self.attrs['id']
+    def parse_value(self, val):
+        ret = {}
+        if isinstance(val, datetime.date):
+            ret['month'] = val.month
+            ret['day'] = val.day
+            ret['year'] = val.year
         else:
-            id_ = 'id_%s' % name
+            try:
+                l = map(int, val.split('-'))
+            except (ValueError, AttributeError):
+                l = (None, None, None)
+            for i, k in [(0, 'year'), (1, 'month'), (2, 'day')]:
+                try:
+                    ret[k] = l[i]
+                except IndexError:
+                    ret[k] = None
+        return ret
 
-        month_choices = MONTHS.items()
-        month_choices.sort()
-        local_attrs = self.build_attrs(id=self.month_field % id_)
-        select_html = Select(choices=month_choices).render(self.month_field % name, month_val, local_attrs)
-        output.append(select_html)
+    def parse_format(self, fmt):
+        """
+        Parse the given format `fmt` and set the format property.
+        """
+        if fmt is None:
+            fmt = settings.DATE_FORMAT
+        ret = []
+        for item in fmt:
+            if item in ['d', 'D', 'j', 'L']:
+                ret.append(('day', item,))
+            elif item in ['n', 'm', 'F', 'b', 'M', 'N']:
+                ret.append(('month', item,))
+            elif item in ['y', 'Y']:
+                ret.append(('year', item,))
+        return ret
 
-        day_choices = [(i, i) for i in range(1, 32)]
-        local_attrs['id'] = self.day_field % id_
-        select_html = Select(choices=day_choices).render(self.day_field % name, day_val, local_attrs)
-        output.append(select_html)
+    def month_choices(self, fmt):
+        """
+        Return list of choices (tuple (key, value)) for monthes select.
+        """
+        if fmt == 'n':
+            # month numbers without leading 0 (1 .. 12)
+            return [(i, i) for i in range(1, 13)]
+        elif fmt == 'm':
+            # month numbers with leading 0 (01 .. 12)
+            return [(i, '%02d' % i) for i in range(1, 13)]
+        elif fmt in ['F', 'b', 'M', 'N']:
+            if fmt == 'F':
+                # full month names
+                month_choices = MONTHS.items()
+            elif fmt == 'b':
+                # 3 first letters of month lowercase
+                month_choices = [(k, v.lower()) for (k, v) in MONTHS_3.items()]
+            elif fmt == 'M':
+                # 3 first letters of month
+                month_choices = MONTHS_3.items()
+            elif fmt == 'N':
+                # abbrev of month names
+                month_choices = MONTHS_AP.items()
+            month_choices.sort()
+            return month_choices
+        return []
 
-        year_choices = [(i, i) for i in self.years]
-        local_attrs['id'] = self.year_field % id_
-        select_html = Select(choices=year_choices).render(self.year_field % name, year_val, local_attrs)
-        output.append(select_html)
+    def day_choices(self, fmt):
+        """
+        Return list of choices (tuple (key, value)) for days select.
+        """
+        if fmt == 'j':
+            # day of month number without leading 0
+            return [(i, i) for i in range(1, 32)]
+        elif fmt == 'd':
+            # day of month number with leading 0
+            return [(i, '%02d' % i) for i in range(1, 32)]
+        return []
 
-        return mark_safe(u'\n'.join(output))
+    def year_choices(self, fmt):
+        """
+        Return list of choices (tuple (key, value)) for years select.
+        """
+        if fmt == 'Y':
+            # years with 4 numbers
+            return [(i, i) for i in self.years]
+        elif fmt == 'y':
+            # years with only the last 2 numbers
+            return [(i, str(i)[-2:]) for i in self.years]
+        return []
 
-    def id_for_label(self, id_):
-        return '%s_month' % id_
-    id_for_label = classmethod(id_for_label)
 
+class SelectTimeWidget(SelectDateWidgetBase):
+    """
+    A Widget that splits time input into two or three <select> boxes.
+    XXX: at the moment it is limited to theses formats: 'Hi' and 'His'.
+    """
+    def __init__(self, attrs=None, format=None):
+        super(SelectTimeWidget, self).__init__(attrs, format)
+
+    def parse_format(self, fmt):
+        if fmt not in ['Hi', 'His']:
+            fmt = 'Hi'
+        ret = []
+        for item in fmt:
+            if item == 'H':
+                ret.append(('hour', item,))
+            elif item == 'i':
+                ret.append(('minute', item,))
+            elif item == 's':
+                ret.append(('second', item,))
+        return ret
+
+    def parse_value(self, val):
+        ret = {}
+        if isinstance(val, datetime.time):
+            ret['hour'] = val.hour
+            ret['minute'] = val.minute
+            ret['second'] = val.second
+        else:
+            try:
+                l = map(int, val.split(':'))
+            except (ValueError, AttributeError):
+                l = (None, None, None)
+            for i, k in [(0, 'hour'), (1, 'minute'), (2, 'second')]:
+                try:
+                    ret[k] = l[i]
+                except IndexError:
+                    ret[k] = None
+        return ret
+
     def value_from_datadict(self, data, files, name):
-        y, m, d = data.get(self.year_field % name), data.get(self.month_field % name), data.get(self.day_field % name)
-        if y and m and d:
-            return '%s-%s-%s' % (y, m, d)
+        vals = []
+        h = data.get('%s_hour' % name)
+        m = data.get('%s_minute' % name)
+        s = data.get('%s_second' % name)
+        if h and m:
+            if s:
+                return u':'.join([h, m, s])
+            else:
+                return u':'.join([h, m])
         return data.get(name, None)
+
+    def hour_choices(self, fmt):
+        """
+        Return list of choices (tuple (key, value)) for hours select.
+        """
+        # hour 24H format with leading 0
+        return [(i, '%02d' % i) for i in range(0, 24)]
+
+    def minute_choices(self, fmt):
+        """
+        Return list of choices (tuple (key, value)) for minutes select.
+        """
+        # minutes with leading 0
+        return [(i, '%02d' % i) for i in range(0, 60)]
+
+    def second_choices(self, fmt):
+        """
+        Return list of choices (tuple (key, value)) for seconds select.
+        """
+        # seconds with leading 0
+        return [(i, '%02d' % i) for i in range(0, 60)]
+
Index: tests/regressiontests/forms/extra.py
===================================================================
--- tests/regressiontests/forms/extra.py	(revision 9337)
+++ tests/regressiontests/forms/extra.py	(working copy)
@@ -20,7 +20,7 @@
 # SelectDateWidget ############################################################
 
 >>> from django.forms.extras import SelectDateWidget
->>> w = SelectDateWidget(years=('2007','2008','2009','2010','2011','2012','2013','2014','2015','2016'))
+>>> w = SelectDateWidget(years=('2007','2008','2009','2010','2011','2012','2013','2014','2015','2016'), format='FjY')
 >>> print w.render('mydate', '')
 <select name="mydate_month" id="id_mydate_month">
 <option value="1">January</option>
@@ -234,6 +234,128 @@
 2008-04-01
 
 
+# SelectTimeWidget ############################################################
+
+>>> from django.forms.extras import SelectTimeWidget
+>>> w = SelectTimeWidget()
+>>> print w.render('mytime', '')
+<select name="mytime_hour" id="id_mytime_hour">
+<option value="0">00</option>
+<option value="1">01</option>
+<option value="2">02</option>
+<option value="3">03</option>
+<option value="4">04</option>
+<option value="5">05</option>
+<option value="6">06</option>
+<option value="7">07</option>
+<option value="8">08</option>
+<option value="9">09</option>
+<option value="10">10</option>
+<option value="11">11</option>
+<option value="12">12</option>
+<option value="13">13</option>
+<option value="14">14</option>
+<option value="15">15</option>
+<option value="16">16</option>
+<option value="17">17</option>
+<option value="18">18</option>
+<option value="19">19</option>
+<option value="20">20</option>
+<option value="21">21</option>
+<option value="22">22</option>
+<option value="23">23</option>
+</select>
+<select name="mytime_minute" id="id_mytime_minute">
+<option value="0">00</option>
+<option value="1">01</option>
+<option value="2">02</option>
+<option value="3">03</option>
+<option value="4">04</option>
+<option value="5">05</option>
+<option value="6">06</option>
+<option value="7">07</option>
+<option value="8">08</option>
+<option value="9">09</option>
+<option value="10">10</option>
+<option value="11">11</option>
+<option value="12">12</option>
+<option value="13">13</option>
+<option value="14">14</option>
+<option value="15">15</option>
+<option value="16">16</option>
+<option value="17">17</option>
+<option value="18">18</option>
+<option value="19">19</option>
+<option value="20">20</option>
+<option value="21">21</option>
+<option value="22">22</option>
+<option value="23">23</option>
+<option value="24">24</option>
+<option value="25">25</option>
+<option value="26">26</option>
+<option value="27">27</option>
+<option value="28">28</option>
+<option value="29">29</option>
+<option value="30">30</option>
+<option value="31">31</option>
+<option value="32">32</option>
+<option value="33">33</option>
+<option value="34">34</option>
+<option value="35">35</option>
+<option value="36">36</option>
+<option value="37">37</option>
+<option value="38">38</option>
+<option value="39">39</option>
+<option value="40">40</option>
+<option value="41">41</option>
+<option value="42">42</option>
+<option value="43">43</option>
+<option value="44">44</option>
+<option value="45">45</option>
+<option value="46">46</option>
+<option value="47">47</option>
+<option value="48">48</option>
+<option value="49">49</option>
+<option value="50">50</option>
+<option value="51">51</option>
+<option value="52">52</option>
+<option value="53">53</option>
+<option value="54">54</option>
+<option value="55">55</option>
+<option value="56">56</option>
+<option value="57">57</option>
+<option value="58">58</option>
+<option value="59">59</option>
+</select>
+
+Accepts a datetime or a string:
+
+>>> w.render('mydate', datetime.time(15, 45, 15)) == w.render('mydate', '15:45:15')
+True
+
+Using a SelectDateWidget in a form:
+
+>>> class GetTime(Form):
+...     mytime = TimeField(widget=SelectTimeWidget)
+>>> a = GetTime({'mytime_hour':'15', 'mytime_minute':'45', 'mytime_second':'15'})
+>>> print a.is_valid()
+True
+>>> print a.cleaned_data['mytime']
+15:45:15
+
+As with any widget that implements get_value_from_datadict,
+we must be prepared to accept the input from the "as_hidden"
+rendering as well.
+
+>>> print a['mytime'].as_hidden()
+<input type="hidden" name="mytime" value="15:45:15" id="id_mytime" />
+>>> b=GetTime({'mytime':'15:45:15'})
+>>> print b.is_valid()
+True
+>>> print b.cleaned_data['mytime']
+15:45:15
+
+
 # MultiWidget and MultiValueField #############################################
 # MultiWidgets are widgets composed of other widgets. They are usually
 # combined with MultiValueFields - a field that is composed of other fields.
