X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup-admin;h=6a1d5c078efa1716ccc4a5e9e17d209857a4a983;hb=a25e89b84b3a5dc04adc4450a85424171b3bb7f9;hp=8ea917329913e3c944b989b9f62ca58c1c6ebe61;hpb=121b0e0695e11306956f502c7dd694a3084bd2dc;p=roundup.git diff --git a/roundup-admin b/roundup-admin index 8ea9173..6a1d5c0 100755 --- a/roundup-admin +++ b/roundup-admin @@ -16,14 +16,12 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundup-admin,v 1.38 2001-11-05 23:45:40 richard Exp $ +# $Id: roundup-admin,v 1.55 2001-12-17 03:52:47 richard Exp $ -import sys -if int(sys.version[0]) < 2: - print 'Roundup requires python 2.0 or later.' - sys.exit(1) +# python version check +from roundup import version_check -import string, os, getpass, getopt, re +import sys, os, getpass, getopt, re, UserDict try: import csv except ImportError: @@ -31,10 +29,32 @@ except ImportError: from roundup import date, hyperdb, roundupdb, init, password import roundup.instance +class CommandDict(UserDict.UserDict): + '''Simple dictionary that lets us do lookups using partial keys. + + Original code submitted by Engelbert Gruber. + ''' + _marker = [] + def get(self, key, default=_marker): + if self.data.has_key(key): + return [(key, self.data[key])] + keylist = self.data.keys() + keylist.sort() + l = [] + for ki in keylist: + if ki.startswith(key): + l.append((ki, self.data[ki])) + if not l and default is self._marker: + raise KeyError, key + return l + +class UsageError(ValueError): + pass + class AdminTool: def __init__(self): - self.commands = {} + self.commands = CommandDict() for k in AdminTool.__dict__.keys(): if k[:3] == 'do_': self.commands[k[3:]] = getattr(self, k) @@ -42,9 +62,11 @@ class AdminTool: for k in AdminTool.__dict__.keys(): if k[:5] == 'help_': self.help[k[5:]] = getattr(self, k) + self.instance_home = '' + self.db = None - def usage(message=''): - if message: message = 'Problem: '+message+'\n' + def usage(self, message=''): + if message: message = 'Problem: '+message+'\n\n' print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] Help: @@ -63,9 +85,35 @@ Options: commands = [''] for command in self.commands.values(): h = command.__doc__.split('\n')[0] - commands.append(h[7:]) + commands.append(' '+h[7:]) commands.sort() - print '\n '.join(commands) + commands.append( +'Commands may be abbreviated as long as the abbreviation matches only one') + commands.append('command, e.g. l == li == lis == list.') + print '\n'.join(commands) + print + + def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')): + commands = self.commands.values() + def sortfun(a, b): + return cmp(a.__name__, b.__name__) + commands.sort(sortfun) + for command in commands: + h = command.__doc__.split('\n') + name = command.__name__[3:] + usage = h[0] + print ''' +%(name)s + %(usage)s

+

