Code

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