X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fadmin.py;h=511046df287691d90d12f80ac398ff1ff17cdbe3;hb=fce71c7148e3c0d3acb6f4195b7d541f9bc8dd0b;hp=b213c14d70624d8cee19dcd3cfc038ae3694fa89;hpb=369fb117ee3618ba00b5386cbaa4b4f19c2db4a4;p=roundup.git diff --git a/roundup/admin.py b/roundup/admin.py index b213c14..511046d 100644 --- a/roundup/admin.py +++ b/roundup/admin.py @@ -15,116 +15,150 @@ # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. -# -# $Id: admin.py,v 1.18 2002-07-18 11:17:30 gmcm Exp $ - -import sys, os, getpass, getopt, re, UserDict, shlex, shutil -try: - import csv -except ImportError: - csv = None +# + +"""Administration commands for maintaining Roundup trackers. +""" +__docformat__ = 'restructuredtext' + +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.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 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(): + 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.instance_home = '' + self.tracker_home = '' self.db = None + 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. + + 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 " "'. + -V -- be verbose when importing + -v -- report Roundup and Python versions (and quit) + + 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 help summary. + """ print _('Commands:'), commands = [''] - for command in self.commands.values(): - h = command.__doc__.split('\n')[0] + for command in self.commands.itervalues(): + 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.')) + commands.append(_( +"""Commands may be abbreviated as long as the abbreviation +matches only one 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') + """ 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:]:
@@ -132,57 +166,58 @@ Options:
                     print line[indent:]
                 else:
                     print line
