SVN: r21313 - trunk/quixote/form
Andrew Kuchling <akuchlin-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]> Wed, 09 Apr 2003 14:35:01 -0400
| Newsgroups | gmane.comp.web.quixote.cvs |
|---|---|
| Message-ID | <[email protected]> |
Author: akuchlin
Date: 2003-04-09 14:34:59 -0400 (Wed, 09 Apr 2003)
New Revision: 21313
Added:
trunk/quixote/form/form.py
Removed:
trunk/quixote/form/form.ptl
Log:
Rename form.ptl to form.py; no code changes
Copied: form.py (from rev 21312, trunk/quixote/form/form.ptl)
Deleted: trunk/quixote/form/form.ptl
==============================================================================
--- trunk/quixote/form/form.ptl (original)
+++ trunk/quixote/form/form.ptl 2003-04-09 14:35:00.000000000 -0400
@@ -1,556 +0,0 @@
-"""quixote.form.form
-
-Provides the Form class and bureaucracy for registering widget classes.
-(The standard widget classes are registered automatically.)
-"""
-
-__revision__ = "$Id$"
-
-from types import StringType
-from quixote import get_session, get_publisher
-from quixote.html import url_quote, htmltag, htmltext, nl2br, htmlescape
-from quixote.form.widget import FormValueError, HiddenWidget
-
-
-class FormTokenWidget (HiddenWidget):
- def render (self, request):
- self.value = get_session().create_form_token()
- return HiddenWidget.render(self, request)
-
-
-JAVASCRIPT_MARKUP = htmltext('''\
-<script type="text/javascript">
-<!--
-%s
-// -->
-</script>
-''')
-
-def _join (seq):
- s = ""
- for item in seq:
- if item is None:
- continue
- s = s + htmlescape(item)
- return s
-
-class Form:
- """
- A form is the major element of an interactive web page. A form
- consists of the following:
- * widgets (input/interaction elements)
- * text
- * layout
- * code to process the form
-
- All four of these are the responsibility of Form classes.
- Typically, you will create one Form subclass for each form in your
- application. Thanks to the separation of responsibilities here,
- it's not too hard to structure things so that a given form is
- rendered and/or processed somewhat differently depending on context.
- That separation is as follows:
- * the constructor declares what widgets are in the form, and
- any static text that is always associated with those widgets
- (in particular, a widget title and "hint" text)
- * the 'render()' method combines the widgets and their associated
- text to create a (1-D) stream of HTML that represents the
- (2-D) web page that will be presented to the user
- * the 'process()' method parses the user input values from the form
- and validates them
- * the 'action()' method takes care of finishing whatever action
- was requested by the user submitting the form -- commit
- a database transaction, update session flags, redirect the
- user to a new page, etc.
-
- This class provides a default 'process()' method that just parses
- each widget, storing any error messages for display on the next
- 'render()', and returns the results (if the form parses
- successfully) in a dictionary.
-
- This class also provides a default 'render()' method that lays out
- widgets and text in a 3-column table: the first column is the widget
- title, the second column is the widget itself, and the third column is
- any hint and/or error text associated with the widget. Also provided
- are methods that can be used to construct this table a row at a time,
- so you can use this layout for most widgets, but escape from it for
- oddities.
-
- Instance attributes:
- widgets : { widget_name:string : widget:Widget }
- dictionary of all widgets in the form
- widget_order : [Widget]
- same widgets as 'widgets', but ordered (because order matters)
- submit_buttons : [SubmitButtonWidget]
- the submit button widgets in the form
-
- error : { widget_name:string : error_message:string }
- hint : { widget_name:string : hint_text:string }
- title : { widget_name:string : widget_title:string }
- required : { widget_name:string : boolean }
-
- """
-
- TOKEN_NAME = "_form_id" # name of hidden token widget
-
- def __init__ (self, method="post", enctype=None, use_tokens=1):
-
- if method not in ("post", "get"):
- raise ValueError("Form method must be 'post' or 'get', "
- "not %r" % method)
- self.method = method
-
- 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
-
- # The first major component of a form: its widgets. We want
- # both easy access and order, so we have a dictionary and a list
- # of the same objects. The dictionary is keyed on widget name.
- # These are populated by the 'add_*_widget()' methods.
- self.widgets = {}
- self.widget_order = []
- self.submit_buttons = []
- self.cancel_url = None
-
- # The second major component: text. It's up to the 'render()'
- # method to figure out how to lay these out; the standard
- # 'render()' does so in a fairly sensible way that should work
- # for most of our forms. These are also populated by the
- # 'add_*_widget()' methods.
- self.error = {}
- self.hint = {}
- self.title = {}
- self.required = {}
-
- config = get_publisher().config
- if self.method == "post" and use_tokens and config.form_tokens:
- # unique token for each form, this prevents many cross-site
- # attacks and prevents a form from being submitted twice
- self.add_widget(FormTokenWidget, self.TOKEN_NAME)
- self.use_form_tokens = 1
- else:
- self.use_form_tokens = 0
-
- # Subclasses should override this method to specify the actual
- # widgets in this form -- typically this consists of a series of
- # calls to 'add_widget()', which updates the data structures we
- # just defined.
-
-
- # -- Layout (rendering) methods ------------------------------------
-
- # The third major component of a web form is layout. These methods
- # combine text and widgets in a 1-D stream of HTML, or in a 2-D web
- # page (depending on your level of abstraction).
-
- def render (self, request, action_url):
- # render(request : HTTPRequest,
- # action_url : string)
- # -> HTML text
- #
- # Render a form as HTML.
- assert type(action_url) in (StringType, htmltext)
- parts = [
- self._render_start(request, action_url,
- enctype=self.enctype, method=self.method),
- self._render_body(request),
- self._render_finish(request),
- ]
-
- return _join(parts)
-
- def _render_start (self, request, action,
- enctype=None, method='post', name=None):
- parts = [
- htmltag('form', enctype=enctype, method=method,
- action=action, name=name),
- self._render_hidden_widgets(request),
- ]
- return _join(parts)
-
- def _render_finish (self, request):
- parts = [
- htmltext('</form>'),
- self._render_javascript(request),
- ]
- return _join(parts)
-
- def _render_sep (self, text, line=1):
- return (
- htmltext('<tr><td colspan="3">%s<strong><big>%s</big></strong></td></tr>') %
- (line and htmltext('<hr>') or '', text))
-
- def _render_error (self, error):
- if error:
- return (htmltext('<font color="red">%s</font><br />')
- % str(nl2br(error)))
- else:
- return ''
-
- def _render_hint (self, hint):
- if hint:
- return htmltext('<em>%s</em>') % hint
- else:
- return ''
-
- def _render_widget_row (self, request, widget):
- if widget.widget_type == 'hidden':
- return ''
- title = self.title[widget.name] or ''
- if self.required.get(widget.name):
- title = title + htmltext(' *')
- parts = [
- htmltext('<tr>'),
- htmltext('<th colspan="3" align="left">'),
- title,
- htmltext('</th>'),
- htmltext('</tr>'),
- htmltext('<tr>'),
- htmltext('<td> </td>'),
- htmltext('<td>'),
- widget.render(request),
- htmltext('</td>'),
- htmltext('<td>'),
- self._render_error(self.error.get(widget.name)),
- self._render_hint(self.hint.get(widget.name)),
- htmltext('</td>'),
- htmltext('</tr>'),
- ]
- print parts
- return _join(parts)
-
- def _render_hidden_widgets (self, request):
- parts = []
- for widget in self.widget_order:
- if widget.widget_type == 'hidden':
- parts += [
- widget.render(request),
- self._render_error(self.error.get(widget.name)),
- ]
- return _join(parts)
-
- def _render_submit_buttons (self, request, ncols=3):
- parts = [htmltext('<tr><td colspan="%d">\n') % ncols]
- for button in self.submit_buttons:
- parts.append(button.render(request))
- parts.append(htmltext('</td></tr>'))
- return _join(parts)
-
- def _render_visible_widgets (self, request):
- parts = []
- for widget in self.widget_order:
- parts.append(self._render_widget_row(request, widget))
- return _join(parts)
-
- def _render_error_notice (self, request):
- parts = []
- if self.error:
- parts += [
- htmltext('<tr><td colspan="3">'),
- htmltext('<font color="red"><strong>Warning:</strong></font> '),
- htmltext('there were errors processing your form. See below for details.'),
- htmltext('</td></tr>'),
- ]
- return _join(parts)
-
- def _render_required_notice (self, request):
- parts = []
- if filter(None, self.required.values()):
- parts += [
- htmltext('<tr><td colspan="3">'),
- htmltext('<b>*</b> = <em>required field</em>'),
- htmltext('</td></tr>'),
- ]
- return _join(parts)
-
- def _render_body (self, request):
- parts = [
- htmltext('<table>'),
- self._render_error_notice(request),
- self._render_required_notice(request),
- self._render_visible_widgets(request),
- self._render_submit_buttons(request),
- htmltext('</table>'),
- ]
- return _join(parts)
-
- def _render_javascript (self, request):
- """Render javacript code for the form, if any.
- Insert code lexically sorted by code_id
- """
- javascript_code = 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 ''
-
-
- # -- Processing methods --------------------------------------------
-
- # The fourth and final major component: code to process the form.
- # The standard 'process()' method just parses every widget and
- # returns a { field_name : field_value } dictionary as 'values'.
-
- def process (self, request):
- """process(request : HTTPRequest) -> values : { string : any }
-
- Process the form data, validating all input fields (widgets).
- If any errors in input fields, adds error messages to the
- 'error' attribute (so that future renderings of the form will
- include the errors). Returns a dictionary mapping widget names to
- parsed values.
- """
- self.error.clear()
-
- values = {}
- for widget in self.widget_order:
- try:
- val = widget.parse(request)
- except FormValueError, exc:
- self.error[widget.name] = exc.msg
- else:
- values[widget.name] = val
-
- return values
-
- def action (self, request, submit, values):
- """action(request : HTTPRequest, submit : string,
- values : { string : any }) -> string
-
- Carry out the action required by a form submission. 'submit' is the
- name of submit button used to submit the form. 'values' is the
- dictionary of parsed values from 'process()'. Note that error
- checking cannot be done here -- it must done in the 'process()'
- method.
- """
- raise NotImplementedError, "sub-classes must implement 'action()'"
-
- def handle (self, request):
- """handle(request : HTTPRequest) -> string
-
- Master method for handling forms. It should be called after
- initializing a form. Controls form action based on a request. You
- probably should override 'process' and 'action' instead of
- overriding this method.
- """
- action_url = self.get_action_url(request)
- if not self.form_submitted(request):
- return self.render(request, action_url)
- submit = self.get_submit_button(request)
- if submit == "cancel":
- return request.redirect(self.cancel_url)
- values = self.process(request)
- if submit == "":
- # The form was submitted by unknown submit button, assume that
- # the submission was required to update the layout of the form.
- # Clear the errors and re-render the form.
- self.error.clear()
- return self.render(request, action_url)
-
- if self.use_form_tokens:
- # before calling action() ensure that there is a valid token
- # present
- token = values.get(self.TOKEN_NAME)
- if not request.session.has_form_token(token):
- if not self.error:
- # if there are other errors then don't show the token
- # error, the form needs to be resubmitted anyhow
- self.error[self.TOKEN_NAME] = (
- "The form you have submitted is invalid. It has "
- "already been submitted or has expired. Please "
- "review and resubmit the form.")
- else:
- request.session.remove_form_token(token)
-
- if self.error:
- return self.render(request, action_url)
- else:
- return self.action(request, submit, values)
-
-
- # -- Convenience methods -------------------------------------------
-
- def form_submitted (self, request):
- """form_submitted(request : HTTPRequest) -> boolean
-
- Return true if a form was submitted in the current request.
- """
- return len(request.form) > 0
-
- def get_action_url (self, request):
- action_url = url_quote(request.get_path())
- query = request.get_environ("QUERY_STRING")
- if query:
- action_url += "?" + query
- return action_url
-
- def get_submit_button (self, request):
- """get_submit_button(request : HTTPRequest) -> string | None
-
- Get the name of the submit button that was used to submit the
- current form. If the browser didn't include this information in
- the request, use the first submit button registered.
- """
- for button in self.submit_buttons:
- if request.form.has_key(button.name):
- return button.name
- else:
- if request.form and self.submit_buttons:
- return ""
- else:
- return None
-
- def get_widget (self, widget_name):
- return self.widgets.get(widget_name)
-
- def parse_widget (self, name, request):
- """parse_widget(name : string, request : HTTPRequest) -> any
-
- Parse the value of named widget. If any parse errors, store the
- error message (in self.error) for use in the next rendering of
- the form and return None; otherwise, return the value parsed
- from the widget (whose type depends on the widget type).
- """
- try:
- return self.widgets[name].parse(request)
- except FormValueError, exc:
- self.error[name] = str(exc)
- return None
-
- def store_value (self, widget_name, request, target,
- mode="modifier",
- key=None,
- missing_error=None):
- """store_value(widget_name : string,
- request : HTTPRequest,
- target : instance | dict,
- mode : string = "modifier",
- key : string = widget_name,
- missing_error : string = None)
-
- Parse a widget and, if it parsed successfully, store its value
- in 'target'. The value is stored in 'target' by name 'key';
- if 'key' is not supplied, it defaults to 'widget_name'.
- How the value is stored depends on 'mode':
- * modifier: call a modifier method, eg. if 'key' is "foo",
- call 'target.set_foo(value)'
- * direct: direct attribute update, eg. if 'key' is
- "foo" do "target.foo = value"
- * dict: dictionary update, eg. if 'key' is "foo" do
- "target['foo'] = value"
-
- If 'missing_error' is supplied, use it as an error message if
- the field doesn't have a value -- ie. supplying 'missing_error'
- means this field is required.
- """
- value = self.parse_widget(widget_name, request)
- if (value is None or value == "") and missing_error:
- self.error[widget_name] = missing_error
- return None
-
- if key is None:
- key = widget_name
- if mode == "modifier":
- # eg. turn "name" into "target.set_name", and
- # call it like "target.set_name(value)"
- mod = getattr(target, "set_" + key)
- mod(value)
- elif mode == "direct":
- if not hasattr(target, key):
- raise AttributeError, \
- ("target object %s doesn't have attribute %s" %
- (`target`, key))
- setattr(target, key, value)
- elif mode == "dict":
- target[key] = value
- else:
- raise ValueError, "unknown update mode %s" % `mode`
-
- def clear_widget (self, widget_name):
- self.widgets[widget_name].clear()
-
- def get_widget_value (self, widget_name):
- return self.widgets[widget_name].value
-
- def set_widget_value (self, widget_name, value):
- self.widgets[widget_name].set_value(value)
-
-
- # -- Form population methods ---------------------------------------
-
- def add_widget (self, widget_type, name, value=None,
- title=None, hint=None, required=0, **args):
- """add_widget(widget_type : string | Widget,
- name : string,
- value : any = None,
- title : string = None,
- hint : string = None,
- required : boolean = 0,
- ...)
-
- Create a new Widget object and add it to the form. The widget
- class used depends on 'widget_type', and the expected type of
- 'value' also depends on the widget class. Any extra keyword
- args are passed to the widget constructor.
- """
- if self.widgets.has_key(name):
- raise ValueError, "form already has '%s' variable" % name
- klass = get_widget_class(widget_type)
- new_widget = apply(klass, (name, value), args)
-
- self.widgets[name] = new_widget
- self.widget_order.append(new_widget)
- self.title[name] = title
- self.hint[name] = hint
- self.required[name] = required
-
- def add_submit_button (self, name, value):
- global _widget_class
- if self.widgets.has_key(name):
- raise ValueError, "form already has '%s' variable" % name
- new_widget = _widget_class['submit_button'](name, value)
-
- self.widgets[name] = new_widget
- self.submit_buttons.append(new_widget)
-
- def add_cancel_button (self, caption, url):
- if not isinstance(url, (StringType, htmltext)):
- raise TypeError, "url must be a string (got %r)" % url
- self.add_submit_button("cancel", caption)
- self.cancel_url = url
-
-# class Form
-
-
-_widget_class = {}
-
-def register_widget_class (klass, widget_type=None):
- global _widget_class
- if widget_type is None:
- widget_type = klass.widget_type
- assert widget_type is not None, "widget_type must be defined"
- _widget_class[widget_type] = klass
-
-def get_widget_class (widget_type):
- global _widget_class
- if callable(widget_type):
- # Presumably someone passed a widget class object to
- # Widget.create_subwidget() or Form.add_widget() --
- # don't bother with the widget class registry at all.
- return widget_type
- else:
- try:
- return _widget_class[widget_type]
- except KeyError:
- raise ValueError("unknown widget type %r" % widget_type)