Code

Modified roundup-mailgw so it can read e-mails from a local mail spool
[roundup.git] / roundup / cgi_client.py
index e8e6c0ba3f230ce3b32a09764acec9cb9011683e..43b648e85cf70746552d98ee706e83ca8715d370 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: cgi_client.py,v 1.29 2001-10-09 07:25:59 richard Exp $
+# $Id: cgi_client.py,v 1.55 2001-11-07 02:34:06 jhermann Exp $
 
 import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
-import base64, Cookie, time
+import binascii, Cookie, time
 
 import roundupdb, htmltemplate, date, hyperdb, password
 
@@ -30,7 +30,6 @@ class NotFound(ValueError):
 
 class Client:
     '''
-
     A note about login
     ------------------
 
@@ -40,33 +39,52 @@ class Client:
     '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.
+
+
+    Customisation
+    -------------
+      FILTER_POSITION - one of 'top', 'bottom', 'top and bottom'
+      ANONYMOUS_ACCESS - one of 'deny', 'allow'
+      ANONYMOUS_REGISTER - one of 'deny', 'allow'
+
     '''
+    FILTER_POSITION = 'bottom'       # one of 'top', 'bottom', 'top and bottom'
+    ANONYMOUS_ACCESS = 'deny'        # one of 'deny', 'allow'
+    ANONYMOUS_REGISTER = 'deny'      # one of 'deny', 'allow'
 
-    def __init__(self, instance, out, env):
+    def __init__(self, instance, request, env):
         self.instance = instance
-        self.out = out
+        self.request = request
         self.env = env
         self.path = env['PATH_INFO']
         self.split_path = self.path.split('/')
 
-        self.headers_done = 0
         self.form = cgi.FieldStorage(environ=env)
         self.headers_done = 0
-        self.debug = 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):
         return self.db.user.lookup(self.user)
 
     def header(self, headers={'Content-Type':'text/html'}):
+        '''Put up the appropriate header.
+        '''
         if not headers.has_key('Content-Type'):
             headers['Content-Type'] = 'text/html'
+        self.request.send_response(200)
         for entry in headers.items():
-            self.out.write('%s: %s\n'%entry)
-        self.out.write('\n')
+            self.request.send_header(*entry)
+        self.request.end_headers()
         self.headers_done = 1
+        if self.debug:
+            self.headers_sent = headers
 
     def pagehead(self, title, message=None):
-        url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
+        url = self.env['SCRIPT_NAME'] + '/'
         machine = self.env['SERVER_NAME']
         port = self.env['SERVER_PORT']
         if port != '80': machine = machine + ':' + port
@@ -76,11 +94,27 @@ class Client:
         else:
             message = ''
         style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
-        if self.user is not None:
+        user_name = self.user or ''
+        if self.user == 'admin':
+            admin_links = ' | <a href="list_classes">Class List</a>'
+        else:
+            admin_links = ''
+        if self.user not in (None, 'anonymous'):
             userid = self.db.user.lookup(self.user)
-            user_info = '(login: <a href="user%s">%s</a>)'%(userid, self.user)
+            user_info = '''
+<a href="issue?assignedto=%s&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:filter=status,assignedto&:sort=activity&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">My Issues</a> |
+<a href="user%s">My Details</a> | <a href="logout">Logout</a>
+'''%(userid, userid)
         else:
