Code

. split instance initialisation into two steps, allowing config changes
[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.11 2002-05-23 01:14:20 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 [adminemail]]
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) > 0:
328             adminpw = args[0]
329         else:
330             adminpw = ''
331             confirm = 'x'
332             while adminpw != confirm:
333                 adminpw = getpass.getpass(_('Admin Password: '))
334                 confirm = getpass.getpass(_('       Confirm: '))
336         # email
337         if len(args) > 1:
338             adminemail = args[1]
339         else:
340             adminemail = ''
341             while not adminemail:
342                 adminemail = raw_input(_('   Admin Email: ')).strip()
344         # make sure the instance home is installed
345         if not os.path.exists(instance_home):
346             raise UsageError, _('Instance home does not exist')%locals()
347         if not os.path.exists(os.path.join(instance_home, 'html')):
348             raise UsageError, _('Instance has not been installed')%locals()
350         # is there already a database?
351         if os.path.exists(os.path.join(instance_home, 'db')):
352             print _('WARNING: The database is already initialised!')
353             print _('If you re-initialise it, you will lose all the data!')
354             ok = raw_input(_('Erase it? Y/[N]: ')).strip()
355             if ok.lower() != 'y':
356                 return 0
358             # nuke it
359             shutil.rmtree(os.path.join(instance_home, 'db'))
361         # GO
362         init.initialise(instance_home, adminpw)
364         return 0
367     def do_get(self, args):
368         '''Usage: get property designator[,designator]*
369         Get the given property of one or more designator(s).
371         Retrieves the property value of the nodes specified by the designators.
372         '''
373         if len(args) < 2:
374             raise UsageError, _('Not enough arguments supplied')
375         propname = args[0]
376         designators = args[1].split(',')
377         l = []
378         for designator in designators:
379             # decode the node designator
380             try:
381                 classname, nodeid = roundupdb.splitDesignator(designator)
382             except roundupdb.DesignatorError, message:
383                 raise UsageError, message
385             # get the class
386             cl = self.get_class(classname)
387             try:
388                 if self.comma_sep:
389                     l.append(cl.get(nodeid, propname))
390                 else:
391                     print cl.get(nodeid, propname)
392             except IndexError:
393                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
394             except KeyError:
395                 raise UsageError, _('no such %(classname)s property '
396                     '"%(propname)s"')%locals()
397         if self.comma_sep:
398             print ','.join(l)
399         return 0
402     def do_set(self, args):
403         '''Usage: set designator[,designator]* propname=value ...
404         Set the given property of one or more designator(s).
406         Sets the property to the value for all designators given.
407         '''
408         if len(args) < 2:
409             raise UsageError, _('Not enough arguments supplied')
410         from roundup import hyperdb
412         designators = args[0].split(',')
414         # get the props from the args
415         props = self.props_from_args(args[1:])
417         # now do the set for all the nodes
418         for designator in designators:
419             # decode the node designator
420             try:
421                 classname, nodeid = roundupdb.splitDesignator(designator)
422             except roundupdb.DesignatorError, message:
423                 raise UsageError, message
425             # get the class
426             cl = self.get_class(classname)
428             properties = cl.getprops()
429             for key, value in props.items():
430                 proptype =  properties[key]
431                 if isinstance(proptype, hyperdb.String):
432                     continue
433                 elif isinstance(proptype, hyperdb.Password):
434                     props[key] = password.Password(value)
435                 elif isinstance(proptype, hyperdb.Date):
436                     try:
437                         props[key] = date.Date(value)
438                     except ValueError, message:
439                         raise UsageError, '"%s": %s'%(value, message)
440                 elif isinstance(proptype, hyperdb.Interval):
441                     try:
442                         props[key] = date.Interval(value)
443                     except ValueError, message:
444                         raise UsageError, '"%s": %s'%(value, message)
445                 elif isinstance(proptype, hyperdb.Link):
446                     props[key] = value
447                 elif isinstance(proptype, hyperdb.Multilink):
448                     props[key] = value.split(',')
450             # try the set
451             try:
452                 apply(cl.set, (nodeid, ), props)
453             except (TypeError, IndexError, ValueError), message:
454                 raise UsageError, message
455         return 0
457     def do_find(self, args):
458         '''Usage: find classname propname=value ...
459         Find the nodes of the given class with a given link property value.
461         Find the nodes of the given class with a given link property value. The
462         value may be either the nodeid of the linked node, or its key value.
463         '''
464         if len(args) < 1:
465             raise UsageError, _('Not enough arguments supplied')
466         classname = args[0]
467         # get the class
468         cl = self.get_class(classname)
470         # handle the propname=value argument
471         props = self.props_from_args(args[1:])
473         # if the value isn't a number, look up the linked class to get the
474         # number
475         for propname, value in props.items():
476             num_re = re.compile('^\d+$')
477             if not num_re.match(value):
478                 # get the property
479                 try:
480                     property = cl.properties[propname]
481                 except KeyError:
482                     raise UsageError, _('%(classname)s has no property '
483                         '"%(propname)s"')%locals()
485                 # make sure it's a link
486                 if (not isinstance(property, hyperdb.Link) and not
487                         isinstance(property, hyperdb.Multilink)):
488                     raise UsageError, _('You may only "find" link properties')
490                 # get the linked-to class and look up the key property
491                 link_class = self.db.getclass(property.classname)
492                 try:
493                     props[propname] = link_class.lookup(value)
494                 except TypeError:
495                     raise UsageError, _('%(classname)s has no key property"')%{
496                         'classname': link_class.classname}
497                 except KeyError:
498                     raise UsageError, _('%(classname)s has no entry "%(propname)s"')%{
499                         'classname': link_class.classname, 'propname': propname}
501         # now do the find 
502         try:
503             if self.comma_sep:
504                 print ','.join(apply(cl.find, (), props))
505             else:
506                 print apply(cl.find, (), props)
507         except KeyError:
508             raise UsageError, _('%(classname)s has no property '
509                 '"%(propname)s"')%locals()
510         except (ValueError, TypeError), message:
511             raise UsageError, message
512         return 0
514     def do_specification(self, args):
515         '''Usage: specification classname
516         Show the properties for a classname.
518         This lists the properties for a given class.
519         '''
520         if len(args) < 1:
521             raise UsageError, _('Not enough arguments supplied')
522         classname = args[0]
523         # get the class
524         cl = self.get_class(classname)
526         # get the key property
527         keyprop = cl.getkey()
528         for key, value in cl.properties.items():
529             if keyprop == key:
530                 print _('%(key)s: %(value)s (key property)')%locals()
531             else:
532                 print _('%(key)s: %(value)s')%locals()
534     def do_display(self, args):
535         '''Usage: display designator
536         Show the property values for the given node.
538         This lists the properties and their associated values for the given
539         node.
540         '''
541         if len(args) < 1:
542             raise UsageError, _('Not enough arguments supplied')
544         # decode the node designator
545         try:
546             classname, nodeid = roundupdb.splitDesignator(args[0])
547         except roundupdb.DesignatorError, message:
548             raise UsageError, message
550         # get the class
551         cl = self.get_class(classname)
553         # display the values
554         for key in cl.properties.keys():
555             value = cl.get(nodeid, key)
556             print _('%(key)s: %(value)s')%locals()
558     def do_create(self, args):
559         '''Usage: create classname property=value ...
560         Create a new entry of a given class.
562         This creates a new entry of the given class using the property
563         name=value arguments provided on the command line after the "create"
564         command.
565         '''
566         if len(args) < 1:
567             raise UsageError, _('Not enough arguments supplied')
568         from roundup import hyperdb
570         classname = args[0]
572         # get the class
573         cl = self.get_class(classname)
575         # now do a create
576         props = {}
577         properties = cl.getprops(protected = 0)
578         if len(args) == 1:
579             # ask for the properties
580             for key, value in properties.items():
581                 if key == 'id': continue
582                 name = value.__class__.__name__
583                 if isinstance(value , hyperdb.Password):
584                     again = None
585                     while value != again:
586                         value = getpass.getpass(_('%(propname)s (Password): ')%{
587                             'propname': key.capitalize()})
588                         again = getpass.getpass(_('   %(propname)s (Again): ')%{
589                             'propname': key.capitalize()})
590                         if value != again: print _('Sorry, try again...')
591                     if value:
592                         props[key] = value
593                 else:
594                     value = raw_input(_('%(propname)s (%(proptype)s): ')%{
595                         'propname': key.capitalize(), 'proptype': name})
596                     if value:
597                         props[key] = value
598         else:
599             props = self.props_from_args(args[1:])
601         # convert types
602         for propname, value in props.items():
603             # get the property
604             try:
605                 proptype = properties[propname]
606             except KeyError:
607                 raise UsageError, _('%(classname)s has no property '
608                     '"%(propname)s"')%locals()
610             if isinstance(proptype, hyperdb.Date):
611                 try:
612                     props[propname] = date.Date(value)
613                 except ValueError, message:
614                     raise UsageError, _('"%(value)s": %(message)s')%locals()
615             elif isinstance(proptype, hyperdb.Interval):
616                 try:
617                     props[propname] = date.Interval(value)
618                 except ValueError, message:
619                     raise UsageError, _('"%(value)s": %(message)s')%locals()
620             elif isinstance(proptype, hyperdb.Password):
621                 props[propname] = password.Password(value)
622             elif isinstance(proptype, hyperdb.Multilink):
623                 props[propname] = value.split(',')
625         # check for the key property
626         propname = cl.getkey()
627         if propname and not props.has_key(propname):
628             raise UsageError, _('you must provide the "%(propname)s" '
629                 'property.')%locals()
631         # do the actual create
632         try:
633             print apply(cl.create, (), props)
634         except (TypeError, IndexError, ValueError), message:
635             raise UsageError, message
636         return 0
638     def do_list(self, args):
639         '''Usage: list classname [property]
640         List the instances of a class.
642         Lists all instances of the given class. If the property is not
643         specified, the  "label" property is used. The label property is tried
644         in order: the key, "name", "title" and then the first property,
645         alphabetically.
646         '''
647         if len(args) < 1:
648             raise UsageError, _('Not enough arguments supplied')
649         classname = args[0]
651         # get the class
652         cl = self.get_class(classname)
654         # figure the property
655         if len(args) > 1:
656             propname = args[1]
657         else:
658             propname = cl.labelprop()
660         if self.comma_sep:
661             print ','.join(cl.list())
662         else:
663             for nodeid in cl.list():
664                 try:
665                     value = cl.get(nodeid, propname)
666                 except KeyError:
667                     raise UsageError, _('%(classname)s has no property '
668                         '"%(propname)s"')%locals()
669                 print _('%(nodeid)4s: %(value)s')%locals()
670         return 0
672     def do_table(self, args):
673         '''Usage: table classname [property[,property]*]
674         List the instances of a class in tabular form.
676         Lists all instances of the given class. If the properties are not
677         specified, all properties are displayed. By default, the column widths
678         are the width of the property names. The width may be explicitly defined
679         by defining the property as "name:width". For example::
680           roundup> table priority id,name:10
681           Id Name
682           1  fatal-bug 
683           2  bug       
684           3  usability 
685           4  feature   
686         '''
687         if len(args) < 1:
688             raise UsageError, _('Not enough arguments supplied')
689         classname = args[0]
691         # get the class
692         cl = self.get_class(classname)
694         # figure the property names to display
695         if len(args) > 1:
696             prop_names = args[1].split(',')
697             all_props = cl.getprops()
698             for spec in prop_names:
699                 if ':' in spec:
700                     try:
701                         propname, width = spec.split(':')
702                     except (ValueError, TypeError):
703                         raise UsageError, _('"%(spec)s" not name:width')%locals()
704                 else:
705                     propname = spec
706                 if not all_props.has_key(propname):
707                     raise UsageError, _('%(classname)s has no property '
708                         '"%(propname)s"')%locals()
709         else:
710             prop_names = cl.getprops().keys()
712         # now figure column widths
713         props = []
714         for spec in prop_names:
715             if ':' in spec:
716                 name, width = spec.split(':')
717                 props.append((name, int(width)))
718             else:
719                 props.append((spec, len(spec)))
721         # now display the heading
722         print ' '.join([name.capitalize().ljust(width) for name,width in props])
724         # and the table data
725         for nodeid in cl.list():
726             l = []
727             for name, width in props:
728                 if name != 'id':
729                     try:
730                         value = str(cl.get(nodeid, name))
731                     except KeyError:
732                         # we already checked if the property is valid - a
733                         # KeyError here means the node just doesn't have a
734                         # value for it
735                         value = ''
736                 else:
737                     value = str(nodeid)
738                 f = '%%-%ds'%width
739                 l.append(f%value[:width])
740             print ' '.join(l)
741         return 0
743     def do_history(self, args):
744         '''Usage: history designator
745         Show the history entries of a designator.
747         Lists the journal entries for the node identified by the designator.
748         '''
749         if len(args) < 1:
750             raise UsageError, _('Not enough arguments supplied')
751         try:
752             classname, nodeid = roundupdb.splitDesignator(args[0])
753         except roundupdb.DesignatorError, message:
754             raise UsageError, message
756         try:
757             print self.db.getclass(classname).history(nodeid)
758         except KeyError:
759             raise UsageError, _('no such class "%(classname)s"')%locals()
760         except IndexError:
761             raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
762         return 0
764     def do_commit(self, args):
765         '''Usage: commit
766         Commit all changes made to the database.
768         The changes made during an interactive session are not
769         automatically written to the database - they must be committed
770         using this command.
772         One-off commands on the command-line are automatically committed if
773         they are successful.
774         '''
775         self.db.commit()
776         return 0
778     def do_rollback(self, args):
779         '''Usage: rollback
780         Undo all changes that are pending commit to the database.
782         The changes made during an interactive session are not
783         automatically written to the database - they must be committed
784         manually. This command undoes all those changes, so a commit
785         immediately after would make no changes to the database.
786         '''
787         self.db.rollback()
788         return 0
790     def do_retire(self, args):
791         '''Usage: retire designator[,designator]*
792         Retire the node specified by designator.
794         This action indicates that a particular node is not to be retrieved by
795         the list or find commands, and its key value may be re-used.
796         '''
797         if len(args) < 1:
798             raise UsageError, _('Not enough arguments supplied')
799         designators = args[0].split(',')
800         for designator in designators:
801             try:
802                 classname, nodeid = roundupdb.splitDesignator(designator)
803             except roundupdb.DesignatorError, message:
804                 raise UsageError, message
805             try:
806                 self.db.getclass(classname).retire(nodeid)
807             except KeyError:
808                 raise UsageError, _('no such class "%(classname)s"')%locals()
809             except IndexError:
810                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
811         return 0
813     def do_export(self, args):
814         '''Usage: export class[,class] destination_dir
815         Export the database to tab-separated-value files.
817         This action exports the current data from the database into
818         tab-separated-value files that are placed in the nominated destination
819         directory. The journals are not exported.
820         '''
821         if len(args) < 2:
822             raise UsageError, _('Not enough arguments supplied')
823         classes = args[0].split(',')
824         dir = args[1]
826         # use the csv parser if we can - it's faster
827         if csv is not None:
828             p = csv.parser(field_sep=':')
830         # do all the classes specified
831         for classname in classes:
832             cl = self.get_class(classname)
833             f = open(os.path.join(dir, classname+'.csv'), 'w')
834             f.write(':'.join(cl.properties.keys()) + '\n')
836             # all nodes for this class
837             properties = cl.properties.items()
838             for nodeid in cl.list():
839                 l = []
840                 for prop, proptype in properties:
841                     value = cl.get(nodeid, prop)
842                     # convert data where needed
843                     if isinstance(proptype, hyperdb.Date):
844                         value = value.get_tuple()
845                     elif isinstance(proptype, hyperdb.Interval):
846                         value = value.get_tuple()
847                     elif isinstance(proptype, hyperdb.Password):
848                         value = str(value)
849                     l.append(repr(value))
851                 # now write
852                 if csv is not None:
853                    f.write(p.join(l) + '\n')
854                 else:
855                    # escape the individual entries to they're valid CSV
856                    m = []
857                    for entry in l:
858                       if '"' in entry:
859                           entry = '""'.join(entry.split('"'))
860                       if ':' in entry:
861                           entry = '"%s"'%entry
862                       m.append(entry)
863                    f.write(':'.join(m) + '\n')
864         return 0
866     def do_import(self, args):
867         '''Usage: import class file
868         Import the contents of the tab-separated-value file.
870         The file must define the same properties as the class (including having
871         a "header" line with those property names.) The new nodes are added to
872         the existing database - if you want to create a new database using the
873         imported data, then create a new database (or, tediously, retire all
874         the old data.)
875         '''
876         if len(args) < 2:
877             raise UsageError, _('Not enough arguments supplied')
878         if csv is None:
879             raise UsageError, \
880                 _('Sorry, you need the csv module to use this function.\n'
881                 'Get it from: http://www.object-craft.com.au/projects/csv/')
883         from roundup import hyperdb
885         # ensure that the properties and the CSV file headings match
886         classname = args[0]
887         cl = self.get_class(classname)
888         f = open(args[1])
889         p = csv.parser(field_sep=':')
890         file_props = p.parse(f.readline())
891         props = cl.properties.keys()
892         m = file_props[:]
893         m.sort()
894         props.sort()
895         if m != props:
896             raise UsageError, _('Import file doesn\'t define the same '
897                 'properties as "%(arg0)s".')%{'arg0': args[0]}
899         # loop through the file and create a node for each entry
900         n = range(len(props))
901         while 1:
902             line = f.readline()
903             if not line: break
905             # parse lines until we get a complete entry
906             while 1:
907                 l = p.parse(line)
908                 if l: break
909                 line = f.readline()
910                 if not line:
911                     raise ValueError, "Unexpected EOF during CSV parse"
913             # make the new node's property map
914             d = {}
915             for i in n:
916                 # Use eval to reverse the repr() used to output the CSV
917                 value = eval(l[i])
918                 # Figure the property for this column
919                 key = file_props[i]
920                 proptype = cl.properties[key]
921                 # Convert for property type
922                 if isinstance(proptype, hyperdb.Date):
923                     value = date.Date(value)
924                 elif isinstance(proptype, hyperdb.Interval):
925                     value = date.Interval(value)
926                 elif isinstance(proptype, hyperdb.Password):
927                     pwd = password.Password()
928                     pwd.unpack(value)
929                     value = pwd
930                 if value is not None:
931                     d[key] = value
933             # and create the new node
934             apply(cl.create, (), d)
935         return 0
937     def do_pack(self, args):
938         '''Usage: pack period | date
940 Remove journal entries older than a period of time specified or
941 before a certain date.
943 A period is specified using the suffixes "y", "m", and "d". The
944 suffix "w" (for "week") means 7 days.
946       "3y" means three years
947       "2y 1m" means two years and one month
948       "1m 25d" means one month and 25 days
949       "2w 3d" means two weeks and three days
951 Date format is "YYYY-MM-DD" eg:
952     2001-01-01
953     
954         '''
955         if len(args) <> 1:
956             raise UsageError, _('Not enough arguments supplied')
957         
958         # are we dealing with a period or a date
959         value = args[0]
960         date_re = re.compile(r'''
961               (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
962               (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
963               ''', re.VERBOSE)
964         m = date_re.match(value)
965         if not m:
966             raise ValueError, _('Invalid format')
967         m = m.groupdict()
968         if m['period']:
969             # TODO: need to fix date module.  one should be able to say
970             # pack_before = date.Date(". - %s"%value)
971             pack_before = date.Date(".") + date.Interval("- %s"%value)
972         elif m['date']:
973             pack_before = date.Date(value)
974         self.db.pack(pack_before)
975         return 0
977     def run_command(self, args):
978         '''Run a single command
979         '''
980         command = args[0]
982         # handle help now
983         if command == 'help':
984             if len(args)>1:
985                 self.do_help(args[1:])
986                 return 0
987             self.do_help(['help'])
988             return 0
989         if command == 'morehelp':
990             self.do_help(['help'])
991             self.help_commands()
992             self.help_all()
993             return 0
995         # figure what the command is
996         try:
997             functions = self.commands.get(command)
998         except KeyError:
999             # not a valid command
1000             print _('Unknown command "%(command)s" ("help commands" for a '
1001                 'list)')%locals()
1002             return 1
1004         # check for multiple matches
1005         if len(functions) > 1:
1006             print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1007                 command, 'list': ', '.join([i[0] for i in functions])}
1008             return 1
1009         command, function = functions[0]
1011         # make sure we have an instance_home
1012         while not self.instance_home:
1013             self.instance_home = raw_input(_('Enter instance home: ')).strip()
1015         # before we open the db, we may be doing an install or init
1016         if command == 'initialise':
1017             try:
1018                 return self.do_initialise(self.instance_home, args)
1019             except UsageError, message:
1020                 print _('Error: %(message)s')%locals()
1021                 return 1
1022         elif command == 'install':
1023             try:
1024                 return self.do_install(self.instance_home, args)
1025             except UsageError, message:
1026                 print _('Error: %(message)s')%locals()
1027                 return 1
1029         # get the instance
1030         try:
1031             instance = roundup.instance.open(self.instance_home)
1032         except ValueError, message:
1033             self.instance_home = ''
1034             print _("Error: Couldn't open instance: %(message)s")%locals()
1035             return 1
1037         # only open the database once!
1038         if not self.db:
1039             self.db = instance.open('admin')
1041         # do the command
1042         ret = 0
1043         try:
1044             ret = function(args[1:])
1045         except UsageError, message:
1046             print _('Error: %(message)s')%locals()
1047             print
1048             print function.__doc__
1049             ret = 1
1050         except:
1051             import traceback
1052             traceback.print_exc()
1053             ret = 1
1054         return ret
1056     def interactive(self):
1057         '''Run in an interactive mode
1058         '''
1059         print _('Roundup {version} ready for input.')
1060         print _('Type "help" for help.')
1061         try:
1062             import readline
1063         except ImportError:
1064             print _('Note: command history and editing not available')
1066         while 1:
1067             try:
1068                 command = raw_input(_('roundup> '))
1069             except EOFError:
1070                 print _('exit...')
1071                 break
1072             if not command: continue
1073             args = token.token_split(command)
1074             if not args: continue
1075             if args[0] in ('quit', 'exit'): break
1076             self.run_command(args)
1078         # exit.. check for transactions
1079         if self.db and self.db.transactions:
1080             commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1081             if commit and commit[0].lower() == 'y':
1082                 self.db.commit()
1083         return 0
1085     def main(self):
1086         try:
1087             opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1088         except getopt.GetoptError, e:
1089             self.usage(str(e))
1090             return 1
1092         # handle command-line args
1093         self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
1094         # TODO: reinstate the user/password stuff (-u arg too)
1095         name = password = ''
1096         if os.environ.has_key('ROUNDUP_LOGIN'):
1097             l = os.environ['ROUNDUP_LOGIN'].split(':')
1098             name = l[0]
1099             if len(l) > 1:
1100                 password = l[1]
1101         self.comma_sep = 0
1102         for opt, arg in opts:
1103             if opt == '-h':
1104                 self.usage()
1105                 return 0
1106             if opt == '-i':
1107                 self.instance_home = arg
1108             if opt == '-c':
1109                 self.comma_sep = 1
1111         # if no command - go interactive
1112         ret = 0
1113         if not args:
1114             self.interactive()
1115         else:
1116             ret = self.run_command(args)
1117             if self.db: self.db.commit()
1118         return ret
1121 if __name__ == '__main__':
1122     tool = AdminTool()
1123     sys.exit(tool.main())
1126 # $Log: not supported by cvs2svn $
1127 # Revision 1.10  2002/04/27 10:07:23  richard
1128 # minor fix to error message
1130 # Revision 1.9  2002/03/12 22:51:47  richard
1131 #  . #527416 ] roundup-admin uses undefined value
1132 #  . #527503 ] unfriendly init blowup when parent dir
1133 #    (also handles UsageError correctly now in init)
1135 # Revision 1.8  2002/02/27 03:28:21  richard
1136 # Ran it through pychecker, made fixes
1138 # Revision 1.7  2002/02/20 05:04:32  richard
1139 # Wasn't handling the cvs parser feeding properly.
1141 # Revision 1.6  2002/01/23 07:27:19  grubert
1142 #  . allow abbreviation of "help" in admin tool too.
1144 # Revision 1.5  2002/01/21 16:33:19  rochecompaan
1145 # You can now use the roundup-admin tool to pack the database
1147 # Revision 1.4  2002/01/14 06:51:09  richard
1148 #  . #503164 ] create and passwords
1150 # Revision 1.3  2002/01/08 05:26:32  rochecompaan
1151 # Missing "self" in props_from_args
1153 # Revision 1.2  2002/01/07 10:41:44  richard
1154 # #500140 ] AdminTool.get_class() returns nothing
1156 # Revision 1.1  2002/01/05 02:11:22  richard
1157 # I18N'ed roundup admin - and split the code off into a module so it can be used
1158 # elsewhere.
1159 # Big issue with this is the doc strings - that's the help. We're probably going to
1160 # have to switch to not use docstrings, which will suck a little :(
1164 # vim: set filetype=python ts=4 sw=4 et si