Code

984485ee4751bc5915723523b7e40a00f1c444ea
[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.48 2001-11-27 22:32:03 richard 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                 proptype =  properties[key]
310                 if isinstance(proptype, hyperdb.String):
311                     continue
312                 elif isinstance(proptype, hyperdb.Password):
313                     props[key] = password.Password(value)
314                 elif isinstance(proptype, hyperdb.Date):
315                     try:
316                         props[key] = date.Date(value)
317                     except ValueError, message:
318                         raise UsageError, '"%s": %s'%(value, message)
319                 elif isinstance(proptype, hyperdb.Interval):
320                     try:
321                         props[key] = date.Interval(value)
322                     except ValueError, message:
323                         raise UsageError, '"%s": %s'%(value, message)
324                 elif isinstance(proptype, hyperdb.Link):
325                     props[key] = value
326                 elif isinstance(proptype, 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 args[1].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(property, 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                 proptype = properties[key]
473             except KeyError:
474                 raise UsageError, '%s has no property "%s"'%(classname, key)
476             if isinstance(proptype, hyperdb.Date):
477                 try:
478                     props[key] = date.Date(value)
479                 except ValueError, message:
480                     raise UsageError, '"%s": %s'%(value, message)
481             elif isinstance(proptype, hyperdb.Interval):
482                 try:
483                     props[key] = date.Interval(value)
484                 except ValueError, message:
485                     raise UsageError, '"%s": %s'%(value, message)
486             elif isinstance(proptype, hyperdb.Password):
487                 props[key] = password.Password(value)
488             elif isinstance(proptype, 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, proptype in properties:
670                     value = cl.get(nodeid, prop)
671                     # convert data where needed
672                     if isinstance(proptype, hyperdb.Date):
673                         value = value.get_tuple()
674                     elif isinstance(proptype, hyperdb.Interval):
675                         value = value.get_tuple()
676                     elif isinstance(proptype, 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         classname = args[0]
716         try:
717             cl = self.db.getclass(classname)
718         except KeyError:
719             raise UsageError, 'no such class "%s"'%classname
720         f = open(args[1])
721         p = csv.parser(field_sep=':')
722         file_props = p.parse(f.readline())
723         props = cl.properties.keys()
724         m = file_props[:]
725         m.sort()
726         props.sort()
727         if m != props:
728             raise UsageError, 'Import file doesn\'t define the same '\
729                 'properties as "%s".'%args[0]
731         # loop through the file and create a node for each entry
732         n = range(len(props))
733         while 1:
734             line = f.readline()
735             if not line: break
737             # parse lines until we get a complete entry
738             while 1:
739                 l = p.parse(line)
740                 if l: break
742             # make the new node's property map
743             d = {}
744             for i in n:
745                 # Use eval to reverse the repr() used to output the CSV
746                 value = eval(l[i])
747                 # Figure the property for this column
748                 key = file_props[i]
749                 proptype = cl.properties[key]
750                 # Convert for property type
751                 if isinstance(proptype, hyperdb.Date):
752                     value = date.Date(value)
753                 elif isinstance(proptype, hyperdb.Interval):
754                     value = date.Interval(value)
755                 elif isinstance(proptype, hyperdb.Password):
756                     pwd = password.Password()
757                     pwd.unpack(value)
758                     value = pwd
759                 if value is not None:
760                     d[key] = value
762             # and create the new node
763             apply(cl.create, (), d)
764         return 0
766     def run_command(self, args):
767         '''Run a single command
768         '''
769         command = args[0]
771         # handle help now
772         if command == 'help':
773             if len(args)>1:
774                 self.do_help(args[1:])
775                 return 0
776             self.do_help(['help'])
777             return 0
778         if command == 'morehelp':
779             self.do_help(['help'])
780             self.help_commands()
781             self.help_all()
782             return 0
784         # figure what the command is
785         try:
786             functions = self.commands.get(command)
787         except KeyError:
788             # not a valid command
789             print 'Unknown command "%s" ("help commands" for a list)'%command
790             return 1
792         # check for multiple matches
793         if len(functions) > 1:
794             print 'Multiple commands match "%s": %s'%(command,
795                 ', '.join([i[0] for i in functions]))
796             return 1
797         command, function = functions[0]
799         # make sure we have an instance_home
800         while not self.instance_home:
801             self.instance_home = raw_input('Enter instance home: ').strip()
803         # before we open the db, we may be doing an init
804         if command == 'initialise':
805             return self.do_initialise(self.instance_home, args)
807         # get the instance
808         try:
809             instance = roundup.instance.open(self.instance_home)
810         except ValueError, message:
811             print "Couldn't open instance: %s"%message
812             return 1
813         self.db = instance.open('admin')
815         if len(args) < 2:
816             print function.__doc__
817             return 1
819         # do the command
820         ret = 0
821         try:
822             ret = function(args[1:])
823         except UsageError, message:
824             print 'Error: %s'%message
825             print function.__doc__
826             ret = 1
827         except:
828             import traceback
829             traceback.print_exc()
830             ret = 1
831         return ret
833     def interactive(self, ws_re=re.compile(r'\s+')):
834         '''Run in an interactive mode
835         '''
836         print 'Roundup {version} ready for input.'
837         print 'Type "help" for help.'
838         try:
839             import readline
840         except ImportError:
841             print "Note: command history and editing not available"
843         while 1:
844             try:
845                 command = raw_input('roundup> ')
846             except EOFError:
847                 print '.. exit'
848                 return 0
849             args = ws_re.split(command)
850             if not args: continue
851             if args[0] in ('quit', 'exit'): return 0
852             self.run_command(args)
854     def main(self):
855         try:
856             opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
857         except getopt.GetoptError, e:
858             self.usage(str(e))
859             return 1
861         # handle command-line args
862         self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
863         name = password = ''
864         if os.environ.has_key('ROUNDUP_LOGIN'):
865             l = os.environ['ROUNDUP_LOGIN'].split(':')
866             name = l[0]
867             if len(l) > 1:
868                 password = l[1]
869         self.comma_sep = 0
870         for opt, arg in opts:
871             if opt == '-h':
872                 self.usage()
873                 return 0
874             if opt == '-i':
875                 self.instance_home = arg
876             if opt == '-c':
877                 self.comma_sep = 1
879         # if no command - go interactive
880         ret = 0
881         if not args:
882             self.interactive()
883         else:
884             ret = self.run_command(args)
885         if self.db:
886             self.db.close()
887         return ret
890 if __name__ == '__main__':
891     tool = AdminTool()
892     sys.exit(tool.main())
895 # $Log: not supported by cvs2svn $
896 # Revision 1.47  2001/11/26 22:55:56  richard
897 # Feature:
898 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
899 #    the instance.
900 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
901 #    signature info in e-mails.
902 #  . Some more flexibility in the mail gateway and more error handling.
903 #  . Login now takes you to the page you back to the were denied access to.
905 # Fixed:
906 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
908 # Revision 1.46  2001/11/21 03:40:54  richard
909 # more new property handling
911 # Revision 1.45  2001/11/12 22:51:59  jhermann
912 # Fixed option & associated error handling
914 # Revision 1.44  2001/11/12 22:01:06  richard
915 # Fixed issues with nosy reaction and author copies.
917 # Revision 1.43  2001/11/09 22:33:28  richard
918 # More error handling fixes.
920 # Revision 1.42  2001/11/09 10:11:08  richard
921 #  . roundup-admin now handles all hyperdb exceptions
923 # Revision 1.41  2001/11/09 01:25:40  richard
924 # Should parse with python 1.5.2 now.
926 # Revision 1.40  2001/11/08 04:42:00  richard
927 # Expanded the already-abbreviated "initialise" and "specification" commands,
928 # and added a comment to the command help about the abbreviation.
930 # Revision 1.39  2001/11/08 04:29:59  richard
931 # roundup-admin now accepts abbreviated commands (eg. l = li = lis = list)
932 # [thanks Engelbert Gruber for the inspiration]
934 # Revision 1.38  2001/11/05 23:45:40  richard
935 # Fixed newuser_action so it sets the cookie with the unencrypted password.
936 # Also made it present nicer error messages (not tracebacks).
938 # Revision 1.37  2001/10/23 01:00:18  richard
939 # Re-enabled login and registration access after lopping them off via
940 # disabling access for anonymous users.
941 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
942 # a couple of bugs while I was there. Probably introduced a couple, but
943 # things seem to work OK at the moment.
945 # Revision 1.36  2001/10/21 00:45:15  richard
946 # Added author identification to e-mail messages from roundup.
948 # Revision 1.35  2001/10/20 11:58:48  richard
949 # Catch errors in login - no username or password supplied.
950 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
952 # Revision 1.34  2001/10/18 02:16:42  richard
953 # Oops, committed the admin script with the wierd #! line.
954 # Also, made the thing into a class to reduce parameter passing.
955 # Nuked the leading whitespace from the help __doc__ displays too.
957 # Revision 1.33  2001/10/17 23:13:19  richard
958 # Did a fair bit of work on the admin tool. Now has an extra command "table"
959 # which displays node information in a tabular format. Also fixed import and
960 # export so they work. Removed freshen.
961 # Fixed quopri usage in mailgw from bug reports.
963 # Revision 1.32  2001/10/17 06:57:29  richard
964 # Interactive startup blurb - need to figure how to get the version in there.
966 # Revision 1.31  2001/10/17 06:17:26  richard
967 # Now with readline support :)
969 # Revision 1.30  2001/10/17 06:04:00  richard
970 # Beginnings of an interactive mode for roundup-admin
972 # Revision 1.29  2001/10/16 03:48:01  richard
973 # admin tool now complains if a "find" is attempted with a non-link property.
975 # Revision 1.28  2001/10/13 00:07:39  richard
976 # More help in admin tool.
978 # Revision 1.27  2001/10/11 23:43:04  richard
979 # Implemented the comma-separated printing option in the admin tool.
980 # Fixed a typo (more of a vim-o actually :) in mailgw.
982 # Revision 1.26  2001/10/11 05:03:51  richard
983 # Marked the roundup-admin import/export as experimental since they're not fully
984 # operational.
986 # Revision 1.25  2001/10/10 04:12:32  richard
987 # The setup.cfg file is just causing pain. Away it goes.
989 # Revision 1.24  2001/10/10 03:54:57  richard
990 # Added database importing and exporting through CSV files.
991 # Uses the csv module from object-craft for exporting if it's available.
992 # Requires the csv module for importing.
994 # Revision 1.23  2001/10/09 23:36:25  richard
995 # Spit out command help if roundup-admin command doesn't get an argument.
997 # Revision 1.22  2001/10/09 07:25:59  richard
998 # Added the Password property type. See "pydoc roundup.password" for
999 # implementation details. Have updated some of the documentation too.
1001 # Revision 1.21  2001/10/05 02:23:24  richard
1002 #  . roundup-admin create now prompts for property info if none is supplied
1003 #    on the command-line.
1004 #  . hyperdb Class getprops() method may now return only the mutable
1005 #    properties.
1006 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
1007 #    now support anonymous user access (read-only, unless there's an
1008 #    "anonymous" user, in which case write access is permitted). Login
1009 #    handling has been moved into cgi_client.Client.main()
1010 #  . The "extended" schema is now the default in roundup init.
1011 #  . The schemas have had their page headings modified to cope with the new
1012 #    login handling. Existing installations should copy the interfaces.py
1013 #    file from the roundup lib directory to their instance home.
1014 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
1015 #    Ping - has been removed.
1016 #  . Fixed a whole bunch of places in the CGI interface where we should have
1017 #    been returning Not Found instead of throwing an exception.
1018 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
1019 #    an item now throws an exception.
1021 # Revision 1.20  2001/10/04 02:12:42  richard
1022 # Added nicer command-line item adding: passing no arguments will enter an
1023 # interactive more which asks for each property in turn. While I was at it, I
1024 # fixed an implementation problem WRT the spec - I wasn't raising a
1025 # ValueError if the key property was missing from a create(). Also added a
1026 # protected=boolean argument to getprops() so we can list only the mutable
1027 # properties (defaults to yes, which lists the immutables).
1029 # Revision 1.19  2001/10/01 06:40:43  richard
1030 # made do_get have the args in the correct order
1032 # Revision 1.18  2001/09/18 22:58:37  richard
1034 # Added some more help to roundu-admin
1036 # Revision 1.17  2001/08/28 05:58:33  anthonybaxter
1037 # added missing 'import' statements.
1039 # Revision 1.16  2001/08/12 06:32:36  richard
1040 # using isinstance(blah, Foo) now instead of isFooType
1042 # Revision 1.15  2001/08/07 00:24:42  richard
1043 # stupid typo
1045 # Revision 1.14  2001/08/07 00:15:51  richard
1046 # Added the copyright/license notice to (nearly) all files at request of
1047 # Bizar Software.
1049 # Revision 1.13  2001/08/05 07:44:13  richard
1050 # Instances are now opened by a special function that generates a unique
1051 # module name for the instances on import time.
1053 # Revision 1.12  2001/08/03 01:28:33  richard
1054 # Used the much nicer load_package, pointed out by Steve Majewski.
1056 # Revision 1.11  2001/08/03 00:59:34  richard
1057 # Instance import now imports the instance using imp.load_module so that
1058 # we can have instance homes of "roundup" or other existing python package
1059 # names.
1061 # Revision 1.10  2001/07/30 08:12:17  richard
1062 # Added time logging and file uploading to the templates.
1064 # Revision 1.9  2001/07/30 03:52:55  richard
1065 # init help now lists templates and backends
1067 # Revision 1.8  2001/07/30 02:37:07  richard
1068 # Freshen is really broken. Commented out.
1070 # Revision 1.7  2001/07/30 01:28:46  richard
1071 # Bugfixes
1073 # Revision 1.6  2001/07/30 00:57:51  richard
1074 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
1075 # better internal structure. It's just BETTER. :)
1077 # Revision 1.5  2001/07/30 00:04:48  richard
1078 # Made the "init" prompting more friendly.
1080 # Revision 1.4  2001/07/29 07:01:39  richard
1081 # Added vim command to all source so that we don't get no steenkin' tabs :)
1083 # Revision 1.3  2001/07/23 08:45:28  richard
1084 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
1085 # workable instance_home set up :)
1086 # _and_ anydbm has had its first test :)
1088 # Revision 1.2  2001/07/23 08:20:44  richard
1089 # Moved over to using marshal in the bsddb and anydbm backends.
1090 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
1091 #  retired - mod hyperdb.Class.list() so it lists retired nodes)
1093 # Revision 1.1  2001/07/23 03:46:48  richard
1094 # moving the bin files to facilitate out-of-the-boxness
1096 # Revision 1.1  2001/07/22 11:15:45  richard
1097 # More Grande Splite stuff
1100 # vim: set filetype=python ts=4 sw=4 et si