X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fadmin.py;h=1a4e241df277116f00424244eefa5658264f3a4c;hb=f2228172dcbed77780ef7b60b10ac3e85cded918;hp=653daf3eb999919a0445e3041c4794ddd1aceba5;hpb=6b956230dba5f60e615bea24d080e845e3bcb48c;p=roundup.git diff --git a/roundup/admin.py b/roundup/admin.py index 653daf3..1a4e241 100644 --- a/roundup/admin.py +++ b/roundup/admin.py @@ -16,42 +16,40 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: admin.py,v 1.110 2008-02-07 03:28:33 richard Exp $ -'''Administration commands for maintaining Roundup trackers. -''' +"""Administration commands for maintaining Roundup trackers. +""" __docformat__ = 'restructuredtext' -import csv, getopt, getpass, os, re, shutil, sys, UserDict +import csv, getopt, getpass, os, re, shutil, sys, UserDict, operator from roundup import date, hyperdb, roundupdb, init, password, token from roundup import __version__ as roundup_version import roundup.instance from roundup.configuration import CoreConfig from roundup.i18n import _ -from roundup.exception import UsageError +from roundup.exceptions import UsageError class CommandDict(UserDict.UserDict): - '''Simple dictionary that lets us do lookups using partial keys. + """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): + if key in self.data: return [(key, self.data[key])] - keylist = self.data.keys() - keylist.sort() + keylist = sorted(self.data) 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 + raise KeyError(key) return l class AdminTool: - ''' A collection of methods used in maintaining Roundup trackers. + """ 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 @@ -61,14 +59,14 @@ class AdminTool: 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(): + for k in AdminTool.__dict__: if k[:3] == 'do_': self.commands[k[3:]] = getattr(self, k) self.help = {} - for k in AdminTool.__dict__.keys(): + for k in AdminTool.__dict__: if k[:5] == 'help_': self.help[k[5:]] = getattr(self, k) self.tracker_home = '' @@ -76,27 +74,27 @@ class AdminTool: self.db_uncommitted = False def get_class(self, classname): - '''Get the class - raise an exception if it doesn't exist. - ''' + """Get the class - raise an exception if it doesn't exist. + """ try: return self.db.getclass(classname) except KeyError: - raise UsageError, _('no such class "%(classname)s"')%locals() + raise UsageError(_('no such class "%(classname)s"')%locals()) def props_from_args(self, args): - ''' Produce a dictionary of prop: value from the args list. + """ 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() + 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() + raise UsageError(_('argument "%(arg)s" not propname=value' + )%locals()) key, value = l[0], '='.join(l[1:]) if value: props[key] = value @@ -105,11 +103,11 @@ class AdminTool: return props def usage(self, message=''): - ''' Display a simple usage message. - ''' + """ Display a simple usage message. + """ if message: message = _('Problem: %(message)s\n\n')%locals() - print _('''%(message)sUsage: roundup-admin [options] [ ] + print _("""%(message)sUsage: roundup-admin [options] [ ] Options: -i instance home -- specify the issue tracker "home directory" to administer @@ -130,15 +128,15 @@ Help: roundup-admin help -- this help roundup-admin help -- command-specific help roundup-admin help all -- all available help -''')%locals() +""")%locals() self.help_commands() def help_commands(self): - ''' List the commands available with their help summary. - ''' + """List the commands available with their help summary. + """ print _('Commands:'), commands = [''] - for command in self.commands.values(): + for command in self.commands.itervalues(): h = _(command.__doc__).split('\n')[0] commands.append(' '+h[7:]) commands.sort() @@ -149,20 +147,18 @@ matches only one command, e.g. l == li == lis == list.""")) print def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')): - ''' Produce an HTML command list. - ''' - commands = self.commands.values() - def sortfun(a, b): - return cmp(a.__name__, b.__name__) - commands.sort(sortfun) + """ Produce an HTML command list. + """ + commands = sorted(self.commands.itervalues(), + operator.attrgetter('__name__')) for command in commands: h = _(command.__doc__).split('\n') name = command.__name__[3:] usage = h[0] - print ''' + print """ %(name)s %(usage)s

-

''' % locals()
+
""" % locals()
             indent = indent_re.match(h[3])
             if indent: indent = len(indent.group(1))
             for line in h[3:]:
