Code

handled some XXXs
[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
19 # $Id: admin.py,v 1.28 2002-09-10 12:44:42 richard Exp $
21 import sys, os, getpass, getopt, re, UserDict, shlex, shutil
22 try:
23     import csv
24 except ImportError:
25     csv = None
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.i18n import _
31 class CommandDict(UserDict.UserDict):
32     '''Simple dictionary that lets us do lookups using partial keys.
34     Original code submitted by Engelbert Gruber.
35     '''
36     _marker = []
37     def get(self, key, default=_marker):
38         if self.data.has_key(key):
39             return [(key, self.data[key])]
40         keylist = self.data.keys()
41         keylist.sort()
42         l = []
43         for ki in keylist:
44             if ki.startswith(key):
45                 l.append((ki, self.data[ki]))
46         if not l and default is self._marker:
47             raise KeyError, key
48         return l
50 class UsageError(ValueError):
51     pass
53 class AdminTool:
55     def __init__(self):
56         self.commands = CommandDict()
57         for k in AdminTool.__dict__.keys():
58             if k[:3] == 'do_':
59                 self.commands[k[3:]] = getattr(self, k)
60         self.help = {}
61         for k in AdminTool.__dict__.keys():
62             if k[:5] == 'help_':
63                 self.help[k[5:]] = getattr(self, k)
64         self.tracker_home = ''
65         self.db = None
67     def get_class(self, classname):
68         '''Get the class - raise an exception if it doesn't exist.
69         '''
70         try:
71             return self.db.getclass(classname)
72         except KeyError:
73             raise UsageError, _('no such class "%(classname)s"')%locals()
75     def props_from_args(self, args):
76         props = {}
77         for arg in args:
78             if arg.find('=') == -1:
79                 raise UsageError, _('argument "%(arg)s" not propname=value')%locals()
80             try:
81                 key, value = arg.split('=')
82             except ValueError:
83                 raise UsageError, _('argument "%(arg)s" not propname=value')%locals()
84             props[key] = value
85         return props
87     def usage(self, message=''):
88         if message:
89             message = _('Problem: %(message)s)\n\n')%locals()
90         print _('''%(message)sUsage: roundup-admin [options] <command> <arguments>
92 Options:
93  -i instance home  -- specify the issue tracker "home directory" to administer
94  -u                -- the user[:password] to use for commands
95  -c                -- when outputting lists of data, just comma-separate them
97 Help:
98  roundup-admin -h
99  roundup-admin help                       -- this help
100  roundup-admin help <command>             -- command-specific help
101  roundup-admin help all                   -- all available help
102 ''')%locals()
103         self.help_commands()
105     def help_commands(self):
106         print _('Commands:'),
107         commands = ['']
108         for command in self.commands.values():
109             h = command.__doc__.split('\n')[0]
110             commands.append(' '+h[7:])
111         commands.sort()
112         commands.append(_('Commands may be abbreviated as long as the abbreviation matches only one'))
113         commands.append(_('command, e.g. l == li == lis == list.'))
114         print '\n'.join(commands)
115         print
117     def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
118         commands = self.commands.values()
119         def sortfun(a, b):
120             return cmp(a.__name__, b.__name__)
121         commands.sort(sortfun)
122         for command in commands:
123             h = command.__doc__.split('\n')
124             name = command.__name__[3:]
125             usage = h[0]
126             print _('''
127 <tr><td valign=top><strong>%(name)s</strong></td>
128     <td><tt>%(usage)s</tt><p>
129 <pre>''')%locals()
130             indent = indent_re.match(h[3])
131             if indent: indent = len(indent.group(1))
132             for line in h[3:]:
133                 if indent:
134                     print line[indent:]
135                 else:
136                     print line
137             print _('</pre></td></tr>\n')
139     def help_all(self):
140         print _('''
141 All commands (except help) require a tracker specifier. This is just the path
142 to the roundup tracker you're working with. A roundup tracker is where 
143 roundup keeps the database and configuration file that defines an issue
144 tracker. It may be thought of as the issue tracker's "home directory". It may
145 be specified in the environment variable TRACKER_HOME or on the command
146 line as "-i tracker".
148 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
150 Property values are represented as strings in command arguments and in the
151 printed results:
152  . Strings are, well, strings.
153  . Date values are printed in the full date format in the local time zone, and
154    accepted in the full format or any of the partial formats explained below.
155  . Link values are printed as node designators. When given as an argument,
156    node designators and key strings are both accepted.
157  . Multilink values are printed as lists of node designators joined by commas.
158    When given as an argument, node designators and key strings are both
159    accepted; an empty string, a single node, or a list of nodes joined by
160    commas is accepted.
162 When property values must contain spaces, just surround the value with
163 quotes, either ' or ". A single space may also be backslash-quoted. If a
164 valuu must contain a quote character, it must be backslash-quoted or inside
165 quotes. Examples:
166            hello world      (2 tokens: hello, world)
167            "hello world"    (1 token: hello world)
168            "Roch'e" Compaan (2 tokens: Roch'e Compaan)
169            Roch\'e Compaan  (2 tokens: Roch'e Compaan)
170            address="1 2 3"  (1 token: address=1 2 3)
171            \\               (1 token: \)
172            \n\r\t           (1 token: a newline, carriage-return and tab)
174 When multiple nodes are specified to the roundup get or roundup set
175 commands, the specified properties are retrieved or set on all the listed
176 nodes. 
178 When multiple results are returned by the roundup get or roundup find
179 commands, they are printed one per line (default) or joined by commas (with
180 the -c) option. 
182 Where the command changes data, a login name/password is required. The
183 login may be specified as either "name" or "name:password".
184  . ROUNDUP_LOGIN environment variable
185  . the -u command-line option
186 If either the name or password is not supplied, they are obtained from the
187 command-line. 
189 Date format examples:
190   "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
191   "2000-04-17" means <Date 2000-04-17.00:00:00>
192   "01-25" means <Date yyyy-01-25.00:00:00>
193   "08-13.22:13" means <Date yyyy-08-14.03:13:00>
194   "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
195   "14:25" means <Date yyyy-mm-dd.19:25:00>
196   "8:47:11" means <Date yyyy-mm-dd.13:47:11>
197   "." means "right now"
199 Command help:
200 ''')
201         for name, command in self.commands.items():
202             print _('%s:')%name
203             print _('   '), command.__doc__
205     def do_help(self, args, nl_re=re.compile('[\r\n]'),
206             indent_re=re.compile(r'^(\s+)\S+')):
207         '''Usage: help topic
208         Give help about topic.
210         commands  -- list commands
211         <command> -- help specific to a command
212         initopts  -- init command options
213         all       -- all available help
214         '''
215         if len(args)>0:
216             topic = args[0]
217         else:
218             topic = 'help'
219  
221         # try help_ methods
222         if self.help.has_key(topic):
223             self.help[topic]()
224             return 0
226         # try command docstrings
227         try:
228             l = self.commands.get(topic)
229         except KeyError:
230             print _('Sorry, no help for "%(topic)s"')%locals()
231             return 1
233         # display the help for each match, removing the docsring indent
234         for name, help in l:
235             lines = nl_re.split(help.__doc__)
236             print lines[0]
237             indent = indent_re.match(lines[1])
238             if indent: indent = len(indent.group(1))
239             for line in lines[1:]:
240                 if indent:
241                     print line[indent:]
242                 else:
243                     print line
244         return 0
246     def help_initopts(self):
247         import roundup.templates
248         templates = roundup.templates.listTemplates()
249         print _('Templates:'), ', '.join(templates)
250         import roundup.backends
251         backends = roundup.backends.__all__
252         print _('Back ends:'), ', '.join(backends)
254     def do_install(self, tracker_home, args):
255         '''Usage: install [template [backend [admin password]]]
256         Install a new Roundup tracker.
258         The command will prompt for the tracker home directory (if not supplied
259         through TRACKER_HOME or the -i option). The template, backend and admin
260         password may be specified on the command-line as arguments, in that
261         order.
263         The initialise command must be called after this command in order
264         to initialise the tracker's database. You may edit the tracker's
265         initial database contents before running that command by editing
266         the tracker's dbinit.py module init() function.
268         See also initopts help.
269         '''
270         if len(args) < 1:
271             raise UsageError, _('Not enough arguments supplied')
273         # make sure the tracker home can be created
274         parent = os.path.split(tracker_home)[0]
275         if not os.path.exists(parent):
276             raise UsageError, _('Instance home parent directory "%(parent)s"'
277                 ' does not exist')%locals()
279         # select template
280         import roundup.templates
281         templates = roundup.templates.listTemplates()
282         template = len(args) > 1 and args[1] or ''
283         if template not in templates:
284             print _('Templates:'), ', '.join(templates)
285         while template not in templates:
286             template = raw_input(_('Select template [classic]: ')).strip()
287             if not template:
288                 template = 'classic'
290         # select hyperdb backend
291         import roundup.backends
292         backends = roundup.backends.__all__
293         backend = len(args) > 2 and args[2] or ''
294         if backend not in backends:
295             print _('Back ends:'), ', '.join(backends)
296         while backend not in backends:
297             backend = raw_input(_('Select backend [anydbm]: ')).strip()
298             if not backend:
299                 backend = 'anydbm'
301         # install!
302         init.install(tracker_home, template, backend)
304         print _('''
305  You should now edit the tracker configuration file:
306    %(config_file)s
307  ... at a minimum, you must set MAILHOST, MAIL_DOMAIN and ADMIN_EMAIL.
309  If you wish to modify the default schema, you should also edit the database
310  initialisation file:
311    %(database_config_file)s
312  ... see the documentation on customizing for more information.
313 ''')%{
314     'config_file': os.path.join(tracker_home, 'config.py'),
315     'database_config_file': os.path.join(tracker_home, 'dbinit.py')
317         return 0
320     def do_initialise(self, tracker_home, args):
321         '''Usage: initialise [adminpw]
322         Initialise a new Roundup tracker.
324         The administrator details will be set at this step.
326         Execute the tracker's initialisation function dbinit.init()
327         '''
328         # password
329         if len(args) > 1:
330             adminpw = args[1]
331         else:
332             adminpw = ''
333             confirm = 'x'
334             while adminpw != confirm:
335                 adminpw = getpass.getpass(_('Admin Password: '))
336                 confirm = getpass.getpass(_('       Confirm: '))
338         # make sure the tracker home is installed
339         if not os.path.exists(tracker_home):
340             raise UsageError, _('Instance home does not exist')%locals()
341         if not os.path.exists(os.path.join(tracker_home, 'html')):
342             raise UsageError, _('Instance has not been installed')%locals()
344         # is there already a database?
345         if os.path.exists(os.path.join(tracker_home, 'db')):
346             print _('WARNING: The database is already initialised!')
347             print _('If you re-initialise it, you will lose all the data!')
348             ok = raw_input(_('Erase it? Y/[N]: ')).strip()
349             if ok.lower() != 'y':
350                 return 0
352             # nuke it
353             shutil.rmtree(os.path.join(tracker_home, 'db'))
355         # GO
356         init.initialise(tracker_home, adminpw)
358         return 0
361     def do_get(self, args):
362         '''Usage: get property designator[,designator]*
363         Get the given property of one or more designator(s).
365         Retrieves the property value of the nodes specified by the designators.
366         '''
367         if len(args) < 2:
368             raise UsageError, _('Not enough arguments supplied')
369         propname = args[0]
370         designators = args[1].split(',')
371         l = []
372         for designator in designators:
373             # decode the node designator
374             try:
375                 classname, nodeid = hyperdb.splitDesignator(designator)
376             except hyperdb.DesignatorError, message:
377                 raise UsageError, message
379             # get the class
380             cl = self.get_class(classname)
381             try:
382                 if self.comma_sep:
383                     l.append(cl.get(nodeid, propname))
384                 else:
385                     print cl.get(nodeid, propname)
386             except IndexError:
387                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
388             except KeyError:
389                 raise UsageError, _('no such %(classname)s property '
390                     '"%(propname)s"')%locals()
391         if self.comma_sep:
392             print ','.join(l)
393         return 0
396     def do_set(self, args):
397         '''Usage: set designator[,designator]* propname=value ...
398         Set the given property of one or more designator(s).
400         Sets the property to the value for all designators given.
401         '''
402         if len(args) < 2:
403             raise UsageError, _('Not enough arguments supplied')
404         from roundup import hyperdb
406         designators = args[0].split(',')
408         # get the props from the args
409         props = self.props_from_args(args[1:])
411         # now do the set for all the nodes
412         for designator in designators:
413             # decode the node designator
414             try:
415                 classname, nodeid = hyperdb.splitDesignator(designator)
416             except hyperdb.DesignatorError, message:
417                 raise UsageError, message
419             # get the class
420             cl = self.get_class(classname)
422             properties = cl.getprops()
423             for key, value in props.items():
424                 proptype =  properties[key]
425                 if isinstance(proptype, hyperdb.String):
426                     continue
427                 elif isinstance(proptype, hyperdb.Password):
428                     props[key] = password.Password(value)
429                 elif isinstance(proptype, hyperdb.Date):
430                     try:
431                         props[key] = date.Date(value)
432                     except ValueError, message:
433                         raise UsageError, '"%s": %s'%(value, message)
434                 elif isinstance(proptype, hyperdb.Interval):
435                     try:
436                         props[key] = date.Interval(value)
437                     except ValueError, message:
438                         raise UsageError, '"%s": %s'%(value, message)
439                 elif isinstance(proptype, hyperdb.Link):
440                     props[key] = value
441                 elif isinstance(proptype, hyperdb.Multilink):
442                     props[key] = value.split(',')
443                 elif isinstance(proptype, hyperdb.Boolean):
444                     props[key] = value.lower() in ('yes', 'true', 'on', '1')
445                 elif isinstance(proptype, hyperdb.Number):
446                     props[key] = int(value)
448             # try the set
449             try:
450                 apply(cl.set, (nodeid, ), props)
451             except (TypeError, IndexError, ValueError), message:
452                 raise UsageError, message
453         return 0
455     def do_find(self, args):
456         '''Usage: find classname propname=value ...
457         Find the nodes of the given class with a given link property value.
459         Find the nodes of the given class with a given link property value. The
460         value may be either the nodeid of the linked node, or its key value.
461         '''
462         if len(args) < 1:
463             raise UsageError, _('Not enough arguments supplied')
464         classname = args[0]
465         # get the class
466         cl = self.get_class(classname)
468         # handle the propname=value argument
469         props = self.props_from_args(args[1:])
471         # if the value isn't a number, look up the linked class to get the
472         # number
473         for propname, value in props.items():
474             num_re = re.compile('^\d+$')
475             if not num_re.match(value):
476                 # get the property
477                 try:
478                     property = cl.properties[propname]
479                 except KeyError:
480                     raise UsageError, _('%(classname)s has no property '
481                         '"%(propname)s"')%locals()
483                 # make sure it's a link
484                 if (not isinstance(property, hyperdb.Link) and not
485                         isinstance(property, hyperdb.Multilink)):
486                     raise UsageError, _('You may only "find" link properties')
488                 # get the linked-to class and look up the key property
489                 link_class = self.db.getclass(property.classname)
490                 try:
491                     props[propname] = link_class.lookup(value)
492                 except TypeError:
493                     raise UsageError, _('%(classname)s has no key property"')%{
494                         'classname': link_class.classname}
496         # now do the find 
497         try:
498             if self.comma_sep:
499                 print ','.join(apply(cl.find, (), props))
500             else:
501                 print apply(cl.find, (), props)
502         except KeyError:
503             raise UsageError, _('%(classname)s has no property '
504                 '"%(propname)s"')%locals()
505         except (ValueError, TypeError), message:
506             raise UsageError, message
507         return 0
509     def do_specification(self, args):
510         '''Usage: specification classname
511         Show the properties for a classname.
513         This lists the properties for a given class.
514         '''
515         if len(args) < 1:
516             raise UsageError, _('Not enough arguments supplied')
517         classname = args[0]
518         # get the class
519         cl = self.get_class(classname)
521         # get the key property
522         keyprop = cl.getkey()
523         for key, value in cl.properties.items():
524             if keyprop == key:
525                 print _('%(key)s: %(value)s (key property)')%locals()
526             else:
527                 print _('%(key)s: %(value)s')%locals()
529     def do_display(self, args):
530         '''Usage: display designator
531         Show the property values for the given node.
533         This lists the properties and their associated values for the given
534         node.
535         '''
536         if len(args) < 1:
537             raise UsageError, _('Not enough arguments supplied')
539         # decode the node designator
540         try:
541             classname, nodeid = hyperdb.splitDesignator(args[0])
542         except hyperdb.DesignatorError, message:
543             raise UsageError, message
545         # get the class
546         cl = self.get_class(classname)
548         # display the values
549         for key in cl.properties.keys():
550             value = cl.get(nodeid, key)
551             print _('%(key)s: %(value)s')%locals()
553     def do_create(self, args):
554         '''Usage: create classname property=value ...
555         Create a new entry of a given class.
557         This creates a new entry of the given class using the property
558         name=value arguments provided on the command line after the "create"
559         command.
560         '''
561         if len(args) < 1:
562             raise UsageError, _('Not enough arguments supplied')
563         from roundup import hyperdb
565         classname = args[0]
567         # get the class
568         cl = self.get_class(classname)
570         # now do a create
571         props = {}
572         properties = cl.getprops(protected = 0)
573         if len(args) == 1:
574             # ask for the properties
575             for key, value in properties.items():
576                 if key == 'id': continue
577                 name = value.__class__.__name__
578                 if isinstance(value , hyperdb.Password):
579                     again = None
580                     while value != again:
581                         value = getpass.getpass(_('%(propname)s (Password): ')%{
582                             'propname': key.capitalize()})
583                         again = getpass.getpass(_('   %(propname)s (Again): ')%{
584                             'propname': key.capitalize()})
585                         if value != again: print _('Sorry, try again...')
586                     if value:
587                         props[key] = value
588                 else:
589                     value = raw_input(_('%(propname)s (%(proptype)s): ')%{
590                         'propname': key.capitalize(), 'proptype': name})
591                     if value:
592                         props[key] = value
593         else:
594             props = self.props_from_args(args[1:])
596         # convert types
597         for propname, value in props.items():
598             # get the property
599             try:
600                 proptype = properties[propname]
601             except KeyError:
602                 raise UsageError, _('%(classname)s has no property '
603                     '"%(propname)s"')%locals()
605             if isinstance(proptype, hyperdb.Date):
606                 try:
607                     props[propname] = date.Date(value)
608                 except ValueError, message:
609                     raise UsageError, _('"%(value)s": %(message)s')%locals()
610             elif isinstance(proptype, hyperdb.Interval):
611                 try:
612                     props[propname] = date.Interval(value)
613                 except ValueError, message:
614                     raise UsageError, _('"%(value)s": %(message)s')%locals()
615             elif isinstance(proptype, hyperdb.Password):
616                 props[propname] = password.Password(value)
617             elif isinstance(proptype, hyperdb.Multilink):
618                 props[propname] = value.split(',')
619             elif isinstance(proptype, hyperdb.Boolean):
620                 props[propname] = value.lower() in ('yes', 'true', 'on', '1')
621             elif isinstance(proptype, hyperdb.Number):
622                 props[propname] = int(value)
624         # check for the key property
625         propname = cl.getkey()
626         if propname and not props.has_key(propname):
627             raise UsageError, _('you must provide the "%(propname)s" '
628                 'property.')%locals()
630         # do the actual create
631         try:
632             print apply(cl.create, (), props)
633         except (TypeError, IndexError, ValueError), message:
634             raise UsageError, message
635         return 0
637     def do_list(self, args):
638         '''Usage: list classname [property]
639         List the instances of a class.
641         Lists all instances of the given class. If the property is not
642         specified, the  "label" property is used. The label property is tried
643         in order: the key, "name", "title" and then the first property,
644         alphabetically.
645         '''
646         if len(args) < 1:
647             raise UsageError, _('Not enough arguments supplied')
648         classname = args[0]
650         # get the class
651         cl = self.get_class(classname)
653         # figure the property
654         if len(args) > 1:
655             propname = args[1]
656         else:
657             propname = cl.labelprop()
659         if self.comma_sep:
660             print ','.join(cl.list())
661         else:
662             for nodeid in cl.list():
663                 try:
664                     value = cl.get(nodeid, propname)
665                 except KeyError:
666                     raise UsageError, _('%(classname)s has no property '
667                         '"%(propname)s"')%locals()
668                 print _('%(nodeid)4s: %(value)s')%locals()
669         return 0
671     def do_table(self, args):
672         '''Usage: table classname [property[,property]*]
673         List the instances of a class in tabular form.
675         Lists all instances of the given class. If the properties are not
676         specified, all properties are displayed. By default, the column widths
677         are the width of the property names. The width may be explicitly defined
678         by defining the property as "name:width". For example::
679           roundup> table priority id,name:10
680           Id Name
681           1  fatal-bug 
682           2  bug       
683           3  usability 
684           4  feature   
685         '''
686         if len(args) < 1:
687             raise UsageError, _('Not enough arguments supplied')
688         classname = args[0]
690         # get the class
691         cl = self.get_class(classname)
693         # figure the property names to display
694         if len(args) > 1:
695             prop_names = args[1].split(',')
696             all_props = cl.getprops()
697             for spec in prop_names:
698                 if ':' in spec:
699                     try:
700                         propname, width = spec.split(':')
701                     except (ValueError, TypeError):
702                         raise UsageError, _('"%(spec)s" not name:width')%locals()
703                 else:
704                     propname = spec
705                 if not all_props.has_key(propname):
706                     raise UsageError, _('%(classname)s has no property '
707                         '"%(propname)s"')%locals()
708         else:
709             prop_names = cl.getprops().keys()
711         # now figure column widths
712         props = []
713         for spec in prop_names:
714             if ':' in spec:
715                 name, width = spec.split(':')
716                 props.append((name, int(width)))
717             else:
718                 props.append((spec, len(spec)))
720         # now display the heading
721         print ' '.join([name.capitalize().ljust(width) for name,width in props])
723         # and the table data
724         for nodeid in cl.list():
725             l = []
726             for name, width in props:
727                 if name != 'id':
728                     try:
729                         value = str(cl.get(nodeid, name))
730                     except KeyError:
731                         # we already checked if the property is valid - a
732                         # KeyError here means the node just doesn't have a
733                         # value for it
734                         value = ''
735                 else:
736                     value = str(nodeid)
737                 f = '%%-%ds'%width
738                 l.append(f%value[:width])
739             print ' '.join(l)
740         return 0
742     def do_history(self, args):
743         '''Usage: history designator
744         Show the history entries of a designator.
746         Lists the journal entries for the node identified by the designator.
747         '''
748         if len(args) < 1:
749             raise UsageError, _('Not enough arguments supplied')
750         try:
751             classname, nodeid = hyperdb.splitDesignator(args[0])
752         except hyperdb.DesignatorError, message:
753             raise UsageError, message
755         try:
756             print self.db.getclass(classname).history(nodeid)
757         except KeyError:
758             raise UsageError, _('no such class "%(classname)s"')%locals()
759         except IndexError:
760             raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
761         return 0
763     def do_commit(self, args):
764         '''Usage: commit
765         Commit all changes made to the database.
767         The changes made during an interactive session are not
768         automatically written to the database - they must be committed
769         using this command.
771         One-off commands on the command-line are automatically committed if
772         they are successful.
773         '''
774         self.db.commit()
775         return 0
777     def do_rollback(self, args):
778         '''Usage: rollback
779         Undo all changes that are pending commit to the database.
781         The changes made during an interactive session are not
782         automatically written to the database - they must be committed
783         manually. This command undoes all those changes, so a commit
784         immediately after would make no changes to the database.
785         '''
786         self.db.rollback()
787         return 0
789     def do_retire(self, args):
790         '''Usage: retire designator[,designator]*
791         Retire the node specified by designator.
793         This action indicates that a particular node is not to be retrieved by
794         the list or find commands, and its key value may be re-used.
795         '''
796         if len(args) < 1:
797             raise UsageError, _('Not enough arguments supplied')
798         designators = args[0].split(',')
799         for designator in designators:
800             try:
801                 classname, nodeid = hyperdb.splitDesignator(designator)
802             except hyperdb.DesignatorError, message:
803                 raise UsageError, message
804             try:
805                 self.db.getclass(classname).retire(nodeid)
806             except KeyError:
807                 raise UsageError, _('no such class "%(classname)s"')%locals()
808             except IndexError:
809                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
810         return 0
812     def do_export(self, args):
813         '''Usage: export [class[,class]] export_dir
814         Export the database to colon-separated-value files.
816         This action exports the current data from the database into
817         colon-separated-value files that are placed in the nominated
818         destination directory. The journals are not exported.
819         '''
820         # we need the CSV module
821         if csv is None:
822             raise UsageError, \
823                 _('Sorry, you need the csv module to use this function.\n'
824                 'Get it from: http://www.object-craft.com.au/projects/csv/')
826         # grab the directory to export to
827         if len(args) < 1:
828             raise UsageError, _('Not enough arguments supplied')
829         dir = args[-1]
831         # get the list of classes to export
832         if len(args) == 2:
833             classes = args[0].split(',')
834         else:
835             classes = self.db.classes.keys()
837         # use the csv parser if we can - it's faster
838         p = csv.parser(field_sep=':')
840         # do all the classes specified
841         for classname in classes:
842             cl = self.get_class(classname)
843             f = open(os.path.join(dir, classname+'.csv'), 'w')
844             properties = cl.getprops()
845             propnames = properties.keys()
846             propnames.sort()
847             print >> f, p.join(propnames)
849             # all nodes for this class
850             for nodeid in cl.list():
851                 print >>f, p.join(cl.export_list(propnames, nodeid))
852         return 0
854     def do_import(self, args):
855         '''Usage: import import_dir
856         Import a database from the directory containing CSV files, one per
857         class to import.
859         The files must define the same properties as the class (including having
860         a "header" line with those property names.)
862         The imported nodes will have the same nodeid as defined in the
863         import file, thus replacing any existing content.
865         The new nodes are added to the existing database - if you want to
866         create a new database using the imported data, then create a new
867         database (or, tediously, retire all the old data.)
868         '''
869         if len(args) < 1:
870             raise UsageError, _('Not enough arguments supplied')
871         if csv is None:
872             raise UsageError, \
873                 _('Sorry, you need the csv module to use this function.\n'
874                 'Get it from: http://www.object-craft.com.au/projects/csv/')
876         from roundup import hyperdb
878         for file in os.listdir(args[0]):
879             f = open(os.path.join(args[0], file))
881             # get the classname
882             classname = os.path.splitext(file)[0]
884             # ensure that the properties and the CSV file headings match
885             cl = self.get_class(classname)
886             p = csv.parser(field_sep=':')
887             file_props = p.parse(f.readline())
888             properties = cl.getprops()
889             propnames = properties.keys()
890             propnames.sort()
891             m = file_props[:]
892             m.sort()
893             if m != propnames:
894                 raise UsageError, _('Import file doesn\'t define the same '
895                     'properties as "%(arg0)s".')%{'arg0': args[0]}
897             # loop through the file and create a node for each entry
898             maxid = 1
899             while 1:
900                 line = f.readline()
901                 if not line: break
903                 # parse lines until we get a complete entry
904                 while 1:
905                     l = p.parse(line)
906                     if l: break
907                     line = f.readline()
908                     if not line:
909                         raise ValueError, "Unexpected EOF during CSV parse"
911                 # do the import and figure the current highest nodeid
912                 maxid = max(maxid, int(cl.import_list(propnames, l)))
914             print 'setting', classname, maxid
915             self.db.setid(classname, str(maxid))
916         return 0
918     def do_pack(self, args):
919         '''Usage: pack period | date
921 Remove journal entries older than a period of time specified or
922 before a certain date.
924 A period is specified using the suffixes "y", "m", and "d". The
925 suffix "w" (for "week") means 7 days.
927       "3y" means three years
928       "2y 1m" means two years and one month
929       "1m 25d" means one month and 25 days
930       "2w 3d" means two weeks and three days
932 Date format is "YYYY-MM-DD" eg:
933     2001-01-01
934     
935         '''
936         if len(args) <> 1:
937             raise UsageError, _('Not enough arguments supplied')
938         
939         # are we dealing with a period or a date
940         value = args[0]
941         date_re = re.compile(r'''
942               (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
943               (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
944               ''', re.VERBOSE)
945         m = date_re.match(value)
946         if not m:
947             raise ValueError, _('Invalid format')
948         m = m.groupdict()
949         if m['period']:
950             pack_before = date.Date(". - %s"%value)
951         elif m['date']:
952             pack_before = date.Date(value)
953         self.db.pack(pack_before)
954         return 0
956     def do_reindex(self, args):
957         '''Usage: reindex
958         Re-generate a tracker's search indexes.
960         This will re-generate the search indexes for a tracker. This will
961         typically happen automatically.
962         '''
963         self.db.indexer.force_reindex()
964         self.db.reindex()
965         return 0
967     def do_security(self, args):
968         '''Usage: security [Role name]
969         Display the Permissions available to one or all Roles.
970         '''
971         if len(args) == 1:
972             role = args[0]
973             try:
974                 roles = [(args[0], self.db.security.role[args[0]])]
975             except KeyError:
976                 print _('No such Role "%(role)s"')%locals()
977                 return 1
978         else:
979             roles = self.db.security.role.items()
980             role = self.db.config.NEW_WEB_USER_ROLES
981             if ',' in role:
982                 print _('New Web users get the Roles "%(role)s"')%locals()
983             else:
984                 print _('New Web users get the Role "%(role)s"')%locals()
985             role = self.db.config.NEW_EMAIL_USER_ROLES
986             if ',' in role:
987                 print _('New Email users get the Roles "%(role)s"')%locals()
988             else:
989                 print _('New Email users get the Role "%(role)s"')%locals()
990         roles.sort()
991         for rolename, role in roles:
992             print _('Role "%(name)s":')%role.__dict__
993             for permission in role.permissions:
994                 if permission.klass:
995                     print _(' %(description)s (%(name)s for "%(klass)s" '
996                         'only)')%permission.__dict__
997                 else:
998                     print _(' %(description)s (%(name)s)')%permission.__dict__
999         return 0
1001     def run_command(self, args):
1002         '''Run a single command
1003         '''
1004         command = args[0]
1006         # handle help now
1007         if command == 'help':
1008             if len(args)>1:
1009                 self.do_help(args[1:])
1010                 return 0
1011             self.do_help(['help'])
1012             return 0
1013         if command == 'morehelp':
1014             self.do_help(['help'])
1015             self.help_commands()
1016             self.help_all()
1017             return 0
1019         # figure what the command is
1020         try:
1021             functions = self.commands.get(command)
1022         except KeyError:
1023             # not a valid command
1024             print _('Unknown command "%(command)s" ("help commands" for a '
1025                 'list)')%locals()
1026             return 1
1028         # check for multiple matches
1029         if len(functions) > 1:
1030             print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1031                 command, 'list': ', '.join([i[0] for i in functions])}
1032             return 1
1033         command, function = functions[0]
1035         # make sure we have a tracker_home
1036         while not self.tracker_home:
1037             self.tracker_home = raw_input(_('Enter tracker home: ')).strip()
1039         # before we open the db, we may be doing an install or init
1040         if command == 'initialise':
1041             try:
1042                 return self.do_initialise(self.tracker_home, args)
1043             except UsageError, message:
1044                 print _('Error: %(message)s')%locals()
1045                 return 1
1046         elif command == 'install':
1047             try:
1048                 return self.do_install(self.tracker_home, args)
1049             except UsageError, message:
1050                 print _('Error: %(message)s')%locals()
1051                 return 1
1053         # get the tracker
1054         try:
1055             tracker = roundup.instance.open(self.tracker_home)
1056         except ValueError, message:
1057             self.tracker_home = ''
1058             print _("Error: Couldn't open tracker: %(message)s")%locals()
1059             return 1
1061         # only open the database once!
1062         if not self.db:
1063             self.db = tracker.open('admin')
1065         # do the command
1066         ret = 0
1067         try:
1068             ret = function(args[1:])
1069         except UsageError, message:
1070             print _('Error: %(message)s')%locals()
1071             print
1072             print function.__doc__
1073             ret = 1
1074         except:
1075             import traceback
1076             traceback.print_exc()
1077             ret = 1
1078         return ret
1080     def interactive(self):
1081         '''Run in an interactive mode
1082         '''
1083         print _('Roundup %s ready for input.'%roundup_version)
1084         print _('Type "help" for help.')
1085         try:
1086             import readline
1087         except ImportError:
1088             print _('Note: command history and editing not available')
1090         while 1:
1091             try:
1092                 command = raw_input(_('roundup> '))
1093             except EOFError:
1094                 print _('exit...')
1095                 break
1096             if not command: continue
1097             args = token.token_split(command)
1098             if not args: continue
1099             if args[0] in ('quit', 'exit'): break
1100             self.run_command(args)
1102         # exit.. check for transactions
1103         if self.db and self.db.transactions:
1104             commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1105             if commit and commit[0].lower() == 'y':
1106                 self.db.commit()
1107         return 0
1109     def main(self):
1110         try:
1111             opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1112         except getopt.GetoptError, e:
1113             self.usage(str(e))
1114             return 1
1116         # handle command-line args
1117         self.tracker_home = os.environ.get('TRACKER_HOME', '')
1118         # TODO: reinstate the user/password stuff (-u arg too)
1119         name = password = ''
1120         if os.environ.has_key('ROUNDUP_LOGIN'):
1121             l = os.environ['ROUNDUP_LOGIN'].split(':')
1122             name = l[0]
1123             if len(l) > 1:
1124                 password = l[1]
1125         self.comma_sep = 0
1126         for opt, arg in opts:
1127             if opt == '-h':
1128                 self.usage()
1129                 return 0
1130             if opt == '-i':
1131                 self.tracker_home = arg
1132             if opt == '-c':
1133                 self.comma_sep = 1
1135         # if no command - go interactive
1136         ret = 0
1137         if not args:
1138             self.interactive()
1139         else:
1140             ret = self.run_command(args)
1141             if self.db: self.db.commit()
1142         return ret
1145 if __name__ == '__main__':
1146     tool = AdminTool()
1147     sys.exit(tool.main())
1149 # vim: set filetype=python ts=4 sw=4 et si