summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: da73293)
raw | patch | inline | side by side (parent: da73293)
author | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Sun, 22 Jul 2001 11:11:14 +0000 (11:11 +0000) | ||
committer | richard <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 | patch | blob | history | |
README | patch | blob | history | |
cgitb.py | [deleted file] | patch | blob | history |
config.py | [deleted file] | patch | blob | history |
date.py | [deleted file] | patch | blob | history |
hyperdb.py | [deleted file] | patch | blob | history |
hyperdb_bsddb.py | [deleted file] | patch | blob | history |
roundup-mailgw.py | [deleted file] | patch | blob | history |
roundup.cgi | [deleted file] | patch | blob | history |
roundup.py | [deleted file] | patch | blob | history |
roundup_cgi.py | [deleted file] | patch | blob | history |
roundupdb.py | [deleted file] | patch | blob | history |
server.py | [deleted file] | patch | blob | history |
style.css | [deleted file] | patch | blob | history |
template.py | [deleted file] | patch | blob | history |
test.py | [deleted file] | patch | blob | history |
index 32eb2a698259cc27a5f40bb9be91535b4ec8e489..1910b0714c0e9fcbe3e35775a9ae3986172d9f58 100644 (file)
--- a/CHANGES
+++ b/CHANGES
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.
+
index 37c80f1f263d79c0ab6bb4e7dac93e4ecfd1cd8f..7c4beaef1585763225dc4fc3aec66102ff765dfa 100644 (file)
--- a/README
+++ b/README
management interface and presumably belongs to Digital Creations.
+TODO: Instructions need re-writing!!
+
2. Installation
===============
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
--- 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> </tt>' % (' ' * 5)
- traceback = []
- for frame, file, lnum, func, lines, index in inspect.trace(context):
- if file is None:
- link = '<file is None - probably inside <tt>eval</tt> or <tt>exec</tt>>'
- 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 = %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 = ' ' * (5-len(str(i))) + str(i)
- number = '<small><font color="#909090">%s</font></small>' % number
- line = '<tt>%s %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 = %s' % (indent, name, value))
-
- return head + string.join(attribs) + string.join(traceback) + '<p> </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
--- 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
--- 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
--- a/hyperdb.py
+++ /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
--- a/hyperdb_bsddb.py
+++ /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
--- a/roundup-mailgw.py
+++ /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
--- a/roundup.cgi
+++ /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
--- a/roundup.py
+++ /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
--- a/roundup_cgi.py
+++ /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
--- a/roundupdb.py
+++ /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
--- 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
--- 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
--- a/template.py
+++ /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 = '"'.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%%"> </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> </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> </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> </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> </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%"> </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
--- 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