-            user_info = ''
+            user_info = '<a href="login">Login</a>'
+        if self.user is not None:
+            add_links = '''
+| Add
+<a href="newissue">Issue</a>,
+<a href="newuser">User</a>
+'''
+        else:
+            add_links = ''
         self.write('''<html><head>
 <title>%s</title>
 <style type="text/css">%s</style>
@@ -88,9 +122,19 @@ class Client:
 <body bgcolor=#ffffff>
 %s
 <table width=100%% border=0 cellspacing=0 cellpadding=2>
-<tr class="location-bar"><td><big><strong>%s</strong></big> %s</td></tr>
+<tr class="location-bar"><td><big><strong>%s</strong></big></td>
+<td align=right valign=bottom>%s</td></tr>
+<tr class="location-bar">
+<td align=left>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>
+%s
+%s</td>
+<td align=right>%s</td>
 </table>
-'''%(title, style, message, title, user_info))
+'''%(title, style, message, title, user_name, add_links, admin_links,
+    user_info))
 
     def pagefoot(self):
         if self.debug:
@@ -102,21 +146,30 @@ class Client:
             if keys:
                 self.write('<dt><b>Form entries</b></dt>')
                 for k in self.form.keys():
-                    v = str(self.form[k].value)
-                    self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
+                    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('<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.out.write(content)
+        self.request.wfile.write(content)
 
     def index_arg(self, arg):
         ''' handle the args to index - they might be a list from the form
@@ -130,7 +183,7 @@ class Client:
             return arg.value.split(',')
         return []
 
-    def index_filterspec(self):
+    def index_filterspec(self, filter):
         ''' pull the index filter spec from the form
 
         Links and multilinks want to be lists - the rest are straight
@@ -142,6 +195,7 @@ class Client:
         for key in self.form.keys():
             if key[0] == ':': continue
             if not props.has_key(key): continue
+            if key not in filter: continue
             prop = props[key]
             value = self.form[key]
             if (isinstance(prop, hyperdb.Link) or
@@ -157,33 +211,56 @@ class Client:
                 filterspec[key] = value.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
+
     default_index_sort = ['-activity']
     default_index_group = ['priority']
-    default_index_filter = []
+    default_index_filter = ['status']
     default_index_columns = ['id','activity','title','status','assignedto']
     default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
     def index(self):
         ''' put up an index
         '''
         self.classname = 'issue'
-        if self.form.has_key(':sort'): sort = self.index_arg(':sort')
-        else: sort = self.default_index_sort
-        if self.form.has_key(':group'): group = self.index_arg(':group')
-        else: group = self.default_index_group
-        if self.form.has_key(':filter'): filter = self.index_arg(':filter')
-        else: filter = self.default_index_filter
-        if self.form.has_key(':columns'): columns = self.index_arg(':columns')
-        else: columns = self.default_index_columns
-        filterspec = self.index_filterspec()
-        if not filterspec:
+        # see if the web has supplied us with any customisation info
+        defaults = 1
+        for key in ':sort', ':group', ':filter', ':columns':
+            if self.form.has_key(key):
+                defaults = 0
+                break
+        if defaults:
+            # no info supplied - use the defaults
+            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
+        else:
+            sort = self.index_arg(':sort')
+            group = self.index_arg(':group')
+            filter = self.index_arg(':filter')
+            columns = self.index_arg(':columns')
+            filterspec = self.index_filterspec(filter)
         return self.list(columns=columns, filter=filter, group=group,
             sort=sort, filterspec=filterspec)
 
     # 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):
+            filterspec=None, show_customization=None):
         ''' call the template index with the args
 
             :sort    - sort by prop name, optionally preceeded with '-'
@@ -202,10 +279,13 @@ class Client:
         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()
+        if filterspec is None: filterspec = self.index_filterspec(filter)
+        if show_customization is None:
+            show_customization = self.customization_widget()
 
-        htmltemplate.index(self, self.TEMPLATES, self.db, cn, filterspec,
-            filter, columns, sort, group)
+        index = htmltemplate.IndexTemplate(self, self.TEMPLATES, cn)
+        index.render(filterspec, filter, columns, sort, group,
+            show_customization=show_customization)
         self.pagefoot()
 
     def shownode(self, message=None):
@@ -219,7 +299,8 @@ class Client:
         num_re = re.compile('^\d+$')
         if keys:
             try:
-                props, changed = parsePropsFromForm(cl, self.form, self.nodeid)
+                props, changed = parsePropsFromForm(self.db, cl, self.form,
+                    self.nodeid)
                 cl.set(self.nodeid, **props)
                 self._post_editnode(self.nodeid, changed)
                 # and some nice feedback for the user
