Code

. roundup-admin create now prompts for property info if none is supplied
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 5 Oct 2001 02:23:24 +0000 (02:23 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 5 Oct 2001 02:23:24 +0000 (02:23 +0000)
   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.

git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@272 57a73879-2fb5-44c3-a270-3262357dd7e2

CHANGES.txt
cgi-bin/roundup.cgi
roundup-admin
roundup-server
roundup/cgi_client.py
roundup/hyperdb.py
roundup/mailgw.py
roundup/templates/extended/htmlbase.py
roundup/templates/extended/interfaces.py

index 49159f5813b765cee0b25f1a667fc338c22f07c9..29abac0b058a739a39f0734f9168617830fda747 100644 (file)
@@ -2,11 +2,34 @@ This file contains the changes to the Roundup system over time. The entries
 are given with the most recent entry first.
 
 2001-??-?? - 0.2.9
+Feature:
+ . 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.
+ . CGI interfaces now generate a top-level index of their known instances.
+
+Changed:
+ . 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.
+
 Fixed:
+ . Incorrectly had a Bizar Software copyright on the cgitb.py module from
+   Ping - has been removed.
  . Pretty time interval wasn't handling > 1 month properly.
  . Generation of links to Link/Multilink in indexes. (thanks Hubert Hoegl)
  . AssignedTo wasn't in the "classic" schema's item page.
-
+ . 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.
+ . The plain() template function now html-escapes the content.
 
 2001-08-30 - 0.2.8
 Fixed:
index 709582583d64d06667d6133b4c24227253733ede..5a5f72847d0f4b3916e06d83c563c919a96b810e 100755 (executable)
@@ -16,7 +16,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: roundup.cgi,v 1.12 2001-10-01 05:55:41 richard Exp $
+# $Id: roundup.cgi,v 1.13 2001-10-05 02:23:24 richard Exp $
 
 # python version check
 import sys
@@ -59,73 +59,46 @@ except:
     traceback.print_exc(None, s)
     print cgi.escape(s.getvalue()), "</pre>"
 
-def main(instance, out):
-    from roundup import cgi_client
-    db = instance.open('admin')
-    auth = os.environ.get("HTTP_CGI_AUTHORIZATION", None)
-    message = 'Unauthorised'
-    if auth:
-        import binascii
-        l = binascii.a2b_base64(auth.split(' ')[1]).split(':')
-        user = l[0]
-        password = None
-        if len(l) > 1:
-            password = l[1]
-        try:
-            uid = db.user.lookup(user)
-        except KeyError:
-            auth = None
-            message = 'Username not recognised'
-        else:
-            if password != db.user.get(uid, 'password'):
-                message = 'Incorrect password'
-                auth = None
-    if not auth:
-        out.write('Content-Type: text/html\n')
-        out.write('Status: 401\n')
-        out.write('WWW-Authenticate: basic realm="Roundup"\n\n')
-        keys = os.environ.keys()
-        keys.sort()
-        out.write(message)
-        return
-    client = instance.Client(out, db, os.environ, user)
-    try:
-        client.main()
-    except cgi_client.Unauthorised:
-        out.write('Content-Type: text/html\n')
-        out.write('Status: 403\n\n')
-        out.write('Unauthorised')
-
-def index(out):
-    ''' Print up an index of the available instances
-    '''
-    import urllib
-    w = out.write
-    w("Content-Type: text/html\n\n")
-    w('<html><head><title>Roundup instances index</title><head>\n')
-    w('<body><h1>Roundup instances index</h1><ol>\n')
-    for instance in ROUNDUP_INSTANCE_HOMES.keys():
-        w('<li><a href="%s/index">%s</a>\n'%(urllib.quote(instance),
-            instance))
-    w('</ol></body></html>')
-
-#
-# Now do the actual CGI handling
-# 
-out, err = sys.stdout, sys.stderr
-try:
-    sys.stdout = sys.stderr = LOG
+def main(out, err):
     import os, string
     import roundup.instance
     path = string.split(os.environ['PATH_INFO'], '/')
     instance = path[1]
+    os.environ['INSTANCE_NAME'] = instance
     os.environ['PATH_INFO'] = string.join(path[2:], '/')
     if ROUNDUP_INSTANCE_HOMES.has_key(instance):
         instance_home = ROUNDUP_INSTANCE_HOMES[instance]
         instance = roundup.instance.open(instance_home)
-        main(instance, out)
+        from roundup import cgi_client
+        client = instance.Client(instance, out, os.environ)
+        try:
+            client.main()
+        except cgi_client.Unauthorised:
+            out.write('Content-Type: text/html\n')
+            out.write('Status: 403\n\n')
+            out.write('Unauthorised')
+        except cgi_client.NotFound:
+            out.write('Content-Type: text/html\n')
+            out.write('Status: 404\n\n')
+            out.write('Not found: %s'%client.path)
     else:
-        index(out)
+        import urllib
+        w = out.write
+        w("Content-Type: text/html\n\n")
+        w('<html><head><title>Roundup instances index</title><head>\n')
+        w('<body><h1>Roundup instances index</h1><ol>\n')
+        for instance in ROUNDUP_INSTANCE_HOMES.keys():
+            w('<li><a href="%s/index">%s</a>\n'%(urllib.quote(instance),
+                instance))
+        w('</ol></body></html>')
+
+#
+# Now do the actual CGI handling
+# 
+out, err = sys.stdout, sys.stderr
+try:
+    sys.stdout = sys.stderr = LOG
+    main(out, err)
 except:
     sys.stdout, sys.stderr = out, err
     out.write('Content-Type: text/html\n\n')
@@ -135,6 +108,9 @@ sys.stdout, sys.stderr = out, err
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.12  2001/10/01 05:55:41  richard
+# Fixes to the top-level index
+#
 # Revision 1.11  2001/09/29 13:27:00  richard
 # CGI interfaces now spit up a top-level index of all the instances they can
 # serve.
index e8082a421ecd5008fcb92d2c926d9438b9256f32..86bc4ae7441976703c121d78f222104a6edf2e93 100755 (executable)
@@ -16,7 +16,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: roundup-admin,v 1.20 2001-10-04 02:12:42 richard Exp $
+# $Id: roundup-admin,v 1.21 2001-10-05 02:23:24 richard Exp $
 
 import sys
 if int(sys.version[0]) < 2:
@@ -119,9 +119,9 @@ def do_init(instance_home, args):
     if template not in templates:
         print 'Templates:', ', '.join(templates)
     while template not in templates:
-        template = raw_input('Select template [classic]: ').strip()
+        template = raw_input('Select template [extended]: ').strip()
         if not template:
-            template = 'classic'
+            template = 'extended'
 
     import roundup.backends
     backends = roundup.backends.__all__
@@ -449,6 +449,14 @@ if __name__ == '__main__':
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.20  2001/10/04 02:12:42  richard
+# Added nicer command-line item adding: passing no arguments will enter an
+# interactive more which asks for each property in turn. While I was at it, I
+# fixed an implementation problem WRT the spec - I wasn't raising a
+# ValueError if the key property was missing from a create(). Also added a
+# protected=boolean argument to getprops() so we can list only the mutable
+# properties (defaults to yes, which lists the immutables).
+#
 # Revision 1.19  2001/10/01 06:40:43  richard
 # made do_get have the args in the correct order
 #
index fb905d17c58298a2ca484ba1f616275e2f90e442..3c6c9e53fd66a94768824f45eeeb941795c07ac6 100755 (executable)
@@ -20,7 +20,7 @@
 
 Based on CGIHTTPServer in the Python library.
 
-$Id: roundup-server,v 1.12 2001-09-29 13:27:00 richard Exp $
+$Id: roundup-server,v 1.13 2001-10-05 02:23:24 richard Exp $
 
 """
 import sys
@@ -75,10 +75,12 @@ class RoundupRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
         sys.stdin = self.rfile
         try:
             self.inner_run_cgi()
+        except cgi_client.NotFound:
+            self.send_error(404, self.path)
         except cgi_client.Unauthorised:
             self.wfile.write('Content-Type: text/html\n')
-            self.wfile.write('Status: 403\n')
-            self.wfile.write('Unauthorised')
+            self.wfile.write('Status: 403\n\n')
+            self.wfile.write('You are not authorised to access this URL.')
         except:
             try:
                 reload(cgitb)
@@ -121,12 +123,12 @@ class RoundupRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
         if rest == '/':
             return self.index()
         l_path = string.split(rest, '/')
-        instance = urllib.unquote(l_path[1])
-        if self.ROUNDUP_INSTANCE_HOMES.has_key(instance):
-            instance_home = self.ROUNDUP_INSTANCE_HOMES[instance]
+        instance_name = urllib.unquote(l_path[1])
+        if self.ROUNDUP_INSTANCE_HOMES.has_key(instance_name):
+            instance_home = self.ROUNDUP_INSTANCE_HOMES[instance_name]
             instance = roundup.instance.open(instance_home)
         else:
-            return self.index()
+            raise cgi_client.NotFound
 
         # figure out what the rest of the path is
         if len(l_path) > 2:
@@ -136,6 +138,7 @@ class RoundupRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
 
         # Set up the CGI environment
         env = {}
+        env['INSTANCE_NAME'] = instance_name
         env['REQUEST_METHOD'] = self.command
         env['PATH_INFO'] = urllib.unquote(rest)
         if query:
@@ -176,41 +179,12 @@ class RoundupRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
         #finally:
         #    del sys.path[0]
 
-        # initialise the roundupdb, check for auth
-        db = instance.open('admin')
-        message = 'Unauthorised'
-        auth = self.headers.getheader('authorization')
-        if auth:
-            l = binascii.a2b_base64(auth.split(' ')[1]).split(':')
-            user = l[0]
-            password = None
-            if len(l) > 1:
-                password = l[1]
-            try:
-                uid = db.user.lookup(user)
-            except KeyError:
-                auth = None
-                message = 'Username not recognised'
-            else:
-                if password != db.user.get(uid, 'password'):
-                    message = 'Incorrect password'
-                    auth = None
-        db.close()
-        del db
-        if not auth:
-            self.send_response(401)
-            self.send_header('Content-Type', 'text/html')
-            self.send_header('WWW-Authenticate', 'basic realm="Roundup"')
-            self.end_headers()
-            self.wfile.write(message)
-            return
-
         self.send_response(200, "Script output follows")
 
         # do the roundup thang
-        db = instance.open(user)
-        client = instance.Client(self.wfile, db, env, user)
+        client = instance.Client(instance, self.wfile, env)
         client.main()
+
     do_POST = run_cgi
 
 nobody = None
@@ -282,6 +256,10 @@ if __name__ == '__main__':
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.12  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.11  2001/08/07 00:24:42  richard
 # stupid typo
 #
index 1cb3113307c71a1dfc8ddd60af2eb571941bbe54..33d2381fcc9f64840f1abee93563bdc19f3550a7 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: cgi_client.py,v 1.26 2001-09-12 08:31:42 richard Exp $
+# $Id: cgi_client.py,v 1.27 2001-10-05 02:23:24 richard Exp $
 
 import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
+import base64, Cookie, time
 
 import roundupdb, htmltemplate, date, hyperdb
 
 class Unauthorised(ValueError):
     pass
 
+class NotFound(ValueError):
+    pass
+
 class Client:
-    def __init__(self, out, db, env, user):
+    '''
+
+    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.
+    '''
+
+    def __init__(self, instance, out, env):
+        self.instance = instance
         self.out = out
-        self.db = db
         self.env = env
-        self.user = user
         self.path = env['PATH_INFO']
         self.split_path = self.path.split('/')
 
@@ -60,7 +76,11 @@ class Client:
         else:
             message = ''
         style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
-        userid = self.db.user.lookup(self.user)
+        if self.user is not None:
+            userid = self.db.user.lookup(self.user)
+            user_info = '(login: <a href="user%s">%s</a>)'%(userid, self.user)
+        else:
+            user_info = ''
         self.write('''<html><head>
 <title>%s</title>
 <style type="text/css">%s</style>
@@ -68,10 +88,9 @@ 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>
-(login: <a href="user%s">%s</a>)</td></tr>
+<tr class="location-bar"><td><big><strong>%s</strong></big> %s</td></tr>
 </table>
-'''%(title, style, message, title, userid, self.user))
+'''%(title, style, message, title, user_info))
 
     def pagefoot(self):
         if self.debug:
@@ -122,6 +141,7 @@ class Client:
         filterspec = {}
         for key in self.form.keys():
             if key[0] == ':': continue
+            if not props.has_key(key): continue
             prop = props[key]
             value = self.form[key]
             if (isinstance(prop, hyperdb.Link) or
@@ -433,7 +453,143 @@ class Client:
         else:
             raise Unauthorised
 
-    def main(self, dre=re.compile(r'([^\d]+)(\d+)'), nre=re.compile(r'new(\w+)')):
+    def login(self, message=None):
+        self.pagehead('Login to roundup', message)
+        self.write('''
+<table>
+<tr><td colspan=2 class="strong-header">Existing User Login</td></tr>
+<form action="login_action" method=POST>
+<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>
+
+<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>
+<tr><td align=right><em>Organisation: </em></td>
+    <td><input name="__newuser_organisation"></td></tr>
+<tr><td align=right>E-Mail Address: </td>
+    <td><input name="__newuser_address"></td></tr>
+<tr><td align=right><em>Phone: </em></td>
+    <td><input name="__newuser_phone"></td></tr>
+<tr><td align=right>Preferred Login name: </td>
+    <td><input name="__newuser_username"></td></tr>
+<tr><td align=right>Password: </td>
+    <td><input type="password" name="__newuser_password"></td></tr>
+<tr><td align=right>Password Again: </td>
+    <td><input type="password" name="__newuser_confirm"></td></tr>
+<tr><td></td>
+    <td><input type="submit" value="Register"></td></tr>
+</form>
+</table>
+''')
+
+    def login_action(self, message=None):
+        self.user = self.form['__login_name'].value
+        password = self.form['__login_password'].value
+        # make sure the user exists
+        try:
+            uid = self.db.user.lookup(self.user)
+        except KeyError:
+            name = self.user
+            self.make_user_anonymous()
+            return self.login(message='No such user "%s"'%name)
+
+        # and that the password is correct
+        if password != self.db.user.get(uid, 'password'):
+            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)})
+        return self.index()
+
+    def make_user_anonymous(self):
+        # make us anonymous if we can
+        try:
+            self.db.user.lookup('anonymous')
+            self.user = 'anonymous'
+        except KeyError:
+            self.user = None
+
+    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()
+
+    def newuser_action(self, message=None):
+        ''' create a new user based on the contents of the form and then
+        set the cookie
+        '''
+        # 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)})
+        return self.index()
+
+    def main(self, dre=re.compile(r'([^\d]+)(\d+)'),
+            nre=re.compile(r'new(\w+)')):
+
+        # determine the uid to use
+        self.db = self.instance.open('admin')
+        cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
+        user = 'anonymous'
+        if (cookie.has_key('roundup_user') and
+                cookie['roundup_user'].value != 'deleted'):
+            cookie = cookie['roundup_user'].value
+            user, password = base64.decodestring(cookie).split(':')
+            # make sure the user exists
+            try:
+                uid = self.db.user.lookup(user)
+                # now validate the password
+                if password != self.db.user.get(uid, 'password'):
+                    user = 'anonymous'
+            except KeyError:
+                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
+        self.db.close()
+
+        # re-open the database for real, using the user
+        self.db = self.instance.open(self.user)
+
+        # now figure which function to call
         path = self.split_path
         if not path or path[0] in ('', 'index'):
             self.index()
@@ -441,18 +597,48 @@ class Client:
             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)
-                getattr(self, 'show%s'%self.classname)()
+                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)
-                getattr(self, 'new%s'%self.classname)()
+                try:
+                    getattr(self, 'new%s'%self.classname)()
+                except AttributeError:
+                    raise NotFound
                 return
             self.classname = path[0]
+            try:
+                self.db.getclass(self.classname)
+            except KeyError:
+                raise NotFound
             self.list()
         else:
             raise 'ValueError', 'Path not understood'
@@ -515,6 +701,9 @@ def parsePropsFromForm(cl, form, nodeid=0):
 
 #
 # $Log: not supported by cvs2svn $
+# 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.
 #
index 367abac6dd8847da3c0bfc9380bd4b042177b88a..32729f19d2e9d91fc662c7f6bd3ffddbcc2a7176 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: hyperdb.py,v 1.20 2001-10-04 02:12:42 richard Exp $
+# $Id: hyperdb.py,v 1.21 2001-10-05 02:23:24 richard Exp $
 
 # standard python modules
 import cPickle, re, string
@@ -219,9 +219,9 @@ class Class:
         IndexError is raised.  'propname' must be the name of a property
         of this class or a KeyError is raised.
         """
+        d = self.db.getnode(self.classname, nodeid)
         if propname == 'id':
             return nodeid
-        d = self.db.getnode(self.classname, nodeid)
         if not d.has_key(propname) and default is not _marker:
             return default
         return d[propname]
@@ -800,6 +800,14 @@ def Choice(name, *options):
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.20  2001/10/04 02:12:42  richard
+# Added nicer command-line item adding: passing no arguments will enter an
+# interactive more which asks for each property in turn. While I was at it, I
+# fixed an implementation problem WRT the spec - I wasn't raising a
+# ValueError if the key property was missing from a create(). Also added a
+# protected=boolean argument to getprops() so we can list only the mutable
+# properties (defaults to yes, which lists the immutables).
+#
 # Revision 1.19  2001/08/29 04:47:18  richard
 # Fixed CGI client change messages so they actually include the properties
 # changed (again).
index 19151b2108934e9c711511fe468dcfcd023e0a7c..9ce05937f54bc4c881116f90f5650421715247de 100644 (file)
@@ -72,7 +72,7 @@ are calling the create() method to create a new node). If an auditor raises
 an exception, the original message is bounced back to the sender with the
 explanatory message given in the exception. 
 