@@ -173,7 +169,7 @@ matches only one command, e.g. l == li == lis == list."""))
             print '
\n' def help_all(self): - print _(''' + print _(""" 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 @@ -234,21 +230,21 @@ Date format examples: "." means "right now" Command help: -''') +""") for name, command in self.commands.items(): print _('%s:')%name print ' ', _(command.__doc__) def do_help(self, args, nl_re=re.compile('[\r\n]'), indent_re=re.compile(r'^(\s+)\S+')): - ""'''Usage: help topic + ''"""Usage: help topic Give help about topic. commands -- list commands -- help specific to a command initopts -- init command options all -- all available help - ''' + """ if len(args)>0: topic = args[0] else: @@ -256,7 +252,7 @@ Command help: # try help_ methods - if self.help.has_key(topic): + if topic in self.help: self.help[topic]() return 0 @@ -281,7 +277,7 @@ Command help: return 0 def listTemplates(self): - ''' List all the available templates. + """ List all the available templates. Look in the following places, where the later rules take precedence: @@ -297,7 +293,7 @@ Command help: this is for when someone unpacks a 3rd-party template 5. this is for someone who "cd"s to the 3rd-party template dir - ''' + """ # OK, try /share/roundup/templates # and /share/roundup/templates # -- this module (roundup.admin) will be installed in something @@ -341,13 +337,13 @@ Command help: def help_initopts(self): templates = self.listTemplates() - print _('Templates:'), ', '.join(templates.keys()) + print _('Templates:'), ', '.join(templates) import roundup.backends backends = roundup.backends.list_backends() print _('Back ends:'), ', '.join(backends) def do_install(self, tracker_home, args): - ""'''Usage: install [template [backend [key=val[,key=val]]]] + ''"""Usage: install [template [backend [key=val[,key=val]]]] Install a new Roundup tracker. The command will prompt for the tracker home directory @@ -368,21 +364,21 @@ Command help: the tracker's dbinit.py module init() function. See also initopts help. - ''' + """ if len(args) < 1: - raise UsageError, _('Not enough arguments supplied') + raise UsageError(_('Not enough arguments supplied')) # make sure the tracker home can be created tracker_home = os.path.abspath(tracker_home) 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() + raise UsageError(_('Instance home parent directory "%(parent)s"' + ' does not exist')%locals()) config_ini_file = os.path.join(tracker_home, CoreConfig.INI_FILE) # check for both old- and new-style configs - if filter(os.path.exists, [config_ini_file, - os.path.join(tracker_home, 'config.py')]): + if list(filter(os.path.exists, [config_ini_file, + os.path.join(tracker_home, 'config.py')])): ok = raw_input(_( """WARNING: There appears to be a tracker in "%(tracker_home)s"! If you re-install it, you will lose all the data! @@ -396,9 +392,9 @@ Erase it? Y/N: """) % locals()) # select template templates = self.listTemplates() template = len(args) > 1 and args[1] or '' - if not templates.has_key(template): - print _('Templates:'), ', '.join(templates.keys()) - while not templates.has_key(template): + 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' @@ -440,8 +436,8 @@ Erase it? Y/N: """) % locals()) need_set = CoreConfig(tracker_home)._get_unset_options() if need_set: print _(" ... at a minimum, you must set following options:") - for section, options in need_set.items(): - print " [%s]: %s" % (section, ", ".join(options)) + for section in need_set: + print " [%s]: %s" % (section, ", ".join(need_set[section])) # note about schema modifications print _(""" @@ -462,23 +458,23 @@ Erase it? Y/N: """) % locals()) return 0 def do_genconfig(self, args): - ""'''Usage: genconfig + ''"""Usage: genconfig Generate a new tracker config file (ini style) with default values in . - ''' + """ if len(args) < 1: - raise UsageError, _('Not enough arguments supplied') + raise UsageError(_('Not enough arguments supplied')) config = CoreConfig() config.save(args[0]) def do_initialise(self, tracker_home, args): - ""'''Usage: initialise [adminpw] + ''"""Usage: initialise [adminpw] Initialise a new Roundup tracker. The administrator details will be set at this step. Execute the tracker's initialisation function dbinit.init() - ''' + """ # password if len(args) > 1: adminpw = args[1] @@ -491,11 +487,11 @@ Erase it? Y/N: """) % locals()) # make sure the tracker home is installed if not os.path.exists(tracker_home): - raise UsageError, _('Instance home does not exist')%locals() + raise UsageError(_('Instance home does not exist')%locals()) try: tracker = roundup.instance.open(tracker_home) except roundup.instance.TrackerError: - raise UsageError, _('Instance has not been installed')%locals() + raise UsageError(_('Instance has not been installed')%locals()) # is there already a database? if tracker.exists(): @@ -512,23 +508,26 @@ Erase it? Y/N: """)) tracker.nuke() # re-write the backend select file - init.write_select_db(tracker_home, backend) + init.write_select_db(tracker_home, backend, tracker.config.DATABASE) # GO - tracker.init(password.Password(adminpw)) + tracker.init(password.Password(adminpw, config=tracker.config)) return 0 def do_get(self, args): - ""'''Usage: get property designator[,designator]* + ''"""Usage: get property designator[,designator]* Get the given property of one or more designator(s). + A designator is a classname and a nodeid concatenated, + eg. bug1, user10, ... + Retrieves the property value of the nodes specified by the designators. - ''' + """ if len(args) < 2: - raise UsageError, _('Not enough arguments supplied') + raise UsageError(_('Not enough arguments supplied')) propname = args[0] designators = args[1].split(',') l = [] @@ -537,7 +536,7 @@ Erase it? Y/N: """)) try: classname, nodeid = hyperdb.splitDesignator(designator) except hyperdb.DesignatorError, message: - raise UsageError, message + raise UsageError(message) # get the class cl = self.get_class(classname) @@ -561,7 +560,9 @@ Erase it? Y/N: """)) 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 + 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: @@ -576,7 +577,9 @@ Erase it? Y/N: """)) 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 + 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: @@ -584,10 +587,11 @@ Erase it? Y/N: """)) else: print cl.get(nodeid, propname) except IndexError: - raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals() + raise UsageError(_('no such %(classname)s node ' + '"%(nodeid)s"')%locals()) except KeyError: - raise UsageError, _('no such %(classname)s property ' - '"%(propname)s"')%locals() + raise UsageError(_('no such %(classname)s property ' + '"%(propname)s"')%locals()) if self.separator: print self.separator.join(l) @@ -595,19 +599,22 @@ Erase it? Y/N: """)) def do_set(self, args): - ""'''Usage: set items property=value property=value ... + ''"""Usage: set items property=value property=value ... Set the given properties of one or more items(s). The items are specified as a class or as a comma-separated list of item designators (ie "designator[,designator,...]"). + A designator is a classname and a nodeid concatenated, + eg. bug1, user10, ... + 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') + raise UsageError(_('Not enough arguments supplied')) from roundup import hyperdb designators = args[0].split(',') @@ -623,7 +630,7 @@ Erase it? Y/N: """)) try: designators = [hyperdb.splitDesignator(x) for x in designators] except hyperdb.DesignatorError, message: - raise UsageError, message + raise UsageError(message) # get the props from the args props = self.props_from_args(args[1:]) @@ -638,27 +645,27 @@ Erase it? Y/N: """)) props[key] = hyperdb.rawToHyperdb(self.db, cl, itemid, key, value) except hyperdb.HyperdbValueError, message: - raise UsageError, message + raise UsageError(message) # try the set try: - apply(cl.set, (itemid, ), props) + cl.set(itemid, **props) except (TypeError, IndexError, ValueError), message: import traceback; traceback.print_exc() - raise UsageError, message + raise UsageError(message) self.db_uncommitted = True return 0 def do_find(self, args): - ""'''Usage: find classname propname=value ... + ''"""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') + raise UsageError(_('Not enough arguments supplied')) classname = args[0] # get the class cl = self.get_class(classname) @@ -667,7 +674,7 @@ Erase it? Y/N: """)) props = self.props_from_args(args[1:]) # convert the user-input value to a value used for find() - for propname, value in props.items(): + for propname, value in props.iteritems(): if ',' in value: values = value.split(',') else: @@ -687,85 +694,88 @@ Erase it? Y/N: """)) designator = [] if self.separator: if self.print_designator: - id=apply(cl.find, (), props) + id = 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)) + print self.separator.join(cl.find(**props)) else: if self.print_designator: - id=apply(cl.find, (), props) + id = cl.find(**props) for i in id: designator.append(classname + i) print designator else: - print apply(cl.find, (), props) + print cl.find(**props) except KeyError: - raise UsageError, _('%(classname)s has no property ' - '"%(propname)s"')%locals() + raise UsageError(_('%(classname)s has no property ' + '"%(propname)s"')%locals()) except (ValueError, TypeError), message: - raise UsageError, message + raise UsageError(message) return 0 def do_specification(self, args): - ""'''Usage: specification classname + ''"""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') + raise UsageError(_('Not enough arguments supplied')) classname = args[0] # get the class cl = self.get_class(classname) # get the key property keyprop = cl.getkey() - for key, value in cl.properties.items(): + for key in cl.properties: + value = cl.properties[key] if keyprop == key: print _('%(key)s: %(value)s (key property)')%locals() else: print _('%(key)s: %(value)s')%locals() def do_display(self, args): - ""'''Usage: display designator[,designator]* + ''"""Usage: display designator[,designator]* Show the property values for the given node(s). + A designator is a classname and a nodeid concatenated, + eg. bug1, user10, ... + This lists the properties and their associated values for the given node. - ''' + """ if len(args) < 1: - raise UsageError, _('Not enough arguments supplied') + raise UsageError(_('Not enough arguments supplied')) # decode the node designator for designator in args[0].split(','): try: classname, nodeid = hyperdb.splitDesignator(designator) except hyperdb.DesignatorError, message: - raise UsageError, message + raise UsageError(message) # get the class cl = self.get_class(classname) # display the values - keys = cl.properties.keys() - keys.sort() + keys = sorted(cl.properties) for key in keys: value = cl.get(nodeid, key) print _('%(key)s: %(value)s')%locals() def do_create(self, args): - ""'''Usage: create classname property=value ... + ''"""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') + raise UsageError(_('Not enough arguments supplied')) from roundup import hyperdb classname = args[0] @@ -778,8 +788,9 @@ Erase it? Y/N: """)) properties = cl.getprops(protected = 0) if len(args) == 1: # ask for the properties - for key, value in properties.items(): + for key in properties: if key == 'id': continue + value = properties[key] name = value.__class__.__name__ if isinstance(value , hyperdb.Password): again = None @@ -800,29 +811,29 @@ Erase it? Y/N: """)) props = self.props_from_args(args[1:]) # convert types - for propname, value in props.items(): + for propname in props: try: props[propname] = hyperdb.rawToHyperdb(self.db, cl, None, - propname, value) + propname, props[propname]) except hyperdb.HyperdbValueError, message: - raise UsageError, message + raise UsageError(message) # check for the key property propname = cl.getkey() - if propname and not props.has_key(propname): - raise UsageError, _('you must provide the "%(propname)s" ' - 'property.')%locals() + if propname and propname not in props: + raise UsageError(_('you must provide the "%(propname)s" ' + 'property.')%locals()) # do the actual create try: - print apply(cl.create, (), props) + print cl.create(**props) except (TypeError, IndexError, ValueError), message: - raise UsageError, message + raise UsageError(message) self.db_uncommitted = True return 0 def do_list(self, args): - ""'''Usage: list classname [property] + ''"""Usage: list classname [property] List the instances of a class. Lists all instances of the given class. If the property is not @@ -833,13 +844,13 @@ Erase it? Y/N: """)) 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') + raise UsageError(_('Too many arguments supplied')) if len(args) < 1: - raise UsageError, _('Not enough arguments supplied') + raise UsageError(_('Not enough arguments supplied')) classname = args[0] - + # get the class cl = self.get_class(classname) @@ -851,14 +862,14 @@ Erase it? Y/N: """)) if self.separator: if len(args) == 2: - # create a list of propnames since user specified propname + # 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() + 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 @@ -869,13 +880,13 @@ Erase it? Y/N: """)) try: value = cl.get(nodeid, propname) except KeyError: - raise UsageError, _('%(classname)s has no property ' - '"%(propname)s"')%locals() + raise UsageError(_('%(classname)s has no property ' + '"%(propname)s"')%locals()) print _('%(nodeid)4s: %(value)s')%locals() return 0 def do_table(self, args): - ""'''Usage: table classname [property[,property]*] + ''"""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 @@ -902,9 +913,9 @@ Erase it? Y/N: """)) 4 feat will result in a the 4 character wide "Name" column. - ''' + """ if len(args) < 1: - raise UsageError, _('Not enough arguments supplied') + raise UsageError(_('Not enough arguments supplied')) classname = args[0] # get the class @@ -919,14 +930,15 @@ Erase it? Y/N: """)) try: propname, width = spec.split(':') except (ValueError, TypeError): - raise UsageError, _('"%(spec)s" not name:width')%locals() + raise UsageError(_('"%(spec)s" not ' + 'name:width')%locals()) else: propname = spec - if not all_props.has_key(propname): - raise UsageError, _('%(classname)s has no property ' - '"%(propname)s"')%locals() + if propname not in all_props: + raise UsageError(_('%(classname)s has no property ' + '"%(propname)s"')%locals()) else: - prop_names = cl.getprops().keys() + prop_names = cl.getprops() # now figure column widths props = [] @@ -969,28 +981,32 @@ Erase it? Y/N: """)) return 0 def do_history(self, args): - ""'''Usage: history designator + ''"""Usage: history designator Show the history entries of a designator. + A designator is a classname and a nodeid concatenated, + eg. bug1, user10, ... + Lists the journal entries for the node identified by the designator. - ''' + """ if len(args) < 1: - raise UsageError, _('Not enough arguments supplied') + raise UsageError(_('Not enough arguments supplied')) try: classname, nodeid = hyperdb.splitDesignator(args[0]) except hyperdb.DesignatorError, message: - raise UsageError, message + raise UsageError(message) try: print self.db.getclass(classname).history(nodeid) except KeyError: - raise UsageError, _('no such class "%(classname)s"')%locals() + raise UsageError(_('no such class "%(classname)s"')%locals()) except IndexError: - raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals() + raise UsageError(_('no such %(classname)s node ' + '"%(nodeid)s"')%locals()) return 0 def do_commit(self, args): - ""'''Usage: commit + ''"""Usage: commit Commit changes made to the database during an interactive session. The changes made during an interactive session are not @@ -999,73 +1015,81 @@ Erase it? Y/N: """)) One-off commands on the command-line are automatically committed if they are successful. - ''' + """ self.db.commit() self.db_uncommitted = False return 0 def do_rollback(self, args): - ""'''Usage: rollback + ''"""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() self.db_uncommitted = False return 0 def do_retire(self, args): - ""'''Usage: retire designator[,designator]* + ''"""Usage: retire designator[,designator]* Retire the node specified by designator. + A designator is a classname and a nodeid concatenated, + eg. bug1, user10, ... + 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') + 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 + raise UsageError(message) try: self.db.getclass(classname).retire(nodeid) except KeyError: - raise UsageError, _('no such class "%(classname)s"')%locals() + raise UsageError(_('no such class "%(classname)s"')%locals()) except IndexError: - raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals() + raise UsageError(_('no such %(classname)s node ' + '"%(nodeid)s"')%locals()) self.db_uncommitted = True return 0 def do_restore(self, args): - ""'''Usage: restore designator[,designator]* + ''"""Usage: restore designator[,designator]* Restore the retired node specified by designator. + A designator is a classname and a nodeid concatenated, + eg. bug1, user10, ... + The given nodes will become available for users again. - ''' + """ if len(args) < 1: - raise UsageError, _('Not enough arguments supplied') + 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 + raise UsageError(message) try: self.db.getclass(classname).restore(nodeid) except KeyError: - raise UsageError, _('no such class "%(classname)s"')%locals() + raise UsageError(_('no such class "%(classname)s"')%locals()) except IndexError: - raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals() + raise UsageError(_('no such %(classname)s node ' + '"%(nodeid)s"')%locals()) self.db_uncommitted = True return 0 def do_export(self, args, export_files=True): - ""'''Usage: export [[-]class[,class]] export_dir + ''"""Usage: export [[-]class[,class]] export_dir Export the database to colon-separated-value files. To exclude the files (e.g. for the msg or file class), use the exporttables command. @@ -1076,22 +1100,22 @@ Erase it? Y/N: """)) This action exports the current data from the database into colon-separated-value files that are placed in the nominated destination directory. - ''' + """ # grab the directory to export to if len(args) < 1: - raise UsageError, _('Not enough arguments supplied') + raise UsageError(_('Not enough arguments supplied')) dir = args[-1] # get the list of classes to export if len(args) == 2: if args[0].startswith('-'): - classes = [ c for c in self.db.classes.keys() + classes = [ c for c in self.db.classes if not c in args[0][1:].split(',') ] else: classes = args[0].split(',') else: - classes = self.db.classes.keys() + classes = self.db.classes class colon_separated(csv.excel): delimiter = ':' @@ -1100,6 +1124,9 @@ Erase it? Y/N: """)) if not os.path.exists(dir): os.makedirs(dir) + # maximum csv field length exceeding configured size? + max_len = self.db.config.CSV_FIELD_SIZE + # do all the classes specified for classname in classes: cl = self.get_class(classname) @@ -1122,7 +1149,18 @@ Erase it? Y/N: """)) if self.verbose: sys.stdout.write('\rExporting %s - %s'%(classname, nodeid)) sys.stdout.flush() - writer.writerow(cl.export_list(propnames, nodeid)) + node = cl.getnode(nodeid) + exp = cl.export_list(propnames, nodeid) + lensum = sum ([len (repr(node[p])) for p in propnames]) + # for a safe upper bound of field length we add + # difference between CSV len and sum of all field lengths + d = sum ([len(x) for x in exp]) - lensum + assert (d > 0) + for p in propnames: + ll = len(repr(node[p])) + d + if ll > max_len: + max_len = ll + writer.writerow(exp) if export_files and hasattr(cl, 'export_files'): cl.export_files(dir, nodeid) @@ -1135,12 +1173,16 @@ Erase it? Y/N: """)) sys.stdout.write("\nExporting Journal for %s\n" % classname) sys.stdout.flush() journals = csv.writer(jf, colon_separated) - map(journals.writerow, cl.export_journals()) + for row in cl.export_journals(): + journals.writerow(row) jf.close() + if max_len > self.db.config.CSV_FIELD_SIZE: + print >> sys.stderr, \ + "Warning: config csv_field_size should be at least %s"%max_len return 0 def do_exporttables(self, args): - ""'''Usage: exporttables [[-]class[,class]] export_dir + ''"""Usage: exporttables [[-]class[,class]] export_dir Export the database to colon-separated-value files, excluding the files below $TRACKER_HOME/db/files/ (which can be archived separately). To include the files, use the export command. @@ -1151,11 +1193,11 @@ Erase it? Y/N: """)) This action exports the current data from the database into colon-separated-value files that are placed in the nominated destination directory. - ''' + """ return self.do_export(args, export_files=False) def do_import(self, args): - ""'''Usage: import import_dir + ''"""Usage: import import_dir Import a database from the directory containing CSV files, two per class to import. @@ -1173,11 +1215,14 @@ Erase it? Y/N: """)) 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) < 1: - raise UsageError, _('Not enough arguments supplied') + raise UsageError(_('Not enough arguments supplied')) from roundup import hyperdb + if hasattr (csv, 'field_size_limit'): + csv.field_size_limit(self.db.config.CSV_FIELD_SIZE) + # directory to import from dir = args[0] @@ -1213,7 +1258,10 @@ Erase it? Y/N: """)) if hasattr(cl, 'import_files'): cl.import_files(dir, nodeid) maxid = max(maxid, int(nodeid)) - print + + # (print to sys.stdout here to allow tests to squash it .. ugh) + print >> sys.stdout + f.close() # import the journals @@ -1222,15 +1270,17 @@ Erase it? Y/N: """)) cl.import_journals(reader) f.close() + # (print to sys.stdout here to allow tests to squash it .. ugh) + print >> sys.stdout, 'setting', classname, maxid+1 + # set the id counter - print 'setting', classname, maxid+1 self.db.setid(classname, str(maxid+1)) self.db_uncommitted = True return 0 def do_pack(self, args): - ""'''Usage: pack period | date + ''"""Usage: pack period | date Remove journal entries older than a period of time specified or before a certain date. @@ -1246,19 +1296,19 @@ Erase it? Y/N: """)) Date format is "YYYY-MM-DD" eg: 2001-01-01 - ''' - if len(args) <> 1: - raise UsageError, _('Not enough arguments supplied') + """ + if len(args) != 1: + raise UsageError(_('Not enough arguments supplied')) # are we dealing with a period or a date value = args[0] - date_re = re.compile(r''' + date_re = re.compile(r""" (?P\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd (?P(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)? - ''', re.VERBOSE) + """, re.VERBOSE) m = date_re.match(value) if not m: - raise ValueError, _('Invalid format') + raise ValueError(_('Invalid format')) m = m.groupdict() if m['period']: pack_before = date.Date(". - %s"%value) @@ -1269,12 +1319,12 @@ Erase it? Y/N: """)) return 0 def do_reindex(self, args, desre=re.compile('([A-Za-z]+)([0-9]+)')): - ""'''Usage: reindex [classname|designator]* + ''"""Usage: reindex [classname|designator]* Re-generate a tracker's search indexes. This will re-generate the search indexes for a tracker. This will typically happen automatically. - ''' + """ if args: for arg in args: m = desre.match(arg) @@ -1283,8 +1333,8 @@ Erase it? Y/N: """)) try: cl.index(m.group(2)) except IndexError: - raise UsageError, _('no such item "%(designator)s"')%{ - 'designator': arg} + raise UsageError(_('no such item "%(designator)s"')%{ + 'designator': arg}) else: cl = self.get_class(arg) self.db.reindex(arg) @@ -1293,9 +1343,9 @@ Erase it? Y/N: """)) return 0 def do_security(self, args): - ""'''Usage: security [Role name] + ''"""Usage: security [Role name] Display the Permissions available to one or all Roles. - ''' + """ if len(args) == 1: role = args[0] try: @@ -1304,7 +1354,7 @@ Erase it? Y/N: """)) print _('No such Role "%(role)s"')%locals() return 1 else: - roles = self.db.security.role.items() + roles = list(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() @@ -1333,7 +1383,7 @@ Erase it? Y/N: """)) def do_migrate(self, args): - '''Usage: migrate + ''"""Usage: migrate Update a tracker's database to be compatible with the Roundup codebase. @@ -1350,7 +1400,7 @@ Erase it? Y/N: """)) It's safe to run this even if it's not required, so just get into the habit. - ''' + """ if getattr(self.db, 'db_version_updated'): print _('Tracker updated') self.db_uncommitted = True @@ -1359,8 +1409,8 @@ Erase it? Y/N: """)) return 0 def run_command(self, args): - '''Run a single command - ''' + """Run a single command + """ command = args[0] # handle help now @@ -1441,8 +1491,8 @@ Erase it? Y/N: """)) return ret def interactive(self): - '''Run in an interactive mode - ''' + """Run in an interactive mode + """ print _('Roundup %s ready for input.\nType "help" for help.' % roundup_version) try: @@ -1480,7 +1530,7 @@ Erase it? Y/N: """)) 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'): + if 'ROUNDUP_LOGIN' in os.environ: l = os.environ['ROUNDUP_LOGIN'].split(':') name = l[0] if len(l) > 1: