From: richard Date: Sun, 22 Jul 2001 11:11:14 +0000 (+0000) Subject: Initial commit of the Grande Splite X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=691a4ffe6bf6fdf10ecbca2ad731d29c42c15a40;p=roundup.git Initial commit of the Grande Splite git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@26 57a73879-2fb5-44c3-a270-3262357dd7e2 --- diff --git a/CHANGES b/CHANGES index 32eb2a6..1910b07 100644 --- a/CHANGES +++ b/CHANGES @@ -55,3 +55,7 @@ quoted. . Fixed a bug in the hyperdb filter - wrong variable names in the error message. + . Major reshuffle of code to allow multiple roundup instances and a + single, site-packages -based installation. Also allows multiple database + back-ends. + diff --git a/README b/README index 37c80f1..7c4beae 100644 --- a/README +++ b/README @@ -11,6 +11,8 @@ The stylesheet included with this package has been copied from the Zope management interface and presumably belongs to Digital Creations. +TODO: Instructions need re-writing!! + 2. Installation =============== @@ -44,24 +46,17 @@ Both need the bsddb module. 2.1 Initial Setup ----------------- - 1. Make a directory in /home/httpd/html called 'roundup'. - 2. Copy the tar file's contents there. - 3. Edit config.py - 4. "python roundup.py init" to initialise the database (by default, it - goes in a directory called 'db' in the current directory). Choose a - sensible admin password. - 5. "chmod -R a+rw db" 2.2 Mail -------- Set up a mail alias called "issue_tracker" as: - "|/usr/bin/python /home/httpd/html/roundup/roundup-mailgw.py" + "|/usr/bin/python /home/httpd/html/roundup/roundup-mailgw " 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 " 2.3 Web Interface diff --git a/cgitb.py b/cgitb.py deleted file mode 100644 index 6eb0848..0000000 --- a/cgitb.py +++ /dev/null @@ -1,121 +0,0 @@ -# $Id: cgitb.py,v 1.3 2001-07-19 06:27:07 anthonybaxter Exp $ - -import sys, os, types, string, keyword, linecache, tokenize, inspect, pydoc - -def breaker(): - return ('' + - ' > ' + - '' * 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] + '
' + sys.executable - head = pydoc.html.heading( - '%s: %s'%(str(etype), str(evalue)), - '#ffffff', '#aa55cc', pyver) - - head = head + ('

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 = '%s ' % (' ' * 5) - traceback = [] - for frame, file, lnum, func, lines, index in inspect.trace(context): - if file is None: - link = '<file is None - probably inside eval or exec>' - else: - file = os.path.abspath(file) - link = '%s' % (file, pydoc.html.escape(file)) - args, varargs, varkw, locals = inspect.getargvalues(frame) - if func == '?': - call = '' - else: - call = 'in %s' % func + inspect.formatargvalues( - args, varargs, varkw, locals, - formatvalue=lambda value: '=' + pydoc.html.repr(value)) - - level = ''' - -
%s %s
''' % (link, call) - - if file is None: - traceback.append('

' + 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 = 'undefined' - name = '%s' % name - else: - if frame.f_globals.has_key(name): - value = pydoc.html.repr(frame.f_globals[name]) - else: - value = 'undefined' - name = 'global %s' % name - lvals.append('%s = %s' % (name, value)) - if lvals: - lvals = string.join(lvals, ', ') - lvals = indent + ''' -%s
''' % lvals - else: - lvals = '' - - excerpt = [] - i = lnum - index - for line in lines: - number = ' ' * (5-len(str(i))) + str(i) - number = '%s' % number - line = '%s %s' % (number, pydoc.html.preformat(line)) - if i == lnum: - line = ''' - -
%s
''' % line - excerpt.append('\n' + line) - if i == lnum: - excerpt.append(lvals) - i = i + 1 - traceback.append('

' + level + string.join(excerpt, '\n')) - - traceback.reverse() - - exception = '

%s: %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('
%s%s = %s' % (indent, name, value)) - - return head + string.join(attribs) + string.join(traceback) + '

 

' - -def handler(): - print breaker() - print html() - -# -# $Log: not supported by cvs2svn $ -# Revision 1.2 2001/07/19 05:52:22 anthonybaxter -# Added CVS keywords Id and Log to all python files. -# -# diff --git a/config.py b/config.py deleted file mode 100644 index 01d807d..0000000 --- a/config.py +++ /dev/null @@ -1,58 +0,0 @@ -# $Id: config.py,v 1.6 2001-07-19 10:43:01 anthonybaxter Exp $ - -ROUNDUP_HOME=MAIL_DOMAIN=MAILHOST=HTTP_HOST=None -HTTP_PORT=0 - -try: - from localconfig import * -except ImportError: - localconfig = None - -import os - -# This is the root directory for roundup -if not ROUNDUP_HOME: - ROUNDUP_HOME='/home/httpd/html/roundup' - -# The SMTP mail host that roundup will use to send mail -if not MAILHOST: - MAILHOST = 'localhost' - -# The domain name used for email addresses. -if not MAIL_DOMAIN: - MAIL_DOMAIN = 'bizarsoftware.com.au' - -# the next two are only used for the standalone HTTP server. -if not HTTP_HOST: - HTTP_HOST = '' -if not HTTP_PORT: - HTTP_PORT = 9080 - -# This is the directory that the database is going to be stored in -DATABASE = os.path.join(ROUNDUP_HOME, 'db') - -# The email address that mail to roundup should go to -ISSUE_TRACKER_EMAIL = 'issue_tracker@%s'%MAIL_DOMAIN - -# The email address that roundup will complain to if it runs into trouble -ADMIN_EMAIL = 'roundup-admin@%s'%MAIL_DOMAIN - -# Somewhere for roundup to log stuff internally sent to stdout or stderr -LOG = os.path.join(ROUNDUP_HOME, 'roundup.log') - -del os - -# -# $Log: not supported by cvs2svn $ -# Revision 1.5 2001/07/19 06:27:07 anthonybaxter -# fixing (manually) the (dollarsign)Log(dollarsign) entries caused by -# my using the magic (dollarsign)Id(dollarsign) and (dollarsign)Log(dollarsign) -# strings in a commit message. I'm a twonk. -# -# Also broke the help string in two. -# -# Revision 1.4 2001/07/19 05:52:22 anthonybaxter -# Added CVS keywords Id and Log to all python files. -# -# - diff --git a/date.py b/date.py deleted file mode 100644 index fc1bc48..0000000 --- a/date.py +++ /dev/null @@ -1,351 +0,0 @@ -# $Id: date.py,v 1.3 2001-07-19 06:27:07 anthonybaxter Exp $ - -import time, re, calendar - -class Date: - ''' - As strings, date-and-time stamps are specified with the date in - international standard format (yyyy-mm-dd) joined to the time - (hh:mm:ss) by a period ("."). Dates in this form can be easily compared - and are fairly readable when printed. An example of a valid stamp is - "2000-06-24.13:03:59". We'll call this the "full date format". When - Timestamp objects are printed as strings, they appear in the full date - format with the time always given in GMT. The full date format is - always exactly 19 characters long. - - For user input, some partial forms are also permitted: the whole time - or just the seconds may be omitted; and the whole date may be omitted - or just the year may be omitted. If the time is given, the time is - interpreted in the user's local time zone. The Date constructor takes - care of these conversions. In the following examples, suppose that yyyy - is the current year, mm is the current month, and dd is the current day - of the month; and suppose that the user is on Eastern Standard Time. - - "2000-04-17" means - "01-25" means - "2000-04-17.03:45" means - "08-13.22:13" means - "11-07.09:32:43" means - "14:25" means - "8:47:11" means - "." 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(".") - - >>> _.local(-5) - "2000-06-25.19:34:02" - >>> Date(". + 2d") - - >>> Date("1997-04-17", -5) - - >>> Date("01-25", -5) - - >>> Date("08-13.22:13", -5) - - >>> Date("14:25", -5) - - ''' - 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\d\d\d\d)-)?((?P\d\d)-(?P\d\d))?)? # yyyy-mm-dd - (?P\.)? # . - (((?P\d?\d):(?P\d\d))?(:(?P\d\d))?)? # hh:mm:ss - (?P.+)? # 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 ''%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") - - >>> Date(". + 2d") - Interval("3w") - - ''' - 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[-+])? # + or - - \s* - ((?P\d+\s*)y)? # year - \s* - ((?P\d+\s*)m)? # month - \s* - ((?P\d+\s*)w)? # week - \s* - ((?P\d+\s*)d)? # day - \s* - (((?P\d?\d):(?P\d\d))?(:(?P\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 ''%self.__str__() - - def pretty(self, threshold=('d', 5)): - ''' print up the date date using one of these nice formats.. - < 1 minute - < 15 minutes - < 30 minutes - < 1 hour - < 12 hours - < 1 day - otherwise, return None (so a full date may be displayed) - ''' - if self.year or self.month or self.day > 5: - return None - if self.day > 1: - return '%s days'%self.day - if self.day == 1 or self.hour > 12: - return 'yesterday' - if self.hour > 1: - return '%s hours'%self.hour - if self.hour == 1: - if self.minute < 15: - return 'an hour' - quart = self.minute/15 - if quart == 2: - return '1 1/2 hours' - return '1 %s/4 hours'%quart - if self.minute < 1: - return 'just now' - if self.minute == 1: - return '1 minute' - if self.minute < 15: - return '%s minutes'%self.minute - quart = self.minute/15 - if quart == 2: - return '1/2 an hour' - return '%s/4 hour'%quart - - -def test(): - intervals = (" 3w 1 d 2:00", " + 2d", "3w") - for interval in intervals: - print '>>> Interval("%s")'%interval - print `Interval(interval)` - - dates = (".", "2000-06-25.19:34:02", ". + 2d", "1997-04-17", "01-25", - "08-13.22:13", "14:25") - for date in dates: - print '>>> Date("%s")'%date - print `Date(date)` - - sums = ((". + 2d", "3w"), (".", " 3w 1 d 2:00")) - for date, interval in sums: - print '>>> Date("%s") + Interval("%s")'%(date, interval) - print `Date(date) + Interval(interval)` - -if __name__ == '__main__': - test() - -# -# $Log: not supported by cvs2svn $ -# Revision 1.2 2001/07/19 05:52:22 anthonybaxter -# Added CVS keywords Id and Log to all python files. -# -# - diff --git a/hyperdb.py b/hyperdb.py deleted file mode 100644 index a3fce09..0000000 --- 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 deleted file mode 100644 index 422fced..0000000 --- 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 deleted file mode 100755 index a83cb06..0000000 --- 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[^\d]+)(?P\d+)?\])' - r'(?P[^\[]+)(\[(?P<args>.+?)\])?', re.I) - -def roundup_mail(db, fp): - # ok, figure the subject, author, recipients and content-type - message = mimetools.Message(fp) - try: - handle_message(db, message) - except: - # send an email to the people who missed out - sendto = [message.getaddrlist('from')[0][1]] - m = ['Subject: failed issue tracker submission'] - m.append('') - # TODO as attachments? - m.append('---- traceback of failure ----') - return - s = StringIO.StringIO() - import traceback - traceback.print_exc(None, s) - m.append(s.getvalue()) - m.append('---- failed message follows ----') - try: - fp.seek(0) - except: - pass - m.append(fp.read()) - try: - smtp = smtplib.SMTP(config.MAILHOST) - smtp.sendmail(config.ADMIN_EMAIL, sendto, '\n'.join(m)) - except socket.error, value: - return "Couldn't send confirmation email: mailhost %s"%value - except smtplib.SMTPException, value: - return "Couldn't send confirmation email: %s"%value - -def handle_message(db, message): - # handle the subject line - m = subject_re.match(message.getheader('subject')) - if not m: - raise ValueError, 'No [designator] found in subject "%s"' - classname = m.group('classname') - nodeid = m.group('nodeid') - title = m.group('title').strip() - subject_args = m.group('args') - cl = db.getclass(classname) - properties = cl.getprops() - props = {} - args = m.group('args') - if args: - for prop in string.split(m.group('args'), ';'): - try: - key, value = prop.split('=') - except ValueError, message: - raise ValueError, 'Args list not of form [arg=value,value,...;arg=value,value,value..] (specific exception message was "%s")'%message - type = properties[key] - if type.isStringType: - props[key] = value - elif type.isDateType: - props[key] = date.Date(value) - elif type.isIntervalType: - props[key] = date.Interval(value) - elif type.isLinkType: - props[key] = value - elif type.isMultilinkType: - props[key] = value.split(',') - - # handle the users - author = db.uidFromAddress(message.getaddrlist('from')[0]) - recipients = [] - for recipient in message.getaddrlist('to') + message.getaddrlist('cc'): - if recipient[1].strip().lower() == config.ISSUE_TRACKER_EMAIL: - continue - recipients.append(db.uidFromAddress(recipient)) - - # now handle the body - find the message - content_type = message.gettype() - attachments = [] - if content_type == 'multipart/mixed': - boundary = message.getparam('boundary') - # skip over the intro to the first boundary - part = getPart(message.fp, boundary) - content = None - while 1: - # get the next part - part = getPart(message.fp, boundary) - if part is None: - break - # parse it - part.seek(0) - submessage = mimetools.Message(part) - subtype = submessage.gettype() - if subtype == 'text/plain' and not content: - # this one's our content - content = part.read() - elif subtype == 'message/rfc822': - i = part.tell() - subsubmess = mimetools.Message(part) - name = subsubmess.getheader('subject') - part.seek(i) - attachments.append((name, 'message/rfc822', part.read())) - else: - # try name on Content-Type - name = submessage.getparam('name') - # this is just an attachment - data = part.read() - encoding = submessage.getencoding() - if encoding == 'base64': - data = binascii.a2b_base64(data) - elif encoding == 'quoted-printable': - data = quopri.decode(data) - elif encoding == 'uuencoded': - data = binascii.a2b_uu(data) - attachments.append((name, submessage.gettype(), data)) - if content is None: - raise ValueError, 'No text/plain part found' - - elif content_type[:10] == 'multipart/': - boundary = message.getparam('boundary') - # skip over the intro to the first boundary - getPart(message.fp, boundary) - content = None - while 1: - # get the next part - part = getPart(message.fp, boundary) - if part is None: - break - # parse it - part.seek(0) - submessage = mimetools.Message(part) - if submessage.gettype() == 'text/plain' and not content: - # this one's our content - content = part.read() - if content is None: - raise ValueError, 'No text/plain part found' - - elif content_type != 'text/plain': - raise ValueError, 'No text/plain part found' - - else: - content = message.fp.read() - - # extract out the summary from the message - summary = [] - for line in content.split('\n'): - line = line.strip() - if summary and not line: - break - if not line: - summary.append('') - elif line[0] not in '>|': - summary.append(line) - summary = '\n'.join(summary) - - # handle the files - files = [] - for (name, type, data) in attachments: - files.append(db.file.create(type=type, name=name, content=data)) - - # now handle the db stuff - if nodeid: - # If an item designator (class name and id number) is found there, the - # newly created "msg" node is added to the "messages" property for - # that item, and any new "file" nodes are added to the "files" - # property for the item. - message_id = db.msg.create(author=author, recipients=recipients, - date=date.Date('.'), summary=summary, content=content, - files=files) - messages = cl.get(nodeid, 'messages') - messages.append(message_id) - props['messages'] = messages - apply(cl.set, (nodeid, ), props) - else: - # If just an item class name is found there, we attempt to create a - # new item of that class with its "messages" property initialized to - # contain the new "msg" node and its "files" property initialized to - # contain any new "file" nodes. - message_id = db.msg.create(author=author, recipients=recipients, - date=date.Date('.'), summary=summary, content=content, - files=files) - if not props.has_key('assignedto'): - props['assignedto'] = 1 # "admin" - if not props.has_key('priority'): - props['priority'] = 1 # "bug-fatal" - if not props.has_key('status'): - props['status'] = 1 # "unread" - if not props.has_key('title'): - props['title'] = title - props['messages'] = [message_id] - props['nosy'] = recipients[:] - props['nosy'].append(author) - props['nosy'].sort() - nodeid = apply(cl.create, (), props) - - return 0 - -if __name__ == '__main__': - db = roundupdb.openDB(config.DATABASE, 'admin', '1') - roundup_mail(db, sys.stdin) - db.close() - -# -# $Log: not supported by cvs2svn $ -# Revision 1.2 2001/07/19 05:52:22 anthonybaxter -# Added CVS keywords Id and Log to all python files. -# -# - diff --git a/roundup.cgi b/roundup.cgi deleted file mode 100755 index 6ea7189..0000000 --- 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 deleted file mode 100755 index 8aa660f..0000000 --- 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 deleted file mode 100644 index 422af51..0000000 --- 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 - - - -%s - - - - - - -
%s%s
All issues | -Bugs | -Support | -Wishlist | -New Issue -%sYour Details
-'''%(title, style, message, title, self.user, extras, userid)) - - def pagefoot(self): - if self.debug: - self.write('
') - self.write('
Path
') - self.write('
%s
'%(', '.join(map(repr, self.split_path)))) - keys = self.form.keys() - keys.sort() - if keys: - self.write('
Form entries
') - for k in self.form.keys(): - v = str(self.form[k].value) - self.write('
%s:%s
'%(k, cgi.escape(v))) - keys = self.env.keys() - keys.sort() - self.write('
CGI environment
') - for k in keys: - v = self.env[k] - self.write('
%s:%s
'%(k, cgi.escape(v))) - self.write('
') - self.write('') - - 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 = '
%s
'%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 = '
%s
'%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('\n') - for cn in classnames: - cl = self.db.getclass(cn) - self.write(''%cn.capitalize()) - for key, value in cl.properties.items(): - if value is None: value = '' - else: value = str(value) - self.write(''%( - key, cgi.escape(value))) - self.write('
%s
%s%s
') - self.pagefoot() - else: - raise Unauthorised - - def main(self, dre=re.compile(r'([^\d]+)(\d+)'), - nre=re.compile(r'new(\w+)')): - path = self.split_path - if not path or path[0] in ('', 'index'): - self.index() - elif len(path) == 1: - if path[0] == 'list_classes': - self.classes() - return - m = dre.match(path[0]) - if m: - self.classname = m.group(1) - self.nodeid = m.group(2) - getattr(self, 'show%s'%self.classname)() - return - m = nre.match(path[0]) - if m: - self.classname = m.group(1) - getattr(self, 'new%s'%self.classname)() - return - self.classname = path[0] - self.list() - else: - raise 'ValueError', 'Path not understood' - - def __del__(self): - self.db.close() - -# -# $Log: not supported by cvs2svn $ -# Revision 1.6 2001/07/20 00:53:20 richard -# Default index now filters out the resolved issues ;) -# -# Revision 1.5 2001/07/20 00:17:16 richard -# Fixed adding a new issue when there is no __note -# -# Revision 1.4 2001/07/19 06:27:07 anthonybaxter -# fixing (manually) the (dollarsign)Log(dollarsign) entries caused by -# my using the magic (dollarsign)Id(dollarsign) and (dollarsign)Log(dollarsign) -# strings in a commit message. I'm a twonk. -# -# Also broke the help string in two. -# -# Revision 1.3 2001/07/19 05:52:22 anthonybaxter -# Added CVS keywords Id and Log to all python files. -# -# - diff --git a/roundupdb.py b/roundupdb.py deleted file mode 100644 index 356abb2..0000000 --- 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 deleted file mode 100755 index 1e4eb83..0000000 --- a/server.py +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/python -""" HTTP Server that serves roundup. - -Stolen from CGIHTTPServer - -$Id: server.py,v 1.5 2001-07-20 12:33:06 richard Exp $ - -""" -import sys -if int(sys.version[0]) < 2: - print "Content-Type: text/plain\n" - print "Roundup requires Python 2.0 or newer." - -__version__ = "0.1" - -__all__ = ["RoundupRequestHandler"] - -import os, urllib, StringIO, traceback, cgi, binascii -import BaseHTTPServer -import SimpleHTTPServer -import date, hyperdb, template, roundupdb, roundup_cgi -import cgitb - -class RoundupRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): - def send_head(self): - """Version of send_head that support CGI scripts""" - return self.run_cgi() - - def run_cgi(self): - """Execute a CGI script.""" - rest = self.path - i = rest.rfind('?') - if i >= 0: - rest, query = rest[:i], rest[i+1:] - else: - query = '' - - # Set up the CGI environment - env = {} - env['REQUEST_METHOD'] = self.command - env['PATH_INFO'] = urllib.unquote(rest) - if query: - env['QUERY_STRING'] = query - host = self.address_string() - if self.headers.typeheader is None: - env['CONTENT_TYPE'] = self.headers.type - else: - env['CONTENT_TYPE'] = self.headers.typeheader - length = self.headers.getheader('content-length') - if length: - env['CONTENT_LENGTH'] = length - co = filter(None, self.headers.getheaders('cookie')) - if co: - env['HTTP_COOKIE'] = ', '.join(co) - env['SCRIPT_NAME'] = '' - env['SERVER_NAME'] = self.server.server_name - env['SERVER_PORT'] = str(self.server.server_port) - - decoded_query = query.replace('+', ' ') - - # if root, setuid to nobody - if not os.getuid(): - nobody = nobody_uid() - os.setuid(nobody) - - # TODO check for file timestamp changes - reload(date) - reload(hyperdb) - reload(roundupdb) - reload(template) - reload(roundup_cgi) - - # initialise the roundupdb, check for auth - db = roundupdb.openDB('db', 'admin') - message = 'Unauthorised' - auth = self.headers.getheader('authorization') - if auth: - l = binascii.a2b_base64(auth.split(' ')[1]).split(':') - user = l[0] - password = None - if len(l) > 1: - password = l[1] - try: - uid = db.user.lookup(user) - except KeyError: - auth = None - message = 'Username not recognised' - else: - if password != db.user.get(uid, 'password'): - message = 'Incorrect password' - auth = None - db.close() - del db - if not auth: - self.send_response(401) - self.send_header('Content-Type', 'text/html') - self.send_header('WWW-Authenticate', 'basic realm="Roundup"') - self.end_headers() - self.wfile.write(message) - return - - self.send_response(200, "Script output follows") - - # do the roundup thang - save_stdin = sys.stdin - try: - sys.stdin = self.rfile - client = roundup_cgi.Client(self.wfile, env, user) - client.main() - except roundup_cgi.Unauthorised: - self.wfile.write('Content-Type: text/html\n') - self.wfile.write('Status: 403\n') - self.wfile.write('Unauthorised') - except: - try: - reload(cgitb) - self.wfile.write(cgitb.breaker()) - self.wfile.write(cgitb.html()) - except: - self.wfile.write("Content-Type: text/html\n\n") - self.wfile.write("
")
-                s = StringIO.StringIO()
-                traceback.print_exc(None, s)
-                self.wfile.write(cgi.escape(s.getvalue()))
-                self.wfile.write("
\n") - sys.stdin = save_stdin - do_POST = run_cgi - - -nobody = None - -def nobody_uid(): - """Internal routine to get nobody's uid""" - global nobody - if nobody: - return nobody - try: - import pwd - except ImportError: - return -1 - try: - nobody = pwd.getpwnam('nobody')[2] - except KeyError: - nobody = 1 + max(map(lambda x: x[2], pwd.getpwall())) - return nobody - -def main(): - from config import HTTP_HOST, HTTP_PORT - address = (HTTP_HOST, HTTP_PORT) - httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler) - print 'Roundup server started on', address - httpd.serve_forever() - -if __name__ == '__main__': - main() - -# -# $Log: not supported by cvs2svn $ -# Revision 1.4 2001/07/19 10:43:01 anthonybaxter -# HTTP_HOST and HTTP_PORT config options. -# -# Revision 1.3 2001/07/19 06:27:07 anthonybaxter -# fixing (manually) the (dollarsign)Log(dollarsign) entries caused by -# my using the magic (dollarsign)Id(dollarsign) and (dollarsign)Log(dollarsign) -# strings in a commit message. I'm a twonk. -# -# Also broke the help string in two. -# -# Revision 1.2 2001/07/19 05:52:22 anthonybaxter -# Added CVS keywords Id and Log to all python files. -# -# - diff --git a/style.css b/style.css deleted file mode 100644 index 2316c7c..0000000 --- a/style.css +++ /dev/null @@ -1,163 +0,0 @@ -h1 { - font-family: Verdana, Helvetica, sans-serif; - font-size: 18pt; - font-weight: bold; -} - -h2 { - font-family: Verdana, Helvetica, sans-serif; - font-size: 16pt; - font-weight: bold; -} - -h3 { - font-family: Verdana, Helvetica, sans-serif; - font-size: 12pt; - font-weight: bold; -} - -a:hover { - font-family: Verdana, Helvetica, sans-serif; - text-decoration: underline; - color: #333333; -} - -a:link { - font-family: Verdana, Helvetica, sans-serif; - text-decoration: none; - color: #000099; -} - -a { - font-family: Verdana, Helvetica, sans-serif; - text-decoration: none; - color: #000099; -} - -p { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - color: #333333; -} - -th { - font-family: Verdana, Helvetica, sans-serif; - font-weight: bold; - font-size: 10pt; - color: #333333; -} - -.form-help { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - color: #333333; -} - -.std-text { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - color: #333333; -} - -.tab-small { - font-family: Verdana, Helvetica, sans-serif; - font-size: 8pt; - color: #333333; -} - -.location-bar { - background-color: #efefef; - border: none; -} - -.strong-header { - font-family: Verdana, Helvetica, sans-serif; - font-size: 12pt; - font-weight: bold; - background-color: #000000; - color: #ffffff; -} - -.list-header { - background-color: #c0c0c0; - border: none; -} - -.list-item { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; -} - -.list-nav { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - font-weight: bold; -} - -.row-normal { - background-color: #ffffff; - border: none; - -} - -.row-hilite { - background-color: #efefef; - border: none; -} - -.section-bar { - background-color: #c0c0c0; - border: none; -} - -.system-msg { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - background-color: #ffffff; - border: 1px solid #000000; - margin-bottom: 6px; - margin-top: 6px; - padding: 4px; - width: 100%; - color: #660033; -} - -.form-title { - font-family: Verdana, Helvetica, sans-serif; - font-weight: bold; - font-size: 12pt; - color: #333333; -} - -.form-label { - font-family: Verdana, Helvetica, sans-serif; - font-weight: bold; - font-size: 10pt; - color: #333333; -} - -.form-optional { - font-family: Verdana, Helvetica, sans-serif; - font-weight: bold; - font-style: italic; - font-size: 10pt; - color: #333333; -} - -.form-element { - font-family: Verdana, Helvetica, aans-serif; - font-size: 10pt; - color: #000000; -} - -.form-text { - font-family: Verdana, Helvetica, sans-serif; - font-size: 10pt; - color: #333333; -} - -.form-mono { - font-family: monospace; - font-size: 12px; - text-decoration: none; -} diff --git a/template.py b/template.py deleted file mode 100644 index 85ddc71..0000000 --- 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 = ''%(property, value, size) - elif propclass.isLinkType: - linkcl = self.db.classes[propclass.classname] - l = ['') - s = '\n'.join(l) - elif propclass.isMultilinkType: - linkcl = self.db.classes[propclass.classname] - list = linkcl.list() - height = height or min(len(list), 7) - l = ['') - 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 = ['') - return '\n'.join(l) - if propclass.isMultilinkType: - linkcl = self.db.classes[propclass.classname] - list = linkcl.list() - height = height or min(len(list), 7) - l = ['') - 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 '%s'%(linkcl, value, linkvalue) - if propclass.isMultilinkType: - linkcl = self.db.classes[propclass.classname] - l = [] - for value in value: - linkvalue = linkcl.get(value, k) - l.append('%s'%(linkcl, value, linkvalue)) - return ', '.join(l) - return '%s'%(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 '%s'%(linkcl, value, linkvalue) - if propclass.isMultilinkType: - linkcl = self.db.classes[propclass.classname] - l = [] - for value in value: - linkvalue = linkcl.get(value, k) - l.append('%s'%(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:'%( - 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 ''%(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 = ['', - '', - '', - '', - '', - ''] - - for id, date, user, action, args in self.cl.history(self.nodeid): - l.append(''%( - date, user, action, args)) - l.append('
DateUserActionArgs
%s%s%s%s
') - return '\n'.join(l) - -# XXX new function -class Submit(Base): - ''' add a submit button for the item - ''' - def __call__(self): - if self.nodeid: - return '' - elif self.form is not None: - return '' - 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'(([^>]+)">(?P.+?))|' - r'(?P[^"]+)">))', 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']+)">')): - - 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('
') - w('') - w('') - w(' ') - w('') - replace = IndexTemplateReplace(globals, locals(), filter) - w(replace.go(template)) - if columns: - w(''%','.join(columns)) - if filter: - w(''%','.join(filter)) - if sort: - w(''%','.join(sort)) - if group: - w(''%','.join(group)) - for k, v in filterspec.items(): - if type(v) == type([]): v = ','.join(v) - w(''%(k, v)) - w('') - w('') - w('
Filter specification...
 
') - w('
') - - # 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('') - w('') - for name in columns: - cname = name.capitalize() - if show_display_form: - anchor = "%s?%s"%(classname, sortby(name, columns, filter, - sort, group, filterspec)) - w(''%( - anchor, cname)) - else: - w(''%cname) - w('') - - # 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('' - ''%( - 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('
%s%s
%s
') - - if not show_display_form: - return - - # now add in the filter/columns/group/etc config table form - w('

') - w('') - for k,v in filterspec.items(): - if type(v) == type([]): v = ','.join(v) - w(''%(k, v)) - if sort: - w(''%','.join(sort)) - names = [] - for name in cl.getprops().keys(): - if name in all_filters or name in all_columns: - names.append(name) - w('') - w(''% - (len(names)+1)) - w('') - for name in names: - w(''%name.capitalize()) - w('') - - # filter - if all_filters: - w('') - for name in names: - if name not in all_filters: - w('') - continue - if name in filter: checked=' checked' - else: checked='' - w(''%(name, - checked)) - w('') - - # columns - if all_columns: - w('') - for name in names: - if name not in all_columns: - w('') - continue - if name in columns: checked=' checked' - else: checked='' - w(''%( - name, checked)) - w('') - - # group - w('') - for name in names: - prop = properties[name] - if name not in all_columns: - w('') - continue - if name in group: checked=' checked' - else: checked='' - w(''%( - name, checked)) - w('') - - w('') - w('') - w('
View customisation...
 %s
Filters ') - w('
Columns ') - w('
Grouping ') - w('
 '%len(names)) - w('
') - w('
') - - -# -# 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'(([^>]+)">(?P.+?))|' - r'(?P[^"]+)">))', 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[^>]+)">)|' - r'(?P)|' - r'(?P[^"]+)">))', 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('
'%(classname, nodeid)) - s = open(os.path.join('templates', classname+'.item')).read() - replace = ItemTemplateReplace(globals, locals(), cl, nodeid) - w(replace.go(s)) - w('
') - - -def newitem(fp, db, classname, form, replace=re.compile( - r'((?P[^>]+)">)|' - r'(?P)|' - r'(?P[^"]+)">))', 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('
'%classname) - replace = ItemTemplateReplace(globals, locals(), None, None) - w(replace.go(s)) - w('
') - -# -# $Log: not supported by cvs2svn $ -# Revision 1.4 2001/07/19 06:27:07 anthonybaxter -# fixing (manually) the (dollarsign)Log(dollarsign) entries caused by -# my using the magic (dollarsign)Id(dollarsign) and (dollarsign)Log(dollarsign) -# strings in a commit message. I'm a twonk. -# -# Also broke the help string in two. -# -# Revision 1.3 2001/07/19 05:52:22 anthonybaxter -# Added CVS keywords Id and Log to all python files. -# -# - diff --git a/test.py b/test.py deleted file mode 100644 index fb8faea..0000000 --- a/test.py +++ /dev/null @@ -1,41 +0,0 @@ - -import pprint -db = Database("test_db", "richard") -status = Class(db, "status", name=String()) -status.setkey("name") -print db.status.create(name="unread") -print db.status.create(name="in-progress") -print db.status.create(name="testing") -print db.status.create(name="resolved") -print db.status.count() -print db.status.list() -print db.status.lookup("in-progress") -db.status.retire(3) -print db.status.list() -issue = Class(db, "issue", title=String(), status=Link("status")) -db.issue.create(title="spam", status=1) -db.issue.create(title="eggs", status=2) -db.issue.create(title="ham", status=4) -db.issue.create(title="arguments", status=2) -db.issue.create(title="abuse", status=1) -user = Class(db, "user", username=String(), password=String()) -user.setkey("username") -db.issue.addprop(fixer=Link("user")) -print db.issue.getprops() -#{"title": , "status": , -#"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) -# [(, "ping", "create", {"title": "abuse", "status": 1}), -# (, "ping", "set", {"status": 2})] -print db.status.history(1) -# [(, "ping", "link", ("issue", 5, "status")), -# (, "ping", "unlink", ("issue", 5, "status"))] -print db.status.history(2) -# [(, "ping", "link", ("issue", 5, "status"))] - -# TODO: set up some filter tests