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('&nbsp;*')
+        r = TemplateIO(html=1)
+        r += htmltext('<tr><th colspan="3" align="left">')
+        r += title
+        r += htmltext('</th></tr>'
+                      '<tr><td>&nbsp;&nbsp;</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