Code

New config option csv_field_size: Pythons csv module (which is used for
[roundup.git] / roundup / admin.py
1 #! /usr/bin/env python
2 #
3 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
4 # This module is free software, and you may redistribute it and/or modify
5 # under the same terms as Python, so long as this copyright message and
6 # disclaimer are retained in their original form.
7 #
8 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
9 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
10 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
11 # POSSIBILITY OF SUCH DAMAGE.
12 #
13 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
14 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
15 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
16 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
17 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
18 #
20 """Administration commands for maintaining Roundup trackers.
21 """
22 __docformat__ = 'restructuredtext'
24 import csv, getopt, getpass, os, re, shutil, sys, UserDict
26 from roundup import date, hyperdb, roundupdb, init, password, token
27 from roundup import __version__ as roundup_version
28 import roundup.instance
29 from roundup.configuration import CoreConfig
30 from roundup.i18n import _
31 from roundup.exceptions import UsageError
33 class CommandDict(UserDict.UserDict):
34     """Simple dictionary that lets us do lookups using partial keys.
36     Original code submitted by Engelbert Gruber.
37     """
38     _marker = []
39     def get(self, key, default=_marker):
40         if self.data.has_key(key):
41             return [(key, self.data[key])]
42         keylist = self.data.keys()
43         keylist.sort()
44         l = []
45         for ki in keylist:
46             if ki.startswith(key):
47                 l.append((ki, self.data[ki]))
48         if not l and default is self._marker:
49             raise KeyError, key
50         return l
52 class AdminTool:
53     """ A collection of methods used in maintaining Roundup trackers.
55         Typically these methods are accessed through the roundup-admin
56         script. The main() method provided on this class gives the main
57         loop for the roundup-admin script.
59         Actions are defined by do_*() methods, with help for the action
60         given in the method docstring.
62         Additional help may be supplied by help_*() methods.
63     """
64     def __init__(self):
65         self.commands = CommandDict()
66         for k in AdminTool.__dict__.keys():
67             if k[:3] == 'do_':
68                 self.commands[k[3:]] = getattr(self, k)
69         self.help = {}
70         for k in AdminTool.__dict__.keys():
71             if k[:5] == 'help_':
72                 self.help[k[5:]] = getattr(self, k)
73         self.tracker_home = ''
74         self.db = None
75         self.db_uncommitted = False
77     def get_class(self, classname):
78         """Get the class - raise an exception if it doesn't exist.
79         """
80         try:
81             return self.db.getclass(classname)
82         except KeyError:
83             raise UsageError, _('no such class "%(classname)s"')%locals()
85     def props_from_args(self, args):
86         """ Produce a dictionary of prop: value from the args list.
88             The args list is specified as ``prop=value prop=value ...``.
89         """
90         props = {}
91         for arg in args:
92             if arg.find('=') == -1:
93                 raise UsageError, _('argument "%(arg)s" not propname=value'
94                     )%locals()
95             l = arg.split('=')
96             if len(l) < 2:
97                 raise UsageError, _('argument "%(arg)s" not propname=value'
98                     )%locals()
99             key, value = l[0], '='.join(l[1:])
100             if value:
101                 props[key] = value
102             else:
103                 props[key] = None
104         return props
106     def usage(self, message=''):
107         """ Display a simple usage message.
108         """
109         if message:
110             message = _('Problem: %(message)s\n\n')%locals()
111         print _("""%(message)sUsage: roundup-admin [options] [<command> <arguments>]
113 Options:
114  -i instance home  -- specify the issue tracker "home directory" to administer
115  -u                -- the user[:password] to use for commands
116  -d                -- print full designators not just class id numbers
117  -c                -- when outputting lists of data, comma-separate them.
118                       Same as '-S ","'.
119  -S <string>       -- when outputting lists of data, string-separate them
120  -s                -- when outputting lists of data, space-separate them.
121                       Same as '-S " "'.
122  -V                -- be verbose when importing
123  -v                -- report Roundup and Python versions (and quit)
125  Only one of -s, -c or -S can be specified.
127 Help:
128  roundup-admin -h
129  roundup-admin help                       -- this help
130  roundup-admin help <command>             -- command-specific help
131  roundup-admin help all                   -- all available help
132 """)%locals()
133         self.help_commands()
135     def help_commands(self):
136         """List the commands available with their help summary.
137         """
138         print _('Commands:'),
139         commands = ['']
140         for command in self.commands.values():
141             h = _(command.__doc__).split('\n')[0]
142             commands.append(' '+h[7:])
143         commands.sort()
144         commands.append(_(
145 """Commands may be abbreviated as long as the abbreviation
146 matches only one command, e.g. l == li == lis == list."""))
147         print '\n'.join(commands)
148         print
150     def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
151         """ Produce an HTML command list.
152         """
153         commands = self.commands.values()
154         def sortfun(a, b):
155             return cmp(a.__name__, b.__name__)
156         commands.sort(sortfun)
157         for command in commands:
158             h = _(command.__doc__).split('\n')
159             name = command.__name__[3:]
160             usage = h[0]
161             print """
162 <tr><td valign=top><strong>%(name)s</strong></td>
163     <td><tt>%(usage)s</tt><p>
164 <pre>""" % locals()
165             indent = indent_re.match(h[3])
166             if indent: indent = len(indent.group(1))
167             for line in h[3:]:
168                 if indent:
169                     print line[indent:]
170                 else:
171                     print line
172             print '</pre></td></tr>\n'
174     def help_all(self):
175         print _("""
176 All commands (except help) require a tracker specifier. This is just
177 the path to the roundup tracker you're working with. A roundup tracker
178 is where roundup keeps the database and configuration file that defines
179 an issue tracker. It may be thought of as the issue tracker's "home
180 directory". It may be specified in the environment variable TRACKER_HOME
181 or on the command line as "-i tracker".
183 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
185 Property values are represented as strings in command arguments and in the
186 printed results:
187  . Strings are, well, strings.
188  . Date values are printed in the full date format in the local time zone,
189    and accepted in the full format or any of the partial formats explained
190    below.
191  . Link values are printed as node designators. When given as an argument,
192    node designators and key strings are both accepted.
193  . Multilink values are printed as lists of node designators joined
194    by commas.  When given as an argument, node designators and key
195    strings are both accepted; an empty string, a single node, or a list
196    of nodes joined by commas is accepted.
198 When property values must contain spaces, just surround the value with
199 quotes, either ' or ". A single space may also be backslash-quoted. If a
200 value must contain a quote character, it must be backslash-quoted or inside
201 quotes. Examples:
202            hello world      (2 tokens: hello, world)
203            "hello world"    (1 token: hello world)
204            "Roch'e" Compaan (2 tokens: Roch'e Compaan)
205            Roch\\'e Compaan  (2 tokens: Roch'e Compaan)
206            address="1 2 3"  (1 token: address=1 2 3)
207            \\\\               (1 token: \\)
208            \\n\\r\\t           (1 token: a newline, carriage-return and tab)
210 When multiple nodes are specified to the roundup get or roundup set
211 commands, the specified properties are retrieved or set on all the listed
212 nodes.
214 When multiple results are returned by the roundup get or roundup find
215 commands, they are printed one per line (default) or joined by commas (with
216 the -c) option.
218 Where the command changes data, a login name/password is required. The
219 login may be specified as either "name" or "name:password".
220  . ROUNDUP_LOGIN environment variable
221  . the -u command-line option
222 If either the name or password is not supplied, they are obtained from the
223 command-line.
225 Date format examples:
226   "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
227   "2000-04-17" means <Date 2000-04-17.00:00:00>
228   "01-25" means <Date yyyy-01-25.00:00:00>
229   "08-13.22:13" means <Date yyyy-08-14.03:13:00>
230   "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
231   "14:25" means <Date yyyy-mm-dd.19:25:00>
232   "8:47:11" means <Date yyyy-mm-dd.13:47:11>
233   "." means "right now"
235 Command help:
236 """)
237         for name, command in self.commands.items():
238             print _('%s:')%name
239             print '   ', _(command.__doc__)
241     def do_help(self, args, nl_re=re.compile('[\r\n]'),
242             indent_re=re.compile(r'^(\s+)\S+')):
243         ''"""Usage: help topic
244         Give help about topic.
246         commands  -- list commands
247         <command> -- help specific to a command
248         initopts  -- init command options
249         all       -- all available help
250         """
251         if len(args)>0:
252             topic = args[0]
253         else:
254             topic = 'help'
257         # try help_ methods
258         if self.help.has_key(topic):
259             self.help[topic]()
260             return 0
262         # try command docstrings
263         try:
264             l = self.commands.get(topic)
265         except KeyError:
266             print _('Sorry, no help for "%(topic)s"')%locals()
267             return 1
269         # display the help for each match, removing the docsring indent
270         for name, help in l:
271             lines = nl_re.split(_(help.__doc__))
272             print lines[0]
273             indent = indent_re.match(lines[1])
274             if indent: indent = len(indent.group(1))
275             for line in lines[1:]:
276                 if indent:
277                     print line[indent:]
278                 else:
279                     print line
280         return 0
282     def listTemplates(self):
283         """ List all the available templates.
285         Look in the following places, where the later rules take precedence:
287          1. <roundup.admin.__file__>/../../share/roundup/templates/*
288             this is where they will be if we installed an egg via easy_install
289          2. <prefix>/share/roundup/templates/*
290             this should be the standard place to find them when Roundup is
291             installed
292          3. <roundup.admin.__file__>/../templates/*
293             this will be used if Roundup's run in the distro (aka. source)
294             directory
295          4. <current working dir>/*
296             this is for when someone unpacks a 3rd-party template
297          5. <current working dir>
298             this is for someone who "cd"s to the 3rd-party template dir
299         """
300         # OK, try <prefix>/share/roundup/templates
301         #     and <egg-directory>/share/roundup/templates
302         # -- this module (roundup.admin) will be installed in something
303         # like:
304         #    /usr/lib/python2.5/site-packages/roundup/admin.py  (5 dirs up)
305         #    c:\python25\lib\site-packages\roundup\admin.py     (4 dirs up)
306         #    /usr/lib/python2.5/site-packages/roundup-1.3.3-py2.5-egg/roundup/admin.py
307         #    (2 dirs up)
308         #
309         # we're interested in where the directory containing "share" is
310         templates = {}
311         for N in 2, 4, 5:
312             path = __file__
313             # move up N elements in the path
314             for i in range(N):
315                 path = os.path.dirname(path)
316             tdir = os.path.join(path, 'share', 'roundup', 'templates')
317             if os.path.isdir(tdir):
318                 templates = init.listTemplates(tdir)
319                 break
321         # OK, now try as if we're in the roundup source distribution
322         # directory, so this module will be in .../roundup-*/roundup/admin.py
323         # and we're interested in the .../roundup-*/ part.
324         path = __file__
325         for i in range(2):
326             path = os.path.dirname(path)
327         tdir = os.path.join(path, 'templates')
328         if os.path.isdir(tdir):
329             templates.update(init.listTemplates(tdir))
331         # Try subdirs of the current dir
332         templates.update(init.listTemplates(os.getcwd()))
334         # Finally, try the current directory as a template
335         template = init.loadTemplateInfo(os.getcwd())
336         if template:
337             templates[template['name']] = template
339         return templates
341     def help_initopts(self):
342         templates = self.listTemplates()
343         print _('Templates:'), ', '.join(templates.keys())
344         import roundup.backends
345         backends = roundup.backends.list_backends()
346         print _('Back ends:'), ', '.join(backends)
348     def do_install(self, tracker_home, args):
349         ''"""Usage: install [template [backend [key=val[,key=val]]]]
350         Install a new Roundup tracker.
352         The command will prompt for the tracker home directory
353         (if not supplied through TRACKER_HOME or the -i option).
354         The template and backend may be specified on the command-line
355         as arguments, in that order.
357         Command line arguments following the backend allows you to
358         pass initial values for config options.  For example, passing
359         "web_http_auth=no,rdbms_user=dinsdale" will override defaults
360         for options http_auth in section [web] and user in section [rdbms].
361         Please be careful to not use spaces in this argument! (Enclose
362         whole argument in quotes if you need spaces in option value).
364         The initialise command must be called after this command in order
365         to initialise the tracker's database. You may edit the tracker's
366         initial database contents before running that command by editing
367         the tracker's dbinit.py module init() function.
369         See also initopts help.
370         """
371         if len(args) < 1:
372             raise UsageError, _('Not enough arguments supplied')
374         # make sure the tracker home can be created
375         tracker_home = os.path.abspath(tracker_home)
376         parent = os.path.split(tracker_home)[0]
377         if not os.path.exists(parent):
378             raise UsageError, _('Instance home parent directory "%(parent)s"'
379                 ' does not exist')%locals()
381         config_ini_file = os.path.join(tracker_home, CoreConfig.INI_FILE)
382         # check for both old- and new-style configs
383         if filter(os.path.exists, [config_ini_file,
384                 os.path.join(tracker_home, 'config.py')]):
385             ok = raw_input(_(
386 """WARNING: There appears to be a tracker in "%(tracker_home)s"!
387 If you re-install it, you will lose all the data!
388 Erase it? Y/N: """) % locals())
389             if ok.strip().lower() != 'y':
390                 return 0
392             # clear it out so the install isn't confused
393             shutil.rmtree(tracker_home)
395         # select template
396         templates = self.listTemplates()
397         template = len(args) > 1 and args[1] or ''
398         if not templates.has_key(template):
399             print _('Templates:'), ', '.join(templates.keys())
400         while not templates.has_key(template):
401             template = raw_input(_('Select template [classic]: ')).strip()
402             if not template:
403                 template = 'classic'
405         # select hyperdb backend
406         import roundup.backends
407         backends = roundup.backends.list_backends()
408         backend = len(args) > 2 and args[2] or ''
409         if backend not in backends:
410             print _('Back ends:'), ', '.join(backends)
411         while backend not in backends:
412             backend = raw_input(_('Select backend [anydbm]: ')).strip()
413             if not backend:
414                 backend = 'anydbm'
415         # XXX perform a unit test based on the user's selections
417         # Process configuration file definitions
418         if len(args) > 3:
419             try:
420                 defns = dict([item.split("=") for item in args[3].split(",")])
421             except:
422                 print _('Error in configuration settings: "%s"') % args[3]
423                 raise
424         else:
425             defns = {}
427         # install!
428         init.install(tracker_home, templates[template]['path'], settings=defns)
429         init.write_select_db(tracker_home, backend)
431         print _("""
432 ---------------------------------------------------------------------------
433  You should now edit the tracker configuration file:
434    %(config_file)s""") % {"config_file": config_ini_file}
436         # find list of options that need manual adjustments
437         # XXX config._get_unset_options() is marked as private
438         #   (leading underscore).  make it public or don't care?
439         need_set = CoreConfig(tracker_home)._get_unset_options()
440         if need_set:
441             print _(" ... at a minimum, you must set following options:")
442             for section, options in need_set.items():
443                 print "   [%s]: %s" % (section, ", ".join(options))
445         # note about schema modifications
446         print _("""
447  If you wish to modify the database schema,
448  you should also edit the schema file:
449    %(database_config_file)s
450  You may also change the database initialisation file:
451    %(database_init_file)s
452  ... see the documentation on customizing for more information.
454  You MUST run the "roundup-admin initialise" command once you've performed
455  the above steps.
456 ---------------------------------------------------------------------------
457 """) % {
458     'database_config_file': os.path.join(tracker_home, 'schema.py'),
459     'database_init_file': os.path.join(tracker_home, 'initial_data.py'),
461         return 0
463     def do_genconfig(self, args):
464         ''"""Usage: genconfig <filename>
465         Generate a new tracker config file (ini style) with default values
466         in <filename>.
467         """
468         if len(args) < 1:
469             raise UsageError, _('Not enough arguments supplied')
470         config = CoreConfig()
471         config.save(args[0])
473     def do_initialise(self, tracker_home, args):
474         ''"""Usage: initialise [adminpw]
475         Initialise a new Roundup tracker.
477         The administrator details will be set at this step.
479         Execute the tracker's initialisation function dbinit.init()
480         """
481         # password
482         if len(args) > 1:
483             adminpw = args[1]
484         else:
485             adminpw = ''
486             confirm = 'x'
487             while adminpw != confirm:
488                 adminpw = getpass.getpass(_('Admin Password: '))
489                 confirm = getpass.getpass(_('       Confirm: '))
491         # make sure the tracker home is installed
492         if not os.path.exists(tracker_home):
493             raise UsageError, _('Instance home does not exist')%locals()
494         try:
495             tracker = roundup.instance.open(tracker_home)
496         except roundup.instance.TrackerError:
497             raise UsageError, _('Instance has not been installed')%locals()
499         # is there already a database?
500         if tracker.exists():
501             ok = raw_input(_(
502 """WARNING: The database is already initialised!
503 If you re-initialise it, you will lose all the data!
504 Erase it? Y/N: """))
505             if ok.strip().lower() != 'y':
506                 return 0
508             backend = tracker.get_backend_name()
510             # nuke it
511             tracker.nuke()
513             # re-write the backend select file
514             init.write_select_db(tracker_home, backend)
516         # GO
517         tracker.init(password.Password(adminpw))
519         return 0
522     def do_get(self, args):
523         ''"""Usage: get property designator[,designator]*
524         Get the given property of one or more designator(s).
526         Retrieves the property value of the nodes specified
527         by the designators.
528         """
529         if len(args) < 2:
530             raise UsageError, _('Not enough arguments supplied')
531         propname = args[0]
532         designators = args[1].split(',')
533         l = []
534         for designator in designators:
535             # decode the node designator
536             try:
537                 classname, nodeid = hyperdb.splitDesignator(designator)
538             except hyperdb.DesignatorError, message:
539                 raise UsageError, message
541             # get the class
542             cl = self.get_class(classname)
543             try:
544                 id=[]
545                 if self.separator:
546                     if self.print_designator:
547                         # see if property is a link or multilink for
548                         # which getting a desginator make sense.
549                         # Algorithm: Get the properties of the
550                         #     current designator's class. (cl.getprops)
551                         # get the property object for the property the
552                         #     user requested (properties[propname])
553                         # verify its type (isinstance...)
554                         # raise error if not link/multilink
555                         # get class name for link/multilink property
556                         # do the get on the designators
557                         # append the new designators
558                         # print
559                         properties = cl.getprops()
560                         property = properties[propname]
561                         if not (isinstance(property, hyperdb.Multilink) or
562                           isinstance(property, hyperdb.Link)):
563                             raise UsageError, _('property %s is not of type Multilink or Link so -d flag does not apply.')%propname
564                         propclassname = self.db.getclass(property.classname).classname
565                         id = cl.get(nodeid, propname)
566                         for i in id:
567                             l.append(propclassname + i)
568                     else:
569                         id = cl.get(nodeid, propname)
570                         for i in id:
571                             l.append(i)
572                 else:
573                     if self.print_designator:
574                         properties = cl.getprops()
575                         property = properties[propname]
576                         if not (isinstance(property, hyperdb.Multilink) or
577                           isinstance(property, hyperdb.Link)):
578                             raise UsageError, _('property %s is not of type Multilink or Link so -d flag does not apply.')%propname
579                         propclassname = self.db.getclass(property.classname).classname
580                         id = cl.get(nodeid, propname)
581                         for i in id:
582                             print propclassname + i
583                     else:
584                         print cl.get(nodeid, propname)
585             except IndexError:
586                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
587             except KeyError:
588                 raise UsageError, _('no such %(classname)s property '
589                     '"%(propname)s"')%locals()
590         if self.separator:
591             print self.separator.join(l)
593         return 0
596     def do_set(self, args):
597         ''"""Usage: set items property=value property=value ...
598         Set the given properties of one or more items(s).
600         The items are specified as a class or as a comma-separated
601         list of item designators (ie "designator[,designator,...]").
603         This command sets the properties to the values for all designators
604         given. If the value is missing (ie. "property=") then the property
605         is un-set. If the property is a multilink, you specify the linked
606         ids for the multilink as comma-separated numbers (ie "1,2,3").
607         """
608         if len(args) < 2:
609             raise UsageError, _('Not enough arguments supplied')
610         from roundup import hyperdb
612         designators = args[0].split(',')
613         if len(designators) == 1:
614             designator = designators[0]
615             try:
616                 designator = hyperdb.splitDesignator(designator)
617                 designators = [designator]
618             except hyperdb.DesignatorError:
619                 cl = self.get_class(designator)
620                 designators = [(designator, x) for x in cl.list()]
621         else:
622             try:
623                 designators = [hyperdb.splitDesignator(x) for x in designators]
624             except hyperdb.DesignatorError, message:
625                 raise UsageError, message
627         # get the props from the args
628         props = self.props_from_args(args[1:])
630         # now do the set for all the nodes
631         for classname, itemid in designators:
632             cl = self.get_class(classname)
634             properties = cl.getprops()
635             for key, value in props.items():
636                 try:
637                     props[key] = hyperdb.rawToHyperdb(self.db, cl, itemid,
638                         key, value)
639                 except hyperdb.HyperdbValueError, message:
640                     raise UsageError, message
642             # try the set
643             try:
644                 apply(cl.set, (itemid, ), props)
645             except (TypeError, IndexError, ValueError), message:
646                 import traceback; traceback.print_exc()
647                 raise UsageError, message
648         self.db_uncommitted = True
649         return 0
651     def do_find(self, args):
652         ''"""Usage: find classname propname=value ...
653         Find the nodes of the given class with a given link property value.
655         Find the nodes of the given class with a given link property value.
656         The value may be either the nodeid of the linked node, or its key
657         value.
658         """
659         if len(args) < 1:
660             raise UsageError, _('Not enough arguments supplied')
661         classname = args[0]
662         # get the class
663         cl = self.get_class(classname)
665         # handle the propname=value argument
666         props = self.props_from_args(args[1:])
668         # convert the user-input value to a value used for find()
669         for propname, value in props.items():
670             if ',' in value:
671                 values = value.split(',')
672             else:
673                 values = [value]
674             d = props[propname] = {}
675             for value in values:
676                 value = hyperdb.rawToHyperdb(self.db, cl, None, propname, value)
677                 if isinstance(value, list):
678                     for entry in value:
679                         d[entry] = 1
680                 else:
681                     d[value] = 1
683         # now do the find
684         try:
685             id = []
686             designator = []
687             if self.separator:
688                 if self.print_designator:
689                     id=apply(cl.find, (), props)
690                     for i in id:
691                         designator.append(classname + i)
692                     print self.separator.join(designator)
693                 else:
694                     print self.separator.join(apply(cl.find, (), props))
696             else:
697                 if self.print_designator:
698                     id=apply(cl.find, (), props)
699                     for i in id:
700                         designator.append(classname + i)
701                     print designator
702                 else:
703                     print apply(cl.find, (), props)
704         except KeyError:
705             raise UsageError, _('%(classname)s has no property '
706                 '"%(propname)s"')%locals()
707         except (ValueError, TypeError), message:
708             raise UsageError, message
709         return 0
711     def do_specification(self, args):
712         ''"""Usage: specification classname
713         Show the properties for a classname.
715         This lists the properties for a given class.
716         """
717         if len(args) < 1:
718             raise UsageError, _('Not enough arguments supplied')
719         classname = args[0]
720         # get the class
721         cl = self.get_class(classname)
723         # get the key property
724         keyprop = cl.getkey()
725         for key, value in cl.properties.items():
726             if keyprop == key:
727                 print _('%(key)s: %(value)s (key property)')%locals()
728             else:
729                 print _('%(key)s: %(value)s')%locals()
731     def do_display(self, args):
732         ''"""Usage: display designator[,designator]*
733         Show the property values for the given node(s).
735         This lists the properties and their associated values for the given
736         node.
737         """
738         if len(args) < 1:
739             raise UsageError, _('Not enough arguments supplied')
741         # decode the node designator
742         for designator in args[0].split(','):
743             try:
744                 classname, nodeid = hyperdb.splitDesignator(designator)
745             except hyperdb.DesignatorError, message:
746                 raise UsageError, message
748             # get the class
749             cl = self.get_class(classname)
751             # display the values
752             keys = cl.properties.keys()
753             keys.sort()
754             for key in keys:
755                 value = cl.get(nodeid, key)
756                 print _('%(key)s: %(value)s')%locals()
758     def do_create(self, args):
759         ''"""Usage: create classname property=value ...
760         Create a new entry of a given class.
762         This creates a new entry of the given class using the property
763         name=value arguments provided on the command line after the "create"
764         command.
765         """
766         if len(args) < 1:
767             raise UsageError, _('Not enough arguments supplied')
768         from roundup import hyperdb
770         classname = args[0]
772         # get the class
773         cl = self.get_class(classname)
775         # now do a create
776         props = {}
777         properties = cl.getprops(protected = 0)
778         if len(args) == 1:
779             # ask for the properties
780             for key, value in properties.items():
781                 if key == 'id': continue
782                 name = value.__class__.__name__
783                 if isinstance(value , hyperdb.Password):
784                     again = None
785                     while value != again:
786                         value = getpass.getpass(_('%(propname)s (Password): ')%{
787                             'propname': key.capitalize()})
788                         again = getpass.getpass(_('   %(propname)s (Again): ')%{
789                             'propname': key.capitalize()})
790                         if value != again: print _('Sorry, try again...')
791                     if value:
792                         props[key] = value
793                 else:
794                     value = raw_input(_('%(propname)s (%(proptype)s): ')%{
795                         'propname': key.capitalize(), 'proptype': name})
796                     if value:
797                         props[key] = value
798         else:
799             props = self.props_from_args(args[1:])
801         # convert types
802         for propname, value in props.items():
803             try:
804                 props[propname] = hyperdb.rawToHyperdb(self.db, cl, None,
805                     propname, value)
806             except hyperdb.HyperdbValueError, message:
807                 raise UsageError, message
809         # check for the key property
810         propname = cl.getkey()
811         if propname and not props.has_key(propname):
812             raise UsageError, _('you must provide the "%(propname)s" '
813                 'property.')%locals()
815         # do the actual create
816         try:
817             print apply(cl.create, (), props)
818         except (TypeError, IndexError, ValueError), message:
819             raise UsageError, message
820         self.db_uncommitted = True
821         return 0
823     def do_list(self, args):
824         ''"""Usage: list classname [property]
825         List the instances of a class.
827         Lists all instances of the given class. If the property is not
828         specified, the  "label" property is used. The label property is
829         tried in order: the key, "name", "title" and then the first
830         property, alphabetically.
832         With -c, -S or -s print a list of item id's if no property
833         specified.  If property specified, print list of that property
834         for every class instance.
835         """
836         if len(args) > 2:
837             raise UsageError, _('Too many arguments supplied')
838         if len(args) < 1:
839             raise UsageError, _('Not enough arguments supplied')
840         classname = args[0]
841         
842         # get the class
843         cl = self.get_class(classname)
845         # figure the property
846         if len(args) > 1:
847             propname = args[1]
848         else:
849             propname = cl.labelprop()
851         if self.separator:
852             if len(args) == 2:
853                 # create a list of propnames since user specified propname
854                 proplist=[]
855                 for nodeid in cl.list():
856                     try:
857                         proplist.append(cl.get(nodeid, propname))
858                     except KeyError:
859                         raise UsageError, _('%(classname)s has no property '
860                             '"%(propname)s"')%locals()
861                 print self.separator.join(proplist)
862             else:
863                 # create a list of index id's since user didn't specify
864                 # otherwise
865                 print self.separator.join(cl.list())
866         else:
867             for nodeid in cl.list():
868                 try:
869                     value = cl.get(nodeid, propname)
870                 except KeyError:
871                     raise UsageError, _('%(classname)s has no property '
872                         '"%(propname)s"')%locals()
873                 print _('%(nodeid)4s: %(value)s')%locals()
874         return 0
876     def do_table(self, args):
877         ''"""Usage: table classname [property[,property]*]
878         List the instances of a class in tabular form.
880         Lists all instances of the given class. If the properties are not
881         specified, all properties are displayed. By default, the column
882         widths are the width of the largest value. The width may be
883         explicitly defined by defining the property as "name:width".
884         For example::
886           roundup> table priority id,name:10
887           Id Name
888           1  fatal-bug
889           2  bug
890           3  usability
891           4  feature
893         Also to make the width of the column the width of the label,
894         leave a trailing : without a width on the property. For example::
896           roundup> table priority id,name:
897           Id Name
898           1  fata
899           2  bug
900           3  usab
901           4  feat
903         will result in a the 4 character wide "Name" column.
904         """
905         if len(args) < 1:
906             raise UsageError, _('Not enough arguments supplied')
907         classname = args[0]
909         # get the class
910         cl = self.get_class(classname)
912         # figure the property names to display
913         if len(args) > 1:
914             prop_names = args[1].split(',')
915             all_props = cl.getprops()
916             for spec in prop_names:
917                 if ':' in spec:
918                     try:
919                         propname, width = spec.split(':')
920                     except (ValueError, TypeError):
921                         raise UsageError, _('"%(spec)s" not name:width')%locals()
922                 else:
923                     propname = spec
924                 if not all_props.has_key(propname):
925                     raise UsageError, _('%(classname)s has no property '
926                         '"%(propname)s"')%locals()
927         else:
928             prop_names = cl.getprops().keys()
930         # now figure column widths
931         props = []
932         for spec in prop_names:
933             if ':' in spec:
934                 name, width = spec.split(':')
935                 if width == '':
936                     props.append((name, len(spec)))
937                 else:
938                     props.append((name, int(width)))
939             else:
940                # this is going to be slow
941                maxlen = len(spec)
942                for nodeid in cl.list():
943                    curlen = len(str(cl.get(nodeid, spec)))
944                    if curlen > maxlen:
945                        maxlen = curlen
946                props.append((spec, maxlen))
948         # now display the heading
949         print ' '.join([name.capitalize().ljust(width) for name,width in props])
951         # and the table data
952         for nodeid in cl.list():
953             l = []
954             for name, width in props:
955                 if name != 'id':
956                     try:
957                         value = str(cl.get(nodeid, name))
958                     except KeyError:
959                         # we already checked if the property is valid - a
960                         # KeyError here means the node just doesn't have a
961                         # value for it
962                         value = ''
963                 else:
964                     value = str(nodeid)
965                 f = '%%-%ds'%width
966                 l.append(f%value[:width])
967             print ' '.join(l)
968         return 0
970     def do_history(self, args):
971         ''"""Usage: history designator
972         Show the history entries of a designator.
974         Lists the journal entries for the node identified by the designator.
975         """
976         if len(args) < 1:
977             raise UsageError, _('Not enough arguments supplied')
978         try:
979             classname, nodeid = hyperdb.splitDesignator(args[0])
980         except hyperdb.DesignatorError, message:
981             raise UsageError, message
983         try:
984             print self.db.getclass(classname).history(nodeid)
985         except KeyError:
986             raise UsageError, _('no such class "%(classname)s"')%locals()
987         except IndexError:
988             raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
989         return 0
991     def do_commit(self, args):
992         ''"""Usage: commit
993         Commit changes made to the database during an interactive session.
995         The changes made during an interactive session are not
996         automatically written to the database - they must be committed
997         using this command.
999         One-off commands on the command-line are automatically committed if
1000         they are successful.
1001         """
1002         self.db.commit()
1003         self.db_uncommitted = False
1004         return 0
1006     def do_rollback(self, args):
1007         ''"""Usage: rollback
1008         Undo all changes that are pending commit to the database.
1010         The changes made during an interactive session are not
1011         automatically written to the database - they must be committed
1012         manually. This command undoes all those changes, so a commit
1013         immediately after would make no changes to the database.
1014         """
1015         self.db.rollback()
1016         self.db_uncommitted = False
1017         return 0
1019     def do_retire(self, args):
1020         ''"""Usage: retire designator[,designator]*
1021         Retire the node specified by designator.
1023         This action indicates that a particular node is not to be retrieved
1024         by the list or find commands, and its key value may be re-used.
1025         """
1026         if len(args) < 1:
1027             raise UsageError, _('Not enough arguments supplied')
1028         designators = args[0].split(',')
1029         for designator in designators:
1030             try:
1031                 classname, nodeid = hyperdb.splitDesignator(designator)
1032             except hyperdb.DesignatorError, message:
1033                 raise UsageError, message
1034             try:
1035                 self.db.getclass(classname).retire(nodeid)
1036             except KeyError:
1037                 raise UsageError, _('no such class "%(classname)s"')%locals()
1038             except IndexError:
1039                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
1040         self.db_uncommitted = True
1041         return 0
1043     def do_restore(self, args):
1044         ''"""Usage: restore designator[,designator]*
1045         Restore the retired node specified by designator.
1047         The given nodes will become available for users again.
1048         """
1049         if len(args) < 1:
1050             raise UsageError, _('Not enough arguments supplied')
1051         designators = args[0].split(',')
1052         for designator in designators:
1053             try:
1054                 classname, nodeid = hyperdb.splitDesignator(designator)
1055             except hyperdb.DesignatorError, message:
1056                 raise UsageError, message
1057             try:
1058                 self.db.getclass(classname).restore(nodeid)
1059             except KeyError:
1060                 raise UsageError, _('no such class "%(classname)s"')%locals()
1061             except IndexError:
1062                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
1063         self.db_uncommitted = True
1064         return 0
1066     def do_export(self, args, export_files=True):
1067         ''"""Usage: export [[-]class[,class]] export_dir
1068         Export the database to colon-separated-value files.
1069         To exclude the files (e.g. for the msg or file class),
1070         use the exporttables command.
1072         Optionally limit the export to just the named classes
1073         or exclude the named classes, if the 1st argument starts with '-'.
1075         This action exports the current data from the database into
1076         colon-separated-value files that are placed in the nominated
1077         destination directory.
1078         """
1079         # grab the directory to export to
1080         if len(args) < 1:
1081             raise UsageError, _('Not enough arguments supplied')
1083         dir = args[-1]
1085         # get the list of classes to export
1086         if len(args) == 2:
1087             if args[0].startswith('-'):
1088                 classes = [ c for c in self.db.classes.keys()
1089                             if not c in args[0][1:].split(',') ]
1090             else:
1091                 classes = args[0].split(',')
1092         else:
1093             classes = self.db.classes.keys()
1095         class colon_separated(csv.excel):
1096             delimiter = ':'
1098         # make sure target dir exists
1099         if not os.path.exists(dir):
1100             os.makedirs(dir)
1102         # maximum csv field length exceeding configured size?
1103         max_len = self.db.config.CSV_FIELD_SIZE
1105         # do all the classes specified
1106         for classname in classes:
1107             cl = self.get_class(classname)
1109             if not export_files and hasattr(cl, 'export_files'):
1110                 sys.stdout.write('Exporting %s WITHOUT the files\r\n'%
1111                     classname)
1113             f = open(os.path.join(dir, classname+'.csv'), 'wb')
1114             writer = csv.writer(f, colon_separated)
1116             properties = cl.getprops()
1117             propnames = cl.export_propnames()
1118             fields = propnames[:]
1119             fields.append('is retired')
1120             writer.writerow(fields)
1122             # all nodes for this class
1123             for nodeid in cl.getnodeids():
1124                 if self.verbose:
1125                     sys.stdout.write('\rExporting %s - %s'%(classname, nodeid))
1126                     sys.stdout.flush()
1127                 node = cl.getnode(nodeid)
1128                 exp = cl.export_list(propnames, nodeid)
1129                 lensum = sum (len (repr(node[p])) for p in propnames)
1130                 # for a safe upper bound of field length we add
1131                 # difference between CSV len and sum of all field lengths
1132                 d = sum (len(x) for x in exp) - lensum
1133                 assert (d > 0)
1134                 for p in propnames:
1135                     ll = len(repr(node[p])) + d
1136                     if ll > max_len:
1137                         max_len = ll
1138                 writer.writerow(exp)
1139                 if export_files and hasattr(cl, 'export_files'):
1140                     cl.export_files(dir, nodeid)
1142             # close this file
1143             f.close()
1145             # export the journals
1146             jf = open(os.path.join(dir, classname+'-journals.csv'), 'wb')
1147             if self.verbose:
1148                 sys.stdout.write("\nExporting Journal for %s\n" % classname)
1149                 sys.stdout.flush()
1150             journals = csv.writer(jf, colon_separated)
1151             map(journals.writerow, cl.export_journals())
1152             jf.close()
1153         if max_len > self.db.config.CSV_FIELD_SIZE:
1154             print >> sys.stderr, \
1155                 "Warning: config csv_field_size should be at least %s"%max_len
1156         return 0
1158     def do_exporttables(self, args):
1159         ''"""Usage: exporttables [[-]class[,class]] export_dir
1160         Export the database to colon-separated-value files, excluding the
1161         files below $TRACKER_HOME/db/files/ (which can be archived separately).
1162         To include the files, use the export command.
1164         Optionally limit the export to just the named classes
1165         or exclude the named classes, if the 1st argument starts with '-'.
1167         This action exports the current data from the database into
1168         colon-separated-value files that are placed in the nominated
1169         destination directory.
1170         """
1171         return self.do_export(args, export_files=False)
1173     def do_import(self, args):
1174         ''"""Usage: import import_dir
1175         Import a database from the directory containing CSV files,
1176         two per class to import.
1178         The files used in the import are:
1180         <class>.csv
1181           This must define the same properties as the class (including
1182           having a "header" line with those property names.)
1183         <class>-journals.csv
1184           This defines the journals for the items being imported.
1186         The imported nodes will have the same nodeid as defined in the
1187         import file, thus replacing any existing content.
1189         The new nodes are added to the existing database - if you want to
1190         create a new database using the imported data, then create a new
1191         database (or, tediously, retire all the old data.)
1192         """
1193         if len(args) < 1:
1194             raise UsageError, _('Not enough arguments supplied')
1195         from roundup import hyperdb
1197         if hasattr (csv, 'field_size_limit'):
1198             csv.field_size_limit(self.db.config.CSV_FIELD_SIZE)
1200         # directory to import from
1201         dir = args[0]
1203         class colon_separated(csv.excel):
1204             delimiter = ':'
1206         # import all the files
1207         for file in os.listdir(dir):
1208             classname, ext = os.path.splitext(file)
1209             # we only care about CSV files
1210             if ext != '.csv' or classname.endswith('-journals'):
1211                 continue
1213             cl = self.get_class(classname)
1215             # ensure that the properties and the CSV file headings match
1216             f = open(os.path.join(dir, file), 'r')
1217             reader = csv.reader(f, colon_separated)
1218             file_props = None
1219             maxid = 1
1220             # loop through the file and create a node for each entry
1221             for n, r in enumerate(reader):
1222                 if file_props is None:
1223                     file_props = r
1224                     continue
1226                 if self.verbose:
1227                     sys.stdout.write('\rImporting %s - %s'%(classname, n))
1228                     sys.stdout.flush()
1230                 # do the import and figure the current highest nodeid
1231                 nodeid = cl.import_list(file_props, r)
1232                 if hasattr(cl, 'import_files'):
1233                     cl.import_files(dir, nodeid)
1234                 maxid = max(maxid, int(nodeid))
1235             print >> sys.stdout
1236             f.close()
1238             # import the journals
1239             f = open(os.path.join(args[0], classname + '-journals.csv'), 'r')
1240             reader = csv.reader(f, colon_separated)
1241             cl.import_journals(reader)
1242             f.close()
1244             # set the id counter
1245             print >> sys.stdout, 'setting', classname, maxid+1
1246             self.db.setid(classname, str(maxid+1))
1248         self.db_uncommitted = True
1249         return 0
1251     def do_pack(self, args):
1252         ''"""Usage: pack period | date
1254         Remove journal entries older than a period of time specified or
1255         before a certain date.
1257         A period is specified using the suffixes "y", "m", and "d". The
1258         suffix "w" (for "week") means 7 days.
1260               "3y" means three years
1261               "2y 1m" means two years and one month
1262               "1m 25d" means one month and 25 days
1263               "2w 3d" means two weeks and three days
1265         Date format is "YYYY-MM-DD" eg:
1266             2001-01-01
1268         """
1269         if len(args) <> 1:
1270             raise UsageError, _('Not enough arguments supplied')
1272         # are we dealing with a period or a date
1273         value = args[0]
1274         date_re = re.compile(r"""
1275               (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
1276               (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
1277               """, re.VERBOSE)
1278         m = date_re.match(value)
1279         if not m:
1280             raise ValueError, _('Invalid format')
1281         m = m.groupdict()
1282         if m['period']:
1283             pack_before = date.Date(". - %s"%value)
1284         elif m['date']:
1285             pack_before = date.Date(value)
1286         self.db.pack(pack_before)
1287         self.db_uncommitted = True
1288         return 0
1290     def do_reindex(self, args, desre=re.compile('([A-Za-z]+)([0-9]+)')):
1291         ''"""Usage: reindex [classname|designator]*
1292         Re-generate a tracker's search indexes.
1294         This will re-generate the search indexes for a tracker.
1295         This will typically happen automatically.
1296         """
1297         if args:
1298             for arg in args:
1299                 m = desre.match(arg)
1300                 if m:
1301                     cl = self.get_class(m.group(1))
1302                     try:
1303                         cl.index(m.group(2))
1304                     except IndexError:
1305                         raise UsageError, _('no such item "%(designator)s"')%{
1306                             'designator': arg}
1307                 else:
1308                     cl = self.get_class(arg)
1309                     self.db.reindex(arg)
1310         else:
1311             self.db.reindex(show_progress=True)
1312         return 0
1314     def do_security(self, args):
1315         ''"""Usage: security [Role name]
1316         Display the Permissions available to one or all Roles.
1317         """
1318         if len(args) == 1:
1319             role = args[0]
1320             try:
1321                 roles = [(args[0], self.db.security.role[args[0]])]
1322             except KeyError:
1323                 print _('No such Role "%(role)s"')%locals()
1324                 return 1
1325         else:
1326             roles = self.db.security.role.items()
1327             role = self.db.config.NEW_WEB_USER_ROLES
1328             if ',' in role:
1329                 print _('New Web users get the Roles "%(role)s"')%locals()
1330             else:
1331                 print _('New Web users get the Role "%(role)s"')%locals()
1332             role = self.db.config.NEW_EMAIL_USER_ROLES
1333             if ',' in role:
1334                 print _('New Email users get the Roles "%(role)s"')%locals()
1335             else:
1336                 print _('New Email users get the Role "%(role)s"')%locals()
1337         roles.sort()
1338         for rolename, role in roles:
1339             print _('Role "%(name)s":')%role.__dict__
1340             for permission in role.permissions:
1341                 d = permission.__dict__
1342                 if permission.klass:
1343                     if permission.properties:
1344                         print _(' %(description)s (%(name)s for "%(klass)s"'
1345                           ': %(properties)s only)')%d
1346                     else:
1347                         print _(' %(description)s (%(name)s for "%(klass)s" '
1348                             'only)')%d
1349                 else:
1350                     print _(' %(description)s (%(name)s)')%d
1351         return 0
1354     def do_migrate(self, args):
1355         ''"""Usage: migrate
1356         Update a tracker's database to be compatible with the Roundup
1357         codebase.
1359         You should run the "migrate" command for your tracker once you've
1360         installed the latest codebase. 
1362         Do this before you use the web, command-line or mail interface and
1363         before any users access the tracker.
1365         This command will respond with either "Tracker updated" (if you've
1366         not previously run it on an RDBMS backend) or "No migration action
1367         required" (if you have run it, or have used another interface to the
1368         tracker, or possibly because you are using anydbm).
1370         It's safe to run this even if it's not required, so just get into
1371         the habit.
1372         """
1373         if getattr(self.db, 'db_version_updated'):
1374             print _('Tracker updated')
1375             self.db_uncommitted = True
1376         else:
1377             print _('No migration action required')
1378         return 0
1380     def run_command(self, args):
1381         """Run a single command
1382         """
1383         command = args[0]
1385         # handle help now
1386         if command == 'help':
1387             if len(args)>1:
1388                 self.do_help(args[1:])
1389                 return 0
1390             self.do_help(['help'])
1391             return 0
1392         if command == 'morehelp':
1393             self.do_help(['help'])
1394             self.help_commands()
1395             self.help_all()
1396             return 0
1397         if command == 'config':
1398             self.do_config(args[1:])
1399             return 0
1401         # figure what the command is
1402         try:
1403             functions = self.commands.get(command)
1404         except KeyError:
1405             # not a valid command
1406             print _('Unknown command "%(command)s" ("help commands" for a '
1407                 'list)')%locals()
1408             return 1
1410         # check for multiple matches
1411         if len(functions) > 1:
1412             print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1413                 command, 'list': ', '.join([i[0] for i in functions])}
1414             return 1
1415         command, function = functions[0]
1417         # make sure we have a tracker_home
1418         while not self.tracker_home:
1419             self.tracker_home = raw_input(_('Enter tracker home: ')).strip()
1421         # before we open the db, we may be doing an install or init
1422         if command == 'initialise':
1423             try:
1424                 return self.do_initialise(self.tracker_home, args)
1425             except UsageError, message:
1426                 print _('Error: %(message)s')%locals()
1427                 return 1
1428         elif command == 'install':
1429             try:
1430                 return self.do_install(self.tracker_home, args)
1431             except UsageError, message:
1432                 print _('Error: %(message)s')%locals()
1433                 return 1
1435         # get the tracker
1436         try:
1437             tracker = roundup.instance.open(self.tracker_home)
1438         except ValueError, message:
1439             self.tracker_home = ''
1440             print _("Error: Couldn't open tracker: %(message)s")%locals()
1441             return 1
1443         # only open the database once!
1444         if not self.db:
1445             self.db = tracker.open('admin')
1447         # do the command
1448         ret = 0
1449         try:
1450             ret = function(args[1:])
1451         except UsageError, message:
1452             print _('Error: %(message)s')%locals()
1453             print
1454             print function.__doc__
1455             ret = 1
1456         except:
1457             import traceback
1458             traceback.print_exc()
1459             ret = 1
1460         return ret
1462     def interactive(self):
1463         """Run in an interactive mode
1464         """
1465         print _('Roundup %s ready for input.\nType "help" for help.'
1466             % roundup_version)
1467         try:
1468             import readline
1469         except ImportError:
1470             print _('Note: command history and editing not available')
1472         while 1:
1473             try:
1474                 command = raw_input(_('roundup> '))
1475             except EOFError:
1476                 print _('exit...')
1477                 break
1478             if not command: continue
1479             args = token.token_split(command)
1480             if not args: continue
1481             if args[0] in ('quit', 'exit'): break
1482             self.run_command(args)
1484         # exit.. check for transactions
1485         if self.db and self.db_uncommitted:
1486             commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1487             if commit and commit[0].lower() == 'y':
1488                 self.db.commit()
1489         return 0
1491     def main(self):
1492         try:
1493             opts, args = getopt.getopt(sys.argv[1:], 'i:u:hcdsS:vV')
1494         except getopt.GetoptError, e:
1495             self.usage(str(e))
1496             return 1
1498         # handle command-line args
1499         self.tracker_home = os.environ.get('TRACKER_HOME', '')
1500         # TODO: reinstate the user/password stuff (-u arg too)
1501         name = password = ''
1502         if os.environ.has_key('ROUNDUP_LOGIN'):
1503             l = os.environ['ROUNDUP_LOGIN'].split(':')
1504             name = l[0]
1505             if len(l) > 1:
1506                 password = l[1]
1507         self.separator = None
1508         self.print_designator = 0
1509         self.verbose = 0
1510         for opt, arg in opts:
1511             if opt == '-h':
1512                 self.usage()
1513                 return 0
1514             elif opt == '-v':
1515                 print '%s (python %s)'%(roundup_version, sys.version.split()[0])
1516                 return 0
1517             elif opt == '-V':
1518                 self.verbose = 1
1519             elif opt == '-i':
1520                 self.tracker_home = arg
1521             elif opt == '-c':
1522                 if self.separator != None:
1523                     self.usage('Only one of -c, -S and -s may be specified')
1524                     return 1
1525                 self.separator = ','
1526             elif opt == '-S':
1527                 if self.separator != None:
1528                     self.usage('Only one of -c, -S and -s may be specified')
1529                     return 1
1530                 self.separator = arg
1531             elif opt == '-s':
1532                 if self.separator != None:
1533                     self.usage('Only one of -c, -S and -s may be specified')
1534                     return 1
1535                 self.separator = ' '
1536             elif opt == '-d':
1537                 self.print_designator = 1
1539         # if no command - go interactive
1540         # wrap in a try/finally so we always close off the db
1541         ret = 0
1542         try:
1543             if not args:
1544                 self.interactive()
1545             else:
1546                 ret = self.run_command(args)
1547                 if self.db: self.db.commit()
1548             return ret
1549         finally:
1550             if self.db:
1551                 self.db.close()
1553 if __name__ == '__main__':
1554     tool = AdminTool()
1555     sys.exit(tool.main())
1557 # vim: set filetype=python sts=4 sw=4 et si :