Code

handle :add: better in cgi form parsing (sf bug 663235)
[roundup.git] / roundup / admin.py
index 51710810eafa2a7876bea19887604788e8505d10..88cbace2c8e6e3bb253a4d1ec896c38eadd70d07 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: admin.py,v 1.27 2002-09-10 07:07:16 richard Exp $
+# $Id: admin.py,v 1.35 2002-10-03 06:56:28 richard Exp $
+
+'''Administration commands for maintaining Roundup trackers.
+'''
 
 import sys, os, getpass, getopt, re, UserDict, shlex, shutil
 try:
@@ -51,7 +54,17 @@ 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():
@@ -73,18 +86,29 @@ class AdminTool:
             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()
+                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()
+            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 [options] <command> <arguments>
@@ -103,6 +127,8 @@ Help:
         self.help_commands()
 
     def help_commands(self):
+        ''' List the commands available with their precis help.
+        '''
         print _('Commands:'),
         commands = ['']
         for command in self.commands.values():
@@ -115,11 +141,13 @@ Help:
         print
 
     def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
-       commands = self.commands.values()
+        ''' Produce an HTML command list.
+        '''
+        commands = self.commands.values()
         def sortfun(a, b):
             return cmp(a.__name__, b.__name__)
         commands.sort(sortfun)
-       for command in commands:
+        for command in commands:
             h = command.__doc__.split('\n')
             name = command.__name__[3:]
             usage = h[0]
@@ -297,6 +325,7 @@ 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(tracker_home, template, backend)
@@ -304,7 +333,8 @@ Command help:
         print _('''
  You should now edit the tracker configuration file:
    %(config_file)s
- ... at a minimum, you must set MAILHOST, MAIL_DOMAIN and ADMIN_EMAIL.
+ ... at a minimum, you must set MAILHOST, TRACKER_WEB, MAIL_DOMAIN and
+ ADMIN_EMAIL.
 
  If you wish to modify the default schema, you should also edit the database
  initialisation file:
@@ -393,39 +423,65 @@ Command help:
         return 0
 
 
-    def do_set(self, args):
-        '''Usage: set designator[,designator]* propname=value ...
-        Set the given property of one or more designator(s).
+    def do_set(self, args, pwre = re.compile(r'{(\w+)}(.+)')):
+        '''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)
+                    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)
@@ -438,8 +494,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):
@@ -447,8 +501,9 @@ Command help:
 
             # try the set
             try:
-                apply(cl.set, (nodeid, ), props)
+                apply(cl.set, (itemid, ), props)
             except (TypeError, IndexError, ValueError), message:
+                import traceback; traceback.print_exc()
                 raise UsageError, message
         return 0
 
@@ -550,7 +605,7 @@ Command help:
             value = cl.get(nodeid, key)
             print _('%(key)s: %(value)s')%locals()
 
-    def do_create(self, args):
+    def do_create(self, args, pwre = re.compile(r'{(\w+)}(.+)')):
         '''Usage: create classname property=value ...
         Create a new entry of a given class.
 
@@ -613,7 +668,15 @@ Command help:
                 except ValueError, message:
                     raise UsageError, _('"%(value)s": %(message)s')%locals()
             elif isinstance(proptype, hyperdb.Password):
-                props[propname] = password.Password(value)
+                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):
@@ -811,11 +874,11 @@ Command help:
 
     def do_export(self, args):
         '''Usage: export [class[,class]] export_dir
-        Export the database to tab-separated-value files.
+        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:
@@ -862,9 +925,9 @@ Command help:
         The imported nodes will have the same nodeid as defined in the
         import file, thus replacing any existing content.
 
-        XXX The new nodes are added to the existing database - if you want to
-        XXX create a new database using the imported data, then create a new
-        XXX database (or, tediously, retire all the old data.)
+        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')
@@ -911,8 +974,8 @@ Command help:
                 # 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))
+            print 'setting', classname, maxid+1
+            self.db.setid(classname, str(maxid+1))
         return 0
 
     def do_pack(self, args):
@@ -1133,14 +1196,18 @@ Date format is "YYYY-MM-DD" eg:
                 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()