X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fadmin.py;h=e89f8643df326727cf15b1142a8be69d564fa09b;hb=d53f951d3021166e7f3bebc037708accdecb7ff5;hp=663bea7dbfcee51727db69d4f57b6659cf4aacc5;hpb=c4ca2f382c0d4538c855cfe0ebfa127c638ced8c;p=roundup.git diff --git a/roundup/admin.py b/roundup/admin.py index 663bea7..e89f864 100644 --- a/roundup/admin.py +++ b/roundup/admin.py @@ -16,14 +16,15 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: admin.py,v 1.13 2002-05-30 23:58:14 richard Exp $ - -import sys, os, getpass, getopt, re, UserDict, shlex, shutil -try: - import csv -except ImportError: - csv = None -from roundup import date, hyperdb, roundupdb, init, password, token +# $Id: admin.py,v 1.63 2004-03-21 23:39:08 richard Exp $ + +'''Administration commands for maintaining Roundup trackers. +''' +__docformat__ = 'restructuredtext' + +import sys, os, getpass, getopt, re, UserDict, shutil, rfc822 +from roundup import date, hyperdb, roundupdb, init, password, token, rcsv +from roundup import __version__ as roundup_version import roundup.instance from roundup.i18n import _ @@ -50,7 +51,17 @@ class UsageError(ValueError): pass class AdminTool: + ''' A collection of methods used in maintaining Roundup trackers. + + Typically these methods are accessed through the roundup-admin + script. The main() method provided on this class gives the main + loop for the roundup-admin script. + Actions are defined by do_*() methods, with help for the action + given in the method docstring. + + Additional help may be supplied by help_*() methods. + ''' def __init__(self): self.commands = CommandDict() for k in AdminTool.__dict__.keys(): @@ -60,7 +71,7 @@ class AdminTool: for k in AdminTool.__dict__.keys(): if k[:5] == 'help_': self.help[k[5:]] = getattr(self, k) - self.instance_home = '' + self.tracker_home = '' self.db = None def get_class(self, classname): @@ -72,34 +83,56 @@ class AdminTool: raise UsageError, _('no such class "%(classname)s"')%locals() def props_from_args(self, args): + ''' Produce a dictionary of prop: value from the args list. + + The args list is specified as ``prop=value prop=value ...``. + ''' props = {} for arg in args: if arg.find('=') == -1: - raise UsageError, _('argument "%(arg)s" not propname=value')%locals() - try: - key, value = arg.split('=') - except ValueError: - raise UsageError, _('argument "%(arg)s" not propname=value')%locals() - props[key] = value + raise UsageError, _('argument "%(arg)s" not propname=value' + )%locals() + l = arg.split('=') + if len(l) < 2: + raise UsageError, _('argument "%(arg)s" not propname=value' + )%locals() + key, value = l[0], '='.join(l[1:]) + if value: + props[key] = value + else: + props[key] = None return props def usage(self, message=''): + ''' Display a simple usage message. + ''' if message: - message = _('Problem: %(message)s)\n\n')%locals() - print _('''%(message)sUsage: roundup-admin [-i instance home] [-u login] [-c] + message = _('Problem: %(message)s\n\n')%locals() + print _('''%(message)sUsage: roundup-admin [options] [ ] + +Options: + -i instance home -- specify the issue tracker "home directory" to administer + -u -- the user[:password] to use for commands + -d -- print full designators not just class id numbers + -c -- when outputting lists of data, comma-separate them. + Same as '-S ","'. + -S -- when outputting lists of data, string-separate them + -s -- when outputting lists of data, space-separate them. + Same as '-S " "'. + + Only one of -s, -c or -S can be specified. Help: roundup-admin -h roundup-admin help -- this help roundup-admin help -- command-specific 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''')%locals() +''')%locals() self.help_commands() def help_commands(self): + ''' List the commands available with their precis help. + ''' print _('Commands:'), commands = [''] for command in self.commands.values(): @@ -112,11 +145,13 @@ Options: print def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')): - commands = self.commands.values() + ''' Produce an HTML command list. + ''' + commands = self.commands.values() def sortfun(a, b): return cmp(a.__name__, b.__name__) commands.sort(sortfun) - for command in commands: + for command in commands: h = command.__doc__.split('\n') name = command.__name__[3:] usage = h[0] @@ -135,12 +170,12 @@ Options: 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 +All commands (except help) require a tracker specifier. This is just the path +to the roundup tracker you're working with. A roundup tracker is where roundup keeps the database and configuration file that defines an issue tracker. It may be thought of as the issue tracker's "home directory". It may -be specified in the environment variable ROUNDUP_INSTANCE or on the command -line as "-i instance". +be specified in the environment variable TRACKER_HOME or on the command +line as "-i tracker". A designator is a classname and a nodeid concatenated, eg. bug1, user10, ... @@ -240,47 +275,98 @@ Command help: print line return 0 + def listTemplates(self): + ''' List all the available templates. + + Look in the following places, where the later rules take precedence: + + 1. /share/roundup/templates/* + this should be the standard place to find them when Roundup is + installed + 2. /../templates/* + this will be used if Roundup's run in the distro (aka. source) + directory + 3. /* + this is for when someone unpacks a 3rd-party template + 4. + this is for someone who "cd"s to the 3rd-party template dir + ''' + # OK, try /share/roundup/templates + # -- this module (roundup.admin) will be installed in something + # like: + # /usr/lib/python2.2/site-packages/roundup/admin.py (5 dirs up) + # c:\python22\lib\site-packages\roundup\admin.py (4 dirs up) + # we're interested in where the "lib" directory is - ie. the /usr/ + # part + templates = {} + for N in 4, 5: + path = __file__ + # move up N elements in the path + for i in range(N): + path = os.path.dirname(path) + tdir = os.path.join(path, 'share', 'roundup', 'templates') + if os.path.isdir(tdir): + templates = init.listTemplates(tdir) + break + + # OK, now try as if we're in the roundup source distribution + # directory, so this module will be in .../roundup-*/roundup/admin.py + # and we're interested in the .../roundup-*/ part. + path = __file__ + for i in range(2): + path = os.path.dirname(path) + tdir = os.path.join(path, 'templates') + if os.path.isdir(tdir): + templates.update(init.listTemplates(tdir)) + + # Try subdirs of the current dir + templates.update(init.listTemplates(os.getcwd())) + + # Finally, try the current directory as a template + template = init.loadTemplateInfo(os.getcwd()) + if template: + templates[template['name']] = template + + return templates + def help_initopts(self): - import roundup.templates - templates = roundup.templates.listTemplates() - print _('Templates:'), ', '.join(templates) + templates = self.listTemplates() + print _('Templates:'), ', '.join(templates.keys()) import roundup.backends backends = roundup.backends.__all__ print _('Back ends:'), ', '.join(backends) - - def do_install(self, instance_home, args): + def do_install(self, tracker_home, args): '''Usage: install [template [backend [admin password]]] - Install a new Roundup instance. + Install a new Roundup tracker. - The command will prompt for the instance home directory (if not supplied - through INSTANCE_HOME or the -i option). The template, backend and admin + The command will prompt for the tracker home directory (if not supplied + through TRACKER_HOME or the -i option). The template, backend and admin password may be specified on the command-line as arguments, in that order. The initialise command must be called after this command in order - to initialise the instance's database. You may edit the instance's + to initialise the tracker's database. You may edit the tracker's initial database contents before running that command by editing - the instance's dbinit.py module init() function. + the tracker's dbinit.py module init() function. See also initopts help. ''' if len(args) < 1: raise UsageError, _('Not enough arguments supplied') - # make sure the instance home can be created - parent = os.path.split(instance_home)[0] + # make sure the tracker home can be created + parent = os.path.split(tracker_home)[0] if not os.path.exists(parent): raise UsageError, _('Instance home parent directory "%(parent)s"' ' does not exist')%locals() # select template - import roundup.templates - templates = roundup.templates.listTemplates() + templates = self.listTemplates() template = len(args) > 1 and args[1] or '' - if template not in templates: - print _('Templates:'), ', '.join(templates) - while template not in templates: + if not templates.has_key(template): + print _('Templates:'), ', '.join(templates.keys()) + while not templates.has_key(template): template = raw_input(_('Select template [classic]: ')).strip() if not template: template = 'classic' @@ -295,33 +381,36 @@ Command help: backend = raw_input(_('Select backend [anydbm]: ')).strip() if not backend: backend = 'anydbm' + # XXX perform a unit test based on the user's selections # install! - init.install(instance_home, template, backend) + init.install(tracker_home, templates[template]['path']) + init.write_select_db(tracker_home, backend) print _(''' - You should now edit the instance configuration file: - %(instance_config_file)s - ... at a minimum, you must set MAILHOST, MAIL_DOMAIN and ADMIN_EMAIL. + You should now edit the tracker configuration file: + %(config_file)s + ... at a minimum, you must set MAILHOST, TRACKER_WEB, MAIL_DOMAIN and + ADMIN_EMAIL. If you wish to modify the default schema, you should also edit the database initialisation file: %(database_config_file)s ... see the documentation on customizing for more information. ''')%{ - 'instance_config_file': os.path.join(instance_home, 'instance_config.py'), - 'database_config_file': os.path.join(instance_home, 'dbinit.py') + 'config_file': os.path.join(tracker_home, 'config.py'), + 'database_config_file': os.path.join(tracker_home, 'dbinit.py') } return 0 - def do_initialise(self, instance_home, args): - '''Usage: initialise [adminpw [adminemail]] - Initialise a new Roundup instance. + def do_initialise(self, tracker_home, args): + '''Usage: initialise [adminpw] + Initialise a new Roundup tracker. The administrator details will be set at this step. - Execute the instance's initialisation function dbinit.init() + Execute the tracker's initialisation function dbinit.init() ''' # password if len(args) > 1: @@ -333,33 +422,37 @@ Command help: adminpw = getpass.getpass(_('Admin Password: ')) confirm = getpass.getpass(_(' Confirm: ')) - # email - if len(args) > 2: - adminemail = args[2] - else: - adminemail = '' - while not adminemail: - adminemail = raw_input(_(' Admin Email: ')).strip() - - # make sure the instance home is installed - if not os.path.exists(instance_home): + # make sure the tracker home is installed + if not os.path.exists(tracker_home): raise UsageError, _('Instance home does not exist')%locals() - if not os.path.exists(os.path.join(instance_home, 'html')): + try: + tracker = roundup.instance.open(tracker_home) + except roundup.instance.TrackerError: raise UsageError, _('Instance has not been installed')%locals() # is there already a database? - if os.path.exists(os.path.join(instance_home, 'db')): + try: + db_exists = tracker.select_db.Database.exists(tracker.config) + except AttributeError: + # TODO: move this code to exists() static method in every backend + db_exists = os.path.exists(os.path.join(tracker_home, 'db')) + if db_exists: print _('WARNING: The database is already initialised!') print _('If you re-initialise it, you will lose all the data!') ok = raw_input(_('Erase it? Y/[N]: ')).strip() if ok.lower() != 'y': return 0 - # nuke it - shutil.rmtree(os.path.join(instance_home, 'db')) + # Get a database backend in use by tracker + try: + # nuke it + tracker.select_db.Database.nuke(tracker.config) + except AttributeError: + # TODO: move this code to nuke() static method in every backend + shutil.rmtree(os.path.join(tracker_home, 'db')) # GO - init.initialise(instance_home, adminpw) + init.initialise(tracker_home, adminpw) return 0 @@ -378,79 +471,116 @@ Command help: for designator in designators: # decode the node designator try: - classname, nodeid = roundupdb.splitDesignator(designator) - except roundupdb.DesignatorError, message: + classname, nodeid = hyperdb.splitDesignator(designator) + except hyperdb.DesignatorError, message: raise UsageError, message # get the class cl = self.get_class(classname) try: - if self.comma_sep: - l.append(cl.get(nodeid, propname)) + id=[] + if self.separator: + if self.print_designator: + # see if property is a link or multilink for + # which getting a desginator make sense. + # Algorithm: Get the properties of the + # current designator's class. (cl.getprops) + # get the property object for the property the + # user requested (properties[propname]) + # verify its type (isinstance...) + # raise error if not link/multilink + # get class name for link/multilink property + # do the get on the designators + # append the new designators + # print + properties = cl.getprops() + property = properties[propname] + if not (isinstance(property, hyperdb.Multilink) or + isinstance(property, hyperdb.Link)): + raise UsageError, _('property %s is not of type Multilink or Link so -d flag does not apply.')%propname + propclassname = self.db.getclass(property.classname).classname + id = cl.get(nodeid, propname) + for i in id: + l.append(propclassname + i) + else: + id = cl.get(nodeid, propname) + for i in id: + l.append(i) else: - print cl.get(nodeid, propname) + if self.print_designator: + properties = cl.getprops() + property = properties[propname] + if not (isinstance(property, hyperdb.Multilink) or + isinstance(property, hyperdb.Link)): + raise UsageError, _('property %s is not of type Multilink or Link so -d flag does not apply.')%propname + propclassname = self.db.getclass(property.classname).classname + id = cl.get(nodeid, propname) + for i in id: + print propclassname + i + else: + print cl.get(nodeid, propname) except IndexError: raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals() except KeyError: raise UsageError, _('no such %(classname)s property ' '"%(propname)s"')%locals() - if self.comma_sep: - print ','.join(l) + if self.separator: + print self.separator.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). + def do_set(self, args, pwre = re.compile(r'{(\w+)}(.+)')): + '''Usage: set items property=value property=value ... + Set the given properties of one or more items(s). - Sets the property to the value for all designators given. + The items are specified as a class or as a comma-separated + list of item designators (ie "designator[,designator,...]"). + + This command sets the properties to the values for all designators + given. If the value is missing (ie. "property=") then the property is + un-set. If the property is a multilink, you specify the linked ids + for the multilink as comma-separated numbers (ie "1,2,3"). ''' if len(args) < 2: raise UsageError, _('Not enough arguments supplied') from roundup import hyperdb designators = args[0].split(',') + if len(designators) == 1: + designator = designators[0] + try: + designator = hyperdb.splitDesignator(designator) + designators = [designator] + except hyperdb.DesignatorError: + cl = self.get_class(designator) + designators = [(designator, x) for x in cl.list()] + else: + try: + designators = [hyperdb.splitDesignator(x) for x in designators] + except hyperdb.DesignatorError, message: + raise UsageError, message # get the props from the args props = self.props_from_args(args[1:]) # now do the set for all the nodes - for designator in designators: - # decode the node designator - try: - classname, nodeid = roundupdb.splitDesignator(designator) - except roundupdb.DesignatorError, message: - raise UsageError, message - - # get the class + for classname, itemid in designators: cl = self.get_class(classname) 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: + props[key] = hyperdb.rawToHyperdb(self.db, cl, itemid, + key, value) + except hyperdb.HyperdbValueError, message: + raise UsageError, message # try the set try: - apply(cl.set, (nodeid, ), props) + apply(cl.set, (itemid, ), props) except (TypeError, IndexError, ValueError), message: + import traceback; traceback.print_exc() raise UsageError, message return 0 @@ -474,7 +604,9 @@ Command help: # number for propname, value in props.items(): num_re = re.compile('^\d+$') - if not num_re.match(value): + if value == '-1': + props[propname] = None + elif not num_re.match(value): # get the property try: property = cl.properties[propname] @@ -497,10 +629,25 @@ Command help: # now do the find try: - if self.comma_sep: - print ','.join(apply(cl.find, (), props)) + id = [] + designator = [] + if self.separator: + if self.print_designator: + id=apply(cl.find, (), props) + for i in id: + designator.append(classname + i) + print self.separator.join(designator) + else: + print self.separator.join(apply(cl.find, (), props)) + else: - print apply(cl.find, (), props) + if self.print_designator: + id=apply(cl.find, (), props) + for i in id: + designator.append(classname + i) + print designator + else: + print apply(cl.find, (), props) except KeyError: raise UsageError, _('%(classname)s has no property ' '"%(propname)s"')%locals() @@ -529,8 +676,8 @@ Command help: print _('%(key)s: %(value)s')%locals() def do_display(self, args): - '''Usage: display designator - Show the property values for the given node. + '''Usage: display designator[,designator]* + Show the property values for the given node(s). This lists the properties and their associated values for the given node. @@ -539,20 +686,23 @@ Command help: raise UsageError, _('Not enough arguments supplied') # decode the node designator - try: - classname, nodeid = roundupdb.splitDesignator(args[0]) - except roundupdb.DesignatorError, message: - raise UsageError, message + for designator in args[0].split(','): + try: + classname, nodeid = hyperdb.splitDesignator(designator) + except hyperdb.DesignatorError, message: + raise UsageError, message - # get the class - cl = self.get_class(classname) + # get the class + cl = self.get_class(classname) - # display the values - for key in cl.properties.keys(): - value = cl.get(nodeid, key) - print _('%(key)s: %(value)s')%locals() + # display the values + keys = cl.properties.keys() + keys.sort() + for key in keys: + value = cl.get(nodeid, key) + print _('%(key)s: %(value)s')%locals() - def do_create(self, args): + def do_create(self, args, pwre = re.compile(r'{(\w+)}(.+)')): '''Usage: create classname property=value ... Create a new entry of a given class. @@ -597,27 +747,11 @@ Command help: # convert types for propname, value in props.items(): - # get the property try: - proptype = properties[propname] - except KeyError: - raise UsageError, _('%(classname)s has no property ' - '"%(propname)s"')%locals() - - if isinstance(proptype, hyperdb.Date): - try: - props[propname] = date.Date(value) - except ValueError, message: - raise UsageError, _('"%(value)s": %(message)s')%locals() - elif isinstance(proptype, hyperdb.Interval): - try: - props[propname] = date.Interval(value) - except ValueError, message: - raise UsageError, _('"%(value)s": %(message)s')%locals() - elif isinstance(proptype, hyperdb.Password): - props[propname] = password.Password(value) - elif isinstance(proptype, hyperdb.Multilink): - props[propname] = value.split(',') + props[propname] = hyperdb.rawToHyperdb(self.db, cl, None, + propname, value) + except hyperdb.HyperdbValueError, message: + raise UsageError, message # check for the key property propname = cl.getkey() @@ -640,7 +774,13 @@ Command help: specified, the "label" property is used. The label property is tried in order: the key, "name", "title" and then the first property, alphabetically. + + With -c, -S or -s print a list of item id's if no property specified. + If property specified, print list of that property for every class + instance. ''' + if len(args) > 2: + raise UsageError, _('Too many arguments supplied') if len(args) < 1: raise UsageError, _('Not enough arguments supplied') classname = args[0] @@ -654,8 +794,21 @@ Command help: else: propname = cl.labelprop() - if self.comma_sep: - print ','.join(cl.list()) + if self.separator: + if len(args) == 2: + # create a list of propnames since user specified propname + proplist=[] + for nodeid in cl.list(): + try: + proplist.append(cl.get(nodeid, propname)) + except KeyError: + raise UsageError, _('%(classname)s has no property ' + '"%(propname)s"')%locals() + print self.separator.join(proplist) + else: + # create a list of index id's since user didn't specify + # otherwise + print self.separator.join(cl.list()) else: for nodeid in cl.list(): try: @@ -672,14 +825,27 @@ Command help: 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 + are the width of the largest value. 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 + + Also to make the width of the column the width of the label, + leave a trailing : without a width on the property. For example:: + + roundup> table priority id,name: + Id Name + 1 fata + 2 bug + 3 usab + 4 feat + + will result in a the 4 character wide "Name" column. ''' if len(args) < 1: raise UsageError, _('Not enough arguments supplied') @@ -711,10 +877,19 @@ Command help: for spec in prop_names: if ':' in spec: name, width = spec.split(':') - props.append((name, int(width))) + if width == '': + props.append((name, len(spec))) + else: + props.append((name, int(width))) else: - props.append((spec, len(spec))) - + # this is going to be slow + maxlen = len(spec) + for nodeid in cl.list(): + curlen = len(str(cl.get(nodeid, spec))) + if curlen > maxlen: + maxlen = curlen + props.append((spec, maxlen)) + # now display the heading print ' '.join([name.capitalize().ljust(width) for name,width in props]) @@ -746,8 +921,8 @@ Command help: if len(args) < 1: raise UsageError, _('Not enough arguments supplied') try: - classname, nodeid = roundupdb.splitDesignator(args[0]) - except roundupdb.DesignatorError, message: + classname, nodeid = hyperdb.splitDesignator(args[0]) + except hyperdb.DesignatorError, message: raise UsageError, message try: @@ -796,8 +971,8 @@ Command help: designators = args[0].split(',') for designator in designators: try: - classname, nodeid = roundupdb.splitDesignator(designator) - except roundupdb.DesignatorError, message: + classname, nodeid = hyperdb.splitDesignator(designator) + except hyperdb.DesignatorError, message: raise UsageError, message try: self.db.getclass(classname).retire(nodeid) @@ -807,128 +982,122 @@ Command help: raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals() return 0 + def do_restore(self, args): + '''Usage: restore designator[,designator]* + Restore the retired node specified by designator. + + The given nodes will become available for users again. + ''' + if len(args) < 1: + raise UsageError, _('Not enough arguments supplied') + designators = args[0].split(',') + for designator in designators: + try: + classname, nodeid = hyperdb.splitDesignator(designator) + except hyperdb.DesignatorError, message: + raise UsageError, message + try: + self.db.getclass(classname).restore(nodeid) + except KeyError: + raise UsageError, _('no such class "%(classname)s"')%locals() + except IndexError: + raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals() + return 0 + def do_export(self, args): - '''Usage: export class[,class] destination_dir - Export the database to tab-separated-value files. + '''Usage: export [class[,class]] export_dir + Export the database to colon-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. + colon-separated-value files that are placed in the nominated + destination directory. The journals are not exported. ''' - if len(args) < 2: + # grab the directory to export to + if len(args) < 1: raise UsageError, _('Not enough arguments supplied') - classes = args[0].split(',') - dir = args[1] + if rcsv.error: + raise UsageError, _(rcsv.error) - # use the csv parser if we can - it's faster - if csv is not None: - p = csv.parser(field_sep=':') + dir = args[-1] + + # get the list of classes to export + if len(args) == 2: + classes = args[0].split(',') + else: + classes = self.db.classes.keys() # do all the classes specified for classname in classes: cl = self.get_class(classname) f = open(os.path.join(dir, classname+'.csv'), 'w') - f.write(':'.join(cl.properties.keys()) + '\n') + writer = rcsv.writer(f, rcsv.colon_separated) + properties = cl.getprops() + propnames = properties.keys() + propnames.sort() + fields = propnames[:] + fields.append('is retired') + writer.writerow(fields) - # 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') + # all nodes for this class (not using list() 'cos it doesn't + # include retired nodes) + + for nodeid in self.db.getclass(classname).getnodeids(): + # get the regular props + writer.writerow (cl.export_list(propnames, nodeid)) + + # close this file + f.close() return 0 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.) + '''Usage: import import_dir + Import a database from the directory containing CSV files, one per + class to import. + + The files must define the same properties as the class (including having + a "header" line with those property names.) + + The imported nodes will have the same nodeid as defined in the + import file, thus replacing any existing content. + + 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: + if len(args) < 1: 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/') - + if rcsv.error: + raise UsageError, _(rcsv.error) from roundup import hyperdb - # ensure that the properties and the CSV file headings match - classname = args[0] - cl = self.get_class(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 "%(arg0)s".')%{'arg0': 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 - line = f.readline() - if not line: - raise ValueError, "Unexpected EOF during CSV parse" - - # 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) + for file in os.listdir(args[0]): + # we only care about CSV files + if not file.endswith('.csv'): + continue + + f = open(os.path.join(args[0], file)) + + # get the classname + classname = os.path.splitext(file)[0] + + # ensure that the properties and the CSV file headings match + cl = self.get_class(classname) + reader = rcsv.reader(f, rcsv.colon_separated) + file_props = None + maxid = 1 + + # loop through the file and create a node for each entry + for r in reader: + if file_props is None: + file_props = r + continue + + # do the import and figure the current highest nodeid + maxid = max(maxid, int(cl.import_list(file_props, r))) + + # set the id counter + print 'setting', classname, maxid+1 + self.db.setid(classname, str(maxid+1)) return 0 def do_pack(self, args): @@ -963,14 +1132,57 @@ Date format is "YYYY-MM-DD" eg: raise ValueError, _('Invalid format') m = m.groupdict() if m['period']: - # TODO: need to fix date module. one should be able to say - # pack_before = date.Date(". - %s"%value) - pack_before = date.Date(".") + date.Interval("- %s"%value) + pack_before = date.Date(". - %s"%value) elif m['date']: pack_before = date.Date(value) self.db.pack(pack_before) return 0 + def do_reindex(self, args): + '''Usage: reindex + Re-generate a tracker's search indexes. + + This will re-generate the search indexes for a tracker. This will + typically happen automatically. + ''' + self.db.indexer.force_reindex() + self.db.reindex() + return 0 + + def do_security(self, args): + '''Usage: security [Role name] + Display the Permissions available to one or all Roles. + ''' + if len(args) == 1: + role = args[0] + try: + roles = [(args[0], self.db.security.role[args[0]])] + except KeyError: + print _('No such Role "%(role)s"')%locals() + return 1 + else: + roles = self.db.security.role.items() + role = self.db.config.NEW_WEB_USER_ROLES + if ',' in role: + print _('New Web users get the Roles "%(role)s"')%locals() + else: + print _('New Web users get the Role "%(role)s"')%locals() + role = self.db.config.NEW_EMAIL_USER_ROLES + if ',' in role: + print _('New Email users get the Roles "%(role)s"')%locals() + else: + print _('New Email users get the Role "%(role)s"')%locals() + roles.sort() + for rolename, role in roles: + print _('Role "%(name)s":')%role.__dict__ + for permission in role.permissions: + if permission.klass: + print _(' %(description)s (%(name)s for "%(klass)s" ' + 'only)')%permission.__dict__ + else: + print _(' %(description)s (%(name)s)')%permission.__dict__ + return 0 + def run_command(self, args): '''Run a single command ''' @@ -1005,35 +1217,35 @@ Date format is "YYYY-MM-DD" eg: 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() + # make sure we have a tracker_home + while not self.tracker_home: + self.tracker_home = raw_input(_('Enter tracker home: ')).strip() # before we open the db, we may be doing an install or init if command == 'initialise': try: - return self.do_initialise(self.instance_home, args) + return self.do_initialise(self.tracker_home, args) except UsageError, message: print _('Error: %(message)s')%locals() return 1 elif command == 'install': try: - return self.do_install(self.instance_home, args) + return self.do_install(self.tracker_home, args) except UsageError, message: print _('Error: %(message)s')%locals() return 1 - # get the instance + # get the tracker try: - instance = roundup.instance.open(self.instance_home) + tracker = roundup.instance.open(self.tracker_home) except ValueError, message: - self.instance_home = '' - print _("Error: Couldn't open instance: %(message)s")%locals() + self.tracker_home = '' + print _("Error: Couldn't open tracker: %(message)s")%locals() return 1 # only open the database once! if not self.db: - self.db = instance.open('admin') + self.db = tracker.open('admin') # do the command ret = 0 @@ -1053,7 +1265,7 @@ Date format is "YYYY-MM-DD" eg: def interactive(self): '''Run in an interactive mode ''' - print _('Roundup {version} ready for input.') + print _('Roundup %s ready for input.'%roundup_version) print _('Type "help" for help.') try: import readline @@ -1081,13 +1293,13 @@ Date format is "YYYY-MM-DD" eg: def main(self): try: - opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc') + opts, args = getopt.getopt(sys.argv[1:], 'i:u:hcdsS:') except getopt.GetoptError, e: self.usage(str(e)) return 1 # handle command-line args - self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '') + self.tracker_home = os.environ.get('TRACKER_HOME', '') # TODO: reinstate the user/password stuff (-u arg too) name = password = '' if os.environ.has_key('ROUNDUP_LOGIN'): @@ -1095,74 +1307,48 @@ Date format is "YYYY-MM-DD" eg: name = l[0] if len(l) > 1: password = l[1] - self.comma_sep = 0 + self.separator = None + self.print_designator = 0 for opt, arg in opts: if opt == '-h': self.usage() return 0 if opt == '-i': - self.instance_home = arg + self.tracker_home = arg if opt == '-c': - self.comma_sep = 1 + if self.separator != None: + self.usage('Only one of -c, -S and -s may be specified') + return 1 + self.separator = ',' + if opt == '-S': + if self.separator != None: + self.usage('Only one of -c, -S and -s may be specified') + return 1 + self.separator = arg + if opt == '-s': + if self.separator != None: + self.usage('Only one of -c, -S and -s may be specified') + return 1 + self.separator = ' ' + if opt == '-d': + self.print_designator = 1 # if no command - go interactive + # wrap in a try/finally so we always close off the db ret = 0 - if not args: - self.interactive() - else: - ret = self.run_command(args) - if self.db: self.db.commit() - return ret - + try: + if not args: + self.interactive() + else: + ret = self.run_command(args) + if self.db: self.db.commit() + return ret + finally: + if self.db: + self.db.close() if __name__ == '__main__': tool = AdminTool() sys.exit(tool.main()) -# -# $Log: not supported by cvs2svn $ -# Revision 1.12 2002/05/26 09:04:42 richard -# out by one in the init args -# -# Revision 1.11 2002/05/23 01:14:20 richard -# . split instance initialisation into two steps, allowing config changes -# before the database is initialised. -# -# Revision 1.10 2002/04/27 10:07:23 richard -# minor fix to error message -# -# Revision 1.9 2002/03/12 22:51:47 richard -# . #527416 ] roundup-admin uses undefined value -# . #527503 ] unfriendly init blowup when parent dir -# (also handles UsageError correctly now in init) -# -# Revision 1.8 2002/02/27 03:28:21 richard -# Ran it through pychecker, made fixes -# -# Revision 1.7 2002/02/20 05:04:32 richard -# Wasn't handling the cvs parser feeding properly. -# -# Revision 1.6 2002/01/23 07:27:19 grubert -# . allow abbreviation of "help" in admin tool too. -# -# Revision 1.5 2002/01/21 16:33:19 rochecompaan -# You can now use the roundup-admin tool to pack the database -# -# Revision 1.4 2002/01/14 06:51:09 richard -# . #503164 ] create and passwords -# -# Revision 1.3 2002/01/08 05:26:32 rochecompaan -# Missing "self" in props_from_args -# -# Revision 1.2 2002/01/07 10:41:44 richard -# #500140 ] AdminTool.get_class() returns nothing -# -# Revision 1.1 2002/01/05 02:11:22 richard -# I18N'ed roundup admin - and split the code off into a module so it can be used -# elsewhere. -# Big issue with this is the doc strings - that's the help. We're probably going to -# have to switch to not use docstrings, which will suck a little :( -# -# -# # vim: set filetype=python ts=4 sw=4 et si