X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fadmin.py;h=e89f8643df326727cf15b1142a8be69d564fa09b;hb=d53f951d3021166e7f3bebc037708accdecb7ff5;hp=2f425c5621e0cb9a598c3347ddcc22f4b3d43736;hpb=5c8db092e60bb1a8fd29a523de0cbd23a1199fb4;p=roundup.git diff --git a/roundup/admin.py b/roundup/admin.py index 2f425c5..e89f864 100644 --- a/roundup/admin.py +++ b/roundup/admin.py @@ -16,17 +16,14 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: admin.py,v 1.44 2003-03-18 23:15:29 richard Exp $ +# $Id: admin.py,v 1.63 2004-03-21 23:39:08 richard Exp $ '''Administration commands for maintaining Roundup trackers. ''' +__docformat__ = 'restructuredtext' -import sys, os, getpass, getopt, re, UserDict, shlex, shutil -try: - import csv -except ImportError: - csv = None -from roundup import date, hyperdb, roundupdb, init, password, token +import sys, os, getpass, getopt, re, UserDict, shutil, rfc822 +from roundup import date, hyperdb, roundupdb, init, password, token, rcsv from roundup import __version__ as roundup_version import roundup.instance from roundup.i18n import _ @@ -95,11 +92,11 @@ class AdminTool: if arg.find('=') == -1: raise UsageError, _('argument "%(arg)s" not propname=value' )%locals() - try: - key, value = arg.split('=') - except ValueError: + 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: @@ -110,13 +107,20 @@ class AdminTool: ''' Display a simple usage message. ''' if message: - message = _('Problem: %(message)s)\n\n')%locals() - print _('''%(message)sUsage: roundup-admin [options] + 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 - -c -- when outputting lists of data, just comma-separate them + -d -- print full designators not just class id numbers + -c -- when outputting lists of data, comma-separate them. + Same as '-S ","'. + -S -- when outputting lists of data, string-separate them + -s -- when outputting lists of data, space-separate them. + Same as '-S " "'. + + Only one of -s, -c or -S can be specified. Help: roundup-admin -h @@ -271,10 +275,63 @@ Command help: print line return 0 + def listTemplates(self): + ''' List all the available templates. + + Look in the following places, where the later rules take precedence: + + 1. /share/roundup/templates/* + this should be the standard place to find them when Roundup is + installed + 2. /../templates/* + this will be used if Roundup's run in the distro (aka. source) + directory + 3. /* + this is for when someone unpacks a 3rd-party template + 4. + this is for someone who "cd"s to the 3rd-party template dir + ''' + # OK, try /share/roundup/templates + # -- this module (roundup.admin) will be installed in something + # like: + # /usr/lib/python2.2/site-packages/roundup/admin.py (5 dirs up) + # c:\python22\lib\site-packages\roundup\admin.py (4 dirs up) + # we're interested in where the "lib" directory is - ie. the /usr/ + # part + templates = {} + for N in 4, 5: + path = __file__ + # move up N elements in the path + for i in range(N): + path = os.path.dirname(path) + tdir = os.path.join(path, 'share', 'roundup', 'templates') + if os.path.isdir(tdir): + templates = init.listTemplates(tdir) + break + + # OK, now try as if we're in the roundup source distribution + # directory, so this module will be in .../roundup-*/roundup/admin.py + # and we're interested in the .../roundup-*/ part. + path = __file__ + for i in range(2): + path = os.path.dirname(path) + tdir = os.path.join(path, 'templates') + if os.path.isdir(tdir): + templates.update(init.listTemplates(tdir)) + + # Try subdirs of the current dir + templates.update(init.listTemplates(os.getcwd())) + + # Finally, try the current directory as a template + template = init.loadTemplateInfo(os.getcwd()) + if template: + templates[template['name']] = template + + return templates + def help_initopts(self): - import roundup.templates - templates = roundup.templates.listTemplates() - print _('Templates:'), ', '.join(templates) + templates = self.listTemplates() + print _('Templates:'), ', '.join(templates.keys()) import roundup.backends backends = roundup.backends.__all__ print _('Back ends:'), ', '.join(backends) @@ -305,12 +362,11 @@ Command help: ' does not exist')%locals() # select template - import roundup.templates - templates = roundup.templates.listTemplates() + templates = self.listTemplates() template = len(args) > 1 and args[1] or '' - if template not in templates: - print _('Templates:'), ', '.join(templates) - while template not in templates: + if not templates.has_key(template): + print _('Templates:'), ', '.join(templates.keys()) + while not templates.has_key(template): template = raw_input(_('Select template [classic]: ')).strip() if not template: template = 'classic' @@ -328,7 +384,7 @@ Command help: # XXX perform a unit test based on the user's selections # install! - init.install(tracker_home, template) + init.install(tracker_home, templates[template]['path']) init.write_select_db(tracker_home, backend) print _(''' @@ -422,25 +478,63 @@ Command help: # get the class cl = self.get_class(classname) try: - if self.comma_sep: - l.append(cl.get(nodeid, propname)) + id=[] + if self.separator: + if self.print_designator: + # see if property is a link or multilink for + # which getting a desginator make sense. + # Algorithm: Get the properties of the + # current designator's class. (cl.getprops) + # get the property object for the property the + # user requested (properties[propname]) + # verify its type (isinstance...) + # raise error if not link/multilink + # get class name for link/multilink property + # do the get on the designators + # append the new designators + # print + properties = cl.getprops() + property = properties[propname] + if not (isinstance(property, hyperdb.Multilink) or + isinstance(property, hyperdb.Link)): + raise UsageError, _('property %s is not of type Multilink or Link so -d flag does not apply.')%propname + propclassname = self.db.getclass(property.classname).classname + id = cl.get(nodeid, propname) + for i in id: + l.append(propclassname + i) + else: + id = cl.get(nodeid, propname) + for i in id: + l.append(i) else: - print cl.get(nodeid, propname) + if self.print_designator: + properties = cl.getprops() + property = properties[propname] + if not (isinstance(property, hyperdb.Multilink) or + isinstance(property, hyperdb.Link)): + raise UsageError, _('property %s is not of type Multilink or Link so -d flag does not apply.')%propname + propclassname = self.db.getclass(property.classname).classname + id = cl.get(nodeid, propname) + for i in id: + print propclassname + i + else: + print cl.get(nodeid, propname) except IndexError: raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals() except KeyError: raise UsageError, _('no such %(classname)s property ' '"%(propname)s"')%locals() - if self.comma_sep: - print ','.join(l) + if self.separator: + print self.separator.join(l) + return 0 def do_set(self, args, pwre = re.compile(r'{(\w+)}(.+)')): - '''Usage: set [items] property=value property=value ... + '''Usage: set items property=value property=value ... Set the given properties of one or more items(s). - The items may be specified as a class or as a comma-separeted + The items are specified as a class or as a comma-separated list of item designators (ie "designator[,designator,...]"). This command sets the properties to the values for all designators @@ -476,42 +570,11 @@ Command help: properties = cl.getprops() for key, value in props.items(): - proptype = properties[key] - 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): - m = pwre.match(value) - if m: - # password is being given to us encrypted - p = password.Password() - p.scheme = m.group(1) - p.password = m.group(2) - props[key] = p - else: - 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.Boolean): - props[key] = value.lower() in ('yes', 'true', 'on', '1') - elif isinstance(proptype, hyperdb.Number): - props[key] = float(value) + try: + props[key] = hyperdb.rawToHyperdb(self.db, cl, itemid, + key, value) + except hyperdb.HyperdbValueError, message: + raise UsageError, message # try the set try: @@ -541,7 +604,9 @@ Command help: # number for propname, value in props.items(): num_re = re.compile('^\d+$') - if not num_re.match(value): + if value == '-1': + props[propname] = None + elif not num_re.match(value): # get the property try: property = cl.properties[propname] @@ -564,10 +629,25 @@ Command help: # now do the find try: - if self.comma_sep: - print ','.join(apply(cl.find, (), props)) + id = [] + designator = [] + if self.separator: + if self.print_designator: + id=apply(cl.find, (), props) + for i in id: + designator.append(classname + i) + print self.separator.join(designator) + else: + print self.separator.join(apply(cl.find, (), props)) + else: - print apply(cl.find, (), props) + if self.print_designator: + id=apply(cl.find, (), props) + for i in id: + designator.append(classname + i) + print designator + else: + print apply(cl.find, (), props) except KeyError: raise UsageError, _('%(classname)s has no property ' '"%(propname)s"')%locals() @@ -596,8 +676,8 @@ Command help: print _('%(key)s: %(value)s')%locals() def do_display(self, args): - '''Usage: display designator - Show the property values for the given node. + '''Usage: display designator[,designator]* + Show the property values for the given node(s). This lists the properties and their associated values for the given node. @@ -606,18 +686,21 @@ Command help: raise UsageError, _('Not enough arguments supplied') # decode the node designator - try: - classname, nodeid = hyperdb.splitDesignator(args[0]) - except hyperdb.DesignatorError, message: - raise UsageError, message + for designator in args[0].split(','): + try: + classname, nodeid = hyperdb.splitDesignator(designator) + except hyperdb.DesignatorError, message: + raise UsageError, message - # get the class - cl = self.get_class(classname) + # get the class + cl = self.get_class(classname) - # display the values - for key in cl.properties.keys(): - value = cl.get(nodeid, key) - print _('%(key)s: %(value)s')%locals() + # display the values + keys = cl.properties.keys() + keys.sort() + for key in keys: + value = cl.get(nodeid, key) + print _('%(key)s: %(value)s')%locals() def do_create(self, args, pwre = re.compile(r'{(\w+)}(.+)')): '''Usage: create classname property=value ... @@ -664,39 +747,11 @@ Command help: # convert types for propname, value in props.items(): - # get the property try: - proptype = properties[propname] - except KeyError: - raise UsageError, _('%(classname)s has no property ' - '"%(propname)s"')%locals() - - if isinstance(proptype, hyperdb.Date): - try: - props[propname] = date.Date(value) - except ValueError, message: - raise UsageError, _('"%(value)s": %(message)s')%locals() - elif isinstance(proptype, hyperdb.Interval): - try: - props[propname] = date.Interval(value) - except ValueError, message: - raise UsageError, _('"%(value)s": %(message)s')%locals() - elif isinstance(proptype, hyperdb.Password): - m = pwre.match(value) - if m: - # password is being given to us encrypted - p = password.Password() - p.scheme = m.group(1) - p.password = m.group(2) - props[propname] = p - else: - 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] = float(value) + props[propname] = hyperdb.rawToHyperdb(self.db, cl, None, + propname, value) + except hyperdb.HyperdbValueError, message: + raise UsageError, message # check for the key property propname = cl.getkey() @@ -719,7 +774,13 @@ Command help: specified, the "label" property is used. The label property is tried in order: the key, "name", "title" and then the first property, alphabetically. + + With -c, -S or -s print a list of item id's if no property specified. + If property specified, print list of that property for every class + instance. ''' + if len(args) > 2: + raise UsageError, _('Too many arguments supplied') if len(args) < 1: raise UsageError, _('Not enough arguments supplied') classname = args[0] @@ -733,8 +794,21 @@ Command help: else: propname = cl.labelprop() - if self.comma_sep: - print ','.join(cl.list()) + if self.separator: + if len(args) == 2: + # create a list of propnames since user specified propname + proplist=[] + for nodeid in cl.list(): + try: + proplist.append(cl.get(nodeid, propname)) + except KeyError: + raise UsageError, _('%(classname)s has no property ' + '"%(propname)s"')%locals() + print self.separator.join(proplist) + else: + # create a list of index id's since user didn't specify + # otherwise + print self.separator.join(cl.list()) else: for nodeid in cl.list(): try: @@ -751,14 +825,27 @@ Command help: Lists all instances of the given class. If the properties are not specified, all properties are displayed. By default, the column widths - are the width of the property names. The width may be explicitly defined + are the width of the largest value. The width may be explicitly defined by defining the property as "name:width". For example:: + roundup> table priority id,name:10 Id Name 1 fatal-bug 2 bug 3 usability 4 feature + + Also to make the width of the column the width of the label, + leave a trailing : without a width on the property. For example:: + + roundup> table priority id,name: + Id Name + 1 fata + 2 bug + 3 usab + 4 feat + + will result in a the 4 character wide "Name" column. ''' if len(args) < 1: raise UsageError, _('Not enough arguments supplied') @@ -790,10 +877,19 @@ Command help: for spec in prop_names: if ':' in spec: name, width = spec.split(':') - props.append((name, int(width))) + if width == '': + props.append((name, len(spec))) + else: + props.append((name, int(width))) else: - props.append((spec, len(spec))) - + # this is going to be slow + maxlen = len(spec) + for nodeid in cl.list(): + curlen = len(str(cl.get(nodeid, spec))) + if curlen > maxlen: + maxlen = curlen + props.append((spec, maxlen)) + # now display the heading print ' '.join([name.capitalize().ljust(width) for name,width in props]) @@ -916,15 +1012,12 @@ Command help: 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') + if rcsv.error: + raise UsageError, _(rcsv.error) + dir = args[-1] # get the list of classes to export @@ -933,26 +1026,27 @@ Command help: else: classes = self.db.classes.keys() - # use the csv parser if we can - it's faster - 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') + writer = rcsv.writer(f, rcsv.colon_separated) properties = cl.getprops() propnames = properties.keys() propnames.sort() - l = propnames[:] - l.append('is retired') - print >> f, p.join(l) + fields = propnames[:] + fields.append('is retired') + writer.writerow(fields) # all nodes for this class (not using list() 'cos it doesn't # include retired nodes) for nodeid in self.db.getclass(classname).getnodeids(): # get the regular props - print >>f, p.join(cl.export_list(propnames, nodeid)) + writer.writerow (cl.export_list(propnames, nodeid)) + + # close this file + f.close() return 0 def do_import(self, args): @@ -972,11 +1066,8 @@ Command help: ''' if len(args) < 1: raise UsageError, _('Not enough arguments supplied') - if csv is None: - raise UsageError, \ - _('Sorry, you need the csv module to use this function.\n' - 'Get it from: http://www.object-craft.com.au/projects/csv/') - + if rcsv.error: + raise UsageError, _(rcsv.error) from roundup import hyperdb for file in os.listdir(args[0]): @@ -991,36 +1082,20 @@ Command help: # 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()) - -# XXX we don't _really_ need to do this... -# 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]} + reader = rcsv.reader(f, rcsv.colon_separated) + file_props = None + maxid = 1 # loop through the file and create a node for each entry - maxid = 1 - 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" + for r in reader: + if file_props is None: + file_props = r + continue # do the import and figure the current highest nodeid - maxid = max(maxid, int(cl.import_list(file_props, l))) + maxid = max(maxid, int(cl.import_list(file_props, r))) + # set the id counter print 'setting', classname, maxid+1 self.db.setid(classname, str(maxid+1)) return 0 @@ -1218,7 +1293,7 @@ Date format is "YYYY-MM-DD" eg: def main(self): try: - opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc') + opts, args = getopt.getopt(sys.argv[1:], 'i:u:hcdsS:') except getopt.GetoptError, e: self.usage(str(e)) return 1 @@ -1232,7 +1307,8 @@ Date format is "YYYY-MM-DD" eg: name = l[0] if len(l) > 1: password = l[1] - self.comma_sep = 0 + self.separator = None + self.print_designator = 0 for opt, arg in opts: if opt == '-h': self.usage() @@ -1240,7 +1316,22 @@ Date format is "YYYY-MM-DD" eg: if opt == '-i': self.tracker_home = arg if opt == '-c': - self.comma_sep = 1 + if self.separator != None: + self.usage('Only one of -c, -S and -s may be specified') + return 1 + self.separator = ',' + if opt == '-S': + if self.separator != None: + self.usage('Only one of -c, -S and -s may be specified') + return 1 + self.separator = arg + if opt == '-s': + if self.separator != None: + self.usage('Only one of -c, -S and -s may be specified') + return 1 + self.separator = ' ' + if opt == '-d': + self.print_designator = 1 # if no command - go interactive # wrap in a try/finally so we always close off the db