1 #! /Users/builder/bin/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.33 2001-10-17 23:13:19 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
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 def usage(message=''):
35 if message: message = 'Problem: '+message+'\n'
36 print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments>
38 Help:
39 roundup-admin -h
40 roundup-admin help -- this help
41 roundup-admin help <command> -- command-specific help
42 roundup-admin help all -- all available help
43 Options:
44 -i instance home -- specify the issue tracker "home directory" to administer
45 -u -- the user[:password] to use for commands
46 -c -- when outputting lists of data, just comma-separate them'''%message
47 help_commands()
49 def help_commands():
50 print 'Commands:',
51 commands = ['']
52 for command in figureCommands().values():
53 h = command.__doc__.split('\n')[0]
54 commands.append(h[7:])
55 commands.sort()
56 print '\n '.join(commands)
58 def help_all():
59 print '''
60 All commands (except help) require an instance specifier. This is just the path
61 to the roundup instance you're working with. A roundup instance is where
62 roundup keeps the database and configuration file that defines an issue
63 tracker. It may be thought of as the issue tracker's "home directory". It may
64 be specified in the environment variable ROUNDUP_INSTANCE or on the command
65 line as "-i instance".
67 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
69 Property values are represented as strings in command arguments and in the
70 printed results:
71 . Strings are, well, strings.
72 . Date values are printed in the full date format in the local time zone, and
73 accepted in the full format or any of the partial formats explained below.
74 . Link values are printed as node designators. When given as an argument,
75 node designators and key strings are both accepted.
76 . Multilink values are printed as lists of node designators joined by commas.
77 When given as an argument, node designators and key strings are both
78 accepted; an empty string, a single node, or a list of nodes joined by
79 commas is accepted.
81 When multiple nodes are specified to the roundup get or roundup set
82 commands, the specified properties are retrieved or set on all the listed
83 nodes.
85 When multiple results are returned by the roundup get or roundup find
86 commands, they are printed one per line (default) or joined by commas (with
87 the -c) option.
89 Where the command changes data, a login name/password is required. The
90 login may be specified as either "name" or "name:password".
91 . ROUNDUP_LOGIN environment variable
92 . the -u command-line option
93 If either the name or password is not supplied, they are obtained from the
94 command-line.
96 Date format examples:
97 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
98 "2000-04-17" means <Date 2000-04-17.00:00:00>
99 "01-25" means <Date yyyy-01-25.00:00:00>
100 "08-13.22:13" means <Date yyyy-08-14.03:13:00>
101 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
102 "14:25" means <Date yyyy-mm-dd.19:25:00>
103 "8:47:11" means <Date yyyy-mm-dd.13:47:11>
104 "." means "right now"
106 Command help:
107 '''
108 for name, command in figureCommands().items():
109 print '%s:'%name
110 print ' ',command.__doc__
112 def do_help(args):
113 '''Usage: help topic
114 Give help about topic.
116 commands -- list commands
117 <command> -- help specific to a command
118 initopts -- init command options
119 all -- all available help
120 '''
121 help = figureHelp().get(args[0], None)
122 if help:
123 help()
124 return
125 help = figureCommands().get(args[0], None)
126 if help:
127 print help.__doc__
128 else:
129 print 'Sorry, no help for "%s"'%args[0]
131 def help_initopts():
132 import roundup.templates
133 templates = roundup.templates.listTemplates()
134 print 'Templates:', ', '.join(templates)
135 import roundup.backends
136 backends = roundup.backends.__all__
137 print 'Back ends:', ', '.join(backends)
140 def do_init(instance_home, args, comma_sep=0):
141 '''Usage: init [template [backend [admin password]]]
142 Initialise a new Roundup instance.
144 The command will prompt for the instance home directory (if not supplied
145 through INSTANCE_HOME or the -i option. The template, backend and admin
146 password may be specified on the command-line as arguments, in that order.
148 See also initopts help.
149 '''
150 # select template
151 import roundup.templates
152 templates = roundup.templates.listTemplates()
153 template = len(args) > 1 and args[1] or ''
154 if template not in templates:
155 print 'Templates:', ', '.join(templates)
156 while template not in templates:
157 template = raw_input('Select template [extended]: ').strip()
158 if not template:
159 template = 'extended'
161 import roundup.backends
162 backends = roundup.backends.__all__
163 backend = len(args) > 2 and args[2] or ''
164 if backend not in backends:
165 print 'Back ends:', ', '.join(backends)
166 while backend not in backends:
167 backend = raw_input('Select backend [anydbm]: ').strip()
168 if not backend:
169 backend = 'anydbm'
170 if len(args) > 3:
171 adminpw = confirm = args[3]
172 else:
173 adminpw = ''
174 confirm = 'x'
175 while adminpw != confirm:
176 adminpw = getpass.getpass('Admin Password: ')
177 confirm = getpass.getpass(' Confirm: ')
178 init.init(instance_home, template, backend, adminpw)
179 return 0
182 def do_get(db, args, comma_sep=0):
183 '''Usage: get property designator[,designator]*
184 Get the given property of one or more designator(s).
186 Retrieves the property value of the nodes specified by the designators.
187 '''
188 propname = args[0]
189 designators = string.split(args[1], ',')
190 l = []
191 for designator in designators:
192 classname, nodeid = roundupdb.splitDesignator(designator)
193 if comma_sep:
194 l.append(db.getclass(classname).get(nodeid, propname))
195 else:
196 print db.getclass(classname).get(nodeid, propname)
197 if comma_sep:
198 print ','.join(l)
199 return 0
202 def do_set(db, args, comma_sep=0):
203 '''Usage: set designator[,designator]* propname=value ...
204 Set the given property of one or more designator(s).
206 Sets the property to the value for all designators given.
207 '''
208 from roundup import hyperdb
210 designators = string.split(args[0], ',')
211 props = {}
212 for prop in args[1:]:
213 key, value = prop.split('=')
214 props[key] = value
215 for designator in designators:
216 classname, nodeid = roundupdb.splitDesignator(designator)
217 cl = db.getclass(classname)
218 properties = cl.getprops()
219 for key, value in props.items():
220 type = properties[key]
221 if isinstance(type, hyperdb.String):
222 continue
223 elif isinstance(type, hyperdb.Password):
224 props[key] = password.Password(value)
225 elif isinstance(type, hyperdb.Date):
226 props[key] = date.Date(value)
227 elif isinstance(type, hyperdb.Interval):
228 props[key] = date.Interval(value)
229 elif isinstance(type, hyperdb.Link):
230 props[key] = value
231 elif isinstance(type, hyperdb.Multilink):
232 props[key] = value.split(',')
233 apply(cl.set, (nodeid, ), props)
234 return 0
236 def do_find(db, args, comma_sep=0):
237 '''Usage: find classname propname=value ...
238 Find the nodes of the given class with a given link property value.
240 Find the nodes of the given class with a given link property value. The
241 value may be either the nodeid of the linked node, or its key value.
242 '''
243 classname = args[0]
244 cl = db.getclass(classname)
246 # look up the linked-to class and get the nodeid that has the value
247 propname, value = args[1].split('=')
248 num_re = re.compile('^\d+$')
249 if not num_re.match(value):
250 propcl = cl.properties[propname]
251 if (not isinstance(propcl, hyperdb.Link) and not
252 isinstance(type, hyperdb.Multilink)):
253 print 'You may only "find" link properties'
254 return 1
255 propcl = db.getclass(propcl.classname)
256 value = propcl.lookup(value)
258 # now do the find
259 if comma_sep:
260 print ','.join(cl.find(**{propname: value}))
261 else:
262 print cl.find(**{propname: value})
263 return 0
265 def do_spec(db, args, comma_sep=0):
266 '''Usage: spec classname
267 Show the properties for a classname.
269 This lists the properties for a given class.
270 '''
271 classname = args[0]
272 cl = db.getclass(classname)
273 keyprop = cl.getkey()
274 for key, value in cl.properties.items():
275 if keyprop == key:
276 print '%s: %s (key property)'%(key, value)
277 else:
278 print '%s: %s'%(key, value)
280 def do_create(db, args, comma_sep=0):
281 '''Usage: create classname property=value ...
282 Create a new entry of a given class.
284 This creates a new entry of the given class using the property
285 name=value arguments provided on the command line after the "create"
286 command.
287 '''
288 from roundup import hyperdb
290 classname = args[0]
291 cl = db.getclass(classname)
292 props = {}
293 properties = cl.getprops(protected = 0)
294 if len(args) == 1:
295 # ask for the properties
296 for key, value in properties.items():
297 if key == 'id': continue
298 name = value.__class__.__name__
299 if isinstance(value , hyperdb.Password):
300 again = None
301 while value != again:
302 value = getpass.getpass('%s (Password): '%key.capitalize())
303 again = getpass.getpass(' %s (Again): '%key.capitalize())
304 if value != again: print 'Sorry, try again...'
305 if value:
306 props[key] = value
307 else:
308 value = raw_input('%s (%s): '%(key.capitalize(), name))
309 if value:
310 props[key] = value
311 else:
312 # use the args
313 for prop in args[1:]:
314 key, value = prop.split('=')
315 props[key] = value
317 # convert types
318 for key in props.keys():
319 type = properties[key]
320 if isinstance(type, hyperdb.Date):
321 props[key] = date.Date(value)
322 elif isinstance(type, hyperdb.Interval):
323 props[key] = date.Interval(value)
324 elif isinstance(type, hyperdb.Password):
325 props[key] = password.Password(value)
326 elif isinstance(type, hyperdb.Multilink):
327 props[key] = value.split(',')
329 if cl.getkey() and not props.has_key(cl.getkey()):
330 print "You must provide the '%s' property."%cl.getkey()
331 else:
332 print apply(cl.create, (), props)
334 return 0
336 def do_list(db, args, comma_sep=0):
337 '''Usage: list classname [property]
338 List the instances of a class.
340 Lists all instances of the given class. If the property is not
341 specified, the "label" property is used. The label property is tried
342 in order: the key, "name", "title" and then the first property,
343 alphabetically.
344 '''
345 classname = args[0]
346 cl = db.getclass(classname)
347 if len(args) > 1:
348 key = args[1]
349 else:
350 key = cl.labelprop()
351 if comma_sep:
352 print ','.join(cl.list())
353 else:
354 for nodeid in cl.list():
355 value = cl.get(nodeid, key)
356 print "%4s: %s"%(nodeid, value)
357 return 0
359 def do_table(db, args, comma_sep=None):
360 '''Usage: table classname [property[,property]*]
361 List the instances of a class in tabular form.
363 Lists all instances of the given class. If the properties are not
364 specified, all properties are displayed. By default, the column widths
365 are the width of the property names. The width may be explicitly defined
366 by defining the property as "name:width". For example::
367 roundup> table priority id,name:10
368 Id Name
369 1 fatal-bug
370 2 bug
371 3 usability
372 4 feature
373 '''
374 classname = args[0]
375 cl = db.getclass(classname)
376 if len(args) > 1:
377 prop_names = args[1].split(',')
378 else:
379 prop_names = cl.getprops().keys()
380 props = []
381 for name in prop_names:
382 if ':' in name:
383 name, width = name.split(':')
384 props.append((name, int(width)))
385 else:
386 props.append((name, len(name)))
388 print ' '.join([string.capitalize(name) for name, width in props])
389 for nodeid in cl.list():
390 l = []
391 for name, width in props:
392 if name != 'id':
393 value = str(cl.get(nodeid, name))
394 else:
395 value = str(nodeid)
396 f = '%%-%ds'%width
397 l.append(f%value[:width])
398 print ' '.join(l)
399 return 0
401 def do_history(db, args, comma_sep=0):
402 '''Usage: history designator
403 Show the history entries of a designator.
405 Lists the journal entries for the node identified by the designator.
406 '''
407 classname, nodeid = roundupdb.splitDesignator(args[0])
408 # TODO: handle the -c option?
409 print db.getclass(classname).history(nodeid)
410 return 0
412 def do_retire(db, args, comma_sep=0):
413 '''Usage: retire designator[,designator]*
414 Retire the node specified by designator.
416 This action indicates that a particular node is not to be retrieved by
417 the list or find commands, and its key value may be re-used.
418 '''
419 designators = string.split(args[0], ',')
420 for designator in designators:
421 classname, nodeid = roundupdb.splitDesignator(designator)
422 db.getclass(classname).retire(nodeid)
423 return 0
425 def do_export(db, args, comma_sep=0):
426 '''Usage: export class[,class] destination_dir
427 Export the database to tab-separated-value files.
429 This action exports the current data from the database into
430 tab-separated-value files that are placed in the nominated destination
431 directory. The journals are not exported.
432 '''
433 if len(args) < 2:
434 print do_export.__doc__
435 return 1
436 classes = string.split(args[0], ',')
437 dir = args[1]
439 # use the csv parser if we can - it's faster
440 if csv is not None:
441 p = csv.parser(field_sep=':')
443 # do all the classes specified
444 for classname in classes:
445 cl = db.getclass(classname)
446 f = open(os.path.join(dir, classname+'.csv'), 'w')
447 f.write(string.join(cl.properties.keys(), ':') + '\n')
449 # all nodes for this class
450 properties = cl.properties.items()
451 for nodeid in cl.list():
452 l = []
453 for prop, type in properties:
454 value = cl.get(nodeid, prop)
455 # convert data where needed
456 if isinstance(type, hyperdb.Date):
457 value = value.get_tuple()
458 elif isinstance(type, hyperdb.Interval):
459 value = value.get_tuple()
460 elif isinstance(type, hyperdb.Password):
461 value = str(value)
462 l.append(repr(value))
464 # now write
465 if csv is not None:
466 f.write(p.join(l) + '\n')
467 else:
468 # escape the individual entries to they're valid CSV
469 m = []
470 for entry in l:
471 if '"' in entry:
472 entry = '""'.join(entry.split('"'))
473 if ':' in entry:
474 entry = '"%s"'%entry
475 m.append(entry)
476 f.write(':'.join(m) + '\n')
477 return 0
479 def do_import(db, args, comma_sep=0):
480 '''Usage: import class file
481 Import the contents of the tab-separated-value file.
483 The file must define the same properties as the class (including having
484 a "header" line with those property names.) The new nodes are added to
485 the existing database - if you want to create a new database using the
486 imported data, then create a new database (or, tediously, retire all
487 the old data.)
488 '''
489 if len(args) < 2:
490 print do_import.__doc__
491 return 1
492 if csv is None:
493 print 'Sorry, you need the csv module to use this function.'
494 print 'Get it from: http://www.object-craft.com.au/projects/csv/'
495 return 1
497 from roundup import hyperdb
499 # ensure that the properties and the CSV file headings match
500 cl = db.getclass(args[0])
501 f = open(args[1])
502 p = csv.parser(field_sep=':')
503 file_props = p.parse(f.readline())
504 props = cl.properties.keys()
505 m = file_props[:]
506 m.sort()
507 props.sort()
508 if m != props:
509 print 'Import file doesn\'t define the same properties as "%s".'%args[0]
510 return 1
512 # loop through the file and create a node for each entry
513 n = range(len(props))
514 while 1:
515 line = f.readline()
516 if not line: break
518 # parse lines until we get a complete entry
519 while 1:
520 l = p.parse(line)
521 if l: break
523 # make the new node's property map
524 d = {}
525 for i in n:
526 # Use eval to reverse the repr() used to output the CSV
527 value = eval(l[i])
528 # Figure the property for this column
529 key = file_props[i]
530 type = cl.properties[key]
531 # Convert for property type
532 if isinstance(type, hyperdb.Date):
533 value = date.Date(value)
534 elif isinstance(type, hyperdb.Interval):
535 value = date.Interval(value)
536 elif isinstance(type, hyperdb.Password):
537 pwd = password.Password()
538 pwd.unpack(value)
539 value = pwd
540 if value is not None:
541 d[key] = value
543 # and create the new node
544 apply(cl.create, (), d)
545 return 0
547 def figureCommands():
548 d = {}
549 for k, v in globals().items():
550 if k[:3] == 'do_':
551 d[k[3:]] = v
552 return d
554 def figureHelp():
555 d = {}
556 for k, v in globals().items():
557 if k[:5] == 'help_':
558 d[k[5:]] = v
559 return d
561 class AdminTool:
562 def run_command(self, args):
563 '''Run a single command
564 '''
565 command = args[0]
567 # handle help now
568 if command == 'help':
569 if len(args)>1:
570 do_help(args[1:])
571 return 0
572 do_help(['help'])
573 return 0
574 if command == 'morehelp':
575 do_help(['help'])
576 help_commands()
577 help_all()
578 return 0
580 # make sure we have an instance_home
581 while not self.instance_home:
582 self.instance_home = raw_input('Enter instance home: ').strip()
584 # before we open the db, we may be doing an init
585 if command == 'init':
586 return do_init(self.instance_home, args)
588 function = figureCommands().get(command, None)
590 # not a valid command
591 if function is None:
592 print 'Unknown command "%s" ("help commands" for a list)'%command
593 return 1
595 # get the instance
596 instance = roundup.instance.open(self.instance_home)
597 db = instance.open('admin')
599 if len(args) < 2:
600 print function.__doc__
601 return 1
603 # do the command
604 try:
605 return function(db, args[1:], comma_sep=self.comma_sep)
606 finally:
607 db.close()
609 return 1
611 def interactive(self, ws_re=re.compile(r'\s+')):
612 '''Run in an interactive mode
613 '''
614 print 'Roundup {version} ready for input.'
615 print 'Type "help" for help.'
616 try:
617 import readline
618 except ImportError:
619 print "Note: command history and editing not available"
621 while 1:
622 try:
623 command = raw_input('roundup> ')
624 except EOFError:
625 print '.. exit'
626 return 0
627 args = ws_re.split(command)
628 if not args: continue
629 if args[0] in ('quit', 'exit'): return 0
630 self.run_command(args)
632 def main(self):
633 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
635 # handle command-line args
636 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
637 name = password = ''
638 if os.environ.has_key('ROUNDUP_LOGIN'):
639 l = os.environ['ROUNDUP_LOGIN'].split(':')
640 name = l[0]
641 if len(l) > 1:
642 password = l[1]
643 self.comma_sep = 0
644 for opt, arg in opts:
645 if opt == '-h':
646 usage()
647 return 0
648 if opt == '-i':
649 self.instance_home = arg
650 if opt == '-c':
651 self.comma_sep = 1
653 # if no command - go interactive
654 if not args:
655 return self.interactive()
657 self.run_command(args)
660 if __name__ == '__main__':
661 tool = AdminTool()
662 sys.exit(tool.main())
664 #
665 # $Log: not supported by cvs2svn $
666 # Revision 1.32 2001/10/17 06:57:29 richard
667 # Interactive startup blurb - need to figure how to get the version in there.
668 #
669 # Revision 1.31 2001/10/17 06:17:26 richard
670 # Now with readline support :)
671 #
672 # Revision 1.30 2001/10/17 06:04:00 richard
673 # Beginnings of an interactive mode for roundup-admin
674 #
675 # Revision 1.29 2001/10/16 03:48:01 richard
676 # admin tool now complains if a "find" is attempted with a non-link property.
677 #
678 # Revision 1.28 2001/10/13 00:07:39 richard
679 # More help in admin tool.
680 #
681 # Revision 1.27 2001/10/11 23:43:04 richard
682 # Implemented the comma-separated printing option in the admin tool.
683 # Fixed a typo (more of a vim-o actually :) in mailgw.
684 #
685 # Revision 1.26 2001/10/11 05:03:51 richard
686 # Marked the roundup-admin import/export as experimental since they're not fully
687 # operational.
688 #
689 # Revision 1.25 2001/10/10 04:12:32 richard
690 # The setup.cfg file is just causing pain. Away it goes.
691 #
692 # Revision 1.24 2001/10/10 03:54:57 richard
693 # Added database importing and exporting through CSV files.
694 # Uses the csv module from object-craft for exporting if it's available.
695 # Requires the csv module for importing.
696 #
697 # Revision 1.23 2001/10/09 23:36:25 richard
698 # Spit out command help if roundup-admin command doesn't get an argument.
699 #
700 # Revision 1.22 2001/10/09 07:25:59 richard
701 # Added the Password property type. See "pydoc roundup.password" for
702 # implementation details. Have updated some of the documentation too.
703 #
704 # Revision 1.21 2001/10/05 02:23:24 richard
705 # . roundup-admin create now prompts for property info if none is supplied
706 # on the command-line.
707 # . hyperdb Class getprops() method may now return only the mutable
708 # properties.
709 # . Login now uses cookies, which makes it a whole lot more flexible. We can
710 # now support anonymous user access (read-only, unless there's an
711 # "anonymous" user, in which case write access is permitted). Login
712 # handling has been moved into cgi_client.Client.main()
713 # . The "extended" schema is now the default in roundup init.
714 # . The schemas have had their page headings modified to cope with the new
715 # login handling. Existing installations should copy the interfaces.py
716 # file from the roundup lib directory to their instance home.
717 # . Incorrectly had a Bizar Software copyright on the cgitb.py module from
718 # Ping - has been removed.
719 # . Fixed a whole bunch of places in the CGI interface where we should have
720 # been returning Not Found instead of throwing an exception.
721 # . Fixed a deviation from the spec: trying to modify the 'id' property of
722 # an item now throws an exception.
723 #
724 # Revision 1.20 2001/10/04 02:12:42 richard
725 # Added nicer command-line item adding: passing no arguments will enter an
726 # interactive more which asks for each property in turn. While I was at it, I
727 # fixed an implementation problem WRT the spec - I wasn't raising a
728 # ValueError if the key property was missing from a create(). Also added a
729 # protected=boolean argument to getprops() so we can list only the mutable
730 # properties (defaults to yes, which lists the immutables).
731 #
732 # Revision 1.19 2001/10/01 06:40:43 richard
733 # made do_get have the args in the correct order
734 #
735 # Revision 1.18 2001/09/18 22:58:37 richard
736 #
737 # Added some more help to roundu-admin
738 #
739 # Revision 1.17 2001/08/28 05:58:33 anthonybaxter
740 # added missing 'import' statements.
741 #
742 # Revision 1.16 2001/08/12 06:32:36 richard
743 # using isinstance(blah, Foo) now instead of isFooType
744 #
745 # Revision 1.15 2001/08/07 00:24:42 richard
746 # stupid typo
747 #
748 # Revision 1.14 2001/08/07 00:15:51 richard
749 # Added the copyright/license notice to (nearly) all files at request of
750 # Bizar Software.
751 #
752 # Revision 1.13 2001/08/05 07:44:13 richard
753 # Instances are now opened by a special function that generates a unique
754 # module name for the instances on import time.
755 #
756 # Revision 1.12 2001/08/03 01:28:33 richard
757 # Used the much nicer load_package, pointed out by Steve Majewski.
758 #
759 # Revision 1.11 2001/08/03 00:59:34 richard
760 # Instance import now imports the instance using imp.load_module so that
761 # we can have instance homes of "roundup" or other existing python package
762 # names.
763 #
764 # Revision 1.10 2001/07/30 08:12:17 richard
765 # Added time logging and file uploading to the templates.
766 #
767 # Revision 1.9 2001/07/30 03:52:55 richard
768 # init help now lists templates and backends
769 #
770 # Revision 1.8 2001/07/30 02:37:07 richard
771 # Freshen is really broken. Commented out.
772 #
773 # Revision 1.7 2001/07/30 01:28:46 richard
774 # Bugfixes
775 #
776 # Revision 1.6 2001/07/30 00:57:51 richard
777 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
778 # better internal structure. It's just BETTER. :)
779 #
780 # Revision 1.5 2001/07/30 00:04:48 richard
781 # Made the "init" prompting more friendly.
782 #
783 # Revision 1.4 2001/07/29 07:01:39 richard
784 # Added vim command to all source so that we don't get no steenkin' tabs :)
785 #
786 # Revision 1.3 2001/07/23 08:45:28 richard
787 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
788 # workable instance_home set up :)
789 # _and_ anydbm has had its first test :)
790 #
791 # Revision 1.2 2001/07/23 08:20:44 richard
792 # Moved over to using marshal in the bsddb and anydbm backends.
793 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
794 # retired - mod hyperdb.Class.list() so it lists retired nodes)
795 #
796 # Revision 1.1 2001/07/23 03:46:48 richard
797 # moving the bin files to facilitate out-of-the-boxness
798 #
799 # Revision 1.1 2001/07/22 11:15:45 richard
800 # More Grande Splite stuff
801 #
802 #
803 # vim: set filetype=python ts=4 sw=4 et si