Code

48b96d6026720611c13590deaf8577a8ab583411
[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.22 2002-08-16 04:26: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.instance_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 [-i instance home] [-u login] [-c] <command> <arguments>
92 Help:
93  roundup-admin -h
94  roundup-admin help                       -- this help
95  roundup-admin help <command>             -- command-specific help
96  roundup-admin help all                   -- all available help
97 Options:
98  -i instance home  -- specify the issue tracker "home directory" to administer
99  -u                -- the user[:password] to use for commands
100  -c                -- when outputting lists of data, just comma-separate them''')%locals()
101         self.help_commands()
103     def help_commands(self):
104         print _('Commands:'),
105         commands = ['']
106         for command in self.commands.values():
107             h = command.__doc__.split('\n')[0]
108             commands.append(' '+h[7:])
109         commands.sort()
110         commands.append(_('Commands may be abbreviated as long as the abbreviation matches only one'))
111         commands.append(_('command, e.g. l == li == lis == list.'))
112         print '\n'.join(commands)
113         print
115     def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
116         commands = self.commands.values()
117         def sortfun(a, b):
118             return cmp(a.__name__, b.__name__)
119         commands.sort(sortfun)
120         for command in commands:
121             h = command.__doc__.split('\n')
122             name = command.__name__[3:]
123             usage = h[0]
124             print _('''
125 <tr><td valign=top><strong>%(name)s</strong></td>
126     <td><tt>%(usage)s</tt><p>
127 <pre>''')%locals()
128             indent = indent_re.match(h[3])
129             if indent: indent = len(indent.group(1))
130             for line in h[3:]:
131                 if indent:
132                     print line[indent:]
133                 else:
134                     print line
135             print _('</pre></td></tr>\n')
137     def help_all(self):
138         print _('''
139 All commands (except help) require an instance specifier. This is just the path
140 to the roundup instance you're working with. A roundup instance is where 
141 roundup keeps the database and configuration file that defines an issue
142 tracker. It may be thought of as the issue tracker's "home directory". It may
143 be specified in the environment variable ROUNDUP_INSTANCE or on the command
144 line as "-i instance".
146 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
148 Property values are represented as strings in command arguments and in the
149 printed results:
150  . Strings are, well, strings.
151  . Date values are printed in the full date format in the local time zone, and
152    accepted in the full format or any of the partial formats explained below.
153  . Link values are printed as node designators. When given as an argument,
154    node designators and key strings are both accepted.
155  . Multilink values are printed as lists of node designators joined by commas.
156    When given as an argument, node designators and key strings are both
157    accepted; an empty string, a single node, or a list of nodes joined by
158    commas is accepted.
160 When property values must contain spaces, just surround the value with
161 quotes, either ' or ". A single space may also be backslash-quoted. If a
162 valuu must contain a quote character, it must be backslash-quoted or inside
163 quotes. Examples:
164            hello world      (2 tokens: hello, world)
165            "hello world"    (1 token: hello world)
166            "Roch'e" Compaan (2 tokens: Roch'e Compaan)
167            Roch\'e Compaan  (2 tokens: Roch'e Compaan)
168            address="1 2 3"  (1 token: address=1 2 3)
169            \\               (1 token: \)
170            \n\r\t           (1 token: a newline, carriage-return and tab)
172 When multiple nodes are specified to the roundup get or roundup set
173 commands, the specified properties are retrieved or set on all the listed
174 nodes. 
176 When multiple results are returned by the roundup get or roundup find
177 commands, they are printed one per line (default) or joined by commas (with
178 the -c) option. 
180 Where the command changes data, a login name/password is required. The
181 login may be specified as either "name" or "name:password".
182  . ROUNDUP_LOGIN environment variable
183  . the -u command-line option
184 If either the name or password is not supplied, they are obtained from the
185 command-line. 
187 Date format examples:
188   "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
189   "2000-04-17" means <Date 2000-04-17.00:00:00>
190   "01-25" means <Date yyyy-01-25.00:00:00>
191   "08-13.22:13" means <Date yyyy-08-14.03:13:00>
192   "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
193   "14:25" means <Date yyyy-mm-dd.19:25:00>
194   "8:47:11" means <Date yyyy-mm-dd.13:47:11>
195   "." means "right now"
197 Command help:
198 ''')
199         for name, command in self.commands.items():
200             print _('%s:')%name
201             print _('   '), command.__doc__
203     def do_help(self, args, nl_re=re.compile('[\r\n]'),
204             indent_re=re.compile(r'^(\s+)\S+')):
205         '''Usage: help topic
206         Give help about topic.
208         commands  -- list commands
209         <command> -- help specific to a command
210         initopts  -- init command options
211         all       -- all available help
212         '''
213         if len(args)>0:
214             topic = args[0]
215         else:
216             topic = 'help'
217  
219         # try help_ methods
220         if self.help.has_key(topic):
221             self.help[topic]()
222             return 0
224         # try command docstrings
225         try:
226             l = self.commands.get(topic)
227         except KeyError:
228             print _('Sorry, no help for "%(topic)s"')%locals()
229             return 1
231         # display the help for each match, removing the docsring indent
232         for name, help in l:
233             lines = nl_re.split(help.__doc__)
234             print lines[0]
235             indent = indent_re.match(lines[1])
236             if indent: indent = len(indent.group(1))
237             for line in lines[1:]:
238                 if indent:
239                     print line[indent:]
240                 else:
241                     print line
242         return 0
244     def help_initopts(self):
245         import roundup.templates
246         templates = roundup.templates.listTemplates()
247         print _('Templates:'), ', '.join(templates)
248         import roundup.backends
249         backends = roundup.backends.__all__
250         print _('Back ends:'), ', '.join(backends)
252     def do_install(self, instance_home, args):
253         '''Usage: install [template [backend [admin password]]]
254         Install a new Roundup instance.
256         The command will prompt for the instance home directory (if not supplied
257         through INSTANCE_HOME or the -i option). The template, backend and admin
258         password may be specified on the command-line as arguments, in that
259         order.
261         The initialise command must be called after this command in order
262         to initialise the instance's database. You may edit the instance's
263         initial database contents before running that command by editing
264         the instance's dbinit.py module init() function.
266         See also initopts help.
267         '''
268         if len(args) < 1:
269             raise UsageError, _('Not enough arguments supplied')
271         # make sure the instance home can be created
272         parent = os.path.split(instance_home)[0]
273         if not os.path.exists(parent):
274             raise UsageError, _('Instance home parent directory "%(parent)s"'
275                 ' does not exist')%locals()
277         # select template
278         import roundup.templates
279         templates = roundup.templates.listTemplates()
280         template = len(args) > 1 and args[1] or ''
281         if template not in templates:
282             print _('Templates:'), ', '.join(templates)
283         while template not in templates:
284             template = raw_input(_('Select template [classic]: ')).strip()
285             if not template:
286                 template = 'classic'
288         # select hyperdb backend
289         import roundup.backends
290         backends = roundup.backends.__all__
291         backend = len(args) > 2 and args[2] or ''
292         if backend not in backends:
293             print _('Back ends:'), ', '.join(backends)
294         while backend not in backends:
295             backend = raw_input(_('Select backend [anydbm]: ')).strip()
296             if not backend:
297                 backend = 'anydbm'
299         # install!
300         init.install(instance_home, template, backend)
302         print _('''
303  You should now edit the instance configuration file:
304    %(instance_config_file)s
305  ... at a minimum, you must set MAILHOST, MAIL_DOMAIN and ADMIN_EMAIL.
307  If you wish to modify the default schema, you should also edit the database
308  initialisation file:
309    %(database_config_file)s
310  ... see the documentation on customizing for more information.
311 ''')%{
312     'instance_config_file': os.path.join(instance_home, 'instance_config.py'),
313     'database_config_file': os.path.join(instance_home, 'dbinit.py')
315         return 0
318     def do_initialise(self, instance_home, args):
319         '''Usage: initialise [adminpw]
320         Initialise a new Roundup instance.
322         The administrator details will be set at this step.
324         Execute the instance's initialisation function dbinit.init()
325         '''
326         # password
327         if len(args) > 1:
328             adminpw = args[1]
329         else:
330             adminpw = ''
331             confirm = 'x'
332             while adminpw != confirm:
333                 adminpw = getpass.getpass(_('Admin Password: '))
334                 confirm = getpass.getpass(_('       Confirm: '))
336         # make sure the instance home is installed
337         if not os.path.exists(instance_home):
338             raise UsageError, _('Instance home does not exist')%locals()
339         if not os.path.exists(os.path.join(instance_home, 'html')):
340             raise UsageError, _('Instance has not been installed')%locals()
342         # is there already a database?
343         if os.path.exists(os.path.join(instance_home, 'db')):
344             print _('WARNING: The database is already initialised!')
345             print _('If you re-initialise it, you will lose all the data!')
346             ok = raw_input(_('Erase it? Y/[N]: ')).strip()
347             if ok.lower() != 'y':
348                 return 0
350             # nuke it
351             shutil.rmtree(os.path.join(instance_home, 'db'))
353         # GO
354         init.initialise(instance_home, adminpw)
356         return 0
359     def do_get(self, args):
360         '''Usage: get property designator[,designator]*
361         Get the given property of one or more designator(s).
363         Retrieves the property value of the nodes specified by the designators.
364         '''
365         if len(args) < 2:
366             raise UsageError, _('Not enough arguments supplied')
367         propname = args[0]
368         designators = args[1].split(',')
369         l = []
370         for designator in designators:
371             # decode the node designator
372             try:
373                 classname, nodeid = hyperdb.splitDesignator(designator)
374             except hyperdb.DesignatorError, message:
375                 raise UsageError, message
377             # get the class
378             cl = self.get_class(classname)
379             try:
380                 if self.comma_sep:
381                     l.append(cl.get(nodeid, propname))
382                 else:
383                     print cl.get(nodeid, propname)
384             except IndexError:
385                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
386             except KeyError:
387                 raise UsageError, _('no such %(classname)s property '
388                     '"%(propname)s"')%locals()
389         if self.comma_sep:
390             print ','.join(l)
391         return 0
394     def do_set(self, args):
395         '''Usage: set designator[,designator]* propname=value ...
396         Set the given property of one or more designator(s).
398         Sets the property to the value for all designators given.
399         '''
400         if len(args) < 2:
401             raise UsageError, _('Not enough arguments supplied')
402         from roundup import hyperdb
404         designators = args[0].split(',')
406         # get the props from the args
407         props = self.props_from_args(args[1:])
409         # now do the set for all the nodes
410         for designator in designators:
411             # decode the node designator
412             try:
413                 classname, nodeid = hyperdb.splitDesignator(designator)
414             except hyperdb.DesignatorError, message:
415                 raise UsageError, message
417             # get the class
418             cl = self.get_class(classname)
420             properties = cl.getprops()
421             for key, value in props.items():
422                 proptype =  properties[key]
423                 if isinstance(proptype, hyperdb.String):
424                     continue
425                 elif isinstance(proptype, hyperdb.Password):
426                     props[key] = password.Password(value)
427                 elif isinstance(proptype, hyperdb.Date):
428                     try:
429                         props[key] = date.Date(value)
430                     except ValueError, message:
431                         raise UsageError, '"%s": %s'%(value, message)
432                 elif isinstance(proptype, hyperdb.Interval):
433                     try:
434                         props[key] = date.Interval(value)
435                     except ValueError, message:
436                         raise UsageError, '"%s": %s'%(value, message)
437                 elif isinstance(proptype, hyperdb.Link):
438                     props[key] = value
439                 elif isinstance(proptype, hyperdb.Multilink):
440                     props[key] = value.split(',')
441                 elif isinstance(proptype, hyperdb.Boolean):
442                     props[key] = value.lower() in ('yes', 'true', 'on', '1')
443                 elif isinstance(proptype, hyperdb.Number):
444                     props[key] = int(value)
446             # try the set
447             try:
448                 apply(cl.set, (nodeid, ), props)
449             except (TypeError, IndexError, ValueError), message:
450                 raise UsageError, message
451         return 0
453     def do_find(self, args):
454         '''Usage: find classname propname=value ...
455         Find the nodes of the given class with a given link property value.
457         Find the nodes of the given class with a given link property value. The
458         value may be either the nodeid of the linked node, or its key value.
459         '''
460         if len(args) < 1:
461             raise UsageError, _('Not enough arguments supplied')
462         classname = args[0]
463         # get the class
464         cl = self.get_class(classname)
466         # handle the propname=value argument
467         props = self.props_from_args(args[1:])
469         # if the value isn't a number, look up the linked class to get the
470         # number
471         for propname, value in props.items():
472             num_re = re.compile('^\d+$')
473             if not num_re.match(value):
474                 # get the property
475                 try:
476                     property = cl.properties[propname]
477                 except KeyError:
478                     raise UsageError, _('%(classname)s has no property '
479                         '"%(propname)s"')%locals()
481                 # make sure it's a link
482                 if (not isinstance(property, hyperdb.Link) and not
483                         isinstance(property, hyperdb.Multilink)):
484                     raise UsageError, _('You may only "find" link properties')
486                 # get the linked-to class and look up the key property
487                 link_class = self.db.getclass(property.classname)
488                 try:
489                     props[propname] = link_class.lookup(value)
490                 except TypeError:
491                     raise UsageError, _('%(classname)s has no key property"')%{
492                         'classname': link_class.classname}
494         # now do the find 
495         try:
496             if self.comma_sep:
497                 print ','.join(apply(cl.find, (), props))
498             else:
499                 print apply(cl.find, (), props)
500         except KeyError:
501             raise UsageError, _('%(classname)s has no property '
502                 '"%(propname)s"')%locals()
503         except (ValueError, TypeError), message:
504             raise UsageError, message
505         return 0
507     def do_specification(self, args):
508         '''Usage: specification classname
509         Show the properties for a classname.
511         This lists the properties for a given class.
512         '''
513         if len(args) < 1:
514             raise UsageError, _('Not enough arguments supplied')
515         classname = args[0]
516         # get the class
517         cl = self.get_class(classname)
519         # get the key property
520         keyprop = cl.getkey()
521         for key, value in cl.properties.items():
522             if keyprop == key:
523                 print _('%(key)s: %(value)s (key property)')%locals()
524             else:
525                 print _('%(key)s: %(value)s')%locals()
527     def do_display(self, args):
528         '''Usage: display designator
529         Show the property values for the given node.
531         This lists the properties and their associated values for the given
532         node.
533         '''
534         if len(args) < 1:
535             raise UsageError, _('Not enough arguments supplied')
537         # decode the node designator
538         try:
539             classname, nodeid = hyperdb.splitDesignator(args[0])
540         except hyperdb.DesignatorError, message:
541             raise UsageError, message
543         # get the class
544         cl = self.get_class(classname)
546         # display the values
547         for key in cl.properties.keys():
548             value = cl.get(nodeid, key)
549             print _('%(key)s: %(value)s')%locals()
551     def do_create(self, args):
552         '''Usage: create classname property=value ...
553         Create a new entry of a given class.
555         This creates a new entry of the given class using the property
556         name=value arguments provided on the command line after the "create"
557         command.
558         '''
559         if len(args) < 1:
560             raise UsageError, _('Not enough arguments supplied')
561         from roundup import hyperdb
563         classname = args[0]
565         # get the class
566         cl = self.get_class(classname)
568         # now do a create
569         props = {}
570         properties = cl.getprops(protected = 0)
571         if len(args) == 1:
572             # ask for the properties
573             for key, value in properties.items():
574                 if key == 'id': continue
575                 name = value.__class__.__name__
576                 if isinstance(value , hyperdb.Password):
577                     again = None
578                     while value != again:
579                         value = getpass.getpass(_('%(propname)s (Password): ')%{
580                             'propname': key.capitalize()})
581                         again = getpass.getpass(_('   %(propname)s (Again): ')%{
582                             'propname': key.capitalize()})
583                         if value != again: print _('Sorry, try again...')
584                     if value:
585                         props[key] = value
586                 else:
587                     value = raw_input(_('%(propname)s (%(proptype)s): ')%{
588                         'propname': key.capitalize(), 'proptype': name})
589                     if value:
590                         props[key] = value
591         else:
592             props = self.props_from_args(args[1:])
594         # convert types
595         for propname, value in props.items():
596             # get the property
597             try:
598                 proptype = properties[propname]
599             except KeyError:
600                 raise UsageError, _('%(classname)s has no property '
601                     '"%(propname)s"')%locals()
603             if isinstance(proptype, hyperdb.Date):
604                 try:
605                     props[propname] = date.Date(value)
606                 except ValueError, message:
607                     raise UsageError, _('"%(value)s": %(message)s')%locals()
608             elif isinstance(proptype, hyperdb.Interval):
609                 try:
610                     props[propname] = date.Interval(value)
611                 except ValueError, message:
612                     raise UsageError, _('"%(value)s": %(message)s')%locals()
613             elif isinstance(proptype, hyperdb.Password):
614                 props[propname] = password.Password(value)
615             elif isinstance(proptype, hyperdb.Multilink):
616                 props[propname] = value.split(',')
617             elif isinstance(proptype, hyperdb.Boolean):
618                 props[propname] = value.lower() in ('yes', 'true', 'on', '1')
619             elif isinstance(proptype, hyperdb.Number):
620                 props[propname] = int(value)
622         # check for the key property
623         propname = cl.getkey()
624         if propname and not props.has_key(propname):
625             raise UsageError, _('you must provide the "%(propname)s" '
626                 'property.')%locals()
628         # do the actual create
629         try:
630             print apply(cl.create, (), props)
631         except (TypeError, IndexError, ValueError), message:
632             raise UsageError, message
633         return 0
635     def do_list(self, args):
636         '''Usage: list classname [property]
637         List the instances of a class.
639         Lists all instances of the given class. If the property is not
640         specified, the  "label" property is used. The label property is tried
641         in order: the key, "name", "title" and then the first property,
642         alphabetically.
643         '''
644         if len(args) < 1:
645             raise UsageError, _('Not enough arguments supplied')
646         classname = args[0]
648         # get the class
649         cl = self.get_class(classname)
651         # figure the property
652         if len(args) > 1:
653             propname = args[1]
654         else:
655             propname = cl.labelprop()
657         if self.comma_sep:
658             print ','.join(cl.list())
659         else:
660             for nodeid in cl.list():
661                 try:
662                     value = cl.get(nodeid, propname)
663                 except KeyError:
664                     raise UsageError, _('%(classname)s has no property '
665                         '"%(propname)s"')%locals()
666                 print _('%(nodeid)4s: %(value)s')%locals()
667         return 0
669     def do_table(self, args):
670         '''Usage: table classname [property[,property]*]
671         List the instances of a class in tabular form.
673         Lists all instances of the given class. If the properties are not
674         specified, all properties are displayed. By default, the column widths
675         are the width of the property names. The width may be explicitly defined
676         by defining the property as "name:width". For example::
677           roundup> table priority id,name:10
678           Id Name
679           1  fatal-bug 
680           2  bug       
681           3  usability 
682           4  feature   
683         '''
684         if len(args) < 1:
685             raise UsageError, _('Not enough arguments supplied')
686         classname = args[0]
688         # get the class
689         cl = self.get_class(classname)
691         # figure the property names to display
692         if len(args) > 1:
693             prop_names = args[1].split(',')
694             all_props = cl.getprops()
695             for spec in prop_names:
696                 if ':' in spec:
697                     try:
698                         propname, width = spec.split(':')
699                     except (ValueError, TypeError):
700                         raise UsageError, _('"%(spec)s" not name:width')%locals()
701                 else:
702                     propname = spec
703                 if not all_props.has_key(propname):
704                     raise UsageError, _('%(classname)s has no property '
705                         '"%(propname)s"')%locals()
706         else:
707             prop_names = cl.getprops().keys()
709         # now figure column widths
710         props = []
711         for spec in prop_names:
712             if ':' in spec:
713                 name, width = spec.split(':')
714                 props.append((name, int(width)))
715             else:
716                 props.append((spec, len(spec)))
718         # now display the heading
719         print ' '.join([name.capitalize().ljust(width) for name,width in props])
721         # and the table data
722         for nodeid in cl.list():
723             l = []
724             for name, width in props:
725                 if name != 'id':
726                     try:
727                         value = str(cl.get(nodeid, name))
728                     except KeyError:
729                         # we already checked if the property is valid - a
730                         # KeyError here means the node just doesn't have a
731                         # value for it
732                         value = ''
733                 else:
734                     value = str(nodeid)
735                 f = '%%-%ds'%width
736                 l.append(f%value[:width])
737             print ' '.join(l)
738         return 0
740     def do_history(self, args):
741         '''Usage: history designator
742         Show the history entries of a designator.
744         Lists the journal entries for the node identified by the designator.
745         '''
746         if len(args) < 1:
747             raise UsageError, _('Not enough arguments supplied')
748         try:
749             classname, nodeid = hyperdb.splitDesignator(args[0])
750         except hyperdb.DesignatorError, message:
751             raise UsageError, message
753         try:
754             print self.db.getclass(classname).history(nodeid)
755         except KeyError:
756             raise UsageError, _('no such class "%(classname)s"')%locals()
757         except IndexError:
758             raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
759         return 0
761     def do_commit(self, args):
762         '''Usage: commit
763         Commit all changes made to the database.
765         The changes made during an interactive session are not
766         automatically written to the database - they must be committed
767         using this command.
769         One-off commands on the command-line are automatically committed if
770         they are successful.
771         '''
772         self.db.commit()
773         return 0
775     def do_rollback(self, args):
776         '''Usage: rollback
777         Undo all changes that are pending commit to the database.
779         The changes made during an interactive session are not
780         automatically written to the database - they must be committed
781         manually. This command undoes all those changes, so a commit
782         immediately after would make no changes to the database.
783         '''
784         self.db.rollback()
785         return 0
787     def do_retire(self, args):
788         '''Usage: retire designator[,designator]*
789         Retire the node specified by designator.
791         This action indicates that a particular node is not to be retrieved by
792         the list or find commands, and its key value may be re-used.
793         '''
794         if len(args) < 1:
795             raise UsageError, _('Not enough arguments supplied')
796         designators = args[0].split(',')
797         for designator in designators:
798             try:
799                 classname, nodeid = hyperdb.splitDesignator(designator)
800             except hyperdb.DesignatorError, message:
801                 raise UsageError, message
802             try:
803                 self.db.getclass(classname).retire(nodeid)
804             except KeyError:
805                 raise UsageError, _('no such class "%(classname)s"')%locals()
806             except IndexError:
807                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
808         return 0
810     def do_export(self, args):
811         '''Usage: export [class[,class]] destination_dir
812         Export the database to tab-separated-value files.
814         This action exports the current data from the database into
815         tab-separated-value files that are placed in the nominated destination
816         directory. The journals are not exported.
817         '''
818         # grab the directory to export to
819         if len(args) < 1:
820             raise UsageError, _('Not enough arguments supplied')
821         dir = args[-1]
823         # get the list of classes to export
824         if len(args) == 2:
825             classes = args[0].split(',')
826         else:
827             classes = self.db.classes.keys()
829         # use the csv parser if we can - it's faster
830         if csv is not None:
831             p = csv.parser(field_sep=':')
833         # do all the classes specified
834         for classname in classes:
835             cl = self.get_class(classname)
836             f = open(os.path.join(dir, classname+'.csv'), 'w')
837             f.write(':'.join(cl.properties.keys()) + '\n')
839             # all nodes for this class
840             properties = cl.getprops()
841             for nodeid in cl.list():
842                 l = []
843                 for prop, proptype in properties:
844                     value = cl.get(nodeid, prop)
845                     # convert data where needed
846                     if isinstance(proptype, hyperdb.Date):
847                         value = value.get_tuple()
848                     elif isinstance(proptype, hyperdb.Interval):
849                         value = value.get_tuple()
850                     elif isinstance(proptype, hyperdb.Password):
851                         value = str(value)
852                     l.append(repr(value))
854                 # now write
855                 if csv is not None:
856                    f.write(p.join(l) + '\n')
857                 else:
858                    # escape the individual entries to they're valid CSV
859                    m = []
860                    for entry in l:
861                       if '"' in entry:
862                           entry = '""'.join(entry.split('"'))
863                       if ':' in entry:
864                           entry = '"%s"'%entry
865                       m.append(entry)
866                    f.write(':'.join(m) + '\n')
867         return 0
869     def do_import(self, args):
870         '''Usage: import class file
871         Import the contents of the tab-separated-value file.
873         The file must define the same properties as the class (including having
874         a "header" line with those property names.) The new nodes are added to
875         the existing database - if you want to create a new database using the
876         imported data, then create a new database (or, tediously, retire all
877         the old data.)
878         '''
879         if len(args) < 2:
880             raise UsageError, _('Not enough arguments supplied')
881         if csv is None:
882             raise UsageError, \
883                 _('Sorry, you need the csv module to use this function.\n'
884                 'Get it from: http://www.object-craft.com.au/projects/csv/')
886         from roundup import hyperdb
888         # ensure that the properties and the CSV file headings match
889         classname = args[0]
890         cl = self.get_class(classname)
891         f = open(args[1])
892         p = csv.parser(field_sep=':')
893         file_props = p.parse(f.readline())
894         props = cl.properties.keys()
895         m = file_props[:]
896         m.sort()
897         props.sort()
898         if m != props:
899             raise UsageError, _('Import file doesn\'t define the same '
900                 'properties as "%(arg0)s".')%{'arg0': args[0]}
902         # loop through the file and create a node for each entry
903         n = range(len(props))
904         while 1:
905             line = f.readline()
906             if not line: break
908             # parse lines until we get a complete entry
909             while 1:
910                 l = p.parse(line)
911                 if l: break
912                 line = f.readline()
913                 if not line:
914                     raise ValueError, "Unexpected EOF during CSV parse"
916             # make the new node's property map
917             d = {}
918             for i in n:
919                 # Use eval to reverse the repr() used to output the CSV
920                 value = eval(l[i])
921                 # Figure the property for this column
922                 key = file_props[i]
923                 proptype = cl.properties[key]
924                 # Convert for property type
925                 if isinstance(proptype, hyperdb.Date):
926                     value = date.Date(value)
927                 elif isinstance(proptype, hyperdb.Interval):
928                     value = date.Interval(value)
929                 elif isinstance(proptype, hyperdb.Password):
930                     pwd = password.Password()
931                     pwd.unpack(value)
932                     value = pwd
933                 if value is not None:
934                     d[key] = value
936             # and create the new node
937             apply(cl.create, (), d)
938         return 0
940     def do_pack(self, args):
941         '''Usage: pack period | date
943 Remove journal entries older than a period of time specified or
944 before a certain date.
946 A period is specified using the suffixes "y", "m", and "d". The
947 suffix "w" (for "week") means 7 days.
949       "3y" means three years
950       "2y 1m" means two years and one month
951       "1m 25d" means one month and 25 days
952       "2w 3d" means two weeks and three days
954 Date format is "YYYY-MM-DD" eg:
955     2001-01-01
956     
957         '''
958         if len(args) <> 1:
959             raise UsageError, _('Not enough arguments supplied')
960         
961         # are we dealing with a period or a date
962         value = args[0]
963         date_re = re.compile(r'''
964               (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
965               (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
966               ''', re.VERBOSE)
967         m = date_re.match(value)
968         if not m:
969             raise ValueError, _('Invalid format')
970         m = m.groupdict()
971         if m['period']:
972             pack_before = date.Date(". - %s"%value)
973         elif m['date']:
974             pack_before = date.Date(value)
975         self.db.pack(pack_before)
976         return 0
978     def do_reindex(self, args):
979         '''Usage: reindex
980         Re-generate an instance's search indexes.
982         This will re-generate the search indexes for an instance. This will
983         typically happen automatically.
984         '''
985         self.db.indexer.force_reindex()
986         self.db.reindex()
987         return 0
989     def do_security(self, args):
990         '''Usage: security [Role name]
991         Display the Permissions available to one or all Roles.
992         '''
993         if len(args) == 1:
994             role = args[0]
995             try:
996                 roles = [(args[0], self.db.security.role[args[0]])]
997             except KeyError:
998                 print _('No such Role "%(role)s"')%locals()
999                 return 1
1000         else:
1001             roles = self.db.security.role.items()
1002             role = self.db.config.NEW_WEB_USER_ROLES
1003             if ',' in role:
1004                 print _('New Web users get the Roles "%(role)s"')%locals()
1005             else:
1006                 print _('New Web users get the Role "%(role)s"')%locals()
1007             role = self.db.config.NEW_EMAIL_USER_ROLES
1008             if ',' in role:
1009                 print _('New Email users get the Roles "%(role)s"')%locals()
1010             else:
1011                 print _('New Email users get the Role "%(role)s"')%locals()
1012         roles.sort()
1013         for rolename, role in roles:
1014             print _('Role "%(name)s":')%role.__dict__
1015             for permission in role.permissions:
1016                 if permission.klass:
1017                     print _(' %(description)s (%(name)s for "%(klass)s" '
1018                         'only)')%permission.__dict__
1019                 else:
1020                     print _(' %(description)s (%(name)s)')%permission.__dict__
1021         return 0
1023     def run_command(self, args):
1024         '''Run a single command
1025         '''
1026         command = args[0]
1028         # handle help now
1029         if command == 'help':
1030             if len(args)>1:
1031                 self.do_help(args[1:])
1032                 return 0
1033             self.do_help(['help'])
1034             return 0
1035         if command == 'morehelp':
1036             self.do_help(['help'])
1037             self.help_commands()
1038             self.help_all()
1039             return 0
1041         # figure what the command is
1042         try:
1043             functions = self.commands.get(command)
1044         except KeyError:
1045             # not a valid command
1046             print _('Unknown command "%(command)s" ("help commands" for a '
1047                 'list)')%locals()
1048             return 1
1050         # check for multiple matches
1051         if len(functions) > 1:
1052             print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1053                 command, 'list': ', '.join([i[0] for i in functions])}
1054             return 1
1055         command, function = functions[0]
1057         # make sure we have an instance_home
1058         while not self.instance_home:
1059             self.instance_home = raw_input(_('Enter instance home: ')).strip()
1061         # before we open the db, we may be doing an install or init
1062         if command == 'initialise':
1063             try:
1064                 return self.do_initialise(self.instance_home, args)
1065             except UsageError, message:
1066                 print _('Error: %(message)s')%locals()
1067                 return 1
1068         elif command == 'install':
1069             try:
1070                 return self.do_install(self.instance_home, args)
1071             except UsageError, message:
1072                 print _('Error: %(message)s')%locals()
1073                 return 1
1075         # get the instance
1076         try:
1077             instance = roundup.instance.open(self.instance_home)
1078         except ValueError, message:
1079             self.instance_home = ''
1080             print _("Error: Couldn't open instance: %(message)s")%locals()
1081             return 1
1083         # only open the database once!
1084         if not self.db:
1085             self.db = instance.open('admin')
1087         # do the command
1088         ret = 0
1089         try:
1090             ret = function(args[1:])
1091         except UsageError, message:
1092             print _('Error: %(message)s')%locals()
1093             print
1094             print function.__doc__
1095             ret = 1
1096         except:
1097             import traceback
1098             traceback.print_exc()
1099             ret = 1
1100         return ret
1102     def interactive(self):
1103         '''Run in an interactive mode
1104         '''
1105         print _('Roundup %s ready for input.'%roundup_version)
1106         print _('Type "help" for help.')
1107         try:
1108             import readline
1109         except ImportError:
1110             print _('Note: command history and editing not available')
1112         while 1:
1113             try:
1114                 command = raw_input(_('roundup> '))
1115             except EOFError:
1116                 print _('exit...')
1117                 break
1118             if not command: continue
1119             args = token.token_split(command)
1120             if not args: continue
1121             if args[0] in ('quit', 'exit'): break
1122             self.run_command(args)
1124         # exit.. check for transactions
1125         if self.db and self.db.transactions:
1126             commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1127             if commit and commit[0].lower() == 'y':
1128                 self.db.commit()
1129         return 0
1131     def main(self):
1132         try:
1133             opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1134         except getopt.GetoptError, e:
1135             self.usage(str(e))
1136             return 1
1138         # handle command-line args
1139         self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
1140         # TODO: reinstate the user/password stuff (-u arg too)
1141         name = password = ''
1142         if os.environ.has_key('ROUNDUP_LOGIN'):
1143             l = os.environ['ROUNDUP_LOGIN'].split(':')
1144             name = l[0]
1145             if len(l) > 1:
1146                 password = l[1]
1147         self.comma_sep = 0
1148         for opt, arg in opts:
1149             if opt == '-h':
1150                 self.usage()
1151                 return 0
1152             if opt == '-i':
1153                 self.instance_home = arg
1154             if opt == '-c':
1155                 self.comma_sep = 1
1157         # if no command - go interactive
1158         ret = 0
1159         if not args:
1160             self.interactive()
1161         else:
1162             ret = self.run_command(args)
1163             if self.db: self.db.commit()
1164         return ret
1167 if __name__ == '__main__':
1168     tool = AdminTool()
1169     sys.exit(tool.main())
1172 # $Log: not supported by cvs2svn $
1173 # Revision 1.21  2002/08/01 01:07:37  richard
1174 # include info about new user roles
1176 # Revision 1.20  2002/08/01 00:56:22  richard
1177 # Added the web access and email access permissions, so people can restrict
1178 # access to users who register through the email interface (for example).
1179 # Also added "security" command to the roundup-admin interface to display the
1180 # Role/Permission config for an instance.
1182 # Revision 1.19  2002/07/25 07:14:05  richard
1183 # Bugger it. Here's the current shape of the new security implementation.
1184 # Still to do:
1185 #  . call the security funcs from cgi and mailgw
1186 #  . change shipped templates to include correct initialisation and remove
1187 #    the old config vars
1188 # ... that seems like a lot. The bulk of the work has been done though. Honest :)
1190 # Revision 1.18  2002/07/18 11:17:30  gmcm
1191 # Add Number and Boolean types to hyperdb.
1192 # Add conversion cases to web, mail & admin interfaces.
1193 # Add storage/serialization cases to back_anydbm & back_metakit.
1195 # Revision 1.17  2002/07/14 06:05:50  richard
1196 #  . fixed the date module so that Date(". - 2d") works
1198 # Revision 1.16  2002/07/09 04:19:09  richard
1199 # Added reindex command to roundup-admin.
1200 # Fixed reindex on first access.
1201 # Also fixed reindexing of entries that change.
1203 # Revision 1.15  2002/06/17 23:14:44  richard
1204 # . #569415 ] {version}
1206 # Revision 1.14  2002/06/11 06:41:50  richard
1207 # Removed prompt for admin email in initialisation.
1209 # Revision 1.13  2002/05/30 23:58:14  richard
1210 # oops
1212 # Revision 1.12  2002/05/26 09:04:42  richard
1213 # out by one in the init args
1215 # Revision 1.11  2002/05/23 01:14:20  richard
1216 #  . split instance initialisation into two steps, allowing config changes
1217 #    before the database is initialised.
1219 # Revision 1.10  2002/04/27 10:07:23  richard
1220 # minor fix to error message
1222 # Revision 1.9  2002/03/12 22:51:47  richard
1223 #  . #527416 ] roundup-admin uses undefined value
1224 #  . #527503 ] unfriendly init blowup when parent dir
1225 #    (also handles UsageError correctly now in init)
1227 # Revision 1.8  2002/02/27 03:28:21  richard
1228 # Ran it through pychecker, made fixes
1230 # Revision 1.7  2002/02/20 05:04:32  richard
1231 # Wasn't handling the cvs parser feeding properly.
1233 # Revision 1.6  2002/01/23 07:27:19  grubert
1234 #  . allow abbreviation of "help" in admin tool too.
1236 # Revision 1.5  2002/01/21 16:33:19  rochecompaan
1237 # You can now use the roundup-admin tool to pack the database
1239 # Revision 1.4  2002/01/14 06:51:09  richard
1240 #  . #503164 ] create and passwords
1242 # Revision 1.3  2002/01/08 05:26:32  rochecompaan
1243 # Missing "self" in props_from_args
1245 # Revision 1.2  2002/01/07 10:41:44  richard
1246 # #500140 ] AdminTool.get_class() returns nothing
1248 # Revision 1.1  2002/01/05 02:11:22  richard
1249 # I18N'ed roundup admin - and split the code off into a module so it can be used
1250 # elsewhere.
1251 # Big issue with this is the doc strings - that's the help. We're probably going to
1252 # have to switch to not use docstrings, which will suck a little :(
1256 # vim: set filetype=python ts=4 sw=4 et si