'''%locals()
+            indent = indent_re.match(h[3])
+            if indent: indent = len(indent.group(1))
+            for line in h[3:]:
+                if indent:
+                    print line[indent:]
+                else:
+                    print line
+            print '
\n' def help_all(self): print ''' @@ -131,13 +179,22 @@ Command help: initopts -- init command options all -- all available help ''' - help = self.help.get(args[0], None) - if help: - help() - return - help = self.commands.get(args[0], None) - if help: - # display the help, removing the docsring indent + topic = args[0] + + # try help_ methods + if self.help.has_key(topic): + self.help[topic]() + return 0 + + # try command docstrings + try: + l = self.commands.get(topic) + except KeyError: + print 'Sorry, no help for "%s"'%topic + return 1 + + # display the help for each match, removing the docsring indent + for name, help in l: lines = nl_re.split(help.__doc__) print lines[0] indent = indent_re.match(lines[1]) @@ -147,8 +204,7 @@ Command help: print line[indent:] else: print line - else: - print 'Sorry, no help for "%s"'%args[0] + return 0 def help_initopts(self): import roundup.templates @@ -159,8 +215,8 @@ Command help: print 'Back ends:', ', '.join(backends) - def do_init(self, instance_home, args): - '''Usage: init [template [backend [admin password]]] + def do_initialise(self, instance_home, args): + '''Usage: initialise [template [backend [admin password]]] Initialise a new Roundup instance. The command will prompt for the instance home directory (if not supplied @@ -170,6 +226,8 @@ Command help: See also initopts help. ''' + if len(args) < 1: + raise UsageError, 'Not enough arguments supplied' # select template import roundup.templates templates = roundup.templates.listTemplates() @@ -208,19 +266,33 @@ Command help: Retrieves the property value of the nodes specified by the designators. ''' + if len(args) < 2: + raise UsageError, 'Not enough arguments supplied' propname = args[0] - designators = string.split(args[1], ',') + designators = args[1].split(',') l = [] for designator in designators: + # decode the node designator try: classname, nodeid = roundupdb.splitDesignator(designator) except roundupdb.DesignatorError, message: - print 'Error: %s'%message - return 1 - if self.comma_sep: - l.append(self.db.getclass(classname).get(nodeid, propname)) - else: - print self.db.getclass(classname).get(nodeid, propname) + raise UsageError, message + + # get the class + try: + cl = self.db.getclass(classname) + except KeyError: + raise UsageError, 'invalid class "%s"'%classname + try: + if self.comma_sep: + l.append(cl.get(nodeid, propname)) + else: + print cl.get(nodeid, propname) + except IndexError: + raise UsageError, 'no such %s node "%s"'%(classname, nodeid) + except KeyError: + raise UsageError, 'no such %s property "%s"'%(classname, + propname) if self.comma_sep: print ','.join(l) return 0 @@ -232,36 +304,60 @@ Command help: Sets the property to the value for all designators given. ''' + if len(args) < 2: + raise UsageError, 'Not enough arguments supplied' from roundup import hyperdb - designators = string.split(args[0], ',') + designators = args[0].split(',') props = {} for prop in args[1:]: - key, value = prop.split('=') + if prop.find('=') == -1: + raise UsageError, 'argument "%s" not propname=value'%prop + try: + key, value = prop.split('=') + except ValueError: + raise UsageError, 'argument "%s" not propname=value'%prop props[key] = value for designator in designators: + # decode the node designator try: classname, nodeid = roundupdb.splitDesignator(designator) except roundupdb.DesignatorError, message: - print 'Error: %s'%message - return 1 - cl = self.db.getclass(classname) + raise UsageError, message + + # get the class + try: + cl = self.db.getclass(classname) + except KeyError: + raise UsageError, 'invalid class "%s"'%classname + properties = cl.getprops() for key, value in props.items(): - type = properties[key] - if isinstance(type, hyperdb.String): + proptype = properties[key] + if isinstance(proptype, hyperdb.String): continue - elif isinstance(type, hyperdb.Password): + elif isinstance(proptype, hyperdb.Password): props[key] = password.Password(value) - elif isinstance(type, hyperdb.Date): - props[key] = date.Date(value) - elif isinstance(type, hyperdb.Interval): - props[key] = date.Interval(value) - elif isinstance(type, hyperdb.Link): + elif isinstance(proptype, hyperdb.Date): + try: + props[key] = date.Date(value) + except ValueError, message: + raise UsageError, '"%s": %s'%(value, message) + elif isinstance(proptype, hyperdb.Interval): + try: + props[key] = date.Interval(value) + except ValueError, message: + raise UsageError, '"%s": %s'%(value, message) + elif isinstance(proptype, hyperdb.Link): props[key] = value - elif isinstance(type, hyperdb.Multilink): + elif isinstance(proptype, hyperdb.Multilink): props[key] = value.split(',') - apply(cl.set, (nodeid, ), props) + + # try the set + try: + apply(cl.set, (nodeid, ), props) + except (TypeError, IndexError, ValueError), message: + raise UsageError, message return 0 def do_find(self, args): @@ -271,36 +367,79 @@ Command help: Find the nodes of the given class with a given link property value. The value may be either the nodeid of the linked node, or its key value. ''' + if len(args) < 1: + raise UsageError, 'Not enough arguments supplied' classname = args[0] - cl = self.db.getclass(classname) + # get the class + try: + cl = self.db.getclass(classname) + except KeyError: + raise UsageError, 'invalid class "%s"'%classname + + # TODO: handle > 1 argument + # handle the propname=value argument + if args[1].find('=') == -1: + raise UsageError, 'argument "%s" not propname=value'%prop + try: + propname, value = args[1].split('=') + except ValueError: + raise UsageError, 'argument "%s" not propname=value'%prop - # look up the linked-to class and get the nodeid that has the value - propname, value = args[1].split('=') + # if the value isn't a number, look up the linked class to get the + # number num_re = re.compile('^\d+$') if not num_re.match(value): - propcl = cl.properties[propname] - if (not isinstance(propcl, hyperdb.Link) and not - isinstance(type, hyperdb.Multilink)): - print 'You may only "find" link properties' - return 1 - propcl = self.db.getclass(propcl.classname) - value = propcl.lookup(value) - - # now do the find - if self.comma_sep: - print ','.join(cl.find(**{propname: value})) - else: - print cl.find(**{propname: value}) + # get the property + try: + property = cl.properties[propname] + except KeyError: + raise UsageError, '%s has no property "%s"'%(classname, + propname) + + # make sure it's a link + if (not isinstance(property, hyperdb.Link) and not + isinstance(property, hyperdb.Multilink)): + raise UsageError, 'You may only "find" link properties' + + # get the linked-to class and look up the key property + link_class = self.db.getclass(property.classname) + try: + value = link_class.lookup(value) + except TypeError: + raise UsageError, '%s has no key property"'%link_class.classname + except KeyError: + raise UsageError, '%s has no entry "%s"'%(link_class.classname, + propname) + + # now do the find + try: + if self.comma_sep: + print ','.join(apply(cl.find, (), {propname: value})) + else: + print apply(cl.find, (), {propname: value}) + except KeyError: + raise UsageError, '%s has no property "%s"'%(classname, + propname) + except (ValueError, TypeError), message: + raise UsageError, message return 0 - def do_spec(self, args): - '''Usage: spec classname + def do_specification(self, args): + '''Usage: specification classname Show the properties for a classname. This lists the properties for a given class. ''' + if len(args) < 1: + raise UsageError, 'Not enough arguments supplied' classname = args[0] - cl = self.db.getclass(classname) + # get the class + try: + cl = self.db.getclass(classname) + except KeyError: + raise UsageError, 'invalid class "%s"'%classname + + # get the key property keyprop = cl.getkey() for key, value in cl.properties.items(): if keyprop == key: @@ -308,6 +447,33 @@ Command help: else: print '%s: %s'%(key, value) + def do_display(self, args): + '''Usage: display designator + Show the property values for the given node. + + This lists the properties and their associated values for the given + node. + ''' + if len(args) < 1: + raise UsageError, 'Not enough arguments supplied' + + # decode the node designator + try: + classname, nodeid = roundupdb.splitDesignator(args[0]) + except roundupdb.DesignatorError, message: + raise UsageError, message + + # get the class + try: + cl = self.db.getclass(classname) + except KeyError: + raise UsageError, 'invalid class "%s"'%classname + + # display the values + for key in cl.properties.keys(): + value = cl.get(nodeid, key) + print '%s: %s'%(key, value) + def do_create(self, args): '''Usage: create classname property=value ... Create a new entry of a given class. @@ -316,10 +482,19 @@ Command help: name=value arguments provided on the command line after the "create" command. ''' + if len(args) < 1: + raise UsageError, 'Not enough arguments supplied' from roundup import hyperdb classname = args[0] - cl = self.db.getclass(classname) + + # get the class + try: + cl = self.db.getclass(classname) + except KeyError: + raise UsageError, 'invalid class "%s"'%classname + + # now do a create props = {} properties = cl.getprops(protected = 0) if len(args) == 1: @@ -342,26 +517,46 @@ Command help: else: # use the args for prop in args[1:]: - key, value = prop.split('=') + if prop.find('=') == -1: + raise UsageError, 'argument "%s" not propname=value'%prop + try: + key, value = prop.split('=') + except ValueError: + raise UsageError, 'argument "%s" not propname=value'%prop props[key] = value # convert types for key in props.keys(): - type = properties[key] - if isinstance(type, hyperdb.Date): - props[key] = date.Date(value) - elif isinstance(type, hyperdb.Interval): - props[key] = date.Interval(value) - elif isinstance(type, hyperdb.Password): + # get the property + try: + proptype = properties[key] + except KeyError: + raise UsageError, '%s has no property "%s"'%(classname, key) + + if isinstance(proptype, hyperdb.Date): + try: + props[key] = date.Date(value) + except ValueError, message: + raise UsageError, '"%s": %s'%(value, message) + elif isinstance(proptype, hyperdb.Interval): + try: + props[key] = date.Interval(value) + except ValueError, message: + raise UsageError, '"%s": %s'%(value, message) + elif isinstance(proptype, hyperdb.Password): props[key] = password.Password(value) - elif isinstance(type, hyperdb.Multilink): + elif isinstance(proptype, hyperdb.Multilink): props[key] = value.split(',') + # check for the key property if cl.getkey() and not props.has_key(cl.getkey()): - print "You must provide the '%s' property."%cl.getkey() - else: - print apply(cl.create, (), props) + raise UsageError, "you must provide the '%s' property."%cl.getkey() + # do the actual create + try: + print apply(cl.create, (), props) + except (TypeError, IndexError, ValueError), message: + raise UsageError, message return 0 def do_list(self, args): @@ -373,17 +568,30 @@ Command help: in order: the key, "name", "title" and then the first property, alphabetically. ''' + if len(args) < 1: + raise UsageError, 'Not enough arguments supplied' classname = args[0] - cl = self.db.getclass(classname) + + # get the class + try: + cl = self.db.getclass(classname) + except KeyError: + raise UsageError, 'invalid class "%s"'%classname + + # figure the property if len(args) > 1: key = args[1] else: key = cl.labelprop() + if self.comma_sep: print ','.join(cl.list()) else: for nodeid in cl.list(): - value = cl.get(nodeid, key) + try: + value = cl.get(nodeid, key) + except KeyError: + raise UsageError, '%s has no property "%s"'%(classname, key) print "%4s: %s"%(nodeid, value) return 0 @@ -402,26 +610,54 @@ Command help: 3 usability 4 feature ''' + if len(args) < 1: + raise UsageError, 'Not enough arguments supplied' classname = args[0] - cl = self.db.getclass(classname) + + # get the class + try: + cl = self.db.getclass(classname) + except KeyError: + raise UsageError, 'invalid class "%s"'%classname + + # figure the property names to display if len(args) > 1: prop_names = args[1].split(',') + all_props = cl.getprops() + for prop_name in prop_names: + if not all_props.has_key(prop_name): + raise UsageError, '%s has no property "%s"'%(classname, + prop_name) else: prop_names = cl.getprops().keys() + + # now figure column widths props = [] - for name in prop_names: - if ':' in name: - name, width = name.split(':') - props.append((name, int(width))) + for spec in prop_names: + if ':' in spec: + try: + name, width = spec.split(':') + except (ValueError, TypeError): + raise UsageError, '"%s" not name:width'%spec + props.append((spec, int(width))) else: - props.append((name, len(name))) + props.append((spec, len(spec))) + + # now display the heading + print ' '.join([name.capitalize() for name, width in props]) - print ' '.join([string.capitalize(name) for name, width in props]) + # and the table data for nodeid in cl.list(): l = [] for name, width in props: if name != 'id': - value = str(cl.get(nodeid, name)) + try: + value = str(cl.get(nodeid, name)) + except KeyError: + # we already checked if the property is valid - a + # KeyError here means the node just doesn't have a + # value for it + value = '' else: value = str(nodeid) f = '%%-%ds'%width @@ -435,13 +671,46 @@ Command help: Lists the journal entries for the node identified by the designator. ''' + if len(args) < 1: + raise UsageError, 'Not enough arguments supplied' try: classname, nodeid = roundupdb.splitDesignator(args[0]) except roundupdb.DesignatorError, message: - print 'Error: %s'%message - return 1 + raise UsageError, message + # TODO: handle the -c option? - print self.db.getclass(classname).history(nodeid) + try: + print self.db.getclass(classname).history(nodeid) + except KeyError: + raise UsageError, 'no such class "%s"'%classname + except IndexError: + raise UsageError, 'no such %s node "%s"'%(classname, nodeid) + return 0 + + def do_commit(self, args): + '''Usage: commit + Commit all changes made to the database. + + The changes made during an interactive session are not + automatically written to the database - they must be committed + using this command. + + One-off commands on the command-line are automatically committed if + they are successful. + ''' + self.db.commit() + return 0 + + def do_rollback(self, args): + '''Usage: rollback + Undo all changes that are pending commit to the database. + + The changes made during an interactive session are not + automatically written to the database - they must be committed + manually. This command undoes all those changes, so a commit + immediately after would make no changes to the database. + ''' + self.db.rollback() return 0 def do_retire(self, args): @@ -451,14 +720,20 @@ Command help: This action indicates that a particular node is not to be retrieved by the list or find commands, and its key value may be re-used. ''' - designators = string.split(args[0], ',') + if len(args) < 1: + raise UsageError, 'Not enough arguments supplied' + designators = args[0].split(',') for designator in designators: try: classname, nodeid = roundupdb.splitDesignator(designator) except roundupdb.DesignatorError, message: - print 'Error: %s'%message - return 1 - self.db.getclass(classname).retire(nodeid) + raise UsageError, message + try: + self.db.getclass(classname).retire(nodeid) + except KeyError: + raise UsageError, 'no such class "%s"'%classname + except IndexError: + raise UsageError, 'no such %s node "%s"'%(classname, nodeid) return 0 def do_export(self, args): @@ -470,9 +745,8 @@ Command help: directory. The journals are not exported. ''' if len(args) < 2: - print do_export.__doc__ - return 1 - classes = string.split(args[0], ',') + raise UsageError, 'Not enough arguments supplied' + classes = args[0].split(',') dir = args[1] # use the csv parser if we can - it's faster @@ -481,22 +755,25 @@ Command help: # do all the classes specified for classname in classes: - cl = self.db.getclass(classname) + try: + cl = self.db.getclass(classname) + except KeyError: + raise UsageError, 'no such class "%s"'%classname f = open(os.path.join(dir, classname+'.csv'), 'w') - f.write(string.join(cl.properties.keys(), ':') + '\n') + f.write(':'.join(cl.properties.keys()) + '\n') # all nodes for this class properties = cl.properties.items() for nodeid in cl.list(): l = [] - for prop, type in properties: + for prop, proptype in properties: value = cl.get(nodeid, prop) # convert data where needed - if isinstance(type, hyperdb.Date): + if isinstance(proptype, hyperdb.Date): value = value.get_tuple() - elif isinstance(type, hyperdb.Interval): + elif isinstance(proptype, hyperdb.Interval): value = value.get_tuple() - elif isinstance(type, hyperdb.Password): + elif isinstance(proptype, hyperdb.Password): value = str(value) l.append(repr(value)) @@ -526,17 +803,20 @@ Command help: the old data.) ''' if len(args) < 2: - print do_import.__doc__ - return 1 + raise UsageError, 'Not enough arguments supplied' if csv is None: - print 'Sorry, you need the csv module to use this function.' - print 'Get it from: http://www.object-craft.com.au/projects/csv/' - return 1 + raise UsageError, \ + 'Sorry, you need the csv module to use this function.\n'\ + 'Get it from: http://www.object-craft.com.au/projects/csv/' from roundup import hyperdb # ensure that the properties and the CSV file headings match - cl = self.db.getclass(args[0]) + classname = args[0] + try: + cl = self.db.getclass(classname) + except KeyError: + raise UsageError, 'no such class "%s"'%classname f = open(args[1]) p = csv.parser(field_sep=':') file_props = p.parse(f.readline()) @@ -545,8 +825,8 @@ Command help: m.sort() props.sort() if m != props: - print 'Import file doesn\'t define the same properties as "%s".'%args[0] - return 1 + raise UsageError, 'Import file doesn\'t define the same '\ + 'properties as "%s".'%args[0] # loop through the file and create a node for each entry n = range(len(props)) @@ -566,13 +846,13 @@ Command help: value = eval(l[i]) # Figure the property for this column key = file_props[i] - type = cl.properties[key] + proptype = cl.properties[key] # Convert for property type - if isinstance(type, hyperdb.Date): + if isinstance(proptype, hyperdb.Date): value = date.Date(value) - elif isinstance(type, hyperdb.Interval): + elif isinstance(proptype, hyperdb.Interval): value = date.Interval(value) - elif isinstance(type, hyperdb.Password): + elif isinstance(proptype, hyperdb.Password): pwd = password.Password() pwd.unpack(value) value = pwd @@ -601,36 +881,54 @@ Command help: self.help_all() return 0 + # figure what the command is + try: + functions = self.commands.get(command) + except KeyError: + # not a valid command + print 'Unknown command "%s" ("help commands" for a list)'%command + return 1 + + # check for multiple matches + if len(functions) > 1: + print 'Multiple commands match "%s": %s'%(command, + ', '.join([i[0] for i in functions])) + return 1 + command, function = functions[0] + # make sure we have an instance_home while not self.instance_home: self.instance_home = raw_input('Enter instance home: ').strip() # before we open the db, we may be doing an init - if command == 'init': - return self.do_init(self.instance_home, args) - - function = self.commands.get(command, None) - - # not a valid command - if function is None: - print 'Unknown command "%s" ("help commands" for a list)'%command - return 1 + if command == 'initialise': + return self.do_initialise(self.instance_home, args) # get the instance - instance = roundup.instance.open(self.instance_home) - self.db = instance.open('admin') - - if len(args) < 2: - print function.__doc__ + try: + instance = roundup.instance.open(self.instance_home) + except ValueError, message: + self.instance_home = '' + print "Couldn't open instance: %s"%message return 1 + # only open the database once! + if not self.db: + self.db = instance.open('admin') + # do the command + ret = 0 try: - return function(args[1:]) - finally: - self.db.close() - - return 1 + ret = function(args[1:]) + except UsageError, message: + print 'Error: %s'%message + print function.__doc__ + ret = 1 + except: + import traceback + traceback.print_exc() + ret = 1 + return ret def interactive(self, ws_re=re.compile(r'\s+')): '''Run in an interactive mode @@ -646,15 +944,27 @@ Command help: try: command = raw_input('roundup> ') except EOFError: - print '.. exit' - return 0 + print 'exit...' + break + if not command: continue args = ws_re.split(command) if not args: continue - if args[0] in ('quit', 'exit'): return 0 + if args[0] in ('quit', 'exit'): break self.run_command(args) + # exit.. check for transactions + if self.db and self.db.transactions: + commit = raw_input("There are unsaved changes. Commit them (y/N)? ") + if commit[0].lower() == 'y': + self.db.commit() + return 0 + def main(self): - opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc') + try: + opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc') + except getopt.GetoptError, e: + self.usage(str(e)) + return 1 # handle command-line args self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '') @@ -667,7 +977,7 @@ Command help: self.comma_sep = 0 for opt, arg in opts: if opt == '-h': - usage() + self.usage() return 0 if opt == '-i': self.instance_home = arg @@ -675,10 +985,13 @@ Command help: self.comma_sep = 1 # if no command - go interactive + ret = 0 if not args: - return self.interactive() - - self.run_command(args) + self.interactive() + else: + ret = self.run_command(args) + if self.db: self.db.commit() + return ret if __name__ == '__main__': @@ -687,6 +1000,97 @@ if __name__ == '__main__': # # $Log: not supported by cvs2svn $ +# Revision 1.54 2001/12/15 23:09:23 richard +# Some cleanups in roundup-admin, also made it work again... +# +# Revision 1.53 2001/12/13 00:20:00 richard +# . Centralised the python version check code, bumped version to 2.1.1 (really +# needs to be 2.1.2, but that isn't released yet :) +# +# Revision 1.52 2001/12/12 21:47:45 richard +# . Message author's name appears in From: instead of roundup instance name +# (which still appears in the Reply-To:) +# . envelope-from is now set to the roundup-admin and not roundup itself so +# delivery reports aren't sent to roundup (thanks Patrick Ohly) +# +# Revision 1.51 2001/12/10 00:57:38 richard +# From CHANGES: +# . Added the "display" command to the admin tool - displays a node's values +# . #489760 ] [issue] only subject +# . fixed the doc/index.html to include the quoting in the mail alias. +# +# Also: +# . fixed roundup-admin so it works with transactions +# . disabled the back_anydbm module if anydbm tries to use dumbdbm +# +# Revision 1.50 2001/12/02 05:06:16 richard +# . We now use weakrefs in the Classes to keep the database reference, so +# the close() method on the database is no longer needed. +# I bumped the minimum python requirement up to 2.1 accordingly. +# . #487480 ] roundup-server +# . #487476 ] INSTALL.txt +# +# I also cleaned up the change message / post-edit stuff in the cgi client. +# There's now a clearly marked "TODO: append the change note" where I believe +# the change note should be added there. The "changes" list will obviously +# have to be modified to be a dict of the changes, or somesuch. +# +# More testing needed. +# +# Revision 1.49 2001/12/01 07:17:50 richard +# . We now have basic transaction support! Information is only written to +# the database when the commit() method is called. Only the anydbm +# backend is modified in this way - neither of the bsddb backends have been. +# The mail, admin and cgi interfaces all use commit (except the admin tool +# doesn't have a commit command, so interactive users can't commit...) +# . Fixed login/registration forwarding the user to the right page (or not, +# on a failure) +# +# Revision 1.48 2001/11/27 22:32:03 richard +# typo +# +# Revision 1.47 2001/11/26 22:55:56 richard +# Feature: +# . Added INSTANCE_NAME to configuration - used in web and email to identify +# the instance. +# . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup +# signature info in e-mails. +# . Some more flexibility in the mail gateway and more error handling. +# . Login now takes you to the page you back to the were denied access to. +# +# Fixed: +# . Lots of bugs, thanks Roché and others on the devel mailing list! +# +# Revision 1.46 2001/11/21 03:40:54 richard +# more new property handling +# +# Revision 1.45 2001/11/12 22:51:59 jhermann +# Fixed option & associated error handling +# +# Revision 1.44 2001/11/12 22:01:06 richard +# Fixed issues with nosy reaction and author copies. +# +# Revision 1.43 2001/11/09 22:33:28 richard +# More error handling fixes. +# +# Revision 1.42 2001/11/09 10:11:08 richard +# . roundup-admin now handles all hyperdb exceptions +# +# Revision 1.41 2001/11/09 01:25:40 richard +# Should parse with python 1.5.2 now. +# +# Revision 1.40 2001/11/08 04:42:00 richard +# Expanded the already-abbreviated "initialise" and "specification" commands, +# and added a comment to the command help about the abbreviation. +# +# Revision 1.39 2001/11/08 04:29:59 richard +# roundup-admin now accepts abbreviated commands (eg. l = li = lis = list) +# [thanks Engelbert Gruber for the inspiration] +# +# Revision 1.38 2001/11/05 23:45:40 richard +# Fixed newuser_action so it sets the cookie with the unencrypted password. +# Also made it present nicer error messages (not tracebacks). +# # Revision 1.37 2001/10/23 01:00:18 richard # Re-enabled login and registration access after lopping them off via # disabling access for anonymous users.