Code

Initial commit of the Grande Splite
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Sun, 22 Jul 2001 11:11:14 +0000 (11:11 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Sun, 22 Jul 2001 11:11:14 +0000 (11:11 +0000)
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@26 57a73879-2fb5-44c3-a270-3262357dd7e2

16 files changed:
CHANGES
README
cgitb.py [deleted file]
config.py [deleted file]
date.py [deleted file]
hyperdb.py [deleted file]
hyperdb_bsddb.py [deleted file]
roundup-mailgw.py [deleted file]
roundup.cgi [deleted file]
roundup.py [deleted file]
roundup_cgi.py [deleted file]
roundupdb.py [deleted file]
server.py [deleted file]
style.css [deleted file]
template.py [deleted file]
test.py [deleted file]

diff --git a/CHANGES b/CHANGES
index 32eb2a698259cc27a5f40bb9be91535b4ec8e489..1910b0714c0e9fcbe3e35775a9ae3986172d9f58 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -55,3 +55,7 @@
    quoted.
  . Fixed a bug in the hyperdb filter - wrong variable names in the error
    message.
+ . Major reshuffle of code to allow multiple roundup instances and a
+   single, site-packages -based installation. Also allows multiple database
+   back-ends.
+
diff --git a/README b/README
index 37c80f1f263d79c0ab6bb4e7dac93e4ecfd1cd8f..7c4beaef1585763225dc4fc3aec66102ff765dfa 100644 (file)
--- a/README
+++ b/README
@@ -11,6 +11,8 @@ The stylesheet included with this package has been copied from the Zope
 management interface and presumably belongs to Digital Creations.
 
 
+TODO: Instructions need re-writing!!
+
 
 2. Installation
 ===============
@@ -44,24 +46,17 @@ Both need the bsddb module.
 
 2.1 Initial Setup
 -----------------
- 1. Make a directory in /home/httpd/html called 'roundup'.
- 2. Copy the tar file's contents there.
- 3. Edit config.py
- 4. "python roundup.py init" to initialise the database (by default, it
-    goes in a directory called 'db' in the current directory). Choose a
-    sensible admin password.
- 5. "chmod -R a+rw db"
 
 
 2.2 Mail
 --------
 Set up a mail alias called "issue_tracker" as:
-  "|/usr/bin/python /home/httpd/html/roundup/roundup-mailgw.py"
+  "|/usr/bin/python /home/httpd/html/roundup/roundup-mailgw <instance_home>"
 
 In some installations (e.g. RedHat 6.2 I think) you'll need to set up smrsh
 so sendmail will accept the pipe command. In that case, symlink
 /etc/smrsh/python to /usr/bin/python and change the command to:
-  "|python /home/httpd/html/roundup/roundup-mailgw.py"
+  "|python /home/httpd/html/roundup/roundup-mailgw <instance_home>"
 
 
 2.3 Web Interface
diff --git a/cgitb.py b/cgitb.py
deleted file mode 100644 (file)
index 6eb0848..0000000
--- a/cgitb.py
+++ /dev/null
@@ -1,121 +0,0 @@
-# $Id: cgitb.py,v 1.3 2001-07-19 06:27:07 anthonybaxter Exp $
-
-import sys, os, types, string, keyword, linecache, tokenize, inspect, pydoc
-
-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 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.2  2001/07/19 05:52:22  anthonybaxter
-# Added CVS keywords Id and Log to all python files.
-#
-#
diff --git a/config.py b/config.py
deleted file mode 100644 (file)
index 01d807d..0000000
--- a/config.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# $Id: config.py,v 1.6 2001-07-19 10:43:01 anthonybaxter Exp $
-
-ROUNDUP_HOME=MAIL_DOMAIN=MAILHOST=HTTP_HOST=None
-HTTP_PORT=0
-
-try:
-    from localconfig import *
-except ImportError:
-    localconfig = None
-
-import os
-
-# This is the root directory for roundup
-if not ROUNDUP_HOME:
-    ROUNDUP_HOME='/home/httpd/html/roundup'
-
-# The SMTP mail host that roundup will use to send mail
-if not MAILHOST:
-    MAILHOST = 'localhost'
-
-# The domain name used for email addresses.
-if not MAIL_DOMAIN:
-    MAIL_DOMAIN = 'bizarsoftware.com.au'
-
-# the next two are only used for the standalone HTTP server.
-if not HTTP_HOST:
-    HTTP_HOST = ''
-if not HTTP_PORT:
-    HTTP_PORT = 9080
-
-# This is the directory that the database is going to be stored in
-DATABASE = os.path.join(ROUNDUP_HOME, 'db')
-
-# The email address that mail to roundup should go to
-ISSUE_TRACKER_EMAIL = 'issue_tracker@%s'%MAIL_DOMAIN
-
-# The email address that roundup will complain to if it runs into trouble
-ADMIN_EMAIL = 'roundup-admin@%s'%MAIL_DOMAIN
-
-# Somewhere for roundup to log stuff internally sent to stdout or stderr
-LOG = os.path.join(ROUNDUP_HOME, 'roundup.log')
-
-del os
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.5  2001/07/19 06:27:07  anthonybaxter
-# fixing (manually) the (dollarsign)Log(dollarsign) entries caused by
-# my using the magic (dollarsign)Id(dollarsign) and (dollarsign)Log(dollarsign)
-# strings in a commit message. I'm a twonk.
-#
-# Also broke the help string in two.
-#
-# Revision 1.4  2001/07/19 05:52:22  anthonybaxter
-# Added CVS keywords Id and Log to all python files.
-#
-#
-
diff --git a/date.py b/date.py
deleted file mode 100644 (file)
index fc1bc48..0000000
--- a/date.py
+++ /dev/null
@@ -1,351 +0,0 @@
-# $Id: date.py,v 1.3 2001-07-19 06:27:07 anthonybaxter Exp $
-
-import time, re, calendar
-
-class Date:
-    '''
-    As strings, date-and-time stamps are specified with the date in
-    international standard format (yyyy-mm-dd) joined to the time
-    (hh:mm:ss) by a period ("."). Dates in this form can be easily compared
-    and are fairly readable when printed. An example of a valid stamp is
-    "2000-06-24.13:03:59". We'll call this the "full date format". When
-    Timestamp objects are printed as strings, they appear in the full date
-    format with the time always given in GMT. The full date format is
-    always exactly 19 characters long. 
-
-    For user input, some partial forms are also permitted: the whole time
-    or just the seconds may be omitted; and the whole date may be omitted
-    or just the year may be omitted. If the time is given, the time is
-    interpreted in the user's local time zone. The Date constructor takes
-    care of these conversions. In the following examples, suppose that yyyy
-    is the current year, mm is the current month, and dd is the current day
-    of the month; and suppose that the user is on Eastern Standard Time.
-
-      "2000-04-17" means <Date 2000-04-17.00:00:00>
-      "01-25" means <Date yyyy-01-25.00:00:00>
-      "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
-      "08-13.22:13" means <Date yyyy-08-14.03:13:00>
-      "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
-      "14:25" means <Date yyyy-mm-dd.19:25:00>
-      "8:47:11" means <Date yyyy-mm-dd.13:47:11>
-      "." means "right now"
-
-    The Date class should understand simple date expressions of the form
-    stamp + interval and stamp - interval. When adding or subtracting
-    intervals involving months or years, the components are handled
-    separately. For example, when evaluating "2000-06-25 + 1m 10d", we
-    first add one month to get 2000-07-25, then add 10 days to get
-    2000-08-04 (rather than trying to decide whether 1m 10d means 38 or 40
-    or 41 days).
-
-    Example usage:
-        >>> Date(".")
-        <Date 2000-06-26.00:34:02>
-        >>> _.local(-5)
-        "2000-06-25.19:34:02"
-        >>> Date(". + 2d")
-        <Date 2000-06-28.00:34:02>
-        >>> Date("1997-04-17", -5)
-        <Date 1997-04-17.00:00:00>
-        >>> Date("01-25", -5)
-        <Date 2000-01-25.00:00:00>
-        >>> Date("08-13.22:13", -5)
-        <Date 2000-08-14.03:13:00>
-        >>> Date("14:25", -5)
-        <Date 2000-06-25.19:25:00>
-    '''
-    isDate = 1
-
-    def __init__(self, spec='.', offset=0, set=None):
-        """Construct a date given a specification and a time zone offset.
-
-          'spec' is a full date or a partial form, with an optional
-                 added or subtracted interval.
-        'offset' is the local time zone offset from GMT in hours.
-        """
-        if set is None:
-            self.set(spec, offset=offset)
-        else:
-            self.year, self.month, self.day, self.hour, self.minute, \
-                self.second, x, x, x = set
-        self.offset = offset
-
-    def applyInterval(self, interval):
-        ''' Apply the interval to this date
-        '''
-        t = (self.year + interval.year,
-             self.month + interval.month,
-             self.day + interval.day,
-             self.hour + interval.hour,
-             self.minute + interval.minute,
-             self.second + interval.second, 0, 0, 0)
-        self.year, self.month, self.day, self.hour, self.minute, \
-            self.second, x, x, x = time.gmtime(calendar.timegm(t))
-
-    def __add__(self, other):
-        """Add an interval to this date to produce another date."""
-        t = (self.year + other.sign * other.year,
-            self.month + other.sign * other.month,
-            self.day + other.sign * other.day,
-            self.hour + other.sign * other.hour,
-            self.minute + other.sign * other.minute,
-            self.second + other.sign * other.second, 0, 0, 0)
-        return Date(set = time.gmtime(calendar.timegm(t)))
-
-    # XXX deviates from spec to allow subtraction of dates as well
-    def __sub__(self, other):
-        """ Subtract:
-             1. an interval from this date to produce another date.
-             2. a date from this date to produce an interval.
-        """
-        if other.isDate:
-            # TODO this code will fall over laughing if the dates cross
-            # leap years, phases of the moon, ....
-            a = calendar.timegm((self.year, self.month, self.day, self.hour,
-                self.minute, self.second, 0, 0, 0))
-            b = calendar.timegm((other.year, other.month, other.day, other.hour,
-                other.minute, other.second, 0, 0, 0))
-            diff = a - b
-            if diff < 0:
-                sign = -1
-                diff = -diff
-            else:
-                sign = 1
-            S = diff%60
-            M = (diff/60)%60
-            H = (diff/(60*60))%60
-            if H>1: S = 0
-            d = (diff/(24*60*60))%30
-            if d>1: H = S = M = 0
-            m = (diff/(30*24*60*60))%12
-            if m>1: H = S = M = 0
-            y = (diff/(365*24*60*60))
-            if y>1: d = H = S = M = 0
-            return Interval((y, m, d, H, M, S), sign=sign)
-        t = (self.year - other.sign * other.year,
-             self.month - other.sign * other.month,
-             self.day - other.sign * other.day,
-             self.hour - other.sign * other.hour,
-             self.minute - other.sign * other.minute,
-             self.second - other.sign * other.second, 0, 0, 0)
-        return Date(set = time.gmtime(calendar.timegm(t)))
-
-    def __cmp__(self, other):
-        """Compare this date to another date."""
-        for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
-            r = cmp(getattr(self, attr), getattr(other, attr))
-            if r: return r
-        return 0
-
-    def __str__(self):
-        """Return this date as a string in the yyyy-mm-dd.hh:mm:ss format."""
-        return time.strftime('%Y-%m-%d.%T', (self.year, self.month,
-            self.day, self.hour, self.minute, self.second, 0, 0, 0))
-
-    def pretty(self):
-        ''' print up the date date using a pretty format...
-        '''
-        return time.strftime('%e %B %Y', (self.year, self.month,
-            self.day, self.hour, self.minute, self.second, 0, 0, 0))
-
-    def set(self, spec, offset=0, date_re=re.compile(r'''
-              (((?P<y>\d\d\d\d)-)?((?P<m>\d\d)-(?P<d>\d\d))?)? # yyyy-mm-dd
-              (?P<n>\.)?                                       # .
-              (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)?    # hh:mm:ss
-              (?P<o>.+)?                                       # offset
-              ''', re.VERBOSE)):
-        ''' set the date to the value in spec
-        '''
-        m = date_re.match(spec)
-        if not m:
-            raise ValueError, 'Not a date spec: [[yyyy-]mm-dd].[[h]h:mm[:ss]] [offset]'
-        info = m.groupdict()
-
-        # get the current date/time using the offset
-        y,m,d,H,M,S,x,x,x = time.gmtime(time.time())
-        ts = calendar.timegm((y,m,d,H+offset,M,S,0,0,0))
-        self.year, self.month, self.day, self.hour, self.minute, \
-            self.second, x, x, x = time.gmtime(ts)
-
-        if info['m'] is not None and info['d'] is not None:
-            self.month = int(info['m'])
-            self.day = int(info['d'])
-            if info['y'] is not None:
-                self.year = int(info['y'])
-            self.hour = self.minute = self.second = 0
-
-        if info['H'] is not None and info['M'] is not None:
-            self.hour = int(info['H'])
-            self.minute = int(info['M'])
-            if info['S'] is not None:
-                self.second = int(info['S'])
-
-        if info['o']:
-            self.applyInterval(Interval(info['o']))
-
-    def __repr__(self):
-        return '<Date %s>'%self.__str__()
-
-    def local(self, offset):
-        """Return this date as yyyy-mm-dd.hh:mm:ss in a local time zone."""
-        t = (self.year, self.month, self.day, self.hour + offset, self.minute,
-             self.second, 0, 0, 0)
-        self.year, self.month, self.day, self.hour, self.minute, \
-            self.second, x, x, x = time.gmtime(calendar.timegm(t))
-
-
-class Interval:
-    '''
-    Date intervals are specified using the suffixes "y", "m", and "d". The
-    suffix "w" (for "week") means 7 days. Time intervals are specified in
-    hh:mm:ss format (the seconds may be omitted, but the hours and minutes
-    may not).
-
-      "3y" means three years
-      "2y 1m" means two years and one month
-      "1m 25d" means one month and 25 days
-      "2w 3d" means two weeks and three days
-      "1d 2:50" means one day, two hours, and 50 minutes
-      "14:00" means 14 hours
-      "0:04:33" means four minutes and 33 seconds
-
-    Example usage:
-        >>> Interval("  3w  1  d  2:00")
-        <Interval 22d 2:00>
-        >>> Date(". + 2d") - Interval("3w")
-        <Date 2000-06-07.00:34:02>
-    '''
-    isInterval = 1
-
-    def __init__(self, spec, sign=1):
-        """Construct an interval given a specification."""
-        if type(spec) == type(''):
-            self.set(spec)
-        else:
-            self.sign = sign
-            self.year, self.month, self.day, self.hour, self.minute, \
-                self.second = spec
-
-    def __cmp__(self, other):
-        """Compare this interval to another interval."""
-        for attr in ('year', 'month', 'day', 'hour', 'minute', 'second'):
-            r = cmp(getattr(self, attr), getattr(other, attr))
-            if r: return r
-        return 0
-        
-    def __str__(self):
-        """Return this interval as a string."""
-        sign = {1:'+', -1:'-'}[self.sign]
-        l = [sign]
-        if self.year: l.append('%sy'%self.year)
-        if self.month: l.append('%sm'%self.month)
-        if self.day: l.append('%sd'%self.day)
-        if self.second:
-            l.append('%d:%02d:%02d'%(self.hour, self.minute, self.second))
-        elif self.hour or self.minute:
-            l.append('%d:%02d'%(self.hour, self.minute))
-        return ' '.join(l)
-
-    def set(self, spec, interval_re = re.compile('''
-            \s*
-            (?P<s>[-+])?         # + or -
-            \s*
-            ((?P<y>\d+\s*)y)?    # year
-            \s*
-            ((?P<m>\d+\s*)m)?    # month
-            \s*
-            ((?P<w>\d+\s*)w)?    # week
-            \s*
-            ((?P<d>\d+\s*)d)?    # day
-            \s*
-            (((?P<H>\d?\d):(?P<M>\d\d))?(:(?P<S>\d\d))?)?   # time
-            \s*
-            ''', re.VERBOSE)):
-        ''' set the date to the value in spec
-        '''
-        self.year = self.month = self.week = self.day = self.hour = \
-            self.minute = self.second = 0
-        self.sign = 1
-        m = interval_re.match(spec)
-        if not m:
-            raise ValueError, 'Not an interval spec: [+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS]'
-
-        info = m.groupdict()
-        for group, attr in {'y':'year', 'm':'month', 'w':'week', 'd':'day',
-                'H':'hour', 'M':'minute', 'S':'second'}.items():
-            if info[group] is not None:
-                setattr(self, attr, int(info[group]))
-
-        if self.week:
-            self.day = self.day + self.week*7
-
-        if info['s'] is not None:
-            self.sign = {'+':1, '-':-1}[info['s']]
-
-    def __repr__(self):
-        return '<Interval %s>'%self.__str__()
-
-    def pretty(self, threshold=('d', 5)):
-        ''' print up the date date using one of these nice formats..
-            < 1 minute
-            < 15 minutes
-            < 30 minutes
-            < 1 hour
-            < 12 hours
-            < 1 day
-            otherwise, return None (so a full date may be displayed)
-        '''
-        if self.year or self.month or self.day > 5:
-            return None
-        if self.day > 1:
-            return '%s days'%self.day
-        if self.day == 1 or self.hour > 12:
-            return 'yesterday'
-        if self.hour > 1:
-            return '%s hours'%self.hour
-        if self.hour == 1:
-            if self.minute < 15:
-                return 'an hour'
-            quart = self.minute/15
-            if quart == 2:
-                return '1 1/2 hours'
-            return '1 %s/4 hours'%quart
-        if self.minute < 1:
-            return 'just now'
-        if self.minute == 1:
-            return '1 minute'
-        if self.minute < 15:
-            return '%s minutes'%self.minute
-        quart = self.minute/15
-        if quart == 2:
-            return '1/2 an hour'
-        return '%s/4 hour'%quart
-
-
-def test():
-    intervals = ("  3w  1  d  2:00", " + 2d", "3w")
-    for interval in intervals:
-        print '>>> Interval("%s")'%interval
-        print `Interval(interval)`
-
-    dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25",
-        "08-13.22:13", "14:25")
-    for date in dates:
-        print '>>> Date("%s")'%date
-        print `Date(date)`
-
-    sums = ((". + 2d", "3w"), (".", "  3w  1  d  2:00"))
-    for date, interval in sums:
-        print '>>> Date("%s") + Interval("%s")'%(date, interval)
-        print `Date(date) + Interval(interval)`
-
-if __name__ == '__main__':
-    test()
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.2  2001/07/19 05:52:22  anthonybaxter
-# Added CVS keywords Id and Log to all python files.
-#
-#
-
diff --git a/hyperdb.py b/hyperdb.py
deleted file mode 100644 (file)
index a3fce09..0000000
+++ /dev/null
@@ -1,744 +0,0 @@
-# $Id: hyperdb.py,v 1.6 2001-07-20 08:20:24 richard Exp $
-
-# standard python modules
-import cPickle, re, string
-
-# roundup modules
-import date
-
-
-RETIRED_FLAG = '__hyperdb_retired'
-
-#
-# Here's where we figure which db to use....
-# 
-import hyperdb_bsddb
-Database = hyperdb_bsddb.Database
-hyperdb_bsddb.RETIRED_FLAG = RETIRED_FLAG
-
-
-#
-# Types
-#
-class BaseType:
-    isStringType = 0
-    isDateType = 0
-    isIntervalType = 0
-    isLinkType = 0
-    isMultilinkType = 0
-
-class String(BaseType):
-    def __init__(self):
-        """An object designating a String property."""
-        pass
-    def __repr__(self):
-        return '<%s>'%self.__class__
-    isStringType = 1
-
-class Date(BaseType, String):
-    isDateType = 1
-
-class Interval(BaseType, String):
-    isIntervalType = 1
-
-class Link(BaseType):
-    def __init__(self, classname):
-        """An object designating a Link property that links to
-        nodes in a specified class."""
-        self.classname = classname
-    def __repr__(self):
-        return '<%s to "%s">'%(self.__class__, self.classname)
-    isLinkType = 1
-
-class Multilink(BaseType, Link):
-    """An object designating a Multilink property that links
-       to nodes in a specified class.
-    """
-    isMultilinkType = 1
-
-class DatabaseError(ValueError):
-    pass
-
-
-#
-# The base Class class
-#
-class Class:
-    """The handle to a particular class of nodes in a hyperdatabase."""
-
-    def __init__(self, db, classname, **properties):
-        """Create a new class with a given name and property specification.
-
-        'classname' must not collide with the name of an existing class,
-        or a ValueError is raised.  The keyword arguments in 'properties'
-        must map names to property objects, or a TypeError is raised.
-        """
-        self.classname = classname
-        self.properties = properties
-        self.db = db
-        self.key = ''
-
-        # do the db-related init stuff
-        db.addclass(self)
-
-    # Editing nodes:
-
-    def create(self, **propvalues):
-        """Create a new node of this class and return its id.
-
-        The keyword arguments in 'propvalues' map property names to values.
-
-        The values of arguments must be acceptable for the types of their
-        corresponding properties or a TypeError is raised.
-        
-        If this class has a key property, it must be present and its value
-        must not collide with other key strings or a ValueError is raised.
-        
-        Any other properties on this class that are missing from the
-        'propvalues' dictionary are set to None.
-        
-        If an id in a link or multilink property does not refer to a valid
-        node, an IndexError is raised.
-        """
-        if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
-        newid = str(self.count() + 1)
-
-        # validate propvalues
-        num_re = re.compile('^\d+$')
-        for key, value in propvalues.items():
-            if key == self.key:
-                try:
-                    self.lookup(value)
-                except KeyError:
-                    pass
-                else:
-                    raise ValueError, 'node with key "%s" exists'%value
-
-            prop = self.properties[key]
-
-            if prop.isLinkType:
-                value = str(value)
-                link_class = self.properties[key].classname
-                if not num_re.match(value):
-                    try:
-                        value = self.db.classes[link_class].lookup(value)
-                    except:
-                        raise ValueError, 'new property "%s": %s not a %s'%(
-                            key, value, self.properties[key].classname)
-                propvalues[key] = value
-                if not self.db.hasnode(link_class, value):
-                    raise ValueError, '%s has no node %s'%(link_class, value)
-
-                # register the link with the newly linked node
-                self.db.addjournal(link_class, value, 'link',
-                    (self.classname, newid, key))
-
-            elif prop.isMultilinkType:
-                if type(value) != type([]):
-                    raise TypeError, 'new property "%s" not a list of ids'%key
-                link_class = self.properties[key].classname
-                l = []
-                for entry in map(str, value):
-                    if not num_re.match(entry):
-                        try:
-                            entry = self.db.classes[link_class].lookup(entry)
-                        except:
-                            raise ValueError, 'new property "%s": %s not a %s'%(
-                                key, entry, self.properties[key].classname)
-                    l.append(entry)
-                value = l
-                propvalues[key] = value
-
-                # handle additions
-                for id in value:
-                    if not self.db.hasnode(link_class, id):
-                        raise ValueError, '%s has no node %s'%(link_class, id)
-                    # register the link with the newly linked node
-                    self.db.addjournal(link_class, id, 'link',
-                        (self.classname, newid, key))
-
-            elif prop.isStringType:
-                if type(value) != type(''):
-                    raise TypeError, 'new property "%s" not a string'%key
-
-            elif prop.isDateType:
-                if not hasattr(value, 'isDate'):
-                    raise TypeError, 'new property "%s" not a Date'% key
-
-            elif prop.isIntervalType:
-                if not hasattr(value, 'isInterval'):
-                    raise TypeError, 'new property "%s" not an Interval'% key
-
-        for key,prop in self.properties.items():
-            if propvalues.has_key(str(key)):
-                continue
-            if prop.isMultilinkType:
-                propvalues[key] = []
-            else:
-                propvalues[key] = None
-
-        # done
-        self.db.addnode(self.classname, newid, propvalues)
-        self.db.addjournal(self.classname, newid, 'create', propvalues)
-        return newid
-
-    def get(self, nodeid, propname):
-        """Get the value of a property on an existing node of this class.
-
-        'nodeid' must be the id of an existing node of this class or an
-        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, str(nodeid))
-        return d[propname]
-
-    # XXX not in spec
-    def getnode(self, nodeid):
-        ''' Return a convenience wrapper for the node
-        '''
-        return Node(self, nodeid)
-
-    def set(self, nodeid, **propvalues):
-        """Modify a property on an existing node of this class.
-        
-        'nodeid' must be the id of an existing node of this class or an
-        IndexError is raised.
-
-        Each key in 'propvalues' must be the name of a property of this
-        class or a KeyError is raised.
-
-        All values in 'propvalues' must be acceptable types for their
-        corresponding properties or a TypeError is raised.
-
-        If the value of the key property is set, it must not collide with
-        other key strings or a ValueError is raised.
-
-        If the value of a Link or Multilink property contains an invalid
-        node id, a ValueError is raised.
-        """
-        if not propvalues:
-            return
-        if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
-        nodeid = str(nodeid)
-        node = self.db.getnode(self.classname, nodeid)
-        if node.has_key(RETIRED_FLAG):
-            raise IndexError
-        num_re = re.compile('^\d+$')
-        for key, value in propvalues.items():
-            if not node.has_key(key):
-                raise KeyError, key
-
-            if key == self.key:
-                try:
-                    self.lookup(value)
-                except KeyError:
-                    pass
-                else:
-                    raise ValueError, 'node with key "%s" exists'%value
-
-            prop = self.properties[key]
-
-            if prop.isLinkType:
-                value = str(value)
-                link_class = self.properties[key].classname
-                if not num_re.match(value):
-                    try:
-                        value = self.db.classes[link_class].lookup(value)
-                    except:
-                        raise ValueError, 'new property "%s": %s not a %s'%(
-                            key, value, self.properties[key].classname)
-
-                if not self.db.hasnode(link_class, value):
-                    raise ValueError, '%s has no node %s'%(link_class, value)
-
-                # register the unlink with the old linked node
-                if node[key] is not None:
-                    self.db.addjournal(link_class, node[key], 'unlink',
-                        (self.classname, nodeid, key))
-
-                # register the link with the newly linked node
-                if value is not None:
-                    self.db.addjournal(link_class, value, 'link',
-                        (self.classname, nodeid, key))
-
-            elif prop.isMultilinkType:
-                if type(value) != type([]):
-                    raise TypeError, 'new property "%s" not a list of ids'%key
-                link_class = self.properties[key].classname
-                l = []
-                for entry in map(str, value):
-                    if not num_re.match(entry):
-                        try:
-                            entry = self.db.classes[link_class].lookup(entry)
-                        except:
-                            raise ValueError, 'new property "%s": %s not a %s'%(
-                                key, entry, self.properties[key].classname)
-                    l.append(entry)
-                value = l
-                propvalues[key] = value
-
-                #handle removals
-                l = node[key]
-                for id in l[:]:
-                    if id in value:
-                        continue
-                    # register the unlink with the old linked node
-                    self.db.addjournal(link_class, id, 'unlink',
-                        (self.classname, nodeid, key))
-                    l.remove(id)
-
-                # handle additions
-                for id in value:
-                    if not self.db.hasnode(link_class, id):
-                        raise ValueError, '%s has no node %s'%(link_class, id)
-                    if id in l:
-                        continue
-                    # register the link with the newly linked node
-                    self.db.addjournal(link_class, id, 'link',
-                        (self.classname, nodeid, key))
-                    l.append(id)
-
-            elif prop.isStringType:
-                if value is not None and type(value) != type(''):
-                    raise TypeError, 'new property "%s" not a string'%key
-
-            elif prop.isDateType:
-                if not hasattr(value, 'isDate'):
-                    raise TypeError, 'new property "%s" not a Date'% key
-
-            elif prop.isIntervalType:
-                if not hasattr(value, 'isInterval'):
-                    raise TypeError, 'new property "%s" not an Interval'% key
-
-            node[key] = value
-
-        self.db.setnode(self.classname, nodeid, node)
-        self.db.addjournal(self.classname, nodeid, 'set', propvalues)
-
-    def retire(self, nodeid):
-        """Retire a node.
-        
-        The properties on the node remain available from the get() method,
-        and the node's id is never reused.
-        
-        Retired nodes are not returned by the find(), list(), or lookup()
-        methods, and other nodes may reuse the values of their key properties.
-        """
-        nodeid = str(nodeid)
-        if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
-        node = self.db.getnode(self.classname, nodeid)
-        node[RETIRED_FLAG] = 1
-        self.db.setnode(self.classname, nodeid, node)
-        self.db.addjournal(self.classname, nodeid, 'retired', None)
-
-    def history(self, nodeid):
-        """Retrieve the journal of edits on a particular node.
-
-        'nodeid' must be the id of an existing node of this class or an
-        IndexError is raised.
-
-        The returned list contains tuples of the form
-
-            (date, tag, action, params)
-
-        'date' is a Timestamp object specifying the time of the change and
-        'tag' is the journaltag specified when the database was opened.
-        """
-        return self.db.getjournal(self.classname, nodeid)
-
-    # Locating nodes:
-
-    def setkey(self, propname):
-        """Select a String property of this class to be the key property.
-
-        'propname' must be the name of a String property of this class or
-        None, or a TypeError is raised.  The values of the key property on
-        all existing nodes must be unique or a ValueError is raised.
-        """
-        self.key = propname
-
-    def getkey(self):
-        """Return the name of the key property for this class or None."""
-        return self.key
-
-    # TODO: set up a separate index db file for this? profile?
-    def lookup(self, keyvalue):
-        """Locate a particular node by its key property and return its id.
-
-        If this class has no key property, a TypeError is raised.  If the
-        'keyvalue' matches one of the values for the key property among
-        the nodes in this class, the matching node's id is returned;
-        otherwise a KeyError is raised.
-        """
-        cldb = self.db.getclassdb(self.classname)
-        for nodeid in self.db.getnodeids(self.classname, cldb):
-            node = self.db.getnode(self.classname, nodeid, cldb)
-            if node.has_key(RETIRED_FLAG):
-                continue
-            if node[self.key] == keyvalue:
-                return nodeid
-        cldb.close()
-        raise KeyError, keyvalue
-
-    # XXX: change from spec - allows multiple props to match
-    def find(self, **propspec):
-        """Get the ids of nodes in this class which link to a given node.
-
-        'propspec' consists of keyword args propname=nodeid   
-          'propname' must be the name of a property in this class, or a
-            KeyError is raised.  That property must be a Link or Multilink
-            property, or a TypeError is raised.
-
-          'nodeid' must be the id of an existing node in the class linked
-            to by the given property, or an IndexError is raised.
-        """
-        propspec = propspec.items()
-        for propname, nodeid in propspec:
-            nodeid = str(nodeid)
-            # check the prop is OK
-            prop = self.properties[propname]
-            if not prop.isLinkType and not prop.isMultilinkType:
-                raise TypeError, "'%s' not a Link/Multilink property"%propname
-            if not self.db.hasnode(prop.classname, nodeid):
-                raise ValueError, '%s has no node %s'%(link_class, nodeid)
-
-        # ok, now do the find
-        cldb = self.db.getclassdb(self.classname)
-        l = []
-        for id in self.db.getnodeids(self.classname, cldb):
-            node = self.db.getnode(self.classname, id, cldb)
-            if node.has_key(RETIRED_FLAG):
-                continue
-            for propname, nodeid in propspec:
-                nodeid = str(nodeid)
-                property = node[propname]
-                if prop.isLinkType and nodeid == property:
-                    l.append(id)
-                elif prop.isMultilinkType and nodeid in property:
-                    l.append(id)
-        cldb.close()
-        return l
-
-    def stringFind(self, **requirements):
-        """Locate a particular node by matching a set of its String properties.
-
-        If the property is not a String property, a TypeError is raised.
-        
-        The return is a list of the id of all nodes that match.
-        """
-        for propname in requirements.keys():
-            prop = self.properties[propname]
-            if not prop.isStringType:
-                raise TypeError, "'%s' not a String property"%propname
-        l = []
-        cldb = self.db.getclassdb(self.classname)
-        for nodeid in self.db.getnodeids(self.classname, cldb):
-            node = self.db.getnode(self.classname, nodeid, cldb)
-            if node.has_key(RETIRED_FLAG):
-                continue
-            for key, value in requirements.items():
-                if node[key] != value:
-                    break
-            else:
-                l.append(nodeid)
-        cldb.close()
-        return l
-
-    def list(self):
-        """Return a list of the ids of the active nodes in this class."""
-        l = []
-        cn = self.classname
-        cldb = self.db.getclassdb(cn)
-        for nodeid in self.db.getnodeids(cn, cldb):
-            node = self.db.getnode(cn, nodeid, cldb)
-            if node.has_key(RETIRED_FLAG):
-                continue
-            l.append(nodeid)
-        l.sort()
-        cldb.close()
-        return l
-
-    # XXX not in spec
-    def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
-        ''' Return a list of the ids of the active nodes in this class that
-            match the 'filter' spec, sorted by the group spec and then the
-            sort spec
-        '''
-        cn = self.classname
-
-        # optimise filterspec
-        l = []
-        props = self.getprops()
-        for k, v in filterspec.items():
-            propclass = props[k]
-            if propclass.isLinkType:
-                if type(v) is not type([]):
-                    v = [v]
-                # replace key values with node ids
-                u = []
-                link_class =  self.db.classes[propclass.classname]
-                for entry in v:
-                    if not num_re.match(entry):
-                        try:
-                            entry = link_class.lookup(entry)
-                        except:
-                            raise ValueError, 'new property "%s": %s not a %s'%(
-                                k, entry, self.properties[k].classname)
-                    u.append(entry)
-
-                l.append((0, k, u))
-            elif propclass.isMultilinkType:
-                if type(v) is not type([]):
-                    v = [v]
-                # replace key values with node ids
-                u = []
-                link_class =  self.db.classes[propclass.classname]
-                for entry in v:
-                    if not num_re.match(entry):
-                        try:
-                            entry = link_class.lookup(entry)
-                        except:
-                            raise ValueError, 'new property "%s": %s not a %s'%(
-                                k, entry, self.properties[k].classname)
-                    u.append(entry)
-                l.append((1, k, u))
-            elif propclass.isStringType:
-                v = v[0]
-                if '*' in v or '?' in v:
-                    # simple glob searching
-                    v = v.replace('?', '.')
-                    v = v.replace('*', '.*?')
-                    v = re.compile(v)
-                    l.append((2, k, v))
-                elif v[0] == '^':
-                    # start-anchored
-                    if v[-1] == '$':
-                        # _and_ end-anchored
-                        l.append((6, k, v[1:-1]))
-                    l.append((3, k, v[1:]))
-                elif v[-1] == '$':
-                    # end-anchored
-                    l.append((4, k, v[:-1]))
-                else:
-                    # substring
-                    l.append((5, k, v))
-            else:
-                l.append((6, k, v))
-        filterspec = l
-
-        # now, find all the nodes that are active and pass filtering
-        l = []
-        cldb = self.db.getclassdb(cn)
-        for nodeid in self.db.getnodeids(cn, cldb):
-            node = self.db.getnode(cn, nodeid, cldb)
-            if node.has_key(RETIRED_FLAG):
-                continue
-            # apply filter
-            for t, k, v in filterspec:
-                if t == 0 and node[k] not in v:
-                    # link - if this node'd property doesn't appear in the
-                    # filterspec's nodeid list, skip it
-                    break
-                elif t == 1:
-                    # multilink - if any of the nodeids required by the
-                    # filterspec aren't in this node's property, then skip
-                    # it
-                    for value in v:
-                        if value not in node[k]:
-                            break
-                    else:
-                        continue
-                    break
-                elif t == 2 and not v.search(node[k]):
-                    # RE search
-                    break
-                elif t == 3 and node[k][:len(v)] != v:
-                    # start anchored
-                    break
-                elif t == 4 and node[k][-len(v):] != v:
-                    # end anchored
-                    break
-                elif t == 5 and node[k].find(v) == -1:
-                    # substring search
-                    break
-                elif t == 6 and node[k] != v:
-                    # straight value comparison for the other types
-                    break
-            else:
-                l.append((nodeid, node))
-        l.sort()
-        cldb.close()
-
-        # optimise sort
-        m = []
-        for entry in sort:
-            if entry[0] != '-':
-                m.append(('+', entry))
-            else:
-                m.append((entry[0], entry[1:]))
-        sort = m
-
-        # optimise group
-        m = []
-        for entry in group:
-            if entry[0] != '-':
-                m.append(('+', entry))
-            else:
-                m.append((entry[0], entry[1:]))
-        group = m
-
-        # now, sort the result
-        def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
-                db = self.db, cl=self):
-            a_id, an = a
-            b_id, bn = b
-            for list in group, sort:
-                for dir, prop in list:
-                    # handle the properties that might be "faked"
-                    if not an.has_key(prop):
-                        an[prop] = cl.get(a_id, prop)
-                    av = an[prop]
-                    if not bn.has_key(prop):
-                        bn[prop] = cl.get(b_id, prop)
-                    bv = bn[prop]
-
-                    # sorting is class-specific
-                    propclass = properties[prop]
-
-                    # String and Date values are sorted in the natural way
-                    if propclass.isStringType:
-                        # clean up the strings
-                        if av and av[0] in string.uppercase:
-                            av = an[prop] = av.lower()
-                        if bv and bv[0] in string.uppercase:
-                            bv = bn[prop] = bv.lower()
-                    if propclass.isStringType or propclass.isDateType:
-                        if dir == '+':
-                            r = cmp(av, bv)
-                            if r != 0: return r
-                        elif dir == '-':
-                            r = cmp(bv, av)
-                            if r != 0: return r
-
-                    # Link properties are sorted according to the value of
-                    # the "order" property on the linked nodes if it is
-                    # present; or otherwise on the key string of the linked
-                    # nodes; or finally on  the node ids.
-                    elif propclass.isLinkType:
-                        link = db.classes[propclass.classname]
-                        if link.getprops().has_key('order'):
-                            if dir == '+':
-                                r = cmp(link.get(av, 'order'),
-                                    link.get(bv, 'order'))
-                                if r != 0: return r
-                            elif dir == '-':
-                                r = cmp(link.get(bv, 'order'),
-                                    link.get(av, 'order'))
-                                if r != 0: return r
-                        elif link.getkey():
-                            key = link.getkey()
-                            if dir == '+':
-                                r = cmp(link.get(av, key), link.get(bv, key))
-                                if r != 0: return r
-                            elif dir == '-':
-                                r = cmp(link.get(bv, key), link.get(av, key))
-                                if r != 0: return r
-                        else:
-                            if dir == '+':
-                                r = cmp(av, bv)
-                                if r != 0: return r
-                            elif dir == '-':
-                                r = cmp(bv, av)
-                                if r != 0: return r
-
-                    # Multilink properties are sorted according to how many
-                    # links are present.
-                    elif propclass.isMultilinkType:
-                        if dir == '+':
-                            r = cmp(len(av), len(bv))
-                            if r != 0: return r
-                        elif dir == '-':
-                            r = cmp(len(bv), len(av))
-                            if r != 0: return r
-            return cmp(a[0], b[0])
-        l.sort(sortfun)
-        return [i[0] for i in l]
-
-    def count(self):
-        """Get the number of nodes in this class.
-
-        If the returned integer is 'numnodes', the ids of all the nodes
-        in this class run from 1 to numnodes, and numnodes+1 will be the
-        id of the next node to be created in this class.
-        """
-        return self.db.countnodes(self.classname)
-
-    # Manipulating properties:
-
-    def getprops(self):
-        """Return a dictionary mapping property names to property objects."""
-        return self.properties
-
-    def addprop(self, **properties):
-        """Add properties to this class.
-
-        The keyword arguments in 'properties' must map names to property
-        objects, or a TypeError is raised.  None of the keys in 'properties'
-        may collide with the names of existing properties, or a ValueError
-        is raised before any properties have been added.
-        """
-        for key in properties.keys():
-            if self.properties.has_key(key):
-                raise ValueError, key
-        self.properties.update(properties)
-
-
-# XXX not in spec
-class Node:
-    ''' A convenience wrapper for the given node
-    '''
-    def __init__(self, cl, nodeid):
-        self.__dict__['cl'] = cl
-        self.__dict__['nodeid'] = nodeid
-    def keys(self):
-        return self.cl.getprops().keys()
-    def has_key(self, name):
-        return self.cl.getprops().has_key(name)
-    def __getattr__(self, name):
-        if self.__dict__.has_key(name):
-            return self.__dict__['name']
-        try:
-            return self.cl.get(self.nodeid, name)
-        except KeyError, value:
-            raise AttributeError, str(value)
-    def __getitem__(self, name):
-        return self.cl.get(self.nodeid, name)
-    def __setattr__(self, name, value):
-        try:
-            return self.cl.set(self.nodeid, **{name: value})
-        except KeyError, value:
-            raise AttributeError, str(value)
-    def __setitem__(self, name, value):
-        self.cl.set(self.nodeid, **{name: value})
-    def history(self):
-        return self.cl.history(self.nodeid)
-    def retire(self):
-        return self.cl.retire(self.nodeid)
-
-
-def Choice(name, *options):
-    cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
-    for i in range(len(options)):
-        cl.create(name=option[i], order=i)
-    return hyperdb.Link(name)
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.5  2001/07/20 07:35:55  richard
-# largish changes as a start of splitting off bits and pieces to allow more
-# flexible installation / database back-ends
-#
-
diff --git a/hyperdb_bsddb.py b/hyperdb_bsddb.py
deleted file mode 100644 (file)
index 422fced..0000000
+++ /dev/null
@@ -1,165 +0,0 @@
-#$Id: hyperdb_bsddb.py,v 1.1 2001-07-20 07:35:55 richard Exp $
-
-import bsddb, os, cPickle
-import date
-
-#
-# Now the database
-#
-class Database:
-    """A database for storing records containing flexible data types."""
-
-    def __init__(self, storagelocator, journaltag=None):
-        """Open a hyperdatabase given a specifier to some storage.
-
-        The meaning of 'storagelocator' depends on the particular
-        implementation of the hyperdatabase.  It could be a file name,
-        a directory path, a socket descriptor for a connection to a
-        database over the network, etc.
-
-        The 'journaltag' is a token that will be attached to the journal
-        entries for any edits done on the database.  If 'journaltag' is
-        None, the database is opened in read-only mode: the Class.create(),
-        Class.set(), and Class.retire() methods are disabled.
-        """
-        self.dir, self.journaltag = storagelocator, journaltag
-        self.classes = {}
-
-    #
-    # Classes
-    #
-    def __getattr__(self, classname):
-        """A convenient way of calling self.getclass(classname)."""
-        return self.classes[classname]
-
-    def addclass(self, cl):
-        cn = cl.classname
-        if self.classes.has_key(cn):
-            raise ValueError, cn
-        self.classes[cn] = cl
-
-    def getclasses(self):
-        """Return a list of the names of all existing classes."""
-        l = self.classes.keys()
-        l.sort()
-        return l
-
-    def getclass(self, classname):
-        """Get the Class object representing a particular class.
-
-        If 'classname' is not a valid class name, a KeyError is raised.
-        """
-        return self.classes[classname]
-
-    #
-    # Class DBs
-    #
-    def clear(self):
-        for cn in self.classes.keys():
-            db = os.path.join(self.dir, 'nodes.%s'%cn)
-            bsddb.btopen(db, 'n')
-            db = os.path.join(self.dir, 'journals.%s'%cn)
-            bsddb.btopen(db, 'n')
-
-    def getclassdb(self, classname, mode='r'):
-        ''' grab a connection to the class db that will be used for
-            multiple actions
-        '''
-        path = os.path.join(os.getcwd(), self.dir, 'nodes.%s'%classname)
-        return bsddb.btopen(path, mode)
-
-    def addnode(self, classname, nodeid, node):
-        ''' add the specified node to its class's db
-        '''
-        db = self.getclassdb(classname, 'c')
-        db[nodeid] = cPickle.dumps(node, 1)
-        db.close()
-    setnode = addnode
-
-    def getnode(self, classname, nodeid, cldb=None):
-        ''' add the specified node to its class's db
-        '''
-        db = cldb or self.getclassdb(classname)
-        if not db.has_key(nodeid):
-            raise IndexError, nodeid
-        res = cPickle.loads(db[nodeid])
-        if not cldb: db.close()
-        return res
-
-    def hasnode(self, classname, nodeid, cldb=None):
-        ''' add the specified node to its class's db
-        '''
-        db = cldb or self.getclassdb(classname)
-        res = db.has_key(nodeid)
-        if not cldb: db.close()
-        return res
-
-    def countnodes(self, classname, cldb=None):
-        db = cldb or self.getclassdb(classname)
-        return len(db.keys())
-        if not cldb: db.close()
-        return res
-
-    def getnodeids(self, classname, cldb=None):
-        db = cldb or self.getclassdb(classname)
-        res = db.keys()
-        if not cldb: db.close()
-        return res
-
-    #
-    # Journal
-    #
-    def addjournal(self, classname, nodeid, action, params):
-        ''' Journal the Action
-        'action' may be:
-
-            'create' or 'set' -- 'params' is a dictionary of property values
-            'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
-            'retire' -- 'params' is None
-        '''
-        entry = (nodeid, date.Date(), self.journaltag, action, params)
-        db = bsddb.btopen(os.path.join(self.dir, 'journals.%s'%classname), 'c')
-        if db.has_key(nodeid):
-            s = db[nodeid]
-            l = cPickle.loads(db[nodeid])
-            l.append(entry)
-        else:
-            l = [entry]
-        db[nodeid] = cPickle.dumps(l)
-        db.close()
-
-    def getjournal(self, classname, nodeid):
-        ''' get the journal for id
-        '''
-        db = bsddb.btopen(os.path.join(self.dir, 'journals.%s'%classname), 'r')
-        res = cPickle.loads(db[nodeid])
-        db.close()
-        return res
-
-    def close(self):
-        ''' Close the Database - we must release the circular refs so that
-            we can be del'ed and the underlying bsddb connections closed
-            cleanly.
-        '''
-        self.classes = None
-
-
-    #
-    # Basic transaction support
-    #
-    # TODO: well, write these methods (and then use them in other code)
-    def register_action(self):
-        ''' Register an action to the transaction undo log
-        '''
-
-    def commit(self):
-        ''' Commit the current transaction, start a new one
-        '''
-
-    def rollback(self):
-        ''' Reverse all actions from the current transaction
-        '''
-
-#
-#$Log: not supported by cvs2svn $
-
diff --git a/roundup-mailgw.py b/roundup-mailgw.py
deleted file mode 100755 (executable)
index a83cb06..0000000
+++ /dev/null
@@ -1,282 +0,0 @@
-#! /usr/bin/python
-'''
-Incoming messages are examined for multiple parts. In a multipart/mixed
-message or part, each subpart is extracted and examined. In a
-multipart/alternative message or part, we look for a text/plain subpart and
-ignore the other parts. The text/plain subparts are assembled to form the
-textual body of the message, to be stored in the file associated with a
-"msg" class node. Any parts of other types are each stored in separate
-files and given "file" class nodes that are linked to the "msg" node. 
-
-The "summary" property on message nodes is taken from the first non-quoting
-section in the message body. The message body is divided into sections by
-blank lines. Sections where the second and all subsequent lines begin with
-a ">" or "|" character are considered "quoting sections". The first line of
-the first non-quoting section becomes the summary of the message. 
-
-All of the addresses in the To: and Cc: headers of the incoming message are
-looked up among the user nodes, and the corresponding users are placed in
-the "recipients" property on the new "msg" node. The address in the From:
-header similarly determines the "author" property of the new "msg"
-node. The default handling for addresses that don't have corresponding
-users is to create new users with no passwords and a username equal to the
-address. (The web interface does not permit logins for users with no
-passwords.) If we prefer to reject mail from outside sources, we can simply
-register an auditor on the "user" class that prevents the creation of user
-nodes with no passwords. 
-
-The subject line of the incoming message is examined to determine whether
-the message is an attempt to create a new item or to discuss an existing
-item. A designator enclosed in square brackets is sought as the first thing
-on the subject line (after skipping any "Fwd:" or "Re:" prefixes). 
-
-If an item designator (class name and id number) is found there, the newly
-created "msg" node is added to the "messages" property for that item, and
-any new "file" nodes are added to the "files" property for the item. 
-
-If just an item class name is found there, we attempt to create a new item
-of that class with its "messages" property initialized to contain the new
-"msg" node and its "files" property initialized to contain any new "file"
-nodes. 
-
-Both cases may trigger detectors (in the first case we are calling the
-set() method to add the message to the item's spool; in the second case we
-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: roundup-mailgw.py,v 1.3 2001-07-19 06:27:07 anthonybaxter Exp $
-'''
-
-import sys
-if int(sys.version[0]) < 2:
-    print "Roundup requires Python 2.0 or newer."
-    sys.exit(0)
-
-import string, re, os, mimetools, StringIO, smtplib, socket, binascii, quopri
-import config, date, roundupdb
-
-def getPart(fp, boundary):
-    line = ''
-    s = StringIO.StringIO()
-    while 1:
-        line_n = fp.readline()
-        if not line_n:
-            break
-        line = line_n.strip()
-        if line == '--'+boundary+'--':
-            break
-        if line == '--'+boundary:
-            break
-        s.write(line_n)
-    if not s.getvalue().strip():
-        return None
-    return s
-
-subject_re = re.compile(r'(\[?(fwd|re):\s*)*'
-    r'(\[(?P<classname>[^\d]+)(?P<nodeid>\d+)?\])'
-    r'(?P<title>[^\[]+)(\[(?P<args>.+?)\])?', re.I)
-
-def roundup_mail(db, fp):
-    # ok, figure the subject, author, recipients and content-type
-    message = mimetools.Message(fp)
-    try:
-        handle_message(db, message)
-    except:
-        # send an email to the people who missed out
-        sendto = [message.getaddrlist('from')[0][1]]
-        m = ['Subject: failed issue tracker submission']
-        m.append('')
-        # TODO as attachments?
-        m.append('----  traceback of failure  ----')
-        return
-        s = StringIO.StringIO()
-        import traceback
-        traceback.print_exc(None, s)
-        m.append(s.getvalue())
-        m.append('---- failed message follows ----')
-        try:
-            fp.seek(0)
-        except:
-            pass
-        m.append(fp.read())
-        try:
-            smtp = smtplib.SMTP(config.MAILHOST)
-            smtp.sendmail(config.ADMIN_EMAIL, sendto, '\n'.join(m))
-        except socket.error, value:
-            return "Couldn't send confirmation email: mailhost %s"%value
-        except smtplib.SMTPException, value:
-            return "Couldn't send confirmation email: %s"%value
-
-def handle_message(db, message):
-    # handle the subject line
-    m = subject_re.match(message.getheader('subject'))
-    if not m:
-        raise ValueError, 'No [designator] found in subject "%s"'
-    classname = m.group('classname')
-    nodeid = m.group('nodeid')
-    title = m.group('title').strip()
-    subject_args = m.group('args')
-    cl = db.getclass(classname)
-    properties = cl.getprops()
-    props = {}
-    args = m.group('args')
-    if args:
-        for prop in string.split(m.group('args'), ';'):
-            try:
-                key, value = prop.split('=')
-            except ValueError, message:
-                raise ValueError, 'Args list not of form [arg=value,value,...;arg=value,value,value..]  (specific exception message was "%s")'%message
-            type =  properties[key]
-            if type.isStringType:
-                props[key] = value 
-            elif type.isDateType:
-                props[key] = date.Date(value)
-            elif type.isIntervalType:
-                props[key] = date.Interval(value)
-            elif type.isLinkType:
-                props[key] = value
-            elif type.isMultilinkType:
-                props[key] = value.split(',')
-
-    # handle the users
-    author = db.uidFromAddress(message.getaddrlist('from')[0])
-    recipients = []
-    for recipient in message.getaddrlist('to') + message.getaddrlist('cc'):
-        if recipient[1].strip().lower() == config.ISSUE_TRACKER_EMAIL:
-            continue
-        recipients.append(db.uidFromAddress(recipient))
-
-    # now handle the body - find the message
-    content_type =  message.gettype()
-    attachments = []
-    if content_type == 'multipart/mixed':
-        boundary = message.getparam('boundary')
-        # skip over the intro to the first boundary
-        part = getPart(message.fp, boundary)
-        content = None
-        while 1:
-            # get the next part
-            part = getPart(message.fp, boundary)
-            if part is None:
-                break
-            # parse it
-            part.seek(0)
-            submessage = mimetools.Message(part)
-            subtype = submessage.gettype()
-            if subtype == 'text/plain' and not content:
-                # this one's our content
-                content = part.read()
-            elif subtype == 'message/rfc822':
-                i = part.tell()
-                subsubmess = mimetools.Message(part)
-                name = subsubmess.getheader('subject')
-                part.seek(i)
-                attachments.append((name, 'message/rfc822', part.read()))
-            else:
-                # try name on Content-Type
-                name = submessage.getparam('name')
-                # this is just an attachment
-                data = part.read()
-                encoding = submessage.getencoding()
-                if encoding == 'base64':
-                    data = binascii.a2b_base64(data)
-                elif encoding == 'quoted-printable':
-                    data = quopri.decode(data)
-                elif encoding == 'uuencoded':
-                    data = binascii.a2b_uu(data)
-                attachments.append((name, submessage.gettype(), data))
-        if content is None:
-            raise ValueError, 'No text/plain part found'
-
-    elif content_type[:10] == 'multipart/':
-        boundary = message.getparam('boundary')
-        # skip over the intro to the first boundary
-        getPart(message.fp, boundary)
-        content = None
-        while 1:
-            # get the next part
-            part = getPart(message.fp, boundary)
-            if part is None:
-                break
-            # parse it
-            part.seek(0)
-            submessage = mimetools.Message(part)
-            if submessage.gettype() == 'text/plain' and not content:
-                # this one's our content
-                content = part.read()
-        if content is None:
-            raise ValueError, 'No text/plain part found'
-
-    elif content_type != 'text/plain':
-        raise ValueError, 'No text/plain part found'
-
-    else:
-        content = message.fp.read()
-
-    # extract out the summary from the message
-    summary = []
-    for line in content.split('\n'):
-        line = line.strip()
-        if summary and not line:
-            break
-        if not line:
-            summary.append('')
-        elif line[0] not in '>|':
-            summary.append(line)
-    summary = '\n'.join(summary)
-
-    # handle the files
-    files = []
-    for (name, type, data) in attachments:
-        files.append(db.file.create(type=type, name=name, content=data))
-
-    # now handle the db stuff
-    if nodeid:
-        # If an item designator (class name and id number) is found there, the
-        # newly created "msg" node is added to the "messages" property for
-        # that item, and any new "file" nodes are added to the "files" 
-        # property for the item. 
-        message_id = db.msg.create(author=author, recipients=recipients,
-            date=date.Date('.'), summary=summary, content=content,
-            files=files)
-        messages = cl.get(nodeid, 'messages')
-        messages.append(message_id)
-        props['messages'] = messages
-        apply(cl.set, (nodeid, ), props)
-    else:
-        # If just an item class name is found there, we attempt to create a
-        # new item of that class with its "messages" property initialized to
-        # contain the new "msg" node and its "files" property initialized to
-        # contain any new "file" nodes. 
-        message_id = db.msg.create(author=author, recipients=recipients,
-            date=date.Date('.'), summary=summary, content=content,
-            files=files)
-        if not props.has_key('assignedto'):
-            props['assignedto'] = 1             # "admin"
-        if not props.has_key('priority'):
-            props['priority'] = 1               # "bug-fatal"
-        if not props.has_key('status'):
-            props['status'] = 1                 # "unread"
-        if not props.has_key('title'):
-            props['title'] = title
-        props['messages'] = [message_id]
-        props['nosy'] = recipients[:]
-        props['nosy'].append(author)
-        props['nosy'].sort()
-        nodeid = apply(cl.create, (), props)
-
-    return 0
-
-if __name__ == '__main__':
-    db = roundupdb.openDB(config.DATABASE, 'admin', '1')
-    roundup_mail(db, sys.stdin)
-    db.close()
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.2  2001/07/19 05:52:22  anthonybaxter
-# Added CVS keywords Id and Log to all python files.
-#
-#
-
diff --git a/roundup.cgi b/roundup.cgi
deleted file mode 100755 (executable)
index 6ea7189..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/env python
-
-import sys
-if int(sys.version[0]) < 2:
-    print "Content-Type: text/plain\n"
-    print "Roundup requires Python 2.0 or newer."
-
-import os, traceback, StringIO, cgi, binascii
-
-try:
-    import cgitb
-except:
-    print "Content-Type: text/html\n"
-    print "Failed to import cgitb"
-    print "<pre>"
-    s = StringIO.StringIO()
-    traceback.print_exc(None, s)
-    print cgi.escape(s.getvalue())
-    print "</pre>"
-
-# Force import first from the same directory where this script lives.
-dir, name = os.path.split(sys.argv[0])
-sys.path[:0] = [dir or "."]
-
-def main(out):
-    import config, roundupdb, roundup_cgi
-    db = roundupdb.openDB(config.DATABASE, 'admin')
-    auth = os.environ.get("HTTP_CGI_AUTHORIZATION", None)
-    message = 'Unauthorised'
-    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
-    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 = roundup_cgi.Client(out, os.environ, user)
-    try:
-        client.main()
-    except roundup_cgi.Unauthorised:
-        out.write('Content-Type: text/html\n')
-        out.write('Status: 403\n\n')
-        out.write('Unauthorised')
-
-out, err = sys.stdout, sys.stderr
-try:
-    import config, roundup_cgi
-    sys.stdout = sys.stderr = open(config.LOG, 'a')
-    main(out)
-except:
-    sys.stdout, sys.stderr = out, err
-    out.write('Content-Type: text/html\n\n')
-    cgitb.handler()
-sys.stdout.flush()
-sys.stdout, sys.stderr = out, err
diff --git a/roundup.py b/roundup.py
deleted file mode 100755 (executable)
index 8aa660f..0000000
+++ /dev/null
@@ -1,241 +0,0 @@
-#! /usr/bin/python
-
-# $Id: roundup.py,v 1.4 2001-07-19 06:27:07 anthonybaxter Exp $ 
-
-import sys
-if int(sys.version[0]) < 2:
-    print 'Roundup requires python 2.0 or later.'
-    sys.exit(1)
-
-import string, os, getpass
-import config, date, roundupdb
-
-def determineLogin(argv):
-    n = 2
-    name = password = ''
-    if sys.argv[2] == '-user':
-        l = sys.argv[3].split(':')
-        name = l[0]
-        if len(l) > 1:
-            password = l[1]
-        n = 4
-    elif os.environ.has_key('ROUNDUP_LOGIN'):
-        l = os.environ['ROUNDUP_LOGIN'].split(':')
-        name = l[0]
-        if len(l) > 1:
-            password = l[1]
-    while not name:
-        name = raw_input('Login name: ')
-    while not password:
-        password = getpass.getpass('  password: ')
-    return n, roundupdb.openDB(config.DATABASE, name, password)
-
-def usage():
-    print '''Usage:
-
- roundup init
-   -- initialise the database
- roundup spec classname
-   -- show the properties for a classname
- roundup create [-user login] classname propname=value ...
-   -- create a new entry of a given class
- roundup list [-list] classname
-   -- list the instances of a class
- roundup history [-list] designator
-   -- show the history entries of a designator
- roundup get [-list] designator[,designator,...] propname
-   -- get the given property of one or more designator(s)
- roundup set [-user login] designator[,designator,...] propname=value ...
-   -- set the given property of one or more designator(s)
- roundup find [-list] classname propname=value ...
-   -- find the class instances with a given property
- roundup retire designator[,designator,...]
-   -- "retire" a designator
- roundup help    
-   -- this help
- roundup morehelp
-   -- even more detailed help
-'''
-
-def moreusage():
-    usage()
-    print '''
-A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
-
-Property values are represented as strings in command arguments and in the
-printed results:
- . Strings are, well, strings.
- . Date values are printed in the full date format in the local time zone, and
-   accepted in the full format or any of the partial formats explained below.
- . Link values are printed as node designators. When given as an argument,
-   node designators and key strings are both accepted.
- . Multilink values are printed as lists of node designators joined by commas.
-   When given as an argument, node designators and key strings are both
-   accepted; an empty string, a single node, or a list of nodes joined by
-   commas is accepted.
-
-When multiple nodes are specified to the roundup get or roundup set
-commands, the specified properties are retrieved or set on all the listed
-nodes. 
-
-When multiple results are returned by the roundup get or roundup find
-commands, they are printed one per line (default) or joined by commas (with
-the -list) option. 
-
-Where the command changes data, a login name/password is required. The
-login may be specified as either "name" or "name:password".
- . ROUNDUP_LOGIN environment variable
- . the -user command-line option
-If either the name or password is not supplied, they are obtained from the
-command-line. 
-
-Date format examples:
-  "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
-  "2000-04-17" means <Date 2000-04-17.00:00:00>
-  "01-25" means <Date yyyy-01-25.00:00:00>
-  "08-13.22:13" means <Date yyyy-08-14.03:13:00>
-  "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
-  "14:25" means <Date yyyy-mm-dd.19:25:00>
-  "8:47:11" means <Date yyyy-mm-dd.13:47:11>
-  "." means "right now"
-'''
-
-def main():
-
-    if len(sys.argv) == 1:
-        usage()
-        return 1
-
-    command = sys.argv[1]
-    if command == 'init':
-        password = ''
-        confirm = 'x'
-        while password != confirm:
-            password = getpass.getpass('Admin Password:')
-            confirm = getpass.getpass('       Confirm:')
-        roundupdb.initDB(config.DATABASE, password)
-        return 0
-
-    if command == 'get':
-        db = roundupdb.openDB(config.DATABASE)
-        designators = string.split(sys.argv[2], ',')
-        propname = sys.argv[3]
-        for designator in designators:
-            classname, nodeid = roundupdb.splitDesignator(designator)
-            print db.getclass(classname).get(nodeid, propname)
-
-    elif command == 'set':
-        n, db = determineLogin(sys.argv)
-        designators = string.split(sys.argv[n], ',')
-        props = {}
-        for prop in sys.argv[n+1:]:
-            key, value = prop.split('=')
-            props[key] = value
-        for designator in designators:
-            classname, nodeid = roundupdb.splitDesignator(designator)
-            cl = db.getclass(classname)
-            properties = cl.getprops()
-            for key, value in props.items():
-                type =  properties[key]
-                if type.isStringType:
-                    continue
-                elif type.isDateType:
-                    props[key] = date.Date(value)
-                elif type.isIntervalType:
-                    props[key] = date.Interval(value)
-                elif type.isLinkType:
-                    props[key] = value
-                elif type.isMultilinkType:
-                    props[key] = value.split(',')
-            apply(cl.set, (nodeid, ), props)
-
-    elif command == 'find':
-        db = roundupdb.openDB(config.DATABASE)
-        classname = sys.argv[2]
-        cl = db.getclass(classname)
-
-        # look up the linked-to class and get the nodeid that has the value
-        propname, value = sys.argv[3:].split('=')
-        propcl = cl[propname].classname
-        nodeid = propcl.lookup(value)
-
-        # now do the find
-        print cl.find(propname, nodeid)
-
-    elif command == 'spec':
-        db = roundupdb.openDB(config.DATABASE)
-        classname = sys.argv[2]
-        cl = db.getclass(classname)
-        for key, value in cl.properties.items():
-            print '%s: %s'%(key, value)
-
-    elif command == 'create':
-        n, db = determineLogin(sys.argv)
-        classname = sys.argv[n]
-        cl = db.getclass(classname)
-        props = {}
-        properties = cl.getprops()
-        for prop in sys.argv[n+1:]:
-            key, value = prop.split('=')
-            type =  properties[key]
-            if type.isStringType:
-                props[key] = value 
-            elif type.isDateType:
-                props[key] = date.Date(value)
-            elif type.isIntervalType:
-                props[key] = date.Interval(value)
-            elif type.isLinkType:
-                props[key] = value
-            elif type.isMultilinkType:
-                props[key] = value.split(',')
-        print apply(cl.create, (), props)
-
-    elif command == 'list':
-        db = roundupdb.openDB(config.DATABASE)
-        classname = sys.argv[2]
-        cl = db.getclass(classname)
-        key = cl.getkey() or cl.properties.keys()[0]
-        for nodeid in cl.list():
-            value = cl.get(nodeid, key)
-            print "%4s: %s"%(nodeid, value)
-
-    elif command == 'history':
-        db = roundupdb.openDB(config.DATABASE)
-        classname, nodeid = roundupdb.splitDesignator(sys.argv[2])
-        print db.getclass(classname).history(nodeid)
-
-    elif command == 'retire':
-        n, db = determineLogin(sys.argv)
-        designators = string.split(sys.argv[2], ',')
-        for designator in designators:
-            classname, nodeid = roundupdb.splitDesignator(designator)
-            db.getclass(classname).retire(nodeid)
-
-    elif command == 'help':
-       usage()
-       return 0
-
-    elif command == 'morehelp':
-       moreusage()
-       return 0
-
-    else:
-       print "Unknown command '%s'"%command
-        usage()
-        return 1
-
-    db.close()
-    return 0
-
-if __name__ == '__main__':
-    sys.exit(main())
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.3  2001/07/19 06:08:24  anthonybaxter
-# fixed typo in usage string because it was bugging me each time I saw it.
-#
-# Revision 1.2  2001/07/19 05:52:22  anthonybaxter
-# Added CVS keywords Id and Log
-#
-
diff --git a/roundup_cgi.py b/roundup_cgi.py
deleted file mode 100644 (file)
index 422af51..0000000
+++ /dev/null
@@ -1,508 +0,0 @@
-# $Id: roundup_cgi.py,v 1.7 2001-07-20 07:35:55 richard Exp $
-
-import os, cgi, pprint, StringIO, urlparse, re, traceback
-
-import config, roundupdb, template, date
-
-class Unauthorised(ValueError):
-    pass
-
-class Client:
-    def __init__(self, out, env, user):
-        self.out = out
-        self.headers_done = 0
-        self.env = env
-        self.path = env.get("PATH_INFO", '').strip()
-        self.user = user
-        self.form = cgi.FieldStorage(environ=env)
-        self.split_path = self.path.split('/')[1:]
-        self.db = roundupdb.openDB(config.DATABASE, self.user)
-        self.headers_done = 0
-        self.debug = 0
-
-    def header(self, headers={'Content-Type':'text/html'}):
-        if not headers.has_key('Content-Type'):
-            headers['Content-Type'] = 'text/html'
-        for entry in headers.items():
-            self.out.write('%s: %s\n'%entry)
-        self.out.write('\n')
-        self.headers_done = 1
-
-    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('style.css').read()
-        userid = self.db.user.lookup(self.user)
-        if self.user == 'admin':
-            extras = ' | <a href="list_classes">Class List</a>'
-        else:
-            extras = ''
-        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><a href="issue?status=unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:columns=activity,status,title&:group=priority">All issues</a> | 
-<a href="issue?priority=fatal-bug,bug">Bugs</a> | 
-<a href="issue?priority=usability">Support</a> | 
-<a href="issue?priority=feature">Wishlist</a> | 
-<a href="newissue">New Issue</a>
-%s</td>
-<td align=right><a href="user%s">Your Details</a></td>
-</table>
-'''%(title, style, message, title, self.user, extras, userid))
-
-    def pagefoot(self):
-        if self.debug:
-            self.write('<hr><small><dl>')
-            self.write('<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 = str(self.form[k].value)
-                    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.out.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_filterspec(self):
-        ''' pull the index filter spec from the form
-        '''
-        # all the other form args are filters
-        filterspec = {}
-        for key in self.form.keys():
-            if key[0] == ':': continue
-            value = self.form[key]
-            if type(value) == type([]):
-                value = [arg.value for arg in value]
-            else:
-                value = value.value.split(',')
-            l = filterspec.get(key, [])
-            l = l + value
-            filterspec[key] = l
-        return filterspec
-
-    def index(self):
-        ''' put up an index
-        '''
-        self.classname = 'issue'
-        if self.form.has_key(':sort'): sort = self.index_arg(':sort')
-        else: sort=['-activity']
-        if self.form.has_key(':group'): group = self.index_arg(':group')
-        else: group=['priority']
-        if self.form.has_key(':filter'): filter = self.index_arg(':filter')
-        else: filter = []
-        if self.form.has_key(':columns'): columns = self.index_arg(':columns')
-        else: columns=['activity','status','title']
-        filterspec = self.index_filterspec()
-        if not filterspec:
-            filterspec['status'] = ['1', '2', '3', '4', '5', '6', '7']
-        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):
-        ''' 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
-        self.pagehead('Index: %s'%cn)
-        if sort is None: sort = self.index_arg(':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()
-
-        template.index(self, self.db, cn, filterspec, filter, columns, sort,
-            group)
-        self.pagefoot()
-
-    def showitem(self, message=None):
-        ''' display an item
-        '''
-        cn = self.classname
-        cl = self.db.classes[cn]
-
-        # possibly perform an edit
-        keys = self.form.keys()
-        num_re = re.compile('^\d+$')
-        if keys:
-            changed = []
-            props = {}
-            try:
-                keys = self.form.keys()
-                for key in keys:
-                    if not cl.properties.has_key(key):
-                        continue
-                    proptype = cl.properties[key]
-                    if proptype.isStringType:
-                        value = str(self.form[key].value).strip()
-                    elif proptype.isDateType:
-                        value = date.Date(str(self.form[key].value))
-                    elif proptype.isIntervalType:
-                        value = date.Interval(str(self.form[key].value))
-                    elif proptype.isLinkType:
-                        value = str(self.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)
-                    elif proptype.isMultilinkType:
-                        value = self.form[key]
-                        if type(value) != type([]):
-                            value = [i.strip() for i in str(value.value).split(',')]
-                        else:
-                            value = [str(i.value).strip() for i in value]
-                        link = cl.properties[key].classname
-                        l = []
-                        for entry in map(str, value):
-                            if not num_re.match(entry):
-                                try:
-                                    entry = self.db.classes[link].lookup(entry)
-                                except:
-                                    raise ValueError, \
-                                        'property "%s": %s not a %s'%(key,
-                                        entry, link)
-                            l.append(entry)
-                        l.sort()
-                        value = l
-                    # if changed, set it
-                    if value != cl.get(self.nodeid, key):
-                        changed.append(key)
-                        props[key] = value
-                cl.set(self.nodeid, **props)
-
-                # if this item has messages, generate an edit message
-                # TODO: don't send the edit message to the person who
-                # performed the edit
-                if (cl.getprops().has_key('messages') and
-                        cl.getprops()['messages'].isMultilinkType and
-                        cl.getprops()['messages'].classname == 'msg'):
-                    nid = self.nodeid
-                    m = []
-                    for name, prop in cl.getprops().items():
-                        value = cl.get(nid, name)
-                        if prop.isLinkType:
-                            link = self.db.classes[prop.classname]
-                            key = link.getkey()
-                            if value is not None and key:
-                                value = link.get(value, key)
-                            else:
-                                value = '-'
-                        elif prop.isMultilinkType:
-                            l = []
-                            link = self.db.classes[prop.classname]
-                            for entry in value:
-                                key = link.getkey()
-                                if key:
-                                    l.append(link.get(entry, link.getkey()))
-                                else:
-                                    l.append(entry)
-                            value = ', '.join(l)
-                        if name in changed:
-                            chg = '*'
-                        else:
-                            chg = ' '
-                        m.append('%s %s: %s'%(chg, name, value))
-
-                    # handle the note
-                    if self.form.has_key('__note'):
-                        note = self.form['__note'].value
-                        if '\n' in note:
-                            summary = re.split(r'\n\r?', note)[0]
-                        else:
-                            summary = note
-                        m.insert(0, '%s\n\n'%note)
-                    else:
-                        if len(changed) > 1:
-                            plural = 's were'
-                        else:
-                            plural = ' was'
-                        summary = 'This %s has been edited through the web '\
-                            'and the %s value%s changed.'%(cn,
-                            ', '.join(changed), plural)
-                        m.insert(0, '%s\n\n'%summary)
-
-                    # now create the message
-                    content = '\n'.join(m)
-                    message_id = self.db.msg.create(author=1, recipients=[],
-                        date=date.Date('.'), summary=summary, content=content)
-                    messages = cl.get(nid, 'messages')
-                    messages.append(message_id)
-                    props = {'messages': messages}
-                    cl.set(nid, **props)
-
-                # and some nice 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())
-
-        # now the display
-        id = self.nodeid
-        if cl.getkey():
-            id = cl.get(id, cl.getkey())
-        self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
-
-        nodeid = self.nodeid
-
-        # use the template to display the item
-        template.item(self, self.db, self.classname, nodeid)
-        self.pagefoot()
-    showissue = showitem
-    showmsg = showitem
-
-    def newissue(self, message=None):
-        ''' add an issue
-        '''
-        cn = self.classname
-        cl = self.db.classes[cn]
-
-        # possibly perform a create
-        keys = self.form.keys()
-        num_re = re.compile('^\d+$')
-        if keys:
-            props = {}
-            try:
-                keys = self.form.keys()
-                for key in keys:
-                    if not cl.properties.has_key(key):
-                        continue
-                    proptype = cl.properties[key]
-                    if proptype.isStringType:
-                        value = str(self.form[key].value).strip()
-                    elif proptype.isDateType:
-                        value = date.Date(str(self.form[key].value))
-                    elif proptype.isIntervalType:
-                        value = date.Interval(str(self.form[key].value))
-                    elif proptype.isLinkType:
-                        value = str(self.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)
-                    elif proptype.isMultilinkType:
-                        value = self.form[key]
-                        if type(value) != type([]):
-                            value = [i.strip() for i in str(value.value).split(',')]
-                        else:
-                            value = [str(i.value).strip() for i in value]
-                        link = cl.properties[key].classname
-                        l = []
-                        for entry in map(str, value):
-                            if not num_re.match(entry):
-                                try:
-                                    entry = self.db.classes[link].lookup(entry)
-                                except:
-                                    raise ValueError, \
-                                        'property "%s": %s not a %s'%(key,
-                                        entry, link)
-                            l.append(entry)
-                        l.sort()
-                        value = l
-                    props[key] = value
-                nid = cl.create(**props)
-
-                # if this item has messages, 
-                if (cl.getprops().has_key('messages') and
-                        cl.getprops()['messages'].isMultilinkType and
-                        cl.getprops()['messages'].classname == 'msg'):
-                    # generate an edit message - nosyreactor will send it
-                    m = []
-                    for name, prop in cl.getprops().items():
-                        value = cl.get(nid, name)
-                        if prop.isLinkType:
-                            link = self.db.classes[prop.classname]
-                            key = link.getkey()
-                            if value is not None and key:
-                                value = link.get(value, key)
-                            else:
-                                value = '-'
-                        elif prop.isMultilinkType:
-                            l = []
-                            link = self.db.classes[prop.classname]
-                            for entry in value:
-                                key = link.getkey()
-                                if key:
-                                    l.append(link.get(entry, link.getkey()))
-                                else:
-                                    l.append(entry)
-                            value = ', '.join(l)
-                        m.append('%s: %s'%(name, value))
-
-                    # handle the note
-                    if self.form.has_key('__note'):
-                        note = self.form['__note'].value
-                        if '\n' in note:
-                            summary = re.split(r'\n\r?', note)[0]
-                        else:
-                            summary = note
-                        m.append('\n%s\n'%note)
-                    else:
-                        m.append('\nThis %s has been created through '
-                            'the web.\n'%cn)
-
-                    # now create the message
-                    content = '\n'.join(m)
-                    message_id = self.db.msg.create(author=1, recipients=[],
-                        date=date.Date('.'), summary=summary, content=content)
-                    messages = cl.get(nid, 'messages')
-                    messages.append(message_id)
-                    props = {'messages': messages}
-                    cl.set(nid, **props)
-
-                # and some nice feedback for the user
-                message = '%s created ok'%cn
-            except:
-                s = StringIO.StringIO()
-                traceback.print_exc(None, s)
-                message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
-        self.pagehead('New %s'%self.classname.capitalize(), message)
-        template.newitem(self, self.db, self.classname, self.form)
-        self.pagefoot()
-
-    def showuser(self, message=None):
-        ''' display an item
-        '''
-        if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
-            self.showitem(message)
-        else:
-            raise Unauthorised
-
-    def showfile(self):
-        ''' display a file
-        '''
-        nodeid = self.nodeid
-        cl = self.db.file
-        type = cl.get(nodeid, 'type')
-        if type == 'message/rfc822':
-            type = 'text/plain'
-        self.header(headers={'Content-Type': type})
-        self.write(cl.get(nodeid, 'content'))
-
-    def classes(self, message=None):
-        ''' display a list of all the classes in the database
-        '''
-        if self.user == 'admin':
-            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>%s</th></tr>'%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()
-        else:
-            raise Unauthorised
-
-    def main(self, dre=re.compile(r'([^\d]+)(\d+)'),
-            nre=re.compile(r'new(\w+)')):
-        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
-            m = dre.match(path[0])
-            if m:
-                self.classname = m.group(1)
-                self.nodeid = m.group(2)
-                getattr(self, 'show%s'%self.classname)()
-                return
-            m = nre.match(path[0])
-            if m:
-                self.classname = m.group(1)
-                getattr(self, 'new%s'%self.classname)()
-                return
-            self.classname = path[0]
-            self.list()
-        else:
-            raise 'ValueError', 'Path not understood'
-
-    def __del__(self):
-        self.db.close()
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.6  2001/07/20 00:53:20  richard
-# Default index now filters out the resolved issues ;)
-#
-# Revision 1.5  2001/07/20 00:17:16  richard
-# Fixed adding a new issue when there is no __note
-#
-# Revision 1.4  2001/07/19 06:27:07  anthonybaxter
-# fixing (manually) the (dollarsign)Log(dollarsign) entries caused by
-# my using the magic (dollarsign)Id(dollarsign) and (dollarsign)Log(dollarsign)
-# strings in a commit message. I'm a twonk.
-#
-# Also broke the help string in two.
-#
-# Revision 1.3  2001/07/19 05:52:22  anthonybaxter
-# Added CVS keywords Id and Log to all python files.
-#
-#
-
diff --git a/roundupdb.py b/roundupdb.py
deleted file mode 100644 (file)
index 356abb2..0000000
+++ /dev/null
@@ -1,394 +0,0 @@
-# $Id: roundupdb.py,v 1.6 2001-07-20 07:35:55 richard Exp $
-
-import re, os, smtplib, socket
-
-import config, hyperdb, date
-
-def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
-    ''' Take a foo123 and return ('foo', 123)
-    '''
-    m = dre.match(designator)
-    return m.group(1), m.group(2)
-
-class Database(hyperdb.Database):
-    def getuid(self):
-        """Return the id of the "user" node associated with the user
-        that owns this connection to the hyperdatabase."""
-        return self.user.lookup(self.journaltag)
-
-    def uidFromAddress(self, address):
-        ''' address is from the rfc822 module, and therefore is (name, addr)
-
-            user is created if they don't exist in the db already
-        '''
-        (realname, address) = address
-        users = self.user.stringFind(address=address)
-        if users: return users[0]
-        return self.user.create(username=address, address=address,
-            realname=realname)
-
-class Class(hyperdb.Class):
-    # Overridden methods:
-    def __init__(self, db, classname, **properties):
-        hyperdb.Class.__init__(self, db, classname, **properties)
-        self.auditors = {'create': [], 'set': [], 'retire': []}
-        self.reactors = {'create': [], 'set': [], 'retire': []}
-
-    def create(self, **propvalues):
-        """These operations trigger detectors and can be vetoed.  Attempts
-        to modify the "creation" or "activity" properties cause a KeyError.
-        """
-        if propvalues.has_key('creation') or propvalues.has_key('activity'):
-            raise KeyError, '"creation" and "activity" are reserved'
-        for audit in self.auditors['create']:
-            audit(self.db, self, None, propvalues)
-        nodeid = hyperdb.Class.create(self, **propvalues)
-        for react in self.reactors['create']:
-            react(self.db, self, nodeid, None)
-        return nodeid
-
-    def set(self, nodeid, **propvalues):
-        """These operations trigger detectors and can be vetoed.  Attempts
-        to modify the "creation" or "activity" properties cause a KeyError.
-        """
-        if propvalues.has_key('creation') or propvalues.has_key('activity'):
-            raise KeyError, '"creation" and "activity" are reserved'
-        for audit in self.auditors['set']:
-            audit(self.db, self, nodeid, propvalues)
-        oldvalues = self.db.getnode(self.classname, nodeid)
-        hyperdb.Class.set(self, nodeid, **propvalues)
-        for react in self.reactors['set']:
-            react(self.db, self, nodeid, oldvalues)
-
-    def retire(self, nodeid):
-        """These operations trigger detectors and can be vetoed.  Attempts
-        to modify the "creation" or "activity" properties cause a KeyError.
-        """
-        for audit in self.auditors['retire']:
-            audit(self.db, self, nodeid, None)
-        hyperdb.Class.retire(self, nodeid)
-        for react in self.reactors['retire']:
-            react(self.db, self, nodeid, None)
-
-    # New methods:
-
-    def audit(self, event, detector):
-        """Register a detector
-        """
-        self.auditors[event].append(detector)
-
-    def react(self, event, detector):
-        """Register a detector
-        """
-        self.reactors[event].append(detector)
-
-class FileClass(Class):
-    def create(self, **propvalues):
-        ''' snaffle the file propvalue and store in a file
-        '''
-        content = propvalues['content']
-        del propvalues['content']
-        newid = Class.create(self, **propvalues)
-        self.setcontent(self.classname, newid, content)
-        return newid
-
-    def filename(self, classname, nodeid):
-        # TODO: split into multiple files directories
-        return os.path.join(self.db.dir, 'files', '%s%s'%(classname, nodeid))
-
-    def setcontent(self, classname, nodeid, content):
-        ''' set the content file for this file
-        '''
-        open(self.filename(classname, nodeid), 'wb').write(content)
-
-    def getcontent(self, classname, nodeid):
-        ''' get the content file for this file
-        '''
-        return open(self.filename(classname, nodeid), 'rb').read()
-
-    def get(self, nodeid, propname):
-        ''' trap the content propname and get it from the file
-        '''
-        if propname == 'content':
-            return self.getcontent(self.classname, nodeid)
-        return Class.get(self, nodeid, propname)
-
-    def getprops(self):
-        ''' In addition to the actual properties on the node, these methods
-            provide the "content" property.
-        '''
-        d = Class.getprops(self).copy()
-        d['content'] = hyperdb.String()
-        return d
-
-# XXX deviation from spec
-class IssueClass(Class):
-    # Overridden methods:
-
-    def __init__(self, db, classname, **properties):
-        """The newly-created class automatically includes the "messages",
-        "files", "nosy", and "superseder" properties.  If the 'properties'
-        dictionary attempts to specify any of these properties or a
-        "creation" or "activity" property, a ValueError is raised."""
-        if not properties.has_key('title'):
-            properties['title'] = hyperdb.String()
-        if not properties.has_key('messages'):
-            properties['messages'] = hyperdb.Multilink("msg")
-        if not properties.has_key('files'):
-            properties['files'] = hyperdb.Multilink("file")
-        if not properties.has_key('nosy'):
-            properties['nosy'] = hyperdb.Multilink("user")
-        if not properties.has_key('superseder'):
-            properties['superseder'] = hyperdb.Multilink("issue")
-        if (properties.has_key('creation') or properties.has_key('activity')
-                or properties.has_key('creator')):
-            raise ValueError, '"creation", "activity" and "creator" are reserved'
-        Class.__init__(self, db, classname, **properties)
-
-    def get(self, nodeid, propname):
-        if propname == 'creation':
-            return self.db.getjournal(self.classname, nodeid)[0][1]
-        if propname == 'activity':
-            return self.db.getjournal(self.classname, nodeid)[-1][1]
-        if propname == 'creator':
-            name = self.db.getjournal(self.classname, nodeid)[0][2]
-            return self.db.user.lookup(name)
-        return Class.get(self, nodeid, propname)
-
-    def getprops(self):
-        """In addition to the actual properties on the node, these
-        methods provide the "creation" and "activity" properties."""
-        d = Class.getprops(self).copy()
-        d['creation'] = hyperdb.Date()
-        d['activity'] = hyperdb.Date()
-        d['creator'] = hyperdb.Link("user")
-        return d
-
-    # New methods:
-
-    def addmessage(self, nodeid, summary, text):
-        """Add a message to an issue's mail spool.
-
-        A new "msg" node is constructed using the current date, the user that
-        owns the database connection as the author, and the specified summary
-        text.
-
-        The "files" and "recipients" fields are left empty.
-
-        The given text is saved as the body of the message and the node is
-        appended to the "messages" field of the specified issue.
-        """
-
-    def sendmessage(self, nodeid, msgid):
-        """Send a message to the members of an issue's nosy list.
-
-        The message is sent only to users on the nosy list who are not
-        already on the "recipients" list for the message.
-        
-        These users are then added to the message's "recipients" list.
-        """
-        # figure the recipient ids
-        recipients = self.db.msg.get(msgid, 'recipients')
-        r = {}
-        for recipid in recipients:
-            r[recipid] = 1
-        authid = self.db.msg.get(msgid, 'author')
-        r[authid] = 1
-
-        # now figure the nosy people who weren't recipients
-        sendto = []
-        nosy = self.get(nodeid, 'nosy')
-        for nosyid in nosy:
-            if not r.has_key(nosyid):
-                sendto.append(nosyid)
-                recipients.append(nosyid)
-
-        if sendto:
-            # update the message's recipients list
-            self.db.msg.set(msgid, recipients=recipients)
-
-            # send an email to the people who missed out
-            sendto = [self.db.user.get(i, 'address') for i in recipients]
-            cn = self.classname
-            title = self.get(nodeid, 'title') or '%s message copy'%cn
-            m = ['Subject: [%s%s] %s'%(cn, nodeid, title)]
-            m.append('To: %s'%', '.join(sendto))
-            m.append('Reply-To: %s'%config.ISSUE_TRACKER_EMAIL)
-            m.append('')
-            m.append(self.db.msg.get(msgid, 'content'))
-            # TODO attachments
-            try:
-                smtp = smtplib.SMTP(config.MAILHOST)
-                smtp.sendmail(config.ISSUE_TRACKER_EMAIL, sendto, '\n'.join(m))
-            except socket.error, value:
-                return "Couldn't send confirmation email: mailhost %s"%value
-            except smtplib.SMTPException, value:
-                return "Couldn't send confirmation email: %s"%value
-
-def nosyreaction(db, cl, nodeid, oldvalues):
-    ''' A standard detector is provided that watches for additions to the
-        "messages" property.
-        
-        When a new message is added, the detector sends it to all the users on
-        the "nosy" list for the issue that are not already on the "recipients"
-        list of the message.
-        
-        Those users are then appended to the "recipients" property on the
-        message, so multiple copies of a message are never sent to the same
-        user.
-        
-        The journal recorded by the hyperdatabase on the "recipients" property
-        then provides a log of when the message was sent to whom. 
-    '''
-    messages = []
-    if oldvalues is None:
-        # the action was a create, so use all the messages in the create
-        messages = cl.get(nodeid, 'messages')
-    elif oldvalues.has_key('messages'):
-        # the action was a set (so adding new messages to an existing issue)
-        m = {}
-        for msgid in oldvalues['messages']:
-            m[msgid] = 1
-        messages = []
-        # figure which of the messages now on the issue weren't there before
-        for msgid in cl.get(nodeid, 'messages'):
-            if not m.has_key(msgid):
-                messages.append(msgid)
-    if not messages:
-        return
-
-    # send a copy to the nosy list
-    for msgid in messages:
-        cl.sendmessage(nodeid, msgid)
-
-    # update the nosy list with the recipients from the new messages
-    nosy = cl.get(nodeid, 'nosy')
-    n = {}
-    for nosyid in nosy: n[nosyid] = 1
-    change = 0
-    # but don't add admin to the nosy list
-    for msgid in messages:
-        for recipid in db.msg.get(msgid, 'recipients'):
-            if recipid != '1' and not n.has_key(recipid):
-                change = 1
-                nosy.append(recipid)
-        authid = db.msg.get(msgid, 'author')
-        if authid != '1' and not n.has_key(authid):
-            change = 1
-            nosy.append(authid)
-    if change:
-        cl.set(nodeid, nosy=nosy)
-
-def openDB(storagelocator, name=None, password=None):
-    ''' Open the Roundup DB
-
-        ... configs up all the classes etc
-    '''
-    db = Database(storagelocator, name)
-    pri = Class(db, "priority", name=hyperdb.String(), order=hyperdb.String())
-    pri.setkey("name")
-    stat = Class(db, "status", name=hyperdb.String(), order=hyperdb.String())
-    stat.setkey("name")
-    Class(db, "keyword", name=hyperdb.String())
-    user = Class(db, "user", username=hyperdb.String(),
-        password=hyperdb.String(), address=hyperdb.String(),
-        realname=hyperdb.String(), phone=hyperdb.String(),
-        organisation=hyperdb.String())
-    user.setkey("username")
-    msg = FileClass(db, "msg", author=hyperdb.Link("user"),
-        recipients=hyperdb.Multilink("user"), date=hyperdb.Date(),
-        summary=hyperdb.String(), files=hyperdb.Multilink("file"))
-    file = FileClass(db, "file", name=hyperdb.String(), type=hyperdb.String())
-
-    # bugs and support calls etc
-    rate = Class(db, "rate", name=hyperdb.String(), order=hyperdb.String())
-    rate.setkey("name")
-    source = Class(db, "source", name=hyperdb.String(), order=hyperdb.String())
-    source.setkey("name")
-    platform = Class(db, "platform", name=hyperdb.String(), order=hyperdb.String())
-    platform.setkey("name")
-    product = Class(db, "product", name=hyperdb.String(), order=hyperdb.String())
-    product.setkey("name")
-    Class(db, "timelog", date=hyperdb.Date(), time=hyperdb.String(),
-        performedby=hyperdb.Link("user"), description=hyperdb.String())
-    issue = IssueClass(db, "issue", assignedto=hyperdb.Link("user"),
-        priority=hyperdb.Link("priority"), status=hyperdb.Link("status"),
-        rate=hyperdb.Link("rate"), source=hyperdb.Link("source"),
-        product=hyperdb.Link("product"), platform=hyperdb.Multilink("platform"),
-        version=hyperdb.String(),
-        timelog=hyperdb.Multilink("timelog"), customername=hyperdb.String())
-    issue.setkey('title')
-    issue.react('create', nosyreaction)
-    issue.react('set', nosyreaction)
-    return db
-
-def initDB(storagelocator, password):
-    ''' Initialise the Roundup DB for use
-    '''
-    dbdir = os.path.join(storagelocator, 'files')
-    if not os.path.isdir(dbdir):
-        os.makedirs(dbdir)
-    db = openDB(storagelocator, "admin")
-    db.clear()
-    pri = db.getclass('priority')
-    pri.create(name="fatal-bug", order="1")
-    pri.create(name="bug", order="2")
-    pri.create(name="usability", order="3")
-    pri.create(name="feature", order="4")
-    pri.create(name="support", order="5")
-
-    stat = db.getclass('status')
-    stat.create(name="unread", order="1")
-    stat.create(name="deferred", order="2")
-    stat.create(name="chatting", order="3")
-    stat.create(name="need-eg", order="4")
-    stat.create(name="in-progress", order="5")
-    stat.create(name="testing", order="6")
-    stat.create(name="done-cbb", order="7")
-    stat.create(name="resolved", order="8")
-
-    rate = db.getclass("rate")
-    rate.create(name='basic', order="1")
-    rate.create(name='premium', order="2")
-    rate.create(name='internal', order="3")
-
-    source = db.getclass("source")
-    source.create(name='phone', order="1")
-    source.create(name='e-mail', order="2")
-    source.create(name='internal', order="3")
-    source.create(name='internal-qa', order="4")
-
-    platform = db.getclass("platform")
-    platform.create(name='linux', order="1")
-    platform.create(name='windows', order="2")
-    platform.create(name='mac', order="3")
-
-    product = db.getclass("product")
-    product.create(name='Bizar Shop', order="1")
-    product.create(name='Bizar Shop Developer', order="2")
-    product.create(name='Bizar Shop Manual', order="3")
-    product.create(name='Bizar Shop Developer Manual', order="4")
-
-    user = db.getclass('user')
-    user.create(username="admin", password=password, address=config.ADMIN_EMAIL)
-
-    db.close()
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.5  2001/07/20 00:22:50  richard
-# Priority list changes - removed the redundant TODO and added support. See
-# roundup-devel for details.
-#
-# Revision 1.4  2001/07/19 06:27:07  anthonybaxter
-# fixing (manually) the (dollarsign)Log(dollarsign) entries caused by
-# my using the magic (dollarsign)Id(dollarsign) and (dollarsign)Log(dollarsign)
-# strings in a commit message. I'm a twonk.
-#
-# Also broke the help string in two.
-#
-# Revision 1.3  2001/07/19 05:52:22  anthonybaxter
-# Added CVS keywords Id and Log to all python files.
-#
-#
-
diff --git a/server.py b/server.py
deleted file mode 100755 (executable)
index 1e4eb83..0000000
--- a/server.py
+++ /dev/null
@@ -1,173 +0,0 @@
-#!/usr/bin/python
-""" HTTP Server that serves roundup.
-
-Stolen from CGIHTTPServer
-
-$Id: server.py,v 1.5 2001-07-20 12:33:06 richard Exp $
-
-"""
-import sys
-if int(sys.version[0]) < 2:
-    print "Content-Type: text/plain\n"
-    print "Roundup requires Python 2.0 or newer."
-
-__version__ = "0.1"
-
-__all__ = ["RoundupRequestHandler"]
-
-import os, urllib, StringIO, traceback, cgi, binascii
-import BaseHTTPServer
-import SimpleHTTPServer
-import date, hyperdb, template, roundupdb, roundup_cgi
-import cgitb
-
-class RoundupRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
-    def send_head(self):
-        """Version of send_head that support CGI scripts"""
-        return self.run_cgi()
-
-    def run_cgi(self):
-        """Execute a CGI script."""
-        rest = self.path
-        i = rest.rfind('?')
-        if i >= 0:
-            rest, query = rest[:i], rest[i+1:]
-        else:
-            query = ''
-
-        # Set up the CGI environment
-        env = {}
-        env['REQUEST_METHOD'] = self.command
-        env['PATH_INFO'] = urllib.unquote(rest)
-        if query:
-            env['QUERY_STRING'] = query
-        host = self.address_string()
-        if self.headers.typeheader is None:
-            env['CONTENT_TYPE'] = self.headers.type
-        else:
-            env['CONTENT_TYPE'] = self.headers.typeheader
-        length = self.headers.getheader('content-length')
-        if length:
-            env['CONTENT_LENGTH'] = length
-        co = filter(None, self.headers.getheaders('cookie'))
-        if co:
-            env['HTTP_COOKIE'] = ', '.join(co)
-        env['SCRIPT_NAME'] = ''
-        env['SERVER_NAME'] = self.server.server_name
-        env['SERVER_PORT'] = str(self.server.server_port)
-
-        decoded_query = query.replace('+', ' ')
-
-        # if root, setuid to nobody
-        if not os.getuid():
-            nobody = nobody_uid()
-            os.setuid(nobody)
-
-        # TODO check for file timestamp changes
-        reload(date)
-        reload(hyperdb)
-        reload(roundupdb)
-        reload(template)
-        reload(roundup_cgi)
-
-        # initialise the roundupdb, check for auth
-        db = roundupdb.openDB('db', '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
-        save_stdin = sys.stdin
-        try:
-            sys.stdin = self.rfile
-            client = roundup_cgi.Client(self.wfile, env, user)
-            client.main()
-        except roundup_cgi.Unauthorised:
-            self.wfile.write('Content-Type: text/html\n')
-            self.wfile.write('Status: 403\n')
-            self.wfile.write('Unauthorised')
-        except:
-            try:
-                reload(cgitb)
-                self.wfile.write(cgitb.breaker())
-                self.wfile.write(cgitb.html())
-            except:
-                self.wfile.write("Content-Type: text/html\n\n")
-                self.wfile.write("<pre>")
-                s = StringIO.StringIO()
-                traceback.print_exc(None, s)
-                self.wfile.write(cgi.escape(s.getvalue()))
-                self.wfile.write("</pre>\n")
-        sys.stdin = save_stdin
-    do_POST = run_cgi
-
-
-nobody = None
-
-def nobody_uid():
-    """Internal routine to get nobody's uid"""
-    global nobody
-    if nobody:
-        return nobody
-    try:
-        import pwd
-    except ImportError:
-        return -1
-    try:
-        nobody = pwd.getpwnam('nobody')[2]
-    except KeyError:
-        nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
-    return nobody
-
-def main():
-    from config import HTTP_HOST, HTTP_PORT
-    address = (HTTP_HOST, HTTP_PORT)
-    httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
-    print 'Roundup server started on', address
-    httpd.serve_forever()
-
-if __name__ == '__main__':
-    main()
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.4  2001/07/19 10:43:01  anthonybaxter
-# HTTP_HOST and HTTP_PORT config options.
-#
-# Revision 1.3  2001/07/19 06:27:07  anthonybaxter
-# fixing (manually) the (dollarsign)Log(dollarsign) entries caused by
-# my using the magic (dollarsign)Id(dollarsign) and (dollarsign)Log(dollarsign)
-# strings in a commit message. I'm a twonk.
-#
-# Also broke the help string in two.
-#
-# Revision 1.2  2001/07/19 05:52:22  anthonybaxter
-# Added CVS keywords Id and Log to all python files.
-#
-#
-
diff --git a/style.css b/style.css
deleted file mode 100644 (file)
index 2316c7c..0000000
--- a/style.css
+++ /dev/null
@@ -1,163 +0,0 @@
-h1 {
-  font-family: Verdana, Helvetica, sans-serif; 
-  font-size: 18pt; 
-  font-weight: bold; 
-}
-
-h2 {
-  font-family: Verdana, Helvetica, sans-serif; 
-  font-size: 16pt; 
-  font-weight: bold; 
-}
-
-h3 {
-  font-family: Verdana, Helvetica, sans-serif; 
-  font-size: 12pt; 
-  font-weight: bold; 
-}
-
-a:hover {  
-  font-family: Verdana, Helvetica, sans-serif; 
-  text-decoration: underline;
-  color: #333333; 
-}
-
-a:link {
-  font-family: Verdana, Helvetica, sans-serif; 
-  text-decoration: none;
-  color: #000099;
-}
-
-a {
-  font-family: Verdana, Helvetica, sans-serif; 
-  text-decoration: none;
-  color: #000099;
-}
-
-p {
-  font-family: Verdana, Helvetica, sans-serif;
-  font-size: 10pt;
-  color: #333333;
-}
-
-th {
-  font-family: Verdana, Helvetica, sans-serif; 
-  font-weight: bold;
-  font-size: 10pt; 
-  color: #333333;
-}
-
-.form-help {
-  font-family: Verdana, Helvetica, sans-serif;
-  font-size: 10pt;
-  color: #333333;
-}
-
-.std-text {
-  font-family: Verdana, Helvetica, sans-serif;
-  font-size: 10pt;
-  color: #333333;
-}
-
-.tab-small {
-  font-family: Verdana, Helvetica, sans-serif; 
-  font-size: 8pt; 
-  color: #333333;
-}
-
-.location-bar {
-  background-color: #efefef;
-  border: none;
-}
-
-.strong-header {
-  font-family: Verdana, Helvetica, sans-serif;
-  font-size: 12pt;
-  font-weight: bold;
-  background-color: #000000;
-  color: #ffffff;
-}
-
-.list-header {
-  background-color: #c0c0c0;
-  border: none;
-}
-
-.list-item {
-  font-family: Verdana, Helvetica, sans-serif; 
-  font-size: 10pt; 
-}
-
-.list-nav {
-  font-family: Verdana, Helvetica, sans-serif; 
-  font-size: 10pt; 
-  font-weight: bold;
-}
-
-.row-normal {
-  background-color: #ffffff;
-  border: none;
-
-}
-
-.row-hilite {
-  background-color: #efefef;
-  border: none;
-}
-
-.section-bar {
-  background-color: #c0c0c0;
-  border: none;
-}
-
-.system-msg {
-  font-family: Verdana, Helvetica, sans-serif; 
-  font-size: 10pt; 
-  background-color: #ffffff;
-  border:  1px solid #000000;
-  margin-bottom: 6px;
-  margin-top: 6px;
-  padding: 4px;
-  width: 100%;
-  color: #660033;
-}
-
-.form-title {
-  font-family: Verdana, Helvetica, sans-serif; 
-  font-weight: bold;
-  font-size: 12pt; 
-  color: #333333;
-}
-
-.form-label {
-  font-family: Verdana, Helvetica, sans-serif; 
-  font-weight: bold;
-  font-size: 10pt; 
-  color: #333333;
-}
-
-.form-optional {
-  font-family: Verdana, Helvetica, sans-serif; 
-  font-weight: bold;
-  font-style: italic;
-  font-size: 10pt; 
-  color: #333333;
-}
-
-.form-element {
-  font-family: Verdana, Helvetica, aans-serif;
-  font-size: 10pt;
-  color: #000000;
-}
-
-.form-text {
-  font-family: Verdana, Helvetica, sans-serif;
-  font-size: 10pt;
-  color: #333333;
-}
-
-.form-mono {
-  font-family: monospace;
-  font-size: 12px;
-  text-decoration: none;
-}
diff --git a/template.py b/template.py
deleted file mode 100644 (file)
index 85ddc71..0000000
+++ /dev/null
@@ -1,714 +0,0 @@
-# $Id: template.py,v 1.5 2001-07-20 07:34:43 richard Exp $
-
-import os, re, StringIO, urllib, cgi
-
-import hyperdb, date
-
-class Base:
-    def __init__(self, db, classname, nodeid=None, form=None):
-        self.db, self.classname, self.nodeid = db, classname, nodeid
-        self.form = form
-        self.cl = self.db.classes[self.classname]
-        self.properties = self.cl.getprops()
-
-class Plain(Base):
-    ''' 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)
-    '''
-    def __call__(self, property):
-        if not self.nodeid and self.form is None:
-            return '[Field: not called from item]'
-        propclass = self.properties[property]
-        if self.nodeid:
-            value = self.cl.get(self.nodeid, property)
-        else:
-            # TODO: pull the value from the form
-            if propclass.isMultilinkType: value = []
-            else: value = ''
-        if propclass.isStringType:
-            if value is None: value = ''
-            else: value = str(value)
-        elif propclass.isDateType:
-            value = str(value)
-        elif propclass.isIntervalType:
-            value = str(value)
-        elif propclass.isLinkType:
-            linkcl = self.db.classes[propclass.classname]
-            if value: value = str(linkcl.get(value, linkcl.getkey()))
-            else: value = '[unselected]'
-        elif propclass.isMultilinkType:
-            linkcl = self.db.classes[propclass.classname]
-            k = linkcl.getkey()
-            value = ', '.join([linkcl.get(i, k) for i in value])
-        else:
-            s = 'Plain: bad propclass "%s"'%propclass
-        return value
-
-class Field(Base):
-    ''' display a property like the plain displayer, but in a text field
-        to be edited
-    '''
-    def __call__(self, property, size=None, height=None, showid=0):
-        if not self.nodeid and self.form is None:
-            return '[Field: not called from item]'
-        propclass = self.properties[property]
-        if self.nodeid:
-            value = self.cl.get(self.nodeid, property)
-        else:
-            # TODO: pull the value from the form
-            if propclass.isMultilinkType: value = []
-            else: value = ''
-        if (propclass.isStringType or propclass.isDateType or
-                propclass.isIntervalType):
-            size = size or 30
-            if value is None:
-                value = ''
-            else:
-                value = cgi.escape(value)
-                value = '&quot;'.join(value.split('"'))
-            s = '<input name="%s" value="%s" size="%s">'%(property, value, size)
-        elif propclass.isLinkType:
-            linkcl = self.db.classes[propclass.classname]
-            l = ['<select name="%s">'%property]
-            k = linkcl.getkey()
-            for optionid in linkcl.list():
-                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] + '...'
-                l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
-            l.append('</select>')
-            s = '\n'.join(l)
-        elif propclass.isMultilinkType:
-            linkcl = self.db.classes[propclass.classname]
-            list = linkcl.list()
-            height = height or min(len(list), 7)
-            l = ['<select multiple name="%s" size="%s">'%(property, height)]
-            k = linkcl.getkey()
-            for optionid in list:
-                option = linkcl.get(optionid, k)
-                s = ''
-                if optionid 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] + '...'
-                l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
-            l.append('</select>')
-            s = '\n'.join(l)
-        else:
-            s = 'Plain: bad propclass "%s"'%propclass
-        return s
-
-class Menu(Base):
-    ''' for a Link property, display a menu of the available choices
-    '''
-    def __call__(self, property, size=None, height=None, showid=0):
-        propclass = self.properties[property]
-        if self.nodeid:
-            value = self.cl.get(self.nodeid, property)
-        else:
-            # TODO: pull the value from the form
-            if propclass.isMultilinkType: value = []
-            else: value = None
-        if propclass.isLinkType:
-            linkcl = self.db.classes[propclass.classname]
-            l = ['<select name="%s">'%property]
-            k = linkcl.getkey()
-            for optionid in linkcl.list():
-                option = linkcl.get(optionid, k)
-                s = ''
-                if optionid == value:
-                    s = 'selected '
-                l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
-            l.append('</select>')
-            return '\n'.join(l)
-        if propclass.isMultilinkType:
-            linkcl = self.db.classes[propclass.classname]
-            list = linkcl.list()
-            height = height or min(len(list), 7)
-            l = ['<select multiple name="%s" size="%s">'%(property, height)]
-            k = linkcl.getkey()
-            for optionid in list:
-                option = linkcl.get(optionid, k)
-                s = ''
-                if optionid 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] + '...'
-                l.append('<option %svalue="%s">%s</option>'%(s, optionid, option))
-            l.append('</select>')
-            return '\n'.join(l)
-        return '[Menu: not a link]'
-
-#XXX deviates from spec
-class Link(Base):
-    ''' 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
-    '''
-    def __call__(self, property=None, **args):
-        if not self.nodeid and self.form is None:
-            return '[Link: not called from item]'
-        propclass = self.properties[property]
-        if self.nodeid:
-            value = self.cl.get(self.nodeid, property)
-        else:
-            if propclass.isMultilinkType: value = []
-            else: value = ''
-        if propclass.isLinkType:
-            linkcl = self.db.classes[propclass.classname]
-            linkvalue = linkcl.get(value, k)
-            return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
-        if propclass.isMultilinkType:
-            linkcl = self.db.classes[propclass.classname]
-            l = []
-            for value in value:
-                linkvalue = linkcl.get(value, k)
-                l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
-            return ', '.join(l)
-        return '<a href="%s%s">%s</a>'%(self.classname, self.nodeid, value)
-
-class Count(Base):
-    ''' for a Multilink property, display a count of the number of links in
-        the list
-    '''
-    def __call__(self, property, **args):
-        if not self.nodeid:
-            return '[Count: not called from item]'
-        propclass = self.properties[property]
-        value = self.cl.get(self.nodeid, property)
-        if propclass.isMultilinkType:
-            return str(len(value))
-        return '[Count: not a Multilink]'
-
-# XXX pretty is definitely new ;)
-class Reldate(Base):
-    ''' 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
-    '''
-    def __call__(self, property, pretty=0):
-        if not self.nodeid and self.form is None:
-            return '[Reldate: not called from item]'
-        propclass = self.properties[property]
-        if not propclass.isDateType:
-            return '[Reldate: not a Date]'
-        if self.nodeid:
-            value = self.cl.get(self.nodeid, property)
-        else:
-            value = date.Date('.')
-        interval = value - date.Date('.')
-        if pretty:
-            if not self.nodeid:
-                return 'now'
-            pretty = interval.pretty()
-            if pretty is None:
-                pretty = value.pretty()
-            return pretty
-        return str(interval)
-
-class Download(Base):
-    ''' show a Link("file") or Multilink("file") property using links that
-        allow you to download files
-    '''
-    def __call__(self, property, **args):
-        if not self.nodeid:
-            return '[Download: not called from item]'
-        propclass = self.properties[property]
-        value = self.cl.get(self.nodeid, property)
-        if propclass.isLinkType:
-            linkcl = self.db.classes[propclass.classname]
-            linkvalue = linkcl.get(value, k)
-            return '<a href="%s%s">%s</a>'%(linkcl, value, linkvalue)
-        if propclass.isMultilinkType:
-            linkcl = self.db.classes[propclass.classname]
-            l = []
-            for value in value:
-                linkvalue = linkcl.get(value, k)
-                l.append('<a href="%s%s">%s</a>'%(linkcl, value, linkvalue))
-            return ', '.join(l)
-        return '[Download: not a link]'
-
-
-class Checklist(Base):
-    ''' for a Link or Multilink property, display checkboxes for the available
-        choices to permit filtering
-    '''
-    def __call__(self, property, **args):
-        propclass = self.properties[property]
-        if self.nodeid:
-            value = self.cl.get(self.nodeid, property)
-        else:
-            value = []
-        if propclass.isLinkType or propclass.isMultilinkType:
-            linkcl = self.db.classes[propclass.classname]
-            l = []
-            k = linkcl.getkey()
-            for optionid in linkcl.list():
-                option = linkcl.get(optionid, k)
-                if optionid in value:
-                    checked = 'checked'
-                else:
-                    checked = ''
-                l.append('%s:<input type="checkbox" %s name="%s" value="%s">'%(
-                    option, checked, propclass.classname, option))
-            return '\n'.join(l)
-        return '[Checklist: not a link]'
-
-class Note(Base):
-    ''' display a "note" field, which is a text area for entering a note to
-        go along with a change. 
-    '''
-    def __call__(self, rows=5, cols=80):
-       # TODO: pull the value from the form
-        return '<textarea name="__note" rows=%s cols=%s></textarea>'%(rows,
-            cols)
-
-# XXX new function
-class List(Base):
-    ''' list the items specified by property using the standard index for
-        the class
-    '''
-    def __call__(self, property, **args):
-        propclass = self.properties[property]
-        if not propclass.isMultilinkType:
-            return '[List: not a Multilink]'
-        fp = StringIO.StringIO()
-        args['show_display_form'] = 0
-        value = self.cl.get(self.nodeid, property)
-        index(fp, self.db, propclass.classname, nodeids=value,
-            show_display_form=0)
-        return fp.getvalue()
-
-# XXX new function
-class History(Base):
-    ''' list the history of the item
-    '''
-    def __call__(self, **args):
-        l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
-            '<tr class="list-header">',
-            '<td><span class="list-item"><strong>Date</strong></span></td>',
-            '<td><span class="list-item"><strong>User</strong></span></td>',
-            '<td><span class="list-item"><strong>Action</strong></span></td>',
-            '<td><span class="list-item"><strong>Args</strong></span></td>']
-
-        for id, date, user, action, args in self.cl.history(self.nodeid):
-            l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
-               date, user, action, args))
-        l.append('</table>')
-        return '\n'.join(l)
-
-# XXX new function
-class Submit(Base):
-    ''' add a submit button for the item
-    '''
-    def __call__(self):
-        if self.nodeid:
-            return '<input type="submit" value="Submit Changes">'
-        elif self.form is not None:
-            return '<input type="submit" value="Submit New Entry">'
-        else:
-            return '[Submit: not called from item]'
-
-
-#
-#   INDEX TEMPLATES
-#
-class IndexTemplateReplace:
-    def __init__(self, globals, locals, props):
-        self.globals = globals
-        self.locals = locals
-        self.props = props
-
-    def go(self, text, replace=re.compile(
-            r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
-            r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
-        return replace.sub(self, text)
-        
-    def __call__(self, m, filter=None, columns=None, sort=None, group=None):
-        if m.group('name'):
-            if m.group('name') in self.props:
-                text = m.group('text')
-                replace = IndexTemplateReplace(self.globals, {}, self.props)
-                return replace.go(m.group('text'))
-            else:
-                return ''
-        if m.group('display'):
-            command = m.group('command')
-            return eval(command, self.globals, self.locals)
-        print '*** unhandled match', m.groupdict()
-
-def sortby(sort_name, columns, filter, sort, group, filterspec):
-    l = []
-    w = l.append
-    for k, v in filterspec.items():
-        k = urllib.quote(k)
-        if type(v) == type([]):
-            w('%s=%s'%(k, ','.join(map(urllib.quote, v))))
-        else:
-            w('%s=%s'%(k, urllib.quote(v)))
-    if columns:
-        w(':columns=%s'%','.join(map(urllib.quote, columns)))
-    if filter:
-        w(':filter=%s'%','.join(map(urllib.quote, filter)))
-    if group:
-        w(':group=%s'%','.join(map(urllib.quote, group)))
-    m = []
-    s_dir = ''
-    for name in sort:
-        dir = name[0]
-        if dir == '-':
-            dir = ''
-        else:
-            name = name[1:]
-        if sort_name == name:
-            if dir == '':
-                s_dir = '-'
-            elif dir == '-':
-                s_dir = ''
-        else:
-            m.append(dir+urllib.quote(name))
-    m.insert(0, s_dir+urllib.quote(sort_name))
-    # so things don't get completely out of hand, limit the sort to two columns
-    w(':sort=%s'%','.join(m[:2]))
-    return '&'.join(l)
-
-def index(fp, db, classname, filterspec={}, filter=[], columns=[], sort=[],
-            group=[], show_display_form=1, nodeids=None,
-            col_re=re.compile(r'<property\s+name="([^>]+)">')):
-
-    globals = {
-        'plain': Plain(db, classname, form={}),
-        'field': Field(db, classname, form={}),
-        'menu': Menu(db, classname, form={}),
-        'link': Link(db, classname, form={}),
-        'count': Count(db, classname, form={}),
-        'reldate': Reldate(db, classname, form={}),
-        'download': Download(db, classname, form={}),
-        'checklist': Checklist(db, classname, form={}),
-        'list': List(db, classname, form={}),
-        'history': History(db, classname, form={}),
-        'submit': Submit(db, classname, form={}),
-        'note': Note(db, classname, form={})
-    }
-    cl = db.classes[classname]
-    properties = cl.getprops()
-    w = fp.write
-
-    try:
-        template = open(os.path.join('templates', classname+'.filter')).read()
-        all_filters = col_re.findall(template)
-    except IOError, error:
-        if error.errno != 2: raise
-        template = None
-        all_filters = []
-    if template and filter:
-        # display the filter section
-        w('<form>')
-        w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
-        w('<tr class="location-bar">')
-        w(' <th align="left" colspan="2">Filter specification...</th>')
-        w('</tr>')
-        replace = IndexTemplateReplace(globals, locals(), filter)
-        w(replace.go(template))
-        if columns:
-            w('<input type="hidden" name=":columns" value="%s">'%','.join(columns))
-        if filter:
-            w('<input type="hidden" name=":filter" value="%s">'%','.join(filter))
-        if sort:
-            w('<input type="hidden" name=":sort" value="%s">'%','.join(sort))
-        if group:
-            w('<input type="hidden" name=":group" value="%s">'%','.join(group))
-        for k, v in filterspec.items():
-            if type(v) == type([]): v = ','.join(v)
-            w('<input type="hidden" name="%s" value="%s">'%(k, v))
-        w('<tr class="location-bar"><td width="1%%">&nbsp;</td>')
-        w('<td><input type="submit" value="Redisplay"></td></tr>')
-        w('</table>')
-        w('</form>')
-
-    # XXX deviate from spec here ...
-    # load the index section template and figure the default columns from it
-    template = open(os.path.join('templates', classname+'.index')).read()
-    all_columns = col_re.findall(template)
-    if not columns:
-        columns = []
-        for name in all_columns:
-            columns.append(name)
-    else:
-        # re-sort columns to be the same order as all_columns
-        l = []
-        for name in all_columns:
-            if name in columns:
-                l.append(name)
-        columns = l
-
-    # now display the index section
-    w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
-    w('<tr class="list-header">')
-    for name in columns:
-        cname = name.capitalize()
-        if show_display_form:
-            anchor = "%s?%s"%(classname, sortby(name, columns, filter,
-                sort, group, filterspec))
-            w('<td><span class="list-item"><a href="%s">%s</a></span></td>'%(
-                anchor, cname))
-        else:
-            w('<td><span class="list-item">%s</span></td>'%cname)
-    w('</tr>')
-
-    # this stuff is used for group headings - optimise the group names
-    old_group = None
-    group_names = []
-    if group:
-        for name in group:
-            if name[0] == '-': group_names.append(name[1:])
-            else: group_names.append(name)
-
-    # now actually loop through all the nodes we get from the filter and
-    # apply the template
-    if nodeids is None:
-        nodeids = cl.filter(filterspec, sort, group)
-    for nodeid in nodeids:
-        # check for a group heading
-        if group_names:
-            this_group = [cl.get(nodeid, name) for name in group_names]
-            if this_group != old_group:
-                l = []
-                for name in group_names:
-                    prop = properties[name]
-                    if prop.isLinkType:
-                        group_cl = db.classes[prop.classname]
-                        key = group_cl.getkey()
-                        value = cl.get(nodeid, name)
-                        if value is None:
-                            l.append('[unselected %s]'%prop.classname)
-                        else:
-                            l.append(group_cl.get(cl.get(nodeid, name), key))
-                    elif prop.isMultilinkType:
-                        group_cl = db.classes[prop.classname]
-                        key = group_cl.getkey()
-                        for value in cl.get(nodeid, name):
-                            l.append(group_cl.get(value, key))
-                    else:
-                        value = cl.get(nodeid, name)
-                        if value is None:
-                            value = '[empty %s]'%name
-                        l.append(value)
-                w('<tr class="list-header">'
-                  '<td align=left colspan=%s><strong>%s</strong></td></tr>'%(
-                    len(columns), ', '.join(l)))
-                old_group = this_group
-
-        # display this node's row
-        for value in globals.values():
-            if hasattr(value, 'nodeid'):
-                value.nodeid = nodeid
-        replace = IndexTemplateReplace(globals, locals(), columns)
-        w(replace.go(template))
-
-    w('</table>')
-
-    if not show_display_form:
-        return
-
-    # now add in the filter/columns/group/etc config table form
-    w('<p><form>')
-    w('<table width=100% border=0 cellspacing=0 cellpadding=2>')
-    for k,v in filterspec.items():
-        if type(v) == type([]): v = ','.join(v)
-        w('<input type="hidden" name="%s" value="%s">'%(k, v))
-    if sort:
-        w('<input type="hidden" name=":sort" value="%s">'%','.join(sort))
-    names = []
-    for name in cl.getprops().keys():
-        if name in all_filters or name in all_columns:
-            names.append(name)
-    w('<tr class="location-bar">')
-    w('<th align="left" colspan=%s>View customisation...</th></tr>'%
-        (len(names)+1))
-    w('<tr class="location-bar"><th>&nbsp;</th>')
-    for name in names:
-        w('<th>%s</th>'%name.capitalize())
-    w('</tr>')
-
-    # filter
-    if all_filters:
-        w('<tr><th width="1%" align=right class="location-bar">Filters</th>')
-        for name in names:
-            if name not in all_filters:
-                w('<td>&nbsp;</td>')
-                continue
-            if name in filter: checked=' checked'
-            else: checked=''
-            w('<td align=middle>')
-            w('<input type="checkbox" name=":filter" value="%s" %s></td>'%(name,
-                checked))
-        w('</tr>')
-
-    # columns
-    if all_columns:
-        w('<tr><th width="1%" align=right class="location-bar">Columns</th>')
-        for name in names:
-            if name not in all_columns:
-                w('<td>&nbsp;</td>')
-                continue
-            if name in columns: checked=' checked'
-            else: checked=''
-            w('<td align=middle>')
-            w('<input type="checkbox" name=":columns" value="%s" %s></td>'%(
-                name, checked))
-        w('</tr>')
-
-        # group
-        w('<tr><th width="1%" align=right class="location-bar">Grouping</th>')
-        for name in names:
-            prop = properties[name]
-            if name not in all_columns:
-                w('<td>&nbsp;</td>')
-                continue
-            if name in group: checked=' checked'
-            else: checked=''
-            w('<td align=middle>')
-            w('<input type="checkbox" name=":group" value="%s" %s></td>'%(
-                name, checked))
-        w('</tr>')
-
-    w('<tr class="location-bar"><td width="1%">&nbsp;</td>')
-    w('<td colspan="%s">'%len(names))
-    w('<input type="submit" value="Redisplay"></td></tr>')
-    w('</table>')
-    w('</form>')
-
-
-#
-#   ITEM TEMPLATES
-#
-class ItemTemplateReplace:
-    def __init__(self, globals, locals, cl, nodeid):
-        self.globals = globals
-        self.locals = locals
-        self.cl = cl
-        self.nodeid = nodeid
-
-    def go(self, text, replace=re.compile(
-            r'((<property\s+name="(?P<name>[^>]+)">(?P<text>.+?)</property>)|'
-            r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I|re.S)):
-        return replace.sub(self, text)
-
-    def __call__(self, m, filter=None, columns=None, sort=None, group=None):
-        if m.group('name'):
-            if self.nodeid and self.cl.get(self.nodeid, m.group('name')):
-                replace = ItemTemplateReplace(self.globals, {}, self.cl,
-                    self.nodeid)
-                return replace.go(m.group('text'))
-            else:
-                return ''
-        if m.group('display'):
-            command = m.group('command')
-            return eval(command, self.globals, self.locals)
-        print '*** unhandled match', m.groupdict()
-
-def item(fp, db, classname, nodeid, replace=re.compile(
-            r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
-            r'(?P<endprop></property>)|'
-            r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
-
-    globals = {
-        'plain': Plain(db, classname, nodeid),
-        'field': Field(db, classname, nodeid),
-        'menu': Menu(db, classname, nodeid),
-        'link': Link(db, classname, nodeid),
-        'count': Count(db, classname, nodeid),
-        'reldate': Reldate(db, classname, nodeid),
-        'download': Download(db, classname, nodeid),
-        'checklist': Checklist(db, classname, nodeid),
-        'list': List(db, classname, nodeid),
-        'history': History(db, classname, nodeid),
-        'submit': Submit(db, classname, nodeid),
-        'note': Note(db, classname, nodeid)
-    }
-
-    cl = db.classes[classname]
-    properties = cl.getprops()
-
-    if properties.has_key('type') and properties.has_key('content'):
-        pass
-        # XXX we really want to return this as a downloadable...
-        #  currently I handle this at a higher level by detecting 'file'
-        #  designators...
-
-    w = fp.write
-    w('<form action="%s%s">'%(classname, nodeid))
-    s = open(os.path.join('templates', classname+'.item')).read()
-    replace = ItemTemplateReplace(globals, locals(), cl, nodeid)
-    w(replace.go(s))
-    w('</form>')
-
-
-def newitem(fp, db, classname, form, replace=re.compile(
-            r'((?P<prop><property\s+name="(?P<propname>[^>]+)">)|'
-            r'(?P<endprop></property>)|'
-            r'(?P<display><display\s+call="(?P<command>[^"]+)">))', re.I)):
-    globals = {
-        'plain': Plain(db, classname, form=form),
-        'field': Field(db, classname, form=form),
-        'menu': Menu(db, classname, form=form),
-        'link': Link(db, classname, form=form),
-        'count': Count(db, classname, form=form),
-        'reldate': Reldate(db, classname, form=form),
-        'download': Download(db, classname, form=form),
-        'checklist': Checklist(db, classname, form=form),
-        'list': List(db, classname, form=form),
-        'history': History(db, classname, form=form),
-        'submit': Submit(db, classname, form=form),
-        'note': Note(db, classname, form=form)
-    }
-
-    cl = db.classes[classname]
-    properties = cl.getprops()
-
-    w = fp.write
-    try:
-        s = open(os.path.join('templates', classname+'.newitem')).read()
-    except:
-        s = open(os.path.join('templates', classname+'.item')).read()
-    w('<form action="new%s">'%classname)
-    replace = ItemTemplateReplace(globals, locals(), None, None)
-    w(replace.go(s))
-    w('</form>')
-
-#
-# $Log: not supported by cvs2svn $
-# Revision 1.4  2001/07/19 06:27:07  anthonybaxter
-# fixing (manually) the (dollarsign)Log(dollarsign) entries caused by
-# my using the magic (dollarsign)Id(dollarsign) and (dollarsign)Log(dollarsign)
-# strings in a commit message. I'm a twonk.
-#
-# Also broke the help string in two.
-#
-# Revision 1.3  2001/07/19 05:52:22  anthonybaxter
-# Added CVS keywords Id and Log to all python files.
-#
-#
-
diff --git a/test.py b/test.py
deleted file mode 100644 (file)
index fb8faea..0000000
--- a/test.py
+++ /dev/null
@@ -1,41 +0,0 @@
-
-import pprint
-db = Database("test_db", "richard")
-status = Class(db, "status", name=String())
-status.setkey("name")
-print db.status.create(name="unread")
-print db.status.create(name="in-progress")
-print db.status.create(name="testing")
-print db.status.create(name="resolved")
-print db.status.count()
-print db.status.list()
-print db.status.lookup("in-progress")
-db.status.retire(3)
-print db.status.list()
-issue = Class(db, "issue", title=String(), status=Link("status"))
-db.issue.create(title="spam", status=1)
-db.issue.create(title="eggs", status=2)
-db.issue.create(title="ham", status=4)
-db.issue.create(title="arguments", status=2)
-db.issue.create(title="abuse", status=1)
-user = Class(db, "user", username=String(), password=String())
-user.setkey("username")
-db.issue.addprop(fixer=Link("user"))
-print db.issue.getprops()
-#{"title": <hyperdb.String>, "status": <hyperdb.Link to "status">,
-#"user": <hyperdb.Link to "user">}
-db.issue.set(5, status=2)
-print db.issue.get(5, "status")
-print db.status.get(2, "name")
-print db.issue.get(5, "title")
-print db.issue.find(status = db.status.lookup("in-progress"))
-print db.issue.history(5)
-# [(<Date 2000-06-28.19:09:43>, "ping", "create", {"title": "abuse", "status": 1}),
-# (<Date 2000-06-28.19:11:04>, "ping", "set", {"status": 2})]
-print db.status.history(1)
-# [(<Date 2000-06-28.19:09:43>, "ping", "link", ("issue", 5, "status")),
-# (<Date 2000-06-28.19:11:04>, "ping", "unlink", ("issue", 5, "status"))]
-print db.status.history(2)
-# [(<Date 2000-06-28.19:11:04>, "ping", "link", ("issue", 5, "status"))]
-
-# TODO: set up some filter tests