Author: adrian
Date: 2007-01-23 14:23:07 -0600 (Tue, 23 Jan 2007)
New Revision: 4403

Modified:
   django/trunk/django/newforms/fields.py
   django/trunk/django/newforms/widgets.py
   django/trunk/tests/regressiontests/forms/tests.py
Log:
newforms: Added MultiValueField, SplitDateTimeField, MultiWidget, 
SplitDateTimeWidget

Modified: django/trunk/django/newforms/fields.py
===================================================================
--- django/trunk/django/newforms/fields.py      2007-01-23 16:46:40 UTC (rev 
4402)
+++ django/trunk/django/newforms/fields.py      2007-01-23 20:23:07 UTC (rev 
4403)
@@ -3,7 +3,7 @@
 """
 
 from django.utils.translation import gettext
-from util import ValidationError, smart_unicode
+from util import ErrorList, ValidationError, smart_unicode
 from widgets import TextInput, PasswordInput, HiddenInput, 
MultipleHiddenInput, CheckboxInput, Select, SelectMultiple
 import datetime
 import re
@@ -16,7 +16,8 @@
     'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField',
     'RegexField', 'EmailField', 'URLField', 'BooleanField',
     'ChoiceField', 'MultipleChoiceField',
-    'ComboField',
+    'ComboField', 'MultiValueField',
+    'SplitDateTimeField',
 )
 
 # These values, if given to to_python(), will trigger the self.required check.
@@ -376,6 +377,9 @@
         return new_value
 
 class ComboField(Field):
+    """
+    A Field whose clean() method calls multiple Field clean() methods.
+    """
     def __init__(self, fields=(), required=True, widget=None, label=None, 
initial=None):
         super(ComboField, self).__init__(required, widget, label, initial)
         # Set 'required' to False on the individual fields, because the
@@ -394,3 +398,84 @@
         for field in self.fields:
             value = field.clean(value)
         return value
+
+class MultiValueField(Field):
+    """
+    A Field that is composed of multiple Fields.
+
+    Its clean() method takes a "decompressed" list of values. Each value in
+    this list is cleaned by the corresponding field -- the first value is
+    cleaned by the first field, the second value is cleaned by the second
+    field, etc. Once all fields are cleaned, the list of clean values is
+    "compressed" into a single value.
+
+    Subclasses should implement compress(), which specifies how a list of
+    valid values should be converted to a single value. Subclasses should not
+    have to implement clean().
+
+    You'll probably want to use this with MultiWidget.
+    """
+    def __init__(self, fields=(), required=True, widget=None, label=None, 
initial=None):
+        super(MultiValueField, self).__init__(required, widget, label, initial)
+        # Set 'required' to False on the individual fields, because the
+        # required validation will be handled by MultiValueField, not by those
+        # individual fields.
+        for f in fields:
+            f.required = False
+        self.fields = fields
+
+    def clean(self, value):
+        """
+        Validates every value in the given list. A value is validated against
+        the corresponding Field in self.fields.
+
+        For example, if this MultiValueField was instantiated with
+        fields=(DateField(), TimeField()), clean() would call
+        DateField.clean(value[0]) and TimeField.clean(value[1]).
+        """
+        clean_data = []
+        errors = ErrorList()
+        if self.required and not value:
+            raise ValidationError(gettext(u'This field is required.'))
+        elif not self.required and not value:
+            return self.compress([])
+        if not isinstance(value, (list, tuple)):
+            raise ValidationError(gettext(u'Enter a list of values.'))
+        for i, field in enumerate(self.fields):
+            try:
+                field_value = value[i]
+            except KeyError:
+                field_value = None
+            if self.required and field_value in EMPTY_VALUES:
+                raise ValidationError(gettext(u'This field is required.'))
+            try:
+                clean_data.append(field.clean(field_value))
+            except ValidationError, e:
+                # Collect all validation errors in a single list, which we'll
+                # raise at the end of clean(), rather than raising a single
+                # exception for the first error we encounter.
+                errors.extend(e.messages)
+        if errors:
+            raise ValidationError(errors)
+        return self.compress(clean_data)
+
+    def compress(self, data_list):
+        """
+        Returns a single value for the given list of values. The values can be
+        assumed to be valid.
+
+        For example, if this MultiValueField was instantiated with
+        fields=(DateField(), TimeField()), this might return a datetime
+        object created by combining the date and time in data_list.
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+class SplitDateTimeField(MultiValueField):
+    def __init__(self, required=True, widget=None, label=None, initial=None):
+        fields = (DateField(), TimeField())
+        super(SplitDateTimeField, self).__init__(fields, required, widget, 
label, initial)
+
+    def compress(self, data_list):
+        if data_list:
+            return datetime.datetime.combine(*data_list)
+        return None

