Code

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