Code

#500140 ] AdminTool.get_class() returns nothing
[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.2 2002-01-07 10:41:44 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(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         topic = args[0]
214         # try help_ methods
215         if self.help.has_key(topic):
216             self.help[topic]()
217             return 0
219         # try command docstrings
220         try:
221             l = self.commands.get(topic)
222         except KeyError:
223             print _('Sorry, no help for "%(topic)s"')%locals()
224             return 1
226         # display the help for each match, removing the docsring indent
227         for name, help in l:
228             lines = nl_re.split(help.__doc__)
229             print lines[0]
230             indent = indent_re.match(lines[1])
231             if indent: indent = len(indent.group(1))
232             for line in lines[1:]:
233                 if indent:
234                     print line[indent:]
235                 else:
236                     print line
237         return 0
239     def help_initopts(self):
240         import roundup.templates
241         templates = roundup.templates.listTemplates()
242         print _('Templates:'), ', '.join(templates)
243         import roundup.backends
244         backends = roundup.backends.__all__
245         print _('Back ends:'), ', '.join(backends)
248     def do_initialise(self, instance_home, args):
249         '''Usage: initialise [template [backend [admin password]]]
250         Initialise a new Roundup instance.
252         The command will prompt for the instance home directory (if not supplied
253         through INSTANCE_HOME or the -i option). The template, backend and admin
254         password may be specified on the command-line as arguments, in that
255         order.
257         See also initopts help.
258         '''
259         if len(args) < 1:
260             raise UsageError, _('Not enough arguments supplied')
261         # select template
262         import roundup.templates
263         templates = roundup.templates.listTemplates()
264         template = len(args) > 1 and args[1] or ''
265         if template not in templates:
266             print _('Templates:'), ', '.join(templates)
267         while template not in templates:
268             template = raw_input(_('Select template [classic]: ')).strip()
269             if not template:
270                 template = 'classic'
272         import roundup.backends
273         backends = roundup.backends.__all__
274         backend = len(args) > 2 and args[2] or ''
275         if backend not in backends:
276             print _('Back ends:'), ', '.join(backends)
277         while backend not in backends:
278             backend = raw_input(_('Select backend [anydbm]: ')).strip()
279             if not backend:
280                 backend = 'anydbm'
281         if len(args) > 3:
282             adminpw = confirm = args[3]
283         else:
284             adminpw = ''
285             confirm = 'x'
286         while adminpw != confirm:
287             adminpw = getpass.getpass(_('Admin Password: '))
288             confirm = getpass.getpass(_('       Confirm: '))
289         init.init(instance_home, template, backend, adminpw)
290         return 0
293     def do_get(self, args):
294         '''Usage: get property designator[,designator]*
295         Get the given property of one or more designator(s).
297         Retrieves the property value of the nodes specified by the designators.
298         '''
299         if len(args) < 2:
300             raise UsageError, _('Not enough arguments supplied')
301         propname = args[0]
302         designators = args[1].split(',')
303         l = []
304         for designator in designators:
305             # decode the node designator
306             try:
307                 classname, nodeid = roundupdb.splitDesignator(designator)
308             except roundupdb.DesignatorError, message:
309                 raise UsageError, message
311             # get the class
312             cl = self.get_class(classname)
313             try:
314                 if self.comma_sep:
315                     l.append(cl.get(nodeid, propname))
316                 else:
317                     print cl.get(nodeid, propname)
318             except IndexError:
319                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
320             except KeyError:
321                 raise UsageError, _('no such %(classname)s property '
322                     '"%(propname)s"')%locals()
323         if self.comma_sep:
324             print ','.join(l)
325         return 0
328     def do_set(self, args):
329         '''Usage: set designator[,designator]* propname=value ...
330         Set the given property of one or more designator(s).
332         Sets the property to the value for all designators given.
333         '''
334         if len(args) < 2:
335             raise UsageError, _('Not enough arguments supplied')
336         from roundup import hyperdb
338         designators = args[0].split(',')
340         # get the props from the args
341         props = self.props_from_args(args[1:])
343         # now do the set for all the nodes
344         for designator in designators:
345             # decode the node designator
346             try:
347                 classname, nodeid = roundupdb.splitDesignator(designator)
348             except roundupdb.DesignatorError, message:
349                 raise UsageError, message
351             # get the class
352             cl = self.get_class(classname)
354             properties = cl.getprops()
355             for key, value in props.items():
356                 proptype =  properties[key]
357                 if isinstance(proptype, hyperdb.String):
358                     continue
359                 elif isinstance(proptype, hyperdb.Password):
360                     props[key] = password.Password(value)
361                 elif isinstance(proptype, hyperdb.Date):
362                     try:
363                         props[key] = date.Date(value)
364                     except ValueError, message:
365                         raise UsageError, '"%s": %s'%(value, message)
366                 elif isinstance(proptype, hyperdb.Interval):
367                     try:
368                         props[key] = date.Interval(value)
369                     except ValueError, message:
370                         raise UsageError, '"%s": %s'%(value, message)
371                 elif isinstance(proptype, hyperdb.Link):
372                     props[key] = value
373                 elif isinstance(proptype, hyperdb.Multilink):
374                     props[key] = value.split(',')
376             # try the set
377             try:
378                 apply(cl.set, (nodeid, ), props)
379             except (TypeError, IndexError, ValueError), message:
380                 raise UsageError, message
381         return 0
383     def do_find(self, args):
384         '''Usage: find classname propname=value ...
385         Find the nodes of the given class with a given link property value.
387         Find the nodes of the given class with a given link property value. The
388         value may be either the nodeid of the linked node, or its key value.
389         '''
390         if len(args) < 1:
391             raise UsageError, _('Not enough arguments supplied')
392         classname = args[0]
393         # get the class
394         cl = self.get_class(classname)
396         # handle the propname=value argument
397         props = self.props_from_args(args[1:])
399         # if the value isn't a number, look up the linked class to get the
400         # number
401         for propname, value in props.items():
402             num_re = re.compile('^\d+$')
403             if not num_re.match(value):
404                 # get the property
405                 try:
406                     property = cl.properties[propname]
407                 except KeyError:
408                     raise UsageError, _('%(classname)s has no property '
409                         '"%(propname)s"')%locals()
411                 # make sure it's a link
412                 if (not isinstance(property, hyperdb.Link) and not
413                         isinstance(property, hyperdb.Multilink)):
414                     raise UsageError, _('You may only "find" link properties')
416                 # get the linked-to class and look up the key property
417                 link_class = self.db.getclass(property.classname)
418                 try:
419                     props[propname] = link_class.lookup(value)
420                 except TypeError:
421                     raise UsageError, _('%(classname)s has no key property"')%{
422                         'classname': link_class.classname}
423                 except KeyError:
424                     raise UsageError, _('%(classname)s has no entry "%(propname)s"')%{
425                         'classname': link_class.classname, 'propname': propname}
427         # now do the find 
428         try:
429             if self.comma_sep:
430                 print ','.join(apply(cl.find, (), props))
431             else:
432                 print apply(cl.find, (), props)
433         except KeyError:
434             raise UsageError, _('%(classname)s has no property '
435                 '"%(propname)s"')%locals()
436         except (ValueError, TypeError), message:
437             raise UsageError, message
438         return 0
440     def do_specification(self, args):
441         '''Usage: specification classname
442         Show the properties for a classname.
444         This lists the properties for a given class.
445         '''
446         if len(args) < 1:
447             raise UsageError, _('Not enough arguments supplied')
448         classname = args[0]
449         # get the class
450         cl = self.get_class(classname)
452         # get the key property
453         keyprop = cl.getkey()
454         for key, value in cl.properties.items():
455             if keyprop == key:
456                 print _('%(key)s: %(value)s (key property)')%locals()
457             else:
458                 print _('%(key)s: %(value)s')%locals()
460     def do_display(self, args):
461         '''Usage: display designator
462         Show the property values for the given node.
464         This lists the properties and their associated values for the given
465         node.
466         '''
467         if len(args) < 1:
468             raise UsageError, _('Not enough arguments supplied')
470         # decode the node designator
471         try:
472             classname, nodeid = roundupdb.splitDesignator(args[0])
473         except roundupdb.DesignatorError, message:
474             raise UsageError, message
476         # get the class
477         cl = self.get_class(classname)
479         # display the values
480         for key in cl.properties.keys():
481             value = cl.get(nodeid, key)
482             print _('%(key)s: %(value)s')%locals()
484     def do_create(self, args):
485         '''Usage: create classname property=value ...
486         Create a new entry of a given class.
488         This creates a new entry of the given class using the property
489         name=value arguments provided on the command line after the "create"
490         command.
491         '''
492         if len(args) < 1:
493             raise UsageError, _('Not enough arguments supplied')
494         from roundup import hyperdb
496         classname = args[0]
498         # get the class
499         cl = self.get_class(classname)
501         # now do a create
502         props = {}
503         properties = cl.getprops(protected = 0)
504         if len(args) == 1:
505             # ask for the properties
506             for key, value in properties.items():
507                 if key == 'id': continue
508                 name = value.__class__.__name__
509                 if isinstance(value , hyperdb.Password):
510                     again = None
511                     while value != again:
512                         value = getpass.getpass(_('%(propname)s (Password): ')%{
513                             'propname': key.capitalize()})
514                         again = getpass.getpass(_('   %(propname)s (Again): ')%{
515                             'propname': key.capitalize()})
516                         if value != again: print _('Sorry, try again...')
517                     if value:
518                         props[key] = value
519                 else:
520                     value = raw_input(_('%(propname)s (%(proptype)s): ')%{
521                         'propname': key.capitalize(), 'proptype': name})
522                     if value:
523                         props[key] = value
524         else:
525             props = self.props_from_args(args[1:])
527         # convert types
528         for propname in props.keys():
529             # get the property
530             try:
531                 proptype = properties[propname]
532             except KeyError:
533                 raise UsageError, _('%(classname)s has no property '
534                     '"%(propname)s"')%locals()
536             if isinstance(proptype, hyperdb.Date):
537                 try:
538                     props[key] = date.Date(value)
539                 except ValueError, message:
540                     raise UsageError, _('"%(value)s": %(message)s')%locals()
541             elif isinstance(proptype, hyperdb.Interval):
542                 try:
543                     props[key] = date.Interval(value)
544                 except ValueError, message:
545                     raise UsageError, _('"%(value)s": %(message)s')%locals()
546             elif isinstance(proptype, hyperdb.Password):
547                 props[key] = password.Password(value)
548             elif isinstance(proptype, hyperdb.Multilink):
549                 props[key] = value.split(',')
551         # check for the key property
552         propname = cl.getkey()
553         if propname and not props.has_key(propname):
554             raise UsageError, _('you must provide the "%(propname)s" '
555                 'property.')%locals()
557         # do the actual create
558         try:
559             print apply(cl.create, (), props)
560         except (TypeError, IndexError, ValueError), message:
561             raise UsageError, message
562         return 0
564     def do_list(self, args):
565         '''Usage: list classname [property]
566         List the instances of a class.
568         Lists all instances of the given class. If the property is not
569         specified, the  "label" property is used. The label property is tried
570         in order: the key, "name", "title" and then the first property,
571         alphabetically.
572         '''
573         if len(args) < 1:
574             raise UsageError, _('Not enough arguments supplied')
575         classname = args[0]
577         # get the class
578         cl = self.get_class(classname)
580         # figure the property
581         if len(args) > 1:
582             propname = args[1]
583         else:
584             propname = cl.labelprop()
586         if self.comma_sep:
587             print ','.join(cl.list())
588         else:
589             for nodeid in cl.list():
590                 try:
591                     value = cl.get(nodeid, propname)
592                 except KeyError:
593                     raise UsageError, _('%(classname)s has no property '
594                         '"%(propname)s"')%locals()
595                 print _('%(nodeid)4s: %(value)s')%locals()
596         return 0
598     def do_table(self, args):
599         '''Usage: table classname [property[,property]*]
600         List the instances of a class in tabular form.
602         Lists all instances of the given class. If the properties are not
603         specified, all properties are displayed. By default, the column widths
604         are the width of the property names. The width may be explicitly defined
605         by defining the property as "name:width". For example::
606           roundup> table priority id,name:10
607           Id Name
608           1  fatal-bug 
609           2  bug       
610           3  usability 
611           4  feature   
612         '''
613         if len(args) < 1:
614             raise UsageError, _('Not enough arguments supplied')
615         classname = args[0]
617         # get the class
618         cl = self.get_class(classname)
620         # figure the property names to display
621         if len(args) > 1:
622             prop_names = args[1].split(',')
623             all_props = cl.getprops()
624             for spec in prop_names:
625                 if ':' in spec:
626                     try:
627                         propname, width = spec.split(':')
628                     except (ValueError, TypeError):
629                         raise UsageError, _('"%(spec)s" not name:width')%locals()
630                 else:
631                     propname = spec
632                 if not all_props.has_key(propname):
633                     raise UsageError, _('%(classname)s has no property '
634                         '"%(propname)s"')%locals()
635         else:
636             prop_names = cl.getprops().keys()
638         # now figure column widths
639         props = []
640         for spec in prop_names:
641             if ':' in spec:
642                 name, width = spec.split(':')
643                 props.append((name, int(width)))
644             else:
645                 props.append((spec, len(spec)))
647         # now display the heading
648         print ' '.join([name.capitalize().ljust(width) for name,width in props])
650         # and the table data
651         for nodeid in cl.list():
652             l = []
653             for name, width in props:
654                 if name != 'id':
655                     try:
656                         value = str(cl.get(nodeid, name))
657                     except KeyError:
658                         # we already checked if the property is valid - a
659                         # KeyError here means the node just doesn't have a
660                         # value for it
661                         value = ''
662                 else:
663                     value = str(nodeid)
664                 f = '%%-%ds'%width
665                 l.append(f%value[:width])
666             print ' '.join(l)
667         return 0
669     def do_history(self, args):
670         '''Usage: history designator
671         Show the history entries of a designator.
673         Lists the journal entries for the node identified by the designator.
674         '''
675         if len(args) < 1:
676             raise UsageError, _('Not enough arguments supplied')
677         try:
678             classname, nodeid = roundupdb.splitDesignator(args[0])
679         except roundupdb.DesignatorError, message:
680             raise UsageError, message
682         try:
683             print self.db.getclass(classname).history(nodeid)
684         except KeyError:
685             raise UsageError, _('no such class "%(classname)s"')%locals()
686         except IndexError:
687             raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
688         return 0
690     def do_commit(self, args):
691         '''Usage: commit
692         Commit all changes made to the database.
694         The changes made during an interactive session are not
695         automatically written to the database - they must be committed
696         using this command.
698         One-off commands on the command-line are automatically committed if
699         they are successful.
700         '''
701         self.db.commit()
702         return 0
704     def do_rollback(self, args):
705         '''Usage: rollback
706         Undo all changes that are pending commit to the database.
708         The changes made during an interactive session are not
709         automatically written to the database - they must be committed
710         manually. This command undoes all those changes, so a commit
711         immediately after would make no changes to the database.
712         '''
713         self.db.rollback()
714         return 0
716     def do_retire(self, args):
717         '''Usage: retire designator[,designator]*
718         Retire the node specified by designator.
720         This action indicates that a particular node is not to be retrieved by
721         the list or find commands, and its key value may be re-used.
722         '''
723         if len(args) < 1:
724             raise UsageError, _('Not enough arguments supplied')
725         designators = args[0].split(',')
726         for designator in designators:
727             try:
728                 classname, nodeid = roundupdb.splitDesignator(designator)
729             except roundupdb.DesignatorError, message:
730                 raise UsageError, message
731             try:
732                 self.db.getclass(classname).retire(nodeid)
733             except KeyError:
734                 raise UsageError, _('no such class "%(classname)s"')%locals()
735             except IndexError:
736                 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
737         return 0
739     def do_export(self, args):
740         '''Usage: export class[,class] destination_dir
741         Export the database to tab-separated-value files.
743         This action exports the current data from the database into
744         tab-separated-value files that are placed in the nominated destination
745         directory. The journals are not exported.
746         '''
747         if len(args) < 2:
748             raise UsageError, _('Not enough arguments supplied')
749         classes = args[0].split(',')
750         dir = args[1]
752         # use the csv parser if we can - it's faster
753         if csv is not None:
754             p = csv.parser(field_sep=':')
756         # do all the classes specified
757         for classname in classes:
758             cl = self.get_class(classname)
759             f = open(os.path.join(dir, classname+'.csv'), 'w')
760             f.write(':'.join(cl.properties.keys()) + '\n')
762             # all nodes for this class
763             properties = cl.properties.items()
764             for nodeid in cl.list():
765                 l = []
766                 for prop, proptype in properties:
767                     value = cl.get(nodeid, prop)
768                     # convert data where needed
769                     if isinstance(proptype, hyperdb.Date):
770                         value = value.get_tuple()
771                     elif isinstance(proptype, hyperdb.Interval):
772                         value = value.get_tuple()
773                     elif isinstance(proptype, hyperdb.Password):
774                         value = str(value)
775                     l.append(repr(value))
777                 # now write
778                 if csv is not None:
779                    f.write(p.join(l) + '\n')
780                 else:
781                    # escape the individual entries to they're valid CSV
782                    m = []
783                    for entry in l:
784                       if '"' in entry:
785                           entry = '""'.join(entry.split('"'))
786                       if ':' in entry:
787                           entry = '"%s"'%entry
788                       m.append(entry)
789                    f.write(':'.join(m) + '\n')
790         return 0
792     def do_import(self, args):
793         '''Usage: import class file
794         Import the contents of the tab-separated-value file.
796         The file must define the same properties as the class (including having
797         a "header" line with those property names.) The new nodes are added to
798         the existing database - if you want to create a new database using the
799         imported data, then create a new database (or, tediously, retire all
800         the old data.)
801         '''
802         if len(args) < 2:
803             raise UsageError, _('Not enough arguments supplied')
804         if csv is None:
805             raise UsageError, \
806                 _('Sorry, you need the csv module to use this function.\n'
807                 'Get it from: http://www.object-craft.com.au/projects/csv/')
809         from roundup import hyperdb
811         # ensure that the properties and the CSV file headings match
812         classname = args[0]
813         cl = self.get_class(classname)
814         f = open(args[1])
815         p = csv.parser(field_sep=':')
816         file_props = p.parse(f.readline())
817         props = cl.properties.keys()
818         m = file_props[:]
819         m.sort()
820         props.sort()
821         if m != props:
822             raise UsageError, _('Import file doesn\'t define the same '
823                 'properties as "%(arg0)s".')%{'arg0': args[0]}
825         # loop through the file and create a node for each entry
826         n = range(len(props))
827         while 1:
828             line = f.readline()
829             if not line: break
831             # parse lines until we get a complete entry
832             while 1:
833                 l = p.parse(line)
834                 if l: break
836             # make the new node's property map
837             d = {}
838             for i in n:
839                 # Use eval to reverse the repr() used to output the CSV
840                 value = eval(l[i])
841                 # Figure the property for this column
842                 key = file_props[i]
843                 proptype = cl.properties[key]
844                 # Convert for property type
845                 if isinstance(proptype, hyperdb.Date):
846                     value = date.Date(value)
847                 elif isinstance(proptype, hyperdb.Interval):
848                     value = date.Interval(value)
849                 elif isinstance(proptype, hyperdb.Password):
850                     pwd = password.Password()
851                     pwd.unpack(value)
852                     value = pwd
853                 if value is not None:
854                     d[key] = value
856             # and create the new node
857             apply(cl.create, (), d)
858         return 0
860     def run_command(self, args):
861         '''Run a single command
862         '''
863         command = args[0]
865         # handle help now
866         if command == 'help':
867             if len(args)>1:
868                 self.do_help(args[1:])
869                 return 0
870             self.do_help(['help'])
871             return 0
872         if command == 'morehelp':
873             self.do_help(['help'])
874             self.help_commands()
875             self.help_all()
876             return 0
878         # figure what the command is
879         try:
880             functions = self.commands.get(command)
881         except KeyError:
882             # not a valid command
883             print _('Unknown command "%(command)s" ("help commands" for a '
884                 'list)')%locals()
885             return 1
887         # check for multiple matches
888         if len(functions) > 1:
889             print _('Multiple commands match "%(command)s": %(list)s')%{'command':
890                 command, 'list': ', '.join([i[0] for i in functions])}
891             return 1
892         command, function = functions[0]
894         # make sure we have an instance_home
895         while not self.instance_home:
896             self.instance_home = raw_input(_('Enter instance home: ')).strip()
898         # before we open the db, we may be doing an init
899         if command == 'initialise':
900             return self.do_initialise(self.instance_home, args)
902         # get the instance
903         try:
904             instance = roundup.instance.open(self.instance_home)
905         except ValueError, message:
906             self.instance_home = ''
907             print _("Couldn't open instance: %(message)s")%locals()
908             return 1
910         # only open the database once!
911         if not self.db:
912             self.db = instance.open('admin')
914         # do the command
915         ret = 0
916         try:
917             ret = function(args[1:])
918         except UsageError, message:
919             print _('Error: %(message)s')%locals()
920             print function.__doc__
921             ret = 1
922         except:
923             import traceback
924             traceback.print_exc()
925             ret = 1
926         return ret
928     def interactive(self):
929         '''Run in an interactive mode
930         '''
931         print _('Roundup {version} ready for input.')
932         print _('Type "help" for help.')
933         try:
934             import readline
935         except ImportError:
936             print _('Note: command history and editing not available')
938         while 1:
939             try:
940                 command = raw_input(_('roundup> '))
941             except EOFError:
942                 print _('exit...')
943                 break
944             if not command: continue
945             args = token.token_split(command)
946             if not args: continue
947             if args[0] in ('quit', 'exit'): break
948             self.run_command(args)
950         # exit.. check for transactions
951         if self.db and self.db.transactions:
952             commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
953             if commit and commit[0].lower() == 'y':
954                 self.db.commit()
955         return 0
957     def main(self):
958         try:
959             opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
960         except getopt.GetoptError, e:
961             self.usage(str(e))
962             return 1
964         # handle command-line args
965         self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
966         name = password = ''
967         if os.environ.has_key('ROUNDUP_LOGIN'):
968             l = os.environ['ROUNDUP_LOGIN'].split(':')
969             name = l[0]
970             if len(l) > 1:
971                 password = l[1]
972         self.comma_sep = 0
973         for opt, arg in opts:
974             if opt == '-h':
975                 self.usage()
976                 return 0
977             if opt == '-i':
978                 self.instance_home = arg
979             if opt == '-c':
980                 self.comma_sep = 1
982         # if no command - go interactive
983         ret = 0
984         if not args:
985             self.interactive()
986         else:
987             ret = self.run_command(args)
988             if self.db: self.db.commit()
989         return ret
992 if __name__ == '__main__':
993     tool = AdminTool()
994     sys.exit(tool.main())
997 # $Log: not supported by cvs2svn $
998 # Revision 1.1  2002/01/05 02:11:22  richard
999 # I18N'ed roundup admin - and split the code off into a module so it can be used
1000 # elsewhere.
1001 # Big issue with this is the doc strings - that's the help. We're probably going to
1002 # have to switch to not use docstrings, which will suck a little :(
1006 # vim: set filetype=python ts=4 sw=4 et si