Modified: django/trunk/django/newforms/widgets.py
===================================================================
--- django/trunk/django/newforms/widgets.py     2007-01-23 16:46:40 UTC (rev 
4402)
+++ django/trunk/django/newforms/widgets.py     2007-01-23 20:23:07 UTC (rev 
4403)
@@ -6,6 +6,7 @@
     'Widget', 'TextInput', 'PasswordInput', 'HiddenInput', 
'MultipleHiddenInput',
     'FileInput', 'Textarea', 'CheckboxInput',
     'Select', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple',
+    'MultiWidget', 'SplitDateTimeWidget',
 )
 
 from util import flatatt, StrAndUnicode, smart_unicode
@@ -252,3 +253,66 @@
             id_ += '_0'
         return id_
     id_for_label = classmethod(id_for_label)
+
+class MultiWidget(Widget):
+    """
+    A widget that is composed of multiple widgets.
+
+    Its render() method takes a "decompressed" list of values, not a single
+    value. Each value in this list is rendered in the corresponding widget --
+    the first value is rendered in the first widget, the second value is
+    rendered in the second widget, etc.
+
+    Subclasses should implement decompress(), which specifies how a single
+    value should be converted to a list of values. Subclasses should not
+    have to implement clean().
+
+    Subclasses may implement format_output(), which takes the list of rendered
+    widgets and returns HTML that formats them any way you'd like.
+
+    You'll probably want to use this with MultiValueField.
+    """
+    def __init__(self, widgets, attrs=None):
+        self.widgets = [isinstance(w, type) and w() or w for w in widgets]
+        super(MultiWidget, self).__init__(attrs)
+
+    def render(self, name, value, attrs=None):
+        # value is a list of values, each corresponding to a widget
+        # in self.widgets.
+        if not isinstance(value, list):
+            value = self.decompress(value)
+        output = []
+        for i, widget in enumerate(self.widgets):
+            try:
+                widget_value = value[i]
+            except KeyError:
+                widget_value = None
+            output.append(widget.render(name + '_%s' % i, widget_value, attrs))
+        return self.format_output(output)
+
+    def value_from_datadict(self, data, name):
+        return [data.get(name + '_%s' % i) for i in range(len(self.widgets))]
+
+    def format_output(self, rendered_widgets):
+        return u''.join(rendered_widgets)
+
+    def decompress(self, value):
+        """
+        Returns a list of decompressed values for the given compressed value.
+        The given value can be assumed to be valid, but not necessarily
+        non-empty.
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+class SplitDateTimeWidget(MultiWidget):
+    """
+    A Widget that splits datetime input into two <input type="text"> boxes.
+    """
+    def __init__(self, attrs=None):
+        widgets = (TextInput(attrs=attrs), TextInput(attrs=attrs))
+        super(SplitDateTimeWidget, self).__init__(widgets, attrs)
+
+    def decompress(self, value):
+        if value:
+            return [value.date(), value.time()]
+        return [None, None]

Modified: django/trunk/tests/regressiontests/forms/tests.py
===================================================================
--- django/trunk/tests/regressiontests/forms/tests.py   2007-01-23 16:46:40 UTC 
(rev 4402)
+++ django/trunk/tests/regressiontests/forms/tests.py   2007-01-23 20:23:07 UTC 
(rev 4403)
@@ -684,6 +684,39 @@
 >>> w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), 
 >>> ('ćžšđ', 'abcćžšđ')])
 u'<ul>\n<li><label><input type="checkbox" name="nums" value="1" /> 
1</label></li>\n<li><label><input type="checkbox" name="nums" value="2" /> 
2</label></li>\n<li><label><input type="checkbox" name="nums" value="3" /> 
3</label></li>\n<li><label><input checked="checked" type="checkbox" name="nums" 
value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" /> 
\u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</label></li>\n<li><label><input
 type="checkbox" name="nums" value="\u0107\u017e\u0161\u0111" /> 
abc\u0107\u017e\u0161\u0111</label></li>\n</ul>'
 
+# MultiWidget #################################################################
+
+>>> class MyMultiWidget(MultiWidget):
+...     def decompress(self, value):
+...         if value:
+...             return value.split('__')
+...         return ['', '']
+...     def format_output(self, rendered_widgets):
+...         return u'<br />'.join(rendered_widgets)
+>>> w = MyMultiWidget(widgets=(TextInput(attrs={'class': 'big'}), 
TextInput(attrs={'class': 'small'})))
+>>> w.render('name', ['john', 'lennon'])
+u'<input type="text" class="big" value="john" name="name_0" /><br /><input 
type="text" class="small" value="lennon" name="name_1" />'
+>>> w.render('name', 'john__lennon')
+u'<input type="text" class="big" value="john" name="name_0" /><br /><input 
type="text" class="small" value="lennon" name="name_1" />'
+
+# SplitDateTimeWidget #########################################################
+
+>>> w = SplitDateTimeWidget()
+>>> w.render('date', '')
+u'<input type="text" name="date_0" /><input type="text" name="date_1" />'
+>>> w.render('date', None)
+u'<input type="text" name="date_0" /><input type="text" name="date_1" />'
+>>> w.render('date', datetime.datetime(2006, 1, 10, 7, 30))
+u'<input type="text" name="date_0" value="2006-01-10" /><input type="text" 
name="date_1" value="07:30:00" />'
+>>> w.render('date', [datetime.date(2006, 1, 10), datetime.time(7, 30)])
+u'<input type="text" name="date_0" value="2006-01-10" /><input type="text" 
name="date_1" value="07:30:00" />'
+
+You can also pass 'attrs' to the constructor. In this case, the attrs will be
+included on both widgets.
+>>> w = SplitDateTimeWidget(attrs={'class': 'pretty'})
+>>> w.render('date', datetime.datetime(2006, 1, 10, 7, 30))
+u'<input type="text" class="pretty" value="2006-01-10" name="date_0" /><input 
type="text" class="pretty" value="07:30:00" name="date_1" />'
+
 ##########
 # Fields #
 ##########
@@ -1536,6 +1569,58 @@
 >>> f.clean(None)
 u''
 
+# SplitDateTimeField ##########################################################
+
+>>> f = SplitDateTimeField()
+>>> f.clean([datetime.date(2006, 1, 10), datetime.time(7, 30)])
+datetime.datetime(2006, 1, 10, 7, 30)
+>>> f.clean(None)
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+>>> f.clean('hello')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a list of values.']
+>>> f.clean(['hello', 'there'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid date.', u'Enter a valid time.']
+>>> f.clean(['2006-01-10', 'there'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid time.']
+>>> f.clean(['hello', '07:30'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid date.']
+
+>>> f = SplitDateTimeField(required=False)
+>>> f.clean([datetime.date(2006, 1, 10), datetime.time(7, 30)])
+datetime.datetime(2006, 1, 10, 7, 30)
+>>> f.clean(None)
+>>> f.clean('')
+>>> f.clean('hello')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a list of values.']
+>>> f.clean(['hello', 'there'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid date.', u'Enter a valid time.']
+>>> f.clean(['2006-01-10', 'there'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid time.']
+>>> f.clean(['hello', '07:30'])
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid date.']
+
 #########
 # Forms #
 #########


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to