Code

8f086f35e8a508b362e932b87cbde49c08daa0c2
[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.14 2002-06-11 06:41:50 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 import roundup.instance
28 from roundup.i18n import _
30 class CommandDict(UserDict.UserDict):
31     '''Simple dictionary that lets us do lookups using partial keys.
33     Original code submitted by Engelbert Gruber.
34     '''
35     _marker = []
36     def get(self, key, default=_marker):
37         if self.data.has_key(key):
38             return [(key, self.data[key])]
39         keylist = self.data.keys()
40         keylist.sort()
41         l = []
42         for ki in keylist:
43             if ki.startswith(key):
44                 l.append((ki, self.data[ki]))
45         if not l and default is self._marker:
46             raise KeyError, key
47         return l
49 class UsageError(ValueError):
50     pass
52 class AdminTool:
54     def __init__(self):
55         self.commands = CommandDict()
56         for k in AdminTool.__dict__.keys():
57             if k[:3] == 'do_':
58                 self.commands[k[3:]] = getattr(self, k)
59         self.help = {}
60         for k in AdminTool.__dict__.keys():
61             if k[:5] == 'help_':
62                 self.help[k[5:]] = getattr(self, k)
63         self.instance_home = ''
64         self.db = None
66     def get_class(self, classname):
67         '''Get the class - raise an exception if it doesn't exist.
68         '''
69         try:
70             return self.db.getclass(classname)
71         except KeyError:
72             raise UsageError, _('no such class "%(classname)s"')%locals()
74     def props_from_args(self, args):
75         props = {}
76         for arg in args:
77             if arg.find('=') == -1:
78                 raise UsageError, _('argument "%(arg)s" not propname=value')%locals()
79             try:
80                 key, value = arg.split('=')
81             except ValueError:
82                 raise UsageError, _('argument "%(arg)s" not propname=value')%locals()
83             props[key] = value
84         return props
86     def usage(self, message=''):
87         if message:
88             message = _('Problem: %(message)s)\n\n')%locals()
89         print _('''%(message)sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments>
91 Help:
92  roundup-admin -h
93  roundup-admin help                       -- this help
94  roundup-admin help <command>             -- command-specific help
95  roundup-admin help all                   -- all available help
96 Options:
97  -i instance home  -- specify the issue tracker "home directory" to administer
98  -u                -- the user[:password] to use for commands
99  -c                -- when outputting lists of data, just comma-separate them''')%locals()
100         self.help_commands()
102     def help_commands(self):
103         print _('Commands:'),
104         commands = ['']
105         for command in self.commands.values():
106             h = command.__doc__.split('\n')[0]
107             commands.append(' '+h[7:])
108         commands.sort()
109         commands.append(_('Commands may be abbreviated as long as the abbreviation matches only one'))
110         commands.append(_('command, e.g. l == li == lis == list.'))
111         print '\n'.join(commands)
112         print
114     def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
115         commands = self.commands.values()
116         def sortfun(a, b):
117             return cmp(a.__name__, b.__name__)
118         commands.sort(sortfun)
119         for command in commands:
120             h = command.__doc__.split('\n')
121             name = command.__name__[3:]
122             usage = h[0]
123             print _('''
124 <tr><td valign=top><strong>%(name)s</strong></td>
125     <td><tt>%(usage)s</tt><p>
126 <pre>''')%locals()
127             indent = indent_re.match(h[3])
128             if indent: indent = len(indent.group(1))
129             for line in h[3:]:
130                 if indent:
131                     print line[indent:]
132                 else:
133                     print line
134             print _('</pre></td></tr>\n')
136     def help_all(self):
137         print _('''
138 All commands (except help) require an instance specifier. This is just the path
139 to the roundup instance you're working with. A roundup instance is where 
140 roundup keeps the database and configuration file that defines an issue
141 tracker. It may be thought of as the issue tracker's "home directory". It may
142 be specified in the environment variable ROUNDUP_INSTANCE or on the command
143 line as "-i instance".
145 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
147 Property values are represented as strings in command arguments and in the
148 printed results:
149  . Strings are, well, strings.
150  . Date values are printed in the full date format in the local time zone, and
151    accepted in the full format or any of the partial formats explained below.
152  . Link values are printed as node designators. When given as an argument,
153    node designators and key strings are both accepted.
154  . Multilink values are printed as lists of node designators joined by commas.
155    When given as an argument, node designators and key strings are both
156    accepted; an empty string, a single node, or a list of nodes joined by
157    commas is accepted.
159 When property values must contain spaces, just surround the value with
160 quotes, either ' or ". A single space may also be backslash-quoted. If a
161 valuu must contain a quote character, it must be backslash-quoted or inside
162 quotes. Examples:
163            hello world      (2 tokens: hello, world)
164            "hello world"    (1 token: hello world)
165            "Roch'e" Compaan (2 tokens: Roch'e Compaan)
166            Roch\'e Compaan  (2 tokens: Roch'e Compaan)
167            address="1 2 3"  (1 token: address=1 2 3)
168            \\               (1 token: \)
169            \n\r\t           (1 token: a newline, carriage-return and tab)
171 When multiple nodes are specified to the roundup get or roundup set
172 commands, the specified properties are retrieved or set on all the listed
173 nodes. 
175 When multiple results are returned by the roundup get or roundup find
176 commands, they are printed one per line (default) or joined by commas (with
177 the -c) option. 
179 Where the command changes data, a login name/password is required. The
180 login may be specified as either "name" or "name:password".
181  . ROUNDUP_LOGIN environment variable
182  . the -u command-line option
183 If either the name or password is not supplied, they are obtained from the
184 command-line. 
186 Date format examples:
187   "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
188   "2000-04-17" means <Date 2000-04-17.00:00:00>
189   "01-25" means <Date yyyy-01-25.00:00:00>
190   "08-13.22:13" means <Date yyyy-08-14.03:13:00>
191   "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
192   "14:25" means <Date yyyy-mm-dd.19:25:00>
193   "8:47:11" means <Date yyyy-mm-dd.13:47:11>
194   "." means "right now"
196 Command help:
197 ''')
198         for name, command in self.commands.items():
199             print _('%s:')%name
200             print _('   '), command.__doc__
202     def do_help(self, args, nl_re=re.compile('[\r\n]'),
203             indent_re=re.compile(r'^(\s+)\S+')):
204         '''Usage: help topic
205         Give help about topic.
207         commands  -- list commands
208         <command> -- help specific to a command
209         initopts  -- init command options
210         all       -- all available help
211         '''
212         if len(args)>0:
213             topic = args[0]
214         else:
215             topic = 'help'
216  
218         # try help_ methods
219         if self.help.has_key(topic):
220             self.help[topic]()
221             return 0
223         # try command docstrings
224         try:
225             l = self.commands.get(topic)
226         except KeyError:
227             print _('Sorry, no help for "%(topic)s"')%locals()
228             return 1
230         # display the help for each match, removing the docsring indent
231         for name, help in l:
232             lines = nl_re.split(help.__doc__)
233             print lines[0]
234             indent = indent_re.match(lines[1])
235             if indent: indent = len(indent.group(1))
236             for line in lines[1:]:
237                 if indent:
238                     print line[indent:]
239                 else:
240                     print line
241         return 0
243     def help_initopts(self):
244         import roundup.templates
245         templates = roundup.templates.listTemplates()
246         print _('Templates:'), ', '.join(templates)
247         import roundup.backends
248         backends = roundup.backends.__all__
249         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 = roundupdb.splitDesignator(designator)
374             except roundupdb.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 = roundupdb.splitDesignator(designator)
414             except roundupdb.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(',')
442             # try the set
443             try:
444                 apply(cl.set, (nodeid, ), props)
445             except (TypeError, IndexError, ValueError), message:
446                 raise UsageError, message
447         return 0
449     def do_find(self, args):
450         '''Usage: find classname propname=value ...
451         Find the nodes of the given class with a given link property value.
453         Find the nodes of the given class with a given link property value. The
454         value may be either the nodeid of the linked node, or its key value.
455         '''
456         if len(args) < 1:
457             raise UsageError, _('Not enough arguments supplied')
458         classname = args[0]
459         # get the class
460         cl = self.get_class(classname)
462         # handle the propname=value argument
463         props = self.props_from_args(args[1:])
465         # if the value isn't a number, look up the linked class to get the
466         # number
467         for propname, value in props.items():
468             num_re = re.compile('^\d+$')
469             if not num_re.match(value):
470                 # get the property
471                 try:
472                     property = cl.properties[propname]
473                 except KeyError:
474                     raise UsageError, _('%(classname)s has no property '
475                         '"%(propname)s"')%locals()
477                 # make sure it's a link
478                 if (not isinstance(property, hyperdb.Link) and not
479                         isinstance(property, hyperdb.Multilink)):
480                     raise UsageError, _('You may only "find" link properties')
482                 # get the linked-to class and look up the key property
483                 link_class = self.db.getclass(property.classname)
484                 try:
485                     props[propname] = link_class.lookup(value)
486                 except TypeError:
487                     raise UsageError, _('%(classname)s has no key property"')%{
488                         'classname': link_class.classname}
490         # now do the find 
491         try:
492             if self.comma_sep:
493                 print ','.join(apply(cl.find, (), props))
494             else:
495                 print apply(cl.find, (), props)
496         except KeyError:
497             raise UsageError, _('%(classname)s has no property '
498                 '"%(propname)s"')%locals()
499         except (ValueError, TypeError), message:
500             raise UsageError, message
501         return 0
503     def do_specification(self, args):
504         '''Usage: specification classname
505         Show the properties for a classname.
507         This lists the properties for a given class.
508         '''
509         if len(args) < 1:
510             raise UsageError, _('Not enough arguments supplied')
511         classname = args[0]
512         # get the class
513         cl = self.get_class(classname)
515         # get the key property
516         keyprop = cl.getkey()
517         for key, value in cl.properties.items():
518             if keyprop == key:
519                 print _('%(key)s: %(value)s (key property)')%locals()
520             else:
521                 print _('%(key)s: %(value)s')%locals()
523     def do_display(self, args):
524         '''Usage: display designator
525         Show the property values for the given node.
527         This lists the properties and their associated values for the given
528         node.
529         '''
530         if len(args) < 1:
531             raise UsageError, _('Not enough arguments supplied')
533         # decode the node designator
534         try:
535             classname, nodeid = roundupdb.splitDesignator(args[0])
536         except roundupdb.DesignatorError, message:
537             raise UsageError, message
539         # get the class
540         cl = self.get_class(classname)
542         # display the values
543         for key in cl.properties.keys():
544             value = cl.get(nodeid, key)
545             print _('%(key)s: %(value)s')%locals()
547     def do_create(self, args):
548         '''Usage: create classname property=value ...
549         Create a new entry of a given class.
551         This creates a new entry of the given class using the property
552         name=value arguments provided on the command line after the "create"
553         command.
554         '''
555         if len(args) < 1:
556             raise UsageError, _('Not enough arguments supplied')
557         from roundup import hyperdb
559         classname = args[0]
561         # get the class
562         cl = self.get_class(classname)
564         # now do a create
565         props = {}
566         properties = cl.getprops(protected = 0)
567         if len(args) == 1:
568             # ask for the properties
569             for key, value in properties.items():
570                 if key == 'id': continue
571                 name = value.__class__.__name__
572                 if isinstance(value , hyperdb.Password):
573                     again = None
574                     while value != again:
575                         value = getpass.getpass(_('%(propname)s (Password): ')%{
576                             'propname': key.capitalize()})
577                         again = getpass.getpass(_('   %(propname)s (Again): ')%{
578                             'propname': key.capitalize()})
579                         if value != again: print _('Sorry, try again...')
580                     if value:
581                         props[key] = value
582                 else:
583                     value = raw_input(_('%(propname)s (%(proptype)s): ')%{
584                         'propname': key.capitalize(), 'proptype': name})
585                     if value:
586                         props[key] = value
587         else:
588             props = self.props_from_args(args[1:])
590         # convert types
591         for propname, value in props.items():
592             # get the property
593             try:
594                 proptype = properties[propname]
595             except KeyError:
596                 raise UsageError, _('%(classname)s has no property '
597                     '"%(propname)s"')%locals()
599             if isinstance(proptype, hyperdb.Date):
600                 try:
601                     props[propname] = date.Date(value)
602                 except ValueError, message:
603                     raise UsageError, _('"%(value)s": %(message)s')%locals()
604             elif isinstance(proptype, hyperdb.Interval):
605                 try:
606                     props[propname] = date.Interval(value)
607                 except ValueError, message:
608                     raise UsageError, _('"%(value)s": %(message)s')%locals()
609             elif isinstance(proptype, hyperdb.Password):
610                 props[propname] = password.Password(value)
611             elif isinstance(proptype, hyperdb.Multilink):
612                 props[propname] = value.split(',')
614         # check for the key property
615         propname = cl.getkey()
616         if propname and not props.has_key(propname):
617             raise UsageError, _('you must provide the "%(propname)s" '
618                 'property.')%locals()
620         # do the actual create
621         try:
622             print apply(cl.create, (), props)
623         except (TypeError, IndexError, ValueError), message:
624             raise UsageError, message
625         return 0
627     def do_list(self, args):
628         '''Usage: list classname [property]
629         List the instances of a class.
631         Lists all instances of the given class. If the property is not
632         specified, the  "label" property is used. The label property is tried
633         in order: the key, "name", "title" and then the first property,
634         alphabetically.
635         '''
636         if len(args) < 1:
637             raise UsageError, _('Not enough arguments supplied')
638         classname = args[0]
640         # get the class
641         cl = self.get_class(classname)
643         # figure the property
644         if len(args) > 1:
645             propname = args[1]
646         else:
647             propname = cl.labelprop()
649         if self.comma_sep:
650             print ','.join(cl.list())
651         else:
652             for nodeid in cl.list():
653                 try:
654                     value = cl.get(nodeid, propname)
655                 except KeyError:
656                     raise UsageError, _('%(classname)s has no property '
657                         '"%(propname)s"')%locals()
658                 print _('%(nodeid)4s: %(value)s')%locals()
659         return 0
661     def do_table(self, args):
662         '''Usage: table classname [property[,property]*]
663         List the instances of a class in tabular form.
665         Lists all instances of the given class. If the properties are not
666         specified, all properties are displayed. By default, the column widths
667         are the width of the property names. The width may be explicitly defined
668         by defining the property as "name:width". For example::
669           roundup> table priority id,name:10
670           Id Name
671           1  fatal-bug 
672           2  bug       
673           3  usability 
674           4  feature   
675         '''
676         if len(args) < 1:
677             raise UsageError, _('Not enough arguments supplied')
678         classname = args[0]
680         # get the class
681         cl = self.get_class(classname)
683         # figure the property names to display
684         if len(args) > 1:
685             prop_names = args[1].split(',')
686             all_props = cl.getprops()
687             for spec in prop_names:
688                 if ':' in spec:
689                     try:
690                         propname, width = spec.split(':')
691                     except (ValueError, TypeError):
692                         raise UsageError, _('"%(spec)s" not name:width')%locals()
693                 else:
694                     propname = spec
695                 if not all_props.has_key(propname):
696                     raise UsageError, _('%(classname)s has no property '
697                         '"%(propname)s"')%locals()
698         else:
699             prop_names = cl.getprops().keys()
701         # now figure column widths
702         props = []
703         for spec in prop_names:
704             if ':' in spec:
705                 name, width = spec.split(':')
706                 props.append((name, int(width)))
707             else:
708                 props.append((spec, len(spec)))
710         # now display the heading
711         print ' '.join([name.capitalize().ljust(width) for name,width in props])
713         # and the table data
714         for nodeid in cl.list():
715             l = []
716             for name, width in props:
717                 if name != 'id':
718                     try:
719                         value = str(cl.get(nodeid, name))
720                     except KeyError:
721                         # we already checked if the property is valid - a
722                         # KeyError here means the node just doesn't have a
723                         # value for it
724                         value = ''
725                 else:
726                     value = str(nodeid)
727                 f = '%%-%ds'%width
728                 l.append(f%value[:width])
729             print ' '.join(l)
730         return 0
732     def do_history(self, args):
733         '''Usage: history designator
734         Show the history entries of a designator.
736         Lists the journal entries for the node identified by the designator.
737         '''
738         if len(args) < 1:
739             raise UsageError, _('Not enough arguments supplied')
740         try:
741             classname, nodeid = roundupdb.splitDesignator(args[0])
742         except roundupdb.DesignatorError, message:
743             raise UsageError, message
745         try:
746             print self.db.getclass(classname).history(nodeid)
747         except KeyError:
748             raise UsageError, _('no such class "%(classname)s"')%locals()
749         except IndexError:
750             raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
751         return 0
753     def do_commit(self, args):
754         '''Usage: commit
755         Commit all changes made to the database.
757         The changes made during an interactive session are not
758         automatically written to the database - they must be committed
759         using this command.
761         One-off commands on the command-line are automatically committed if
762         they are successful.
763         '''
764         self.db.commit()
765         return 0
767     def do_rollback(self, args):
768         '''Usage: rollback
769         Undo all changes that are pending commit to the database.
771         The changes made during an interactive session are not
772         automatically written to the database - they must be committed
773         manually. This command undoes all those changes, so a commit
774         immediately after would make no changes to the database.
775         '''
776         self.db.rollback()
777         return 0
779     def do_retire(self, args):
780         '''Usage: retire designator[,designator]*
781         Retire the node specified by designator.
783         This action indicates that a particular node is not to be retrieved by
784         the list or find commands, and its key value may be re-used.
785         '''
786         if len(args) < 1:
787             raise UsageError, _('Not enough arguments supplied')
788         designators = args[0].split(',')
789         for designator in designators:
790             try:
791                 classname, nodeid = roundupdb.splitDesignator(designator)
792             except roundupdb.DesignatorError, message:
793                 raise UsageError, message
794             try:
795                 self.db.getclass(classname).retire(nodeid)
796             except KeyError:
797                 raise UsageError, _('no such class "%(classname)s"')%locals()
798             except IndexError:
799                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
800         return 0
802     def do_export(self, args):
803         '''Usage: export class[,class] destination_dir
804         Export the database to tab-separated-value files.
806         This action exports the current data from the database into
807         tab-separated-value files that are placed in the nominated destination
808         directory. The journals are not exported.
809         '''
810         if len(args) < 2:
811             raise UsageError, _('Not enough arguments supplied')
812         classes = args[0].split(',')
813         dir = args[1]
815         # use the csv parser if we can - it's faster
816         if csv is not None:
817             p = csv.parser(field_sep=':')
819         # do all the classes specified
820         for classname in classes:
821             cl = self.get_class(classname)
822             f = open(os.path.join(dir, classname+'.csv'), 'w')
823             f.write(':'.join(cl.properties.keys()) + '\n')
825             # all nodes for this class
826             properties = cl.properties.items()
827             for nodeid in cl.list():
828                 l = []
829                 for prop, proptype in properties:
830                     value = cl.get(nodeid, prop)
831                     # convert data where needed
832                     if isinstance(proptype, hyperdb.Date):
833                         value = value.get_tuple()
834                     elif isinstance(proptype, hyperdb.Interval):
835                         value = value.get_tuple()
836                     elif isinstance(proptype, hyperdb.Password):
837                         value = str(value)
838                     l.append(repr(value))
840                 # now write
841                 if csv is not None:
842                    f.write(p.join(l) + '\n')
843                 else:
844                    # escape the individual entries to they're valid CSV
845                    m = []
846                    for entry in l:
847                       if '"' in entry:
848                           entry = '""'.join(entry.split('"'))
849                       if ':' in entry:
850                           entry = '"%s"'%entry
851                       m.append(entry)
852                    f.write(':'.join(m) + '\n')
853         return 0
855     def do_import(self, args):
856         '''Usage: import class file
857         Import the contents of the tab-separated-value file.
859         The file must define the same properties as the class (including having
860         a "header" line with those property names.) The new nodes are added to
861         the existing database - if you want to create a new database using the
862         imported data, then create a new database (or, tediously, retire all
863         the old data.)
864         '''
865         if len(args) < 2:
866             raise UsageError, _('Not enough arguments supplied')
867         if csv is None:
868             raise UsageError, \
869                 _('Sorry, you need the csv module to use this function.\n'
870                 'Get it from: http://www.object-craft.com.au/projects/csv/')
872         from roundup import hyperdb
874         # ensure that the properties and the CSV file headings match
875         classname = args[0]
876         cl = self.get_class(classname)
877         f = open(args[1])
878         p = csv.parser(field_sep=':')
879         file_props = p.parse(f.readline())
880         props = cl.properties.keys()
881         m = file_props[:]
882         m.sort()
883         props.sort()
884         if m != props:
885             raise UsageError, _('Import file doesn\'t define the same '
886                 'properties as "%(arg0)s".')%{'arg0': args[0]}
888         # loop through the file and create a node for each entry
889         n = range(len(props))
890         while 1:
891             line = f.readline()
892             if not line: break
894             # parse lines until we get a complete entry
895             while 1:
896                 l = p.parse(line)
897                 if l: break
898                 line = f.readline()
899                 if not line:
900                     raise ValueError, "Unexpected EOF during CSV parse"
902             # make the new node's property map
903             d = {}
904             for i in n:
905                 # Use eval to reverse the repr() used to output the CSV
906                 value = eval(l[i])
907                 # Figure the property for this column
908                 key = file_props[i]
909                 proptype = cl.properties[key]
910                 # Convert for property type
911                 if isinstance(proptype, hyperdb.Date):
912                     value = date.Date(value)
913                 elif isinstance(proptype, hyperdb.Interval):
914                     value = date.Interval(value)
915                 elif isinstance(proptype, hyperdb.Password):
916                     pwd = password.Password()
917                     pwd.unpack(value)
918                     value = pwd
919                 if value is not None:
920                     d[key] = value
922             # and create the new node
923             apply(cl.create, (), d)
924         return 0
926     def do_pack(self, args):
927         '''Usage: pack period | date
929 Remove journal entries older than a period of time specified or
930 before a certain date.
932 A period is specified using the suffixes "y", "m", and "d". The
933 suffix "w" (for "week") means 7 days.
935       "3y" means three years
936       "2y 1m" means two years and one month
937       "1m 25d" means one month and 25 days
938       "2w 3d" means two weeks and three days
940 Date format is "YYYY-MM-DD" eg:
941     2001-01-01
942     
943         '''
944         if len(args) <> 1:
945             raise UsageError, _('Not enough arguments supplied')
946         
947         # are we dealing with a period or a date
948         value = args[0]
949         date_re = re.compile(r'''
950               (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
951               (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
952               ''', re.VERBOSE)
953         m = date_re.match(value)
954         if not m:
955             raise ValueError, _('Invalid format')
956         m = m.groupdict()
957         if m['period']:
958             # TODO: need to fix date module.  one should be able to say
959             # pack_before = date.Date(". - %s"%value)
960             pack_before = date.Date(".") + date.Interval("- %s"%value)
961         elif m['date']:
962             pack_before = date.Date(value)
963         self.db.pack(pack_before)
964         return 0
966     def run_command(self, args):
967         '''Run a single command
968         '''
969         command = args[0]
971         # handle help now
972         if command == 'help':
973             if len(args)>1:
974                 self.do_help(args[1:])
975                 return 0
976             self.do_help(['help'])
977             return 0
978         if command == 'morehelp':
979             self.do_help(['help'])
980             self.help_commands()
981             self.help_all()
982             return 0
984         # figure what the command is
985         try:
986             functions = self.commands.get(command)
987         except KeyError:
988             # not a valid command
989             print _('Unknown command "%(command)s" ("help commands" for a '
990                 'list)')%locals()
991             return 1
993         # check for multiple matches
994         if len(functions) > 1:
995             print _('Multiple commands match "%(command)s": %(list)s')%{'command':
996                 command, 'list': ', '.join([i[0] for i in functions])}
997             return 1
998         command, function = functions[0]
1000         # make sure we have an instance_home
1001         while not self.instance_home:
1002             self.instance_home = raw_input(_('Enter instance home: ')).strip()
1004         # before we open the db, we may be doing an install or init
1005         if command == 'initialise':
1006             try:
1007                 return self.do_initialise(self.instance_home, args)
1008             except UsageError, message:
1009                 print _('Error: %(message)s')%locals()
1010                 return 1
1011         elif command == 'install':
1012             try:
1013                 return self.do_install(self.instance_home, args)
1014             except UsageError, message:
1015                 print _('Error: %(message)s')%locals()
1016                 return 1
1018         # get the instance
1019         try:
1020             instance = roundup.instance.open(self.instance_home)
1021         except ValueError, message:
1022             self.instance_home = ''
1023             print _("Error: Couldn't open instance: %(message)s")%locals()
1024             return 1
1026         # only open the database once!
1027         if not self.db:
1028             self.db = instance.open('admin')
1030         # do the command
1031         ret = 0
1032         try:
1033             ret = function(args[1:])
1034         except UsageError, message:
1035             print _('Error: %(message)s')%locals()
1036             print
1037             print function.__doc__
1038             ret = 1
1039         except:
1040             import traceback
1041             traceback.print_exc()
1042             ret = 1
1043         return ret
1045     def interactive(self):
1046         '''Run in an interactive mode
1047         '''
1048         print _('Roundup {version} ready for input.')
1049         print _('Type "help" for help.')
1050         try:
1051             import readline
1052         except ImportError:
1053             print _('Note: command history and editing not available')
1055         while 1:
1056             try:
1057                 command = raw_input(_('roundup> '))
1058             except EOFError:
1059                 print _('exit...')
1060                 break
1061             if not command: continue
1062             args = token.token_split(command)
1063             if not args: continue
1064             if args[0] in ('quit', 'exit'): break
1065             self.run_command(args)
1067         # exit.. check for transactions
1068         if self.db and self.db.transactions:
1069             commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1070             if commit and commit[0].lower() == 'y':
1071                 self.db.commit()
1072         return 0
1074     def main(self):
1075         try:
1076             opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1077         except getopt.GetoptError, e:
1078             self.usage(str(e))
1079             return 1
1081         # handle command-line args
1082         self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
1083         # TODO: reinstate the user/password stuff (-u arg too)
1084         name = password = ''
1085         if os.environ.has_key('ROUNDUP_LOGIN'):
1086             l = os.environ['ROUNDUP_LOGIN'].split(':')
1087             name = l[0]
1088             if len(l) > 1:
1089                 password = l[1]
1090         self.comma_sep = 0
1091         for opt, arg in opts:
1092             if opt == '-h':
1093                 self.usage()
1094                 return 0
1095             if opt == '-i':
1096                 self.instance_home = arg
1097             if opt == '-c':
1098                 self.comma_sep = 1
1100         # if no command - go interactive
1101         ret = 0
1102         if not args:
1103             self.interactive()
1104         else:
1105             ret = self.run_command(args)
1106             if self.db: self.db.commit()
1107         return ret
1110 if __name__ == '__main__':
1111     tool = AdminTool()
1112     sys.exit(tool.main())
1115 # $Log: not supported by cvs2svn $
1116 # Revision 1.13  2002/05/30 23:58:14  richard
1117 # oops
1119 # Revision 1.12  2002/05/26 09:04:42  richard
1120 # out by one in the init args
1122 # Revision 1.11  2002/05/23 01:14:20  richard
1123 #  . split instance initialisation into two steps, allowing config changes
1124 #    before the database is initialised.
1126 # Revision 1.10  2002/04/27 10:07:23  richard
1127 # minor fix to error message
1129 # Revision 1.9  2002/03/12 22:51:47  richard
1130 #  . #527416 ] roundup-admin uses undefined value
1131 #  . #527503 ] unfriendly init blowup when parent dir
1132 #    (also handles UsageError correctly now in init)
1134 # Revision 1.8  2002/02/27 03:28:21  richard
1135 # Ran it through pychecker, made fixes
1137 # Revision 1.7  2002/02/20 05:04:32  richard
1138 # Wasn't handling the cvs parser feeding properly.
1140 # Revision 1.6  2002/01/23 07:27:19  grubert
1141 #  . allow abbreviation of "help" in admin tool too.
1143 # Revision 1.5  2002/01/21 16:33:19  rochecompaan
1144 # You can now use the roundup-admin tool to pack the database
1146 # Revision 1.4  2002/01/14 06:51:09  richard
1147 #  . #503164 ] create and passwords
1149 # Revision 1.3  2002/01/08 05:26:32  rochecompaan
1150 # Missing "self" in props_from_args
1152 # Revision 1.2  2002/01/07 10:41:44  richard
1153 # #500140 ] AdminTool.get_class() returns nothing
1155 # Revision 1.1  2002/01/05 02:11:22  richard
1156 # I18N'ed roundup admin - and split the code off into a module so it can be used
1157 # elsewhere.
1158 # Big issue with this is the doc strings - that's the help. We're probably going to
1159 # have to switch to not use docstrings, which will suck a little :(
1163 # vim: set filetype=python ts=4 sw=4 et si