Code

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