SVN: r22711 - in trunk/quixote: . form2
Neil Schemenauer <nascheme-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]> Wed, 08 Oct 2003 18:18:10 -0400
| Newsgroups | gmane.comp.web.quixote.cvs |
|---|---|
| Message-ID | <[email protected]> |
Author: nascheme
Date: 2003-10-08 18:18:10 -0400 (Wed, 08 Oct 2003)
New Revision: 22711
Added:
trunk/quixote/form2/
trunk/quixote/form2/__init__.py
trunk/quixote/form2/form.py
trunk/quixote/form2/widget.py
Log:
Implement next generation form framework.
Added: trunk/quixote/form2/__init__.py
===================================================================
--- trunk/quixote/form2/__init__.py 2003-10-08 21:55:48 UTC (rev 22710)
+++ trunk/quixote/form2/__init__.py 2003-10-08 22:18:10 UTC (rev 22711)
@@ -0,0 +1,19 @@
+"""$URL$
+$Id$
+
+The web interface framework, consisting of Form and Widget base classes
+(and a bunch of standard widget classes recognized by Form).
+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.
+"""
+
+from quixote.form2.form import Form, FormComponent, FormTokenWidget, \
+ WidgetList
+from quixote.form2.widget import Widget, StringWidget, FileWidget, \
+ PasswordWidget, TextWidget, CheckboxWidget, RadiobuttonsWidget, \
+ SingleSelectWidget, SelectWidget, OptionSelectWidget, \
+ MultipleSelectWidget, SubmitWidget, HiddenWidget, \
+ FloatWidget, IntWidget, subname
Added: trunk/quixote/form2/form.py
===================================================================
--- trunk/quixote/form2/form.py 2003-10-08 21:55:48 UTC (rev 22710)
+++ trunk/quixote/form2/form.py 2003-10-08 22:18:10 UTC (rev 22711)
@@ -0,0 +1,469 @@
+"""$URL$
+$Id$
+
+Provides the Form class and related classes. Forms are a convenient
+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, \
+ SubmitWidget, subname
+
+
+try:
+ True, False
+except NameError:
+ True = 1
+ False = 0
+
+
+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.
+
+ Instance attribues:
+ widget : Widget
+ title : string
+ hint : string
+ required : bool
+ """
+
+ def __init__(self, name, widget_class, value, title=None, hint=None,
+ required=False, **args):
+ self.widget = apply(widget_class, (name, value), args)
+ self.title = title
+ self.hint = hint
+ self.required = required
+ if required and get_request().form:
+ if self.get_value() is None and not self.get_error():
+ 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 _get_error(self):
+ error = self.widget.get_error()
+ if not error and self.required and self.get_value() is None:
+ error = 'value is required'
+ return error
+
+ def has_errors(self):
+ return self._get_error()
+
+ def _render_error(self):
+ return render_error(self._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(' *')
+ r = TemplateIO(html=True)
+ r += htmltext('<tr><th colspan="3" align="left">')
+ r += title
+ r += htmltext('</th></tr>'
+ '<tr><td> </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> </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(' ')
+ 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> </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]
+ submit_buttons = [SubmitWidget]
+ _names : { name:string : Widget|FormComponent }
+ names used in the form
+ """
+
+ TOKEN_NAME = "_form_id" # name of hidden token widget
+
+ WIDGET_ROW_CLASS = WidgetRow
+
+ def __init__(self, name=None, method="post", action_url=None,
+ enctype=None, use_tokens=True):
+
+ if method not in ("post", "get"):
+ raise ValueError("Form method must be 'post' or 'get', "
+ "not %r" % method)
+ self.name = name
+ self.method = method
+ self.action_url = action_url or self._get_default_action_url()
+ self.components = []
+ self.hidden_widgets = []
+ self.submit_buttons = []
+ self._names = {}
+
+ if enctype is not None and enctype not in (
+ "application/x-www-form-urlencoded", "multipart/form-data"):
+ raise ValueError, ("Form enctype must be "
+ "'application/x-www-form-urlencoded' or "
+ "'multipart/form-data', not %r" % enctype)
+ self.enctype = enctype
+
+ self.use_form_tokens = False
+ if use_tokens and self.method == "post":
+ config = get_publisher().config
+ 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.use_form_tokens = True
+
+ def _get_default_action_url(self):
+ request = get_request()
+ action_url = url_quote(request.get_path())
+ query = request.get_environ("QUERY_STRING")
+ if query:
+ action_url += "?" + query
+ return action_url
+
+ def __getitem__(self, name):
+ """(name) -> any
+ Return a component's or widget's value.
+ """
+ try:
+ return self._names[name].get_value()
+ except KeyError:
+ raise KeyError, 'no widget or component named %r' % name
+
+ def is_submitted(self):
+ """() -> bool
+
+ Return true if a form was submitted.
+ """
+ return len(get_request().form) > 0
+
+ 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.
+ """
+ for component in self.components:
+ if component.has_errors():
+ return True
+ return False
+
+ def get_submit(self):
+ """() -> string | bool
+
+ Get the name of the submit button that was used to submit the
+ current form. If the form is submitted but not by a button added by
+ add_submit() then return True. Otherwise, return False.
+
+ """
+ request = get_request()
+ for button in self.submit_buttons:
+ if request.form.has_key(button.name):
+ return button.name
+ else:
+ if request.form:
+ return True
+ else:
+ return False
+
+ # -- Form population methods ---------------------------------------
+
+ def _add_name(self, name, obj):
+ if self._names.has_key(name):
+ raise ValueError, "form already has '%s' variable" % name
+ self._names[name] = obj
+
+ def add_component(self, klass, name, *args, **kwargs):
+ """(FormComponent, name : string, ...)
+
+ Add a form component object to the form.
+ """
+ if not issubclass(klass, FormComponent):
+ raise TypeError, 'FormComponent subclass required (got %r)' % klass
+ component = apply(klass, (name,) + args, kwargs)
+ self._add_name(name, component)
+ self.components.append(component)
+
+ def add(self, klass, name, value=None,
+ title=None, hint=None, required=False, **args):
+ """(Widget,
+ name : string,
+ value : any = None,
+ title : string = None,
+ hint : string = None,
+ required : boolean = False,
+ ...)
+
+ 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(klass, HiddenWidget):
+ raise TypeError, "use add_hidden() to add hidden widgets"
+ apply(self.add_component, (self.WIDGET_ROW_CLASS, name, klass, value,
+ title, hint, required), args)
+
+
+ def add_submit(self, name, value):
+ widget = SubmitWidget(name, value)
+ self._add_name(name, widget)
+ self.submit_buttons.append(widget)
+
+ def add_hidden(self, name, value, klass=HiddenWidget):
+ widget = klass(name, value)
+ self._add_name(name, widget)
+ self.hidden_widgets.append(widget)
+
+ # -- Layout (rendering) methods ------------------------------------
+
+ def render(self):
+ """() -> HTML text
+ Render a form as HTML.
+ """
+ r = TemplateIO(html=True)
+ r += self._render_start()
+ r += self._render_body()
+ r += self._render_finish()
+ return r.getvalue()
+
+ def _render_start(self):
+ r = TemplateIO(html=True)
+ r += htmltag('form', name=self.name, method=self.method,
+ enctype=self.enctype, action=self.action_url)
+ r += self._render_hidden_widgets()
+ return r.getvalue()
+
+ def _render_finish(self):
+ r = TemplateIO(html=True)
+ r += htmltext('</form>')
+ r += self._render_javascript()
+ return r.getvalue()
+
+ def _render_sep(self, text, line=True):
+ return htmltext('<tr><td colspan="3">%s<strong><big>%s'
+ '</big></strong></td></tr>') % \
+ (line and htmltext('<hr>') or '', text)
+
+ def _render_hidden_widgets(self):
+ r = TemplateIO(html=True)
+ for widget in self.hidden_widgets:
+ r += widget.render()
+ return r.getvalue()
+
+ def _render_submit_buttons(self, ncols=3):
+ r = TemplateIO(html=True)
+ r += htmltext('<tr><td colspan="%d">\n') % ncols
+ for button in self.submit_buttons:
+ r += button.render()
+ r += htmltext('</td></tr>')
+ return r.getvalue()
+
+ def _render_components(self):
+ r = TemplateIO(html=True)
+ for component in self.components:
+ r += component.render()
+ 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>')
+ else:
+ r = ''
+ return r
+
+ def _render_body(self):
+ r = TemplateIO(html=True)
+ r += htmltext('<table>')
+ r += self._render_error_notice()
+ r += self._render_components()
+ r += self._render_submit_buttons()
+ 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 ''
+
Added: trunk/quixote/form2/widget.py
===================================================================
--- trunk/quixote/form2/widget.py 2003-10-08 21:55:48 UTC (rev 22710)
+++ trunk/quixote/form2/widget.py 2003-10-08 22:18:10 UTC (rev 22711)
@@ -0,0 +1,629 @@
+"""$URL$
+$Id$
+
+Provides the basic web widget classes: Widget itself, plus StringWidget,
+TextWidget, CheckboxWidget, etc.
+"""
+
+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.upload import Upload
+
+try:
+ True, False
+except NameError:
+ True = 1
+ False = 0
+
+
+def subname(prefix, name):
+ """Create a unique name for a sub-widget or sub-component."""
+ # $ is nice because it's valid as part of a Javascript identifier
+ return "%s$%s" % (prefix, name)
+
+
+class Widget:
+ """Abstract base class for web widgets.
+
+ Instance attributes:
+ name : string
+ value : any
+ error : string
+
+ Feel free to access these directly; to set them, use the 'set_*()'
+ modifier methods.
+ """
+
+ def __init__(self, name, value=None):
+ assert self.__class__ is not Widget, "abstract class"
+ self.name = name
+ self.error = None
+ request = get_request()
+ if request.form:
+ self._parse(request)
+ else:
+ self.set_value(value)
+
+ def __repr__(self):
+ return "<%s at %x: %s>" % (self.__class__.__name__,
+ id(self),
+ self.name)
+
+ def __str__(self):
+ return "%s: %s" % (self.__class__.__name__, self.name)
+
+ 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
+
+ def set_error(self, error):
+ self.error = error
+
+ def get_error(self):
+ return self.error
+
+ def _parse(self, request):
+ # subclasses may override but this is not part of the public API
+ value = request.form.get(self.name)
+ if type(value) is StringType and value.strip():
+ self.value = value
+ else:
+ self.value = None
+
+ def render(self):
+ """render() -> HTML text"""
+ raise NotImplementedError, 'subclass must implement'
+
+
+# class Widget
+
+# -- Fundamental widget types ------------------------------------------
+# These correspond to the standard types of input tag in HTML:
+# text StringWidget
+# password PasswordWidget
+# radio RadiobuttonWidget
+# checkbox CheckboxWidget
+#
+# and also to the other basic form elements:
+# <textarea> TextWidget
+# <select> SingleSelectWidget
+# <select multiple>
+# MultipleSelectWidget
+
+class StringWidget(Widget):
+ """Widget for entering a single string: corresponds to
+ '<input type="text">' in HTML.
+
+ Instance attributes:
+ value : string
+ size : int
+ maxlength : int
+ """
+
+ # This lets PasswordWidget be a trivial subclass
+ HTML_TYPE = "text"
+
+ def __init__(self, name, value=None,
+ size=None, maxlength=None):
+ Widget.__init__(self, name, value)
+ self.size = size
+ self.maxlength = maxlength
+
+ def render(self, **attributes):
+ return htmltag("input", xml_end=True,
+ type=self.HTML_TYPE,
+ name=self.name,
+ size=self.size,
+ maxlength=self.maxlength,
+ value=self.value,
+ **attributes)
+
+
+class FileWidget(StringWidget):
+ """Subclass of StringWidget for uploading files.
+
+ Instance attributes: none
+ """
+
+ HTML_TYPE = "file"
+
+ def _parse(self, request):
+ parsed_value = request.form.get(self.name)
+ if isinstance(value, Upload):
+ self.value = parsed_value
+ else:
+ self.value = None
+
+
+class PasswordWidget(StringWidget):
+ """Trivial subclass of StringWidget for entering passwords (different
+ widget type because HTML does it that way).
+
+ Instance attributes: none
+ """
+
+ HTML_TYPE = "password"
+
+
+class TextWidget(Widget):
+ """Widget for entering a long, multi-line string; corresponds to
+ the HTML "<textarea>" tag.
+
+ Instance attributes:
+ value : string
+ cols : int
+ rows : int
+ wrap : string
+ (see an HTML book for details on text widget wrap options)
+ """
+
+ def __init__(self, name, value=None, cols=None, rows=None, wrap=None):
+ Widget.__init__(self, name, value)
+ self.cols = cols
+ self.rows = rows
+ self.wrap = wrap
+
+ def _parse(self, request):
+ Widget._parse(self, request)
+ if self.value and self.value.find("\r\n") >= 0:
+ self.value = self.value.replace("\r\n", "\n")
+
+ def render(self):
+ return (htmltag("textarea", name=self.name,
+ cols=self.cols,
+ rows=self.rows,
+ wrap=self.wrap) +
+ htmlescape(self.value or "") +
+ htmltext("</textarea>"))
+
+
+class CheckboxWidget(Widget):
+ """Widget for a single checkbox: corresponds to "<input
+ type=checkbox>". Do not put multiple CheckboxWidgets with the same
+ name in the same form.
+
+ Instance attributes:
+ value : boolean
+ """
+
+ def _parse(self, request):
+ self.value = request.form.has_key(self.name)
+
+ def render(self):
+ return htmltag("input", xml_end=True,
+ type="checkbox",
+ name=self.name,
+ value="yes",
+ checked=self.value and ValuelessAttr or None)
+
+
+
+class SelectWidget(Widget):
+ """Widget for single or multiple selection; corresponds to
+ <select name=...>
+ <option value="Foo">Foo</option>
+ ...
+ </select>
+
+ Instance attributes:
+ options : [ (value:any, description:any, key:string) ]
+ value : any
+ The value is None or an element of dict(options.values()).
+ size : int
+ The number of options that should be presented without scrolling.
+ """
+
+ def __init__(self, name, value=None,
+ allowed_values=None,
+ descriptions=None,
+ options=None,
+ size=None,
+ sort=True,
+ verify_selection=True):
+ assert self.__class__ is not SelectWidget, "abstract class"
+ self.options = []
+ # if options passed, cannot pass allowed_values or descriptions
+ if allowed_values is not None:
+ assert options is None, (
+ 'cannot pass both allowed_values and options')
+ assert allowed_values, (
+ 'cannot pass empty allowed_values list')
+ self.set_allowed_values(allowed_values, descriptions, sort)
+ elif options is not None:
+ assert descriptions is None, (
+ 'cannot pass both options and descriptions')
+ assert options, (
+ 'cannot pass empty options list')
+ self.set_options(options, sort)
+ self.verify_selection = verify_selection
+ self.size = size
+ Widget.__init__(self, name, value)
+
+ def get_allowed_values(self):
+ return [item[0] for item in self.options]
+
+ def get_descriptions(self):
+ return [item[1] for item in self.options]
+
+ def set_value(self, value):
+ self.value = None
+ for object, description, key in self.options:
+ if value == object:
+ self.value = value
+ break
+
+ def _generate_keys(self, values, descriptions):
+ """Called if no keys were provided. Try to generate a set of keys
+ that will be consistent between rendering and parsing.
+ """
+ # try to use ZODB object IDs
+ keys = []
+ for value in values:
+ if value is None:
+ oid = ""
+ else:
+ oid = getattr(value, "_p_oid", None)
+ if not oid:
+ break
+ hi, lo = struct.unpack(">LL", oid)
+ oid = "%x" % ((hi << 32) | lo)
+ keys.append(oid)
+ else:
+ # found OID for every value
+ return keys
+ # can't use OIDs, try using descriptions
+ used_keys = {}
+ keys = map(str, descriptions)
+ for key in keys:
+ if used_keys.has_key(key):
+ raise ValueError, "duplicated descriptions (provide keys)"
+ used_keys[key] = 1
+ return keys
+
+ def set_options(self, options, sort=False):
+ """(options: [objects:any], sort=False)
+ or
+ (options: [(object:any, description:any)], sort=False)
+ or
+ (options: [(object:any, description:any, key:any)], sort=False)
+ """
+
+ """
+ Set the options list. The list of options can be a list of objects, in
+ which case the descriptions default to map(htmlescape, objects)
+ applying htmlescape() to each description and
+ key.
+ If keys are provided they must be distinct. If the sort keyword
+ argument is true, sort the options by case-insensitive lexicographic
+ order of descriptions, except that options with value None appear
+ before others.
+ """
+ if options:
+ first = options[0]
+ values = []
+ descriptions = []
+ keys = []
+ if type(first) is TupleType:
+ if len(first) == 2:
+ for value, description in options:
+ values.append(value)
+ descriptions.append(description)
+ elif len(first) == 3:
+ for value, description, key in options:
+ values.append(value)
+ descriptions.append(description)
+ keys.append(str(key))
+ else:
+ raise ValueError, 'invalid options %r' % options
+ else:
+ values = descriptions = options
+
+ if not keys:
+ keys = self._generate_keys(values, descriptions)
+
+ options = zip(values, descriptions, keys)
+
+ if sort:
+ def make_sort_key(option):
+ value, description, key = option
+ if value is None:
+ return ('', option)
+ else:
+ return (str(description).lower(), option)
+ doptions = map(make_sort_key, options)
+ doptions.sort()
+ options = [item[1] for item in doptions]
+ self.options = options
+
+ def _parse_single_selection(self, parsed_key):
+ for value, description, key in self.options:
+ if key == parsed_key:
+ return value
+ else:
+ if self.verify_selection:
+ self.error = "invalid value selected"
+ return None
+ elif self.options:
+ return self.options[0][0]
+ else:
+ return None
+
+ def set_allowed_values(self, allowed_values, descriptions=None,
+ sort=False):
+ """(allowed_values:[any], descriptions:[any], sort:boolean=False)
+
+ Set the options for this widget. The allowed_values and descriptions
+ parameters must be sequences of the same length. The sort option
+ causes the options to be sorted using case-insensitive lexicographic
+ order of descriptions, except that options with value None appear
+ before others.
+ """
+ if descriptions is None:
+ self.set_options(allowed_values, sort)
+ else:
+ assert len(descriptions) == len(allowed_values)
+ self.set_options(zip(allowed_values, descriptions), sort)
+
+ def is_selected(self, value):
+ return value == self.value
+
+ def render(self):
+ if self.SELECT_TYPE == "multiple_select":
+ multiple = ValuelessAttr
+ else:
+ multiple = None
+ if self.SELECT_TYPE == "option_select":
+ onchange = "submit()"
+ else:
+ onchange = None
+ tags = [htmltag("select", name=self.name,
+ multiple=multiple, onchange=onchange,
+ size=self.size)]
+ for object, description, key in self.options:
+ if self.is_selected(object):
+ selected = ValuelessAttr
+ else:
+ selected = None
+ if description is None:
+ description = ""
+ r = htmltag("option", value=key, selected=selected)
+ tags.append(r + htmlescape(description) + htmltext('</option>'))
+ tags.append(htmltext("</select>"))
+ return htmltext("\n").join(tags)
+
+
+class SingleSelectWidget(SelectWidget):
+ """Widget for single selection.
+ """
+
+ SELECT_TYPE = "single_select"
+
+ def _parse(self, request):
+ parsed_key = request.form.get(self.name)
+ if parsed_key:
+ if type(parsed_key) is ListType:
+ self.error = "cannot select multiple values"
+ else:
+ self.value = self._parse_single_selection(parsed_key)
+ else:
+ self.value = None
+
+
+class RadiobuttonsWidget(SingleSelectWidget):
+ """Widget for a *set* of related radiobuttons -- all have the
+ same name, but different values (and only one of those values
+ is returned by the whole group).
+
+ Instance attributes:
+ delim : string = None
+ string to emit between each radiobutton in the group. If
+ None, a single newline is emitted.
+ """
+
+ SELECT_TYPE = "radiobuttons"
+
+ def __init__(self, name, value=None,
+ allowed_values=None,
+ descriptions=None,
+ options=None,
+ delim=None):
+ SingleSelectWidget.__init__(self, name, value, allowed_values,
+ descriptions, options)
+ if delim is None:
+ self.delim = "\n"
+ else:
+ self.delim = delim
+
+ def render(self, request):
+ tags = []
+ for object, description, key in self.options:
+ if self.is_selected(object):
+ checked = ValuelessAttr
+ else:
+ checked = None
+ r = htmltag("input",
+ type="radio",
+ name=self.name,
+ value=key,
+ checked=checked)
+ tags.append(r + htmlescape(description) + htmltext('</input>'))
+ return htmlescape(self.delim).join(tags)
+
+
+class MultipleSelectWidget(SelectWidget):
+ """Widget for multiple selection.
+
+ Instance attributes:
+ value : [any]
+ for multipe selects, the value is None or a list of
+ elements from dict(self.options).values()
+ """
+
+ SELECT_TYPE = "multiple_select"
+
+ def set_value(self, value):
+ allowed_values = self.get_allowed_values()
+ if value in allowed_values:
+ self.value = [ value ]
+ elif type(value) in (ListType, TupleType):
+ self.value = [ element
+ for element in value
+ if element in allowed_values ] or None
+ else:
+ self.value = None
+
+ def is_selected(self, value):
+ if self.value is None:
+ return value is None
+ else:
+ return value in self.value
+
+ def _parse(self, request):
+ parsed_keys = request.form.get(self.name)
+ if parsed_keys:
+ if type(parsed_keys) is ListType:
+ self.value = [value
+ for value, description, key in self.options
+ if key in parsed_keys] or None
+ else:
+ self.value = [self._parse_single_selection(parsed_keys)]
+ else:
+ self.value = None
+
+
+class SubmitWidget(Widget):
+ """
+ Instance attributes:
+ value : boolean
+ """
+
+ def __init__(self, name, value=None):
+ self.name = name
+ self.error = None
+ # 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.
+ 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):
+ value = (self.label and htmlescape(self.label) or None)
+ return htmltag("input", xml_end=True, type="submit",
+ name=self.name, value=value)
+
+ def _parse(self, request):
+ self.value = request.form.has_key(self.name)
+
+
+
+class HiddenWidget(Widget):
+ """
+ Instance attributes:
+ value : string
+ """
+
+ def set_error(self, error):
+ return TypeError, 'error not allowed on hidden widgets'
+
+ def render(self):
+ if self.value is None:
+ value = None
+ else:
+ value = htmlescape(self.value)
+ return htmltag("input", xml_end=True,
+ type="hidden",
+ name=self.name,
+ value=value)
+
+
+# -- Derived widget types ----------------------------------------------
+# (these don't correspond to fundamental widget types in HTML,
+# so they're separated)
+
+class NumberWidget(StringWidget):
+ """
+ Instance attributes: none
+ """
+
+ # Parameterize the number type (either float or int) through
+ # these class attributes:
+ TYPE_OBJECT = None # eg. int, float
+ TYPE_ERROR = None # human-readable error message
+ TYPE_CONVERTER = None # eg. int(), float()
+
+ def __init__(self, name,
+ value=None,
+ size=None, maxlength=None):
+ 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, size, maxlength)
+
+ def _parse(self, request):
+ StringWidget._parse(self, request)
+ if self.value is not None:
+ try:
+ self.value = self.TYPE_CONVERTER(self.value)
+ except ValueError:
+ self.error = self.TYPE_ERROR
+
+
+class FloatWidget(NumberWidget):
+ """
+ Instance attributes:
+ value : float
+ """
+ TYPE_OBJECT = FloatType
+ TYPE_CONVERTER = float
+ TYPE_ERROR = "must be a number"
+
+
+class IntWidget(NumberWidget):
+ """
+ Instance attributes:
+ value : int
+ """
+ TYPE_OBJECT = IntType
+ TYPE_CONVERTER = int
+ TYPE_ERROR = "must be an integer"
+
+
+class OptionSelectWidget(SingleSelectWidget):
+ """Widget for single selection with automatic submission and early
+ parsing. This widget parses the request when it is created. This
+ allows its value to be used to decide what other widgets need to be
+ created in a form. It's a powerful feature but it can be hard to
+ understand what's going on.
+
+ Instance attributes:
+ value : any
+ """
+
+ SELECT_TYPE = "option_select"
+
+ def render(self, request):
+ return (SingleSelectWidget.render(self) +
+ htmltext('<noscript>'
+ '<input type="submit" name="" value="apply" />'
+ '</noscript>'))
+