Code

typo
[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         A designator is a classname and a nodeid concatenated,
527         eg. bug1, user10, ...
529         Retrieves the property value of the nodes specified
530         by the designators.
531         """
532         if len(args) < 2:
533             raise UsageError, _('Not enough arguments supplied')
534         propname = args[0]
535         designators = args[1].split(',')
536         l = []
537         for designator in designators:
538             # decode the node designator
539             try:
540                 classname, nodeid = hyperdb.splitDesignator(designator)
541             except hyperdb.DesignatorError, message:
542                 raise UsageError, message
544             # get the class
545             cl = self.get_class(classname)
546             try:
547                 id=[]
548                 if self.separator:
549                     if self.print_designator:
550                         # see if property is a link or multilink for
551                         # which getting a desginator make sense.
552                         # Algorithm: Get the properties of the
553                         #     current designator's class. (cl.getprops)
554                         # get the property object for the property the
555                         #     user requested (properties[propname])
556                         # verify its type (isinstance...)
557                         # raise error if not link/multilink
558                         # get class name for link/multilink property
559                         # do the get on the designators
560                         # append the new designators
561                         # print
562                         properties = cl.getprops()
563                         property = properties[propname]
564                         if not (isinstance(property, hyperdb.Multilink) or
565                           isinstance(property, hyperdb.Link)):
566                             raise UsageError, _('property %s is not of type Multilink or Link so -d flag does not apply.')%propname
567                         propclassname = self.db.getclass(property.classname).classname
568                         id = cl.get(nodeid, propname)
569                         for i in id:
570                             l.append(propclassname + i)
571                     else:
572                         id = cl.get(nodeid, propname)
573                         for i in id:
574                             l.append(i)
575                 else:
576                     if self.print_designator:
577                         properties = cl.getprops()
578                         property = properties[propname]
579                         if not (isinstance(property, hyperdb.Multilink) or
580                           isinstance(property, hyperdb.Link)):
581                             raise UsageError, _('property %s is not of type Multilink or Link so -d flag does not apply.')%propname
582                         propclassname = self.db.getclass(property.classname).classname
583                         id = cl.get(nodeid, propname)
584                         for i in id:
585                             print propclassname + i
586                     else:
587                         print cl.get(nodeid, propname)
588             except IndexError:
589                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
590             except KeyError:
591                 raise UsageError, _('no such %(classname)s property '
592                     '"%(propname)s"')%locals()
593         if self.separator:
594             print self.separator.join(l)
596         return 0
599     def do_set(self, args):
600         ''"""Usage: set items property=value property=value ...
601         Set the given properties of one or more items(s).
603         The items are specified as a class or as a comma-separated
604         list of item designators (ie "designator[,designator,...]").
606         A designator is a classname and a nodeid concatenated,
607         eg. bug1, user10, ...
609         This command sets the properties to the values for all designators
610         given. If the value is missing (ie. "property=") then the property
611         is un-set. If the property is a multilink, you specify the linked
612         ids for the multilink as comma-separated numbers (ie "1,2,3").
613         """
614         if len(args) < 2:
615             raise UsageError, _('Not enough arguments supplied')
616         from roundup import hyperdb
618         designators = args[0].split(',')
619         if len(designators) == 1:
620             designator = designators[0]
621             try:
622                 designator = hyperdb.splitDesignator(designator)
623                 designators = [designator]
624             except hyperdb.DesignatorError:
625                 cl = self.get_class(designator)
626                 designators = [(designator, x) for x in cl.list()]
627         else:
628             try:
629                 designators = [hyperdb.splitDesignator(x) for x in designators]
630             except hyperdb.DesignatorError, message:
631                 raise UsageError, message
633         # get the props from the args
634         props = self.props_from_args(args[1:])
636         # now do the set for all the nodes
637         for classname, itemid in designators:
638             cl = self.get_class(classname)
640             properties = cl.getprops()
641             for key, value in props.items():
642                 try:
643                     props[key] = hyperdb.rawToHyperdb(self.db, cl, itemid,
644                         key, value)
645                 except hyperdb.HyperdbValueError, message:
646                     raise UsageError, message
648             # try the set
649             try:
650                 apply(cl.set, (itemid, ), props)
651             except (TypeError, IndexError, ValueError), message:
652                 import traceback; traceback.print_exc()
653                 raise UsageError, message
654         self.db_uncommitted = True
655         return 0
657     def do_find(self, args):
658         ''"""Usage: find classname propname=value ...
659         Find the nodes of the given class with a given link property value.
661         Find the nodes of the given class with a given link property value.
662         The value may be either the nodeid of the linked node, or its key
663         value.
664         """
665         if len(args) < 1:
666             raise UsageError, _('Not enough arguments supplied')
667         classname = args[0]
668         # get the class
669         cl = self.get_class(classname)
671         # handle the propname=value argument
672         props = self.props_from_args(args[1:])
674         # convert the user-input value to a value used for find()
675         for propname, value in props.items():
676             if ',' in value:
677                 values = value.split(',')
678             else:
679                 values = [value]
680             d = props[propname] = {}
681             for value in values:
682                 value = hyperdb.rawToHyperdb(self.db, cl, None, propname, value)
683                 if isinstance(value, list):
684                     for entry in value:
685                         d[entry] = 1
686                 else:
687                     d[value] = 1
689         # now do the find
690         try:
691             id = []
692             designator = []
693             if self.separator:
694                 if self.print_designator:
695                     id=apply(cl.find, (), props)
696                     for i in id:
697                         designator.append(classname + i)
698                     print self.separator.join(designator)
699                 else:
700                     print self.separator.join(apply(cl.find, (), props))
702             else:
703                 if self.print_designator:
704                     id=apply(cl.find, (), props)
705                     for i in id:
706                         designator.append(classname + i)
707                     print designator
708                 else:
709                     print apply(cl.find, (), props)
710         except KeyError:
711             raise UsageError, _('%(classname)s has no property '
712                 '"%(propname)s"')%locals()
713         except (ValueError, TypeError), message:
714             raise UsageError, message
715         return 0
717     def do_specification(self, args):
718         ''"""Usage: specification classname
719         Show the properties for a classname.
721         This lists the properties for a given class.
722         """
723         if len(args) < 1:
724             raise UsageError, _('Not enough arguments supplied')
725         classname = args[0]
726         # get the class
727         cl = self.get_class(classname)
729         # get the key property
730         keyprop = cl.getkey()
731         for key, value in cl.properties.items():
732             if keyprop == key:
733                 print _('%(key)s: %(value)s (key property)')%locals()
734             else:
735                 print _('%(key)s: %(value)s')%locals()
737     def do_display(self, args):
738         ''"""Usage: display designator[,designator]*
739         Show the property values for the given node(s).
741         A designator is a classname and a nodeid concatenated,
742         eg. bug1, user10, ...
744         This lists the properties and their associated values for the given
745         node.
746         """
747         if len(args) < 1:
748             raise UsageError, _('Not enough arguments supplied')
750         # decode the node designator
751         for designator in args[0].split(','):
752             try:
753                 classname, nodeid = hyperdb.splitDesignator(designator)
754             except hyperdb.DesignatorError, message:
755                 raise UsageError, message
757             # get the class
758             cl = self.get_class(classname)
760             # display the values
761             keys = cl.properties.keys()
762             keys.sort()
763             for key in keys:
764                 value = cl.get(nodeid, key)
765                 print _('%(key)s: %(value)s')%locals()
767     def do_create(self, args):
768         ''"""Usage: create classname property=value ...
769         Create a new entry of a given class.
771         This creates a new entry of the given class using the property
772         name=value arguments provided on the command line after the "create"
773         command.
774         """
775         if len(args) < 1:
776             raise UsageError, _('Not enough arguments supplied')
777         from roundup import hyperdb
779         classname = args[0]
781         # get the class
782         cl = self.get_class(classname)
784         # now do a create
785         props = {}
786         properties = cl.getprops(protected = 0)
787         if len(args) == 1:
788             # ask for the properties
789             for key, value in properties.items():
790                 if key == 'id': continue
791                 name = value.__class__.__name__
792                 if isinstance(value , hyperdb.Password):
793                     again = None
794                     while value != again:
795                         value = getpass.getpass(_('%(propname)s (Password): ')%{
796                             'propname': key.capitalize()})
797                         again = getpass.getpass(_('   %(propname)s (Again): ')%{
798                             'propname': key.capitalize()})
799                         if value != again: print _('Sorry, try again...')
800                     if value:
801                         props[key] = value
802                 else:
803                     value = raw_input(_('%(propname)s (%(proptype)s): ')%{
804                         'propname': key.capitalize(), 'proptype': name})
805                     if value:
806                         props[key] = value
807         else:
808             props = self.props_from_args(args[1:])
810         # convert types
811         for propname, value in props.items():
812             try:
813                 props[propname] = hyperdb.rawToHyperdb(self.db, cl, None,
814                     propname, value)
815             except hyperdb.HyperdbValueError, message:
816                 raise UsageError, message
818         # check for the key property
819         propname = cl.getkey()
820         if propname and not props.has_key(propname):
821             raise UsageError, _('you must provide the "%(propname)s" '
822                 'property.')%locals()
824         # do the actual create
825         try:
826             print apply(cl.create, (), props)
827         except (TypeError, IndexError, ValueError), message:
828             raise UsageError, message
829         self.db_uncommitted = True
830         return 0
832     def do_list(self, args):
833         ''"""Usage: list classname [property]
834         List the instances of a class.
836         Lists all instances of the given class. If the property is not
837         specified, the  "label" property is used. The label property is
838         tried in order: the key, "name", "title" and then the first
839         property, alphabetically.
841         With -c, -S or -s print a list of item id's if no property
842         specified.  If property specified, print list of that property
843         for every class instance.
844         """
845         if len(args) > 2:
846             raise UsageError, _('Too many arguments supplied')
847         if len(args) < 1:
848             raise UsageError, _('Not enough arguments supplied')
849         classname = args[0]
850         
851         # get the class
852         cl = self.get_class(classname)
854         # figure the property
855         if len(args) > 1:
856             propname = args[1]
857         else:
858             propname = cl.labelprop()
860         if self.separator:
861             if len(args) == 2:
862                 # create a list of propnames since user specified propname
863                 proplist=[]
864                 for nodeid in cl.list():
865                     try:
866                         proplist.append(cl.get(nodeid, propname))
867                     except KeyError:
868                         raise UsageError, _('%(classname)s has no property '
869                             '"%(propname)s"')%locals()
870                 print self.separator.join(proplist)
871             else:
872                 # create a list of index id's since user didn't specify
873                 # otherwise
874                 print self.separator.join(cl.list())
875         else:
876             for nodeid in cl.list():
877                 try:
878                     value = cl.get(nodeid, propname)
879                 except KeyError:
880                     raise UsageError, _('%(classname)s has no property '
881                         '"%(propname)s"')%locals()
882                 print _('%(nodeid)4s: %(value)s')%locals()
883         return 0
885     def do_table(self, args):
886         ''"""Usage: table classname [property[,property]*]
887         List the instances of a class in tabular form.
889         Lists all instances of the given class. If the properties are not
890         specified, all properties are displayed. By default, the column
891         widths are the width of the largest value. The width may be
892         explicitly defined by defining the property as "name:width".
893         For example::
895           roundup> table priority id,name:10
896           Id Name
897           1  fatal-bug
898           2  bug
899           3  usability
900           4  feature
902         Also to make the width of the column the width of the label,
903         leave a trailing : without a width on the property. For example::
905           roundup> table priority id,name:
906           Id Name
907           1  fata
908           2  bug
909           3  usab
910           4  feat
912         will result in a the 4 character wide "Name" column.
913         """
914         if len(args) < 1:
915             raise UsageError, _('Not enough arguments supplied')
916         classname = args[0]
918         # get the class
919         cl = self.get_class(classname)
921         # figure the property names to display
922         if len(args) > 1:
923             prop_names = args[1].split(',')
924             all_props = cl.getprops()
925             for spec in prop_names:
926                 if ':' in spec:
927                     try:
928                         propname, width = spec.split(':')
929                     except (ValueError, TypeError):
930                         raise UsageError, _('"%(spec)s" not name:width')%locals()
931                 else:
932                     propname = spec
933                 if not all_props.has_key(propname):
934                     raise UsageError, _('%(classname)s has no property '
935                         '"%(propname)s"')%locals()
936         else:
937             prop_names = cl.getprops().keys()
939         # now figure column widths
940         props = []
941         for spec in prop_names:
942             if ':' in spec:
943                 name, width = spec.split(':')
944                 if width == '':
945                     props.append((name, len(spec)))
946                 else:
947                     props.append((name, int(width)))
948             else:
949                # this is going to be slow
950                maxlen = len(spec)
951                for nodeid in cl.list():
952                    curlen = len(str(cl.get(nodeid, spec)))
953                    if curlen > maxlen:
954                        maxlen = curlen
955                props.append((spec, maxlen))
957         # now display the heading
958         print ' '.join([name.capitalize().ljust(width) for name,width in props])
960         # and the table data
961         for nodeid in cl.list():
962             l = []
963             for name, width in props:
964                 if name != 'id':
965                     try:
966                         value = str(cl.get(nodeid, name))
967                     except KeyError:
968                         # we already checked if the property is valid - a
969                         # KeyError here means the node just doesn't have a
970                         # value for it
971                         value = ''
972                 else:
973                     value = str(nodeid)
974                 f = '%%-%ds'%width
975                 l.append(f%value[:width])
976             print ' '.join(l)
977         return 0
979     def do_history(self, args):
980         ''"""Usage: history designator
981         Show the history entries of a designator.
983         A designator is a classname and a nodeid concatenated,
984         eg. bug1, user10, ...
986         Lists the journal entries for the node identified by the designator.
987         """
988         if len(args) < 1:
989             raise UsageError, _('Not enough arguments supplied')
990         try:
991             classname, nodeid = hyperdb.splitDesignator(args[0])
992         except hyperdb.DesignatorError, message:
993             raise UsageError, message
995         try:
996             print self.db.getclass(classname).history(nodeid)
997         except KeyError:
998             raise UsageError, _('no such class "%(classname)s"')%locals()
999         except IndexError:
1000             raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
1001         return 0
1003     def do_commit(self, args):
1004         ''"""Usage: commit
1005         Commit changes made to the database during an interactive session.
1007         The changes made during an interactive session are not
1008         automatically written to the database - they must be committed
1009         using this command.
1011         One-off commands on the command-line are automatically committed if
1012         they are successful.
1013         """
1014         self.db.commit()
1015         self.db_uncommitted = False
1016         return 0
1018     def do_rollback(self, args):
1019         ''"""Usage: rollback
1020         Undo all changes that are pending commit to the database.
1022         The changes made during an interactive session are not
1023         automatically written to the database - they must be committed
1024         manually. This command undoes all those changes, so a commit
1025         immediately after would make no changes to the database.
1026         """
1027         self.db.rollback()
1028         self.db_uncommitted = False
1029         return 0
1031     def do_retire(self, args):
1032         ''"""Usage: retire designator[,designator]*
1033         Retire the node specified by designator.
1035         A designator is a classname and a nodeid concatenated,
1036         eg. bug1, user10, ...
1038         This action indicates that a particular node is not to be retrieved
1039         by the list or find commands, and its key value may be re-used.
1040         """
1041         if len(args) < 1:
1042             raise UsageError, _('Not enough arguments supplied')
1043         designators = args[0].split(',')
1044         for designator in designators:
1045             try:
1046                 classname, nodeid = hyperdb.splitDesignator(designator)
1047             except hyperdb.DesignatorError, message:
1048                 raise UsageError, message
1049             try:
1050                 self.db.getclass(classname).retire(nodeid)
1051             except KeyError:
1052                 raise UsageError, _('no such class "%(classname)s"')%locals()
1053             except IndexError:
1054                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
1055         self.db_uncommitted = True
1056         return 0
1058     def do_restore(self, args):
1059         ''"""Usage: restore designator[,designator]*
1060         Restore the retired node specified by designator.
1062         A designator is a classname and a nodeid concatenated,
1063         eg. bug1, user10, ...
1065         The given nodes will become available for users again.
1066         """
1067         if len(args) < 1:
1068             raise UsageError, _('Not enough arguments supplied')
1069         designators = args[0].split(',')
1070         for designator in designators:
1071             try:
1072                 classname, nodeid = hyperdb.splitDesignator(designator)
1073             except hyperdb.DesignatorError, message:
1074                 raise UsageError, message
1075             try:
1076                 self.db.getclass(classname).restore(nodeid)
1077             except KeyError:
1078                 raise UsageError, _('no such class "%(classname)s"')%locals()
1079             except IndexError:
1080                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
1081         self.db_uncommitted = True
1082         return 0
1084     def do_export(self, args, export_files=True):
1085         ''"""Usage: export [[-]class[,class]] export_dir
1086         Export the database to colon-separated-value files.
1087         To exclude the files (e.g. for the msg or file class),
1088         use the exporttables command.
1090         Optionally limit the export to just the named classes
1091         or exclude the named classes, if the 1st argument starts with '-'.
1093         This action exports the current data from the database into
1094         colon-separated-value files that are placed in the nominated
1095         destination directory.
1096         """
1097         # grab the directory to export to
1098         if len(args) < 1:
1099             raise UsageError, _('Not enough arguments supplied')
1101         dir = args[-1]
1103         # get the list of classes to export
1104         if len(args) == 2:
1105             if args[0].startswith('-'):
1106                 classes = [ c for c in self.db.classes.keys()
1107                             if not c in args[0][1:].split(',') ]
1108             else:
1109                 classes = args[0].split(',')
1110         else:
1111             classes = self.db.classes.keys()
1113         class colon_separated(csv.excel):
1114             delimiter = ':'
1116         # make sure target dir exists
1117         if not os.path.exists(dir):
1118             os.makedirs(dir)
1120         # maximum csv field length exceeding configured size?
1121         max_len = self.db.config.CSV_FIELD_SIZE
1123         # do all the classes specified
1124         for classname in classes:
1125             cl = self.get_class(classname)
1127             if not export_files and hasattr(cl, 'export_files'):
1128                 sys.stdout.write('Exporting %s WITHOUT the files\r\n'%
1129                     classname)
1131             f = open(os.path.join(dir, classname+'.csv'), 'wb')
1132             writer = csv.writer(f, colon_separated)
1134             properties = cl.getprops()
1135             propnames = cl.export_propnames()
1136             fields = propnames[:]
1137             fields.append('is retired')
1138             writer.writerow(fields)
1140             # all nodes for this class
1141             for nodeid in cl.getnodeids():
1142                 if self.verbose:
1143                     sys.stdout.write('\rExporting %s - %s'%(classname, nodeid))
1144                     sys.stdout.flush()
1145                 node = cl.getnode(nodeid)
1146                 exp = cl.export_list(propnames, nodeid)
1147                 lensum = sum ([len (repr(node[p])) for p in propnames])
1148                 # for a safe upper bound of field length we add
1149                 # difference between CSV len and sum of all field lengths
1150                 d = sum ([len(x) for x in exp]) - lensum
1151                 assert (d > 0)
1152                 for p in propnames:
1153                     ll = len(repr(node[p])) + d
1154                     if ll > max_len:
1155                         max_len = ll
1156                 writer.writerow(exp)
1157                 if export_files and hasattr(cl, 'export_files'):
1158                     cl.export_files(dir, nodeid)
1160             # close this file
1161             f.close()
1163             # export the journals
1164             jf = open(os.path.join(dir, classname+'-journals.csv'), 'wb')
1165             if self.verbose:
1166                 sys.stdout.write("\nExporting Journal for %s\n" % classname)
1167                 sys.stdout.flush()
1168             journals = csv.writer(jf, colon_separated)
1169             map(journals.writerow, cl.export_journals())
1170             jf.close()
1171         if max_len > self.db.config.CSV_FIELD_SIZE:
1172             print >> sys.stderr, \
1173                 "Warning: config csv_field_size should be at least %s"%max_len
1174         return 0
1176     def do_exporttables(self, args):
1177         ''"""Usage: exporttables [[-]class[,class]] export_dir
1178         Export the database to colon-separated-value files, excluding the
1179         files below $TRACKER_HOME/db/files/ (which can be archived separately).
1180         To include the files, use the export command.
1182         Optionally limit the export to just the named classes
1183         or exclude the named classes, if the 1st argument starts with '-'.
1185         This action exports the current data from the database into
1186         colon-separated-value files that are placed in the nominated
1187         destination directory.
1188         """
1189         return self.do_export(args, export_files=False)
1191     def do_import(self, args):
1192         ''"""Usage: import import_dir
1193         Import a database from the directory containing CSV files,
1194         two per class to import.
1196         The files used in the import are:
1198         <class>.csv
1199           This must define the same properties as the class (including
1200           having a "header" line with those property names.)
1201         <class>-journals.csv
1202           This defines the journals for the items being imported.
1204         The imported nodes will have the same nodeid as defined in the
1205         import file, thus replacing any existing content.
1207         The new nodes are added to the existing database - if you want to
1208         create a new database using the imported data, then create a new
1209         database (or, tediously, retire all the old data.)
1210         """
1211         if len(args) < 1:
1212             raise UsageError, _('Not enough arguments supplied')
1213         from roundup import hyperdb
1215         if hasattr (csv, 'field_size_limit'):
1216             csv.field_size_limit(self.db.config.CSV_FIELD_SIZE)
1218         # directory to import from
1219         dir = args[0]
1221         class colon_separated(csv.excel):
1222             delimiter = ':'
1224         # import all the files
1225         for file in os.listdir(dir):
1226             classname, ext = os.path.splitext(file)
1227             # we only care about CSV files
1228             if ext != '.csv' or classname.endswith('-journals'):
1229                 continue
1231             cl = self.get_class(classname)
1233             # ensure that the properties and the CSV file headings match
1234             f = open(os.path.join(dir, file), 'r')
1235             reader = csv.reader(f, colon_separated)
1236             file_props = None
1237             maxid = 1
1238             # loop through the file and create a node for each entry
1239             for n, r in enumerate(reader):
1240                 if file_props is None:
1241                     file_props = r
1242                     continue
1244                 if self.verbose:
1245                     sys.stdout.write('\rImporting %s - %s'%(classname, n))
1246                     sys.stdout.flush()
1248                 # do the import and figure the current highest nodeid
1249                 nodeid = cl.import_list(file_props, r)
1250                 if hasattr(cl, 'import_files'):
1251                     cl.import_files(dir, nodeid)
1252                 maxid = max(maxid, int(nodeid))
1253             print >> sys.stdout
1254             f.close()
1256             # import the journals
1257             f = open(os.path.join(args[0], classname + '-journals.csv'), 'r')
1258             reader = csv.reader(f, colon_separated)
1259             cl.import_journals(reader)
1260             f.close()
1262             # set the id counter
1263             print >> sys.stdout, 'setting', classname, maxid+1
1264             self.db.setid(classname, str(maxid+1))
1266         self.db_uncommitted = True
1267         return 0
1269     def do_pack(self, args):
1270         ''"""Usage: pack period | date
1272         Remove journal entries older than a period of time specified or
1273         before a certain date.
1275         A period is specified using the suffixes "y", "m", and "d". The
1276         suffix "w" (for "week") means 7 days.
1278               "3y" means three years
1279               "2y 1m" means two years and one month
1280               "1m 25d" means one month and 25 days
1281               "2w 3d" means two weeks and three days
1283         Date format is "YYYY-MM-DD" eg:
1284             2001-01-01
1286         """
1287         if len(args) <> 1:
1288             raise UsageError, _('Not enough arguments supplied')
1290         # are we dealing with a period or a date
1291         value = args[0]
1292         date_re = re.compile(r"""
1293               (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
1294               (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
1295               """, re.VERBOSE)
1296         m = date_re.match(value)
1297         if not m:
1298             raise ValueError, _('Invalid format')
1299         m = m.groupdict()
1300         if m['period']:
1301             pack_before = date.Date(". - %s"%value)
1302         elif m['date']:
1303             pack_before = date.Date(value)
1304         self.db.pack(pack_before)
1305         self.db_uncommitted = True
1306         return 0
1308     def do_reindex(self, args, desre=re.compile('([A-Za-z]+)([0-9]+)')):
1309         ''"""Usage: reindex [classname|designator]*
1310         Re-generate a tracker's search indexes.
1312         This will re-generate the search indexes for a tracker.
1313         This will typically happen automatically.
1314         """
1315         if args:
1316             for arg in args:
1317                 m = desre.match(arg)
1318                 if m:
1319                     cl = self.get_class(m.group(1))
1320                     try:
1321                         cl.index(m.group(2))
1322                     except IndexError:
1323                         raise UsageError, _('no such item "%(designator)s"')%{
1324                             'designator': arg}
1325                 else:
1326                     cl = self.get_class(arg)
1327                     self.db.reindex(arg)
1328         else:
1329             self.db.reindex(show_progress=True)
1330         return 0
1332     def do_security(self, args):
1333         ''"""Usage: security [Role name]
1334         Display the Permissions available to one or all Roles.
1335         """
1336         if len(args) == 1:
1337             role = args[0]
1338             try:
1339                 roles = [(args[0], self.db.security.role[args[0]])]
1340             except KeyError:
1341                 print _('No such Role "%(role)s"')%locals()
1342                 return 1
1343         else:
1344             roles = self.db.security.role.items()
1345             role = self.db.config.NEW_WEB_USER_ROLES
1346             if ',' in role:
1347                 print _('New Web users get the Roles "%(role)s"')%locals()
1348             else:
1349                 print _('New Web users get the Role "%(role)s"')%locals()
1350             role = self.db.config.NEW_EMAIL_USER_ROLES
1351             if ',' in role:
1352                 print _('New Email users get the Roles "%(role)s"')%locals()
1353             else:
1354                 print _('New Email users get the Role "%(role)s"')%locals()
1355         roles.sort()
1356         for rolename, role in roles:
1357             print _('Role "%(name)s":')%role.__dict__
1358             for permission in role.permissions:
1359                 d = permission.__dict__
1360                 if permission.klass:
1361                     if permission.properties:
1362                         print _(' %(description)s (%(name)s for "%(klass)s"'
1363                           ': %(properties)s only)')%d
1364                     else:
1365                         print _(' %(description)s (%(name)s for "%(klass)s" '
1366                             'only)')%d
1367                 else:
1368                     print _(' %(description)s (%(name)s)')%d
1369         return 0
1372     def do_migrate(self, args):
1373         ''"""Usage: migrate
1374         Update a tracker's database to be compatible with the Roundup
1375         codebase.
1377         You should run the "migrate" command for your tracker once you've
1378         installed the latest codebase. 
1380         Do this before you use the web, command-line or mail interface and
1381         before any users access the tracker.
1383         This command will respond with either "Tracker updated" (if you've
1384         not previously run it on an RDBMS backend) or "No migration action
1385         required" (if you have run it, or have used another interface to the
1386         tracker, or possibly because you are using anydbm).
1388         It's safe to run this even if it's not required, so just get into
1389         the habit.
1390         """
1391         if getattr(self.db, 'db_version_updated'):
1392             print _('Tracker updated')
1393             self.db_uncommitted = True
1394         else:
1395             print _('No migration action required')
1396         return 0
1398     def run_command(self, args):
1399         """Run a single command
1400         """
1401         command = args[0]
1403         # handle help now
1404         if command == 'help':
1405             if len(args)>1:
1406                 self.do_help(args[1:])
1407                 return 0
1408             self.do_help(['help'])
1409             return 0
1410         if command == 'morehelp':
1411             self.do_help(['help'])
1412             self.help_commands()
1413             self.help_all()
1414             return 0
1415         if command == 'config':
1416             self.do_config(args[1:])
1417             return 0
1419         # figure what the command is
1420         try:
1421             functions = self.commands.get(command)
1422         except KeyError:
1423             # not a valid command
1424             print _('Unknown command "%(command)s" ("help commands" for a '
1425                 'list)')%locals()
1426             return 1
1428         # check for multiple matches
1429         if len(functions) > 1:
1430             print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1431                 command, 'list': ', '.join([i[0] for i in functions])}
1432             return 1
1433         command, function = functions[0]
1435         # make sure we have a tracker_home
1436         while not self.tracker_home:
1437             self.tracker_home = raw_input(_('Enter tracker home: ')).strip()
1439         # before we open the db, we may be doing an install or init
1440         if command == 'initialise':
1441             try:
1442                 return self.do_initialise(self.tracker_home, args)
1443             except UsageError, message:
1444                 print _('Error: %(message)s')%locals()
1445                 return 1
1446         elif command == 'install':
1447             try:
1448                 return self.do_install(self.tracker_home, args)
1449             except UsageError, message:
1450                 print _('Error: %(message)s')%locals()
1451                 return 1
1453         # get the tracker
1454         try:
1455             tracker = roundup.instance.open(self.tracker_home)
1456         except ValueError, message:
1457             self.tracker_home = ''
1458             print _("Error: Couldn't open tracker: %(message)s")%locals()
1459             return 1
1461         # only open the database once!
1462         if not self.db:
1463             self.db = tracker.open('admin')
1465         # do the command
1466         ret = 0
1467         try:
1468             ret = function(args[1:])
1469         except UsageError, message:
1470             print _('Error: %(message)s')%locals()
1471             print
1472             print function.__doc__
1473             ret = 1
1474         except:
1475             import traceback
1476             traceback.print_exc()
1477             ret = 1
1478         return ret
1480     def interactive(self):
1481         """Run in an interactive mode
1482         """
1483         print _('Roundup %s ready for input.\nType "help" for help.'
1484             % roundup_version)
1485         try:
1486             import readline
1487         except ImportError:
1488             print _('Note: command history and editing not available')
1490         while 1:
1491             try:
1492                 command = raw_input(_('roundup> '))
1493             except EOFError:
1494                 print _('exit...')
1495                 break
1496             if not command: continue
1497             args = token.token_split(command)
1498             if not args: continue
1499             if args[0] in ('quit', 'exit'): break
1500             self.run_command(args)
1502         # exit.. check for transactions
1503         if self.db and self.db_uncommitted:
1504             commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1505             if commit and commit[0].lower() == 'y':
1506                 self.db.commit()
1507         return 0
1509     def main(self):
1510         try:
1511             opts, args = getopt.getopt(sys.argv[1:], 'i:u:hcdsS:vV')
1512         except getopt.GetoptError, e:
1513             self.usage(str(e))
1514             return 1
1516         # handle command-line args
1517         self.tracker_home = os.environ.get('TRACKER_HOME', '')
1518         # TODO: reinstate the user/password stuff (-u arg too)
1519         name = password = ''
1520         if os.environ.has_key('ROUNDUP_LOGIN'):
1521             l = os.environ['ROUNDUP_LOGIN'].split(':')
1522             name = l[0]
1523             if len(l) > 1:
1524                 password = l[1]
1525         self.separator = None
1526         self.print_designator = 0
1527         self.verbose = 0
1528         for opt, arg in opts:
1529             if opt == '-h':
1530                 self.usage()
1531                 return 0
1532             elif opt == '-v':
1533                 print '%s (python %s)'%(roundup_version, sys.version.split()[0])
1534                 return 0
1535             elif opt == '-V':
1536                 self.verbose = 1
1537             elif opt == '-i':
1538                 self.tracker_home = arg
1539             elif opt == '-c':
1540                 if self.separator != None:
1541                     self.usage('Only one of -c, -S and -s may be specified')
1542                     return 1
1543                 self.separator = ','
1544             elif opt == '-S':
1545                 if self.separator != None:
1546                     self.usage('Only one of -c, -S and -s may be specified')
1547                     return 1
1548                 self.separator = arg
1549             elif opt == '-s':
1550                 if self.separator != None:
1551                     self.usage('Only one of -c, -S and -s may be specified')
1552                     return 1
1553                 self.separator = ' '
1554             elif opt == '-d':
1555                 self.print_designator = 1
1557         # if no command - go interactive
1558         # wrap in a try/finally so we always close off the db
1559         ret = 0
1560         try:
1561             if not args:
1562                 self.interactive()
1563             else:
1564                 ret = self.run_command(args)
1565                 if self.db: self.db.commit()
1566             return ret
1567         finally:
1568             if self.db:
1569                 self.db.close()
1571 if __name__ == '__main__':
1572     tool = AdminTool()
1573     sys.exit(tool.main())
1575 # vim: set filetype=python sts=4 sw=4 et si :