@@ -238,19 +319,66 @@ class Client:
         nodeid = self.nodeid
 
         # use the template to display the item
-        htmltemplate.item(self, self.TEMPLATES, self.db, self.classname, nodeid)
+        item = htmltemplate.ItemTemplate(self, self.TEMPLATES, self.classname)
+        item.render(nodeid)
+
         self.pagefoot()
     showissue = shownode
     showmsg = shownode
 
     def showuser(self, message=None):
-        ''' display an item
+        '''Display a user page for editing. Make sure the user is allowed
+            to edit this node, and also check for password changes.
         '''
-        if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
-            self.shownode(message)
-        else:
+        if self.user == 'anonymous':
+            raise Unauthorised
+
+        user = self.db.user
+
+        # get the username of the node being edited
+        node_user = user.get(self.nodeid, 'username')
+
+        if self.user not in ('admin', node_user):
             raise Unauthorised
 
+        #
+        # perform any editing
+        #
+        keys = self.form.keys()
+        num_re = re.compile('^\d+$')
+        if keys:
+            try:
+                props, changed = parsePropsFromForm(self.db, user, self.form,
+                    self.nodeid)
+                if self.nodeid == self.getuid() and 'password' in changed:
+                    set_cookie = self.form['password'].value.strip()
+                else:
+                    set_cookie = 0
+                user.set(self.nodeid, **props)
+                self._post_editnode(self.nodeid, changed)
+                # and some feedback for the user
+                message = '%s edited ok'%', '.join(changed)
+            except:
+                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: %s'%node_user, message)
+
+        # use the template to display the item
+        item = htmltemplate.ItemTemplate(self, self.TEMPLATES, 'user')
+        item.render(self.nodeid)
+        self.pagefoot()
+
     def showfile(self):
         ''' display a file
         '''
@@ -266,7 +394,7 @@ class Client:
         ''' create a node based on the contents of the form
         '''
         cl = self.db.classes[self.classname]
-        props, dummy = parsePropsFromForm(cl, self.form)
+        props, dummy = parsePropsFromForm(self.db, cl, self.form)
         return cl.create(**props)
 
     def _post_editnode(self, nid, changes=None):
@@ -395,8 +523,12 @@ class Client:
                 traceback.print_exc(None, s)
                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
         self.pagehead('New %s'%self.classname.capitalize(), message)
-        htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
-            self.form)
+
+        # call the template
+        newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
+            self.classname)
+        newitem.render(self.form)
+
         self.pagefoot()
     newissue = newnode
     newuser = newnode
@@ -428,8 +560,9 @@ class Client:
                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
 
         self.pagehead('New %s'%self.classname.capitalize(), message)
-        htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname,
-            self.form)
+        newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
+            self.classname)
+        newitem.render(self.form)
         self.pagefoot()
 
     def classes(self, message=None):
@@ -453,7 +586,7 @@ class Client:
         else:
             raise Unauthorised
 
-    def login(self, message=None):
+    def login(self, message=None, newuser_form=None):
         self.pagehead('Login to roundup', message)
         self.write('''
 <table>
@@ -466,34 +599,50 @@ class Client:
 <tr><td></td>
     <td><input type="submit" value="Log In"></td></tr>
 </form>
