Code

removal of the old cgi stuff
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 30 Aug 2002 08:49:15 +0000 (08:49 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 30 Aug 2002 08:49:15 +0000 (08:49 +0000)
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1014 57a73879-2fb5-44c3-a270-3262357dd7e2

roundup/cgi_client.py [deleted file]
roundup/cgitb.py [deleted file]
roundup/template_funcs.py [deleted file]
roundup/template_parser.py [deleted file]

diff --git a/roundup/cgi_client.py b/roundup/cgi_client.py
deleted file mode 100644 (file)
index 1f1c02d..0000000
+++ /dev/null
@@ -1,2486 +0,0 @@
-#
-# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
-# This module is free software, and you may redistribute it and/or modify
-# under the same terms as Python, so long as this copyright message and
-# disclaimer are retained in their original form.
-#
-# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
-# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
-# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
-# POSSIBILITY OF SUCH DAMAGE.
-#
-# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
-# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-# FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
-# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
-# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
-# 
-# $Id: cgi_client.py,v 1.162 2002-08-23 04:42:30 richard Exp $
-
-__doc__ = """
-WWW request handler (also used in the stand-alone server).
-"""
-
-import os, cgi, StringIO, urlparse, re, traceback, mimetypes, urllib
-import binascii, Cookie, time, random
-
-import roundupdb, htmltemplate, date, hyperdb, password
-from roundup.i18n import _
-
-class Unauthorised(ValueError):
-    pass
-
-class NotFound(ValueError):
-    pass
-
-def initialiseSecurity(security):
-    ''' Create some Permissions and Roles on the security object
-
-        This function is directly invoked by security.Security.__init__()
-        as a part of the Security object instantiation.
-    '''
-    security.addPermission(name="Web Registration",
-        description="User may register through the web")
-    p = security.addPermission(name="Web Access",
-        description="User may access the web interface")
-    security.addPermissionToRole('Admin', p)
-
-    # doing Role stuff through the web - make sure Admin can
-    p = security.addPermission(name="Web Roles",
-        description="User may manipulate user Roles through the web")
-    security.addPermissionToRole('Admin', p)
-
-class Client:
-    '''
-    A note about login
-    ------------------
-
-    If the user has no login cookie, then they are anonymous. There
-    are two levels of anonymous use. If there is no 'anonymous' user, there
-    is no login at all and the database is opened in read-only mode. If the
-    'anonymous' user exists, the user is logged in using that user (though
-    there is no cookie). This allows them to modify the database, and all
-    modifications are attributed to the 'anonymous' user.
-
-    Once a user logs in, they are assigned a session. The Client instance
-    keeps the nodeid of the session as the "session" attribute.
-    '''
-
-    def __init__(self, instance, request, env, form=None):
-        hyperdb.traceMark()
-        self.instance = instance
-        self.request = request
-        self.env = env
-        self.path = env['PATH_INFO']
-        self.split_path = self.path.split('/')
-        self.instance_path_name = env['INSTANCE_NAME']
-        url = self.env['SCRIPT_NAME'] + '/'
-        machine = self.env['SERVER_NAME']
-        port = self.env['SERVER_PORT']
-        if port != '80': machine = machine + ':' + port
-        self.base = urlparse.urlunparse(('http', env['HTTP_HOST'], url,
-            None, None, None))
-
-        if form is None:
-            self.form = cgi.FieldStorage(environ=env)
-        else:
-            self.form = form
-        self.headers_done = 0
-        try:
-            self.debug = int(env.get("ROUNDUP_DEBUG", 0))
-        except ValueError:
-            # someone gave us a non-int debug level, turn it off
-            self.debug = 0
-
-    def getuid(self):
-        try:
-            return self.db.user.lookup(self.user)
-        except KeyError:
-            if self.user is None:
-                # user is not logged in and username 'anonymous' doesn't
-                # exist in the database
-                err = _('anonymous users have read-only access only')
-            else:
-                err = _("sanity check: unknown user name `%s'")%self.user
-            raise Unauthorised, errmsg
-
-    def header(self, headers=None, response=200):
-        '''Put up the appropriate header.
-        '''
-        if headers is None:
-            headers = {'Content-Type':'text/html'}
-        if not headers.has_key('Content-Type'):
-            headers['Content-Type'] = 'text/html'
-        self.request.send_response(response)
-        for entry in headers.items():
-            self.request.send_header(*entry)
-        self.request.end_headers()
-        self.headers_done = 1
-        if self.debug:
-            self.headers_sent = headers
-
-    global_javascript = '''
-<script language="javascript">
-submitted = false;
-function submit_once() {
-    if (submitted) {
-        alert("Your request is being processed.\\nPlease be patient.");
-        return 0;
-    }
-    submitted = true;
-    return 1;
-}
-
-function help_window(helpurl, width, height) {
-    HelpWin = window.open('%(base)s%(instance_path_name)s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
-}
-
-</script>
-'''
-    def make_index_link(self, name):
-        '''Turn a configuration entry into a hyperlink...
-        '''
-        # get the link label and spec
-        spec = getattr(self.instance, name+'_INDEX')
-
-        d = {}
-        d[':sort'] = ','.join(map(urllib.quote, spec['SORT']))
-        d[':group'] = ','.join(map(urllib.quote, spec['GROUP']))
-        d[':filter'] = ','.join(map(urllib.quote, spec['FILTER']))
-        d[':columns'] = ','.join(map(urllib.quote, spec['COLUMNS']))
-        d[':pagesize'] = spec.get('PAGESIZE','50')
-
-        # snarf the filterspec
-        filterspec = spec['FILTERSPEC'].copy()
-
-        # now format the filterspec
-        for k, l in filterspec.items():
-            # fix up the CURRENT USER if needed (handle None too since that's
-            # the old flag value)
-            if l in (None, 'CURRENT USER'):
-                if not self.user:
-                    continue
-                l = [self.db.user.lookup(self.user)]
-
-            # add
-            d[urllib.quote(k)] = ','.join(map(urllib.quote, l))
-
-        # finally, format the URL
-        return '<a href="%s?%s">%s</a>'%(spec['CLASS'],
-            '&'.join([k+'='+v for k,v in d.items()]), spec['LABEL'])
-
-
-    def pagehead(self, title, message=None):
-        '''Display the page heading, with information about the tracker and
-            links to more information
-        '''
-
-        # include any important message
-        if message is not None:
-            message = _('<div class="system-msg">%(message)s</div>')%locals()
-        else:
-            message = ''
-
-        # style sheet (CSS)
-        style = open(os.path.join(self.instance.TEMPLATES, 'style.css')).read()
-
-        # figure who the user is
-        user_name = self.user
-        userid = self.db.user.lookup(user_name)
-        default_queries = 1
-        links = []
-        if user_name != 'anonymous':
-            try:
-                default_queries = self.db.user.get(userid, 'defaultqueries')
-            except KeyError:
-                pass
-
-        # figure all the header links
-        if default_queries:
-            if hasattr(self.instance, 'HEADER_INDEX_LINKS'):
-                for name in self.instance.HEADER_INDEX_LINKS:
-                    spec = getattr(self.instance, name + '_INDEX')
-                    # skip if we need to fill in the logged-in user id and
-                    # we're anonymous
-                    if (spec['FILTERSPEC'].has_key('assignedto') and
-                            spec['FILTERSPEC']['assignedto'] in ('CURRENT USER',
-                            None) and user_name == 'anonymous'):
-                        continue
-                    links.append(self.make_index_link(name))
-            else:
-                # no config spec - hard-code
-                links = [
-                    _('All <a href="issue?status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=-activity&:filter=status&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>'),
-                    _('Unassigned <a href="issue?assignedto=-1&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=-activity&:filter=status,assignedto&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>')
-                ]
-
-        user_info = _('<a href="login">Login</a>')
-        add_links = ''
-        if user_name != 'anonymous':
-            # add any personal queries to the menu
-            try:
-                queries = self.db.getclass('query')
-            except KeyError:
-                # no query class
-                queries = self.instance.dbinit.Class(self.db, "query",
-                    klass=hyperdb.String(), name=hyperdb.String(),
-                    url=hyperdb.String())
-                queries.setkey('name')
-                #queries.disableJournalling()
-            try:
-                qids = self.db.getclass('user').get(userid, 'queries')
-            except KeyError, e:
-                #self.db.getclass('user').addprop(queries=hyperdb.Multilink('query'))
-                qids = []
-            for qid in qids:
-                links.append('<a href=%s?%s>%s</a>'%(queries.get(qid, 'klass'),
-                    queries.get(qid, 'url'), queries.get(qid, 'name')))
-
-            # if they're logged in, include links to their information,
-            # and the ability to add an issue
-            user_info = _('''
-<a href="user%(userid)s">My Details</a> | <a href="logout">Logout</a>
-''')%locals()
-
-        # figure the "add class" links
-        if hasattr(self.instance, 'HEADER_ADD_LINKS'):
-            classes = self.instance.HEADER_ADD_LINKS
-        else:
-            classes = ['issue']
-        l = []
-        for class_name in classes:
-            # make sure the user has permission to add
-            if not self.db.security.hasPermission('Edit', userid, class_name):
-                continue
-            cap_class = class_name.capitalize()
-            links.append(_('Add <a href="new%(class_name)s">'
-                '%(cap_class)s</a>')%locals())
-
-        # if the user can edit everything, include the links
-        admin_links = ''
-        userid = self.db.user.lookup(user_name)
-        if self.db.security.hasPermission('Edit', userid):
-            links.append(_('<a href="list_classes">Class List</a>'))
-            links.append(_('<a href="user?:sort=username&:group=roles">User List</a>'))
-            links.append(_('<a href="newuser">Add User</a>'))
-
-        # add the search links
-        if hasattr(self.instance, 'HEADER_SEARCH_LINKS'):
-            classes = self.instance.HEADER_SEARCH_LINKS
-        else:
-            classes = ['issue']
-        l = []
-        for class_name in classes:
-            # make sure the user has permission to view
-            if not self.db.security.hasPermission('View', userid, class_name):
-                continue
-            cap_class = class_name.capitalize()
-            links.append(_('Search <a href="search%(class_name)s">'
-                '%(cap_class)s</a>')%locals())
-
-        # now we have all the links, join 'em
-        links = '\n | '.join(links)
-
-        # include the javascript bit
-        global_javascript = self.global_javascript%self.__dict__
-
-        # finally, format the header
-        self.write(_('''<html><head>
-<title>%(title)s</title>
-<style type="text/css">%(style)s</style>
-</head>
-%(global_javascript)s
-<body bgcolor=#ffffff>
-%(message)s
-<table width=100%% border=0 cellspacing=0 cellpadding=2>
- <tr class="location-bar">
-  <td><big><strong>%(title)s</strong></big></td>
-  <td align=right valign=bottom>%(user_name)s</td>
- </tr>
- <tr class="location-bar">
-  <td align=left>%(links)s</td>
-  <td align=right>%(user_info)s</td>
- </tr>
-</table><br>
-''')%locals())
-
-    def pagefoot(self):
-        if self.debug:
-            self.write(_('<hr><small><dl><dt><b>Path</b></dt>'))
-            self.write('<dd>%s</dd>'%(', '.join(map(repr, self.split_path))))
-            keys = self.form.keys()
-            keys.sort()
-            if keys:
-                self.write(_('<dt><b>Form entries</b></dt>'))
-                for k in self.form.keys():
-                    v = self.form.getvalue(k, "<empty>")
-                    if type(v) is type([]):
-                        # Multiple username fields specified
-                        v = "|".join(v)
-                    self.write('<dd><em>%s</em>=%s</dd>'%(k, cgi.escape(v)))
-            keys = self.headers_sent.keys()
-            keys.sort()
-            self.write(_('<dt><b>Sent these HTTP headers</b></dt>'))
-            for k in keys:
-                v = self.headers_sent[k]
-                self.write('<dd><em>%s</em>=%s</dd>'%(k, cgi.escape(v)))
-            keys = self.env.keys()
-            keys.sort()
-            self.write(_('<dt><b>CGI environment</b></dt>'))
-            for k in keys:
-                v = self.env[k]
-                self.write('<dd><em>%s</em>=%s</dd>'%(k, cgi.escape(v)))
-            self.write('</dl></small>')
-        self.write('</body></html>')
-
-    def write(self, content):
-        if not self.headers_done:
-            self.header()
-        self.request.wfile.write(content)
-
-    def index_arg(self, arg):
-        ''' handle the args to index - they might be a list from the form
-            (ie. submitted from a form) or they might be a command-separated
-            single string (ie. manually constructed GET args)
-        '''
-        if self.form.has_key(arg):
-            arg =  self.form[arg]
-            if type(arg) == type([]):
-                return [arg.value for arg in arg]
-            return arg.value.split(',')
-        return []
-
-    def index_sort(self):
-        # first try query string / simple form
-        x = self.index_arg(':sort')
-        if x:
-            if self.index_arg(':descending'):
-                return ['-'+x[0]]
-            return x
-        # nope - get the specs out of the form
-        specs = []
-        for colnm in self.db.getclass(self.classname).getprops().keys():
-            desc = ''
-            try:
-                spec = self.form[':%s_ss' % colnm]
-            except KeyError:
-                continue
-            spec = spec.value
-            if spec:
-                if spec[-1] == '-':
-                    desc='-'
-                    spec = spec[0]
-                specs.append((int(spec), colnm, desc))
-        specs.sort()
-        x = []
-        for _, colnm, desc in specs:
-            x.append('%s%s' % (desc, colnm))
-        return x
-    
-    def index_filterspec(self, filter, classname=None):
-        ''' pull the index filter spec from the form
-
-        Links and multilinks want to be lists - the rest are straight
-        strings.
-        '''
-        if classname is None:
-            classname = self.classname
-        klass = self.db.getclass(classname)
-        filterspec = {}
-        props = klass.getprops()
-        for colnm in filter:
-            widget = ':%s_fs' % colnm
-            try:
-                val = self.form[widget]
-            except KeyError:
-                try:
-                    val = self.form[colnm]
-                except KeyError:
-                    # they checked the filter box but didn't enter a value
-                    continue
-            propdescr = props.get(colnm, None)
-            if propdescr is None:
-                print "huh? %r is in filter & form, but not in Class!" % colnm
-                raise "butthead programmer"
-            if (isinstance(propdescr, hyperdb.Link) or
-                isinstance(propdescr, hyperdb.Multilink)):
-                if type(val) == type([]):
-                    val = [arg.value for arg in val]
-                else:
-                    val = val.value.split(',')
-                l = filterspec.get(colnm, [])
-                l = l + val
-                filterspec[colnm] = l
-            else:
-                filterspec[colnm] = val.value
-            
-        return filterspec
-    
-    def customization_widget(self):
-        ''' The customization widget is visible by default. The widget
-            visibility is remembered by show_customization.  Visibility
-            is not toggled if the action value is "Redisplay"
-        '''
-        if not self.form.has_key('show_customization'):
-            visible = 1
-        else:
-            visible = int(self.form['show_customization'].value)
-            if self.form.has_key('action'):
-                if self.form['action'].value != 'Redisplay':
-                    visible = self.form['action'].value == '+'
-            
-        return visible
-
-    # TODO: make this go away some day...
-    default_index_sort = ['-activity']
-    default_index_group = ['priority']
-    default_index_filter = ['status']
-    default_index_columns = ['id','activity','title','status','assignedto']
-    default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
-    default_pagesize = '50'
-
-    def _get_customisation_info(self):
-        # see if the web has supplied us with any customisation info
-        for key in ':sort', ':group', ':filter', ':columns', ':pagesize':
-            if self.form.has_key(key):
-                # make list() extract the info from the CGI environ
-                self.classname = 'issue'
-                sort = group = filter = columns = filterspec = pagesize = None
-                break
-        else:
-            # TODO: look up the session first
-            # try the instance config first
-            if hasattr(self.instance, 'DEFAULT_INDEX'):
-                d = self.instance.DEFAULT_INDEX
-                self.classname = d['CLASS']
-                sort = d['SORT']
-                group = d['GROUP']
-                filter = d['FILTER']
-                columns = d['COLUMNS']
-                filterspec = d['FILTERSPEC']
-                pagesize = d.get('PAGESIZE', '50')
-            else:
-                # nope - fall back on the old way of doing it
-                self.classname = 'issue'
-                sort = self.default_index_sort
-                group = self.default_index_group
-                filter = self.default_index_filter
-                columns = self.default_index_columns
-                filterspec = self.default_index_filterspec
-                pagesize = self.default_pagesize
-        return columns, filter, group, sort, filterspec, pagesize
-
-    def index(self):
-        ''' put up an index - no class specified
-        '''
-        columns, filter, group, sort, filterspec, pagesize = \
-            self._get_customisation_info()
-        return self.list(columns=columns, filter=filter, group=group,
-            sort=sort, filterspec=filterspec, pagesize=pagesize)
-
-    def searchnode(self):
-        columns, filter, group, sort, filterspec, pagesize = \
-            self._get_customisation_info()
-        cn = self.classname
-        self.pagehead(_('%(instancename)s: Index of %(classname)s')%{
-            'classname': cn, 'instancename': self.instance.INSTANCE_NAME})
-        index = htmltemplate.IndexTemplate(self, self.instance.TEMPLATES, cn)
-        self.write('<form onSubmit="return submit_once()" action="%s">\n'%self.classname)
-        all_columns = self.db.getclass(cn).getprops().keys()
-        all_columns.sort()
-        index.filter_section('', filter, columns, group, all_columns, sort,
-                             filterspec, pagesize, 0, 0)
-        self.pagefoot()
-
-    # XXX deviates from spec - loses the '+' (that's a reserved character
-    # in URLS
-    def list(self, sort=None, group=None, filter=None, columns=None,
-            filterspec=None, show_customization=None, show_nodes=1,
-            pagesize=None):
-        ''' call the template index with the args
-
-            :sort    - sort by prop name, optionally preceeded with '-'
-                     to give descending or nothing for ascending sorting.
-            :group   - group by prop name, optionally preceeded with '-' or
-                     to sort in descending or nothing for ascending order.
-            :filter  - selects which props should be displayed in the filter
-                     section. Default is all.
-            :columns - selects the columns that should be displayed.
-                     Default is all.
-
-        '''
-        cn = self.classname
-        cl = self.db.classes[cn]
-        if sort is None: sort = self.index_sort()
-        if group is None: group = self.index_arg(':group')
-        if filter is None: filter = self.index_arg(':filter')
-        if columns is None: columns = self.index_arg(':columns')
-        if filterspec is None: filterspec = self.index_filterspec(filter)
-        if show_customization is None:
-            show_customization = self.customization_widget()
-        if self.form.has_key('search_text'):
-            search_text = self.form['search_text'].value
-        else:
-            search_text = ''
-        if pagesize is None:
-            if self.form.has_key(':pagesize'):
-                pagesize = self.form[':pagesize'].value
-            else:
-                pagesize = '50'
-        pagesize = int(pagesize)
-        if self.form.has_key(':startwith'):
-            startwith = int(self.form[':startwith'].value)
-        else:
-            startwith = 0
-        simpleform = 1
-        if self.form.has_key(':advancedsearch'):
-            simpleform = 0
-
-        if self.form.has_key('Query') and self.form['Query'].value == 'Save':
-            # format a query string
-            qd = {}
-            qd[':sort'] = ','.join(map(urllib.quote, sort))
-            qd[':group'] = ','.join(map(urllib.quote, group))
-            qd[':filter'] = ','.join(map(urllib.quote, filter))
-            qd[':columns'] = ','.join(map(urllib.quote, columns))
-            for k, l in filterspec.items():
-                qd[urllib.quote(k)] = ','.join(map(urllib.quote, l))
-            url = '&'.join([k+'='+v for k,v in qd.items()])
-            url += '&:pagesize=%s' % pagesize
-            if search_text:
-                url += '&search_text=%s' % search_text
-
-            # create a query
-            d = {}
-            d['name'] = nm = self.form[':name'].value
-            if not nm:
-                d['name'] = nm = 'New Query'
-            d['klass'] = self.form[':classname'].value
-            d['url'] = url
-            qid = self.db.getclass('query').create(**d)
-
-            # and add it to the user's query multilink
-            uid = self.getuid()
-            usercl = self.db.getclass('user')
-            queries = usercl.get(uid, 'queries')
-            queries.append(qid)
-            usercl.set(uid, queries=queries)
-            
-        self.pagehead(_('%(instancename)s: Index of %(classname)s')%{
-            'classname': cn, 'instancename': self.instance.INSTANCE_NAME})
-
-        index = htmltemplate.IndexTemplate(self, self.instance.TEMPLATES, cn)
-        try:
-            index.render(filterspec=filterspec, search_text=search_text,
-                filter=filter, columns=columns, sort=sort, group=group,
-                show_customization=show_customization, 
-                show_nodes=show_nodes, pagesize=pagesize, startwith=startwith,
-                simple_search=simpleform)
-        except htmltemplate.MissingTemplateError:
-            self.basicClassEditPage()
-        self.pagefoot()
-
-    def basicClassEditPage(self):
-        '''Display a basic edit page that allows simple editing of the
-           nodes of the current class
-        '''
-        userid = self.db.user.lookup(self.user)
-        if not self.db.security.hasPermission('Edit', userid):
-            raise Unauthorised, _("You do not have permission to access"\
-                        " %(action)s.")%{'action': self.classname}
-        w = self.write
-        cn = self.classname
-        cl = self.db.classes[cn]
-        idlessprops = cl.getprops(protected=0).keys()
-        props = ['id'] + idlessprops
-
-        # get the CSV module
-        try:
-            import csv
-        except ImportError:
-            w(_('Sorry, you need the csv module to use this function.<br>\n'
-                'Get it from: <a href="http://www.object-craft.com.au/projects/csv/">http://www.object-craft.com.au/projects/csv/'))
-            return
-
-        # do the edit
-        if self.form.has_key('rows'):
-            rows = self.form['rows'].value.splitlines()
-            p = csv.parser()
-            found = {}
-            line = 0
-            for row in rows:
-                line += 1
-                values = p.parse(row)
-                # not a complete row, keep going
-                if not values: continue
-
-                # extract the nodeid
-                nodeid, values = values[0], values[1:]
-                found[nodeid] = 1
-
-                # confirm correct weight
-                if len(idlessprops) != len(values):
-                    w(_('Not enough values on line %(line)s'%{'line':line}))
-                    return
-
-                # extract the new values
-                d = {}
-                for name, value in zip(idlessprops, values):
-                    value = value.strip()
-                    # only add the property if it has a value
-                    if value:
-                        # if it's a multilink, split it
-                        if isinstance(cl.properties[name], hyperdb.Multilink):
-                            value = value.split(':')
-                        d[name] = value
-
-                # perform the edit
-                if cl.hasnode(nodeid):
-                    # edit existing
-                    cl.set(nodeid, **d)
-                else:
-                    # new node
-                    found[cl.create(**d)] = 1
-
-            # retire the removed entries
-            for nodeid in cl.list():
-                if not found.has_key(nodeid):
-                    cl.retire(nodeid)
-
-        w(_('''<p class="form-help">You may edit the contents of the
-        "%(classname)s" class using this form. Commas, newlines and double
-        quotes (") must be handled delicately. You may include commas and
-        newlines by enclosing the values in double-quotes ("). Double
-        quotes themselves must be quoted by doubling ("").</p>
-        <p class="form-help">Multilink properties have their multiple
-        values colon (":") separated (... ,"one:two:three", ...)</p>
-        <p class="form-help">Remove entries by deleting their line. Add
-        new entries by appending
-        them to the table - put an X in the id column.</p>''')%{'classname':cn})
-
-        l = []
-        for name in props:
-            l.append(name)
-        w('<tt>')
-        w(', '.join(l) + '\n')
-        w('</tt>')
-
-        w('<form onSubmit="return submit_once()" method="POST">')
-        w('<textarea name="rows" cols=80 rows=15>')
-        p = csv.parser()
-        for nodeid in cl.list():
-            l = []
-            for name in props:
-                value = cl.get(nodeid, name)
-                if value is None:
-                    l.append('')
-                elif isinstance(value, type([])):
-                    l.append(cgi.escape(':'.join(map(str, value))))
-                else:
-                    l.append(cgi.escape(str(cl.get(nodeid, name))))
-            w(p.join(l) + '\n')
-
-        w(_('</textarea><br><input type="submit" value="Save Changes"></form>'))
-
-    def classhelp(self):
-        '''Display a table of class info
-        '''
-        w = self.write
-        cn = self.form['classname'].value
-        cl = self.db.classes[cn]
-        props = self.form['properties'].value.split(',')
-        if cl.labelprop(1) in props:
-            sort = [cl.labelprop(1)]
-        else:
-            sort = props[0]
-
-        w('<table border=1 cellspacing=0 cellpaddin=2>')
-        w('<tr>')
-        for name in props:
-            w('<th align=left>%s</th>'%name)
-        w('</tr>')
-        for nodeid in cl.filter(None, {}, sort, []):
-            w('<tr>')
-            for name in props:
-                value = cgi.escape(str(cl.get(nodeid, name)))
-                w('<td align="left" valign="top">%s</td>'%value)
-            w('</tr>')
-        w('</table>')
-
-    def shownode(self, message=None, num_re=re.compile('^\d+$')):
-        ''' display an item
-        '''
-        cn = self.classname
-        cl = self.db.classes[cn]
-        keys = self.form.keys()
-        fromremove = 0
-        if self.form.has_key(':multilink'):
-            # is the multilink there because we came from remove()?
-            if self.form.has_key(':target'):
-                xtra = ''
-                fromremove = 1
-                message = _('%s removed' % self.index_arg(":target")[0])
-            else:
-                link = self.form[':multilink'].value
-                designator, linkprop = link.split(':')
-                xtra = ' for <a href="%s">%s</a>' % (designator, designator)
-        else:
-            xtra = ''
-        
-        # possibly perform an edit
-        # don't try to set properties if the user has just logged in
-        if keys and not fromremove and not self.form.has_key('__login_name'):
-            try:
-                userid = self.db.user.lookup(self.user)
-                if not self.db.security.hasPermission('Edit', userid, cn):
-                    message = _('You do not have permission to edit %s' %cn)
-                else:
-                    props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
-                    # make changes to the node
-                    props = self._changenode(props)
-                    # handle linked nodes 
-                    self._post_editnode(self.nodeid)
-                    # and some nice feedback for the user
-                    if props:
-                        message = _('%(changes)s edited ok')%{'changes':
-                            ', '.join(props.keys())}
-                    elif self.form.has_key('__note') and self.form['__note'].value:
-                        message = _('note added')
-                    elif (self.form.has_key('__file') and
-                            self.form['__file'].filename):
-                        message = _('file added')
-                    else:
-                        message = _('nothing changed')
-            except:
-                self.db.rollback()
-                s = StringIO.StringIO()
-                traceback.print_exc(None, s)
-                message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
-
-        # now the display
-        id = self.nodeid
-        if cl.getkey():
-            id = cl.get(id, cl.getkey())
-        self.pagehead('%s: %s %s'%(self.classname.capitalize(), id, xtra),
-            message)
-
-        nodeid = self.nodeid
-
-        # use the template to display the item
-        item = htmltemplate.ItemTemplate(self, self.instance.TEMPLATES,
-            self.classname)
-        item.render(nodeid)
-
-        self.pagefoot()
-    showissue = shownode
-    showmsg = shownode
-    searchissue = searchnode
-
-    def showquery(self):
-        queries = self.db.getclass(self.classname)
-        if self.form.keys():
-            sort = self.index_sort()
-            group = self.index_arg(':group')
-            filter = self.index_arg(':filter')
-            columns = self.index_arg(':columns')
-            filterspec = self.index_filterspec(filter, queries.get(self.nodeid, 'klass'))
-            if self.form.has_key('search_text'):
-                search_text = self.form['search_text'].value
-                search_text = urllib.quote(search_text)
-            else:
-                search_text = ''
-            if self.form.has_key(':pagesize'):
-                pagesize = int(self.form[':pagesize'].value)
-            else:
-                pagesize = 50
-            # format a query string
-            qd = {}
-            qd[':sort'] = ','.join(map(urllib.quote, sort))
-            qd[':group'] = ','.join(map(urllib.quote, group))
-            qd[':filter'] = ','.join(map(urllib.quote, filter))
-            qd[':columns'] = ','.join(map(urllib.quote, columns))
-            for k, l in filterspec.items():
-                qd[urllib.quote(k)] = ','.join(map(urllib.quote, l))
-            url = '&'.join([k+'='+v for k,v in qd.items()])
-            url += '&:pagesize=%s' % pagesize
-            if search_text:
-                url += '&search_text=%s' % search_text
-            if url != queries.get(self.nodeid, 'url'):
-                queries.set(self.nodeid, url=url)
-                message = _('url edited ok')
-            else:
-                message = _('nothing changed')
-        else:
-            message = None
-        nm = queries.get(self.nodeid, 'name')
-        self.pagehead('%s: %s'%(self.classname.capitalize(), nm), message)
-
-        # use the template to display the item
-        item = htmltemplate.ItemTemplate(self, self.instance.TEMPLATES,
-            self.classname)
-        item.render(self.nodeid)
-        self.pagefoot()
-        
-    def _changenode(self, props):
-        ''' change the node based on the contents of the form
-        '''
-        cl = self.db.classes[self.classname]
-
-        # create the message
-        message, files = self._handle_message()
-        if message:
-            props['messages'] = cl.get(self.nodeid, 'messages') + [message]
-        if files:
-            props['files'] = cl.get(self.nodeid, 'files') + files
-
-        # make the changes
-        return cl.set(self.nodeid, **props)
-
-    def _createnode(self):
-        ''' create a node based on the contents of the form
-        '''
-        cl = self.db.classes[self.classname]
-        props = parsePropsFromForm(self.db, cl, self.form)
-
-        # check for messages and files
-        message, files = self._handle_message()
-        if message:
-            props['messages'] = [message]
-        if files:
-            props['files'] = files
-        # create the node and return it's id
-        return cl.create(**props)
-
-    def _handle_message(self):
-        ''' generate an edit message
-        '''
-        # handle file attachments 
-        files = []
-        if self.form.has_key('__file'):
-            file = self.form['__file']
-            if file.filename:
-                filename = file.filename.split('\\')[-1]
-                mime_type = mimetypes.guess_type(filename)[0]
-                if not mime_type:
-                    mime_type = "application/octet-stream"
-                # create the new file entry
-                files.append(self.db.file.create(type=mime_type,
-                    name=filename, content=file.file.read()))
-
-        # we don't want to do a message if none of the following is true...
-        cn = self.classname
-        cl = self.db.classes[self.classname]
-        props = cl.getprops()
-        note = None
-        # in a nutshell, don't do anything if there's no note or there's no
-        # NOSY
-        if self.form.has_key('__note'):
-            note = self.form['__note'].value.strip()
-        if not note:
-            return None, files
-        if not props.has_key('messages'):
-            return None, files
-        if not isinstance(props['messages'], hyperdb.Multilink):
-            return None, files
-        if not props['messages'].classname == 'msg':
-            return None, files
-        if not (self.form.has_key('nosy') or note):
-            return None, files
-
-        # handle the note
-        if '\n' in note:
-            summary = re.split(r'\n\r?', note)[0]
-        else:
-            summary = note
-        m = ['%s\n'%note]
-
-        # handle the messageid
-        # TODO: handle inreplyto
-        messageid = "<%s.%s.%s@%s>"%(time.time(), random.random(),
-            self.classname, self.instance.MAIL_DOMAIN)
-
-        # now create the message, attaching the files
-        content = '\n'.join(m)
-        message_id = self.db.msg.create(author=self.getuid(),
-            recipients=[], date=date.Date('.'), summary=summary,
-            content=content, files=files, messageid=messageid)
-
-        # update the messages property
-        return message_id, files
-
-    def _post_editnode(self, nid):
-        '''Do the linking part of the node creation.
-
-           If a form element has :link or :multilink appended to it, its
-           value specifies a node designator and the property on that node
-           to add _this_ node to as a link or multilink.
-
-           This is typically used on, eg. the file upload page to indicated
-           which issue to link the file to.
-
-           TODO: I suspect that this and newfile will go away now that
-           there's the ability to upload a file using the issue __file form
-           element!
-        '''
-        cn = self.classname
-        cl = self.db.classes[cn]
-        # link if necessary
-        keys = self.form.keys()
-        for key in keys:
-            if key == ':multilink':
-                value = self.form[key].value
-                if type(value) != type([]): value = [value]
-                for value in value:
-                    designator, property = value.split(':')
-                    link, nodeid = hyperdb.splitDesignator(designator)
-                    link = self.db.classes[link]
-                    # take a dupe of the list so we're not changing the cache
-                    value = link.get(nodeid, property)[:]
-                    value.append(nid)
-                    link.set(nodeid, **{property: value})
-            elif key == ':link':
-                value = self.form[key].value
-                if type(value) != type([]): value = [value]
-                for value in value:
-                    designator, property = value.split(':')
-                    link, nodeid = hyperdb.splitDesignator(designator)
-                    link = self.db.classes[link]
-                    link.set(nodeid, **{property: nid})
-
-    def newnode(self, message=None):
-        ''' Add a new node to the database.
-        
-        The form works in two modes: blank form and submission (that is,
-        the submission goes to the same URL). **Eventually this means that
-        the form will have previously entered information in it if
-        submission fails.
-
-        The new node will be created with the properties specified in the
-        form submission. For multilinks, multiple form entries are handled,
-        as are prop=value,value,value. You can't mix them though.
-
-        If the new node is to be referenced from somewhere else immediately
-        (ie. the new node is a file that is to be attached to a support
-        issue) then supply one of these arguments in addition to the usual
-        form entries:
-            :link=designator:property
-            :multilink=designator:property
-        ... which means that once the new node is created, the "property"
-        on the node given by "designator" should now reference the new
-        node's id. The node id will be appended to the multilink.
-        '''
-        cn = self.classname
-        userid = self.db.user.lookup(self.user)
-        if not self.db.security.hasPermission('View', userid, cn):
-            raise Unauthorised, _("You do not have permission to access"\
-                        " %(action)s.")%{'action': self.classname}
-        cl = self.db.classes[cn]
-        if self.form.has_key(':multilink'):
-            link = self.form[':multilink'].value
-            designator, linkprop = link.split(':')
-            xtra = ' for <a href="%s">%s</a>' % (designator, designator)
-        else:
-            xtra = ''
-
-        # possibly perform a create
-        keys = self.form.keys()
-        if [i for i in keys if i[0] != ':']:
-            # no dice if you can't edit!
-            if not self.db.security.hasPermission('Edit', userid, cn):
-                raise Unauthorised, _("You do not have permission to access"\
-                            " %(action)s.")%{'action': 'new'+self.classname}
-            props = {}
-            try:
-                nid = self._createnode()
-                # handle linked nodes 
-                self._post_editnode(nid)
-                # and some nice feedback for the user
-                message = _('%(classname)s created ok')%{'classname': cn}
-
-                # render the newly created issue
-                self.db.commit()
-                self.nodeid = nid
-                self.pagehead('%s: %s'%(self.classname.capitalize(), nid),
-                    message)
-                item = htmltemplate.ItemTemplate(self, self.instance.TEMPLATES, 
-                    self.classname)
-                item.render(nid)
-                self.pagefoot()
-                return
-            except:
-                self.db.rollback()
-                s = StringIO.StringIO()
-                traceback.print_exc(None, s)
-                message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
-        self.pagehead(_('New %(classname)s %(xtra)s')%{
-                'classname': self.classname.capitalize(),
-                'xtra': xtra }, message)
-
-        # call the template
-        newitem = htmltemplate.NewItemTemplate(self, self.instance.TEMPLATES,
-            self.classname)
-        newitem.render(self.form)
-
-        self.pagefoot()
-    newissue = newnode
-
-    def newuser(self, message=None):
-        ''' Add a new user to the database.
-
-            Don't do any of the message or file handling, just create the node.
-        '''
-        userid = self.db.user.lookup(self.user)
-        if not self.db.security.hasPermission('Edit', userid, 'user'):
-            raise Unauthorised, _("You do not have permission to access"\
-                        " %(action)s.")%{'action': 'newuser'}
-
-        cn = self.classname
-        cl = self.db.classes[cn]
-
-        # possibly perform a create
-        keys = self.form.keys()
-        if [i for i in keys if i[0] != ':']:
-            try:
-                props = parsePropsFromForm(self.db, cl, self.form)
-                nid = cl.create(**props)
-                # handle linked nodes 
-                self._post_editnode(nid)
-                # and some nice feedback for the user
-                message = _('%(classname)s created ok')%{'classname': cn}
-            except:
-                self.db.rollback()
-                s = StringIO.StringIO()
-                traceback.print_exc(None, s)
-                message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
-        self.pagehead(_('New %(classname)s')%{'classname':
-             self.classname.capitalize()}, message)
-
-        # call the template
-        newitem = htmltemplate.NewItemTemplate(self, self.instance.TEMPLATES,
-            self.classname)
-        newitem.render(self.form)
-
-        self.pagefoot()
-
-    def newfile(self, message=None):
-        ''' Add a new file to the database.
-        
-        This form works very much the same way as newnode - it just has a
-        file upload.
-        '''
-        userid = self.db.user.lookup(self.user)
-        if not self.db.security.hasPermission('Edit', userid, 'file'):
-            raise Unauthorised, _("You do not have permission to access"\
-                        " %(action)s.")%{'action': 'newfile'}
-        cn = self.classname
-        cl = self.db.classes[cn]
-        props = parsePropsFromForm(self.db, cl, self.form)
-        if self.form.has_key(':multilink'):
-            link = self.form[':multilink'].value
-            designator, linkprop = link.split(':')
-            xtra = ' for <a href="%s">%s</a>' % (designator, designator)
-        else:
-            xtra = ''
-
-        # possibly perform a create
-        keys = self.form.keys()
-        if [i for i in keys if i[0] != ':']:
-            try:
-                file = self.form['content']
-                mime_type = mimetypes.guess_type(file.filename)[0]
-                if not mime_type:
-                    mime_type = "application/octet-stream"
-                # save the file
-                props['type'] = mime_type
-                props['name'] = file.filename
-                props['content'] = file.file.read()
-                nid = cl.create(**props)
-                # handle linked nodes
-                self._post_editnode(nid)
-                # and some nice feedback for the user
-                message = _('%(classname)s created ok')%{'classname': cn}
-            except:
-                self.db.rollback()
-                s = StringIO.StringIO()
-                traceback.print_exc(None, s)
-                message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
-
-        self.pagehead(_('New %(classname)s %(xtra)s')%{
-                'classname': self.classname.capitalize(),
-                'xtra': xtra }, message)
-        newitem = htmltemplate.NewItemTemplate(self, self.instance.TEMPLATES,
-            self.classname)
-        newitem.render(self.form)
-        self.pagefoot()
-
-    def showuser(self, message=None, num_re=re.compile('^\d+$')):
-        '''Display a user page for editing. Make sure the user is allowed
-           to edit this node, and also check for password changes.
-
-           Note: permission checks for this node are handled in the template.
-        '''
-        user = self.db.user
-
-        # get the username of the node being edited
-        try:
-            node_user = user.get(self.nodeid, 'username')
-        except IndexError:
-            raise NotFound, 'user%s'%self.nodeid
-
-        #
-        # perform any editing
-        #
-        keys = self.form.keys()
-        if keys:
-            try:
-                props = parsePropsFromForm(self.db, user, self.form,
-                    self.nodeid)
-                set_cookie = 0
-                if props.has_key('password'):
-                    password = self.form['password'].value.strip()
-                    if not password:
-                        # no password was supplied - don't change it
-                        del props['password']
-                    elif self.nodeid == self.getuid():
-                        # this is the logged-in user's password
-                        set_cookie = password
-                user.set(self.nodeid, **props)
-                # and some feedback for the user
-                message = _('%(changes)s edited ok')%{'changes':
-                    ', '.join(props.keys())}
-            except:
-                self.db.rollback()
-                s = StringIO.StringIO()
-                traceback.print_exc(None, s)
-                message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
-        else:
-            set_cookie = 0
-
-        # fix the cookie if the password has changed
-        if set_cookie:
-            self.set_cookie(self.user, set_cookie)
-
-        #
-        # now the display
-        #
-        self.pagehead(_('User: %(user)s')%{'user': node_user}, message)
-
-        # use the template to display the item
-        item = htmltemplate.ItemTemplate(self, self.instance.TEMPLATES, 'user')
-        item.render(self.nodeid)
-        self.pagefoot()
-
-    def showfile(self):
-        ''' display a file
-        '''
-       # nothing in xtrapath - edit the file's metadata
-        if self.xtrapath is None:
-            return self.shownode()
-
-        # something in xtrapath - download the file    
-        nodeid = self.nodeid
-        cl = self.db.classes[self.classname]
-        try:
-            mime_type = cl.get(nodeid, 'type')
-        except IndexError:
-            raise NotFound, 'file%s'%nodeid
-        if mime_type == 'message/rfc822':
-            mime_type = 'text/plain'
-        self.header(headers={'Content-Type': mime_type})
-        self.write(cl.get(nodeid, 'content'))
-        
-    def permission(self):
-        '''
-        '''
-
-    def classes(self, message=None):
-        ''' display a list of all the classes in the database
-        '''
-        userid = self.db.user.lookup(self.user)
-        if not self.db.security.hasPermission('Edit', userid):
-            raise Unauthorised, _("You do not have permission to access"\
-                        " %(action)s.")%{'action': 'all classes'}
-
-        self.pagehead(_('Table of classes'), message)
-        classnames = self.db.classes.keys()
-        classnames.sort()
-        self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
-        for cn in classnames:
-            cl = self.db.getclass(cn)
-            self.write('<tr class="list-header"><th colspan=2 align=left>'
-                '<a href="%s">%s</a></th></tr>'%(cn, cn.capitalize()))
-            for key, value in cl.properties.items():
-                if value is None: value = ''
-                else: value = str(value)
-                self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
-                    key, cgi.escape(value)))
-        self.write('</table>')
-        self.pagefoot()
-
-    def unauthorised(self, message):
-        ''' The user is not authorised to do something. If they're
-            anonymous, throw up a login box. If not, just tell them they
-            can't do whatever it was they were trying to do.
-
-            Bot cases print up the message, which is most likely the
-            argument to the Unauthorised exception.
-        '''
-        self.header(response=403)
-        if self.desired_action is None or self.desired_action == 'login':
-            if not message:
-                message=_("You do not have permission.")
-            action = 'index'
-        else:
-            if not message:
-                message=_("You do not have permission to access"\
-                    " %(action)s.")%{'action': self.desired_action}
-            action = self.desired_action
-        if self.user == 'anonymous':
-            self.login(action=action, message=message)
-        else:
-            self.pagehead(_('Not Authorised'))
-            self.write('<p class="system-msg">%s</p>'%message)
-            self.pagefoot()
-
-    def login(self, message=None, newuser_form=None, action='index'):
-        '''Display a login page.
-        '''
-        self.pagehead(_('Login to roundup'))
-        if message:
-            self.write('<p class="system-msg">%s</p>'%message)
-        self.write(_('''
-<table>
-<tr><td colspan=2 class="strong-header">Existing User Login</td></tr>
-<form onSubmit="return submit_once()" action="login_action" method=POST>
-<input type="hidden" name="__destination_url" value="%(action)s">
-<tr><td align=right>Login name: </td>
-    <td><input name="__login_name"></td></tr>
-<tr><td align=right>Password: </td>
-    <td><input type="password" name="__login_password"></td></tr>
-<tr><td></td>
-    <td><input type="submit" value="Log In"></td></tr>
-</form>
-''')%locals())
-        userid = self.db.user.lookup(self.user)
-        if not self.db.security.hasPermission('Web Registration', userid):
-            self.write('</table>')
-            self.pagefoot()
-            return
-        values = {'realname': '', 'organisation': '', 'address': '',
-            'phone': '', 'username': '', 'password': '', 'confirm': '',
-            'action': action, 'alternate_addresses': ''}
-        if newuser_form is not None:
-            for key in newuser_form.keys():
-                values[key] = newuser_form[key].value
-        self.write(_('''
-<p>
-<tr><td colspan=2 class="strong-header">New User Registration</td></tr>
-<tr><td colspan=2><em>marked items</em> are optional...</td></tr>
-<form onSubmit="return submit_once()" action="newuser_action" method=POST>
-<input type="hidden" name="__destination_url" value="%(action)s">
-<tr><td align=right><em>Name: </em></td>
-    <td><input name="realname" value="%(realname)s" size=40></td></tr>
-<tr><td align=right><em>Organisation: </em></td>
-    <td><input name="organisation" value="%(organisation)s" size=40></td></tr>
-<tr><td align=right>E-Mail Address: </td>
-    <td><input name="address" value="%(address)s" size=40></td></tr>
-<tr><td align=right><em>Alternate E-mail Addresses: </em></td>
-    <td><textarea name="alternate_addresses" rows=5 cols=40>%(alternate_addresses)s</textarea></td></tr>
-<tr><td align=right><em>Phone: </em></td>
-    <td><input name="phone" value="%(phone)s"></td></tr>
-<tr><td align=right>Preferred Login name: </td>
-    <td><input name="username" value="%(username)s"></td></tr>
-<tr><td align=right>Password: </td>
-    <td><input type="password" name="password" value="%(password)s"></td></tr>
-<tr><td align=right>Password Again: </td>
-    <td><input type="password" name="confirm" value="%(confirm)s"></td></tr>
-<tr><td></td>
-    <td><input type="submit" value="Register"></td></tr>
-</form>
-</table>
-''')%values)
-        self.pagefoot()
-
-    def login_action(self, message=None):
-        '''Attempt to log a user in and set the cookie
-
-        returns 0 if a page is generated as a result of this call, and
-        1 if not (ie. the login is successful
-        '''
-        if not self.form.has_key('__login_name'):
-            self.login(message=_('Username required'))
-            return 0
-        self.user = self.form['__login_name'].value
-        # re-open the database for real, using the user
-        self.opendb(self.user)
-        if self.form.has_key('__login_password'):
-            password = self.form['__login_password'].value
-        else:
-            password = ''
-        # make sure the user exists
-        try:
-            uid = self.db.user.lookup(self.user)
-        except KeyError:
-            name = self.user
-            self.make_user_anonymous()
-            action = self.form['__destination_url'].value
-            self.login(message=_('No such user "%(name)s"')%locals(),
-                action=action)
-            return 0
-
-        # and that the password is correct
-        pw = self.db.user.get(uid, 'password')
-        if password != pw:
-            self.make_user_anonymous()
-            action = self.form['__destination_url'].value
-            self.login(message=_('Incorrect password'), action=action)
-            return 0
-
-        self.set_cookie(self.user, password)
-        return 1
-
-    def newuser_action(self, message=None):
-        '''Attempt to create a new user based on the contents of the form
-        and then set the cookie.
-
-        return 1 on successful login
-        '''
-        # make sure we're allowed to register
-        userid = self.db.user.lookup(self.user)
-        if not self.db.security.hasPermission('Web Registration', userid):
-            raise Unauthorised, _("You do not have permission to access"\
-                        " %(action)s.")%{'action': 'registration'}
-
-        # re-open the database as "admin"
-        if self.user != 'admin':
-            self.opendb('admin')
-            
-        # create the new user
-        cl = self.db.user
-        try:
-            props = parsePropsFromForm(self.db, cl, self.form)
-            props['roles'] = self.instance.NEW_WEB_USER_ROLES
-            uid = cl.create(**props)
-            self.db.commit()
-        except ValueError, message:
-            action = self.form['__destination_url'].value
-            self.login(message, action=action)
-            return 0
-
-        # log the new user in
-        self.user = cl.get(uid, 'username')
-        # re-open the database for real, using the user
-        self.opendb(self.user)
-        password = cl.get(uid, 'password')
-        self.set_cookie(self.user, password)
-        return 1
-
-    def set_cookie(self, user, password):
-        # TODO generate a much, much stronger session key ;)
-        self.session = binascii.b2a_base64(repr(time.time())).strip()
-
-        # clean up the base64
-        if self.session[-1] == '=':
-            if self.session[-2] == '=':
-                self.session = self.session[:-2]
-            else:
-                self.session = self.session[:-1]
-
-        # insert the session in the sessiondb
-        self.db.sessions.set(self.session, user=user, last_use=time.time())
-
-        # and commit immediately
-        self.db.sessions.commit()
-
-        # expire us in a long, long time
-        expire = Cookie._getdate(86400*365)
-
-        # generate the cookie path - make sure it has a trailing '/'
-        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
-            ''))
-        self.header({'Set-Cookie': 'roundup_user=%s; expires=%s; Path=%s;'%(
-            self.session, expire, path)})
-
-    def make_user_anonymous(self):
-        ''' Make us anonymous
-
-            This method used to handle non-existence of the 'anonymous'
-            user, but that user is mandatory now.
-        '''
-        self.db.user.lookup('anonymous')
-        self.user = 'anonymous'
-
-    def logout(self, message=None):
-        ''' Make us really anonymous - nuke the cookie too
-        '''
-        self.make_user_anonymous()
-
-        # construct the logout cookie
-        now = Cookie._getdate()
-        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
-            ''))
-        self.header({'Set-Cookie':
-            'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now,
-            path)})
-        self.login()
-
-    def opendb(self, user):
-        ''' Open the database.
-        '''
-        # open the db if the user has changed
-        if not hasattr(self, 'db') or user != self.db.journaltag:
-            self.db = self.instance.open(user)
-
-    def main(self):
-        ''' Wrap the request and handle unauthorised requests
-        '''
-        self.desired_action = None
-        try:
-            self.main_action()
-        except Unauthorised, message:
-            self.unauthorised(message)
-
-    def main_action(self):
-        '''Wrap the database accesses so we can close the database cleanly
-        '''
-        # determine the uid to use
-        self.opendb('admin')
-
-        # make sure we have the session Class
-        sessions = self.db.sessions
-
-        # age sessions, remove when they haven't been used for a week
-        # TODO: this shouldn't be done every access
-        week = 60*60*24*7
-        now = time.time()
-        for sessid in sessions.list():
-            interval = now - sessions.get(sessid, 'last_use')
-            if interval > week:
-                sessions.destroy(sessid)
-
-        # look up the user session cookie
-        cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
-        user = 'anonymous'
-
-        if (cookie.has_key('roundup_user') and
-                cookie['roundup_user'].value != 'deleted'):
-
-            # get the session key from the cookie
-            self.session = cookie['roundup_user'].value
-            # get the user from the session
-            try:
-                # update the lifetime datestamp
-                sessions.set(self.session, last_use=time.time())
-                sessions.commit()
-                user = sessions.get(self.session, 'user')
-            except KeyError:
-                user = 'anonymous'
-
-        # sanity check on the user still being valid
-        try:
-            self.db.user.lookup(user)
-        except (KeyError, TypeError):
-            user = 'anonymous'
-
-        # make sure the anonymous user is valid if we're using it
-        if user == 'anonymous':
-            self.make_user_anonymous()
-        else:
-            self.user = user
-
-        # now figure which function to call
-        path = self.split_path
-        self.xtrapath = None
-
-        # default action to index if the path has no information in it
-        if not path or path[0] in ('', 'index'):
-            action = 'index'
-        else:
-            action = path[0]
-            if len(path) > 1:
-                self.xtrapath = path[1:]
-        self.desired_action = action
-
-        # everyone is allowed to try to log in
-        if action == 'login_action':
-            # try to login
-            if not self.login_action():
-                return
-            # figure the resulting page
-            action = self.form['__destination_url'].value
-
-        # allow anonymous people to register
-        elif action == 'newuser_action':
-            # try to add the user
-            if not self.newuser_action():
-                return
-            # figure the resulting page
-            action = self.form['__destination_url'].value
-
-        # ok, now we have figured out who the user is, make sure the user
-        # has permission to use this interface
-        userid = self.db.user.lookup(self.user)
-        if not self.db.security.hasPermission('Web Access', userid):
-            raise Unauthorised, \
-                _("You do not have permission to access this interface.")
-
-        # re-open the database for real, using the user
-        self.opendb(self.user)
-
-        # make sure we have a sane action
-        if not action:
-            action = 'index'
-
-        # just a regular action
-        try:
-            self.do_action(action)
-        except Unauthorised, message:
-            # if unauth is raised here, then a page header will have 
-            # been displayed
-            self.write('<p class="system-msg">%s</p>'%message)
-        else:
-            # commit all changes to the database
-            self.db.commit()
-
-    def do_action(self, action, dre=re.compile(r'([^\d]+)(\d+)'),
-            nre=re.compile(r'new(\w+)'), sre=re.compile(r'search(\w+)')):
-        '''Figure the user's action and do it.
-        '''
-        # here be the "normal" functionality
-        if action == 'index':
-            self.index()
-            return
-        if action == 'list_classes':
-            self.classes()
-            return
-        if action == 'classhelp':
-            self.classhelp()
-            return
-        if action == 'login':
-            self.login()
-            return
-        if action == 'logout':
-            self.logout()
-            return
-        if action == 'remove':
-            self.remove()
-            return
-
-        # see if we're to display an existing node
-        m = dre.match(action)
-        if m:
-            self.classname = m.group(1)
-            self.nodeid = m.group(2)
-            try:
-                cl = self.db.classes[self.classname]
-            except KeyError:
-                raise NotFound, self.classname
-            try:
-                cl.get(self.nodeid, 'id')
-            except IndexError:
-                raise NotFound, self.nodeid
-            try:
-                func = getattr(self, 'show%s'%self.classname)
-            except AttributeError:
-                raise NotFound, 'show%s'%self.classname
-            func()
-            return
-
-        # see if we're to put up the new node page
-        m = nre.match(action)
-        if m:
-            self.classname = m.group(1)
-            try:
-                func = getattr(self, 'new%s'%self.classname)
-            except AttributeError:
-                raise NotFound, 'new%s'%self.classname
-            func()
-            return
-
-        # see if we're to put up the new node page
-        m = sre.match(action)
-        if m:
-            self.classname = m.group(1)
-            try:
-                func = getattr(self, 'search%s'%self.classname)
-            except AttributeError:
-                raise NotFound
-            func()
-            return
-
-        # otherwise, display the named class
-        self.classname = action
-        try:
-            self.db.getclass(self.classname)
-        except KeyError:
-            raise NotFound, self.classname
-        self.list()
-
-    def remove(self,  dre=re.compile(r'([^\d]+)(\d+)')):
-        target = self.index_arg(':target')[0]
-        m = dre.match(target)
-        if m:
-            classname = m.group(1)
-            nodeid = m.group(2)
-            cl = self.db.getclass(classname)
-            cl.retire(nodeid)
-            # now take care of the reference
-            parentref =  self.index_arg(':multilink')[0]
-            parent, prop = parentref.split(':')
-            m = dre.match(parent)
-            if m:
-                self.classname = m.group(1)
-                self.nodeid = m.group(2)
-                cl = self.db.getclass(self.classname)
-                value = cl.get(self.nodeid, prop)
-                value.remove(nodeid)
-                cl.set(self.nodeid, **{prop:value})
-                func = getattr(self, 'show%s'%self.classname)
-                return func()
-            else:
-                raise NotFound, parent
-        else:
-            raise NotFound, target
-
-class ExtendedClient(Client): 
-    '''Includes pages and page heading information that relate to the
-       extended schema.
-    ''' 
-    showsupport = Client.shownode
-    showtimelog = Client.shownode
-    newsupport = Client.newnode
-    newtimelog = Client.newnode
-    searchsupport = Client.searchnode
-
-    default_index_sort = ['-activity']
-    default_index_group = ['priority']
-    default_index_filter = ['status']
-    default_index_columns = ['activity','status','title','assignedto']
-    default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
-    default_pagesize = '50'
-
-def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
-    '''Pull properties for the given class out of the form.
-    '''
-    props = {}
-    keys = form.keys()
-    for key in keys:
-        if not cl.properties.has_key(key):
-            continue
-        proptype = cl.properties[key]
-        if isinstance(proptype, hyperdb.String):
-            value = form[key].value.strip()
-        elif isinstance(proptype, hyperdb.Password):
-            value = password.Password(form[key].value.strip())
-        elif isinstance(proptype, hyperdb.Date):
-            value = form[key].value.strip()
-            if value:
-                value = date.Date(form[key].value.strip())
-            else:
-                value = None
-        elif isinstance(proptype, hyperdb.Interval):
-            value = form[key].value.strip()
-            if value:
-                value = date.Interval(form[key].value.strip())
-            else:
-                value = None
-        elif isinstance(proptype, hyperdb.Link):
-            value = form[key].value.strip()
-            # see if it's the "no selection" choice
-            if value == '-1':
-                value = None
-            else:
-                # handle key values
-                link = cl.properties[key].classname
-                if not num_re.match(value):
-                    try:
-                        value = db.classes[link].lookup(value)
-                    except KeyError:
-                        raise ValueError, _('property "%(propname)s": '
-                            '%(value)s not a %(classname)s')%{'propname':key, 
-                            'value': value, 'classname': link}
-        elif isinstance(proptype, hyperdb.Multilink):
-            value = form[key]
-            if hasattr(value, 'value'):
-                # Quite likely to be a FormItem instance
-                value = value.value
-            if not isinstance(value, type([])):
-                value = [i.strip() for i in value.split(',')]
-            else:
-                value = [i.strip() for i in value]
-            link = cl.properties[key].classname
-            l = []
-            for entry in map(str, value):
-                if entry == '': continue
-                if not num_re.match(entry):
-                    try:
-                        entry = db.classes[link].lookup(entry)
-                    except KeyError:
-                        raise ValueError, _('property "%(propname)s": '
-                            '"%(value)s" not an entry of %(classname)s')%{
-                            'propname':key, 'value': entry, 'classname': link}
-                l.append(entry)
-            l.sort()
-            value = l
-        elif isinstance(proptype, hyperdb.Boolean):
-            value = form[key].value.strip()
-            props[key] = value = value.lower() in ('yes', 'true', 'on', '1')
-        elif isinstance(proptype, hyperdb.Number):
-            value = form[key].value.strip()
-            props[key] = value = int(value)
-
-        # get the old value
-        if nodeid:
-            try:
-                existing = cl.get(nodeid, key)
-            except KeyError:
-                # this might be a new property for which there is no existing
-                # value
-                if not cl.properties.has_key(key): raise
-
-            # if changed, set it
-            if value != existing:
-                props[key] = value
-        else:
-            props[key] = value
-    return props
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.161  2002/08/19 00:21:10  richard
-# removed debug prints
-#
-# Revision 1.160  2002/08/19 00:20:34  richard
-# grant web access to admin ;)
-#
-# Revision 1.159  2002/08/16 04:29:41  richard
-# bugfix
-#
-# Revision 1.158  2002/08/15 00:40:10  richard
-# cleanup
-#
-# Revision 1.157  2002/08/13 20:16:09  gmcm
-# Use a real parser for templates.
-# Rewrite htmltemplate to use the parser (hack, hack).
-# Move the "do_XXX" methods to template_funcs.py.
-# Redo the funcion tests (but not Template tests - they're hopeless).
-# Simplified query form in cgi_client.
-# Ability to delete msgs, files, queries.
-# Ability to edit the metadata on files.
-#
-# Revision 1.156  2002/08/01 15:06:06  gmcm
-# Use same regex to split search terms as used to index text.
-# Fix to back_metakit for not changing journaltag on reopen.
-# Fix htmltemplate's do_link so [No <whatever>] strings are href'd.
-# Fix bogus "nosy edited ok" msg - the **d syntax does NOT share d between caller and callee.
-#
-# Revision 1.155  2002/08/01 00:56:22  richard
-# Added the web access and email access permissions, so people can restrict
-# access to users who register through the email interface (for example).
-# Also added "security" command to the roundup-admin interface to display the
-# Role/Permission config for an instance.
-#
-# Revision 1.154  2002/07/31 23:57:36  richard
-#  . web forms may now unset Link values (like assignedto)
-#
-# Revision 1.153  2002/07/31 22:40:50  gmcm
-# Fixes to the search form and saving queries.
-# Fixes to  sorting in back_metakit.py.
-#
-# Revision 1.152  2002/07/31 22:04:14  richard
-# cleanup
-#
-# Revision 1.151  2002/07/30 21:37:43  richard
-# oops, thanks Duncan Booth for spotting this one
-#
-# Revision 1.150  2002/07/30 20:43:18  gmcm
-# Oops, fix the permission check!
-#
-# Revision 1.149  2002/07/30 20:04:38  gmcm
-# Adapt metakit backend to new security scheme.
-# Put some more permission checks in cgi_client.
-#
-# Revision 1.148  2002/07/30 16:09:11  gmcm
-# Simple optimization.
-#
-# Revision 1.147  2002/07/30 08:22:38  richard
-# Session storage in the hyperdb was horribly, horribly inefficient. We use
-# a simple anydbm wrapper now - which could be overridden by the metakit
-# backend or RDB backend if necessary.
-# Much, much better.
-#
-# Revision 1.146  2002/07/30 05:27:30  richard
-# nicer error messages, and a bugfix
-#
-# Revision 1.145  2002/07/26 08:26:59  richard
-# Very close now. The cgi and mailgw now use the new security API. The two
-# templates have been migrated to that setup. Lots of unit tests. Still some
-# issue in the web form for editing Roles assigned to users.
-#
-# Revision 1.144  2002/07/25 07:14:05  richard
-# Bugger it. Here's the current shape of the new security implementation.
-# Still to do:
-#  . call the security funcs from cgi and mailgw
-#  . change shipped templates to include correct initialisation and remove
-#    the old config vars
-# ... that seems like a lot. The bulk of the work has been done though. Honest :)
-#
-# Revision 1.143  2002/07/20 19:29:10  gmcm
-# Fixes/improvements to the search form & saved queries.
-#
-# Revision 1.142  2002/07/18 11:17:30  gmcm
-# Add Number and Boolean types to hyperdb.
-# Add conversion cases to web, mail & admin interfaces.
-# Add storage/serialization cases to back_anydbm & back_metakit.
-#
-# Revision 1.141  2002/07/17 12:39:10  gmcm
-# Saving, running & editing queries.
-#
-# Revision 1.140  2002/07/14 23:17:15  richard
-# cleaned up structure
-#
-# Revision 1.139  2002/07/14 06:14:40  richard
-# Some more TODOs
-#
-# Revision 1.138  2002/07/14 04:03:13  richard
-# Implemented a switch to disable journalling for a Class. CGI session
-# database now uses it.
-#
-# Revision 1.137  2002/07/10 07:00:30  richard
-# removed debugging
-#
-# Revision 1.136  2002/07/10 06:51:08  richard
-# . #576241 ] MultiLink problems in parsePropsFromForm
-#
-# Revision 1.135  2002/07/10 00:22:34  richard
-#  . switched to using a session-based web login
-#
-# Revision 1.134  2002/07/09 04:19:09  richard
-# Added reindex command to roundup-admin.
-# Fixed reindex on first access.
-# Also fixed reindexing of entries that change.
-#
-# Revision 1.133  2002/07/08 15:32:05  gmcm
-# Pagination of index pages.
-# New search form.
-#
-# Revision 1.132  2002/07/08 07:26:14  richard
-# ehem
-#
-# Revision 1.131  2002/07/08 06:53:57  richard
-# Not sure why the cgi_client had an indexer argument.
-#
-# Revision 1.130  2002/06/27 12:01:53  gmcm
-# If the form has a :multilink, put a back href in the pageheader (back to the linked-to node).
-# Some minor optimizations (only compile regexes once).
-#
-# Revision 1.129  2002/06/20 23:52:11  richard
-# Better handling of unauth attempt to edit stuff
-#
-# Revision 1.128  2002/06/12 21:28:25  gmcm
-# Allow form to set user-properties on a Fileclass.
-# Don't assume that a Fileclass is named "files".
-#
-# Revision 1.127  2002/06/11 06:38:24  richard
-#  . #565996 ] The "Attach a File to this Issue" fails
-#
-# Revision 1.126  2002/05/29 01:16:17  richard
-# Sorry about this huge checkin! It's fixing a lot of related stuff in one go
-# though.
-#
-# . #541941 ] changing multilink properties by mail
-# . #526730 ] search for messages capability
-# . #505180 ] split MailGW.handle_Message
-#   - also changed cgi client since it was duplicating the functionality
-# . build htmlbase if tests are run using CVS checkout (removed note from
-#   installation.txt)
-# . don't create an empty message on email issue creation if the email is empty
-#
-# Revision 1.125  2002/05/25 07:16:24  rochecompaan
-# Merged search_indexing-branch with HEAD
-#
-# Revision 1.124  2002/05/24 02:09:24  richard
-# Nothing like a live demo to show up the bugs ;)
-#
-# Revision 1.123  2002/05/22 05:04:13  richard
-# Oops
-#
-# Revision 1.122  2002/05/22 04:12:05  richard
-#  . applied patch #558876 ] cgi client customization
-#    ... with significant additions and modifications ;)
-#    - extended handling of ML assignedto to all places it's handled
-#    - added more NotFound info
-#
-# Revision 1.121  2002/05/21 06:08:10  richard
-# Handle migration
-#
-# Revision 1.120  2002/05/21 06:05:53  richard
-#  . #551483 ] assignedto in Client.make_index_link
-#
-# Revision 1.119  2002/05/15 06:21:21  richard
-#  . node caching now works, and gives a small boost in performance
-#
-# As a part of this, I cleaned up the DEBUG output and implemented TRACE
-# output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
-# CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
-# (using if __debug__ which is compiled out with -O)
-#
-# Revision 1.118  2002/05/12 23:46:33  richard
-# ehem, part 2
-#
-# Revision 1.117  2002/05/12 23:42:29  richard
-# ehem
-#
-# Revision 1.116  2002/05/02 08:07:49  richard
-# Added the ADD_AUTHOR_TO_NOSY handling to the CGI interface.
-#
-# Revision 1.115  2002/04/02 01:56:10  richard
-#  . stop sending blank (whitespace-only) notes
-#
-# Revision 1.114.2.4  2002/05/02 11:49:18  rochecompaan
-# Allow customization of the search filters that should be displayed
-# on the search page.
-#
-# Revision 1.114.2.3  2002/04/20 13:23:31  rochecompaan
-# We now have a separate search page for nodes.  Search links for
-# different classes can be customized in instance_config similar to
-# index links.
-#
-# Revision 1.114.2.2  2002/04/19 19:54:42  rochecompaan
-# cgi_client.py
-#     removed search link for the time being
-#     moved rendering of matches to htmltemplate
-# hyperdb.py
-#     filtering of nodes on full text search incorporated in filter method
-# roundupdb.py
-#     added paramater to call of filter method
-# roundup_indexer.py
-#     added search method to RoundupIndexer class
-#
-# Revision 1.114.2.1  2002/04/03 11:55:57  rochecompaan
-#  . Added feature #526730 - search for messages capability
-#
-# Revision 1.114  2002/03/17 23:06:05  richard
-# oops
-#
-# Revision 1.113  2002/03/14 23:59:24  richard
-#  . #517734 ] web header customisation is obscure
-#
-# Revision 1.112  2002/03/12 22:52:26  richard
-# more pychecker warnings removed
-#
-# Revision 1.111  2002/02/25 04:32:21  richard
-# ahem
-#
-# Revision 1.110  2002/02/21 07:19:08  richard
-# ... and label, width and height control for extra flavour!
-#
-# Revision 1.109  2002/02/21 07:08:19  richard
-# oops
-#
-# Revision 1.108  2002/02/21 07:02:54  richard
-# The correct var is "HTTP_HOST"
-#
-# Revision 1.107  2002/02/21 06:57:38  richard
-#  . Added popup help for classes using the classhelp html template function.
-#    - add <display call="classhelp('priority', 'id,name,description')">
-#      to an item page, and it generates a link to a popup window which displays
-#      the id, name and description for the priority class. The description
-#      field won't exist in most installations, but it will be added to the
-#      default templates.
-#
-# Revision 1.106  2002/02/21 06:23:00  richard
-# *** empty log message ***
-#
-# Revision 1.105  2002/02/20 05:52:10  richard
-# better error handling
-#
-# Revision 1.104  2002/02/20 05:45:17  richard
-# Use the csv module for generating the form entry so it's correct.
-# [also noted the sf.net feature request id in the change log]
-#
-# Revision 1.103  2002/02/20 05:05:28  richard
-#  . Added simple editing for classes that don't define a templated interface.
-#    - access using the admin "class list" interface
-#    - limited to admin-only
-#    - requires the csv module from object-craft (url given if it's missing)
-#
-# Revision 1.102  2002/02/15 07:08:44  richard
-#  . Alternate email addresses are now available for users. See the MIGRATION
-#    file for info on how to activate the feature.
-#
-# Revision 1.101  2002/02/14 23:39:18  richard
-# . All forms now have "double-submit" protection when Javascript is enabled
-#   on the client-side.
-#
-# Revision 1.100  2002/01/16 07:02:57  richard
-#  . lots of date/interval related changes:
-#    - more relaxed date format for input
-#
-# Revision 1.99  2002/01/16 03:02:42  richard
-# #503793 ] changing assignedto resets nosy list
-#
-# Revision 1.98  2002/01/14 02:20:14  richard
-#  . changed all config accesses so they access either the instance or the
-#    config attriubute on the db. This means that all config is obtained from
-#    instance_config instead of the mish-mash of classes. This will make
-#    switching to a ConfigParser setup easier too, I hope.
-#
-# At a minimum, this makes migration a _little_ easier (a lot easier in the
-# 0.5.0 switch, I hope!)
-#
-# Revision 1.97  2002/01/11 23:22:29  richard
-#  . #502437 ] rogue reactor and unittest
-#    in short, the nosy reactor was modifying the nosy list. That code had
-#    been there for a long time, and I suspsect it was there because we
-#    weren't generating the nosy list correctly in other places of the code.
-#    We're now doing that, so the nosy-modifying code can go away from the
-#    nosy reactor.
-#
-# Revision 1.96  2002/01/10 05:26:10  richard
-# missed a parsePropsFromForm in last update
-#
-# Revision 1.95  2002/01/10 03:39:45  richard
-#  . fixed some problems with web editing and change detection
-#
-# Revision 1.94  2002/01/09 13:54:21  grubert
-# _add_assignedto_to_nosy did set nosy to assignedto only, no adding.
-#
-# Revision 1.93  2002/01/08 11:57:12  richard
-# crying out for real configuration handling... :(
-#
-# Revision 1.92  2002/01/08 04:12:05  richard
-# Changed message-id format to "<%s.%s.%s%s@%s>" so it complies with RFC822
-#
-# Revision 1.91  2002/01/08 04:03:47  richard
-# I mucked the intent of the code up.
-#
-# Revision 1.90  2002/01/08 03:56:55  richard
-# Oops, missed this before the beta:
-#  . #495392 ] empty nosy -patch
-#
-# Revision 1.89  2002/01/07 20:24:45  richard
-# *mutter* stupid cutnpaste
-#
-# Revision 1.88  2002/01/02 02:31:38  richard
-# Sorry for the huge checkin message - I was only intending to implement #496356
-# but I found a number of places where things had been broken by transactions:
-#  . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
-#    for _all_ roundup-generated smtp messages to be sent to.
-#  . the transaction cache had broken the roundupdb.Class set() reactors
-#  . newly-created author users in the mailgw weren't being committed to the db
-#
-# Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
-# on when I found that stuff :):
-#  . #496356 ] Use threading in messages
-#  . detectors were being registered multiple times
-#  . added tests for mailgw
-#  . much better attaching of erroneous messages in the mail gateway
-#
-# Revision 1.87  2001/12/23 23:18:49  richard
-# We already had an admin-specific section of the web heading, no need to add
-# another one :)
-#
-# Revision 1.86  2001/12/20 15:43:01  rochecompaan
-# Features added:
-#  .  Multilink properties are now displayed as comma separated values in
-#     a textbox
-#  .  The add user link is now only visible to the admin user
-#  .  Modified the mail gateway to reject submissions from unknown
-#     addresses if ANONYMOUS_ACCESS is denied
-#
-# Revision 1.85  2001/12/20 06:13:24  rochecompaan
-# Bugs fixed:
-#   . Exception handling in hyperdb for strings-that-look-like numbers got
-#     lost somewhere
-#   . Internet Explorer submits full path for filename - we now strip away
-#     the path
-# Features added:
-#   . Link and multilink properties are now displayed sorted in the cgi
-#     interface
-#
-# Revision 1.84  2001/12/18 15:30:30  rochecompaan
-# Fixed bugs:
-#  .  Fixed file creation and retrieval in same transaction in anydbm
-#     backend
-#  .  Cgi interface now renders new issue after issue creation
-#  .  Could not set issue status to resolved through cgi interface
-#  .  Mail gateway was changing status back to 'chatting' if status was
-#     omitted as an argument
-#
-# Revision 1.83  2001/12/15 23:51:01  richard
-# Tested the changes and fixed a few problems:
-#  . files are now attached to the issue as well as the message
-#  . newuser is a real method now since we don't want to do the message/file
-#    stuff for it
-#  . added some documentation
-# The really big changes in the diff are a result of me moving some code
-# around to keep like methods together a bit better.
-#
-# Revision 1.82  2001/12/15 19:24:39  rochecompaan
-#  . Modified cgi interface to change properties only once all changes are
-#    collected, files created and messages generated.
-#  . Moved generation of change note to nosyreactors.
-#  . We now check for changes to "assignedto" to ensure it's added to the
-#    nosy list.
-#
-# Revision 1.81  2001/12/12 23:55:00  richard
-# Fixed some problems with user editing
-#
-# Revision 1.80  2001/12/12 23:27:14  richard
-# Added a Zope frontend for roundup.
-#
-# Revision 1.79  2001/12/10 22:20:01  richard
-# Enabled transaction support in the bsddb backend. It uses the anydbm code
-# where possible, only replacing methods where the db is opened (it uses the
-# btree opener specifically.)
-# Also cleaned up some change note generation.
-# Made the backends package work with pydoc too.
-#
-# Revision 1.78  2001/12/07 05:59:27  rochecompaan
-# Fixed small bug that prevented adding issues through the web.
-#
-# Revision 1.77  2001/12/06 22:48:29  richard
-# files multilink was being nuked in post_edit_node
-#
-# Revision 1.76  2001/12/05 14:26:44  rochecompaan
-# Removed generation of change note from "sendmessage" in roundupdb.py.
-# The change note is now generated when the message is created.
-#
-# Revision 1.75  2001/12/04 01:25:08  richard
-# Added some rollbacks where we were catching exceptions that would otherwise
-# have stopped committing.
-#
-# Revision 1.74  2001/12/02 05:06:16  richard
-# . We now use weakrefs in the Classes to keep the database reference, so
-#   the close() method on the database is no longer needed.
-#   I bumped the minimum python requirement up to 2.1 accordingly.
-# . #487480 ] roundup-server
-# . #487476 ] INSTALL.txt
-#
-# I also cleaned up the change message / post-edit stuff in the cgi client.
-# There's now a clearly marked "TODO: append the change note" where I believe
-# the change note should be added there. The "changes" list will obviously
-# have to be modified to be a dict of the changes, or somesuch.
-#
-# More testing needed.
-#
-# Revision 1.73  2001/12/01 07:17:50  richard
-# . We now have basic transaction support! Information is only written to
-#   the database when the commit() method is called. Only the anydbm
-#   backend is modified in this way - neither of the bsddb backends have been.
-#   The mail, admin and cgi interfaces all use commit (except the admin tool
-#   doesn't have a commit command, so interactive users can't commit...)
-# . Fixed login/registration forwarding the user to the right page (or not,
-#   on a failure)
-#
-# Revision 1.72  2001/11/30 20:47:58  rochecompaan
-# Links in page header are now consistent with default sort order.
-#
-# Fixed bugs:
-#     - When login failed the list of issues were still rendered.
-#     - User was redirected to index page and not to his destination url
-#       if his first login attempt failed.
-#
-# Revision 1.71  2001/11/30 20:28:10  rochecompaan
-# Property changes are now completely traceable, whether changes are
-# made through the web or by email
-#
-# Revision 1.70  2001/11/30 00:06:29  richard
-# Converted roundup/cgi_client.py to use _()
-# Added the status file, I18N_PROGRESS.txt
-#
-# Revision 1.69  2001/11/29 23:19:51  richard
-# Removed the "This issue has been edited through the web" when a valid
-# change note is supplied.
-#
-# Revision 1.68  2001/11/29 04:57:23  richard
-# a little comment
-#
-# Revision 1.67  2001/11/28 21:55:35  richard
-#  . login_action and newuser_action return values were being ignored
-#  . Woohoo! Found that bloody re-login bug that was killing the mail
-#    gateway.
-#  (also a minor cleanup in hyperdb)
-#
-# Revision 1.66  2001/11/27 03:00:50  richard
-# couple of bugfixes from latest patch integration
-#
-# Revision 1.65  2001/11/26 23:00:53  richard
-# This config stuff is getting to be a real mess...
-#
-# Revision 1.64  2001/11/26 22:56:35  richard
-# typo
-#
-# Revision 1.63  2001/11/26 22:55:56  richard
-# Feature:
-#  . Added INSTANCE_NAME to configuration - used in web and email to identify
-#    the instance.
-#  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
-#    signature info in e-mails.
-#  . Some more flexibility in the mail gateway and more error handling.
-#  . Login now takes you to the page you back to the were denied access to.
-#
-# Fixed:
-#  . Lots of bugs, thanks Roché and others on the devel mailing list!
-#
-# Revision 1.62  2001/11/24 00:45:42  jhermann
-# typeof() instead of type(): avoid clash with database field(?) "type"
-#
-# Fixes this traceback:
-#
-# Traceback (most recent call last):
-#   File "roundup\cgi_client.py", line 535, in newnode
-#     self._post_editnode(nid)
-#   File "roundup\cgi_client.py", line 415, in _post_editnode
-#     if type(value) != type([]): value = [value]
-# UnboundLocalError: local variable 'type' referenced before assignment
-#
-# Revision 1.61  2001/11/22 15:46:42  jhermann
-# Added module docstrings to all modules.
-#
-# Revision 1.60  2001/11/21 22:57:28  jhermann
-# Added dummy hooks for I18N and some preliminary (test) markup of
-# translatable messages
-#
-# Revision 1.59  2001/11/21 03:21:13  richard
-# oops
-#
-# Revision 1.58  2001/11/21 03:11:28  richard
-# Better handling of new properties.
-#
-# Revision 1.57  2001/11/15 10:24:27  richard
-# handle the case where there is no file attached
-#
-# Revision 1.56  2001/11/14 21:35:21  richard
-#  . users may attach files to issues (and support in ext) through the web now
-#
-# Revision 1.55  2001/11/07 02:34:06  jhermann
-# Handling of damaged login cookies
-#
-# Revision 1.54  2001/11/07 01:16:12  richard
-# Remove the '=' padding from cookie value so quoting isn't an issue.
-#
-# Revision 1.53  2001/11/06 23:22:05  jhermann
-# More IE fixes: it does not like quotes around cookie values; in the
-# hope this does not break anything for other browser; if it does, we
-# need to check HTTP_USER_AGENT
-#
-# Revision 1.52  2001/11/06 23:11:22  jhermann
-# Fixed debug output in page footer; added expiry date to the login cookie
-# (expires 1 year in the future) to prevent probs with certain versions
-# of IE
-#
-# Revision 1.51  2001/11/06 22:00:34  jhermann
-# Get debug level from ROUNDUP_DEBUG env var
-#
-# Revision 1.50  2001/11/05 23:45:40  richard
-# Fixed newuser_action so it sets the cookie with the unencrypted password.
-# Also made it present nicer error messages (not tracebacks).
-#
-# Revision 1.49  2001/11/04 03:07:12  richard
-# Fixed various cookie-related bugs:
-#  . bug #477685 ] base64.decodestring breaks
-#  . bug #477837 ] lynx does not like the cookie
-#  . bug #477892 ] Password edit doesn't fix login cookie
-# Also closed a security hole - a logged-in user could edit another user's
-# details.
-#
-# Revision 1.48  2001/11/03 01:30:18  richard
-# Oops. uses pagefoot now.
-#
-# Revision 1.47  2001/11/03 01:29:28  richard
-# Login page didn't have all close tags.
-#
-# Revision 1.46  2001/11/03 01:26:55  richard
-# possibly fix truncated base64'ed user:pass
-#
-# Revision 1.45  2001/11/01 22:04:37  richard
-# Started work on supporting a pop3-fetching server
-# Fixed bugs:
-#  . bug #477104 ] HTML tag error in roundup-server
-#  . bug #477107 ] HTTP header problem
-#
-# Revision 1.44  2001/10/28 23:03:08  richard
-# Added more useful header to the classic schema.
-#
-# Revision 1.43  2001/10/24 00:01:42  richard
-# More fixes to lockout logic.
-#
-# Revision 1.42  2001/10/23 23:56:03  richard
-# HTML typo
-#
-# Revision 1.41  2001/10/23 23:52:35  richard
-# Fixed lock-out logic, thanks Roch'e for pointing out the problems.
-#
-# Revision 1.40  2001/10/23 23:06:39  richard
-# Some cleanup.
-#
-# Revision 1.39  2001/10/23 01:00:18  richard
-# Re-enabled login and registration access after lopping them off via
-# disabling access for anonymous users.
-# Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
-# a couple of bugs while I was there. Probably introduced a couple, but
-# things seem to work OK at the moment.
-#
-# Revision 1.38  2001/10/22 03:25:01  richard
-# Added configuration for:
-#  . anonymous user access and registration (deny/allow)
-#  . filter "widget" location on index page (top, bottom, both)
-# Updated some documentation.
-#
-# Revision 1.37  2001/10/21 07:26:35  richard
-# feature #473127: Filenames. I modified the file.index and htmltemplate
-#  source so that the filename is used in the link and the creation
-#  information is displayed.
-#
-# Revision 1.36  2001/10/21 04:44:50  richard
-# bug #473124: UI inconsistency with Link fields.
-#    This also prompted me to fix a fairly long-standing usability issue -
-#    that of being able to turn off certain filters.
-#
-# Revision 1.35  2001/10/21 00:17:54  richard
-# CGI interface view customisation section may now be hidden (patch from
-#  Roch'e Compaan.)
-#
-# Revision 1.34  2001/10/20 11:58:48  richard
-# Catch errors in login - no username or password supplied.
-# Fixed editing of password (Password property type) thanks Roch'e Compaan.
-#
-# Revision 1.33  2001/10/17 00:18:41  richard
-# Manually constructing cookie headers now.
-#
-# Revision 1.32  2001/10/16 03:36:21  richard
-# CGI interface wasn't handling checkboxes at all.
-#
-# Revision 1.31  2001/10/14 10:55:00  richard
-# Handle empty strings in HTML template Link function
-#
-# Revision 1.30  2001/10/09 07:38:58  richard
-# Pushed the base code for the extended schema CGI interface back into the
-# code cgi_client module so that future updates will be less painful.
-# Also removed a debugging print statement from cgi_client.
-#
-# Revision 1.29  2001/10/09 07:25:59  richard
-# Added the Password property type. See "pydoc roundup.password" for
-# implementation details. Have updated some of the documentation too.
-#
-# Revision 1.28  2001/10/08 00:34:31  richard
-# Change message was stuffing up for multilinks with no key property.
-#
-# Revision 1.27  2001/10/05 02:23:24  richard
-#  . roundup-admin create now prompts for property info if none is supplied
-#    on the command-line.
-#  . hyperdb Class getprops() method may now return only the mutable
-#    properties.
-#  . Login now uses cookies, which makes it a whole lot more flexible. We can
-#    now support anonymous user access (read-only, unless there's an
-#    "anonymous" user, in which case write access is permitted). Login
-#    handling has been moved into cgi_client.Client.main()
-#  . The "extended" schema is now the default in roundup init.
-#  . The schemas have had their page headings modified to cope with the new
-#    login handling. Existing installations should copy the interfaces.py
-#    file from the roundup lib directory to their instance home.
-#  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
-#    Ping - has been removed.
-#  . Fixed a whole bunch of places in the CGI interface where we should have
-#    been returning Not Found instead of throwing an exception.
-#  . Fixed a deviation from the spec: trying to modify the 'id' property of
-#    an item now throws an exception.
-#
-# Revision 1.26  2001/09/12 08:31:42  richard
-# handle cases where mime type is not guessable
-#
-# Revision 1.25  2001/08/29 05:30:49  richard
-# change messages weren't being saved when there was no-one on the nosy list.
-#
-# Revision 1.24  2001/08/29 04:49:39  richard
-# didn't clean up fully after debugging :(
-#
-# Revision 1.23  2001/08/29 04:47:18  richard
-# Fixed CGI client change messages so they actually include the properties
-# changed (again).
-#
-# Revision 1.22  2001/08/17 00:08:10  richard
-# reverted back to sending messages always regardless of who is doing the web
-# edit. change notes weren't being saved. bleah. hackish.
-#
-# Revision 1.21  2001/08/15 23:43:18  richard
-# Fixed some isFooTypes that I missed.
-# Refactored some code in the CGI code.
-#
-# Revision 1.20  2001/08/12 06:32:36  richard
-# using isinstance(blah, Foo) now instead of isFooType
-#
-# Revision 1.19  2001/08/07 00:24:42  richard
-# stupid typo
-#
-# Revision 1.18  2001/08/07 00:15:51  richard
-# Added the copyright/license notice to (nearly) all files at request of
-# Bizar Software.
-#
-# Revision 1.17  2001/08/02 06:38:17  richard
-# Roundupdb now appends "mailing list" information to its messages which
-# include the e-mail address and web interface address. Templates may
-# override this in their db classes to include specific information (support
-# instructions, etc).
-#
-# Revision 1.16  2001/08/02 05:55:25  richard
-# Web edit messages aren't sent to the person who did the edit any more. No
-# message is generated if they are the only person on the nosy list.
-#
-# Revision 1.15  2001/08/02 00:34:10  richard
-# bleah syntax error
-#
-# Revision 1.14  2001/08/02 00:26:16  richard
-# Changed the order of the information in the message generated by web edits.
-#
-# Revision 1.13  2001/07/30 08:12:17  richard
-# Added time logging and file uploading to the templates.
-#
-# Revision 1.12  2001/07/30 06:26:31  richard
-# Added some documentation on how the newblah works.
-#
-# Revision 1.11  2001/07/30 06:17:45  richard
-# Features:
-#  . Added ability for cgi newblah forms to indicate that the new node
-#    should be linked somewhere.
-# Fixed:
-#  . Fixed the agument handling for the roundup-admin find command.
-#  . Fixed handling of summary when no note supplied for newblah. Again.
-#  . Fixed detection of no form in htmltemplate Field display.
-#
-# Revision 1.10  2001/07/30 02:37:34  richard
-# Temporary measure until we have decent schema migration...
-#
-# Revision 1.9  2001/07/30 01:25:07  richard
-# Default implementation is now "classic" rather than "extended" as one would
-# expect.
-#
-# Revision 1.8  2001/07/29 08:27:40  richard
-# Fixed handling of passed-in values in form elements (ie. during a
-# drill-down)
-#
-# Revision 1.7  2001/07/29 07:01:39  richard
-# Added vim command to all source so that we don't get no steenkin' tabs :)
-#
-# Revision 1.6  2001/07/29 04:04:00  richard
-# Moved some code around allowing for subclassing to change behaviour.
-#
-# Revision 1.5  2001/07/28 08:16:52  richard
-# New issue form handles lack of note better now.
-#
-# Revision 1.4  2001/07/28 00:34:34  richard
-# Fixed some non-string node ids.
-#
-# Revision 1.3  2001/07/23 03:56:30  richard
-# oops, missed a config removal
-#
-# Revision 1.2  2001/07/22 12:09:32  richard
-# Final commit of Grande Splite
-#
-# Revision 1.1  2001/07/22 11:58:35  richard
-# More Grande Splite
-#
-#
-# vim: set filetype=python ts=4 sw=4 et si
diff --git a/roundup/cgitb.py b/roundup/cgitb.py
deleted file mode 100644 (file)
index d7837ca..0000000
+++ /dev/null
@@ -1,157 +0,0 @@
-#
-# This module was written by Ka-Ping Yee, <ping@lfw.org>.
-# 
-# $Id: cgitb.py,v 1.10 2002-01-16 04:49:45 richard Exp $
-
-__doc__ = """
-Extended CGI traceback handler by Ka-Ping Yee, <ping@lfw.org>.
-"""
-
-import sys, os, types, string, keyword, linecache, tokenize, inspect, pydoc
-
-from i18n import _
-
-def breaker():
-    return ('<body bgcolor="#f0f0ff">' +
-            '<font color="#f0f0ff" size="-5"> > </font> ' +
-            '</table>' * 5)
-
-def html(context=5):
-    etype, evalue = sys.exc_type, sys.exc_value
-    if type(etype) is types.ClassType:
-        etype = etype.__name__
-    pyver = 'Python ' + string.split(sys.version)[0] + '<br>' + sys.executable
-    head = pydoc.html.heading(
-        '<font size=+1><strong>%s</strong>: %s</font>'%(str(etype), str(evalue)),
-        '#ffffff', '#aa55cc', pyver)
-
-    head = head + (_('<p>A problem occurred while running a Python script. '
-                   'Here is the sequence of function calls leading up to '
-                   'the error, with the most recent (innermost) call first. '
-                   'The exception attributes are:'))
-
-    indent = '<tt><small>%s</small>&nbsp;</tt>' % ('&nbsp;' * 5)
-    traceback = []
-    for frame, file, lnum, func, lines, index in inspect.trace(context):
-        if file is None:
-            link = '&lt;file is None - probably inside <tt>eval</tt> or <tt>exec</tt>&gt;'
-        else:
-            file = os.path.abspath(file)
-            link = '<a href="file:%s">%s</a>' % (file, pydoc.html.escape(file))
-        args, varargs, varkw, locals = inspect.getargvalues(frame)
-        if func == '?':
-            call = ''
-        else:
-            call = 'in <strong>%s</strong>' % func + inspect.formatargvalues(
-                    args, varargs, varkw, locals,
-                    formatvalue=lambda value: '=' + pydoc.html.repr(value))
-
-        level = '''
-<table width="100%%" bgcolor="#d8bbff" cellspacing=0 cellpadding=2 border=0>
-<tr><td>%s %s</td></tr></table>''' % (link, call)
-
-        if index is None or file is None:
-            traceback.append('<p>' + level)
-            continue
-
-        # do a fil inspection
-        names = []
-        def tokeneater(type, token, start, end, line, names=names):
-            if type == tokenize.NAME and token not in keyword.kwlist:
-                if token not in names:
-                    names.append(token)
-            if type == tokenize.NEWLINE: raise IndexError
-        def linereader(file=file, lnum=[lnum]):
-            line = linecache.getline(file, lnum[0])
-            lnum[0] = lnum[0] + 1
-            return line
-
-        try:
-            tokenize.tokenize(linereader, tokeneater)
-        except IndexError: pass
-        lvals = []
-        for name in names:
-            if name in frame.f_code.co_varnames:
-                if locals.has_key(name):
-                    value = pydoc.html.repr(locals[name])
-                else:
-                    value = _('<em>undefined</em>')
-                name = '<strong>%s</strong>' % name
-            else:
-                if frame.f_globals.has_key(name):
-                    value = pydoc.html.repr(frame.f_globals[name])
-                else:
-                    value = _('<em>undefined</em>')
-                name = '<em>global</em> <strong>%s</strong>' % name
-            lvals.append('%s&nbsp;= %s' % (name, value))
-        if lvals:
-            lvals = string.join(lvals, ', ')
-            lvals = indent + '''
-<small><font color="#909090">%s</font></small><br>''' % lvals
-        else:
-            lvals = ''
-
-        excerpt = []
-        i = lnum - index
-        for line in lines:
-            number = '&nbsp;' * (5-len(str(i))) + str(i)
-            number = '<small><font color="#909090">%s</font></small>' % number
-            line = '<tt>%s&nbsp;%s</tt>' % (number, pydoc.html.preformat(line))
-            if i == lnum:
-                line = '''
-<table width="100%%" bgcolor="#ffccee" cellspacing=0 cellpadding=0 border=0>
-<tr><td>%s</td></tr></table>''' % line
-            excerpt.append('\n' + line)
-            if i == lnum:
-                excerpt.append(lvals)
-            i = i + 1
-        traceback.append('<p>' + level + string.join(excerpt, '\n'))
-
-    traceback.reverse()
-
-    exception = '<p><strong>%s</strong>: %s' % (str(etype), str(evalue))
-    attribs = []
-    if type(evalue) is types.InstanceType:
-        for name in dir(evalue):
-            value = pydoc.html.repr(getattr(evalue, name))
-            attribs.append('<br>%s%s&nbsp;= %s' % (indent, name, value))
-
-    return head + string.join(attribs) + string.join(traceback) + '<p>&nbsp;</p>'
-
-def handler():
-    print breaker()
-    print html()
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.9  2002/01/08 11:56:24  richard
-# missed an import _
-#
-# Revision 1.8  2002/01/05 02:22:32  richard
-# i18n'ification
-#
-# Revision 1.7  2001/11/22 15:46:42  jhermann
-# Added module docstrings to all modules.
-#
-# Revision 1.6  2001/09/29 13:27:00  richard
-# CGI interfaces now spit up a top-level index of all the instances they can
-# serve.
-#
-# Revision 1.5  2001/08/07 00:24:42  richard
-# stupid typo
-#
-# Revision 1.4  2001/08/07 00:15:51  richard
-# Added the copyright/license notice to (nearly) all files at request of
-# Bizar Software.
-#
-# Revision 1.3  2001/07/29 07:01:39  richard
-# Added vim command to all source so that we don't get no steenkin' tabs :)
-#
-# Revision 1.2  2001/07/22 12:09:32  richard
-# Final commit of Grande Splite
-#
-# Revision 1.1  2001/07/22 11:58:35  richard
-# More Grande Splite
-#
-#
-# vim: set filetype=python ts=4 sw=4 et si
diff --git a/roundup/template_funcs.py b/roundup/template_funcs.py
deleted file mode 100755 (executable)
index 65c4f69..0000000
+++ /dev/null
@@ -1,826 +0,0 @@
-# 
-# $Id: template_funcs.py,v 1.3 2002-08-19 00:22:47 richard Exp $
-#
-import hyperdb, date, password
-from i18n import _
-import htmltemplate
-import cgi, os, StringIO, urllib, types
-
-def do_plain(client, classname, cl, props, nodeid, filterspec, property,
-        escape=0, lookup=1):
-    ''' display a String property directly;
-
-        display a Date property in a specified time zone with an option to
-        omit the time from the date stamp;
-
-        for a Link or Multilink property, display the key strings of the
-        linked nodes (or the ids if the linked class has no key property)
-        when the lookup argument is true, otherwise just return the
-        linked ids
-    '''
-    if not nodeid and client.form is None:
-        return _('[Field: not called from item]')
-    propclass = props[property]
-    value = determine_value(cl, props, nodeid, filterspec, property)
-        
-    if isinstance(propclass, hyperdb.Password):
-        value = _('*encrypted*')
-    elif isinstance(propclass, hyperdb.Boolean):
-        value = value and "Yes" or "No"
-    elif isinstance(propclass, hyperdb.Link):
-        if value:
-            if lookup:
-                linkcl = client.db.classes[propclass.classname]
-                k = linkcl.labelprop(1)
-                value = linkcl.get(value, k)
-        else:
-            value = _('[unselected]')
-    elif isinstance(propclass, hyperdb.Multilink):
-        if value:
-            if lookup:
-                linkcl = client.db.classes[propclass.classname]
-                k = linkcl.labelprop(1)
-                labels = []
-                for v in value:
-                    labels.append(linkcl.get(v, k))
-                value = ', '.join(labels)
-            else:
-                value = ', '.join(value)
-        else:
-            value = ''
-    else:
-        value = str(value)
-            
-    if escape:
-        value = cgi.escape(value)
-    return value
-
-def do_stext(client, classname, cl, props, nodeid, filterspec, property,
-        escape=0):
-    '''Render as structured text using the StructuredText module
-       (see above for details)
-    '''
-    s = do_plain(client, classname, cl, props, nodeid, filterspec, property,
-        escape=escape)
-    if not StructuredText:
-        return s
-    return StructuredText(s,level=1,header=0)
-
-def determine_value(cl, props, nodeid, filterspec, property):
-    '''determine the value of a property using the node, form or
-       filterspec
-    '''
-    if nodeid:
-        value = cl.get(nodeid, property, None)
-        if value is None:
-            if isinstance(props[property], hyperdb.Multilink):
-                return []
-            return ''
-        return value
-    elif filterspec is not None:
-        if isinstance(props[property], hyperdb.Multilink):
-            return filterspec.get(property, [])
-        else:
-            return filterspec.get(property, '')
-    # TODO: pull the value from the form
-    if isinstance(props[property], hyperdb.Multilink):
-        return []
-    else:
-        return ''
-
-def make_sort_function(client, filterspec, classname):
-    '''Make a sort function for a given class
-    '''
-    linkcl = client.db.getclass(classname)
-    if linkcl.getprops().has_key('order'):
-        sort_on = 'order'
-    else:
-        sort_on = linkcl.labelprop()
-    def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
-        return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
-    return sortfunc
-
-def do_field(client, classname, cl, props, nodeid, filterspec, property,
-        size=None, showid=0):
-    ''' display a property like the plain displayer, but in a text field
-        to be edited
-
-        Note: if you would prefer an option list style display for
-        link or multilink editing, use menu().
-    '''
-    if not nodeid and client.form is None and filterspec is None:
-        return _('[Field: not called from item]')
-    if size is None:
-        size = 30
-
-    propclass = props[property]
-
-    # get the value
-    value = determine_value(cl, props, nodeid, filterspec, property)
-    # now display
-    if (isinstance(propclass, hyperdb.String) or
-            isinstance(propclass, hyperdb.Date) or
-            isinstance(propclass, hyperdb.Interval)):
-        if value is None:
-            value = ''
-        else:
-            value = cgi.escape(str(value))
-            value = '&quot;'.join(value.split('"'))
-        s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
-    elif isinstance(propclass, hyperdb.Boolean):
-        checked = value and "checked" or ""
-        s = '<input type="radio" name="%s" value="yes" %s>Yes'%(property,
-            checked)
-        if checked:
-            checked = ""
-        else:
-            checked = "checked"
-        s += '<input type="radio" name="%s" value="no" %s>No'%(property,
-            checked)
-    elif isinstance(propclass, hyperdb.Number):
-        s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
-    elif isinstance(propclass, hyperdb.Password):
-        s = '<input type="password" name="%s" size="%s">'%(property, size)
-    elif isinstance(propclass, hyperdb.Link):
-        linkcl = client.db.getclass(propclass.classname)
-        if linkcl.getprops().has_key('order'):  
-            sort_on = 'order'  
-        else:  
-            sort_on = linkcl.labelprop()  
-        options = linkcl.filter(None, {}, [sort_on], []) 
-        # TODO: make this a field display, not a menu one!
-        l = ['<select name="%s">'%property]
-        k = linkcl.labelprop(1)
-        if value is None:
-            s = 'selected '
-        else:
-            s = ''
-        l.append(_('<option %svalue="-1">- no selection -</option>')%s)
-        for optionid in options:
-            option = linkcl.get(optionid, k)
-            s = ''
-            if optionid == value:
-                s = 'selected '
-            if showid:
-                lab = '%s%s: %s'%(propclass.classname, optionid, option)
-            else:
-                lab = option
-            if size is not None and len(lab) > size:
-                lab = lab[:size-3] + '...'
-            lab = cgi.escape(lab)
-            l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
-        l.append('</select>')
-        s = '\n'.join(l)
-    elif isinstance(propclass, hyperdb.Multilink):
-        sortfunc = make_sort_function(client, filterspec, propclass.classname)
-        linkcl = client.db.getclass(propclass.classname)
-        if value:
-            value.sort(sortfunc)
-        # map the id to the label property
-        if not showid:
-            k = linkcl.labelprop(1)
-            value = [linkcl.get(v, k) for v in value]
-        value = cgi.escape(','.join(value))
-        s = '<input name="%s" size="%s" value="%s">'%(property, size, value)
-    else:
-        s = _('Plain: bad propclass "%(propclass)s"')%locals()
-    return s
-
-def do_multiline(client, classname, cl, props, nodeid, filterspec, property,
-        rows=5, cols=40):
-    ''' display a string property in a multiline text edit field
-    '''
-    if not nodeid and client.form is None and filterspec is None:
-        return _('[Multiline: not called from item]')
-
-    propclass = props[property]
-
-    # make sure this is a link property
-    if not isinstance(propclass, hyperdb.String):
-        return _('[Multiline: not a string]')
-
-    # get the value
-    value = determine_value(cl, props, nodeid, filterspec, property)
-    if value is None:
-        value = ''
-
-    # display
-    return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
-        property, rows, cols, value)
-
-def do_menu(client, classname, cl, props, nodeid, filterspec, property,
-        size=None, height=None, showid=0, additional=[], **conditions):
-    ''' For a Link/Multilink property, display a menu of the available
-        choices
-
-        If the additional properties are specified, they will be
-        included in the text of each option in (brackets, with, commas).
-    '''
-    if not nodeid and client.form is None and filterspec is None:
-        return _('[Field: not called from item]')
-
-    propclass = props[property]
-
-    # make sure this is a link property
-    if not (isinstance(propclass, hyperdb.Link) or
-            isinstance(propclass, hyperdb.Multilink)):
-        return _('[Menu: not a link]')
-
-    # sort function
-    sortfunc = make_sort_function(client, filterspec, propclass.classname)
-
-    # get the value
-    value = determine_value(cl, props, nodeid, filterspec, property)
-
-    # display
-    if isinstance(propclass, hyperdb.Multilink):
-        linkcl = client.db.getclass(propclass.classname)
-        if linkcl.getprops().has_key('order'):  
-            sort_on = 'order'  
-        else:  
-            sort_on = linkcl.labelprop()
-        options = linkcl.filter(None, conditions, [sort_on], []) 
-        height = height or min(len(options), 7)
-        l = ['<select multiple name="%s" size="%s">'%(property, height)]
-        k = linkcl.labelprop(1)
-        for optionid in options:
-            option = linkcl.get(optionid, k)
-            s = ''
-            if optionid in value or option in value:
-                s = 'selected '
-            if showid:
-                lab = '%s%s: %s'%(propclass.classname, optionid, option)
-            else:
-                lab = option
-            if size is not None and len(lab) > size:
-                lab = lab[:size-3] + '...'
-            if additional:
-                m = []
-                for propname in additional:
-                    m.append(linkcl.get(optionid, propname))
-                lab = lab + ' (%s)'%', '.join(m)
-            lab = cgi.escape(lab)
-            l.append('<option %svalue="%s">%s</option>'%(s, optionid,
-                lab))
-        l.append('</select>')
-        return '\n'.join(l)
-    if isinstance(propclass, hyperdb.Link):
-        # force the value to be a single choice
-        if type(value) is types.ListType:
-            value = value[0]
-        linkcl = client.db.getclass(propclass.classname)
-        l = ['<select name="%s">'%property]
-        k = linkcl.labelprop(1)
-        s = ''
-        if value is None:
-            s = 'selected '
-        l.append(_('<option %svalue="-1">- no selection -</option>')%s)
-        if linkcl.getprops().has_key('order'):  
-            sort_on = 'order'  
-        else:  
-            sort_on = linkcl.labelprop() 
-        options = linkcl.filter(None, conditions, [sort_on], []) 
-        for optionid in options:
-            option = linkcl.get(optionid, k)
-            s = ''
-            if value in [optionid, option]:
-                s = 'selected '
-            if showid:
-                lab = '%s%s: %s'%(propclass.classname, optionid, option)
-            else:
-                lab = option
-            if size is not None and len(lab) > size:
-                lab = lab[:size-3] + '...'
-            if additional:
-                m = []
-                for propname in additional:
-                    m.append(linkcl.get(optionid, propname))
-                lab = lab + ' (%s)'%', '.join(map(str, m))
-            lab = cgi.escape(lab)
-            l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
-        l.append('</select>')
-        return '\n'.join(l)
-    return _('[Menu: not a link]')
-
-#XXX deviates from spec
-def do_link(client, classname, cl, props, nodeid, filterspec, property=None,
-        is_download=0, showid=0):
-    '''For a Link or Multilink property, display the names of the linked
-       nodes, hyperlinked to the item views on those nodes.
-       For other properties, link to this node with the property as the
-       text.
-
-       If is_download is true, append the property value to the generated
-       URL so that the link may be used as a download link and the
-       downloaded file name is correct.
-    '''
-    if not nodeid and client.form is None:
-        return _('[Link: not called from item]')
-
-    # get the value
-    value = determine_value(cl, props, nodeid, filterspec, property)
-    propclass = props[property]
-    if isinstance(propclass, hyperdb.Boolean):
-        value = value and "Yes" or "No"
-    elif isinstance(propclass, hyperdb.Link):
-        if value in ('', None, []):
-            return _('[no %(propname)s]')%{'propname':property.capitalize()}
-        linkname = propclass.classname
-        linkcl = client.db.getclass(linkname)
-        k = linkcl.labelprop(1)
-        linkvalue = cgi.escape(str(linkcl.get(value, k)))
-        if showid:
-            label = value
-            title = ' title="%s"'%linkvalue
-            # note ... this should be urllib.quote(linkcl.get(value, k))
-        else:
-            label = linkvalue
-            title = ''
-        if is_download:
-            return '<a href="%s%s/%s"%s>%s</a>'%(linkname, value,
-                linkvalue, title, label)
-        else:
-            return '<a href="%s%s"%s>%s</a>'%(linkname, value, title, label)
-    elif isinstance(propclass, hyperdb.Multilink):
-        if value in ('', None, []):
-            return _('[no %(propname)s]')%{'propname':property.capitalize()}
-        linkname = propclass.classname
-        linkcl = client.db.getclass(linkname)
-        k = linkcl.labelprop(1)
-        l = []
-        for value in value:
-            linkvalue = cgi.escape(str(linkcl.get(value, k)))
-            if showid:
-                label = value
-                title = ' title="%s"'%linkvalue
-                # note ... this should be urllib.quote(linkcl.get(value, k))
-            else:
-                label = linkvalue
-                title = ''
-            if is_download:
-                l.append('<a href="%s%s/%s"%s>%s</a>'%(linkname, value,
-                    linkvalue, title, label))
-            else:
-                l.append('<a href="%s%s"%s>%s</a>'%(linkname, value,
-                    title, label))
-        return ', '.join(l)
-    if is_download:
-        if value in ('', None, []):
-            return _('[no %(propname)s]')%{'propname':property.capitalize()}
-        return '<a href="%s%s/%s">%s</a>'%(classname, nodeid,
-            value, value)
-    else:
-        if value in ('', None, []):
-            value =  _('[no %(propname)s]')%{'propname':property.capitalize()}
-        return '<a href="%s%s">%s</a>'%(classname, nodeid, value)
-
-def do_count(client, classname, cl, props, nodeid, filterspec, property,
-        **args):
-    ''' for a Multilink property, display a count of the number of links in
-        the list
-    '''
-    if not nodeid:
-        return _('[Count: not called from item]')
-
-    propclass = props[property]
-    if not isinstance(propclass, hyperdb.Multilink):
-        return _('[Count: not a Multilink]')
-
-    # figure the length then...
-    value = cl.get(nodeid, property)
-    return str(len(value))
-
-# XXX pretty is definitely new ;)
-def do_reldate(client, classname, cl, props, nodeid, filterspec, property,
-        pretty=0):
-    ''' display a Date property in terms of an interval relative to the
-        current date (e.g. "+ 3w", "- 2d").
-
-        with the 'pretty' flag, make it pretty
-    '''
-    if not nodeid and client.form is None:
-        return _('[Reldate: not called from item]')
-
-    propclass = props[property]
-    if not isinstance(propclass, hyperdb.Date):
-        return _('[Reldate: not a Date]')
-
-    if nodeid:
-        value = cl.get(nodeid, property)
-    else:
-        return ''
-    if not value:
-        return ''
-
-    # figure the interval
-    interval = date.Date('.') - value
-    if pretty:
-        if not nodeid:
-            return _('now')
-        return interval.pretty()
-    return str(interval)
-
-def do_download(client, classname, cl, props, nodeid, filterspec, property,
-        **args):
-    ''' show a Link("file") or Multilink("file") property using links that
-        allow you to download files
-    '''
-    if not nodeid:
-        return _('[Download: not called from item]')
-    return do_link(client, classname, cl, props, nodeid, filterspec, property,
-        is_download=1)
-
-def do_checklist(client, classname, cl, props, nodeid, filterspec, property,
-        sortby=None):
-    ''' for a Link or Multilink property, display checkboxes for the
-        available choices to permit filtering
-
-        sort the checklist by the argument (+/- property name)
-    '''
-    propclass = props[property]
-    if (not isinstance(propclass, hyperdb.Link) and not
-            isinstance(propclass, hyperdb.Multilink)):
-        return _('[Checklist: not a link]')
-
-    # get our current checkbox state
-    if nodeid:
-        # get the info from the node - make sure it's a list
-        if isinstance(propclass, hyperdb.Link):
-            value = [cl.get(nodeid, property)]
-        else:
-            value = cl.get(nodeid, property)
-    elif filterspec is not None:
-        # get the state from the filter specification (always a list)
-        value = filterspec.get(property, [])
-    else:
-        # it's a new node, so there's no state
-        value = []
-
-    # so we can map to the linked node's "lable" property
-    linkcl = client.db.getclass(propclass.classname)
-    l = []
-    k = linkcl.labelprop(1)
-
-    # build list of options and then sort it, either
-    # by id + label or <sortby>-value + label;
-    # a minus reverses the sort order, while + or no
-    # prefix sort in increasing order
-    reversed = 0
-    if sortby:
-        if sortby[0] == '-':
-            reversed = 1
-            sortby = sortby[1:]
-        elif sortby[0] == '+':
-            sortby = sortby[1:]
-    options = []
-    for optionid in linkcl.list():
-        if sortby:
-            sortval = linkcl.get(optionid, sortby)
-        else:
-            sortval = int(optionid)
-        option = cgi.escape(str(linkcl.get(optionid, k)))
-        options.append((sortval, option, optionid))
-    options.sort()
-    if reversed:
-        options.reverse()
-
-    # build checkboxes
-    for sortval, option, optionid in options:
-        if optionid in value or option in value:
-            checked = 'checked'
-        else:
-            checked = ''
-        l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
-            option, checked, property, option))
-
-    # for Links, allow the "unselected" option too
-    if isinstance(propclass, hyperdb.Link):
-        if value is None or '-1' in value:
-            checked = 'checked'
-        else:
-            checked = ''
-        l.append(_('[unselected]:<input type="checkbox" %s name="%s" '
-            'value="-1">')%(checked, property))
-    return '\n'.join(l)
-
-def do_note(client, classname, cl, props, nodeid, filterspec, rows=5, cols=80):
-    ''' display a "note" field, which is a text area for entering a note to
-        go along with a change. 
-    '''
-    # TODO: pull the value from the form
-    return '<textarea name="__note" wrap="hard" rows=%s cols=%s>'\
-        '</textarea>'%(rows, cols)
-
-# XXX new function
-def do_list(client, classname, cl, props, nodeid, filterspec, property,
-        reverse=0, xtracols=None):
-    ''' list the items specified by property using the standard index for
-        the class
-    '''
-    propcl = props[property]
-    if not isinstance(propcl, hyperdb.Multilink):
-        return _('[List: not a Multilink]')
-
-    value = determine_value(cl, props, nodeid, filterspec, property)
-    if not value:
-        return ''
-
-    # sort, possibly revers and then re-stringify
-    value = map(int, value)
-    value.sort()
-    if reverse:
-        value.reverse()
-    value = map(str, value)
-
-    # render the sub-index into a string
-    fp = StringIO.StringIO()
-    try:
-        write_save = client.write
-        client.write = fp.write
-        client.listcontext = ('%s%s' % (classname, nodeid), property)
-        index = htmltemplate.IndexTemplate(client, client.instance.TEMPLATES,
-            propcl.classname)
-        index.render(nodeids=value, show_display_form=0, xtracols=xtracols)
-    finally:
-        client.listcontext = None
-        client.write = write_save
-
-    return fp.getvalue()
-
-# XXX new function
-def do_history(client, classname, cl, props, nodeid, filterspec,
-        direction='descending'):
-    ''' list the history of the item
-
-        If "direction" is 'descending' then the most recent event will
-        be displayed first. If it is 'ascending' then the oldest event
-        will be displayed first.
-    '''
-    if nodeid is None:
-        return _("[History: node doesn't exist]")
-
-    l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
-        '<tr class="list-header">',
-        _('<th align=left><span class="list-item">Date</span></th>'),
-        _('<th align=left><span class="list-item">User</span></th>'),
-        _('<th align=left><span class="list-item">Action</span></th>'),
-        _('<th align=left><span class="list-item">Args</span></th>'),
-        '</tr>']
-    comments = {}
-    history = cl.history(nodeid)
-    history.sort()
-    if direction == 'descending':
-        history.reverse()
-    for id, evt_date, user, action, args in history:
-        date_s = str(evt_date).replace("."," ")
-        arg_s = ''
-        if action == 'link' and type(args) == type(()):
-            if len(args) == 3:
-                linkcl, linkid, key = args
-                arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
-                    linkcl, linkid, key)
-            else:
-                arg_s = str(args)
-
-        elif action == 'unlink' and type(args) == type(()):
-            if len(args) == 3:
-                linkcl, linkid, key = args
-                arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
-                    linkcl, linkid, key)
-            else:
-                arg_s = str(args)
-
-        elif type(args) == type({}):
-            cell = []
-            for k in args.keys():
-                # try to get the relevant property and treat it
-                # specially
-                try:
-                    prop = props[k]
-                except:
-                    prop = None
-                if prop is not None:
-                    if args[k] and (isinstance(prop, hyperdb.Multilink) or
-                            isinstance(prop, hyperdb.Link)):
-                        # figure what the link class is
-                        classname = prop.classname
-                        try:
-                            linkcl = client.db.getclass(classname)
-                        except KeyError:
-                            labelprop = None
-                            comments[classname] = _('''The linked class
-                                %(classname)s no longer exists''')%locals()
-                        labelprop = linkcl.labelprop(1)
-                        hrefable = os.path.exists(
-                            os.path.join(client.instance.TEMPLATES, classname+'.item'))
-
-                    if isinstance(prop, hyperdb.Multilink) and \
-                            len(args[k]) > 0:
-                        ml = []
-                        for linkid in args[k]:
-                            if isinstance(linkid, type(())):
-                                sublabel = linkid[0] + ' '
-                                linkids = linkid[1]
-                            else:
-                                sublabel = ''
-                                linkids = [linkid]
-                            subml = []
-                            for linkid in linkids:
-                                label = classname + linkid
-                                # if we have a label property, try to use it
-                                # TODO: test for node existence even when
-                                # there's no labelprop!
-                                try:
-                                    if labelprop is not None:
-                                        label = linkcl.get(linkid, labelprop)
-                                except IndexError:
-                                    comments['no_link'] = _('''<strike>The
-                                        linked node no longer
-                                        exists</strike>''')
-                                    subml.append('<strike>%s</strike>'%label)
-                                else:
-                                    if hrefable:
-                                        subml.append('<a href="%s%s">%s</a>'%(
-                                            classname, linkid, label))
-                                    else:
-                                        subml.append(label)
-                            ml.append(sublabel + ', '.join(subml))
-                        cell.append('%s:\n  %s'%(k, ', '.join(ml)))
-                    elif isinstance(prop, hyperdb.Link) and args[k]:
-                        label = classname + args[k]
-                        # if we have a label property, try to use it
-                        # TODO: test for node existence even when
-                        # there's no labelprop!
-                        if labelprop is not None:
-                            try:
-                                label = linkcl.get(args[k], labelprop)
-                            except IndexError:
-                                comments['no_link'] = _('''<strike>The
-                                    linked node no longer
-                                    exists</strike>''')
-                                cell.append(' <strike>%s</strike>,\n'%label)
-                                # "flag" this is done .... euwww
-                                label = None
-                        if label is not None:
-                            if hrefable:
-                                cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
-                                    classname, args[k], label))
-                            else:
-                                cell.append('%s: %s' % (k,label))
-
-                    elif isinstance(prop, hyperdb.Date) and args[k]:
-                        d = date.Date(args[k])
-                        cell.append('%s: %s'%(k, str(d)))
-
-                    elif isinstance(prop, hyperdb.Interval) and args[k]:
-                        d = date.Interval(args[k])
-                        cell.append('%s: %s'%(k, str(d)))
-
-                    elif isinstance(prop, hyperdb.String) and args[k]:
-                        cell.append('%s: %s'%(k, cgi.escape(args[k])))
-
-                    elif not args[k]:
-                        cell.append('%s: (no value)\n'%k)
-
-                    else:
-                        cell.append('%s: %s\n'%(k, str(args[k])))
-                else:
-                    # property no longer exists
-                    comments['no_exist'] = _('''<em>The indicated property
-                        no longer exists</em>''')
-                    cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
-            arg_s = '<br />'.join(cell)
-        else:
-            # unkown event!!
-            comments['unknown'] = _('''<strong><em>This event is not
-                handled by the history display!</em></strong>''')
-            arg_s = '<strong><em>' + str(args) + '</em></strong>'
-        date_s = date_s.replace(' ', '&nbsp;')
-        l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
-            '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
-            user, action, arg_s))
-    if comments:
-        l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
-    for entry in comments.values():
-        l.append('<tr><td colspan=4>%s</td></tr>'%entry)
-    l.append('</table>')
-    return '\n'.join(l)
-
-# XXX new function
-def do_submit(client, classname, cl, props, nodeid, filterspec, value=None):
-    ''' add a submit button for the item
-    '''
-    if value is None:
-        if nodeid:
-            value = "Submit Changes"
-        else:
-            value = "Submit New Entry"
-    if nodeid or client.form is not None:
-        return _('<input type="submit" name="submit" value="%s">' % value)
-    else:
-        return _('[Submit: not called from item]')
-
-def do_classhelp(client, classname, cl, props, nodeid, filterspec, clname,
-        properties, label='?', width='400', height='400'):
-    '''pop up a javascript window with class help
-
-       This generates a link to a popup window which displays the 
-       properties indicated by "properties" of the class named by
-       "classname". The "properties" should be a comma-separated list
-       (eg. 'id,name,description').
-
-       You may optionally override the label displayed, the width and
-       height. The popup window will be resizable and scrollable.
-    '''
-    return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
-        'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(clname,
-        properties, width, height, label)
-
-def do_email(client, classname, cl, props, nodeid, filterspec, property,
-        escape=0):
-    '''display the property as one or more "fudged" email addrs
-    '''
-    
-    if not nodeid and client.form is None:
-        return _('[Email: not called from item]')
-    propclass = props[property]
-    if nodeid:
-        # get the value for this property
-        try:
-            value = cl.get(nodeid, property)
-        except KeyError:
-            # a KeyError here means that the node doesn't have a value
-            # for the specified property
-            value = ''
-    else:
-        value = ''
-    if isinstance(propclass, hyperdb.String):
-        if value is None: value = ''
-        else: value = str(value)
-        value = value.replace('@', ' at ')
-        value = value.replace('.', ' ')
-    else:
-        value = _('[Email: not a string]')%locals()
-    if escape:
-        value = cgi.escape(value)
-    return value
-
-def do_filterspec(client, classname, cl, props, nodeid, filterspec, classprop,
-        urlprop):
-
-    qs = cl.get(nodeid, urlprop)
-    classname = cl.get(nodeid, classprop)
-    filterspec = {}
-    query = cgi.parse_qs(qs)
-    for k,v in query.items():
-        query[k] = v[0].split(',')
-    pagesize = query.get(':pagesize',['25'])[0]
-    search_text = query.get('search_text', [''])[0]
-    search_text = urllib.unquote(search_text)
-    for k,v in query.items():
-        if k[0] != ':':
-            filterspec[k] = v
-    ixtmplt = htmltemplate.IndexTemplate(client, client.instance.TEMPLATES,
-        classname)
-    qform = '<form onSubmit="return submit_once()" action="%s%s">\n'%(
-        classname,nodeid)
-    qform += ixtmplt.filter_form(search_text,
-                                 query.get(':filter', []),
-                                 query.get(':columns', []),
-                                 query.get(':group', []),
-                                 [],
-                                 query.get(':sort',[]),
-                                 filterspec,
-                                 pagesize)
-    return qform + '</table>\n'
-
-def do_href(client, classname, cl, props, nodeid, filterspec, property,
-        prefix='', suffix='', label=''):
-    ''' Generate a link to the value of the property, with the form:
-
-            <a href="[prefix][value][suffix]">[label]</a>
-
-        where the [value] is the specified property value.
-    '''
-    value = determine_value(cl, props, nodeid, filterspec, property)
-    return '<a href="%s%s%s">%s</a>'%(prefix, value, suffix, label)
-
-def do_remove(client, classname, cl, props, nodeid, filterspec):
-    ''' put a remove href for an item in a list '''
-    if not nodeid:
-        return _('[Remove not called from item]')
-    try:
-        parentdesignator, mlprop = client.listcontext
-    except (AttributeError, TypeError):
-        return _('[Remove not called form listing of multilink]')
-    return '<a href="remove?:target=%s%s&:multilink=%s:%s">[Remove]</a>'%(
-        classname, nodeid, parentdesignator, mlprop)
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.2  2002/08/15 00:40:10  richard
-# cleanup
-#
-#
-#
-# vim: set filetype=python ts=4 sw=4 et si
diff --git a/roundup/template_parser.py b/roundup/template_parser.py
deleted file mode 100644 (file)
index e3e8ec4..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-import htmllib, formatter
-
-class Require:
-    ''' Encapsulates a parsed <require attributes>...[<else>...]</require>
-    '''
-    def __init__(self, attributes):
-        self.attributes = attributes
-        self.current = self.ok = []
-        self.fail = []
-    def __len__(self):
-        return len(self.current)
-    def __getitem__(self, n):
-        return self.current[n]
-    def __setitem__(self, n, data):
-        self.current[n] = data
-    def append(self, data):
-        self.current.append(data)
-    def elseMode(self):
-        self.current = self.fail
-    def __repr__(self):
-        return '<Require %r ok:%r fail:%r>'%(self.attributes, self.ok,
-            self.fail)
-
-class Display:
-    ''' Encapsulates a parsed <display attributes>
-    '''
-    def __init__(self, attributes):
-        self.attributes = attributes
-    def __repr__(self):
-        return '<Display %r>'%self.attributes
-
-class Property:
-    ''' Encapsulates a parsed <property attributes>
-    '''
-    def __init__(self, attributes):
-        self.attributes = attributes
-        self.current = self.ok = []
-    def __len__(self):
-        return len(self.current)
-    def __getitem__(self, n):
-        return self.current[n]
-    def __setitem__(self, n, data):
-        self.current[n] = data
-    def append(self, data):
-        self.current.append(data)
-    def __repr__(self):
-        return '<Property %r %r>'%(self.attributes, self.structure)
-
-class RoundupTemplate(htmllib.HTMLParser):
-    ''' Parse Roundup's HTML template structure into a list of components:
-
-        'string': this is just plain data to be displayed
-        Display : instances indicate that display functions are to be called
-        Require : if/else style check using the conditions in the attributes,
-                  displaying the "ok" list of components or "fail" list
-
-    '''
-    def __init__(self):
-        htmllib.HTMLParser.__init__(self, formatter.NullFormatter())
-        self.current = self.structure = []
-        self.stack = []
-
-    def handle_data(self, data):
-        self.append_data(data)
-
-    def append_data(self, data):
-        if self.current and isinstance(self.current[-1], type('')):
-            self.current[-1] = self.current[-1] + data
-        else:
-            self.current.append(data)
-
-    def unknown_starttag(self, tag, attributes):
-        self.append_data('<%s' % tag)
-        closeit = 1
-        for name, value in attributes:
-            pos = value.find('<')
-            if pos > -1:
-                self.append_data(' %s="%s' % (name, value[:pos]))
-                closeit = 0
-            else:
-                self.append_data(' %s="%s"' % (name, value))
-        if closeit:
-            self.append_data('>')
-
-    def handle_starttag(self, tag, method, attributes):
-        if tag in ('require', 'else', 'display', 'property'):
-            method(attributes)
-        else:
-            self.unknown_starttag(tag, attributes)
-
-    def unknown_endtag(self, tag):
-        if tag in ('require','property'):
-            self.current = self.stack.pop()
-        else:
-            self.append_data('</%s>'%tag)
-
-    def handle_endtag(self, tag, method):
-        self.unknown_endtag(tag)
-
-    def close(self):
-        htmllib.HTMLParser.close(self)
-
-    def do_display(self, attributes):
-        self.current.append(Display(attributes))
-
-    def do_property(self, attributes):
-        p = Property(attributes)
-        self.current.append(p)
-        self.stack.append(self.current)
-        self.current = p
-
-    def do_require(self, attributes):
-        r = Require(attributes)
-        self.current.append(r)
-        self.stack.append(self.current)
-        self.current = r
-
-    def do_else(self, attributes):
-        self.current.elseMode()
-
-    def __repr__(self):
-        return '<RoundupTemplate %r>'%self.structure
-
-def display(structure, indent=''):
-    ''' Pretty-print the parsed structure for debugging
-    '''
-    l = []
-    for entry in structure:
-        if isinstance(entry, type('')):
-            l.append("%s%s"%(indent, entry))
-        elif isinstance(entry, Require):
-            l.append('%sTEST: %r\n'%(indent, entry.attributes))
-            l.append('%sOK...'%indent)
-            l.append(display(entry.ok, indent+' '))
-            if entry.fail:
-                l.append('%sFAIL...'%indent)
-                l.append(display(entry.fail, indent+' '))
-        elif isinstance(entry, Display):
-            l.append('%sDISPLAY: %r'%(indent, entry.attributes))
-        elif isinstance(entry, Property):
-            l.append('%sPROPERTY: %r'%(indent, entry.attributes))
-            l.append(display(entry.ok, indent+' '))
-    return ''.join(l)
-
-if __name__ == '__main__':
-    import sys
-    parser = RoundupTemplate()
-    parser.feed(open(sys.argv[1], 'r').read())
-    print display(parser.structure)
-