Code

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