-
+''')
+        if self.user is None and self.ANONYMOUS_REGISTER == 'deny':
+            self.write('</table>')
+            self.pagefoot()
+            return
+        values = {'realname': '', 'organisation': '', 'address': '',
+            'phone': '', 'username': '', 'password': '', 'confirm': ''}
+        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 action="newuser_action" method=POST>
 <tr><td align=right><em>Name: </em></td>
-    <td><input name="__newuser_realname"></td></tr>
+    <td><input name="realname" value="%(realname)s"></td></tr>
 <tr><td align=right><em>Organisation: </em></td>
-    <td><input name="__newuser_organisation"></td></tr>
+    <td><input name="organisation" value="%(organisation)s"></td></tr>
 <tr><td align=right>E-Mail Address: </td>
-    <td><input name="__newuser_address"></td></tr>
+    <td><input name="address" value="%(address)s"></td></tr>
 <tr><td align=right><em>Phone: </em></td>
-    <td><input name="__newuser_phone"></td></tr>
+    <td><input name="phone" value="%(phone)s"></td></tr>
 <tr><td align=right>Preferred Login name: </td>
-    <td><input name="__newuser_username"></td></tr>
+    <td><input name="username" value="%(username)s"></td></tr>
 <tr><td align=right>Password: </td>
-    <td><input type="password" name="__newuser_password"></td></tr>
+    <td><input type="password" name="password" value="%(password)s"></td></tr>
 <tr><td align=right>Password Again: </td>
-    <td><input type="password" name="__newuser_confirm"></td></tr>
+    <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):
+        if not self.form.has_key('__login_name'):
+            return self.login(message='Username required')
         self.user = self.form['__login_name'].value
-        password = self.form['__login_password'].value
+        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)
@@ -504,22 +653,26 @@ class Client:
 
         # and that the password is correct
         pw = self.db.user.get(uid, 'password')
-        print password, pw, `pw`
         if password != self.db.user.get(uid, 'password'):
             self.make_user_anonymous()
             return self.login(message='Incorrect password')
 
-        # construct the cookie
-        uid = self.db.user.lookup(self.user)
-        user = base64.encodestring('%s:%s'%(self.user, password))[:-1]
-        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
-            ''))
-        cookie = Cookie.SmartCookie()
-        cookie['roundup_user'] = user
-        cookie['roundup_user']['path'] = path
-        self.header({'Set-Cookie': str(cookie)})
+        self.set_cookie(self.user, password)
         return self.index()
 
+    def set_cookie(self, user, password):
+        # construct the cookie
+        user = binascii.b2a_base64('%s:%s'%(user, password)).strip()
+        if user[-1] == '=':
+          if user[-2] == '=':
+            user = user[:-2]
+          else:
+            user = user[:-1]
+        expire = Cookie._getdate(86400*365)
+        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME']))
+        self.header({'Set-Cookie': 'roundup_user=%s; expires=%s; Path=%s;' % (
+            user, expire, path)})
+
     def make_user_anonymous(self):
         # make us anonymous if we can
         try:
@@ -531,35 +684,31 @@ class Client:
     def logout(self, message=None):
         self.make_user_anonymous()
         # construct the logout cookie
-        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
-            ''))
-        cookie = Cookie.SmartCookie()
-        cookie['roundup_user'] = 'deleted'
-        cookie['roundup_user']['path'] = path
-        cookie['roundup_user']['expires'] = 0
-        cookie['roundup_user']['max-age'] = 0
-        self.header({'Set-Cookie': str(cookie)})
-        return self.index()
+        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)})
+        return self.login()
 
     def newuser_action(self, message=None):
         ''' create a new user based on the contents of the form and then
         set the cookie
         '''
+        # re-open the database as "admin"
+        self.db.close()
+        self.db = self.instance.open('admin')
+
         # TODO: pre-check the required fields and username key property
-        cl = self.db.classes['user']
-        props, dummy = parsePropsFromForm(cl, self.form)
-        uid = cl.create(**props)
-        self.user = self.db.user.get(uid, 'username')
-        password = self.db.user.get(uid, 'password')
-        # construct the cookie
-        uid = self.db.user.lookup(self.user)
-        user = base64.encodestring('%s:%s'%(self.user, password))[:-1]
-        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
-            ''))
-        cookie = Cookie.SmartCookie()
-        cookie['roundup_user'] = user
-        cookie['roundup_user']['path'] = path
-        self.header({'Set-Cookie': str(cookie)})
+        cl = self.db.user
+        try:
+            props, dummy = parsePropsFromForm(self.db, cl, self.form)
+            uid = cl.create(**props)
+        except ValueError, message:
+            return self.login(message, newuser_form=self.form)
+        self.user = cl.get(uid, 'username')
+        password = cl.get(uid, 'password')
+        self.set_cookie(self.user, self.form['password'].value)
         return self.index()
 
     def main(self, dre=re.compile(r'([^\d]+)(\d+)'),
@@ -572,7 +721,14 @@ class Client:
         if (cookie.has_key('roundup_user') and
                 cookie['roundup_user'].value != 'deleted'):
             cookie = cookie['roundup_user'].value
-            user, password = base64.decodestring(cookie).split(':')
+            if len(cookie)%4:
+              cookie = cookie + '='*(4-len(cookie)%4)
+            try:
+                user, password = binascii.a2b_base64(cookie).split(':')
+            except (TypeError, binascii.Error, binascii.Incomplete):
+                # damaged cookie!
+                user, password = 'anonymous', ''
+
             # make sure the user exists
             try:
                 uid = self.db.user.lookup(user)
@@ -595,61 +751,149 @@ class Client:
         # now figure which function to call
         path = self.split_path
         if not path or path[0] in ('', 'index'):
-            self.index()
-        elif len(path) == 1:
-            if path[0] == 'list_classes':
-                self.classes()
-                return
-            if path[0] == 'login':
-                self.login()
-                return
-            if path[0] == 'login_action':
-                self.login_action()
-                return
-            if path[0] == 'newuser_action':
-                self.newuser_action()
-                return
-            if path[0] == 'logout':
-                self.logout()
-                return
-            m = dre.match(path[0])
-            if m:
-                self.classname = m.group(1)
-                self.nodeid = m.group(2)
-                try:
-                    cl = self.db.classes[self.classname]
-                except KeyError:
-                    raise NotFound
-                try:
-                    cl.get(self.nodeid, 'id')
-                except IndexError:
-                    raise NotFound
-                try:
-                    getattr(self, 'show%s'%self.classname)()
-                except AttributeError:
-                    raise NotFound
-                return
-            m = nre.match(path[0])
-            if m:
-                self.classname = m.group(1)
-                try:
-                    getattr(self, 'new%s'%self.classname)()
-                except AttributeError:
-                    raise NotFound
-                return
-            self.classname = path[0]
+            action = 'index'
+        else:
+            action = path[0]
+
+        # Everthing ignores path[1:]
+        #  - The file download link generator actually relies on this - it
+        #    appends the name of the file to the URL so the download file name
+        #    is correct, but doesn't actually use it.
+
+        # everyone is allowed to try to log in
+        if action == 'login_action':
+            return self.login_action()
+
+        # allow anonymous people to register
+        if action == 'newuser_action':
+            # if we don't have a login and anonymous people aren't allowed to
+            # register, then spit up the login form
+            if self.ANONYMOUS_REGISTER == 'deny' and self.user is None:
+                return self.login()
+            return self.newuser_action()
+
+        # make sure totally anonymous access is OK
+        if self.ANONYMOUS_ACCESS == 'deny' and self.user is None:
+            return self.login()
+
+        # here be the "normal" functionality
+        if action == 'index':
+            return self.index()
+        if action == 'list_classes':
+            return self.classes()
+        if action == 'login':
+            return self.login()
+        if action == 'logout':
+            return self.logout()
+        m = dre.match(action)
+        if m:
+            self.classname = m.group(1)
+            self.nodeid = m.group(2)
             try:
-                self.db.getclass(self.classname)
+                cl = self.db.classes[self.classname]
             except KeyError:
                 raise NotFound
-            self.list()
-        else:
-            raise 'ValueError', 'Path not understood'
+            try:
+                cl.get(self.nodeid, 'id')
+            except IndexError:
+                raise NotFound
+            try:
+                func = getattr(self, 'show%s'%self.classname)
+            except AttributeError:
+                raise NotFound
+            return func()
+        m = nre.match(action)
+        if m:
+            self.classname = m.group(1)
+            try:
+                func = getattr(self, 'new%s'%self.classname)
+            except AttributeError:
+                raise NotFound
+            return func()
+        self.classname = action
+        try:
+            self.db.getclass(self.classname)
+        except KeyError:
+            raise NotFound
+        self.list()
 
     def __del__(self):
         self.db.close()
 
-def parsePropsFromForm(cl, form, nodeid=0):
+
+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
+
+    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']}
+
+    def pagehead(self, title, message=None):
+        url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
+        machine = self.env['SERVER_NAME']
+        port = self.env['SERVER_PORT']
+        if port != '80': machine = machine + ':' + port
+        base = urlparse.urlunparse(('http', machine, url, None, None, None))
+        if message is not None:
+            message = '<div class="system-msg">%s</div>'%message
+        else:
+            message = ''
+        style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
+        user_name = self.user or ''
+        if self.user == 'admin':
+            admin_links = ' | <a href="list_classes">Class List</a>'
+        else:
+            admin_links = ''
+        if self.user not in (None, 'anonymous'):
+            userid = self.db.user.lookup(self.user)
+            user_info = '''
+<a href="issue?assignedto=%s&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:filter=status,assignedto&:sort=activity&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">My Issues</a> |
+<a href="support?assignedto=%s&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:filter=status,assignedto&:sort=activity&:columns=id,activity,status,title,assignedto&:group=customername&show_customization=1">My Support</a> |
+<a href="user%s">My Details</a> | <a href="logout">Logout</a>
+'''%(userid, userid, userid)
+        else:
+            user_info = '<a href="login">Login</a>'
+        if self.user is not None:
+            add_links = '''
+| Add
+<a href="newissue">Issue</a>,
+<a href="newsupport">Support</a>,
+<a href="newuser">User</a>
+'''
+        else:
+            add_links = ''
+        self.write('''<html><head>
+<title>%s</title>
+<style type="text/css">%s</style>
+</head>
+<body bgcolor=#ffffff>
+%s
+<table width=100%% border=0 cellspacing=0 cellpadding=2>
+<tr class="location-bar"><td><big><strong>%s</strong></big></td>
+<td align=right valign=bottom>%s</td></tr>
+<tr class="location-bar">
+<td align=left>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>,
+<a href="support?status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:filter=status&:columns=id,activity,status,title,assignedto&:group=customername&show_customization=1">Support</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>,
+<a href="support?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=customername&show_customization=1">Support</a>
+%s
+%s</td>
+<td align=right>%s</td>
+</table>
+'''%(title, style, message, title, user_name, add_links, admin_links,
+    user_info))
+
+def parsePropsFromForm(db, cl, form, nodeid=0):
     '''Pull properties for the given class out of the form.
     '''
     props = {}
@@ -670,14 +914,19 @@ def parsePropsFromForm(cl, form, nodeid=0):
             value = date.Interval(form[key].value.strip())
         elif isinstance(proptype, hyperdb.Link):
             value = form[key].value.strip()
-            # handle key values
-            link = cl.properties[key].classname
-            if not num_re.match(value):
-                try:
-                    value = self.db.classes[link].lookup(value)
-                except:
-                    raise ValueError, 'property "%s": %s not a %s'%(
-                        key, value, link)
+            # see if it's the "no selection" choice
+            if value == '-1':
+                # don't set this property
+                continue
+            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 "%s": %s not a %s'%(
+                            key, value, link)
         elif isinstance(proptype, hyperdb.Multilink):
             value = form[key]
             if type(value) != type([]):
@@ -689,11 +938,11 @@ def parsePropsFromForm(cl, form, nodeid=0):
             for entry in map(str, value):
                 if not num_re.match(entry):
                     try:
-                        entry = self.db.classes[link].lookup(entry)
-                    except:
+                        entry = db.classes[link].lookup(entry)
+                    except KeyError:
                         raise ValueError, \
-                            'property "%s": %s not a %s'%(key,
-                            entry, link)
+                            'property "%s": "%s" not an entry of %s'%(key,
+                            entry, link.capitalize())
                 l.append(entry)
             l.sort()
             value = l
@@ -706,6 +955,113 @@ def parsePropsFromForm(cl, form, nodeid=0):
 
 #
 # $Log: not supported by cvs2svn $
+# 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.
 #