X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fadmin.py;h=a73c086cbd82ed76392efe0fe6c482b431e127d5;hb=429f34ed2930c3712619b36894c4275e8567863b;hp=48b96d6026720611c13590deaf8577a8ab583411;hpb=356467c77c906d84407c28f872f94c4c4c160ade;p=roundup.git diff --git a/roundup/admin.py b/roundup/admin.py index 48b96d6..a73c086 100644 --- a/roundup/admin.py +++ b/roundup/admin.py @@ -16,7 +16,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: admin.py,v 1.22 2002-08-16 04:26:42 richard Exp $ +# $Id: admin.py,v 1.31 2002-09-18 05:07:47 richard Exp $ import sys, os, getpass, getopt, re, UserDict, shlex, shutil try: @@ -61,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): @@ -81,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): @@ -136,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, ... @@ -249,27 +254,27 @@ Command help: backends = roundup.backends.__all__ print _('Back ends:'), ', '.join(backends) - def do_install(self, instance_home, args): + def do_install(self, tracker_home, args): '''Usage: install [template [backend [admin password]]] - Install a new Roundup instance. + Install a new Roundup tracker. - The command will prompt for the instance home directory (if not supplied - through INSTANCE_HOME or the -i option). The template, backend and admin + The command will prompt for the tracker home directory (if not supplied + through TRACKER_HOME or the -i option). The template, backend and admin password may be specified on the command-line as arguments, in that order. The initialise command must be called after this command in order - to initialise the instance's database. You may edit the instance's + to initialise the tracker's database. You may edit the tracker's initial database contents before running that command by editing - the instance's dbinit.py module init() function. + the tracker's dbinit.py module init() function. See also initopts help. ''' if len(args) < 1: raise UsageError, _('Not enough arguments supplied') - # make sure the instance home can be created - parent = os.path.split(instance_home)[0] + # make sure the tracker home can be created + parent = os.path.split(tracker_home)[0] if not os.path.exists(parent): raise UsageError, _('Instance home parent directory "%(parent)s"' ' does not exist')%locals() @@ -295,13 +300,14 @@ Command help: backend = raw_input(_('Select backend [anydbm]: ')).strip() if not backend: backend = 'anydbm' + # XXX perform a unit test based on the user's selections # install! - init.install(instance_home, template, backend) + init.install(tracker_home, template, backend) print _(''' - You should now edit the instance configuration file: - %(instance_config_file)s + 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 @@ -309,19 +315,19 @@ Command help: %(database_config_file)s ... see the documentation on customizing for more information. ''')%{ - 'instance_config_file': os.path.join(instance_home, 'instance_config.py'), - 'database_config_file': os.path.join(instance_home, 'dbinit.py') + 'config_file': os.path.join(tracker_home, 'config.py'), + 'database_config_file': os.path.join(tracker_home, 'dbinit.py') } return 0 - def do_initialise(self, instance_home, args): + def do_initialise(self, tracker_home, args): '''Usage: initialise [adminpw] - Initialise a new Roundup instance. + Initialise a new Roundup tracker. The administrator details will be set at this step. - Execute the instance's initialisation function dbinit.init() + Execute the tracker's initialisation function dbinit.init() ''' # password if len(args) > 1: @@ -333,14 +339,14 @@ 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): + # make sure the tracker home is installed + if not os.path.exists(tracker_home): raise UsageError, _('Instance home does not exist')%locals() - if not os.path.exists(os.path.join(instance_home, 'html')): + 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(instance_home, 'db')): + 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() @@ -348,10 +354,10 @@ Command help: return 0 # nuke it - shutil.rmtree(os.path.join(instance_home, 'db')) + shutil.rmtree(os.path.join(tracker_home, 'db')) # GO - init.initialise(instance_home, adminpw) + init.initialise(tracker_home, adminpw) return 0 @@ -392,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). - Sets the property to the value for all designators given. + The items may be specified as a class or as a comma-separeted + list of item designators (ie "designator[,designator,...]"). + + This command sets the properties to the values for all designators + given. If the value is missing (ie. "property=") then the property is + un-set. ''' if 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 = hyperdb.splitDesignator(designator) - except hyperdb.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) @@ -436,8 +460,6 @@ 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): @@ -445,7 +467,7 @@ Command help: # try the set try: - apply(cl.set, (nodeid, ), props) + apply(cl.set, (itemid, ), props) except (TypeError, IndexError, ValueError), message: raise UsageError, message return 0 @@ -808,13 +830,19 @@ 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. ''' + # 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') @@ -827,56 +855,38 @@ Command help: 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.getprops() 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, \ @@ -885,56 +895,44 @@ 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 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 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 + self.db.setid(classname, str(maxid)) return 0 def do_pack(self, args): @@ -977,9 +975,9 @@ Date format is "YYYY-MM-DD" eg: def do_reindex(self, args): '''Usage: reindex - Re-generate an instance's search indexes. + Re-generate a tracker's search indexes. - This will re-generate the search indexes for an instance. This will + This will re-generate the search indexes for a tracker. This will typically happen automatically. ''' self.db.indexer.force_reindex() @@ -1054,35 +1052,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 @@ -1136,7 +1134,7 @@ Date format is "YYYY-MM-DD" eg: 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'): @@ -1150,107 +1148,26 @@ Date format is "YYYY-MM-DD" eg: 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.21 2002/08/01 01:07:37 richard -# include info about new user roles -# -# Revision 1.20 2002/08/01 00:56:22 richard -# Added the web access and email access permissions, so people can restrict -# access to users who register through the email interface (for example). -# Also added "security" command to the roundup-admin interface to display the -# Role/Permission config for an instance. -# -# Revision 1.19 2002/07/25 07:14:05 richard -# Bugger it. Here's the current shape of the new security implementation. -# Still to do: -# . call the security funcs from cgi and mailgw -# . change shipped templates to include correct initialisation and remove -# the old config vars -# ... that seems like a lot. The bulk of the work has been done though. Honest :) -# -# Revision 1.18 2002/07/18 11:17:30 gmcm -# Add Number and Boolean types to hyperdb. -# Add conversion cases to web, mail & admin interfaces. -# Add storage/serialization cases to back_anydbm & back_metakit. -# -# 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