Code

5e2ac94d01ed76efea8a1ffa02ffdfd0d807a1b8
[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.10 2002-04-27 10:07:23 richard Exp $
21 import sys, os, getpass, getopt, re, UserDict, shlex
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_initialise(self, instance_home, args):
253         '''Usage: initialise [template [backend [admin password]]]
254         Initialise 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         See also initopts help.
262         '''
263         if len(args) < 1:
264             raise UsageError, _('Not enough arguments supplied')
266         # make sure the instance home can be created
267         parent = os.path.split(instance_home)[0]
268         if not os.path.exists(parent):
269             raise UsageError, _('Instance home parent directory "%(parent)s"'
270                 ' does not exist')%locals()
272         # select template
273         import roundup.templates
274         templates = roundup.templates.listTemplates()
275         template = len(args) > 1 and args[1] or ''
276         if template not in templates:
277             print _('Templates:'), ', '.join(templates)
278         while template not in templates:
279             template = raw_input(_('Select template [classic]: ')).strip()
280             if not template:
281                 template = 'classic'
283         # select hyperdb backend
284         import roundup.backends
285         backends = roundup.backends.__all__
286         backend = len(args) > 2 and args[2] or ''
287         if backend not in backends:
288             print _('Back ends:'), ', '.join(backends)
289         while backend not in backends:
290             backend = raw_input(_('Select backend [anydbm]: ')).strip()
291             if not backend:
292                 backend = 'anydbm'
294         # admin password
295         if len(args) > 3:
296             adminpw = confirm = args[3]
297         else:
298             adminpw = ''
299             confirm = 'x'
300         while adminpw != confirm:
301             adminpw = getpass.getpass(_('Admin Password: '))
302             confirm = getpass.getpass(_('       Confirm: '))
304         # create!
305         init.init(instance_home, template, backend, adminpw)
307         return 0
310     def do_get(self, args):
311         '''Usage: get property designator[,designator]*
312         Get the given property of one or more designator(s).
314         Retrieves the property value of the nodes specified by the designators.
315         '''
316         if len(args) < 2:
317             raise UsageError, _('Not enough arguments supplied')
318         propname = args[0]
319         designators = args[1].split(',')
320         l = []
321         for designator in designators:
322             # decode the node designator
323             try:
324                 classname, nodeid = roundupdb.splitDesignator(designator)
325             except roundupdb.DesignatorError, message:
326                 raise UsageError, message
328             # get the class
329             cl = self.get_class(classname)
330             try:
331                 if self.comma_sep:
332                     l.append(cl.get(nodeid, propname))
333                 else:
334                     print cl.get(nodeid, propname)
335             except IndexError:
336                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
337             except KeyError:
338                 raise UsageError, _('no such %(classname)s property '
339                     '"%(propname)s"')%locals()
340         if self.comma_sep:
341             print ','.join(l)
342         return 0
345     def do_set(self, args):
346         '''Usage: set designator[,designator]* propname=value ...
347         Set the given property of one or more designator(s).
349         Sets the property to the value for all designators given.
350         '''
351         if len(args) < 2:
352             raise UsageError, _('Not enough arguments supplied')
353         from roundup import hyperdb
355         designators = args[0].split(',')
357         # get the props from the args
358         props = self.props_from_args(args[1:])
360         # now do the set for all the nodes
361         for designator in designators:
362             # decode the node designator
363             try:
364                 classname, nodeid = roundupdb.splitDesignator(designator)
365             except roundupdb.DesignatorError, message:
366                 raise UsageError, message
368             # get the class
369             cl = self.get_class(classname)
371             properties = cl.getprops()
372             for key, value in props.items():
373                 proptype =  properties[key]
374                 if isinstance(proptype, hyperdb.String):
375                     continue
376                 elif isinstance(proptype, hyperdb.Password):
377                     props[key] = password.Password(value)
378                 elif isinstance(proptype, hyperdb.Date):
379                     try:
380                         props[key] = date.Date(value)
381                     except ValueError, message:
382                         raise UsageError, '"%s": %s'%(value, message)
383                 elif isinstance(proptype, hyperdb.Interval):
384                     try:
385                         props[key] = date.Interval(value)
386                     except ValueError, message:
387                         raise UsageError, '"%s": %s'%(value, message)
388                 elif isinstance(proptype, hyperdb.Link):
389                     props[key] = value
390                 elif isinstance(proptype, hyperdb.Multilink):
391                     props[key] = value.split(',')
393             # try the set
394             try:
395                 apply(cl.set, (nodeid, ), props)
396             except (TypeError, IndexError, ValueError), message:
397                 raise UsageError, message
398         return 0
400     def do_find(self, args):
401         '''Usage: find classname propname=value ...
402         Find the nodes of the given class with a given link property value.
404         Find the nodes of the given class with a given link property value. The
405         value may be either the nodeid of the linked node, or its key value.
406         '''
407         if len(args) < 1:
408             raise UsageError, _('Not enough arguments supplied')
409         classname = args[0]
410         # get the class
411         cl = self.get_class(classname)
413         # handle the propname=value argument
414         props = self.props_from_args(args[1:])
416         # if the value isn't a number, look up the linked class to get the
417         # number
418         for propname, value in props.items():
419             num_re = re.compile('^\d+$')
420             if not num_re.match(value):
421                 # get the property
422                 try:
423                     property = cl.properties[propname]
424                 except KeyError:
425                     raise UsageError, _('%(classname)s has no property '
426                         '"%(propname)s"')%locals()
428                 # make sure it's a link
429                 if (not isinstance(property, hyperdb.Link) and not
430                         isinstance(property, hyperdb.Multilink)):
431                     raise UsageError, _('You may only "find" link properties')
433                 # get the linked-to class and look up the key property
434                 link_class = self.db.getclass(property.classname)
435                 try:
436                     props[propname] = link_class.lookup(value)
437                 except TypeError:
438                     raise UsageError, _('%(classname)s has no key property"')%{
439                         'classname': link_class.classname}
440                 except KeyError:
441                     raise UsageError, _('%(classname)s has no entry "%(propname)s"')%{
442                         'classname': link_class.classname, 'propname': propname}
444         # now do the find 
445         try:
446             if self.comma_sep:
447                 print ','.join(apply(cl.find, (), props))
448             else:
449                 print apply(cl.find, (), props)
450         except KeyError:
451             raise UsageError, _('%(classname)s has no property '
452                 '"%(propname)s"')%locals()
453         except (ValueError, TypeError), message:
454             raise UsageError, message
455         return 0
457     def do_specification(self, args):
458         '''Usage: specification classname
459         Show the properties for a classname.
461         This lists the properties for a given class.
462         '''
463         if len(args) < 1:
464             raise UsageError, _('Not enough arguments supplied')
465         classname = args[0]
466         # get the class
467         cl = self.get_class(classname)
469         # get the key property
470         keyprop = cl.getkey()
471         for key, value in cl.properties.items():
472             if keyprop == key:
473                 print _('%(key)s: %(value)s (key property)')%locals()
474             else:
475                 print _('%(key)s: %(value)s')%locals()
477     def do_display(self, args):
478         '''Usage: display designator
479         Show the property values for the given node.
481         This lists the properties and their associated values for the given
482         node.
483         '''
484         if len(args) < 1:
485             raise UsageError, _('Not enough arguments supplied')
487         # decode the node designator
488         try:
489             classname, nodeid = roundupdb.splitDesignator(args[0])
490         except roundupdb.DesignatorError, message:
491             raise UsageError, message
493         # get the class
494         cl = self.get_class(classname)
496         # display the values
497         for key in cl.properties.keys():
498             value = cl.get(nodeid, key)
499             print _('%(key)s: %(value)s')%locals()
501     def do_create(self, args):
502         '''Usage: create classname property=value ...
503         Create a new entry of a given class.
505         This creates a new entry of the given class using the property
506         name=value arguments provided on the command line after the "create"
507         command.
508         '''
509         if len(args) < 1:
510             raise UsageError, _('Not enough arguments supplied')
511         from roundup import hyperdb
513         classname = args[0]
515         # get the class
516         cl = self.get_class(classname)
518         # now do a create
519         props = {}
520         properties = cl.getprops(protected = 0)
521         if len(args) == 1:
522             # ask for the properties
523             for key, value in properties.items():
524                 if key == 'id': continue
525                 name = value.__class__.__name__
526                 if isinstance(value , hyperdb.Password):
527                     again = None
528                     while value != again:
529                         value = getpass.getpass(_('%(propname)s (Password): ')%{
530                             'propname': key.capitalize()})
531                         again = getpass.getpass(_('   %(propname)s (Again): ')%{
532                             'propname': key.capitalize()})
533                         if value != again: print _('Sorry, try again...')
534                     if value:
535                         props[key] = value
536                 else:
537                     value = raw_input(_('%(propname)s (%(proptype)s): ')%{
538                         'propname': key.capitalize(), 'proptype': name})
539                     if value:
540                         props[key] = value
541         else:
542             props = self.props_from_args(args[1:])
544         # convert types
545         for propname, value in props.items():
546             # get the property
547             try:
548                 proptype = properties[propname]
549             except KeyError:
550                 raise UsageError, _('%(classname)s has no property '
551                     '"%(propname)s"')%locals()
553             if isinstance(proptype, hyperdb.Date):
554                 try:
555                     props[propname] = date.Date(value)
556                 except ValueError, message:
557                     raise UsageError, _('"%(value)s": %(message)s')%locals()
558             elif isinstance(proptype, hyperdb.Interval):
559                 try:
560                     props[propname] = date.Interval(value)
561                 except ValueError, message:
562                     raise UsageError, _('"%(value)s": %(message)s')%locals()
563             elif isinstance(proptype, hyperdb.Password):
564                 props[propname] = password.Password(value)
565             elif isinstance(proptype, hyperdb.Multilink):
566                 props[propname] = value.split(',')
568         # check for the key property
569         propname = cl.getkey()
570         if propname and not props.has_key(propname):
571             raise UsageError, _('you must provide the "%(propname)s" '
572                 'property.')%locals()
574         # do the actual create
575         try:
576             print apply(cl.create, (), props)
577         except (TypeError, IndexError, ValueError), message:
578             raise UsageError, message
579         return 0
581     def do_list(self, args):
582         '''Usage: list classname [property]
583         List the instances of a class.
585         Lists all instances of the given class. If the property is not
586         specified, the  "label" property is used. The label property is tried
587         in order: the key, "name", "title" and then the first property,
588         alphabetically.
589         '''
590         if len(args) < 1:
591             raise UsageError, _('Not enough arguments supplied')
592         classname = args[0]
594         # get the class
595         cl = self.get_class(classname)
597         # figure the property
598         if len(args) > 1:
599             propname = args[1]
600         else:
601             propname = cl.labelprop()
603         if self.comma_sep:
604             print ','.join(cl.list())
605         else:
606             for nodeid in cl.list():
607                 try:
608                     value = cl.get(nodeid, propname)
609                 except KeyError:
610                     raise UsageError, _('%(classname)s has no property '
611                         '"%(propname)s"')%locals()
612                 print _('%(nodeid)4s: %(value)s')%locals()
613         return 0
615     def do_table(self, args):
616         '''Usage: table classname [property[,property]*]
617         List the instances of a class in tabular form.
619         Lists all instances of the given class. If the properties are not
620         specified, all properties are displayed. By default, the column widths
621         are the width of the property names. The width may be explicitly defined
622         by defining the property as "name:width". For example::
623           roundup> table priority id,name:10
624           Id Name
625           1  fatal-bug 
626           2  bug       
627           3  usability 
628           4  feature   
629         '''
630         if len(args) < 1:
631             raise UsageError, _('Not enough arguments supplied')
632         classname = args[0]
634         # get the class
635         cl = self.get_class(classname)
637         # figure the property names to display
638         if len(args) > 1:
639             prop_names = args[1].split(',')
640             all_props = cl.getprops()
641             for spec in prop_names:
642                 if ':' in spec:
643                     try:
644                         propname, width = spec.split(':')
645                     except (ValueError, TypeError):
646                         raise UsageError, _('"%(spec)s" not name:width')%locals()
647                 else:
648                     propname = spec
649                 if not all_props.has_key(propname):
650                     raise UsageError, _('%(classname)s has no property '
651                         '"%(propname)s"')%locals()
652         else:
653             prop_names = cl.getprops().keys()
655         # now figure column widths
656         props = []
657         for spec in prop_names:
658             if ':' in spec:
659                 name, width = spec.split(':')
660                 props.append((name, int(width)))
661             else:
662                 props.append((spec, len(spec)))
664         # now display the heading
665         print ' '.join([name.capitalize().ljust(width) for name,width in props])
667         # and the table data
668         for nodeid in cl.list():
669             l = []
670             for name, width in props:
671                 if name != 'id':
672                     try:
673                         value = str(cl.get(nodeid, name))
674                     except KeyError:
675                         # we already checked if the property is valid - a
676                         # KeyError here means the node just doesn't have a
677                         # value for it
678                         value = ''
679                 else:
680                     value = str(nodeid)
681                 f = '%%-%ds'%width
682                 l.append(f%value[:width])
683             print ' '.join(l)
684         return 0
686     def do_history(self, args):
687         '''Usage: history designator
688         Show the history entries of a designator.
690         Lists the journal entries for the node identified by the designator.
691         '''
692         if len(args) < 1:
693             raise UsageError, _('Not enough arguments supplied')
694         try:
695             classname, nodeid = roundupdb.splitDesignator(args[0])
696         except roundupdb.DesignatorError, message:
697             raise UsageError, message
699         try:
700             print self.db.getclass(classname).history(nodeid)
701         except KeyError:
702             raise UsageError, _('no such class "%(classname)s"')%locals()
703         except IndexError:
704             raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
705         return 0
707     def do_commit(self, args):
708         '''Usage: commit
709         Commit all changes made to the database.
711         The changes made during an interactive session are not
712         automatically written to the database - they must be committed
713         using this command.
715         One-off commands on the command-line are automatically committed if
716         they are successful.
717         '''
718         self.db.commit()
719         return 0
721     def do_rollback(self, args):
722         '''Usage: rollback
723         Undo all changes that are pending commit to the database.
725         The changes made during an interactive session are not
726         automatically written to the database - they must be committed
727         manually. This command undoes all those changes, so a commit
728         immediately after would make no changes to the database.
729         '''
730         self.db.rollback()
731         return 0
733     def do_retire(self, args):
734         '''Usage: retire designator[,designator]*
735         Retire the node specified by designator.
737         This action indicates that a particular node is not to be retrieved by
738         the list or find commands, and its key value may be re-used.
739         '''
740         if len(args) < 1:
741             raise UsageError, _('Not enough arguments supplied')
742         designators = args[0].split(',')
743         for designator in designators:
744             try:
745                 classname, nodeid = roundupdb.splitDesignator(designator)
746             except roundupdb.DesignatorError, message:
747                 raise UsageError, message
748             try:
749                 self.db.getclass(classname).retire(nodeid)
750             except KeyError:
751                 raise UsageError, _('no such class "%(classname)s"')%locals()
752             except IndexError:
753                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
754         return 0
756     def do_export(self, args):
757         '''Usage: export class[,class] destination_dir
758         Export the database to tab-separated-value files.
760         This action exports the current data from the database into
761         tab-separated-value files that are placed in the nominated destination
762         directory. The journals are not exported.
763         '''
764         if len(args) < 2:
765             raise UsageError, _('Not enough arguments supplied')
766         classes = args[0].split(',')
767         dir = args[1]
769         # use the csv parser if we can - it's faster
770         if csv is not None:
771             p = csv.parser(field_sep=':')
773         # do all the classes specified
774         for classname in classes:
775             cl = self.get_class(classname)
776             f = open(os.path.join(dir, classname+'.csv'), 'w')
777             f.write(':'.join(cl.properties.keys()) + '\n')
779             # all nodes for this class
780             properties = cl.properties.items()
781             for nodeid in cl.list():
782                 l = []
783                 for prop, proptype in properties:
784                     value = cl.get(nodeid, prop)
785                     # convert data where needed
786                     if isinstance(proptype, hyperdb.Date):
787                         value = value.get_tuple()
788                     elif isinstance(proptype, hyperdb.Interval):
789                         value = value.get_tuple()
790                     elif isinstance(proptype, hyperdb.Password):
791                         value = str(value)
792                     l.append(repr(value))
794                 # now write
795                 if csv is not None:
796                    f.write(p.join(l) + '\n')
797                 else:
798                    # escape the individual entries to they're valid CSV
799                    m = []
800                    for entry in l:
801                       if '"' in entry:
802                           entry = '""'.join(entry.split('"'))
803                       if ':' in entry:
804                           entry = '"%s"'%entry
805                       m.append(entry)
806                    f.write(':'.join(m) + '\n')
807         return 0
809     def do_import(self, args):
810         '''Usage: import class file
811         Import the contents of the tab-separated-value file.
813         The file must define the same properties as the class (including having
814         a "header" line with those property names.) The new nodes are added to
815         the existing database - if you want to create a new database using the
816         imported data, then create a new database (or, tediously, retire all
817         the old data.)
818         '''
819         if len(args) < 2:
820             raise UsageError, _('Not enough arguments supplied')
821         if csv is None:
822             raise UsageError, \
823                 _('Sorry, you need the csv module to use this function.\n'
824                 'Get it from: http://www.object-craft.com.au/projects/csv/')
826         from roundup import hyperdb
828         # ensure that the properties and the CSV file headings match
829         classname = args[0]
830         cl = self.get_class(classname)
831         f = open(args[1])
832         p = csv.parser(field_sep=':')
833         file_props = p.parse(f.readline())
834         props = cl.properties.keys()
835         m = file_props[:]
836         m.sort()
837         props.sort()
838         if m != props:
839             raise UsageError, _('Import file doesn\'t define the same '
840                 'properties as "%(arg0)s".')%{'arg0': args[0]}
842         # loop through the file and create a node for each entry
843         n = range(len(props))
844         while 1:
845             line = f.readline()
846             if not line: break
848             # parse lines until we get a complete entry
849             while 1:
850                 l = p.parse(line)
851                 if l: break
852                 line = f.readline()
853                 if not line:
854                     raise ValueError, "Unexpected EOF during CSV parse"
856             # make the new node's property map
857             d = {}
858             for i in n:
859                 # Use eval to reverse the repr() used to output the CSV
860                 value = eval(l[i])
861                 # Figure the property for this column
862                 key = file_props[i]
863                 proptype = cl.properties[key]
864                 # Convert for property type
865                 if isinstance(proptype, hyperdb.Date):
866                     value = date.Date(value)
867                 elif isinstance(proptype, hyperdb.Interval):
868                     value = date.Interval(value)
869                 elif isinstance(proptype, hyperdb.Password):
870                     pwd = password.Password()
871                     pwd.unpack(value)
872                     value = pwd
873                 if value is not None:
874                     d[key] = value
876             # and create the new node
877             apply(cl.create, (), d)
878         return 0
880     def do_pack(self, args):
881         '''Usage: pack period | date
883 Remove journal entries older than a period of time specified or
884 before a certain date.
886 A period is specified using the suffixes "y", "m", and "d". The
887 suffix "w" (for "week") means 7 days.
889       "3y" means three years
890       "2y 1m" means two years and one month
891       "1m 25d" means one month and 25 days
892       "2w 3d" means two weeks and three days
894 Date format is "YYYY-MM-DD" eg:
895     2001-01-01
896     
897         '''
898         if len(args) <> 1:
899             raise UsageError, _('Not enough arguments supplied')
900         
901         # are we dealing with a period or a date
902         value = args[0]
903         date_re = re.compile(r'''
904               (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
905               (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
906               ''', re.VERBOSE)
907         m = date_re.match(value)
908         if not m:
909             raise ValueError, _('Invalid format')
910         m = m.groupdict()
911         if m['period']:
912             # TODO: need to fix date module.  one should be able to say
913             # pack_before = date.Date(". - %s"%value)
914             pack_before = date.Date(".") + date.Interval("- %s"%value)
915         elif m['date']:
916             pack_before = date.Date(value)
917         self.db.pack(pack_before)
918         return 0
920     def run_command(self, args):
921         '''Run a single command
922         '''
923         command = args[0]
925         # handle help now
926         if command == 'help':
927             if len(args)>1:
928                 self.do_help(args[1:])
929                 return 0
930             self.do_help(['help'])
931             return 0
932         if command == 'morehelp':
933             self.do_help(['help'])
934             self.help_commands()
935             self.help_all()
936             return 0
938         # figure what the command is
939         try:
940             functions = self.commands.get(command)
941         except KeyError:
942             # not a valid command
943             print _('Unknown command "%(command)s" ("help commands" for a '
944                 'list)')%locals()
945             return 1
947         # check for multiple matches
948         if len(functions) > 1:
949             print _('Multiple commands match "%(command)s": %(list)s')%{'command':
950                 command, 'list': ', '.join([i[0] for i in functions])}
951             return 1
952         command, function = functions[0]
954         # make sure we have an instance_home
955         while not self.instance_home:
956             self.instance_home = raw_input(_('Enter instance home: ')).strip()
958         # before we open the db, we may be doing an init
959         if command == 'initialise':
960             try:
961                 return self.do_initialise(self.instance_home, args)
962             except UsageError, message:
963                 print _('Error: %(message)s')%locals()
964                 return 1
966         # get the instance
967         try:
968             instance = roundup.instance.open(self.instance_home)
969         except ValueError, message:
970             self.instance_home = ''
971             print _("Error: Couldn't open instance: %(message)s")%locals()
972             return 1
974         # only open the database once!
975         if not self.db:
976             self.db = instance.open('admin')
978         # do the command
979         ret = 0
980         try:
981             ret = function(args[1:])
982         except UsageError, message:
983             print _('Error: %(message)s')%locals()
984             print
985             print function.__doc__
986             ret = 1
987         except:
988             import traceback
989             traceback.print_exc()
990             ret = 1
991         return ret
993     def interactive(self):
994         '''Run in an interactive mode
995         '''
996         print _('Roundup {version} ready for input.')
997         print _('Type "help" for help.')
998         try:
999             import readline
1000         except ImportError:
1001             print _('Note: command history and editing not available')
1003         while 1:
1004             try:
1005                 command = raw_input(_('roundup> '))
1006             except EOFError:
1007                 print _('exit...')
1008                 break
1009             if not command: continue
1010             args = token.token_split(command)
1011             if not args: continue
1012             if args[0] in ('quit', 'exit'): break
1013             self.run_command(args)
1015         # exit.. check for transactions
1016         if self.db and self.db.transactions:
1017             commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1018             if commit and commit[0].lower() == 'y':
1019                 self.db.commit()
1020         return 0
1022     def main(self):
1023         try:
1024             opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1025         except getopt.GetoptError, e:
1026             self.usage(str(e))
1027             return 1
1029         # handle command-line args
1030         self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
1031         # TODO: reinstate the user/password stuff (-u arg too)
1032         name = password = ''
1033         if os.environ.has_key('ROUNDUP_LOGIN'):
1034             l = os.environ['ROUNDUP_LOGIN'].split(':')
1035             name = l[0]
1036             if len(l) > 1:
1037                 password = l[1]
1038         self.comma_sep = 0
1039         for opt, arg in opts:
1040             if opt == '-h':
1041                 self.usage()
1042                 return 0
1043             if opt == '-i':
1044                 self.instance_home = arg
1045             if opt == '-c':
1046                 self.comma_sep = 1
1048         # if no command - go interactive
1049         ret = 0
1050         if not args:
1051             self.interactive()
1052         else:
1053             ret = self.run_command(args)
1054             if self.db: self.db.commit()
1055         return ret
1058 if __name__ == '__main__':
1059     tool = AdminTool()
1060     sys.exit(tool.main())
1063 # $Log: not supported by cvs2svn $
1064 # Revision 1.9  2002/03/12 22:51:47  richard
1065 #  . #527416 ] roundup-admin uses undefined value
1066 #  . #527503 ] unfriendly init blowup when parent dir
1067 #    (also handles UsageError correctly now in init)
1069 # Revision 1.8  2002/02/27 03:28:21  richard
1070 # Ran it through pychecker, made fixes
1072 # Revision 1.7  2002/02/20 05:04:32  richard
1073 # Wasn't handling the cvs parser feeding properly.
1075 # Revision 1.6  2002/01/23 07:27:19  grubert
1076 #  . allow abbreviation of "help" in admin tool too.
1078 # Revision 1.5  2002/01/21 16:33:19  rochecompaan
1079 # You can now use the roundup-admin tool to pack the database
1081 # Revision 1.4  2002/01/14 06:51:09  richard
1082 #  . #503164 ] create and passwords
1084 # Revision 1.3  2002/01/08 05:26:32  rochecompaan
1085 # Missing "self" in props_from_args
1087 # Revision 1.2  2002/01/07 10:41:44  richard
1088 # #500140 ] AdminTool.get_class() returns nothing
1090 # Revision 1.1  2002/01/05 02:11:22  richard
1091 # I18N'ed roundup admin - and split the code off into a module so it can be used
1092 # elsewhere.
1093 # Big issue with this is the doc strings - that's the help. We're probably going to
1094 # have to switch to not use docstrings, which will suck a little :(
1098 # vim: set filetype=python ts=4 sw=4 et si