-$Id: mailgw.py,v 1.15 2001-08-30 06:01:17 richard Exp $
+$Id: mailgw.py,v 1.16 2001-10-05 02:23:24 richard Exp $
 '''
 
 
@@ -230,7 +230,9 @@ Subject was: "%s"
                 elif isinstance(type, hyperdb.Multilink):
                     props[key] = value.split(',')
 
+        #
         # handle the users
+        #
         author = self.db.uidFromAddress(message.getaddrlist('from')[0])
         recipients = []
         for recipient in message.getaddrlist('to') + message.getaddrlist('cc'):
@@ -398,6 +400,9 @@ def parseContent(content, blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'),
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.15  2001/08/30 06:01:17  richard
+# Fixed missing import in mailgw :(
+#
 # Revision 1.14  2001/08/13 23:02:54  richard
 # Make the mail parser a little more robust.
 #
index bee173eb01d0925a32fb2f4e7c6c10611dfe6c9f..f32f5b217532d5e4e7703d389c3fef4c0d6286aa 100644 (file)
@@ -182,7 +182,7 @@ issueDOTitem = """<!-- dollarId: issue.item,v 1.5 2001/07/30 08:03:56 richard Ex
 
 """
 
-msgDOTindex = """<!-- dollarId: msg.index,v 1.1 2001/07/23 04:21:20 richard Exp dollar-->
+msgDOTindex = """<!-- dollarId: msg.index,v 1.3 2001/09/27 06:45:58 richard Exp dollar-->
 <tr class="row-hilite">
     <property name="date">
         <td><display call="link('date')"></td>
index b3db3c7f12438b0ab21fc8477e0a8866a2ed5fe1..77fd54fe193bb7cf7697f588324d5cce081cb46e 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: interfaces.py,v 1.9 2001-08-07 00:24:43 richard Exp $
+# $Id: interfaces.py,v 1.10 2001-10-05 02:23:24 richard Exp $
 
 import instance_config, urlparse, os
 from roundup import cgi_client, mailgw 
@@ -47,11 +47,29 @@ class Client(cgi_client.Client):
         else:
             message = ''
         style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
-        userid = self.db.user.lookup(self.user)
+        user_name = self.user or ''
         if self.user == 'admin':
-            extras = ' | <a href="list_classes">Class List</a>'
+            admin_links = ' | <a href="list_classes">Class List</a>'
         else:
-            extras = ''
+            admin_links = ''
+        if self.user not in (None, 'anonymous'):
+            userid = self.db.user.lookup(self.user)
+            user_info = '''
+<a href="issue?assignedto=%s&status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=priority">My Issues</a> |
+<a href="support?assignedto=%s&status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=customername">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>
@@ -68,17 +86,12 @@ class Client(cgi_client.Client):
 | Unassigned
 <a href="issue?assignedto=admin&status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=priority">Issues</a>,
 <a href="support?assignedto=admin&status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=customername">Support</a>
-| Add
-<a href="newissue">Issue</a>,
-<a href="newsupport">Support</a>,
-<a href="newuser">User</a>
+%s
 %s</td>
-<td align=right>
-<a href="issue?assignedto=%s&status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=priority">My Issues</a> |
-<a href="support?assignedto=%s&status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=id,activity,status,title,assignedto&:group=customername">My Support</a> |
-<a href="user%s">My Details</a></td>
+<td align=right>%s</td>
 </table>
-'''%(title, style, message, title, self.user, extras, userid, userid, userid))
+'''%(title, style, message, title, user_name, add_links, admin_links,
+    user_info))
  
 class MailGW(mailgw.MailGW): 
     ''' derives basic mail gateway implementation from the standard module, 
@@ -90,6 +103,9 @@ class MailGW(mailgw.MailGW):
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.9  2001/08/07 00:24:43  richard
+# stupid typo
+#
 # Revision 1.8  2001/08/07 00:15:51  richard
 # Added the copyright/license notice to (nearly) all files at request of
 # Bizar Software.