Code

Copy function, and proper handling of unknown file types
[roundup.git] / roundup-admin
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: roundup-admin,v 1.45 2001-11-12 22:51:59 jhermann Exp $
21 import sys
22 if int(sys.version[0]) < 2:
23     print 'Roundup requires python 2.0 or later.'
24     sys.exit(1)
26 import string, os, getpass, getopt, re, UserDict
27 try:
28     import csv
29 except ImportError:
30     csv = None
31 from roundup import date, hyperdb, roundupdb, init, password
32 import roundup.instance
34 class CommandDict(UserDict.UserDict):
35     '''Simple dictionary that lets us do lookups using partial keys.
37     Original code submitted by Engelbert Gruber.
38     '''
39     _marker = []
40     def get(self, key, default=_marker):
41         if self.data.has_key(key):
42             return [(key, self.data[key])]
43         keylist = self.data.keys()
44         keylist.sort()
45         l = []
46         for ki in keylist:
47             if ki.startswith(key):
48                 l.append((ki, self.data[ki]))
49         if not l and default is self._marker:
50             raise KeyError, key
51         return l
53 class UsageError(ValueError):
54     pass
56 class AdminTool:
58     def __init__(self):
59         self.commands = CommandDict()
60         for k in AdminTool.__dict__.keys():
61             if k[:3] == 'do_':
62                 self.commands[k[3:]] = getattr(self, k)
63         self.help = {}
64         for k in AdminTool.__dict__.keys():
65             if k[:5] == 'help_':
66                 self.help[k[5:]] = getattr(self, k)
67         self.db = None
69     def usage(self, message=''):
70         if message: message = 'Problem: '+message+'\n\n'
71         print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments>
73 Help:
74  roundup-admin -h
75  roundup-admin help                       -- this help
76  roundup-admin help <command>             -- command-specific help
77  roundup-admin help all                   -- all available help
78 Options:
79  -i instance home  -- specify the issue tracker "home directory" to administer
80  -u                -- the user[:password] to use for commands
81  -c                -- when outputting lists of data, just comma-separate them'''%message
82         self.help_commands()
84     def help_commands(self):
85         print 'Commands:',
86         commands = ['']
87         for command in self.commands.values():
88             h = command.__doc__.split('\n')[0]
89             commands.append(' '+h[7:])
90         commands.sort()
91         commands.append(
92 'Commands may be abbreviated as long as the abbreviation matches only one')
93         commands.append('command, e.g. l == li == lis == list.')
94         print '\n'.join(commands)
95         print
97     def help_all(self):
98         print '''
99 All commands (except help) require an instance specifier. This is just the path
100 to the roundup instance you're working with. A roundup instance is where 
101 roundup keeps the database and configuration file that defines an issue
102 tracker. It may be thought of as the issue tracker's "home directory". It may
103 be specified in the environment variable ROUNDUP_INSTANCE or on the command
104 line as "-i instance".
106 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
108 Property values are represented as strings in command arguments and in the
109 printed results:
110  . Strings are, well, strings.
111  . Date values are printed in the full date format in the local time zone, and
112    accepted in the full format or any of the partial formats explained below.
113  . Link values are printed as node designators. When given as an argument,
114    node designators and key strings are both accepted.
115  . Multilink values are printed as lists of node designators joined by commas.
116    When given as an argument, node designators and key strings are both
117    accepted; an empty string, a single node, or a list of nodes joined by
118    commas is accepted.
120 When multiple nodes are specified to the roundup get or roundup set
121 commands, the specified properties are retrieved or set on all the listed
122 nodes. 
124 When multiple results are returned by the roundup get or roundup find
125 commands, they are printed one per line (default) or joined by commas (with
126 the -c) option. 
128 Where the command changes data, a login name/password is required. The
129 login may be specified as either "name" or "name:password".
130  . ROUNDUP_LOGIN environment variable
131  . the -u command-line option
132 If either the name or password is not supplied, they are obtained from the
133 command-line. 
135 Date format examples:
136   "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
137   "2000-04-17" means <Date 2000-04-17.00:00:00>
138   "01-25" means <Date yyyy-01-25.00:00:00>
139   "08-13.22:13" means <Date yyyy-08-14.03:13:00>
140   "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
141   "14:25" means <Date yyyy-mm-dd.19:25:00>
142   "8:47:11" means <Date yyyy-mm-dd.13:47:11>
143   "." means "right now"
145 Command help:
146 '''
147         for name, command in self.commands.items():
148             print '%s:'%name
149             print '   ',command.__doc__
151     def do_help(self, args, nl_re=re.compile('[\r\n]'),
152             indent_re=re.compile(r'^(\s+)\S+')):
153         '''Usage: help topic
154         Give help about topic.
156         commands  -- list commands
157         <command> -- help specific to a command
158         initopts  -- init command options
159         all       -- all available help
160         '''
161         topic = args[0]
163         # try help_ methods
164         if self.help.has_key(topic):
165             self.help[topic]()
166             return 0
168         # try command docstrings
169         try:
170             l = self.commands.get(topic)
171         except KeyError:
172             print 'Sorry, no help for "%s"'%topic
173             return 1
175         # display the help for each match, removing the docsring indent
176         for name, help in l:
177             lines = nl_re.split(help.__doc__)
178             print lines[0]
179             indent = indent_re.match(lines[1])
180             if indent: indent = len(indent.group(1))
181             for line in lines[1:]:
182                 if indent:
183                     print line[indent:]
184                 else:
185                     print line
186         return 0
188     def help_initopts(self):
189         import roundup.templates
190         templates = roundup.templates.listTemplates()
191         print 'Templates:', ', '.join(templates)
192         import roundup.backends
193         backends = roundup.backends.__all__
194         print 'Back ends:', ', '.join(backends)
197     def do_initialise(self, instance_home, args):
198         '''Usage: initialise [template [backend [admin password]]]
199         Initialise a new Roundup instance.
201         The command will prompt for the instance home directory (if not supplied
202         through INSTANCE_HOME or the -i option. The template, backend and admin
203         password may be specified on the command-line as arguments, in that
204         order.
206         See also initopts help.
207         '''
208         # select template
209         import roundup.templates
210         templates = roundup.templates.listTemplates()
211         template = len(args) > 1 and args[1] or ''
212         if template not in templates:
213             print 'Templates:', ', '.join(templates)
214         while template not in templates:
215             template = raw_input('Select template [classic]: ').strip()
216             if not template:
217                 template = 'classic'
219         import roundup.backends
220         backends = roundup.backends.__all__
221         backend = len(args) > 2 and args[2] or ''
222         if backend not in backends:
223             print 'Back ends:', ', '.join(backends)
224         while backend not in backends:
225             backend = raw_input('Select backend [anydbm]: ').strip()
226             if not backend:
227                 backend = 'anydbm'
228         if len(args) > 3:
229             adminpw = confirm = args[3]
230         else:
231             adminpw = ''
232             confirm = 'x'
233         while adminpw != confirm:
234             adminpw = getpass.getpass('Admin Password: ')
235             confirm = getpass.getpass('       Confirm: ')
236         init.init(instance_home, template, backend, adminpw)
237         return 0
240     def do_get(self, args):
241         '''Usage: get property designator[,designator]*
242         Get the given property of one or more designator(s).
244         Retrieves the property value of the nodes specified by the designators.
245         '''
246         propname = args[0]
247         designators = string.split(args[1], ',')
248         l = []
249         for designator in designators:
250             # decode the node designator
251             try:
252                 classname, nodeid = roundupdb.splitDesignator(designator)
253             except roundupdb.DesignatorError, message:
254                 raise UsageError, message
256             # get the class
257             try:
258                 cl = self.db.getclass(classname)
259             except KeyError:
260                 raise UsageError, 'invalid class "%s"'%classname
261             try:
262                 if self.comma_sep:
263                     l.append(cl.get(nodeid, propname))
264                 else:
265                     print cl.get(nodeid, propname)
266             except IndexError:
267                 raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
268             except KeyError:
269                 raise UsageError, 'no such %s property "%s"'%(classname,
270                     propname)
271         if self.comma_sep:
272             print ','.join(l)
273         return 0
276     def do_set(self, args):
277         '''Usage: set designator[,designator]* propname=value ...
278         Set the given property of one or more designator(s).
280         Sets the property to the value for all designators given.
281         '''
282         from roundup import hyperdb
284         designators = string.split(args[0], ',')
285         props = {}
286         for prop in args[1:]:
287             if prop.find('=') == -1:
288                 raise UsageError, 'argument "%s" not propname=value'%prop
289             try:
290                 key, value = prop.split('=')
291             except ValueError:
292                 raise UsageError, 'argument "%s" not propname=value'%prop
293             props[key] = value
294         for designator in designators:
295             # decode the node designator
296             try:
297                 classname, nodeid = roundupdb.splitDesignator(designator)
298             except roundupdb.DesignatorError, message:
299                 raise UsageError, message
301             # get the class
302             try:
303                 cl = self.db.getclass(classname)
304             except KeyError:
305                 raise UsageError, 'invalid class "%s"'%classname
307             properties = cl.getprops()
308             for key, value in props.items():
309                 type =  properties[key]
310                 if isinstance(type, hyperdb.String):
311                     continue
312                 elif isinstance(type, hyperdb.Password):
313                     props[key] = password.Password(value)
314                 elif isinstance(type, hyperdb.Date):
315                     try:
316                         props[key] = date.Date(value)
317                     except ValueError, message:
318                         raise UsageError, '"%s": %s'%(value, message)
319                 elif isinstance(type, hyperdb.Interval):
320                     try:
321                         props[key] = date.Interval(value)
322                     except ValueError, message:
323                         raise UsageError, '"%s": %s'%(value, message)
324                 elif isinstance(type, hyperdb.Link):
325                     props[key] = value
326                 elif isinstance(type, hyperdb.Multilink):
327                     props[key] = value.split(',')
329             # try the set
330             try:
331                 apply(cl.set, (nodeid, ), props)
332             except (TypeError, IndexError, ValueError), message:
333                 raise UsageError, message
334         return 0
336     def do_find(self, args):
337         '''Usage: find classname propname=value ...
338         Find the nodes of the given class with a given link property value.
340         Find the nodes of the given class with a given link property value. The
341         value may be either the nodeid of the linked node, or its key value.
342         '''
343         classname = args[0]
344         # get the class
345         try:
346             cl = self.db.getclass(classname)
347         except KeyError:
348             raise UsageError, 'invalid class "%s"'%classname
350         # TODO: handle > 1 argument
351         # handle the propname=value argument
352         if prop.find('=') == -1:
353             raise UsageError, 'argument "%s" not propname=value'%prop
354         try:
355             propname, value = args[1].split('=')
356         except ValueError:
357             raise UsageError, 'argument "%s" not propname=value'%prop
359         # if the value isn't a number, look up the linked class to get the
360         # number
361         num_re = re.compile('^\d+$')
362         if not num_re.match(value):
363             # get the property
364             try:
365                 property = cl.properties[propname]
366             except KeyError:
367                 raise UsageError, '%s has no property "%s"'%(classname,
368                     propname)
370             # make sure it's a link
371             if (not isinstance(property, hyperdb.Link) and not
372                     isinstance(type, hyperdb.Multilink)):
373                 raise UsageError, 'You may only "find" link properties'
375             # get the linked-to class and look up the key property
376             link_class = self.db.getclass(property.classname)
377             try:
378                 value = link_class.lookup(value)
379             except TypeError:
380                 raise UsageError, '%s has no key property"'%link_class.classname
381             except KeyError:
382                 raise UsageError, '%s has no entry "%s"'%(link_class.classname,
383                     propname)
385         # now do the find 
386         try:
387             if self.comma_sep:
388                 print ','.join(apply(cl.find, (), {propname: value}))
389             else:
390                 print apply(cl.find, (), {propname: value})
391         except KeyError:
392             raise UsageError, '%s has no property "%s"'%(classname,
393                 propname)
394         except (ValueError, TypeError), message:
395             raise UsageError, message
396         return 0
398     def do_specification(self, args):
399         '''Usage: specification classname
400         Show the properties for a classname.
402         This lists the properties for a given class.
403         '''
404         classname = args[0]
405         # get the class
406         try:
407             cl = self.db.getclass(classname)
408         except KeyError:
409             raise UsageError, 'invalid class "%s"'%classname
411         # get the key property
412         keyprop = cl.getkey()
413         for key, value in cl.properties.items():
414             if keyprop == key:
415                 print '%s: %s (key property)'%(key, value)
416             else:
417                 print '%s: %s'%(key, value)
419     def do_create(self, args):
420         '''Usage: create classname property=value ...
421         Create a new entry of a given class.
423         This creates a new entry of the given class using the property
424         name=value arguments provided on the command line after the "create"
425         command.
426         '''
427         from roundup import hyperdb
429         classname = args[0]
431         # get the class
432         try:
433             cl = self.db.getclass(classname)
434         except KeyError:
435             raise UsageError, 'invalid class "%s"'%classname
437         # now do a create
438         props = {}
439         properties = cl.getprops(protected = 0)
440         if len(args) == 1:
441             # ask for the properties
442             for key, value in properties.items():
443                 if key == 'id': continue
444                 name = value.__class__.__name__
445                 if isinstance(value , hyperdb.Password):
446                     again = None
447                     while value != again:
448                         value = getpass.getpass('%s (Password): '%key.capitalize())
449                         again = getpass.getpass('   %s (Again): '%key.capitalize())
450                         if value != again: print 'Sorry, try again...'
451                     if value:
452                         props[key] = value
453                 else:
454                     value = raw_input('%s (%s): '%(key.capitalize(), name))
455                     if value:
456                         props[key] = value
457         else:
458             # use the args
459             for prop in args[1:]:
460                 if prop.find('=') == -1:
461                     raise UsageError, 'argument "%s" not propname=value'%prop
462                 try:
463                     key, value = prop.split('=')
464                 except ValueError:
465                     raise UsageError, 'argument "%s" not propname=value'%prop
466                 props[key] = value 
468         # convert types
469         for key in props.keys():
470             # get the property
471             try:
472                 type = properties[key]
473             except KeyError:
474                 raise UsageError, '%s has no property "%s"'%(classname, key)
476             if isinstance(type, hyperdb.Date):
477                 try:
478                     props[key] = date.Date(value)
479                 except ValueError, message:
480                     raise UsageError, '"%s": %s'%(value, message)
481             elif isinstance(type, hyperdb.Interval):
482                 try:
483                     props[key] = date.Interval(value)
484                 except ValueError, message:
485                     raise UsageError, '"%s": %s'%(value, message)
486             elif isinstance(type, hyperdb.Password):
487                 props[key] = password.Password(value)
488             elif isinstance(type, hyperdb.Multilink):
489                 props[key] = value.split(',')
491         # check for the key property
492         if cl.getkey() and not props.has_key(cl.getkey()):
493             raise UsageError, "you must provide the '%s' property."%cl.getkey()
495         # do the actual create
496         try:
497             print apply(cl.create, (), props)
498         except (TypeError, IndexError, ValueError), message:
499             raise UsageError, message
500         return 0
502     def do_list(self, args):
503         '''Usage: list classname [property]
504         List the instances of a class.
506         Lists all instances of the given class. If the property is not
507         specified, the  "label" property is used. The label property is tried
508         in order: the key, "name", "title" and then the first property,
509         alphabetically.
510         '''
511         classname = args[0]
513         # get the class
514         try:
515             cl = self.db.getclass(classname)
516         except KeyError:
517             raise UsageError, 'invalid class "%s"'%classname
519         # figure the property
520         if len(args) > 1:
521             key = args[1]
522         else:
523             key = cl.labelprop()
525         if self.comma_sep:
526             print ','.join(cl.list())
527         else:
528             for nodeid in cl.list():
529                 try:
530                     value = cl.get(nodeid, key)
531                 except KeyError:
532                     raise UsageError, '%s has no property "%s"'%(classname, key)
533                 print "%4s: %s"%(nodeid, value)
534         return 0
536     def do_table(self, args):
537         '''Usage: table classname [property[,property]*]
538         List the instances of a class in tabular form.
540         Lists all instances of the given class. If the properties are not
541         specified, all properties are displayed. By default, the column widths
542         are the width of the property names. The width may be explicitly defined
543         by defining the property as "name:width". For example::
544           roundup> table priority id,name:10
545           Id Name
546           1  fatal-bug 
547           2  bug       
548           3  usability 
549           4  feature   
550         '''
551         classname = args[0]
553         # get the class
554         try:
555             cl = self.db.getclass(classname)
556         except KeyError:
557             raise UsageError, 'invalid class "%s"'%classname
559         # figure the property names to display
560         if len(args) > 1:
561             prop_names = args[1].split(',')
562         else:
563             prop_names = cl.getprops().keys()
565         # now figure column widths
566         props = []
567         for spec in prop_names:
568             if ':' in spec:
569                 try:
570                     name, width = spec.split(':')
571                 except (ValueError, TypeError):
572                     raise UsageError, '"%s" not name:width'%spec
573                 props.append((spec, int(width)))
574             else:
575                 props.append((spec, len(spec)))
577         # now display the heading
578         print ' '.join([string.capitalize(name) for name, width in props])
580         # and the table data
581         for nodeid in cl.list():
582             l = []
583             for name, width in props:
584                 if name != 'id':
585                     try:
586                         value = str(cl.get(nodeid, name))
587                     except KeyError:
588                         raise UsageError, '%s has no property "%s"'%(classname,
589                             name)
590                 else:
591                     value = str(nodeid)
592                 f = '%%-%ds'%width
593                 l.append(f%value[:width])
594             print ' '.join(l)
595         return 0
597     def do_history(self, args):
598         '''Usage: history designator
599         Show the history entries of a designator.
601         Lists the journal entries for the node identified by the designator.
602         '''
603         try:
604             classname, nodeid = roundupdb.splitDesignator(args[0])
605         except roundupdb.DesignatorError, message:
606             raise UsageError, message
608         # TODO: handle the -c option?
609         try:
610             print self.db.getclass(classname).history(nodeid)
611         except KeyError:
612             raise UsageError, 'no such class "%s"'%classname
613         except IndexError:
614             raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
615         return 0
617     def do_retire(self, args):
618         '''Usage: retire designator[,designator]*
619         Retire the node specified by designator.
621         This action indicates that a particular node is not to be retrieved by
622         the list or find commands, and its key value may be re-used.
623         '''
624         designators = string.split(args[0], ',')
625         for designator in designators:
626             try:
627                 classname, nodeid = roundupdb.splitDesignator(designator)
628             except roundupdb.DesignatorError, message:
629                 raise UsageError, message
630             try:
631                 self.db.getclass(classname).retire(nodeid)
632             except KeyError:
633                 raise UsageError, 'no such class "%s"'%classname
634             except IndexError:
635                 raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
636         return 0
638     def do_export(self, args):
639         '''Usage: export class[,class] destination_dir
640         Export the database to tab-separated-value files.
642         This action exports the current data from the database into
643         tab-separated-value files that are placed in the nominated destination
644         directory. The journals are not exported.
645         '''
646         if len(args) < 2:
647             print do_export.__doc__
648             return 1
649         classes = string.split(args[0], ',')
650         dir = args[1]
652         # use the csv parser if we can - it's faster
653         if csv is not None:
654             p = csv.parser(field_sep=':')
656         # do all the classes specified
657         for classname in classes:
658             try:
659                 cl = self.db.getclass(classname)
660             except KeyError:
661                 raise UsageError, 'no such class "%s"'%classname
662             f = open(os.path.join(dir, classname+'.csv'), 'w')
663             f.write(string.join(cl.properties.keys(), ':') + '\n')
665             # all nodes for this class
666             properties = cl.properties.items()
667             for nodeid in cl.list():
668                 l = []
669                 for prop, type in properties:
670                     value = cl.get(nodeid, prop)
671                     # convert data where needed
672                     if isinstance(type, hyperdb.Date):
673                         value = value.get_tuple()
674                     elif isinstance(type, hyperdb.Interval):
675                         value = value.get_tuple()
676                     elif isinstance(type, hyperdb.Password):
677                         value = str(value)
678                     l.append(repr(value))
680                 # now write
681                 if csv is not None:
682                    f.write(p.join(l) + '\n')
683                 else:
684                    # escape the individual entries to they're valid CSV
685                    m = []
686                    for entry in l:
687                       if '"' in entry:
688                           entry = '""'.join(entry.split('"'))
689                       if ':' in entry:
690                           entry = '"%s"'%entry
691                       m.append(entry)
692                    f.write(':'.join(m) + '\n')
693         return 0
695     def do_import(self, args):
696         '''Usage: import class file
697         Import the contents of the tab-separated-value file.
699         The file must define the same properties as the class (including having
700         a "header" line with those property names.) The new nodes are added to
701         the existing database - if you want to create a new database using the
702         imported data, then create a new database (or, tediously, retire all
703         the old data.)
704         '''
705         if len(args) < 2:
706             raise UsageError, 'Not enough arguments supplied'
707         if csv is None:
708             raise UsageError, \
709                 'Sorry, you need the csv module to use this function.\n'\
710                 'Get it from: http://www.object-craft.com.au/projects/csv/'
712         from roundup import hyperdb
714         # ensure that the properties and the CSV file headings match
715         try:
716             cl = self.db.getclass(classname)
717         except KeyError:
718             raise UsageError, 'no such class "%s"'%classname
719         f = open(args[1])
720         p = csv.parser(field_sep=':')
721         file_props = p.parse(f.readline())
722         props = cl.properties.keys()
723         m = file_props[:]
724         m.sort()
725         props.sort()
726         if m != props:
727             raise UsageError, 'Import file doesn\'t define the same '\
728                 'properties as "%s".'%args[0]
730         # loop through the file and create a node for each entry
731         n = range(len(props))
732         while 1:
733             line = f.readline()
734             if not line: break
736             # parse lines until we get a complete entry
737             while 1:
738                 l = p.parse(line)
739                 if l: break
741             # make the new node's property map
742             d = {}
743             for i in n:
744                 # Use eval to reverse the repr() used to output the CSV
745                 value = eval(l[i])
746                 # Figure the property for this column
747                 key = file_props[i]
748                 type = cl.properties[key]
749                 # Convert for property type
750                 if isinstance(type, hyperdb.Date):
751                     value = date.Date(value)
752                 elif isinstance(type, hyperdb.Interval):
753                     value = date.Interval(value)
754                 elif isinstance(type, hyperdb.Password):
755                     pwd = password.Password()
756                     pwd.unpack(value)
757                     value = pwd
758                 if value is not None:
759                     d[key] = value
761             # and create the new node
762             apply(cl.create, (), d)
763         return 0
765     def run_command(self, args):
766         '''Run a single command
767         '''
768         command = args[0]
770         # handle help now
771         if command == 'help':
772             if len(args)>1:
773                 self.do_help(args[1:])
774                 return 0
775             self.do_help(['help'])
776             return 0
777         if command == 'morehelp':
778             self.do_help(['help'])
779             self.help_commands()
780             self.help_all()
781             return 0
783         # figure what the command is
784         try:
785             functions = self.commands.get(command)
786         except KeyError:
787             # not a valid command
788             print 'Unknown command "%s" ("help commands" for a list)'%command
789             return 1
791         # check for multiple matches
792         if len(functions) > 1:
793             print 'Multiple commands match "%s": %s'%(command,
794                 ', '.join([i[0] for i in functions]))
795             return 1
796         command, function = functions[0]
798         # make sure we have an instance_home
799         while not self.instance_home:
800             self.instance_home = raw_input('Enter instance home: ').strip()
802         # before we open the db, we may be doing an init
803         if command == 'initialise':
804             return self.do_initialise(self.instance_home, args)
806         # get the instance
807         try:
808             instance = roundup.instance.open(self.instance_home)
809         except ValueError, message:
810             print "Couldn't open instance: %s"%message
811             return 1
812         self.db = instance.open('admin')
814         if len(args) < 2:
815             print function.__doc__
816             return 1
818         # do the command
819         ret = 0
820         try:
821             ret = function(args[1:])
822         except UsageError, message:
823             print 'Error: %s'%message
824             print function.__doc__
825             ret = 1
826         except:
827             import traceback
828             traceback.print_exc()
829             ret = 1
830         return ret
832     def interactive(self, ws_re=re.compile(r'\s+')):
833         '''Run in an interactive mode
834         '''
835         print 'Roundup {version} ready for input.'
836         print 'Type "help" for help.'
837         try:
838             import readline
839         except ImportError:
840             print "Note: command history and editing not available"
842         while 1:
843             try:
844                 command = raw_input('roundup> ')
845             except EOFError:
846                 print '.. exit'
847                 return 0
848             args = ws_re.split(command)
849             if not args: continue
850             if args[0] in ('quit', 'exit'): return 0
851             self.run_command(args)
853     def main(self):
854         try:
855             opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
856         except getopt.GetoptError, e:
857             self.usage(str(e))
858             return 1
860         # handle command-line args
861         self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
862         name = password = ''
863         if os.environ.has_key('ROUNDUP_LOGIN'):
864             l = os.environ['ROUNDUP_LOGIN'].split(':')
865             name = l[0]
866             if len(l) > 1:
867                 password = l[1]
868         self.comma_sep = 0
869         for opt, arg in opts:
870             if opt == '-h':
871                 self.usage()
872                 return 0
873             if opt == '-i':
874                 self.instance_home = arg
875             if opt == '-c':
876                 self.comma_sep = 1
878         # if no command - go interactive
879         ret = 0
880         if not args:
881             self.interactive()
882         else:
883             ret = self.run_command(args)
884         if self.db:
885             self.db.close()
886         return ret
889 if __name__ == '__main__':
890     tool = AdminTool()
891     sys.exit(tool.main())
894 # $Log: not supported by cvs2svn $
895 # Revision 1.44  2001/11/12 22:01:06  richard
896 # Fixed issues with nosy reaction and author copies.
898 # Revision 1.43  2001/11/09 22:33:28  richard
899 # More error handling fixes.
901 # Revision 1.42  2001/11/09 10:11:08  richard
902 #  . roundup-admin now handles all hyperdb exceptions
904 # Revision 1.41  2001/11/09 01:25:40  richard
905 # Should parse with python 1.5.2 now.
907 # Revision 1.40  2001/11/08 04:42:00  richard
908 # Expanded the already-abbreviated "initialise" and "specification" commands,
909 # and added a comment to the command help about the abbreviation.
911 # Revision 1.39  2001/11/08 04:29:59  richard
912 # roundup-admin now accepts abbreviated commands (eg. l = li = lis = list)
913 # [thanks Engelbert Gruber for the inspiration]
915 # Revision 1.38  2001/11/05 23:45:40  richard
916 # Fixed newuser_action so it sets the cookie with the unencrypted password.
917 # Also made it present nicer error messages (not tracebacks).
919 # Revision 1.37  2001/10/23 01:00:18  richard
920 # Re-enabled login and registration access after lopping them off via
921 # disabling access for anonymous users.
922 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
923 # a couple of bugs while I was there. Probably introduced a couple, but
924 # things seem to work OK at the moment.
926 # Revision 1.36  2001/10/21 00:45:15  richard
927 # Added author identification to e-mail messages from roundup.
929 # Revision 1.35  2001/10/20 11:58:48  richard
930 # Catch errors in login - no username or password supplied.
931 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
933 # Revision 1.34  2001/10/18 02:16:42  richard
934 # Oops, committed the admin script with the wierd #! line.
935 # Also, made the thing into a class to reduce parameter passing.
936 # Nuked the leading whitespace from the help __doc__ displays too.
938 # Revision 1.33  2001/10/17 23:13:19  richard
939 # Did a fair bit of work on the admin tool. Now has an extra command "table"
940 # which displays node information in a tabular format. Also fixed import and
941 # export so they work. Removed freshen.
942 # Fixed quopri usage in mailgw from bug reports.
944 # Revision 1.32  2001/10/17 06:57:29  richard
945 # Interactive startup blurb - need to figure how to get the version in there.
947 # Revision 1.31  2001/10/17 06:17:26  richard
948 # Now with readline support :)
950 # Revision 1.30  2001/10/17 06:04:00  richard
951 # Beginnings of an interactive mode for roundup-admin
953 # Revision 1.29  2001/10/16 03:48:01  richard
954 # admin tool now complains if a "find" is attempted with a non-link property.
956 # Revision 1.28  2001/10/13 00:07:39  richard
957 # More help in admin tool.
959 # Revision 1.27  2001/10/11 23:43:04  richard
960 # Implemented the comma-separated printing option in the admin tool.
961 # Fixed a typo (more of a vim-o actually :) in mailgw.
963 # Revision 1.26  2001/10/11 05:03:51  richard
964 # Marked the roundup-admin import/export as experimental since they're not fully
965 # operational.
967 # Revision 1.25  2001/10/10 04:12:32  richard
968 # The setup.cfg file is just causing pain. Away it goes.
970 # Revision 1.24  2001/10/10 03:54:57  richard
971 # Added database importing and exporting through CSV files.
972 # Uses the csv module from object-craft for exporting if it's available.
973 # Requires the csv module for importing.
975 # Revision 1.23  2001/10/09 23:36:25  richard
976 # Spit out command help if roundup-admin command doesn't get an argument.
978 # Revision 1.22  2001/10/09 07:25:59  richard
979 # Added the Password property type. See "pydoc roundup.password" for
980 # implementation details. Have updated some of the documentation too.
982 # Revision 1.21  2001/10/05 02:23:24  richard
983 #  . roundup-admin create now prompts for property info if none is supplied
984 #    on the command-line.
985 #  . hyperdb Class getprops() method may now return only the mutable
986 #    properties.
987 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
988 #    now support anonymous user access (read-only, unless there's an
989 #    "anonymous" user, in which case write access is permitted). Login
990 #    handling has been moved into cgi_client.Client.main()
991 #  . The "extended" schema is now the default in roundup init.
992 #  . The schemas have had their page headings modified to cope with the new
993 #    login handling. Existing installations should copy the interfaces.py
994 #    file from the roundup lib directory to their instance home.
995 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
996 #    Ping - has been removed.
997 #  . Fixed a whole bunch of places in the CGI interface where we should have
998 #    been returning Not Found instead of throwing an exception.
999 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
1000 #    an item now throws an exception.
1002 # Revision 1.20  2001/10/04 02:12:42  richard
1003 # Added nicer command-line item adding: passing no arguments will enter an
1004 # interactive more which asks for each property in turn. While I was at it, I
1005 # fixed an implementation problem WRT the spec - I wasn't raising a
1006 # ValueError if the key property was missing from a create(). Also added a
1007 # protected=boolean argument to getprops() so we can list only the mutable
1008 # properties (defaults to yes, which lists the immutables).
1010 # Revision 1.19  2001/10/01 06:40:43  richard
1011 # made do_get have the args in the correct order
1013 # Revision 1.18  2001/09/18 22:58:37  richard
1015 # Added some more help to roundu-admin
1017 # Revision 1.17  2001/08/28 05:58:33  anthonybaxter
1018 # added missing 'import' statements.
1020 # Revision 1.16  2001/08/12 06:32:36  richard
1021 # using isinstance(blah, Foo) now instead of isFooType
1023 # Revision 1.15  2001/08/07 00:24:42  richard
1024 # stupid typo
1026 # Revision 1.14  2001/08/07 00:15:51  richard
1027 # Added the copyright/license notice to (nearly) all files at request of
1028 # Bizar Software.
1030 # Revision 1.13  2001/08/05 07:44:13  richard
1031 # Instances are now opened by a special function that generates a unique
1032 # module name for the instances on import time.
1034 # Revision 1.12  2001/08/03 01:28:33  richard
1035 # Used the much nicer load_package, pointed out by Steve Majewski.
1037 # Revision 1.11  2001/08/03 00:59:34  richard
1038 # Instance import now imports the instance using imp.load_module so that
1039 # we can have instance homes of "roundup" or other existing python package
1040 # names.
1042 # Revision 1.10  2001/07/30 08:12:17  richard
1043 # Added time logging and file uploading to the templates.
1045 # Revision 1.9  2001/07/30 03:52:55  richard
1046 # init help now lists templates and backends
1048 # Revision 1.8  2001/07/30 02:37:07  richard
1049 # Freshen is really broken. Commented out.
1051 # Revision 1.7  2001/07/30 01:28:46  richard
1052 # Bugfixes
1054 # Revision 1.6  2001/07/30 00:57:51  richard
1055 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
1056 # better internal structure. It's just BETTER. :)
1058 # Revision 1.5  2001/07/30 00:04:48  richard
1059 # Made the "init" prompting more friendly.
1061 # Revision 1.4  2001/07/29 07:01:39  richard
1062 # Added vim command to all source so that we don't get no steenkin' tabs :)
1064 # Revision 1.3  2001/07/23 08:45:28  richard
1065 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
1066 # workable instance_home set up :)
1067 # _and_ anydbm has had its first test :)
1069 # Revision 1.2  2001/07/23 08:20:44  richard
1070 # Moved over to using marshal in the bsddb and anydbm backends.
1071 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
1072 #  retired - mod hyperdb.Class.list() so it lists retired nodes)
1074 # Revision 1.1  2001/07/23 03:46:48  richard
1075 # moving the bin files to facilitate out-of-the-boxness
1077 # Revision 1.1  2001/07/22 11:15:45  richard
1078 # More Grande Splite stuff
1081 # vim: set filetype=python ts=4 sw=4 et si