From bef4e35ddbb782cec7656afd2cb48bc6a517c475 Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 17 Oct 2001 23:13:19 +0000 Subject: [PATCH] Did a fair bit of work on the admin tool. Now has an extra command "table" which displays node information in a tabular format. Also fixed import and export so they work. Removed freshen. Fixed quopri usage in mailgw from bug reports. git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@309 57a73879-2fb5-44c3-a270-3262357dd7e2 --- roundup-admin | 137 +++++++++++++++++++++++++++++----------------- roundup/mailgw.py | 16 ++++-- 2 files changed, 97 insertions(+), 56 deletions(-) diff --git a/roundup-admin b/roundup-admin index be49474..f7a0ff2 100755 --- a/roundup-admin +++ b/roundup-admin @@ -1,4 +1,4 @@ -#! /usr/bin/python +#! /Users/builder/bin/python # # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/) # This module is free software, and you may redistribute it and/or modify @@ -16,7 +16,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: roundup-admin,v 1.32 2001-10-17 06:57:29 richard Exp $ +# $Id: roundup-admin,v 1.33 2001-10-17 23:13:19 richard Exp $ import sys if int(sys.version[0]) < 2: @@ -28,7 +28,7 @@ try: import csv except ImportError: csv = None -from roundup import date, roundupdb, init, password +from roundup import date, hyperdb, roundupdb, init, password import roundup.instance def usage(message=''): @@ -337,7 +337,7 @@ def do_list(db, args, comma_sep=0): '''Usage: list classname [property] List the instances of a class. - Lists all instances of the given class along. If the property is not + 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. @@ -356,6 +356,48 @@ def do_list(db, args, comma_sep=0): print "%4s: %s"%(nodeid, value) return 0 +def do_table(db, args, comma_sep=None): + '''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:: + roundup> table priority id,name:10 + Id Name + 1 fatal-bug + 2 bug + 3 usability + 4 feature + ''' + classname = args[0] + cl = db.getclass(classname) + if len(args) > 1: + prop_names = args[1].split(',') + else: + prop_names = cl.getprops().keys() + props = [] + for name in prop_names: + if ':' in name: + name, width = name.split(':') + props.append((name, int(width))) + else: + props.append((name, len(name))) + + print ' '.join([string.capitalize(name) for name, width in props]) + for nodeid in cl.list(): + l = [] + for name, width in props: + if name != 'id': + value = str(cl.get(nodeid, name)) + else: + value = str(nodeid) + f = '%%-%ds'%width + l.append(f%value[:width]) + print ' '.join(l) + return 0 + def do_history(db, args, comma_sep=0): '''Usage: history designator Show the history entries of a designator. @@ -382,11 +424,10 @@ def do_retire(db, args, comma_sep=0): def do_export(db, args, comma_sep=0): '''Usage: export class[,class] destination_dir - ** EXPERIMENTAL ** - Export the database to CSV files by class in the given directory. + Export the database to tab-separated-value files. This action exports the current data from the database into - comma-separated files that are placed in the nominated destination + tab-separated-value files that are placed in the nominated destination directory. The journals are not exported. ''' if len(args) < 2: @@ -397,35 +438,47 @@ def do_export(db, args, comma_sep=0): # use the csv parser if we can - it's faster if csv is not None: - p = csv.parser() + p = csv.parser(field_sep=':') # do all the classes specified for classname in classes: cl = db.getclass(classname) f = open(os.path.join(dir, classname+'.csv'), 'w') - f.write(string.join(cl.properties.keys(), ',') + '\n') + f.write(string.join(cl.properties.keys(), ':') + '\n') # all nodes for this class + properties = cl.properties.items() for nodeid in cl.list(): + l = [] + for prop, type in properties: + value = cl.get(nodeid, prop) + # convert data where needed + if isinstance(type, hyperdb.Date): + value = value.get_tuple() + elif isinstance(type, hyperdb.Interval): + value = value.get_tuple() + elif isinstance(type, hyperdb.Password): + value = str(value) + l.append(repr(value)) + + # now write if csv is not None: - s = p.join(map(str, cl.getnode(nodeid).values(protected=0))) - f.write(s + '\n') + f.write(p.join(l) + '\n') else: - l = [] # escape the individual entries to they're valid CSV - for entry in map(str, cl.getnode(nodeid).values(protected=0)): + m = [] + for entry in l: if '"' in entry: entry = '""'.join(entry.split('"')) - if ',' in entry: + if ':' in entry: entry = '"%s"'%entry - l.append(entry) - f.write(','.join(l) + '\n') + m.append(entry) + f.write(':'.join(m) + '\n') return 0 def do_import(db, args, comma_sep=0): '''Usage: import class file - ** EXPERIMENTAL ** - Import the contents of the CSV file as new nodes for the given class. + 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 @@ -434,7 +487,7 @@ def do_import(db, args, comma_sep=0): the old data.) ''' if len(args) < 2: - print do_export.__doc__ + print do_import.__doc__ return 1 if csv is None: print 'Sorry, you need the csv module to use this function.' @@ -446,15 +499,14 @@ def do_import(db, args, comma_sep=0): # ensure that the properties and the CSV file headings match cl = db.getclass(args[0]) f = open(args[1]) - p = csv.parser() + 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: - print do_export.__doc__ - print "\n\nFile doesn't define the same properties" + print 'Import file doesn\'t define the same properties as "%s".'%args[0] return 1 # loop through the file and create a node for each entry @@ -471,9 +523,12 @@ def do_import(db, args, comma_sep=0): # make the new node's property map d = {} for i in n: - value = l[i] + # 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] type = cl.properties[key] + # Convert for property type if isinstance(type, hyperdb.Date): value = date.Date(value) elif isinstance(type, hyperdb.Interval): @@ -482,37 +537,13 @@ def do_import(db, args, comma_sep=0): pwd = password.Password() pwd.unpack(value) value = pwd - elif isinstance(type, hyperdb.Multilink): - value = value.split(',') - d[key] = value + if value is not None: + d[key] = value # and create the new node apply(cl.create, (), d) return 0 -def do_freshen(db, args, comma_sep=0): - '''Usage: freshen - Freshen an existing instance. **DO NOT USE** - - This currently kills databases!!!! - - This action should generally not be used. It reads in an instance - database and writes it again. In the future, is may also update - instance code to account for changes in templates. It's probably wise - not to use it anyway. Until we're sure it won't break things... - ''' -# for classname, cl in db.classes.items(): -# properties = cl.properties.items() -# for nodeid in cl.list(): -# node = {} -# for name, type in properties: -# isinstance( if type, hyperdb.Multilink): -# node[name] = cl.get(nodeid, name, []) -# else: -# node[name] = cl.get(nodeid, name, None) -# db.setnode(classname, nodeid, node) - return 1 - def figureCommands(): d = {} for k, v in globals().items(): @@ -528,8 +559,9 @@ def figureHelp(): return d class AdminTool: - def run_command(self, args): + '''Run a single command + ''' command = args[0] # handle help now @@ -557,7 +589,7 @@ class AdminTool: # not a valid command if function is None: - usage('Unknown command "%s"'%command) + print 'Unknown command "%s" ("help commands" for a list)'%command return 1 # get the instance @@ -631,6 +663,9 @@ if __name__ == '__main__': # # $Log: not supported by cvs2svn $ +# Revision 1.32 2001/10/17 06:57:29 richard +# Interactive startup blurb - need to figure how to get the version in there. +# # Revision 1.31 2001/10/17 06:17:26 richard # Now with readline support :) # diff --git a/roundup/mailgw.py b/roundup/mailgw.py index 702fd55..88fe1e7 100644 --- a/roundup/mailgw.py +++ b/roundup/mailgw.py @@ -72,7 +72,7 @@ are calling the create() method to create a new node). If an auditor raises an exception, the original message is bounced back to the sender with the explanatory message given in the exception. -$Id: mailgw.py,v 1.19 2001-10-11 23:43:04 richard Exp $ +$Id: mailgw.py,v 1.20 2001-10-17 23:13:19 richard Exp $ ''' @@ -290,14 +290,16 @@ Subject was: "%s" # try name on Content-Type name = part.getparam('name') # this is just an attachment - data = part.fp.read() encoding = part.getencoding() if encoding == 'base64': - data = binascii.a2b_base64(data) + data = binascii.a2b_base64(part.fp.read()) elif encoding == 'quoted-printable': - data = quopri.decode(data) + # the quopri module wants to work with files + decoded = cStringIO.StringIO() + quopri.decode(part.fp, decoded) + data = decoded.getvalue() elif encoding == 'uuencoded': - data = binascii.a2b_uu(data) + data = binascii.a2b_uu(part.fp.read()) attachments.append((name, part.gettype(), data)) if content is None: @@ -416,6 +418,10 @@ def parseContent(content, blank_line=re.compile(r'[\r\n]+\s*[\r\n]+'), # # $Log: not supported by cvs2svn $ +# Revision 1.19 2001/10/11 23:43:04 richard +# Implemented the comma-separated printing option in the admin tool. +# Fixed a typo (more of a vim-o actually :) in mailgw. +# # Revision 1.18 2001/10/11 06:38:57 richard # Initial cut at trying to handle people responding to CC'ed messages that # create an issue. -- 2.30.2