X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fadmin.py;h=ff961b77eb63c50084f049ff32a7d8835e20ba46;hb=ac5f91aa489bb614476c2d49b336c342f641ddaf;hp=7b42052d80a2ad374fb8343234bfa230da782c70;hpb=c50a14f24c30170a8efaf2b29f0f14541838d5ba;p=roundup.git diff --git a/roundup/admin.py b/roundup/admin.py index 7b42052..ff961b7 100644 --- a/roundup/admin.py +++ b/roundup/admin.py @@ -16,14 +16,15 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: admin.py,v 1.2 2002-01-07 10:41:44 richard Exp $ +# $Id: admin.py,v 1.32 2002-09-24 01:36:04 richard Exp $ -import sys, os, getpass, getopt, re, UserDict, shlex +import sys, os, getpass, getopt, re, UserDict, shlex, shutil try: import csv except ImportError: csv = None from roundup import date, hyperdb, roundupdb, init, password, token +from roundup import __version__ as roundup_version import roundup.instance from roundup.i18n import _ @@ -60,7 +61,7 @@ class AdminTool: for k in AdminTool.__dict__.keys(): if k[:5] == 'help_': self.help[k[5:]] = getattr(self, k) - self.instance_home = '' + self.tracker_home = '' self.db = None def get_class(self, classname): @@ -71,7 +72,7 @@ class AdminTool: except KeyError: raise UsageError, _('no such class "%(classname)s"')%locals() - def props_from_args(args, klass=None): + def props_from_args(self, args): props = {} for arg in args: if arg.find('=') == -1: @@ -80,23 +81,28 @@ class AdminTool: key, value = arg.split('=') except ValueError: raise UsageError, _('argument "%(arg)s" not propname=value')%locals() - props[key] = value + if value: + props[key] = value + else: + props[key] = None return props def usage(self, message=''): if message: message = _('Problem: %(message)s)\n\n')%locals() - print _('''%(message)sUsage: roundup-admin [-i instance home] [-u login] [-c] + 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 + -c -- when outputting lists of data, just comma-separate them 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): @@ -135,12 +141,12 @@ Options: def help_all(self): print _(''' -All commands (except help) require an instance specifier. This is just the path -to the roundup instance you're working with. A roundup instance is where +All commands (except help) require a tracker specifier. This is just the path +to the roundup tracker you're working with. A roundup tracker is where roundup keeps the database and configuration file that defines an issue tracker. It may be thought of as the issue tracker's "home directory". It may -be specified in the environment variable ROUNDUP_INSTANCE or on the command -line as "-i instance". +be specified in the environment variable TRACKER_HOME or on the command +line as "-i tracker". A designator is a classname and a nodeid concatenated, eg. bug1, user10, ... @@ -209,7 +215,11 @@ Command help: initopts -- init command options all -- all available help ''' - topic = args[0] + if len(args)>0: + topic = args[0] + else: + topic = 'help' + # try help_ methods if self.help.has_key(topic): @@ -244,20 +254,31 @@ Command help: backends = roundup.backends.__all__ print _('Back ends:'), ', '.join(backends) + def do_install(self, tracker_home, args): + '''Usage: install [template [backend [admin password]]] + Install a new Roundup tracker. - def do_initialise(self, instance_home, args): - '''Usage: initialise [template [backend [admin password]]] - Initialise a new Roundup instance. - - The command will prompt for the instance home directory (if not supplied - through INSTANCE_HOME or the -i option). The template, backend and admin + The command will prompt for the tracker home directory (if not supplied + through TRACKER_HOME or the -i option). The template, backend and admin password may be specified on the command-line as arguments, in that order. + The initialise command must be called after this command in order + to initialise the tracker's database. You may edit the tracker's + initial database contents before running that command by editing + the tracker's dbinit.py module init() function. + See also initopts help. ''' if len(args) < 1: raise UsageError, _('Not enough arguments supplied') + + # make sure the tracker home can be created + parent = os.path.split(tracker_home)[0] + if not os.path.exists(parent): + raise UsageError, _('Instance home parent directory "%(parent)s"' + ' does not exist')%locals() + # select template import roundup.templates templates = roundup.templates.listTemplates() @@ -269,6 +290,7 @@ Command help: if not template: template = 'classic' + # select hyperdb backend import roundup.backends backends = roundup.backends.__all__ backend = len(args) > 2 and args[2] or '' @@ -278,15 +300,65 @@ Command help: backend = raw_input(_('Select backend [anydbm]: ')).strip() if not backend: backend = 'anydbm' - if len(args) > 3: - adminpw = confirm = args[3] + # XXX perform a unit test based on the user's selections + + # install! + init.install(tracker_home, template, backend) + + print _(''' + You should now edit the tracker configuration file: + %(config_file)s + ... at a minimum, you must set MAILHOST, MAIL_DOMAIN and ADMIN_EMAIL. + + If you wish to modify the default schema, you should also edit the database + initialisation file: + %(database_config_file)s + ... see the documentation on customizing for more information. +''')%{ + 'config_file': os.path.join(tracker_home, 'config.py'), + 'database_config_file': os.path.join(tracker_home, 'dbinit.py') +} + return 0 + + + def do_initialise(self, tracker_home, args): + '''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] else: adminpw = '' confirm = 'x' - while adminpw != confirm: - adminpw = getpass.getpass(_('Admin Password: ')) - confirm = getpass.getpass(_(' Confirm: ')) - init.init(instance_home, template, backend, adminpw) + while adminpw != confirm: + adminpw = getpass.getpass(_('Admin Password: ')) + confirm = getpass.getpass(_(' Confirm: ')) + + # make sure the tracker home is installed + if not os.path.exists(tracker_home): + raise UsageError, _('Instance home does not exist')%locals() + if not os.path.exists(os.path.join(tracker_home, 'html')): + raise UsageError, _('Instance has not been installed')%locals() + + # is there already a database? + if os.path.exists(os.path.join(tracker_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': + return 0 + + # nuke it + shutil.rmtree(os.path.join(tracker_home, 'db')) + + # GO + init.initialise(tracker_home, adminpw) + return 0 @@ -304,8 +376,8 @@ Command help: for designator in designators: # decode the node designator try: - classname, nodeid = roundupdb.splitDesignator(designator) - except roundupdb.DesignatorError, message: + classname, nodeid = hyperdb.splitDesignator(designator) + except hyperdb.DesignatorError, message: raise UsageError, message # get the class @@ -326,35 +398,53 @@ Command help: 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). + + The items may be specified as a class or as a comma-separeted + list of item designators (ie "designator[,designator,...]"). - Sets the property to the value for all designators given. + 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 len(args) < 2: raise UsageError, _('Not enough arguments supplied') from roundup import hyperdb designators = args[0].split(',') + if len(designators) == 1: + designator = designators[0] + try: + designator = hyperdb.splitDesignator(designator) + designators = [designator] + except hyperdb.DesignatorError: + cl = self.get_class(designator) + designators = [(designator, x) for x in cl.list()] + else: + try: + designators = [hyperdb.splitDesignator(x) for x in designators] + except hyperdb.DesignatorError, message: + raise UsageError, message # get the props from the args props = self.props_from_args(args[1:]) # now do the set for all the nodes - for designator in designators: - # decode the node designator - try: - classname, nodeid = roundupdb.splitDesignator(designator) - except roundupdb.DesignatorError, message: - raise UsageError, message - - # get the class + for classname, itemid in designators: cl = self.get_class(classname) properties = cl.getprops() for key, value in props.items(): proptype = properties[key] - if isinstance(proptype, hyperdb.String): + if isinstance(proptype, hyperdb.Multilink): + if value is None: + props[key] = [] + else: + props[key] = value.split(',') + elif value is None: + continue + elif isinstance(proptype, hyperdb.String): continue elif isinstance(proptype, hyperdb.Password): props[key] = password.Password(value) @@ -370,12 +460,14 @@ Command help: 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 the set try: - apply(cl.set, (nodeid, ), props) + apply(cl.set, (itemid, ), props) except (TypeError, IndexError, ValueError), message: raise UsageError, message return 0 @@ -420,9 +512,6 @@ Command help: except TypeError: raise UsageError, _('%(classname)s has no key property"')%{ 'classname': link_class.classname} - except KeyError: - raise UsageError, _('%(classname)s has no entry "%(propname)s"')%{ - 'classname': link_class.classname, 'propname': propname} # now do the find try: @@ -469,8 +558,8 @@ Command help: # decode the node designator try: - classname, nodeid = roundupdb.splitDesignator(args[0]) - except roundupdb.DesignatorError, message: + classname, nodeid = hyperdb.splitDesignator(args[0]) + except hyperdb.DesignatorError, message: raise UsageError, message # get the class @@ -525,7 +614,7 @@ Command help: props = self.props_from_args(args[1:]) # convert types - for propname in props.keys(): + for propname, value in props.items(): # get the property try: proptype = properties[propname] @@ -535,18 +624,22 @@ Command help: if isinstance(proptype, hyperdb.Date): try: - props[key] = date.Date(value) + props[propname] = date.Date(value) except ValueError, message: raise UsageError, _('"%(value)s": %(message)s')%locals() elif isinstance(proptype, hyperdb.Interval): try: - props[key] = date.Interval(value) + props[propname] = date.Interval(value) except ValueError, message: raise UsageError, _('"%(value)s": %(message)s')%locals() elif isinstance(proptype, hyperdb.Password): - props[key] = password.Password(value) + props[propname] = password.Password(value) elif isinstance(proptype, hyperdb.Multilink): - props[key] = value.split(',') + 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) # check for the key property propname = cl.getkey() @@ -675,8 +768,8 @@ Command help: if len(args) < 1: raise UsageError, _('Not enough arguments supplied') try: - classname, nodeid = roundupdb.splitDesignator(args[0]) - except roundupdb.DesignatorError, message: + classname, nodeid = hyperdb.splitDesignator(args[0]) + except hyperdb.DesignatorError, message: raise UsageError, message try: @@ -725,8 +818,8 @@ Command help: designators = args[0].split(',') for designator in designators: try: - classname, nodeid = roundupdb.splitDesignator(designator) - except roundupdb.DesignatorError, message: + classname, nodeid = hyperdb.splitDesignator(designator) + except hyperdb.DesignatorError, message: raise UsageError, message try: self.db.getclass(classname).retire(nodeid) @@ -737,69 +830,63 @@ Command help: return 0 def do_export(self, args): - '''Usage: export class[,class] destination_dir - Export the database to tab-separated-value files. + '''Usage: export [class[,class]] export_dir + Export the database to colon-separated-value files. This action exports the current data from the database into - tab-separated-value files that are placed in the nominated destination - directory. The journals are not exported. + colon-separated-value files that are placed in the nominated + destination directory. The journals are not exported. ''' - if len(args) < 2: + # we need the CSV module + 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/') + + # grab the directory to export to + if len(args) < 1: raise UsageError, _('Not enough arguments supplied') - classes = args[0].split(',') - dir = args[1] + dir = args[-1] + + # get the list of classes to export + if len(args) == 2: + classes = args[0].split(',') + else: + classes = self.db.classes.keys() # use the csv parser if we can - it's faster - if csv is not None: - p = csv.parser(field_sep=':') + p = csv.parser(field_sep=':') # 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') + properties = cl.getprops() + propnames = properties.keys() + propnames.sort() + print >> f, p.join(propnames) # 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') + print >>f, p.join(cl.export_list(propnames, nodeid)) return 0 def do_import(self, args): - '''Usage: import class file - Import the contents of the tab-separated-value file. - - The file must define the same properties as the class (including having - a "header" line with those property names.) The new nodes are added to - the existing database - if you want to create a new database using the - imported data, then create a new database (or, tediously, retire all - the old data.) + '''Usage: import import_dir + Import a database from the directory containing CSV files, one per + class to import. + + The files must define the same properties as the class (including having + a "header" line with those property names.) + + The imported nodes will have the same nodeid as defined in the + import file, thus replacing any existing content. + + The new nodes are added to the existing database - if you want to + create a new database using the imported data, then create a new + database (or, tediously, retire all the old data.) ''' - if len(args) < 2: + if len(args) < 1: raise UsageError, _('Not enough arguments supplied') if csv is None: raise UsageError, \ @@ -808,53 +895,127 @@ Command help: 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 + for file in os.listdir(args[0]): + f = open(os.path.join(args[0], file)) + + # get the classname + classname = os.path.splitext(file)[0] - # parse lines until we get a complete entry + # ensure that the properties and the CSV file headings match + cl = self.get_class(classname) + p = csv.parser(field_sep=':') + file_props = p.parse(f.readline()) + properties = cl.getprops() + propnames = properties.keys() + propnames.sort() + m = file_props[:] + m.sort() + if m != propnames: + 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 + maxid = 1 while 1: - l = p.parse(line) - if l: break - - # make the new node's property map - d = {} - for i in n: - # Use eval to reverse the repr() used to output the CSV - value = eval(l[i]) - # Figure the property for this column - key = file_props[i] - proptype = cl.properties[key] - # Convert for property type - if isinstance(proptype, hyperdb.Date): - value = date.Date(value) - elif isinstance(proptype, hyperdb.Interval): - value = date.Interval(value) - elif isinstance(proptype, hyperdb.Password): - pwd = password.Password() - pwd.unpack(value) - value = pwd - if value is not None: - d[key] = value - - # and create the new node - apply(cl.create, (), d) + 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" + + # do the import and figure the current highest nodeid + maxid = max(maxid, int(cl.import_list(propnames, l))) + + print 'setting', classname, maxid+1 + self.db.setid(classname, str(maxid+1)) + return 0 + + def do_pack(self, args): + '''Usage: pack period | 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. + + "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') + + # are we dealing with a period or a date + value = args[0] + 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) + m = date_re.match(value) + if not m: + 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) + return 0 + + def do_reindex(self, args): + '''Usage: reindex + Re-generate a tracker's search indexes. + + This will re-generate the search indexes for a tracker. This will + typically happen automatically. + ''' + self.db.indexer.force_reindex() + self.db.reindex() + return 0 + + def do_security(self, args): + '''Usage: security [Role name] + Display the Permissions available to one or all Roles. + ''' + if len(args) == 1: + role = args[0] + try: + roles = [(args[0], self.db.security.role[args[0]])] + except KeyError: + print _('No such Role "%(role)s"')%locals() + return 1 + else: + roles = self.db.security.role.items() + role = self.db.config.NEW_WEB_USER_ROLES + if ',' in role: + print _('New Web users get the Roles "%(role)s"')%locals() + else: + print _('New Web users get the Role "%(role)s"')%locals() + role = self.db.config.NEW_EMAIL_USER_ROLES + if ',' in role: + print _('New Email users get the Roles "%(role)s"')%locals() + else: + print _('New Email users get the Role "%(role)s"')%locals() + roles.sort() + for rolename, role in roles: + print _('Role "%(name)s":')%role.__dict__ + for permission in role.permissions: + if permission.klass: + print _(' %(description)s (%(name)s for "%(klass)s" ' + 'only)')%permission.__dict__ + else: + print _(' %(description)s (%(name)s)')%permission.__dict__ return 0 def run_command(self, args): @@ -891,25 +1052,35 @@ Command help: 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 init + # before we open the db, we may be doing an install or init if command == 'initialise': - return self.do_initialise(self.instance_home, args) + try: + 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.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 _("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 @@ -917,6 +1088,7 @@ Command help: ret = function(args[1:]) except UsageError, message: print _('Error: %(message)s')%locals() + print print function.__doc__ ret = 1 except: @@ -928,7 +1100,7 @@ Command help: def interactive(self): '''Run in an interactive mode ''' - print _('Roundup {version} ready for input.') + print _('Roundup %s ready for input.'%roundup_version) print _('Type "help" for help.') try: import readline @@ -962,7 +1134,8 @@ Command help: 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'): l = os.environ['ROUNDUP_LOGIN'].split(':') @@ -975,32 +1148,26 @@ Command help: self.usage() return 0 if opt == '-i': - self.instance_home = arg + self.tracker_home = arg if opt == '-c': self.comma_sep = 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.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