SVN: r25664 - in trunk/quixote: . form1
Neil Schemenauer <nascheme-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]> Mon, 22 Nov 2004 15:35:17 -0500
| Newsgroups | gmane.comp.web.quixote.cvs |
|---|---|
| Message-ID | <[email protected]> |
Author: nascheme
Date: 2004-11-22 15:35:07 -0500 (Mon, 22 Nov 2004)
New Revision: 25664
Added:
trunk/quixote/form1/
trunk/quixote/form1/__init__.py
trunk/quixote/form1/form.py
trunk/quixote/form1/widget.py
trunk/quixote/publish1.py
Log:
Provide a Publisher object that works more like Quixote 1. Also,
restore the original form framework with the name "form1".
Added: trunk/quixote/form1/__init__.py
===================================================================
--- trunk/quixote/form1/__init__.py 2004-11-22 19:45:33 UTC (rev 25663)
+++ trunk/quixote/form1/__init__.py 2004-11-22 20:35:07 UTC (rev 25664)
@@ -0,0 +1,34 @@
+"""$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 subclass for 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.
+"""
+
+from quixote.form1.form import Form, register_widget_class, FormTokenWidget
+from quixote.form1.widget import Widget, StringWidget, FileWidget, \
+ PasswordWidget, TextWidget, CheckboxWidget, RadiobuttonsWidget, \
+ SingleSelectWidget, SelectWidget, OptionSelectWidget, \
+ MultipleSelectWidget, ListWidget, SubmitButtonWidget, HiddenWidget, \
+ FloatWidget, IntWidget, CollapsibleListWidget, FormValueError
+
+# Register the standard widget classes
+register_widget_class(StringWidget)
+register_widget_class(FileWidget)
+register_widget_class(PasswordWidget)
+register_widget_class(TextWidget)
+register_widget_class(CheckboxWidget)
+register_widget_class(RadiobuttonsWidget)
+register_widget_class(SingleSelectWidget)
+register_widget_class(OptionSelectWidget)
+register_widget_class(MultipleSelectWidget)
+register_widget_class(ListWidget)
+register_widget_class(SubmitButtonWidget)
+register_widget_class(HiddenWidget)
+register_widget_class(FloatWidget)
+register_widget_class(IntWidget)
+register_widget_class(CollapsibleListWidget)
Property changes on: trunk/quixote/form1/__init__.py
___________________________________________________________________
Name: svn:keywords
+ HeadURL Id
Added: trunk/quixote/form1/form.py
===================================================================
--- trunk/quixote/form1/form.py 2004-11-22 19:45:33 UTC (rev 25663)
+++ trunk/quixote/form1/form.py 2004-11-22 20:35:07 UTC (rev 25664)
@@ -0,0 +1,534 @@
+"""$URL$
+$Id$
+
+Provides the Form class and bureaucracy for registering widget classes.
+(The standard widget classes are registered automatically.)
+"""
+
+from types import StringType
+from quixote import get_session, get_publisher, redirect
+from quixote.html import url_quote, htmltag, htmltext, nl2br, TemplateIO
+from quixote.form1.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>
+''')
+
+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)
+ r = TemplateIO(html=1)
+ r += self._render_start(request, action_url,
+ enctype=self.enctype, method=self.method)
+ r += self._render_body(request)
+ r += self._render_finish(request)
+ return r.getvalue()
+
+ def _render_start(self, request, action,
+ enctype=None, method='post', name=None):
+ r = TemplateIO(html=1)
+ r += htmltag('form', enctype=enctype, method=method,
+ action=action, name=name)
+ r += self._render_hidden_widgets(request)
+ return r.getvalue()
+
+ def _render_finish(self, request):
+ r = TemplateIO(html=1)
+ r += htmltext('</form>')
+ r += self._render_javascript(request)
+ return r.getvalue()
+
+ 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 />') % 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(' *')
+ r = TemplateIO(html=1)
+ r += htmltext('<tr><th colspan="3" align="left">')
+ r += title
+ r += htmltext('</th></tr>'
+ '<tr><td> </td><td>')
+ r += widget.render(request)
+ r += htmltext('</td><td>')
+ r += self._render_error(self.error.get(widget.name))
+ r += self._render_hint(self.hint.get(widget.name))
+ r += htmltext('</td></tr>')
+ return r.getvalue()
+
+ def _render_hidden_widgets(self, request):
+ r = TemplateIO(html=1)
+ for widget in self.widget_order:
+ if widget.widget_type == 'hidden':
+ r += widget.render(request)
+ r += self._render_error(self.error.get(widget.name))
+ return r.getvalue()
+
+ def _render_submit_buttons(self, request, ncols=3):
+ r = TemplateIO(html=1)
+ r += htmltext('<tr><td colspan="%d">\n') % ncols
+ for button in self.submit_buttons:
+ r += button.render(request)
+ r += htmltext('</td></tr>')
+ return r.getvalue()
+
+ def _render_visible_widgets(self, request):
+ r = TemplateIO(html=1)
+ for widget in self.widget_order:
+ r += self._render_widget_row(request, widget)
+ return r.getvalue()
+
+ def _render_error_notice(self, request):
+ if self.error:
+ 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_required_notice(self, request):
+ if filter(None, self.required.values()):
+ r = htmltext('<tr><td colspan="3">'
+ '<b>*</b> = <em>required field</em>'
+ '</td></tr>')
+ else:
+ r = ''
+ return r
+
+ def _render_body(self, request):
+ r = TemplateIO(html=1)
+ r += htmltext('<table>')
+ r += self._render_error_notice(request)
+ r += self._render_required_notice(request)
+ r += self._render_visible_widgets(request)
+ r += self._render_submit_buttons(request)
+ r += htmltext('</table>')
+ return r.getvalue()
+
+ 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 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,
+ ...) -> Widget
+
+ 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.
+
+ Returns the new Widget.
+ """
+ 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
+ return new_widget
+
+ 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)
Property changes on: trunk/quixote/form1/form.py
___________________________________________________________________
Name: svn:keywords
+ HeadURL Id
Added: trunk/quixote/form1/widget.py
===================================================================
--- trunk/quixote/form1/widget.py 2004-11-22 19:45:33 UTC (rev 25663)
+++ trunk/quixote/form1/widget.py 2004-11-22 20:35:07 UTC (rev 25664)
@@ -0,0 +1,842 @@
+"""$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
+from quixote.http_request import Upload
+
+
+class FormValueError (Exception):
+ """Raised whenever 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. The key elements
+ of a web widget are:
+ - name
+ - widget type (how the widget looks/works in the browser)
+ - value
+
+ The name and value are instance attributes (because they're specific to
+ a particular widget in a particular context); widget type is a
+ class attributes.
+
+ Instance attributes:
+ name : string
+ value : any
+
+ Feel free to access these directly; to set them, use the 'set_*()'
+ modifier methods.
+ """
+
+ # Subclasses must define. 'widget_type' is just a string, e.g.
+ # "string", "text", "checkbox".
+ widget_type = None
+
+ def __init__(self, name, value=None):
+ assert self.__class__ is not Widget, "abstract class"
+ self.set_name(name)
+ 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.widget_type, self.name)
+
+
+ def set_name(self, name):
+ self.name = name
+
+
+ def set_value(self, value):
+ self.value = value
+
+
+ def clear(self):
+ self.value = None
+
+ # -- Subclasses must implement these -------------------------------
+
+ def render(self, request):
+ """render(request) -> HTML text"""
+ raise NotImplementedError
+
+
+ def parse(self, request):
+ """parse(request) -> any"""
+ value = request.form.get(self.name)
+ if type(value) is StringType and value.strip():
+ self.value = value
+ else:
+ self.value = None
+
+ return self.value
+
+ # -- Convenience methods for subclasses ----------------------------
+
+ # This one's really only for composite widgets; lives here until
+ # we have a demonstrated need for a CompositeWidget class.
+ def get_subwidget_name(self, name):
+ return "%s$%s" % (self.name, name)
+
+
+ def create_subwidget(self, widget_type, widget_name, value=None, **args):
+ from quixote.form.form import get_widget_class
+ klass = get_widget_class(widget_type)
+ name = self.get_subwidget_name(widget_name)
+ return apply(klass, (name, value), args)
+
+# 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
+ """
+
+ widget_type = "string"
+
+ # 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, request, **attributes):
+ return htmltag("input", xml_end=1,
+ type=self.html_type,
+ name=self.name,
+ size=self.size,
+ maxlength=self.maxlength,
+ value=self.value,
+ **attributes)
+
+
+class FileWidget (StringWidget):
+ """Trivial subclass of StringWidget for uploading files.
+
+ Instance attributes: none
+ """
+ widget_type = "file"
+ html_type = "file"
+
+ def parse(self, request):
+ """parse(request) -> any"""
+ value = request.form.get(self.name)
+ if isinstance(value, Upload):
+ self.value = value
+ else:
+ self.value = None
+ return self.value
+
+
+class PasswordWidget (StringWidget):
+ """Trivial subclass of StringWidget for entering passwords (different
+ widget type because HTML does it that way).
+
+ Instance attributes: none
+ """
+
+ widget_type = "password"
+ 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)
+ css_class : string
+ """
+
+ widget_type = "text"
+
+ def __init__(self, name, value=None, cols=None, rows=None, wrap=None,
+ css_class=None):
+ Widget.__init__(self, name, value)
+ self.cols = cols
+ self.rows = rows
+ self.wrap = wrap
+ self.css_class = css_class
+
+ def render(self, request):
+ return (htmltag("textarea", name=self.name,
+ cols=self.cols,
+ rows=self.rows,
+ wrap=self.wrap,
+ css_class=self.css_class) +
+ htmlescape(self.value or "") +
+ htmltext("</textarea>"))
+
+
+ def parse(self, request):
+ value = Widget.parse(self, request)
+ if value:
+ value = value.replace("\r\n", "\n")
+ self.value = value
+ return self.value
+
+
+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
+ """
+
+ widget_type = "checkbox"
+
+ def render(self, request):
+ return htmltag("input", xml_end=1,
+ type="checkbox",
+ name=self.name,
+ value="yes",
+ checked=self.value and "checked" or None)
+
+
+ def parse(self, request):
+ self.value = request.form.has_key(self.name)
+ return self.value
+
+
+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.
+ """
+
+ # NB. 'widget_type' not set here because this is an abstract class: it's
+ # set by subclasses SingleSelectWidget and MultipleSelectWidget.
+
+ def __init__(self, name, value=None,
+ allowed_values=None,
+ descriptions=None,
+ options=None,
+ size=None,
+ sort=0,
+ verify_selection=1):
+ 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.set_name(name)
+ self.set_value(value)
+ self.size = size
+ self.verify_selection = verify_selection
+
+
+ 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=0):
+ """(options: [objects:any], sort=0)
+ or
+ (options: [(object:any, description:any)], sort=0)
+ or
+ (options: [(object:any, description:any, key:any)], sort=0)
+ """
+
+ """
+ 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:
+ raise FormValueError, "invalid value selected"
+ else:
+ return self.options[0][0]
+
+
+ def set_allowed_values(self, allowed_values, descriptions=None, sort=0):
+ """(allowed_values:[any], descriptions:[any], sort:boolean=0)
+
+ 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, request):
+ if self.widget_type == "multiple_select":
+ multiple = "multiple"
+ else:
+ multiple = None
+ if self.widget_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 = "selected"
+ 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.
+ """
+
+ widget_type = "single_select"
+
+ def parse(self, request):
+ parsed_key = request.form.get(self.name)
+ self.value = None
+ if parsed_key:
+ if type(parsed_key) is ListType:
+ raise FormValueError, "cannot select multiple values"
+ self.value = self.parse_single_selection(parsed_key)
+ return self.value
+
+
+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.
+ """
+
+ widget_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 = "checked"
+ else:
+ checked = None
+ r = htmltag("input", xml_end=True,
+ type="radio",
+ name=self.name,
+ value=key,
+ checked=checked)
+ tags.append(r + htmlescape(description))
+ 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()
+ """
+
+ widget_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)
+ self.value = None
+ 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)]
+ return self.value
+
+
+class SubmitButtonWidget (Widget):
+ """
+ Instance attributes:
+ value : boolean
+ """
+
+ widget_type = "submit_button"
+
+ def __init__(self, name=None, value=None):
+ Widget.__init__(self, name, value)
+
+
+ def render(self, request):
+ value = (self.value and htmlescape(self.value) or None)
+ return htmltag("input", xml_end=1, type="submit",
+ name=self.name, value=value)
+
+
+ def parse(self, request):
+ return request.form.get(self.name)
+
+
+ def is_submitted(self):
+ return self.parse(get_request())
+
+
+class HiddenWidget (Widget):
+ """
+ Instance attributes:
+ value : string
+ """
+
+ widget_type = "hidden"
+
+ def render(self, request):
+ if self.value is None:
+ value = None
+ else:
+ value = htmlescape(self.value)
+ return htmltag("input", xml_end=1,
+ type="hidden",
+ name=self.name,
+ value=value)
+
+
+ def set_current_value(self, value):
+ self.value = value
+ request = get_request()
+ if request.form:
+ request.form[self.name] = value
+
+
+ def get_current_value(self):
+ request = get_request()
+ if request.form:
+ return self.parse(request)
+ else:
+ return self.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):
+ value = StringWidget.parse(self, request)
+ if value:
+ try:
+ self.value = self.type_converter(value)
+ except ValueError:
+ raise FormValueError, self.type_error
+ return self.value
+
+
+class FloatWidget (NumberWidget):
+ """
+ Instance attributes:
+ value : float
+ """
+
+ widget_type = "float"
+ type_object = FloatType
+ type_converter = float
+ type_error = "must be a number"
+
+
+class IntWidget (NumberWidget):
+ """
+ Instance attributes:
+ value : int
+ """
+
+ widget_type = "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
+ """
+
+ widget_type = "option_select"
+
+ def __init__(self, *args, **kwargs):
+ SingleSelectWidget.__init__(self, *args, **kwargs)
+
+ request = get_request()
+ if request.form:
+ SingleSelectWidget.parse(self, request)
+ if self.value is None:
+ self.value = self.options[0][0]
+
+
+ def render(self, request):
+ return (SingleSelectWidget.render(self, request) +
+ htmltext('<noscript>'
+ '<input type="submit" name="" value="apply" />'
+ '</noscript>'))
+
+
+ def parse(self, request):
+ return self.value
+
+
+ def get_current_option(self):
+ return self.value
+
+
+class ListWidget (Widget):
+ """Widget for lists of objects.
+
+ Instance attributes:
+ value : [any]
+ """
+
+ widget_type = "list"
+
+ def __init__(self, name, value=None,
+ element_type=None,
+ element_name="row",
+ **args):
+ assert value is None or type(value) is ListType, (
+ "form value '%s' not a list: got %r" % (name, value))
+ assert type(element_name) in (StringType, htmltext), (
+ "form value '%s' element_name not a string: "
+ "got %r" % (name, element_name))
+
+ Widget.__init__(self, name, value)
+
+ if element_type is None:
+ self.element_type = "string"
+ else:
+ self.element_type = element_type
+ self.args = args
+
+ self.added_elements_widget = self.create_subwidget(
+ "hidden", "added_elements")
+
+ added_elements = int(self.added_elements_widget.get_current_value() or
+ '1')
+
+ self.add_button = self.create_subwidget(
+ "submit_button", "add_element",
+ value="Add %s" % element_name)
+
+ if self.add_button.is_submitted():
+ added_elements += 1
+ self.added_elements_widget.set_current_value(str(added_elements))
+
+ self.element_widgets = []
+ self.element_count = 0
+
+ if self.value is not None:
+ for element in self.value:
+ self.add_element(element)
+
+ for index in range(added_elements):
+ self.add_element()
+
+ def add_element(self, value=None):
+ self.element_widgets.append(
+ self.create_subwidget(self.element_type,
+ "element_%d" % self.element_count,
+ value=value,
+ **self.args))
+ self.element_count += 1
+
+ def render(self, request):
+ tags = []
+ for element_widget in self.element_widgets:
+ tags.append(element_widget.render(request))
+ tags.append(self.add_button.render(request))
+ tags.append(self.added_elements_widget.render(request))
+ return htmltext('<br />\n').join(tags)
+
+ def parse(self, request):
+ self.value = []
+ for element_widget in self.element_widgets:
+ value = element_widget.parse(request)
+ if value is not None:
+ self.value.append(value)
+ self.value = self.value or None
+ return self.value
+
+
+
+class CollapsibleListWidget (ListWidget):
+ """Widget for lists of objects with associated delete buttons.
+
+ CollapsibleListWidget behaves like ListWidget except that each element
+ is rendered with an associated delete button. Pressing the delete
+ button will cause the associated element name to be added to a hidden
+ widget that remembers all deletions until the form is submitted.
+ Only elements that are not marked as deleted will be rendered and
+ ultimately added to the value of the widget.
+
+ Instance attributes:
+ value : [any]
+ """
+
+ widget_type = "collapsible_list"
+
+ def __init__(self, name, value=None, element_name="row", **args):
+ self.name = name
+ self.element_name = element_name
+ self.deleted_elements_widget = self.create_subwidget(
+ "hidden", "deleted_elements")
+ self.element_delete_buttons = []
+ self.deleted_elements = (
+ self.deleted_elements_widget.get_current_value() or '')
+ ListWidget.__init__(self, name, value=value,
+ element_name=element_name,
+ **args)
+
+ def add_element(self, value=None):
+ element_widget_name = "element_%d" % self.element_count
+ if self.deleted_elements.find(element_widget_name) == -1:
+ delete_button = self.create_subwidget(
+ "submit_button", "delete_" + element_widget_name,
+ value="Delete %s" % self.element_name)
+ if delete_button.is_submitted():
+ self.element_count += 1
+ self.deleted_elements += element_widget_name
+ self.deleted_elements_widget.set_current_value(
+ self.deleted_elements)
+ else:
+ self.element_delete_buttons.append(delete_button)
+ ListWidget.add_element(self, value=value)
+ else:
+ self.element_count += 1
+
+ def render(self, request):
+ tags = []
+ for element_widget, element_delete_button in zip(
+ self.element_widgets, self.element_delete_buttons):
+ if self.deleted_elements.find(element_widget.name) == -1:
+ tags.append(element_widget.render(request) +
+ element_delete_button.render(request))
+ tags.append(self.add_button.render(request))
+ tags.append(self.added_elements_widget.render(request))
+ tags.append(self.deleted_elements_widget.render(request))
+ return htmltext('<br />\n').join(tags)
Property changes on: trunk/quixote/form1/widget.py
___________________________________________________________________
Name: svn:keywords
+ HeadURL Id
Added: trunk/quixote/publish1.py
===================================================================
--- trunk/quixote/publish1.py 2004-11-22 19:45:33 UTC (rev 25663)
+++ trunk/quixote/publish1.py 2004-11-22 20:35:07 UTC (rev 25664)
@@ -0,0 +1,270 @@
+"""$URL$
+$Id$
+
+Provides a publisher object that behaves like the Quixote 1 Publisher.
+Specifically, arbitrary namespaces may be exported and the HTTPRequest
+object is passed as the first argument to exported functions. Also,
+the _q_lookup(), _q_resolve(), and _q_access() methods work as they did
+in Quixote 1.
+"""
+
+import sys
+import re
+import types
+import warnings
+from quixote import errors, get_request, redirect
+from quixote.publish import Publisher as _Publisher
+from quixote.directory import Directory
+from quixote.html import htmltext
+
+
+class Publisher(_Publisher):
+ """
+ Instance attributes:
+ namespace_stack : [ module | instance | class ]
+ """
+
+ def __init__(self, root_namespace, config=None):
+ from quixote.config import Config
+ if type(root_namespace) is types.StringType:
+ root_namespace = _get_module(root_namespace)
+ self.namespace_stack = [root_namespace]
+ if config is None:
+ config = Config()
+ directory = RootDirectory(root_namespace, self.namespace_stack)
+ _Publisher.__init__(self, directory, config=config)
+
+ def debug(self, msg):
+ self.log(msg)
+
+ def get_namespace_stack(self):
+ """get_namespace_stack() -> [ module | instance | class ]
+ """
+ return self.namespace_stack
+
+
+class RootDirectory(Directory):
+ def __init__(self, root_namespace, namespace_stack):
+ self.root_namespace = root_namespace
+ self.namespace_stack = namespace_stack
+
+ def _q_traverse(self, path):
+ # Initialize the publisher's namespace_stack
+ del self.namespace_stack[:]
+
+ request = get_request()
+
+ # Traverse package to a (hopefully-) callable object
+ object = _traverse_url(self.root_namespace, path, request,
+ self.namespace_stack)
+
+ # None means no output -- traverse_url() just issued a redirect.
+ if object is None:
+ return None
+
+ # Anything else must be either a string...
+ if isstring(object):
+ output = object
+
+ # ...or a callable.
+ elif callable(object):
+ output = object(request)
+ if output is None:
+ raise RuntimeError, 'callable %s returned None' % repr(object)
+
+ # Uh-oh: 'object' is neither a string nor a callable.
+ else:
+ raise RuntimeError(
+ "object is neither callable nor a string: %s" % repr(object))
+
+ return output
+
+
+def _get_module(name):
+ """Get a module object by name."""
+ __import__(name)
+ module = sys.modules[name]
+ return module
+
+
+_slash_pat = re.compile("//*")
+
+def _traverse_url(root_namespace, path_components, request, namespace_stack):
+ """(root_namespace : any, path_components : [string],
+ request : HTTPRequest, namespace_stack : list) -> (object : any)
+
+ Perform traversal based on the provided path, starting at the root
+ object. It returns the script name and path info values for
+ the arrived-at object, along with the object itself and
+ a list of the namespaces traversed to get there.
+
+ It's expected that the final object is something callable like a
+ function or a method; intermediate objects along the way will
+ usually be packages or modules.
+
+ To prevent crackers from writing URLs that traverse private
+ objects, every package, module, or object along the way must have
+ a _q_exports attribute containing a list of publicly visible
+ names. Not having a _q_exports attribute is an error, though
+ having _q_exports be an empty list is OK. If a component of the path
+ isn't in _q_exports, that also produces an error.
+
+ Modifies the namespace_stack as it traverses the url, so that
+ any exceptions encountered along the way can be handled by the
+ nearest handler.
+ """
+
+ path = '/' + '/'.join(path_components)
+
+ # If someone accesses a Quixote driver script without a trailing
+ # slash, we'll wind up here with an empty path. This won't
+ # work; relative references in the page generated by the root
+ # namespace's _q_index() will be off. Fix it by redirecting the
+ # user to the right URL; when the client follows the redirect,
+ # we'll wind up here again with path == '/'.
+ if not path:
+ return redirect(request.environ['SCRIPT_NAME'] + '/' , permanent=1)
+
+ # Traverse starting at the root
+ object = root_namespace
+ namespace_stack.append(object)
+
+ # Loop over the components of the path
+ for component in path_components:
+ if component == "":
+ # "/q/foo/" == "/q/foo/_q_index"
+ component = "_q_index"
+ object = _get_component(object, component, request, namespace_stack)
+
+ if not (isstring(object) or callable(object)):
+ # We went through all the components of the path and ended up at
+ # something which isn't callable, like a module or an instance
+ # without a __call__ method.
+ if path[-1] != '/':
+ if not request.form:
+ # This is for the convenience of users who type in paths.
+ # Repair the path and redirect. This should not happen for
+ # URLs within the site.
+ return redirect(request.get_path() + "/", permanent=1)
+
+ else:
+ # Automatic redirects disabled or there is form data. If
+ # there is form data then the programmer is using the
+ # wrong path. A redirect won't work if the form data came
+ # from a POST anyhow.
+ raise errors.TraversalError(
+ "object is neither callable nor string "
+ "(missing trailing slash?)",
+ private_msg=repr(object),
+ path=path)
+ else:
+ raise errors.TraversalError(
+ "object is neither callable nor string",
+ private_msg=repr(object),
+ path=path)
+
+ return object
+
+
+def _get_component(container, component, request, namespace_stack):
+ """Get one component of a path from a namespace.
+ """
+ # First security check: if the container doesn't even have an
+ # _q_exports list, fail now: all Quixote-traversable namespaces
+ # (modules, packages, instances) must have an export list!
+ if not hasattr(container, '_q_exports'):
+ raise errors.TraversalError(
+ private_msg="%r has no _q_exports list" % container)
+
+ # Second security check: call _q_access function if it's present.
+ if hasattr(container, '_q_access'):
+ # will raise AccessError if access failed
+ container._q_access(request)
+
+ # Third security check: make sure the current name component
+ # is in the export list or is '_q_index'. If neither
+ # condition is true, check for a _q_lookup() and call it.
+ # '_q_lookup()' translates an arbitrary string into an object
+ # that we continue traversing. (This is very handy; it lets
+ # you put user-space objects into your URL-space, eliminating
+ # the need for digging ID strings out of a query, or checking
+ # PATHINFO after Quixote's done with it. But it is a
+ # compromise to security: it opens up the traversal algorithm
+ # to arbitrary names not listed in _q_exports!) If
+ # _q_lookup() doesn't exist or is None, a TraversalError is
+ # raised.
+
+ # Check if component is in _q_exports. The elements in
+ # _q_exports can be strings or 2-tuples mapping external names
+ # to internal names.
+ if component in container._q_exports or component == '_q_index':
+ internal_name = component
+ else:
+ # check for an explicit external to internal mapping
+ for value in container._q_exports:
+ if type(value) is types.TupleType:
+ if value[0] == component:
+ internal_name = value[1]
+ break
+ else:
+ internal_name = None
+
+ if internal_name is None:
+ # Component is not in exports list.
+ object = None
+ if hasattr(container, "_q_lookup"):
+ object = container._q_lookup(request, component)
+ elif hasattr(container, "_q_getname"):
+ warnings.warn("_q_getname() on %s used; should "
+ "be replaced by _q_lookup()" % type(container))
+ object = container._q_getname(request, component)
+ if object is None:
+ raise errors.TraversalError(
+ private_msg="object %r has no attribute %r" % (
+ container,
+ component))
+
+ # From here on, you can assume that the internal_name is not None
+ elif hasattr(container, internal_name):
+ # attribute is in _q_exports and exists
+ object = getattr(container, internal_name)
+
+ elif internal_name == '_q_index':
+ if hasattr(container, "_q_lookup"):
+ object = container._q_lookup(request, "")
+ else:
+ raise errors.AccessError(
+ private_msg=("_q_index not found in %r" % container))
+
+ elif hasattr(container, "_q_resolve"):
+ object = container._q_resolve(internal_name)
+ if object is None:
+ raise RuntimeError, ("component listed in _q_exports, "
+ "but not returned by _q_resolve(%r)"
+ % internal_name)
+ else:
+ # Set the object, so _q_resolve won't need to be called again.
+ setattr(container, internal_name, object)
+
+ elif type(container) is types.ModuleType:
+ # try importing it as a sub-module. If we get an ImportError
+ # here we don't catch it. It means that something that
+ # doesn't exist was exported or an exception was raised from
+ # deeper in the code.
+ mod_name = container.__name__ + '.' + internal_name
+ object = _get_module(mod_name)
+
+ else:
+ # a non-existent attribute is in _q_exports,
+ # and the container is not a module. Give up.
+ raise errors.TraversalError(
+ private_msg=("%r in _q_exports list, "
+ "but not found in %r" % (component,
+ container)))
+
+ namespace_stack.append(object)
+ return object
+
+
+def isstring(x):
+ return isinstance(x, (str, unicode, htmltext))
Property changes on: trunk/quixote/publish1.py
___________________________________________________________________
Name: svn:keywords
+ HeadURL Id