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())
919 #
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)
929 #
930 # Revision 1.48 2001/11/27 22:32:03 richard
931 # typo
932 #
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.
941 #
942 # Fixed:
943 # . Lots of bugs, thanks Roché and others on the devel mailing list!
944 #
945 # Revision 1.46 2001/11/21 03:40:54 richard
946 # more new property handling
947 #
948 # Revision 1.45 2001/11/12 22:51:59 jhermann
949 # Fixed option & associated error handling
950 #
951 # Revision 1.44 2001/11/12 22:01:06 richard
952 # Fixed issues with nosy reaction and author copies.
953 #
954 # Revision 1.43 2001/11/09 22:33:28 richard
955 # More error handling fixes.
956 #
957 # Revision 1.42 2001/11/09 10:11:08 richard
958 # . roundup-admin now handles all hyperdb exceptions
959 #
960 # Revision 1.41 2001/11/09 01:25:40 richard
961 # Should parse with python 1.5.2 now.
962 #
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.
966 #
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]
970 #
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).
974 #
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.
981 #
982 # Revision 1.36 2001/10/21 00:45:15 richard
983 # Added author identification to e-mail messages from roundup.
984 #
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.
988 #
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.
993 #
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.
999 #
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.
1002 #
1003 # Revision 1.31 2001/10/17 06:17:26 richard
1004 # Now with readline support :)
1005 #
1006 # Revision 1.30 2001/10/17 06:04:00 richard
1007 # Beginnings of an interactive mode for roundup-admin
1008 #
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.
1011 #
1012 # Revision 1.28 2001/10/13 00:07:39 richard
1013 # More help in admin tool.
1014 #
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.
1018 #
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.
1022 #
1023 # Revision 1.25 2001/10/10 04:12:32 richard
1024 # The setup.cfg file is just causing pain. Away it goes.
1025 #
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.
1030 #
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.
1033 #
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.
1037 #
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.
1057 #
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).
1065 #
1066 # Revision 1.19 2001/10/01 06:40:43 richard
1067 # made do_get have the args in the correct order
1068 #
1069 # Revision 1.18 2001/09/18 22:58:37 richard
1070 #
1071 # Added some more help to roundu-admin
1072 #
1073 # Revision 1.17 2001/08/28 05:58:33 anthonybaxter
1074 # added missing 'import' statements.
1075 #
1076 # Revision 1.16 2001/08/12 06:32:36 richard
1077 # using isinstance(blah, Foo) now instead of isFooType
1078 #
1079 # Revision 1.15 2001/08/07 00:24:42 richard
1080 # stupid typo
1081 #
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.
1085 #
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.
1089 #
1090 # Revision 1.12 2001/08/03 01:28:33 richard
1091 # Used the much nicer load_package, pointed out by Steve Majewski.
1092 #
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.
1097 #
1098 # Revision 1.10 2001/07/30 08:12:17 richard
1099 # Added time logging and file uploading to the templates.
1100 #
1101 # Revision 1.9 2001/07/30 03:52:55 richard
1102 # init help now lists templates and backends
1103 #
1104 # Revision 1.8 2001/07/30 02:37:07 richard
1105 # Freshen is really broken. Commented out.
1106 #
1107 # Revision 1.7 2001/07/30 01:28:46 richard
1108 # Bugfixes
1109 #
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. :)
1113 #
1114 # Revision 1.5 2001/07/30 00:04:48 richard
1115 # Made the "init" prompting more friendly.
1116 #
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 :)
1119 #
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 :)
1124 #
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)
1129 #
1130 # Revision 1.1 2001/07/23 03:46:48 richard
1131 # moving the bin files to facilitate out-of-the-boxness
1132 #
1133 # Revision 1.1 2001/07/22 11:15:45 richard
1134 # More Grande Splite stuff
1135 #
1136 #
1137 # vim: set filetype=python ts=4 sw=4 et si