SVN: r23720 - trunk/quixote/form2

Neil Schemenauer <nascheme-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]> Wed, 17 Mar 2004 11:45:59 -0500
Newsgroups gmane.comp.web.quixote.cvs
Message-ID <[email protected]>
Author: nascheme
Date: 2004-03-17 11:45:59 -0500 (Wed, 17 Mar 2004)
New Revision: 23720

Modified:
   trunk/quixote/form2/__init__.py
   trunk/quixote/form2/form.py
   trunk/quixote/form2/widget.py
Log:
* Remove FormComponent and related machinery.  It's simpler to add
  'title', 'hint', and 'required' as optional Widget attributes.

* Allow widgets to control how they are rendered within forms (by
  overriding the form_render() method).

* Reorganize rendering code in the form module so that it is easier
  to subclass form and provide a different look.

* Change the parsing behavior of widgets.  They no longer parse
  themselves when __init__ is called.  That causes lots of other
  problems.  Instead, each widget has a parse() method that calls
  _parse(), if it hasn't been called already, and returns the widgets
  value.  Change Form.has_errors() to call parse() on all the widgets
  in the form.

* Allow Widget._parse() to raise WidgetValueError in order to signal
  a parsing error.  Using an exception is often more convenient than
  setting Widget.error within the _parse() method.

* Add a CompositeWidget class.  It has proven to be useful when
  creating widgets that are composed of lower-level widgets.

* Remove Widget.get_value() since parse() seems to be sufficient.


Modified: trunk/quixote/form2/__init__.py
===================================================================
--- trunk/quixote/form2/__init__.py	2004-03-17 15:19:23 UTC (rev 23719)
+++ trunk/quixote/form2/__init__.py	2004-03-17 16:45:59 UTC (rev 23720)
@@ -6,14 +6,13 @@
 Application developers will typically create a Form instance each
 form in their application; each form object will contain a number
 of widget objects.  Custom widgets can be created by inheriting
-and/or composing the standard widget classes.  More complicated components
-of forms can be built by inheriting from the FormComponent class.
+and/or composing the standard widget classes.
 """
 
-from quixote.form2.form import Form, FormComponent, FormTokenWidget, \
-    WidgetRow, WidgetList
+from quixote.form2.form import Form, FormTokenWidget
 from quixote.form2.widget import Widget, StringWidget, FileWidget, \
     PasswordWidget, TextWidget, CheckboxWidget, RadiobuttonsWidget, \
     SingleSelectWidget, SelectWidget, OptionSelectWidget, \
     MultipleSelectWidget, SubmitWidget, HiddenWidget, \
-    FloatWidget, IntWidget, subname
+    FloatWidget, IntWidget, subname, WidgetValueError, CompositeWidget, \
+    WidgetList

Modified: trunk/quixote/form2/form.py
===================================================================
--- trunk/quixote/form2/form.py	2004-03-17 15:19:23 UTC (rev 23719)
+++ trunk/quixote/form2/form.py	2004-03-17 16:45:59 UTC (rev 23720)
@@ -5,11 +5,12 @@
 way of building HTML forms that are composed of Widget objects.
 """
 
-from types import StringType, ListType
 from quixote import get_request, get_session, get_publisher
 from quixote.html import url_quote, htmltag, htmltext, nl2br, TemplateIO
-from quixote.form2.widget import Widget, HiddenWidget, StringWidget, \
-    ButtonWidget, ResetWidget, SubmitWidget, subname
+from quixote.form2.widget import HiddenWidget, StringWidget, TextWidget, \
+    CheckboxWidget, SingleSelectWidget, RadiobuttonsWidget, \
+    MultipleSelectWidget, ResetWidget, SubmitWidget, FloatWidget, \
+    IntWidget
 
 
 try:
@@ -21,226 +22,30 @@
         return not not v
 
 
