X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup-admin;h=6a1d5c078efa1716ccc4a5e9e17d209857a4a983;hb=a25e89b84b3a5dc04adc4450a85424171b3bb7f9;hp=86bc4ae7441976703c121d78f222104a6edf2e93;hpb=2550432a24423f5f9b38085a6a9c256cfeebfd19;p=roundup.git diff --git a/roundup-admin b/roundup-admin index 86bc4ae..6a1d5c0 100755 --- a/roundup-admin +++ b/roundup-admin @@ -1,4 +1,4 @@ -#! /usr/bin/python +#! /usr/bin/env python # # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/) # This module is free software, and you may redistribute it and/or modify @@ -16,42 +16,107 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundup-admin,v 1.21 2001-10-05 02:23:24 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 -from roundup import date, roundupdb, init +import sys, os, getpass, getopt, re, UserDict +try: + import csv +except ImportError: + csv = None +from roundup import date, hyperdb, roundupdb, init, password import roundup.instance -def usage(message=''): - if message: message = 'Problem: '+message+'\n' - commands = [] - for command in figureCommands().values(): - h = command.__doc__.split('\n')[0] - commands.append(h[7:]) - commands.sort() - print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] - -Commands: - %s +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 = CommandDict() + for k in AdminTool.__dict__.keys(): + if k[:3] == 'do_': + self.commands[k[3:]] = getattr(self, k) + self.help = {} + 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(self, message=''): + if message: message = 'Problem: '+message+'\n\n' + print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] + Help: roundup-admin -h roundup-admin help -- this help roundup-admin help -- command-specific help - roundup-admin morehelp -- even more detailed help + roundup-admin help all -- all available help Options: -i instance home -- specify the issue tracker "home directory" to administer -u -- the user[:password] to use for commands - -c -- when outputting lists of data, just comma-separate them'''%( -message, '\n '.join(commands)) + -c -- when outputting lists of data, just comma-separate them'''%message + self.help_commands() + + def help_commands(self): + print 'Commands:', + commands = [''] + for command in self.commands.values(): + h = command.__doc__.split('\n')[0] + commands.append(' '+h[7:]) + commands.sort() + 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 moreusage(message=''): - usage(message) - print ''' + def help_all(self): + print ''' All commands (except help) require an instance specifier. This is just the path to the roundup instance you're working with. A roundup instance is where roundup keeps the database and configuration file that defines an issue @@ -100,355 +165,1015 @@ Date format examples: Command help: ''' - for name, command in figureCommands().items(): - print '%s:'%name - print ' ',command.__doc__ + for name, command in self.commands.items(): + print '%s:'%name + print ' ',command.__doc__ -def do_init(instance_home, args): - '''Usage: init [template [backend [admin password]]] - Initialise a new Roundup instance. + def do_help(self, args, nl_re=re.compile('[\r\n]'), + indent_re=re.compile(r'^(\s+)\S+')): + '''Usage: help topic + Give help about topic. - The command will prompt for the instance home directory (if not supplied - through INSTANCE_HOME or the -i option. The template, backend and admin - password may be specified on the command-line as arguments, in that order. - ''' - # select template - import roundup.templates - templates = roundup.templates.listTemplates() - template = len(args) > 1 and args[1] or '' - if template not in templates: + commands -- list commands + -- help specific to a command + initopts -- init command options + all -- all available help + ''' + 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]) + if indent: indent = len(indent.group(1)) + for line in lines[1:]: + if indent: + print line[indent:] + else: + print line + return 0 + + def help_initopts(self): + import roundup.templates + templates = roundup.templates.listTemplates() print 'Templates:', ', '.join(templates) - while template not in templates: - template = raw_input('Select template [extended]: ').strip() - if not template: - template = 'extended' - - import roundup.backends - backends = roundup.backends.__all__ - backend = len(args) > 2 and args[2] or '' - if backend not in backends: + import roundup.backends + backends = roundup.backends.__all__ print 'Back ends:', ', '.join(backends) - while backend not in backends: - backend = raw_input('Select backend [anydbm]: ').strip() - if not backend: - backend = 'anydbm' - if len(args) > 3: - adminpw = confirm = args[3] - else: - adminpw = '' - confirm = 'x' - while adminpw != confirm: - adminpw = getpass.getpass('Admin Password: ') - confirm = getpass.getpass(' Confirm: ') - init.init(instance_home, template, backend, adminpw) - return 0 - - -def do_get(db, args): - '''Usage: get property designator[,designator]* - Get the given property of one or more designator(s). - - Retrieves the property value of the nodes specified by the designators. - ''' - propname = args[0] - designators = string.split(args[1], ',') - # TODO: handle the -c option - for designator in designators: - classname, nodeid = roundupdb.splitDesignator(designator) - print db.getclass(classname).get(nodeid, propname) - return 0 -def do_set(db, args): - '''Usage: set designator[,designator]* propname=value ... - Set the given property of one or more designator(s). + def do_initialise(self, instance_home, args): + '''Usage: initialise [template [backend [admin password]]] + Initialise a new Roundup instance. - Sets the property to the value for all designators given. - ''' - from roundup import hyperdb - - designators = string.split(args[0], ',') - props = {} - for prop in args[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 isinstance(type, hyperdb.String): - continue - 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): - props[key] = value - elif isinstance(type, hyperdb.Multilink): - props[key] = value.split(',') - apply(cl.set, (nodeid, ), props) - return 0 + The command will prompt for the instance home directory (if not supplied + through INSTANCE_HOME or the -i option. The template, backend and admin + password may be specified on the command-line as arguments, in that + order. -def do_find(db, args): - '''Usage: find classname propname=value ... - Find the nodes of the given class with a given property value. + See also initopts help. + ''' + if len(args) < 1: + raise UsageError, 'Not enough arguments supplied' + # select template + import roundup.templates + templates = roundup.templates.listTemplates() + template = len(args) > 1 and args[1] or '' + if template not in templates: + print 'Templates:', ', '.join(templates) + while template not in templates: + template = raw_input('Select template [classic]: ').strip() + if not template: + template = 'classic' - Find the nodes of the given class with a given property value. The - value may be either the nodeid of the linked node, or its key value. - ''' - classname = args[0] - cl = db.getclass(classname) - - # look up the linked-to class and get the nodeid that has the value - propname, value = args[1].split('=') - num_re = re.compile('^\d+$') - if num_re.match(value): - nodeid = value - else: - propcl = cl.properties[propname].classname - propcl = db.getclass(propcl) - nodeid = propcl.lookup(value) - - # now do the find - # TODO: handle the -c option - print cl.find(**{propname: nodeid}) - return 0 - -def do_spec(db, args): - '''Usage: spec classname - Show the properties for a classname. - - This lists the properties for a given class. - ''' - classname = args[0] - cl = db.getclass(classname) - keyprop = cl.getkey() - for key, value in cl.properties.items(): - if keyprop == key: - print '%s: %s (key property)'%(key, value) + import roundup.backends + backends = roundup.backends.__all__ + backend = len(args) > 2 and args[2] or '' + if backend not in backends: + print 'Back ends:', ', '.join(backends) + while backend not in backends: + backend = raw_input('Select backend [anydbm]: ').strip() + if not backend: + backend = 'anydbm' + if len(args) > 3: + adminpw = confirm = args[3] else: - print '%s: %s'%(key, value) + adminpw = '' + confirm = 'x' + while adminpw != confirm: + adminpw = getpass.getpass('Admin Password: ') + confirm = getpass.getpass(' Confirm: ') + init.init(instance_home, template, backend, adminpw) + return 0 -def do_create(db, args, pretty_re=re.compile(r'')): - '''Usage: create classname property=value ... - Create a new entry of a given class. - This creates a new entry of the given class using the property - name=value arguments provided on the command line after the "create" - command. - ''' - from roundup import hyperdb - - classname = args[0] - cl = db.getclass(classname) - props = {} - properties = cl.getprops(protected = 0) - if len(args) == 1: - # ask for the properties - for key, value in properties.items(): - if key == 'id': continue - m = pretty_re.match(str(value)) - if m: - value = m.group(1) - value = raw_input('%s (%s): '%(key.capitalize(), value)) - if value: - props[key] = value - else: - # use the args + def do_get(self, args): + '''Usage: get property designator[,designator]* + Get the given property of one or more designator(s). + + 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 = args[1].split(',') + l = [] + for designator in designators: + # decode the node designator + try: + classname, nodeid = roundupdb.splitDesignator(designator) + except roundupdb.DesignatorError, message: + 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 + + + def do_set(self, args): + '''Usage: set designator[,designator]* propname=value ... + Set the given property of one or more designator(s). + + 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 = args[0].split(',') + props = {} for prop in args[1:]: - key, value = prop.split('=') - 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.Multilink): - props[key] = value.split(',') - - 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) - - return 0 - -def do_list(db, args): - '''Usage: list classname [property] - List the instances of a class. - - Lists all instances of the given class along. If the property is not - specified, the "label" property is used. The label property is tried - in order: the key, "name", "title" and then the first property, - alphabetically. - ''' - classname = args[0] - cl = db.getclass(classname) - if len(args) > 1: - key = args[1] - else: - key = cl.labelprop() - # TODO: handle the -c option - for nodeid in cl.list(): - value = cl.get(nodeid, key) - print "%4s: %s"%(nodeid, value) - return 0 - -def do_history(db, args): - '''Usage: history designator - Show the history entries of a designator. - - Lists the journal entries for the node identified by the designator. - ''' - classname, nodeid = roundupdb.splitDesignator(args[0]) - # TODO: handle the -c option - print db.getclass(classname).history(nodeid) - return 0 + 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: + raise UsageError, message -def do_retire(db, args): - '''Usage: retire designator[,designator]* - Retire the node specified by designator. + # get the class + try: + cl = self.db.getclass(classname) + except KeyError: + raise UsageError, 'invalid class "%s"'%classname - 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], ',') - for designator in designators: - classname, nodeid = roundupdb.splitDesignator(designator) - db.getclass(classname).retire(nodeid) - return 0 - -def do_freshen(db, args): - '''Usage: freshen - Freshen an existing instance. **DO NOT USE** - - This currently kills databases!!!! - - This action should generally not be used. It reads in an instance - database and writes it again. In the future, is may also update - instance code to account for changes in templates. It's probably wise - not to use it anyway. Until we're sure it won't break things... - ''' -# for classname, cl in db.classes.items(): -# properties = cl.properties.items() -# for nodeid in cl.list(): -# node = {} -# for name, type in properties: -# isinstance( if type, hyperdb.Multilink): -# node[name] = cl.get(nodeid, name, []) -# else: -# node[name] = cl.get(nodeid, name, None) -# db.setnode(classname, nodeid, node) - return 1 - -def figureCommands(): - d = {} - for k, v in globals().items(): - if k[:3] == 'do_': - d[k[3:]] = v - return d - -def printInitOptions(): - import roundup.templates - templates = roundup.templates.listTemplates() - print 'Templates:', ', '.join(templates) - import roundup.backends - backends = roundup.backends.__all__ - print 'Back ends:', ', '.join(backends) - -def main(): - opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc') - - # handle command-line args - instance_home = os.environ.get('ROUNDUP_INSTANCE', '') - name = password = '' - if os.environ.has_key('ROUNDUP_LOGIN'): - l = os.environ['ROUNDUP_LOGIN'].split(':') - name = l[0] - if len(l) > 1: - password = l[1] - comma_sep = 0 - for opt, arg in opts: - if opt == '-h': - usage() - return 0 - if opt == '-i': - instance_home = arg - if opt == '-u': - l = arg.split(':') - name = l[0] - if len(l) > 1: - password = l[1] - if opt == '-c': - comma_sep = 1 - - # figure the command - if not args: - usage('No command specified') - return 1 - command = args[0] - - # handle help now - if command == 'help': - if len(args)>1: - command = figureCommands().get(args[1], None) - if not command: - usage('no such command "%s"'%args[1]) - return 1 - print command.__doc__ - if args[1] == 'init': - printInitOptions() - return 0 - usage() + properties = cl.getprops() + for key, value in props.items(): + proptype = properties[key] + if isinstance(proptype, hyperdb.String): + continue + elif isinstance(proptype, hyperdb.Password): + props[key] = password.Password(value) + 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(proptype, hyperdb.Multilink): + props[key] = value.split(',') + + # try the set + try: + apply(cl.set, (nodeid, ), props) + except (TypeError, IndexError, ValueError), message: + raise UsageError, message + return 0 + + def do_find(self, args): + '''Usage: find classname propname=value ... + Find the nodes of the given class with a given link property value. + + 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] + # 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 + + # 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): + # 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_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] + # 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: + print '%s: %s (key property)'%(key, value) + 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. + + This creates a new entry of the given class using the property + 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] + + # 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: + # ask for the properties + for key, value in properties.items(): + if key == 'id': continue + name = value.__class__.__name__ + if isinstance(value , hyperdb.Password): + again = None + while value != again: + value = getpass.getpass('%s (Password): '%key.capitalize()) + again = getpass.getpass(' %s (Again): '%key.capitalize()) + if value != again: print 'Sorry, try again...' + if value: + props[key] = value + else: + value = raw_input('%s (%s): '%(key.capitalize(), name)) + if value: + props[key] = value + else: + # use the args + for prop in args[1:]: + 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(): + # 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(proptype, hyperdb.Multilink): + props[key] = value.split(',') + + # check for the key property + if cl.getkey() and not props.has_key(cl.getkey()): + 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): + '''Usage: list classname [property] + List the instances of a class. + + Lists all instances of the given class. If the property is not + specified, the "label" property is used. The label property is tried + 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] + + # 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(): + try: + value = cl.get(nodeid, key) + except KeyError: + raise UsageError, '%s has no property "%s"'%(classname, key) + print "%4s: %s"%(nodeid, value) + return 0 + + def do_table(self, args): + '''Usage: table classname [property[,property]*] + List the instances of a class in tabular form. + + Lists all instances of the given class. If the properties are not + specified, all properties are displayed. By default, the column widths + are the width of the property names. The width may be explicitly defined + by defining the property as "name:width". For example:: + roundup> table priority id,name:10 + Id Name + 1 fatal-bug + 2 bug + 3 usability + 4 feature + ''' + if len(args) < 1: + raise UsageError, 'Not enough arguments supplied' + classname = args[0] + + # 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 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((spec, len(spec))) + + # now display the heading + print ' '.join([name.capitalize() for name, width in props]) + + # and the table data + for nodeid in cl.list(): + l = [] + for name, width in props: + if name != 'id': + 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 + l.append(f%value[:width]) + print ' '.join(l) + return 0 + + def do_history(self, args): + '''Usage: history designator + Show the history entries of a designator. + + 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: + raise UsageError, message + + # TODO: handle the -c option? + 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): + '''Usage: retire designator[,designator]* + Retire the node specified by designator. + + 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. + ''' + 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: + 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): + '''Usage: export class[,class] destination_dir + Export the database to tab-separated-value files. + + This action exports the current data from the database into + tab-separated-value files that are placed in the nominated destination + directory. The journals are not exported. + ''' + if len(args) < 2: + raise UsageError, 'Not enough arguments supplied' + classes = args[0].split(',') + dir = args[1] + + # use the csv parser if we can - it's faster + if csv is not None: + p = csv.parser(field_sep=':') + + # do all the classes specified + for classname in classes: + 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(':'.join(cl.properties.keys()) + '\n') + + # all nodes for this class + properties = cl.properties.items() + for nodeid in cl.list(): + l = [] + for prop, proptype in properties: + value = cl.get(nodeid, prop) + # convert data where needed + if isinstance(proptype, hyperdb.Date): + value = value.get_tuple() + elif isinstance(proptype, hyperdb.Interval): + value = value.get_tuple() + elif isinstance(proptype, hyperdb.Password): + value = str(value) + l.append(repr(value)) + + # now write + if csv is not None: + f.write(p.join(l) + '\n') + else: + # escape the individual entries to they're valid CSV + m = [] + for entry in l: + if '"' in entry: + entry = '""'.join(entry.split('"')) + if ':' in entry: + entry = '"%s"'%entry + m.append(entry) + f.write(':'.join(m) + '\n') return 0 - if command == 'morehelp': - moreusage() + + def do_import(self, args): + '''Usage: import class file + Import the contents of the tab-separated-value file. + + The file must define the same properties as the class (including having + a "header" line with those property names.) The new nodes are added to + the existing database - if you want to create a new database using the + imported data, then create a new database (or, tediously, retire all + the old data.) + ''' + if len(args) < 2: + raise UsageError, 'Not enough arguments supplied' + if csv is None: + 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 + 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()) + props = cl.properties.keys() + m = file_props[:] + m.sort() + props.sort() + if m != props: + 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)) + while 1: + line = f.readline() + if not line: break + + # parse lines until we get a complete entry + while 1: + l = p.parse(line) + if l: break + + # make the new node's property map + d = {} + for i in n: + # Use eval to reverse the repr() used to output the CSV + value = eval(l[i]) + # Figure the property for this column + key = file_props[i] + proptype = cl.properties[key] + # Convert for property type + if isinstance(proptype, hyperdb.Date): + value = date.Date(value) + elif isinstance(proptype, hyperdb.Interval): + value = date.Interval(value) + elif isinstance(proptype, hyperdb.Password): + pwd = password.Password() + pwd.unpack(value) + value = pwd + if value is not None: + d[key] = value + + # and create the new node + apply(cl.create, (), d) return 0 - # make sure we have an instance_home - while not instance_home: - instance_home = raw_input('Enter instance home: ').strip() + def run_command(self, args): + '''Run a single command + ''' + command = args[0] - # before we open the db, we may be doing an init - if command == 'init': - return do_init(instance_home, args) + # handle help now + if command == 'help': + if len(args)>1: + self.do_help(args[1:]) + return 0 + self.do_help(['help']) + return 0 + if command == 'morehelp': + self.do_help(['help']) + self.help_commands() + 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() - # open the database - if command in ('create', 'set', 'retire', 'freshen'): - while not name: - name = raw_input('Login name: ') - while not password: - password = getpass.getpass(' password: ') + # before we open the db, we may be doing an init + if command == 'initialise': + return self.do_initialise(self.instance_home, args) - # get the instance - instance = roundup.instance.open(instance_home) + # get the instance + try: + instance = roundup.instance.open(self.instance_home) + except ValueError, message: + self.instance_home = '' + print "Couldn't open instance: %s"%message + return 1 - function = figureCommands().get(command, None) + # only open the database once! + if not self.db: + self.db = instance.open('admin') + + # do the command + ret = 0 + try: + 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 + ''' + print 'Roundup {version} ready for input.' + print 'Type "help" for help.' + try: + import readline + except ImportError: + print "Note: command history and editing not available" + + while 1: + try: + command = raw_input('roundup> ') + except EOFError: + print 'exit...' + break + if not command: continue + args = ws_re.split(command) + if not args: continue + 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 - # not a valid command - if function is None: - usage('Unknown command "%s"'%command) - return 1 + def main(self): + try: + opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc') + except getopt.GetoptError, e: + self.usage(str(e)) + return 1 - db = instance.open(name or 'admin') - try: - return function(db, args[1:]) - finally: - db.close() + # handle command-line args + self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '') + name = password = '' + if os.environ.has_key('ROUNDUP_LOGIN'): + l = os.environ['ROUNDUP_LOGIN'].split(':') + name = l[0] + if len(l) > 1: + password = l[1] + self.comma_sep = 0 + for opt, arg in opts: + if opt == '-h': + self.usage() + return 0 + if opt == '-i': + self.instance_home = arg + if opt == '-c': + self.comma_sep = 1 - return 1 + # if no command - go interactive + ret = 0 + if not args: + self.interactive() + else: + ret = self.run_command(args) + if self.db: self.db.commit() + return ret if __name__ == '__main__': - sys.exit(main()) + tool = AdminTool() + sys.exit(tool.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. +# Major re-org of the htmltemplate code, cleaning it up significantly. Fixed +# a couple of bugs while I was there. Probably introduced a couple, but +# things seem to work OK at the moment. +# +# Revision 1.36 2001/10/21 00:45:15 richard +# Added author identification to e-mail messages from roundup. +# +# Revision 1.35 2001/10/20 11:58:48 richard +# Catch errors in login - no username or password supplied. +# Fixed editing of password (Password property type) thanks Roch'e Compaan. +# +# Revision 1.34 2001/10/18 02:16:42 richard +# Oops, committed the admin script with the wierd #! line. +# Also, made the thing into a class to reduce parameter passing. +# Nuked the leading whitespace from the help __doc__ displays too. +# +# Revision 1.33 2001/10/17 23:13:19 richard +# Did a fair bit of work on the admin tool. Now has an extra command "table" +# which displays node information in a tabular format. Also fixed import and +# export so they work. Removed freshen. +# Fixed quopri usage in mailgw from bug reports. +# +# Revision 1.32 2001/10/17 06:57:29 richard +# Interactive startup blurb - need to figure how to get the version in there. +# +# Revision 1.31 2001/10/17 06:17:26 richard +# Now with readline support :) +# +# Revision 1.30 2001/10/17 06:04:00 richard +# Beginnings of an interactive mode for roundup-admin +# +# Revision 1.29 2001/10/16 03:48:01 richard +# admin tool now complains if a "find" is attempted with a non-link property. +# +# Revision 1.28 2001/10/13 00:07:39 richard +# More help in admin tool. +# +# Revision 1.27 2001/10/11 23:43:04 richard +# Implemented the comma-separated printing option in the admin tool. +# Fixed a typo (more of a vim-o actually :) in mailgw. +# +# Revision 1.26 2001/10/11 05:03:51 richard +# Marked the roundup-admin import/export as experimental since they're not fully +# operational. +# +# Revision 1.25 2001/10/10 04:12:32 richard +# The setup.cfg file is just causing pain. Away it goes. +# +# Revision 1.24 2001/10/10 03:54:57 richard +# Added database importing and exporting through CSV files. +# Uses the csv module from object-craft for exporting if it's available. +# Requires the csv module for importing. +# +# Revision 1.23 2001/10/09 23:36:25 richard +# Spit out command help if roundup-admin command doesn't get an argument. +# +# Revision 1.22 2001/10/09 07:25:59 richard +# Added the Password property type. See "pydoc roundup.password" for +# implementation details. Have updated some of the documentation too. +# +# Revision 1.21 2001/10/05 02:23:24 richard +# . roundup-admin create now prompts for property info if none is supplied +# on the command-line. +# . hyperdb Class getprops() method may now return only the mutable +# properties. +# . Login now uses cookies, which makes it a whole lot more flexible. We can +# now support anonymous user access (read-only, unless there's an +# "anonymous" user, in which case write access is permitted). Login +# handling has been moved into cgi_client.Client.main() +# . The "extended" schema is now the default in roundup init. +# . The schemas have had their page headings modified to cope with the new +# login handling. Existing installations should copy the interfaces.py +# file from the roundup lib directory to their instance home. +# . Incorrectly had a Bizar Software copyright on the cgitb.py module from +# Ping - has been removed. +# . Fixed a whole bunch of places in the CGI interface where we should have +# been returning Not Found instead of throwing an exception. +# . Fixed a deviation from the spec: trying to modify the 'id' property of +# an item now throws an exception. +# # Revision 1.20 2001/10/04 02:12:42 richard # Added nicer command-line item adding: passing no arguments will enter an # interactive more which asks for each property in turn. While I was at it, I