-            print _('
\n') + print '
\n' 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 -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". + 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 +an issue tracker. It may be thought of as the issue tracker's "home +directory". It may 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, ... Property values are represented as strings in command arguments and in the printed results: . Strings are, well, strings. - . Date values are printed in the full date format in the local time zone, and - accepted in the full format or any of the partial formats explained below. + . Date values are printed in the full date format in the local time zone, + and accepted in the full format or any of the partial formats explained + below. . Link values are printed as node designators. When given as an argument, node designators and key strings are both accepted. - . Multilink values are printed as lists of node designators joined by commas. - When given as an argument, node designators and key strings are both - accepted; an empty string, a single node, or a list of nodes joined by - commas is accepted. + . Multilink values are printed as lists of node designators joined + by commas. When given as an argument, node designators and key + strings are both accepted; an empty string, a single node, or a list + of nodes joined by commas is accepted. When property values must contain spaces, just surround the value with quotes, either ' or ". A single space may also be backslash-quoted. If a -valuu must contain a quote character, it must be backslash-quoted or inside +value must contain a quote character, it must be backslash-quoted or inside quotes. Examples: hello world (2 tokens: hello, world) "hello world" (1 token: hello world) "Roch'e" Compaan (2 tokens: Roch'e Compaan) - Roch\'e Compaan (2 tokens: Roch'e Compaan) + Roch\\'e Compaan (2 tokens: Roch'e Compaan) address="1 2 3" (1 token: address=1 2 3) - \\ (1 token: \) - \n\r\t (1 token: a newline, carriage-return and tab) + \\\\ (1 token: \\) + \\n\\r\\t (1 token: a newline, carriage-return and tab) When multiple nodes are specified to the roundup get or roundup set commands, the specified properties are retrieved or set on all the listed -nodes. +nodes. When multiple results are returned by the roundup get or roundup find commands, they are printed one per line (default) or joined by commas (with -the -c) option. +the -c) option. Where the command changes data, a login name/password is required. The login may be specified as either "name" or "name:password". . ROUNDUP_LOGIN environment variable . the -u command-line option If either the name or password is not supplied, they are obtained from the -command-line. +command-line. Date format examples: "2000-04-17.03:45" means @@ -195,29 +230,29 @@ Date format examples: "." means "right now" Command help: -''') +""") for name, command in self.commands.items(): print _('%s:')%name - print _(' '), command.__doc__ + 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: topic = 'help' - + # try help_ methods - if self.help.has_key(topic): + if topic in self.help: self.help[topic]() return 0 @@ -230,7 +265,7 @@ Command help: # display the help for each match, removing the docsring indent for name, help in l: - lines = nl_re.split(help.__doc__) + lines = nl_re.split(_(help.__doc__)) print lines[0] indent = indent_re.match(lines[1]) if indent: indent = len(indent.group(1)) @@ -241,43 +276,121 @@ 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 is where they will be if we installed an egg via easy_install + 2. /share/roundup/templates/* + this should be the standard place to find them when Roundup is + installed + 3. /../templates/* + this will be used if Roundup's run in the distro (aka. source) + directory + 4. /* + 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 + # like: + # /usr/lib/python2.5/site-packages/roundup/admin.py (5 dirs up) + # c:\python25\lib\site-packages\roundup\admin.py (4 dirs up) + # /usr/lib/python2.5/site-packages/roundup-1.3.3-py2.5-egg/roundup/admin.py + # (2 dirs up) + # + # we're interested in where the directory containing "share" is + templates = {} + for N in 2, 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() + templates = self.listTemplates() print _('Templates:'), ', '.join(templates) import roundup.backends - backends = roundup.backends.__all__ + 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]]]] + Install a new Roundup tracker. - def do_install(self, instance_home, args): - '''Usage: install [template [backend [admin password]]] - Install a new Roundup instance. + The command will prompt for the tracker home directory + (if not supplied through TRACKER_HOME or the -i option). + The template and backend may be specified on the command-line + as arguments, in that order. - 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. + Command line arguments following the backend allows you to + pass initial values for config options. For example, passing + "web_http_auth=no,rdbms_user=dinsdale" will override defaults + for options http_auth in section [web] and user in section [rdbms]. + Please be careful to not use spaces in this argument! (Enclose + whole argument in quotes if you need spaces in option value). 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') + 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 + 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 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! +Erase it? Y/N: """) % locals()) + if ok.strip().lower() != 'y': + return 0 + + # clear it out so the install isn't confused + shutil.rmtree(tracker_home) # 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) @@ -288,7 +401,7 @@ Command help: # select hyperdb backend import roundup.backends - backends = roundup.backends.__all__ + backends = roundup.backends.list_backends() backend = len(args) > 2 and args[2] or '' if backend not in backends: print _('Back ends:'), ', '.join(backends) @@ -296,34 +409,72 @@ 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) - - 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. + # Process configuration file definitions + if len(args) > 3: + try: + defns = dict([item.split("=") for item in args[3].split(",")]) + except: + print _('Error in configuration settings: "%s"') % args[3] + raise + else: + defns = {} - If you wish to modify the default schema, you should also edit the database - initialisation file: + # install! + init.install(tracker_home, templates[template]['path'], settings=defns) + init.write_select_db(tracker_home, backend) + + print _(""" +--------------------------------------------------------------------------- + You should now edit the tracker configuration file: + %(config_file)s""") % {"config_file": config_ini_file} + + # find list of options that need manual adjustments + # XXX config._get_unset_options() is marked as private + # (leading underscore). make it public or don't care? + need_set = CoreConfig(tracker_home)._get_unset_options() + if need_set: + print _(" ... at a minimum, you must set following options:") + for section in need_set: + print " [%s]: %s" % (section, ", ".join(need_set[section])) + + # note about schema modifications + print _(""" + If you wish to modify the database schema, + you should also edit the schema file: %(database_config_file)s + You may also change the database initialisation file: + %(database_init_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') + + You MUST run the "roundup-admin initialise" command once you've performed + the above steps. +--------------------------------------------------------------------------- +""") % { + 'database_config_file': os.path.join(tracker_home, 'schema.py'), + 'database_init_file': os.path.join(tracker_home, 'initial_data.py'), } return 0 + def do_genconfig(self, args): + ''"""Usage: genconfig + Generate a new tracker config file (ini style) with default values + in . + """ + if len(args) < 1: + raise UsageError(_('Not enough arguments supplied')) + config = CoreConfig() + config.save(args[0]) - def do_initialise(self, instance_home, args): - '''Usage: initialise [adminpw] - 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: adminpw = args[1] @@ -334,132 +485,187 @@ Command help: adminpw = getpass.getpass(_('Admin Password: ')) confirm = getpass.getpass(_(' Confirm: ')) - # make sure the instance home is installed - if not os.path.exists(instance_home): - raise UsageError, _('Instance home does not exist')%locals() - if not os.path.exists(os.path.join(instance_home, 'html')): - raise UsageError, _('Instance has not been installed')%locals() + # make sure the tracker home is installed + if not os.path.exists(tracker_home): + 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()) # is there already a database? - if os.path.exists(os.path.join(instance_home, 'db')): - 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': + if tracker.exists(): + ok = raw_input(_( +"""WARNING: The database is already initialised! +If you re-initialise it, you will lose all the data! +Erase it? Y/N: """)) + if ok.strip().lower() != 'y': return 0 + backend = tracker.get_backend_name() + # nuke it - shutil.rmtree(os.path.join(instance_home, 'db')) + tracker.nuke() + + # re-write the backend select file + init.write_select_db(tracker_home, backend, tracker.config.DATABASE) # GO - init.initialise(instance_home, adminpw) + tracker.init(password.Password(adminpw)) 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). - Retrieves the property value of the nodes specified by the designators. - ''' + 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 = [] for designator in designators: # decode the node designator try: - classname, nodeid = roundupdb.splitDesignator(designator) - except roundupdb.DesignatorError, message: - raise UsageError, 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() + 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) + raise UsageError(_('no such %(classname)s property ' + '"%(propname)s"')%locals()) + 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). + ''"""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,...]"). + + 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(',') + 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(',') - elif isinstance(proptype, hyperdb.Boolean): - props[key] = value.lower() in ('yes', 'true', 'on', '1') - elif isinstance(proptype, hyperdb.Number): - props[key] = int(value) + 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) + cl.set(itemid, **props) except (TypeError, IndexError, ValueError), message: - raise UsageError, message + import traceback; traceback.print_exc() + 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. - ''' + 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) @@ -467,98 +673,109 @@ Command help: # handle the propname=value argument props = self.props_from_args(args[1:]) - # if the value isn't a number, look up the linked class to get the - # number - for propname, value in props.items(): - num_re = re.compile('^\d+$') - if not num_re.match(value): - # get the property - try: - property = cl.properties[propname] - except KeyError: - raise UsageError, _('%(classname)s has no property ' - '"%(propname)s"')%locals() - - # 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: - props[propname] = link_class.lookup(value) - except TypeError: - raise UsageError, _('%(classname)s has no key property"')%{ - 'classname': link_class.classname} + # convert the user-input value to a value used for find() + for propname, value in props.iteritems(): + if ',' in value: + values = value.split(',') + else: + values = [value] + d = props[propname] = {} + for value in values: + value = hyperdb.rawToHyperdb(self.db, cl, None, propname, value) + if isinstance(value, list): + for entry in value: + d[entry] = 1 + else: + d[value] = 1 - # now do the find + # 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 = cl.find(**props) + for i in id: + designator.append(classname + i) + print self.separator.join(designator) + else: + print self.separator.join(cl.find(**props)) + else: - print apply(cl.find, (), props) + if self.print_designator: + id = cl.find(**props) + for i in id: + designator.append(classname + i) + print designator + else: + 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 - Show the property values for the given node. + ''"""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 - 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 = 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] @@ -571,8 +788,9 @@ Command help: 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 @@ -593,59 +811,46 @@ Command help: props = self.props_from_args(args[1:]) # convert types - for propname, value in props.items(): - # get the property + for propname in props: 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(',') - elif isinstance(proptype, hyperdb.Boolean): - props[propname] = value.lower() in ('yes', 'true', 'on', '1') - elif isinstance(proptype, hyperdb.Number): - props[propname] = int(value) + props[propname] = hyperdb.rawToHyperdb(self.db, cl, None, + propname, props[propname]) + except hyperdb.HyperdbValueError, 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 - specified, the "label" property is used. The label property is tried - in order: the key, "name", "title" and then the first property, - alphabetically. - ''' + 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') + raise UsageError(_('Not enough arguments supplied')) classname = args[0] - + # get the class cl = self.get_class(classname) @@ -655,35 +860,62 @@ 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: 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 - 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:: + specified, all properties are displayed. By default, the column + widths 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 - ''' + 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') + raise UsageError(_('Not enough arguments supplied')) classname = args[0] # get the class @@ -698,23 +930,33 @@ Command help: 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 = [] 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]) @@ -739,29 +981,33 @@ Command help: 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 = roundupdb.splitDesignator(args[0]) - except roundupdb.DesignatorError, message: - raise UsageError, message + classname, nodeid = hyperdb.splitDesignator(args[0]) + except hyperdb.DesignatorError, 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 - Commit all changes made to the database. + ''"""Usage: commit + Commit changes made to the database during an interactive session. The changes made during an interactive session are not automatically written to the database - they must be committed @@ -769,221 +1015,402 @@ Command help: 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. - 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. - ''' + 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 = roundupdb.splitDesignator(designator) - except roundupdb.DesignatorError, message: - raise UsageError, message + classname, nodeid = hyperdb.splitDesignator(designator) + except hyperdb.DesignatorError, 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()) + self.db_uncommitted = True + return 0 + + def do_restore(self, args): + ''"""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')) + 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() + raise UsageError(_('no such %(classname)s node ' + '"%(nodeid)s"')%locals()) + self.db_uncommitted = True return 0 - def do_export(self, args): - '''Usage: export class[,class] destination_dir - Export the database to tab-separated-value files. + def do_export(self, args, export_files=True): + ''"""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. + + Optionally limit the export to just the named classes + or exclude the named classes, if the 1st argument starts with '-'. 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] + 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')) + + 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 + if not c in args[0][1:].split(',') ] + else: + classes = args[0].split(',') + else: + classes = self.db.classes + + class colon_separated(csv.excel): + delimiter = ':' - # use the csv parser if we can - it's faster - if csv is not None: - p = csv.parser(field_sep=':') + # make sure target dir exists + 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) - f = open(os.path.join(dir, classname+'.csv'), 'w') - f.write(':'.join(cl.properties.keys()) + '\n') + + if not export_files and hasattr(cl, 'export_files'): + sys.stdout.write('Exporting %s WITHOUT the files\r\n'% + classname) + + f = open(os.path.join(dir, classname+'.csv'), 'wb') + writer = csv.writer(f, colon_separated) + + properties = cl.getprops() + propnames = cl.export_propnames() + 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') + for nodeid in cl.getnodeids(): + if self.verbose: + sys.stdout.write('\rExporting %s - %s'%(classname, nodeid)) + sys.stdout.flush() + 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) + + # close this file + f.close() + + # export the journals + jf = open(os.path.join(dir, classname+'-journals.csv'), 'wb') + if self.verbose: + sys.stdout.write("\nExporting Journal for %s\n" % classname) + sys.stdout.flush() + journals = csv.writer(jf, colon_separated) + 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 + 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. + + Optionally limit the export to just the named classes + or exclude the named classes, if the 1st argument starts with '-'. + + 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 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/') + ''"""Usage: import import_dir + Import a database from the directory containing CSV files, + two per class to import. + The files used in the import are: + + .csv + This must define the same properties as the class (including + having a "header" line with those property names.) + -journals.csv + This defines the journals for the items being imported. + + 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) < 1: + raise UsageError(_('Not enough arguments supplied')) 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) + if hasattr (csv, 'field_size_limit'): + csv.field_size_limit(self.db.config.CSV_FIELD_SIZE) + + # directory to import from + dir = args[0] + + class colon_separated(csv.excel): + delimiter = ':' + + # import all the files + for file in os.listdir(dir): + classname, ext = os.path.splitext(file) + # we only care about CSV files + if ext != '.csv' or classname.endswith('-journals'): + continue + + cl = self.get_class(classname) + + # ensure that the properties and the CSV file headings match + f = open(os.path.join(dir, file), 'r') + reader = csv.reader(f, colon_separated) + file_props = None + maxid = 1 + # loop through the file and create a node for each entry + for n, r in enumerate(reader): + if file_props is None: + file_props = r + continue + + if self.verbose: + sys.stdout.write('\rImporting %s - %s'%(classname, n)) + sys.stdout.flush() + + # do the import and figure the current highest nodeid + nodeid = cl.import_list(file_props, r) + if hasattr(cl, 'import_files'): + cl.import_files(dir, nodeid) + maxid = max(maxid, int(nodeid)) + + # (print to sys.stdout here to allow tests to squash it .. ugh) + print >> sys.stdout + + f.close() + + # import the journals + f = open(os.path.join(args[0], classname + '-journals.csv'), 'r') + reader = csv.reader(f, colon_separated) + 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 + 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. + Remove journal entries older than a period of time specified or + before a certain date. -A period is specified using the suffixes "y", "m", and "d". The -suffix "w" (for "week") means 7 days. + A period is specified using the suffixes "y", "m", and "d". The + suffix "w" (for "week") means 7 days. - "3y" means three years - "2y 1m" means two years and one month - "1m 25d" means one month and 25 days - "2w 3d" means two weeks and three days + "3y" means three years + "2y 1m" means two years and one month + "1m 25d" means one month and 25 days + "2w 3d" means two weeks and three days + + Date format is "YYYY-MM-DD" eg: + 2001-01-01 + + """ + if len(args) != 1: + raise UsageError(_('Not enough arguments supplied')) -Date format is "YYYY-MM-DD" eg: - 2001-01-01 - - ''' - 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) elif m['date']: pack_before = date.Date(value) self.db.pack(pack_before) + self.db_uncommitted = True + return 0 + + def do_reindex(self, args, desre=re.compile('([A-Za-z]+)([0-9]+)')): + ''"""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) + if m: + cl = self.get_class(m.group(1)) + try: + cl.index(m.group(2)) + except IndexError: + raise UsageError(_('no such item "%(designator)s"')%{ + 'designator': arg}) + else: + cl = self.get_class(arg) + self.db.reindex(arg) + else: + self.db.reindex(show_progress=True) + 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 = 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() + 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: + d = permission.__dict__ + if permission.klass: + if permission.properties: + print _(' %(description)s (%(name)s for "%(klass)s"' + ': %(properties)s only)')%d + else: + print _(' %(description)s (%(name)s for "%(klass)s" ' + 'only)')%d + else: + print _(' %(description)s (%(name)s)')%d return 0 - def do_reindex(self, args): - '''Usage: reindex - Re-generate an instance's search indexes. - This will re-generate the search indexes for an instance. This will - typically happen automatically. - ''' - self.db.indexer.force_reindex() - self.db.reindex() + def do_migrate(self, args): + ''"""Usage: migrate + Update a tracker's database to be compatible with the Roundup + codebase. + + You should run the "migrate" command for your tracker once you've + installed the latest codebase. + + Do this before you use the web, command-line or mail interface and + before any users access the tracker. + + This command will respond with either "Tracker updated" (if you've + not previously run it on an RDBMS backend) or "No migration action + required" (if you have run it, or have used another interface to the + tracker, or possibly because you are using anydbm). + + 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 + else: + print _('No migration action required') return 0 def run_command(self, args): - '''Run a single command - ''' + """Run a single command + """ command = args[0] # handle help now @@ -998,6 +1425,9 @@ Date format is "YYYY-MM-DD" eg: self.help_commands() self.help_all() return 0 + if command == 'config': + self.do_config(args[1:]) + return 0 # figure what the command is try: @@ -1015,35 +1445,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 @@ -1061,10 +1491,10 @@ Date format is "YYYY-MM-DD" eg: return ret def interactive(self): - '''Run in an interactive mode - ''' - print _('Roundup %s ready for input.'%roundup_version) - print _('Type "help" for help.') + """Run in an interactive mode + """ + print _('Roundup %s ready for input.\nType "help" for help.' + % roundup_version) try: import readline except ImportError: @@ -1083,7 +1513,7 @@ Date format is "YYYY-MM-DD" eg: self.run_command(args) # exit.. check for transactions - if self.db and self.db.transactions: + if self.db and self.db_uncommitted: commit = raw_input(_('There are unsaved changes. Commit them (y/N)? ')) if commit and commit[0].lower() == 'y': self.db.commit() @@ -1091,105 +1521,68 @@ 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:vV') 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'): + if 'ROUNDUP_LOGIN' in os.environ: l = os.environ['ROUNDUP_LOGIN'].split(':') name = l[0] if len(l) > 1: password = l[1] - self.comma_sep = 0 + self.separator = None + self.print_designator = 0 + self.verbose = 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 + elif opt == '-v': + print '%s (python %s)'%(roundup_version, sys.version.split()[0]) + return 0 + elif opt == '-V': + self.verbose = 1 + elif opt == '-i': + self.tracker_home = arg + elif opt == '-c': + if self.separator != None: + self.usage('Only one of -c, -S and -s may be specified') + return 1 + self.separator = ',' + elif opt == '-S': + if self.separator != None: + self.usage('Only one of -c, -S and -s may be specified') + return 1 + self.separator = arg + elif opt == '-s': + if self.separator != None: + self.usage('Only one of -c, -S and -s may be specified') + return 1 + self.separator = ' ' + elif 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.17 2002/07/14 06:05:50 richard -# . fixed the date module so that Date(". - 2d") works -# -# Revision 1.16 2002/07/09 04:19:09 richard -# Added reindex command to roundup-admin. -# Fixed reindex on first access. -# Also fixed reindexing of entries that change. -# -# Revision 1.15 2002/06/17 23:14:44 richard -# . #569415 ] {version} -# -# Revision 1.14 2002/06/11 06:41:50 richard -# Removed prompt for admin email in initialisation. -# -# Revision 1.13 2002/05/30 23:58:14 richard -# oops -# -# 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 +# vim: set filetype=python sts=4 sw=4 et si :