-class FormComponent:
-    """Part of a form. Generally contains one or more than one widgets.
-    The standard Form class renders components inside a 'table' tag.
-    Some sites may wish to provide their own Form.render() and WidgetRow
-    in order to get a different look.
-
-    The purpose of this class is to document the FormComponent interface.
-
-    Instance attributes: none
-    """
-
-    def __init__(self, name, *args, **kwargs):
-        raise NotImplementedError, 'subclass must implement'
-
-    def get_value(self):
-        raise NotImplementedError, 'subclass must implement'
-
-    def set_error(self, error):
-        raise NotImplementedError, 'subclass must implement'
-
-    def has_errors(self):
-        raise NotImplementedError, 'subclass must implement'
-
-    def render(self):
-        raise NotImplementedError, 'subclass must implement'
-
-
-def render_error(error):
-    if error:
-        return htmltext('<font color="red">%s</font><br />') % nl2br(error)
-    else:
-        return ''
-
-
-def render_hint(hint):
-    if hint:
-        return htmltext('<em>%s</em>') % hint
-    else:
-        return ''
-
-
-class WidgetRow(FormComponent):
-    """
-    Standard wrapper for widgets added to a Form: renders each widget in
-    a couple of table rows (one for the widget title, and another for
-    the widget itself, along with error and/or hint text).
-
-    Instance attribues:
-        widget : Widget
-        title : string
-        hint : string
-        required : bool
-    """
-
-    def __init__(self, name, widget_class, value, title=None, hint=None,
-                 required=False, **kwargs):
-        self.widget = widget_class(name, value, **kwargs)
-        self.title = title
-        self.hint = hint
-        self.required = required
-        if required and get_request().form:
-            if self.get_value() is None and not self.has_errors():
-                self.set_error('value is required')
-
-    def get_value(self):
-        return self.widget.get_value()
-
-    def set_error(self, error):
-        self.widget.set_error(error)
-
-    def has_errors(self):
-        return bool(self.widget.get_error())
-
-    def _render_error(self):
-        return render_error(self.widget.get_error())
-
-    def _render_hint(self):
-        return render_hint(self.hint)
-
-    def render(self):
-        title = self.title or ''
-        if title and self.required:
-            title = title + htmltext('&nbsp;*')
-        r = TemplateIO(html=True)
-        r += htmltext('<tr><th colspan="3" align="left">')
-        r += title
-        r += htmltext('</th></tr>'
-                      '<tr><td>&nbsp;&nbsp;</td><td>')
-        r += self.widget.render()
-        r += htmltext('</td><td>')
-        r += self._render_error()
-        r += self._render_hint()
-        r += htmltext('</td></tr>')
-        return r.getvalue()
-
-
-class WidgetList(FormComponent):
-    """A variable length list of widgets.
-
-    Instance attributes:
-      value : [any]
-      error : string
-    """
-
-    def __init__(self,
-                 name,
-                 element_type=StringWidget,
-                 value=None,
-                 title=None,
-                 hint=None,
-                 element_name="row",
-                 **args):
-        assert value is None or type(value) is ListType, (
-            "value '%s' not a list: got %r" % (name, value))
-        assert type(element_name) in (StringType, htmltext), (
-            "value '%s' element_name not a string: "
-            "got %r" % (name, element_name))
-
-        self.element_type = element_type
-        self.title = title
-        self.hint = hint
-        self.error = None
-
-        self.added_elements = HiddenWidget(subname(name, "added_elements"))
-        self.add_button = SubmitWidget(subname(name, "add_element"),
-                                       value="Add %s" % element_name)
-        self.elements = []
-        def add_element(value=None):
-            self.elements.append(
-                element_type(subname(name, "element%d" % len(self.elements)),
-                             value=value, **args))
-        if value is not None:
-            for element_value in value:
-                add_element(element_value)
-        num_added = int(self.added_elements.get_value() or 1)
-        for i in range(num_added):
-            add_element()
-        if self.add_button.get_value():
-            add_element()
-            num_added += 1
-        self.added_elements.set_value(num_added)
-        request = get_request()
-        if request.form:
-            self.value = []
-            for element in self.elements:
-                value = element.get_value()
-                if value is not None:
-                    self.value.append(value)
-            self.value = value or None
-        else:
-            self.value = value
-
-    def get_value(self):
-        return self.value
-
-    def set_error(self, error):
-        self.error = error
-
-    def has_errors(self):
-        if self.error:
-            return True
-        for element in self.elements:
-            if element.get_error():
-                return True
-        return False
-
-    def _render_element(self, element, hint=None, error=None):
-        r = TemplateIO(html=True)
-        r += htmltext('<tr><td>&nbsp;&nbsp;</td><td>')
-        r += element.render()
-        r += htmltext('</td><td>')
-        r += render_error(element.get_error() or error)
-        r += render_hint(hint)
-        r += htmltext('</td></tr>\n')
-        return r.getvalue()
-
-    def render(self):
-        r = TemplateIO(html=True)
-        r += htmltext('\n<tr><th colspan="3" align="left">')
-        r += self.title or htmltext('&nbsp;')
-        r += htmltext('</th></tr>')
-        r += self._render_element(self.elements[0], self.hint, self.error)
-        for element in self.elements[1:]:
-            r += self._render_element(element)
-        r += htmltext('<tr><td>&nbsp;</td><td>')
-        r += self.add_button.render()
-        r += self.added_elements.render()
-        r += htmltext('</td></tr>')
-        return r.getvalue()
-
-
 class FormTokenWidget(HiddenWidget):
     def render(self):
         self.value = get_session().create_form_token()
         return HiddenWidget.render(self)
 
 
-JAVASCRIPT_MARKUP = htmltext('''\
-<script type="text/javascript">
-<!--
-%s
-// -->
-</script>
-''')
-
-
 class Form:
     # XXX needs a nice docstring that explains typical usage!
     """
     Instance attributes:
-      components : [FormComponent]
-      hidden_widgets = [HiddenWidget]
-      button_widgets = [ButtonWidget]
-      _names : { name:string : Widget|FormComponent }
-        names used in the form
+      widgets : [Widget]
+      submit_widgets : [SubmitWidget]
+      hidden_widgets : [HiddenWidget]
+      _names : { name:string : Widget }
+        names used in the form and the widgets associated with them
     """
 
     TOKEN_NAME = "_form_id" # name of hidden token widget
 
-    COMPONENT_CLASS = WidgetRow
+    JAVASCRIPT_MARKUP = htmltext('<script type="text/javascript">\n'
+                                 '<!--\n'
+                                 '%s\n'
+                                 '// -->p\n'
+                                 '</script>\n')
 
     def __init__(self,
                  name=None,
@@ -256,9 +61,9 @@
         self.name = name
         self.method = method
         self.action_url = action_url or self._get_default_action_url()
-        self.components = []
+        self.widgets = []
+        self.submit_widgets = []
         self.hidden_widgets = []
-        self.button_widgets = []
         self._names = {}
 
         if enctype is not None and enctype not in (
@@ -274,10 +79,9 @@
             if config.form_tokens:
                 # unique token for each form, this prevents many cross-site
                 # attacks and prevents a form from being submitted twice
-                self.add_hidden(self.TOKEN_NAME, None, FormTokenWidget)
+                self.add(FormTokenWidget, self.TOKEN_NAME, value=None)
                 self.use_form_tokens = True
 
-        self.component_class = component_class or self.COMPONENT_CLASS
 
     def _get_default_action_url(self):
         request = get_request()
@@ -287,57 +91,69 @@
             action_url += "?" + query
         return action_url
 
+    # -- Form data access methods --------------------------------------
+
     def __getitem__(self, name):
         """(name:string) -> any
-        Return a component's or widget's value.
+        Return a widget's value.
         """
         try:
-            return self._names[name].get_value()
+            return self._names[name].parse()
         except KeyError:
-            raise KeyError, 'no widget or component named %r' % name
+            raise KeyError, 'no widget named %r' % name
 
     def get(self, name):
-        component = self._names.get(name)
-        if component:
-            return component.get_value()
+        widget = self._names.get(name)
+        if widget is not None:
+            return widget.parse()
         else:
             return None
 
     def get_widget(self, name):
         """(name:string) -> Widget | None
         """
-        obj = self._names.get(name)
-        if isinstance(obj, WidgetRow):
-            return obj.widget
-        elif isinstance(obj, Widget):
-            return obj
-        return None
+        return self._names.get(name)
 
+    def get_submit_widgets(self):
+        return self.submit_widgets
+
+    def get_all_widgets(self):
+        return self._names.values()
+
+    # -- Form processing and error checking ----------------------------
+
     def is_submitted(self):
         """() -> bool
 
         Return true if a form was submitted.
         """
-        return bool(get_request().form)
+        request = get_request()
+        if self.method == 'post':
+            if request.get_method() == 'POST':
+                return True
+            else:
+                return False
+        else:
+            return bool(request.form)
 
-    def set_error(self, name, error):
-        widget = self._names.get(name)
-        if not widget:
-            raise KeyError, "unknown name %r" % name
-        widget.set_error(error)
-
     def has_errors(self):
         """() -> bool
 
-        Return true if form has errors.
+        Ensure that all components of the form have parsed themselves. Return
+        true if any of them have errors.
         """
-        for component in self.components:
-            if component.has_errors():
-                return True
+        request = get_request()
+        if request.form:
+            for widget in self.get_all_widgets():
+                widget.parse(request)
+            for widget in self.get_all_widgets():
+                if widget.get_error():
+                    return True
         return False
 
-    def get_buttons(self):
-        return self.button_widgets
+    def clear_errors(self):
+        for widget in self.get_all_widgets():
+            widget.set_error(None)
 
     def get_submit(self):
         """() -> string | bool
@@ -348,73 +164,69 @@
 
         """
         request = get_request()
-        for button in self.button_widgets:
-            if request.form.has_key(button.name):
+        for button in self.submit_widgets:
+            if button.parse(request):
                 return button.name
         else:
-            if request.form:
+            if self.is_submitted():
                 return True
             else:
                 return False
 
+    def set_error(self, name, error):
+        widget = self._names.get(name)
+        if not widget:
+            raise KeyError, "unknown name %r" % name
+        widget.set_error(error)
+
     # -- Form population methods ---------------------------------------
 
-    def _add_name(self, name, obj):
+    def add(self, widget_class, name, *args, **kwargs):
         if self._names.has_key(name):
-            raise ValueError, "form already has '%s' variable" % name
-        self._names[name] = obj
+            raise ValueError, "form already has '%s' widget" % name
+        widget = widget_class(name, *args, **kwargs)
+        self._names[name] = widget
+        if isinstance(widget, SubmitWidget):
+            self.submit_widgets.append(widget) # will be rendered at end
+        elif isinstance(widget, HiddenWidget):
+            self.hidden_widgets.append(widget) # will be render at beginning
+        else:
+            self.widgets.append(widget)
 
-    def add_component(self, component_class, name, *args, **kwargs):
-        """(FormComponent, name : string, ...)
+    def add_submit(self, name, value=None, **kwargs):
+        self.add(SubmitWidget, name, value, **kwargs)
 
-        Add a form component object to the form.
-        """
-        if not issubclass(component_class, FormComponent):
-            raise TypeError('FormComponent subclass required (got %r)' %
-                            component_class)
-        component = component_class(name, *args, **kwargs)
-        self._add_name(name, component)
-        self.components.append(component)
+    def add_reset(self, name, value=None, **kwargs):
+        self.add(ResetWidget, name, value, **kwargs)
 
-    def add(self, widget_class, name, value=None,
-             title=None, hint=None, required=False, **kwargs):
-        """(Widget,
-            name : string,
-            value : any = None,
-            title : string = None,
-            hint : string = None,
-            required : boolean = False,
-            ...)
+    def add_hidden(self, name, value=None, **kwargs):
+        self.add(HiddenWidget, name, value, **kwargs)
 
-        Create a new widget and add it to the form.  The expected type of
-        'value' also depends on the widget class.  Any extra keyword args are
-        passed to the constructor method.
-        """
-        if issubclass(widget_class, HiddenWidget):
-            raise TypeError, "use add_hidden() to add hidden widgets"
-        self.add_component(self.component_class, name, widget_class, value,
-                           title, hint, required, **kwargs)
+    def add_string(self, name, value=None, **kwargs):
+        self.add(StringWidget, name, value, **kwargs)
 
+    def add_text(self, name, value=None, **kwargs):
+        self.add(TextWidget, name, value, **kwargs)
 
-    def _add_button_widget(self, klass, name, value, **kwargs):
-        widget = klass(name, value, **kwargs)
-        self._add_name(name, widget)
-        self.button_widgets.append(widget)
+    def add_checkbox(self, name, value=None, **kwargs):
+        self.add(CheckboxWidget, name, value, **kwargs)
 
-    def add_button(self, name, value, **kwargs):
-        self._add_button_widget(ButtonWidget, name, value, **kwargs)
+    def add_single_select(self, name, value=None, **kwargs):
+        self.add(SingleSelectWidget, name, value, **kwargs)
 
-    def add_reset(self, name, value, **kwargs):
-        self._add_button_widget(ResetWidget, name, value, **kwargs)
+    def add_multiple_select(self, name, value=None, **kwargs):
+        self.add(MultipleSelectWidget, name, value, **kwargs)
 
-    def add_submit(self, name, value, **kwargs):
-        self._add_button_widget(SubmitWidget, name, value, **kwargs)
+    def add_radiobutton(self, name, value=None, **kwargs):
+        self.add(RadiobuttonsWidget, name, value, **kwargs)
 
-    def add_hidden(self, name, value, klass=HiddenWidget):
-        widget = klass(name, value)
-        self._add_name(name, widget)
-        self.hidden_widgets.append(widget)
+    def add_float(self, name, value=None, **kwargs):
+        self.add(FloatWidget, name, value, **kwargs)
 
+    def add_int(self, name, value=None, **kwargs):
+        self.add(IntWidget, name, value, **kwargs)
+
+
     # -- Layout (rendering) methods ------------------------------------
 
     def render(self):
@@ -437,7 +249,9 @@
     def _render_finish(self):
         r = TemplateIO(html=True)
         r += htmltext('</form>')
-        r += self._render_javascript()
+        code = get_request().response.javascript_code
+        if code:
+            r += self._render_javascript(code)
         return r.getvalue()
 
     def _render_sep(self, text, line=True):
@@ -445,61 +259,82 @@
                         '</big></strong></td></tr>') % \
                                       (line and htmltext('<hr>') or '', text)
 
+    def _render_widgets(self):
+        r = TemplateIO(html=True)
+        for widget in self.widgets:
+            r += widget.form_render(self)
+        return r.getvalue()
+            
     def _render_hidden_widgets(self):
         r = TemplateIO(html=True)
         for widget in self.hidden_widgets:
             r += widget.render()
         return r.getvalue()
 
-    def _render_button_widgets(self, ncols=3):
+    def _render_submit_widgets(self, ncols=3):
         r = TemplateIO(html=True)
         r += htmltext('<tr><td colspan="%d">\n') % ncols
-        for button in self.button_widgets:
-            r += button.render()
+        for widget in self.submit_widgets:
+            r += widget.render()
         r += htmltext('</td></tr>')
         return r.getvalue()
 
-    def _render_components(self):
+    def _render_error_notice(self):
+        return htmltext('<tr><td colspan="3">'
+                        '<font color="red"><strong>Warning:</strong></font> '
+                        'there were errors processing your form.  '
+                        'See below for details.'
+                        '</td></tr>')
+
+    def render_error(self, error):
+        return htmltext('<font color="red">%s</font><br />') % nl2br(error)
+
+    def render_hint(self, hint):
+        return htmltext('<em>%s</em>') % hint
+
+    def render_field(self, title, contents, hint, error, required=False):
+        title = title or ''
+        if title and required:
+            title = title + htmltext('&nbsp;*')
         r = TemplateIO(html=True)
-        for component in self.components:
-            r += component.render()
+        r += htmltext('<tr><th colspan="3" align="left">')
+        r += title
+        r += htmltext('</th></tr>'
+                      '<tr><td>&nbsp;&nbsp;</td><td>')
+        r += contents
+        r += htmltext('</td><td>')
+        if error:
+            r += self.render_error(error)
+        if hint:
+            r += self.render_hint(hint)
+        r += htmltext('</td></tr>')
         return r.getvalue()
 
-    def _render_error_notice(self):
-        if self.has_errors():
-            r = htmltext('<tr><td colspan="3">'
-                         '<font color="red"><strong>Warning:</strong></font> '
-                         'there were errors processing your form.  '
-                         'See below for details.'
-                         '</td></tr>')
+    def _render_javascript(self, javascript_code):
+        """Render javacript code for the form.  Insert code lexically
+        sorted by code_id.
+        """
+        form_code = []
+        code_ids = javascript_code.keys()
+        code_ids.sort()
+        for code_id in code_ids:
+            code = javascript_code[code_id]
+            if code:
+                form_code.append(code)
+                javascript_code[code_id] = ''
+        if form_code:
+            return self.JAVASCRIPT_MARKUP % htmltext(''.join(form_code))
         else:
-            r = ''
-        return r
+            return ''
 
     def _render_body(self):
         r = TemplateIO(html=True)
         r += htmltext('<table>')
-        r += self._render_error_notice()
-        r += self._render_components()
-        r += self._render_button_widgets()
+        if self.has_errors():
+            r += self._render_error_notice()
+        r += self._render_widgets()
+        r += self._render_submit_widgets()
         r += htmltext('</table>')
         return r.getvalue()
 
-    def _render_javascript(self):
-        """Render javacript code for the form, if any.
-           Insert code lexically sorted by code_id
-        """
-        javascript_code = get_request().response.javascript_code
-        if javascript_code:
-            form_code = []
-            code_ids = javascript_code.keys()
-            code_ids.sort()
-            for code_id in code_ids:
-                code = javascript_code[code_id]
-                if code:
-                    form_code.append(code)
-                    javascript_code[code_id] = ''
-            if form_code:
-                return JAVASCRIPT_MARKUP % htmltext(''.join(form_code))
-        return ''
 

Modified: trunk/quixote/form2/widget.py
===================================================================
--- trunk/quixote/form2/widget.py	2004-03-17 15:19:23 UTC (rev 23719)
+++ trunk/quixote/form2/widget.py	2004-03-17 16:45:59 UTC (rev 23720)
@@ -8,7 +8,7 @@
 import struct
 from types import FloatType, IntType, ListType, StringType, TupleType
 from quixote import get_request
-from quixote.html import htmltext, htmlescape, htmltag, ValuelessAttr
+from quixote.html import htmltext, htmlescape, htmltag, TemplateIO
 from quixote.upload import Upload
 
 try:
@@ -40,6 +40,16 @@
     return attrs
 
 
+class WidgetValueError(Exception):
+    """May be raised a widget has problems parsing its value."""
+
+    def __init__(self, msg):
+        self.msg = msg
+
+    def __str__(self):
+        return str(self.msg)
+
+
 class Widget:
     """Abstract base class for web widgets.
 
@@ -47,22 +57,27 @@
       name : string
       value : any
       error : string
+      title : string
+      hint : string
+      required : bool
       attrs : {string: any}
+      _parsed : bool
 
     Feel free to access these directly; to set them, use the 'set_*()'
     modifier methods.
     """
 
-    def __init__(self, name, value=None, attrs=None, **kwattrs):
+    def __init__(self, name, value=None, title="", hint="", required=False,
+                 attrs=None, **kwattrs):
         assert self.__class__ is not Widget, "abstract class"
         self.name = name
+        self.value = value
         self.error = None
+        self.title = title
+        self.hint = hint
+        self.required = required
         self.attrs = merge_attrs(attrs, kwattrs)
-        request = get_request()
-        if request.form:
-            self._parse(request)
-        else:
-            self.set_value(value)
+        self._parsed = False
 
     def __repr__(self):
         return "<%s at %x: %s>" % (self.__class__.__name__,
@@ -75,12 +90,6 @@
     def get_name(self):
         return self.name
 
-    def set_name(self, name):
-        self.name = name
-
-    def get_value(self):
-        return self.value
-
     def set_value(self, value):
         self.value = value
 
@@ -90,6 +99,32 @@
     def get_error(self):
         return self.error
 
+    def get_title(self):
+        return self.title
+
+    def get_hint(self):
+        return self.hint
+
+    def is_required(self):
+        return self.required
+
+    def _is_submitted(self, request):
+        return True # subclass should override if it actually knows
+
+    def parse(self, request=None):
+        if not self._parsed:
+            if request is None:
+                request = get_request()
+            if self._is_submitted(request):
+                try:
+                    self._parse(request)
+                except WidgetValueError, exc:
+                    self.set_error(str(exc))
+                if self.required and self.value is None and not self.error:
+                    self.set_error('value is required')
+            self._parsed = True
+        return self.value
+
     def _parse(self, request):
         # subclasses may override but this is not part of the public API
         value = request.form.get(self.name)
@@ -102,7 +137,15 @@
         """render() -> HTML text"""
         raise NotImplementedError, 'subclass must implement'
 
+    def form_render(self, form):
+        return form.render_field(self.title,
+                                 self.render(),
+                                 self.hint,
+                                 self.error,
+                                 self.required)
 
+
+
 # class Widget
 
 # -- Fundamental widget types ------------------------------------------
@@ -129,6 +172,9 @@
     # This lets PasswordWidget be a trivial subclass
     HTML_TYPE = "text"
 
+    def _is_submitted(self, request):
+        return request.form.has_key(self.name)
+
     def render(self):
         return htmltag("input", xml_end=True,
                        type=self.HTML_TYPE,
@@ -171,6 +217,9 @@
       value : string
     """
 
+    def _is_submitted(self, request):
+        return request.form.has_key(self.name)
+
     def _parse(self, request):
         Widget._parse(self, request)
         if self.value and self.value.find("\r\n") >= 0:
@@ -199,7 +248,7 @@
                        type="checkbox",
                        name=self.name,
                        value="yes",
-                       checked=self.value and ValuelessAttr or None,
+                       checked=self.value and "checked" or None,
                        **self.attrs)
 
 
@@ -218,14 +267,14 @@
     """
 
     def __init__(self, name, value=None, options=None, sort=True,
-                 verify_selection=True, attrs=None, **kwattrs):
+                 verify_selection=True, **kwargs):
         assert self.__class__ is not SelectWidget, "abstract class"
+        Widget.__init__(self, name, value, **kwargs)
         self.options = []
         if options is not None:
             assert options, 'cannot pass empty options list'
             self.set_options(options, sort)
         self.verify_selection = verify_selection
-        Widget.__init__(self, name, value, attrs=attrs, **kwattrs)
 
     def get_allowed_values(self):
         return [item[0] for item in self.options]
@@ -359,7 +408,7 @@
         tags = [htmltag("select", name=self.name, **self.attrs)]
         for object, description, key in self.options:
             if self.is_selected(object):
-                selected = ValuelessAttr
+                selected = 'selected'
             else:
                 selected = None
             if description is None:
@@ -376,7 +425,16 @@
 
     SELECT_TYPE = "single_select"
 
+    def _is_submitted(self, request):
+        for value, description, key in self.options:
+            if value is None:
+                # empty item is in list so we don't know
+                return True
+        else:
+            return request.form.has_key(self.name)
+
     def _parse(self, request):
+        print 'parsing SingleSelectWidget', self.value
         parsed_key = request.form.get(self.name)
         if parsed_key:
             if type(parsed_key) is ListType:
@@ -400,11 +458,9 @@
 
     SELECT_TYPE = "radiobuttons"
 
-    def __init__(self, name, value=None,
-                 options=None,
-                 delim=None, attrs=None, **kwattrs):
-        SingleSelectWidget.__init__(self, name, value, options,
-                                    attrs=attrs, **kwattrs)
+    def __init__(self, name, value=None, options=None, delim=None, **kwargs):
+        SingleSelectWidget.__init__(self, name, value, options=options,
+                                    **kwargs)
         if delim is None:
             self.delim = "\n"
         else:
@@ -414,7 +470,7 @@
         tags = []
         for object, description, key in self.options:
             if self.is_selected(object):
-                checked = ValuelessAttr
+                checked = 'checked'
             else:
                 checked = None
             r = htmltag("input",
@@ -439,7 +495,7 @@
     SELECT_TYPE = "multiple_select"
 
     def __init__(self, name, value=None, options=None, **kwargs):
-        SelectWidget.__init__(self, name, value, options,
+        SelectWidget.__init__(self, name, value, options=options,
                               multiple='multiple', **kwargs)
 
     def set_value(self, value):
@@ -481,25 +537,18 @@
 
     HTML_TYPE = "button"
 
-    def __init__(self, name, value=None, attrs=None, **kwattrs):
-        self.name = name
-        self.error = None
-        self.attrs = merge_attrs(attrs, kwattrs)
-        # slightly different behavior here, we always render the
-        # tag using the 'value' passed in as a parameter.  The 'value'
-        # attribute is a boolean that is true if the button's name appears
-        # in the request.
+    def __init__(self, name, value=None, **kwargs):
+        Widget.__init__(self, name, value, **kwargs)
         self.label = value
-        request = get_request()
-        if request.form:
-            self._parse(request)
-        else:
-            self.value = False
 
     def set_error(self, error):
         return TypeError, 'error not allowed on submit buttons'
 
     def render(self):
+        # slightly different behavior here, we always render the
+        # tag using the 'value' passed in as a parameter.  'self.value'
+        # is a boolean that is true if the button's name appears
+        # in the request.
         value = (self.label and htmlescape(self.label) or None)
         return htmltag("input", xml_end=True, type=self.HTML_TYPE,
                        name=self.name, value=value, **self.attrs)
@@ -508,14 +557,13 @@
         self.value = request.form.has_key(self.name)
 
 
-class ResetWidget(ButtonWidget):
-    HTML_TYPE = "reset"
-
-
 class SubmitWidget(ButtonWidget):
     HTML_TYPE = "submit"
 
+class ResetWidget(SubmitWidget):
+    HTML_TYPE = "reset"
 
+
 class HiddenWidget(Widget):
     """
     Instance attributes:
@@ -552,13 +600,13 @@
     TYPE_ERROR = None                   # human-readable error message
     TYPE_CONVERTER = None               # eg. int(), float()
 
-    def __init__(self, name, value=None, attrs=None, **kwattrs):
+    def __init__(self, name, value=None, **kwargs):
         assert self.__class__ is not NumberWidget, "abstract class"
         assert value is None or type(value) is self.TYPE_OBJECT, (
             "form value '%s' not a %s: got %r" % (name,
                                                   self.TYPE_OBJECT,
                                                   value))
-        StringWidget.__init__(self, name, value, attrs=attrs, **kwattrs)
+        StringWidget.__init__(self, name, value, **kwargs)
 
     def _parse(self, request):
         StringWidget._parse(self, request)
@@ -603,7 +651,7 @@
     SELECT_TYPE = "option_select"
 
     def __init__(self, name, value=None, options=None, **kwargs):
-        SingleSelectWidget.__init__(self, name, value, options,
+        SingleSelectWidget.__init__(self, name, value, options=options,
                                     onchange='submit()', **kwargs)
 
     def render(self):
@@ -612,3 +660,133 @@
                          '<input type="submit" name="" value="apply" />'
                          '</noscript>'))
 
+
+class CompositeWidget(Widget):
+    """
+    Instance attributes:
+      widgets : [Widget]
+      _names : {name:string : Widget}
+    """
+    def __init__(self, name, value=None, **kwargs):
+        Widget.__init__(self, name, value, **kwargs)
+        self.widgets = []
+        self._names = {}
+
+    def _is_submitted(self, request):
+        for widget in self.widgets:
+            if not widget._is_submitted(request):
+                return False
+        return True
+
+    def _parse(self, request):
+        for widget in self.widgets:
+            widget.parse(request)
+
+    def __getitem__(self, name):
+        return self._names[name].parse()
+        
+    def get(self, name):
+        widget = self._names.get(name)
+        if widget:
+            return widget.parse()
+        return None
+
+    def get_widget(self, name):
+        return self._names.get(name)
+
+    def get_widgets(self):
+        return self.widgets
+
+    def set_error(self, error):
+        self.error = error
+        if error is None:
+            for widget in self.widgets:
+                widget.set_error(None)
+
+    def set_widget_error(self, name, error):
+        self._names[name].set_error(error)
+
+    def get_error(self):
+        if self.error:
+            return self.error
+        else:
+            for widget in self.widgets:
+                error = widget.get_error()
+                if error:
+                    return error
+        
+    def add(self, widget_class, name, *args, **kwargs):
+        if self._names.has_key(name):
+            raise ValueError, 'the name %r is already used' % name
+        widget = widget_class(subname(self.name, name), *args, **kwargs)
+        self._names[name] = widget
+        self.widgets.append(widget)
+
+
+class WidgetList(CompositeWidget):
+    """A variable length list of widgets.  There is only one
+    title and hint but each element of the list can have its own
+    error.  You can also set an error on the WidgetList itself (e.g. as a
+    result of higher-level processing).
+
+    Instance attributes:
+      element_names : string
+    """
+
+    def __init__(self,
+                 name,
+                 element_type=StringWidget,
+                 value=None,
+                 element_name="row",
+                 title="",
+                 hint="",
+                 required=False,
+                 **kwargs):
+        assert value is None or type(value) is ListType, (
+            "value '%s' not a list: got %r" % (name, value))
+        assert type(element_name) in (StringType, htmltext), (
+            "value '%s' element_name not a string: "
+            "got %r" % (name, element_name))
+        CompositeWidget.__init__(self, name, value, title=title, hint=hint,
+                                 required=required)
+
+        self.element_names = []
+        def add_element(value=None):
+            name = "element%d" % len(self.element_names)
+            self.add(element_type, name, value=element_value)
+            self.element_names.append(name)
+        if value is not None:
+            for element_value in value:
+                add_element("element%d" % n, value=element_value)
+        self.add(HiddenWidget, "added_elements")
+        self.add(SubmitWidget, "add_element", value="Add %s" % element_name)
+        num_added = int(self["added_elements"] or 1)
+        for i in range(num_added):
+            add_element()
+        if self.add_button.parse():
+            add_element()
+            num_added += 1
+        self.added_elements.set_value(num_added)
+
+    def _parse(self, request):
+        values = []
+        for name in self.element_names:
+            value = element.parse(request)
+            if value is not None:
+                values.append(value)
+        self.value = values or None
+
+    def form_render(self, form):
+        r = TemplateIO(html=True)
+        widget = self.get_widget(self.element_names[0])
+        r += form.render_field(self.title,
+                               widget.render(),
+                               self.hint,
+                               self.error or widget.get_error(),
+                               self.required)
+        for name in self.element_names[1:]:
+            widget = self.get_widget(name)
+            r += form.render_field('', widget.render(), '', widget.get_error())
+        r += self.get_widget("add_button").render()
+        r += self.get_widget("added_elements").render()
+        return r.getvalue()