Code

Fixed small bug that prevented adding issues through the web.
[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.50 2001-12-02 05:06:16 richard Exp $
21 import sys
22 if not hasattr(sys, 'version_info') or sys.version_info[:2] < (2,1):
23     print 'Roundup requires python 2.1 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_commit(self, args):
618         '''Usage: commit
619         Commit all changes made to the database.
621         The changes made during an interactive session are not
622         automatically written to the database - they must be committed
623         using this command.
625         One-off commands on the command-line are automatically committed if
626         they are successful.
627         '''
628         self.db.commit()
629         return 0
631     def do_rollback(self, args):
632         '''Usage: rollback
633         Undo all changes that are pending commit to the database.
635         The changes made during an interactive session are not
636         automatically written to the database - they must be committed
637         manually. This command undoes all those changes, so a commit
638         immediately after would make no changes to the database.
639         '''
640         self.db.commit()
641         return 0
643     def do_retire(self, args):
644         '''Usage: retire designator[,designator]*
645         Retire the node specified by designator.
647         This action indicates that a particular node is not to be retrieved by
648         the list or find commands, and its key value may be re-used.
649         '''
650         designators = string.split(args[0], ',')
651         for designator in designators:
652             try:
653                 classname, nodeid = roundupdb.splitDesignator(designator)
654             except roundupdb.DesignatorError, message:
655                 raise UsageError, message
656             try:
657                 self.db.getclass(classname).retire(nodeid)
658             except KeyError:
659                 raise UsageError, 'no such class "%s"'%classname
660             except IndexError:
661                 raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
662         return 0
664     def do_export(self, args):
665         '''Usage: export class[,class] destination_dir
666         Export the database to tab-separated-value files.
668         This action exports the current data from the database into
669         tab-separated-value files that are placed in the nominated destination
670         directory. The journals are not exported.
671         '''
672         if len(args) < 2:
673             print do_export.__doc__
674             return 1
675         classes = string.split(args[0], ',')
676         dir = args[1]
678         # use the csv parser if we can - it's faster
679         if csv is not None:
680             p = csv.parser(field_sep=':')
682         # do all the classes specified
683         for classname in classes:
684             try:
685                 cl = self.db.getclass(classname)
686             except KeyError:
687                 raise UsageError, 'no such class "%s"'%classname
688             f = open(os.path.join(dir, classname+'.csv'), 'w')
689             f.write(string.join(cl.properties.keys(), ':') + '\n')
691             # all nodes for this class
692             properties = cl.properties.items()
693             for nodeid in cl.list():
694                 l = []
695                 for prop, proptype in properties:
696                     value = cl.get(nodeid, prop)
697                     # convert data where needed
698                     if isinstance(proptype, hyperdb.Date):
699                         value = value.get_tuple()
700                     elif isinstance(proptype, hyperdb.Interval):
701                         value = value.get_tuple()
702                     elif isinstance(proptype, hyperdb.Password):
703                         value = str(value)
704                     l.append(repr(value))
706                 # now write
707                 if csv is not None:
708                    f.write(p.join(l) + '\n')
709                 else:
710                    # escape the individual entries to they're valid CSV
711                    m = []
712                    for entry in l:
713                       if '"' in entry:
714                           entry = '""'.join(entry.split('"'))
715                       if ':' in entry:
716                           entry = '"%s"'%entry
717                       m.append(entry)
718                    f.write(':'.join(m) + '\n')
719         return 0
721     def do_import(self, args):
722         '''Usage: import class file
723         Import the contents of the tab-separated-value file.
725         The file must define the same properties as the class (including having
726         a "header" line with those property names.) The new nodes are added to
727         the existing database - if you want to create a new database using the
728         imported data, then create a new database (or, tediously, retire all
729         the old data.)
730         '''
731         if len(args) < 2:
732             raise UsageError, 'Not enough arguments supplied'
733         if csv is None:
734             raise UsageError, \
735                 'Sorry, you need the csv module to use this function.\n'\
736                 'Get it from: http://www.object-craft.com.au/projects/csv/'
738         from roundup import hyperdb
740         # ensure that the properties and the CSV file headings match
741         classname = args[0]
742         try:
743             cl = self.db.getclass(classname)
744         except KeyError:
745             raise UsageError, 'no such class "%s"'%classname
746         f = open(args[1])
747         p = csv.parser(field_sep=':')
748         file_props = p.parse(f.readline())
749         props = cl.properties.keys()
750         m = file_props[:]
751         m.sort()
752         props.sort()
753         if m != props:
754             raise UsageError, 'Import file doesn\'t define the same '\
755                 'properties as "%s".'%args[0]
757         # loop through the file and create a node for each entry
758         n = range(len(props))
759         while 1:
760             line = f.readline()
761             if not line: break
763             # parse lines until we get a complete entry
764             while 1:
765                 l = p.parse(line)
766                 if l: break
768             # make the new node's property map
769             d = {}
770             for i in n:
771                 # Use eval to reverse the repr() used to output the CSV
772                 value = eval(l[i])
773                 # Figure the property for this column
774                 key = file_props[i]
775                 proptype = cl.properties[key]
776                 # Convert for property type
777                 if isinstance(proptype, hyperdb.Date):
778                     value = date.Date(value)
779                 elif isinstance(proptype, hyperdb.Interval):
780                     value = date.Interval(value)
781                 elif isinstance(proptype, hyperdb.Password):
782                     pwd = password.Password()
783                     pwd.unpack(value)
784                     value = pwd
785                 if value is not None:
786                     d[key] = value
788             # and create the new node
789             apply(cl.create, (), d)
790         return 0
792     def run_command(self, args):
793         '''Run a single command
794         '''
795         command = args[0]
797         # handle help now
798         if command == 'help':
799             if len(args)>1:
800                 self.do_help(args[1:])
801                 return 0
802             self.do_help(['help'])
803             return 0
804         if command == 'morehelp':
805             self.do_help(['help'])
806             self.help_commands()
807             self.help_all()
808             return 0
810         # figure what the command is
811         try:
812             functions = self.commands.get(command)
813         except KeyError:
814             # not a valid command
815             print 'Unknown command "%s" ("help commands" for a list)'%command
816             return 1
818         # check for multiple matches
819         if len(functions) > 1:
820             print 'Multiple commands match "%s": %s'%(command,
821                 ', '.join([i[0] for i in functions]))
822             return 1
823         command, function = functions[0]
825         # make sure we have an instance_home
826         while not self.instance_home:
827             self.instance_home = raw_input('Enter instance home: ').strip()
829         # before we open the db, we may be doing an init
830         if command == 'initialise':
831             return self.do_initialise(self.instance_home, args)
833         # get the instance
834         try:
835             instance = roundup.instance.open(self.instance_home)
836         except ValueError, message:
837             print "Couldn't open instance: %s"%message
838             return 1
839         self.db = instance.open('admin')
841         if len(args) < 2:
842             print function.__doc__
843             return 1
845         # do the command
846         ret = 0
847         try:
848             ret = function(args[1:])
849         except UsageError, message:
850             print 'Error: %s'%message
851             print function.__doc__
852             ret = 1
853         except:
854             import traceback
855             traceback.print_exc()
856             ret = 1
857         return ret
859     def interactive(self, ws_re=re.compile(r'\s+')):
860         '''Run in an interactive mode
861         '''
862         print 'Roundup {version} ready for input.'
863         print 'Type "help" for help.'
864         try:
865             import readline
866         except ImportError:
867             print "Note: command history and editing not available"
869         while 1:
870             try:
871                 command = raw_input('roundup> ')
872             except EOFError:
873                 print '.. exit'
874                 return 0
875             args = ws_re.split(command)
876             if not args: continue
877             if args[0] in ('quit', 'exit'): return 0
878             self.run_command(args)
880     def main(self):
881         try:
882             opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
883         except getopt.GetoptError, e:
884             self.usage(str(e))
885             return 1
887         # handle command-line args
888         self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
889         name = password = ''
890         if os.environ.has_key('ROUNDUP_LOGIN'):
891             l = os.environ['ROUNDUP_LOGIN'].split(':')
892             name = l[0]
893             if len(l) > 1:
894                 password = l[1]
895         self.comma_sep = 0
896         for opt, arg in opts:
897             if opt == '-h':
898                 self.usage()
899                 return 0
900             if opt == '-i':
901                 self.instance_home = arg
902             if opt == '-c':
903                 self.comma_sep = 1
905         # if no command - go interactive
906         ret = 0
907         if not args:
908             self.interactive()
909         else:
910             ret = self.run_command(args)
911             if self.db: self.db.commit()
912         return ret
915 if __name__ == '__main__':
916     tool = AdminTool()
917     sys.exit(tool.main())
920 # $Log: not supported by cvs2svn $
921 # Revision 1.49  2001/12/01 07:17:50  richard
922 # . We now have basic transaction support! Information is only written to
923 #   the database when the commit() method is called. Only the anydbm
924 #   backend is modified in this way - neither of the bsddb backends have been.
925 #   The mail, admin and cgi interfaces all use commit (except the admin tool
926 #   doesn't have a commit command, so interactive users can't commit...)
927 # . Fixed login/registration forwarding the user to the right page (or not,
928 #   on a failure)
930 # Revision 1.48  2001/11/27 22:32:03  richard
931 # typo
933 # Revision 1.47  2001/11/26 22:55:56  richard
934 # Feature:
935 #  . Added INSTANCE_NAME to configuration - used in web and email to identify
936 #    the instance.
937 #  . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
938 #    signature info in e-mails.
939 #  . Some more flexibility in the mail gateway and more error handling.
940 #  . Login now takes you to the page you back to the were denied access to.
942 # Fixed:
943 #  . Lots of bugs, thanks Roché and others on the devel mailing list!
945 # Revision 1.46  2001/11/21 03:40:54  richard
946 # more new property handling
948 # Revision 1.45  2001/11/12 22:51:59  jhermann
949 # Fixed option & associated error handling
951 # Revision 1.44  2001/11/12 22:01:06  richard
952 # Fixed issues with nosy reaction and author copies.
954 # Revision 1.43  2001/11/09 22:33:28  richard
955 # More error handling fixes.
957 # Revision 1.42  2001/11/09 10:11:08  richard
958 #  . roundup-admin now handles all hyperdb exceptions
960 # Revision 1.41  2001/11/09 01:25:40  richard
961 # Should parse with python 1.5.2 now.
963 # Revision 1.40  2001/11/08 04:42:00  richard
964 # Expanded the already-abbreviated "initialise" and "specification" commands,
965 # and added a comment to the command help about the abbreviation.
967 # Revision 1.39  2001/11/08 04:29:59  richard
968 # roundup-admin now accepts abbreviated commands (eg. l = li = lis = list)
969 # [thanks Engelbert Gruber for the inspiration]
971 # Revision 1.38  2001/11/05 23:45:40  richard
972 # Fixed newuser_action so it sets the cookie with the unencrypted password.
973 # Also made it present nicer error messages (not tracebacks).
975 # Revision 1.37  2001/10/23 01:00:18  richard
976 # Re-enabled login and registration access after lopping them off via
977 # disabling access for anonymous users.
978 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
979 # a couple of bugs while I was there. Probably introduced a couple, but
980 # things seem to work OK at the moment.
982 # Revision 1.36  2001/10/21 00:45:15  richard
983 # Added author identification to e-mail messages from roundup.
985 # Revision 1.35  2001/10/20 11:58:48  richard
986 # Catch errors in login - no username or password supplied.
987 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
989 # Revision 1.34  2001/10/18 02:16:42  richard
990 # Oops, committed the admin script with the wierd #! line.
991 # Also, made the thing into a class to reduce parameter passing.
992 # Nuked the leading whitespace from the help __doc__ displays too.
994 # Revision 1.33  2001/10/17 23:13:19  richard
995 # Did a fair bit of work on the admin tool. Now has an extra command "table"
996 # which displays node information in a tabular format. Also fixed import and
997 # export so they work. Removed freshen.
998 # Fixed quopri usage in mailgw from bug reports.
1000 # Revision 1.32  2001/10/17 06:57:29  richard
1001 # Interactive startup blurb - need to figure how to get the version in there.
1003 # Revision 1.31  2001/10/17 06:17:26  richard
1004 # Now with readline support :)
1006 # Revision 1.30  2001/10/17 06:04:00  richard
1007 # Beginnings of an interactive mode for roundup-admin
1009 # Revision 1.29  2001/10/16 03:48:01  richard
1010 # admin tool now complains if a "find" is attempted with a non-link property.
1012 # Revision 1.28  2001/10/13 00:07:39  richard
1013 # More help in admin tool.
1015 # Revision 1.27  2001/10/11 23:43:04  richard
1016 # Implemented the comma-separated printing option in the admin tool.
1017 # Fixed a typo (more of a vim-o actually :) in mailgw.
1019 # Revision 1.26  2001/10/11 05:03:51  richard
1020 # Marked the roundup-admin import/export as experimental since they're not fully
1021 # operational.
1023 # Revision 1.25  2001/10/10 04:12:32  richard
1024 # The setup.cfg file is just causing pain. Away it goes.
1026 # Revision 1.24  2001/10/10 03:54:57  richard
1027 # Added database importing and exporting through CSV files.
1028 # Uses the csv module from object-craft for exporting if it's available.
1029 # Requires the csv module for importing.
1031 # Revision 1.23  2001/10/09 23:36:25  richard
1032 # Spit out command help if roundup-admin command doesn't get an argument.
1034 # Revision 1.22  2001/10/09 07:25:59  richard
1035 # Added the Password property type. See "pydoc roundup.password" for
1036 # implementation details. Have updated some of the documentation too.
1038 # Revision 1.21  2001/10/05 02:23:24  richard
1039 #  . roundup-admin create now prompts for property info if none is supplied
1040 #    on the command-line.
1041 #  . hyperdb Class getprops() method may now return only the mutable
1042 #    properties.
1043 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
1044 #    now support anonymous user access (read-only, unless there's an
1045 #    "anonymous" user, in which case write access is permitted). Login
1046 #    handling has been moved into cgi_client.Client.main()
1047 #  . The "extended" schema is now the default in roundup init.
1048 #  . The schemas have had their page headings modified to cope with the new
1049 #    login handling. Existing installations should copy the interfaces.py
1050 #    file from the roundup lib directory to their instance home.
1051 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
1052 #    Ping - has been removed.
1053 #  . Fixed a whole bunch of places in the CGI interface where we should have
1054 #    been returning Not Found instead of throwing an exception.
1055 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
1056 #    an item now throws an exception.
1058 # Revision 1.20  2001/10/04 02:12:42  richard
1059 # Added nicer command-line item adding: passing no arguments will enter an
1060 # interactive more which asks for each property in turn. While I was at it, I
1061 # fixed an implementation problem WRT the spec - I wasn't raising a
1062 # ValueError if the key property was missing from a create(). Also added a
1063 # protected=boolean argument to getprops() so we can list only the mutable
1064 # properties (defaults to yes, which lists the immutables).
1066 # Revision 1.19  2001/10/01 06:40:43  richard
1067 # made do_get have the args in the correct order
1069 # Revision 1.18  2001/09/18 22:58:37  richard
1071 # Added some more help to roundu-admin
1073 # Revision 1.17  2001/08/28 05:58:33  anthonybaxter
1074 # added missing 'import' statements.
1076 # Revision 1.16  2001/08/12 06:32:36  richard
1077 # using isinstance(blah, Foo) now instead of isFooType
1079 # Revision 1.15  2001/08/07 00:24:42  richard
1080 # stupid typo
1082 # Revision 1.14  2001/08/07 00:15:51  richard
1083 # Added the copyright/license notice to (nearly) all files at request of
1084 # Bizar Software.
1086 # Revision 1.13  2001/08/05 07:44:13  richard
1087 # Instances are now opened by a special function that generates a unique
1088 # module name for the instances on import time.
1090 # Revision 1.12  2001/08/03 01:28:33  richard
1091 # Used the much nicer load_package, pointed out by Steve Majewski.
1093 # Revision 1.11  2001/08/03 00:59:34  richard
1094 # Instance import now imports the instance using imp.load_module so that
1095 # we can have instance homes of "roundup" or other existing python package
1096 # names.
1098 # Revision 1.10  2001/07/30 08:12:17  richard
1099 # Added time logging and file uploading to the templates.
1101 # Revision 1.9  2001/07/30 03:52:55  richard
1102 # init help now lists templates and backends
1104 # Revision 1.8  2001/07/30 02:37:07  richard
1105 # Freshen is really broken. Commented out.
1107 # Revision 1.7  2001/07/30 01:28:46  richard
1108 # Bugfixes
1110 # Revision 1.6  2001/07/30 00:57:51  richard
1111 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
1112 # better internal structure. It's just BETTER. :)
1114 # Revision 1.5  2001/07/30 00:04:48  richard
1115 # Made the "init" prompting more friendly.
1117 # Revision 1.4  2001/07/29 07:01:39  richard
1118 # Added vim command to all source so that we don't get no steenkin' tabs :)
1120 # Revision 1.3  2001/07/23 08:45:28  richard
1121 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
1122 # workable instance_home set up :)
1123 # _and_ anydbm has had its first test :)
1125 # Revision 1.2  2001/07/23 08:20:44  richard
1126 # Moved over to using marshal in the bsddb and anydbm backends.
1127 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
1128 #  retired - mod hyperdb.Class.list() so it lists retired nodes)
1130 # Revision 1.1  2001/07/23 03:46:48  richard
1131 # moving the bin files to facilitate out-of-the-boxness
1133 # Revision 1.1  2001/07/22 11:15:45  richard
1134 # More Grande Splite stuff
1137 # vim: set filetype=python ts=4 sw=4 et si