Code

Install roundup.cgi to share/roundup
[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.38 2001-11-05 23:45:40 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 in AdminTool.__dict__.keys():
39             if k[:3] == 'do_':
40                 self.commands[k[3:]] = getattr(self, k)
41         self.help = {}
42         for k in AdminTool.__dict__.keys():
43             if k[:5] == 'help_':
44                 self.help[k[5:]] = getattr(self, k)
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()
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(self, 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 [classic]: ').strip()
181             if not template:
182                 template = 'classic'
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             try:
216                 classname, nodeid = roundupdb.splitDesignator(designator)
217             except roundupdb.DesignatorError, message:
218                 print 'Error: %s'%message
219                 return 1
220             if self.comma_sep:
221                 l.append(self.db.getclass(classname).get(nodeid, propname))
222             else:
223                 print self.db.getclass(classname).get(nodeid, propname)
224         if self.comma_sep:
225             print ','.join(l)
226         return 0
229     def do_set(self, args):
230         '''Usage: set designator[,designator]* propname=value ...
231         Set the given property of one or more designator(s).
233         Sets the property to the value for all designators given.
234         '''
235         from roundup import hyperdb
237         designators = string.split(args[0], ',')
238         props = {}
239         for prop in args[1:]:
240             key, value = prop.split('=')
241             props[key] = value
242         for designator in designators:
243             try:
244                 classname, nodeid = roundupdb.splitDesignator(designator)
245             except roundupdb.DesignatorError, message:
246                 print 'Error: %s'%message
247                 return 1
248             cl = self.db.getclass(classname)
249             properties = cl.getprops()
250             for key, value in props.items():
251                 type =  properties[key]
252                 if isinstance(type, hyperdb.String):
253                     continue
254                 elif isinstance(type, hyperdb.Password):
255                     props[key] = password.Password(value)
256                 elif isinstance(type, hyperdb.Date):
257                     props[key] = date.Date(value)
258                 elif isinstance(type, hyperdb.Interval):
259                     props[key] = date.Interval(value)
260                 elif isinstance(type, hyperdb.Link):
261                     props[key] = value
262                 elif isinstance(type, hyperdb.Multilink):
263                     props[key] = value.split(',')
264             apply(cl.set, (nodeid, ), props)
265         return 0
267     def do_find(self, args):
268         '''Usage: find classname propname=value ...
269         Find the nodes of the given class with a given link property value.
271         Find the nodes of the given class with a given link property value. The
272         value may be either the nodeid of the linked node, or its key value.
273         '''
274         classname = args[0]
275         cl = self.db.getclass(classname)
277         # look up the linked-to class and get the nodeid that has the value
278         propname, value = args[1].split('=')
279         num_re = re.compile('^\d+$')
280         if not num_re.match(value):
281             propcl = cl.properties[propname]
282             if (not isinstance(propcl, hyperdb.Link) and not
283                     isinstance(type, hyperdb.Multilink)):
284                 print 'You may only "find" link properties'
285                 return 1
286             propcl = self.db.getclass(propcl.classname)
287             value = propcl.lookup(value)
289         # now do the find
290         if self.comma_sep:
291             print ','.join(cl.find(**{propname: value}))
292         else:
293             print cl.find(**{propname: value})
294         return 0
296     def do_spec(self, args):
297         '''Usage: spec classname
298         Show the properties for a classname.
300         This lists the properties for a given class.
301         '''
302         classname = args[0]
303         cl = self.db.getclass(classname)
304         keyprop = cl.getkey()
305         for key, value in cl.properties.items():
306             if keyprop == key:
307                 print '%s: %s (key property)'%(key, value)
308             else:
309                 print '%s: %s'%(key, value)
311     def do_create(self, args):
312         '''Usage: create classname property=value ...
313         Create a new entry of a given class.
315         This creates a new entry of the given class using the property
316         name=value arguments provided on the command line after the "create"
317         command.
318         '''
319         from roundup import hyperdb
321         classname = args[0]
322         cl = self.db.getclass(classname)
323         props = {}
324         properties = cl.getprops(protected = 0)
325         if len(args) == 1:
326             # ask for the properties
327             for key, value in properties.items():
328                 if key == 'id': continue
329                 name = value.__class__.__name__
330                 if isinstance(value , hyperdb.Password):
331                     again = None
332                     while value != again:
333                         value = getpass.getpass('%s (Password): '%key.capitalize())
334                         again = getpass.getpass('   %s (Again): '%key.capitalize())
335                         if value != again: print 'Sorry, try again...'
336                     if value:
337                         props[key] = value
338                 else:
339                     value = raw_input('%s (%s): '%(key.capitalize(), name))
340                     if value:
341                         props[key] = value
342         else:
343             # use the args
344             for prop in args[1:]:
345                 key, value = prop.split('=')
346                 props[key] = value 
348         # convert types
349         for key in props.keys():
350             type =  properties[key]
351             if isinstance(type, hyperdb.Date):
352                 props[key] = date.Date(value)
353             elif isinstance(type, hyperdb.Interval):
354                 props[key] = date.Interval(value)
355             elif isinstance(type, hyperdb.Password):
356                 props[key] = password.Password(value)
357             elif isinstance(type, hyperdb.Multilink):
358                 props[key] = value.split(',')
360         if cl.getkey() and not props.has_key(cl.getkey()):
361             print "You must provide the '%s' property."%cl.getkey()
362         else:
363             print apply(cl.create, (), props)
365         return 0
367     def do_list(self, args):
368         '''Usage: list classname [property]
369         List the instances of a class.
371         Lists all instances of the given class. If the property is not
372         specified, the  "label" property is used. The label property is tried
373         in order: the key, "name", "title" and then the first property,
374         alphabetically.
375         '''
376         classname = args[0]
377         cl = self.db.getclass(classname)
378         if len(args) > 1:
379             key = args[1]
380         else:
381             key = cl.labelprop()
382         if self.comma_sep:
383             print ','.join(cl.list())
384         else:
385             for nodeid in cl.list():
386                 value = cl.get(nodeid, key)
387                 print "%4s: %s"%(nodeid, value)
388         return 0
390     def do_table(self, args):
391         '''Usage: table classname [property[,property]*]
392         List the instances of a class in tabular form.
394         Lists all instances of the given class. If the properties are not
395         specified, all properties are displayed. By default, the column widths
396         are the width of the property names. The width may be explicitly defined
397         by defining the property as "name:width". For example::
398           roundup> table priority id,name:10
399           Id Name
400           1  fatal-bug 
401           2  bug       
402           3  usability 
403           4  feature   
404         '''
405         classname = args[0]
406         cl = self.db.getclass(classname)
407         if len(args) > 1:
408             prop_names = args[1].split(',')
409         else:
410             prop_names = cl.getprops().keys()
411         props = []
412         for name in prop_names:
413             if ':' in name:
414                 name, width = name.split(':')
415                 props.append((name, int(width)))
416             else:
417                 props.append((name, len(name)))
419         print ' '.join([string.capitalize(name) for name, width in props])
420         for nodeid in cl.list():
421             l = []
422             for name, width in props:
423                 if name != 'id':
424                     value = str(cl.get(nodeid, name))
425                 else:
426                     value = str(nodeid)
427                 f = '%%-%ds'%width
428                 l.append(f%value[:width])
429             print ' '.join(l)
430         return 0
432     def do_history(self, args):
433         '''Usage: history designator
434         Show the history entries of a designator.
436         Lists the journal entries for the node identified by the designator.
437         '''
438         try:
439             classname, nodeid = roundupdb.splitDesignator(args[0])
440         except roundupdb.DesignatorError, message:
441             print 'Error: %s'%message
442             return 1
443         # TODO: handle the -c option?
444         print self.db.getclass(classname).history(nodeid)
445         return 0
447     def do_retire(self, args):
448         '''Usage: retire designator[,designator]*
449         Retire the node specified by designator.
451         This action indicates that a particular node is not to be retrieved by
452         the list or find commands, and its key value may be re-used.
453         '''
454         designators = string.split(args[0], ',')
455         for designator in designators:
456             try:
457                 classname, nodeid = roundupdb.splitDesignator(designator)
458             except roundupdb.DesignatorError, message:
459                 print 'Error: %s'%message
460                 return 1
461             self.db.getclass(classname).retire(nodeid)
462         return 0
464     def do_export(self, args):
465         '''Usage: export class[,class] destination_dir
466         Export the database to tab-separated-value files.
468         This action exports the current data from the database into
469         tab-separated-value files that are placed in the nominated destination
470         directory. The journals are not exported.
471         '''
472         if len(args) < 2:
473             print do_export.__doc__
474             return 1
475         classes = string.split(args[0], ',')
476         dir = args[1]
478         # use the csv parser if we can - it's faster
479         if csv is not None:
480             p = csv.parser(field_sep=':')
482         # do all the classes specified
483         for classname in classes:
484             cl = self.db.getclass(classname)
485             f = open(os.path.join(dir, classname+'.csv'), 'w')
486             f.write(string.join(cl.properties.keys(), ':') + '\n')
488             # all nodes for this class
489             properties = cl.properties.items()
490             for nodeid in cl.list():
491                 l = []
492                 for prop, type in properties:
493                     value = cl.get(nodeid, prop)
494                     # convert data where needed
495                     if isinstance(type, hyperdb.Date):
496                         value = value.get_tuple()
497                     elif isinstance(type, hyperdb.Interval):
498                         value = value.get_tuple()
499                     elif isinstance(type, hyperdb.Password):
500                         value = str(value)
501                     l.append(repr(value))
503                 # now write
504                 if csv is not None:
505                    f.write(p.join(l) + '\n')
506                 else:
507                    # escape the individual entries to they're valid CSV
508                    m = []
509                    for entry in l:
510                       if '"' in entry:
511                           entry = '""'.join(entry.split('"'))
512                       if ':' in entry:
513                           entry = '"%s"'%entry
514                       m.append(entry)
515                    f.write(':'.join(m) + '\n')
516         return 0
518     def do_import(self, args):
519         '''Usage: import class file
520         Import the contents of the tab-separated-value file.
522         The file must define the same properties as the class (including having
523         a "header" line with those property names.) The new nodes are added to
524         the existing database - if you want to create a new database using the
525         imported data, then create a new database (or, tediously, retire all
526         the old data.)
527         '''
528         if len(args) < 2:
529             print do_import.__doc__
530             return 1
531         if csv is None:
532             print 'Sorry, you need the csv module to use this function.'
533             print 'Get it from: http://www.object-craft.com.au/projects/csv/'
534             return 1
536         from roundup import hyperdb
538         # ensure that the properties and the CSV file headings match
539         cl = self.db.getclass(args[0])
540         f = open(args[1])
541         p = csv.parser(field_sep=':')
542         file_props = p.parse(f.readline())
543         props = cl.properties.keys()
544         m = file_props[:]
545         m.sort()
546         props.sort()
547         if m != props:
548             print 'Import file doesn\'t define the same properties as "%s".'%args[0]
549             return 1
551         # loop through the file and create a node for each entry
552         n = range(len(props))
553         while 1:
554             line = f.readline()
555             if not line: break
557             # parse lines until we get a complete entry
558             while 1:
559                 l = p.parse(line)
560                 if l: break
562             # make the new node's property map
563             d = {}
564             for i in n:
565                 # Use eval to reverse the repr() used to output the CSV
566                 value = eval(l[i])
567                 # Figure the property for this column
568                 key = file_props[i]
569                 type = cl.properties[key]
570                 # Convert for property type
571                 if isinstance(type, hyperdb.Date):
572                     value = date.Date(value)
573                 elif isinstance(type, hyperdb.Interval):
574                     value = date.Interval(value)
575                 elif isinstance(type, hyperdb.Password):
576                     pwd = password.Password()
577                     pwd.unpack(value)
578                     value = pwd
579                 if value is not None:
580                     d[key] = value
582             # and create the new node
583             apply(cl.create, (), d)
584         return 0
586     def run_command(self, args):
587         '''Run a single command
588         '''
589         command = args[0]
591         # handle help now
592         if command == 'help':
593             if len(args)>1:
594                 self.do_help(args[1:])
595                 return 0
596             self.do_help(['help'])
597             return 0
598         if command == 'morehelp':
599             self.do_help(['help'])
600             self.help_commands()
601             self.help_all()
602             return 0
604         # make sure we have an instance_home
605         while not self.instance_home:
606             self.instance_home = raw_input('Enter instance home: ').strip()
608         # before we open the db, we may be doing an init
609         if command == 'init':
610             return self.do_init(self.instance_home, args)
612         function = self.commands.get(command, None)
614         # not a valid command
615         if function is None:
616             print 'Unknown command "%s" ("help commands" for a list)'%command
617             return 1
619         # get the instance
620         instance = roundup.instance.open(self.instance_home)
621         self.db = instance.open('admin')
623         if len(args) < 2:
624             print function.__doc__
625             return 1
627         # do the command
628         try:
629             return function(args[1:])
630         finally:
631             self.db.close()
633         return 1
635     def interactive(self, ws_re=re.compile(r'\s+')):
636         '''Run in an interactive mode
637         '''
638         print 'Roundup {version} ready for input.'
639         print 'Type "help" for help.'
640         try:
641             import readline
642         except ImportError:
643             print "Note: command history and editing not available"
645         while 1:
646             try:
647                 command = raw_input('roundup> ')
648             except EOFError:
649                 print '.. exit'
650                 return 0
651             args = ws_re.split(command)
652             if not args: continue
653             if args[0] in ('quit', 'exit'): return 0
654             self.run_command(args)
656     def main(self):
657         opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
659         # handle command-line args
660         self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
661         name = password = ''
662         if os.environ.has_key('ROUNDUP_LOGIN'):
663             l = os.environ['ROUNDUP_LOGIN'].split(':')
664             name = l[0]
665             if len(l) > 1:
666                 password = l[1]
667         self.comma_sep = 0
668         for opt, arg in opts:
669             if opt == '-h':
670                 usage()
671                 return 0
672             if opt == '-i':
673                 self.instance_home = arg
674             if opt == '-c':
675                 self.comma_sep = 1
677         # if no command - go interactive
678         if not args:
679             return self.interactive()
681         self.run_command(args)
684 if __name__ == '__main__':
685     tool = AdminTool()
686     sys.exit(tool.main())
689 # $Log: not supported by cvs2svn $
690 # Revision 1.37  2001/10/23 01:00:18  richard
691 # Re-enabled login and registration access after lopping them off via
692 # disabling access for anonymous users.
693 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
694 # a couple of bugs while I was there. Probably introduced a couple, but
695 # things seem to work OK at the moment.
697 # Revision 1.36  2001/10/21 00:45:15  richard
698 # Added author identification to e-mail messages from roundup.
700 # Revision 1.35  2001/10/20 11:58:48  richard
701 # Catch errors in login - no username or password supplied.
702 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
704 # Revision 1.34  2001/10/18 02:16:42  richard
705 # Oops, committed the admin script with the wierd #! line.
706 # Also, made the thing into a class to reduce parameter passing.
707 # Nuked the leading whitespace from the help __doc__ displays too.
709 # Revision 1.33  2001/10/17 23:13:19  richard
710 # Did a fair bit of work on the admin tool. Now has an extra command "table"
711 # which displays node information in a tabular format. Also fixed import and
712 # export so they work. Removed freshen.
713 # Fixed quopri usage in mailgw from bug reports.
715 # Revision 1.32  2001/10/17 06:57:29  richard
716 # Interactive startup blurb - need to figure how to get the version in there.
718 # Revision 1.31  2001/10/17 06:17:26  richard
719 # Now with readline support :)
721 # Revision 1.30  2001/10/17 06:04:00  richard
722 # Beginnings of an interactive mode for roundup-admin
724 # Revision 1.29  2001/10/16 03:48:01  richard
725 # admin tool now complains if a "find" is attempted with a non-link property.
727 # Revision 1.28  2001/10/13 00:07:39  richard
728 # More help in admin tool.
730 # Revision 1.27  2001/10/11 23:43:04  richard
731 # Implemented the comma-separated printing option in the admin tool.
732 # Fixed a typo (more of a vim-o actually :) in mailgw.
734 # Revision 1.26  2001/10/11 05:03:51  richard
735 # Marked the roundup-admin import/export as experimental since they're not fully
736 # operational.
738 # Revision 1.25  2001/10/10 04:12:32  richard
739 # The setup.cfg file is just causing pain. Away it goes.
741 # Revision 1.24  2001/10/10 03:54:57  richard
742 # Added database importing and exporting through CSV files.
743 # Uses the csv module from object-craft for exporting if it's available.
744 # Requires the csv module for importing.
746 # Revision 1.23  2001/10/09 23:36:25  richard
747 # Spit out command help if roundup-admin command doesn't get an argument.
749 # Revision 1.22  2001/10/09 07:25:59  richard
750 # Added the Password property type. See "pydoc roundup.password" for
751 # implementation details. Have updated some of the documentation too.
753 # Revision 1.21  2001/10/05 02:23:24  richard
754 #  . roundup-admin create now prompts for property info if none is supplied
755 #    on the command-line.
756 #  . hyperdb Class getprops() method may now return only the mutable
757 #    properties.
758 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
759 #    now support anonymous user access (read-only, unless there's an
760 #    "anonymous" user, in which case write access is permitted). Login
761 #    handling has been moved into cgi_client.Client.main()
762 #  . The "extended" schema is now the default in roundup init.
763 #  . The schemas have had their page headings modified to cope with the new
764 #    login handling. Existing installations should copy the interfaces.py
765 #    file from the roundup lib directory to their instance home.
766 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
767 #    Ping - has been removed.
768 #  . Fixed a whole bunch of places in the CGI interface where we should have
769 #    been returning Not Found instead of throwing an exception.
770 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
771 #    an item now throws an exception.
773 # Revision 1.20  2001/10/04 02:12:42  richard
774 # Added nicer command-line item adding: passing no arguments will enter an
775 # interactive more which asks for each property in turn. While I was at it, I
776 # fixed an implementation problem WRT the spec - I wasn't raising a
777 # ValueError if the key property was missing from a create(). Also added a
778 # protected=boolean argument to getprops() so we can list only the mutable
779 # properties (defaults to yes, which lists the immutables).
781 # Revision 1.19  2001/10/01 06:40:43  richard
782 # made do_get have the args in the correct order
784 # Revision 1.18  2001/09/18 22:58:37  richard
786 # Added some more help to roundu-admin
788 # Revision 1.17  2001/08/28 05:58:33  anthonybaxter
789 # added missing 'import' statements.
791 # Revision 1.16  2001/08/12 06:32:36  richard
792 # using isinstance(blah, Foo) now instead of isFooType
794 # Revision 1.15  2001/08/07 00:24:42  richard
795 # stupid typo
797 # Revision 1.14  2001/08/07 00:15:51  richard
798 # Added the copyright/license notice to (nearly) all files at request of
799 # Bizar Software.
801 # Revision 1.13  2001/08/05 07:44:13  richard
802 # Instances are now opened by a special function that generates a unique
803 # module name for the instances on import time.
805 # Revision 1.12  2001/08/03 01:28:33  richard
806 # Used the much nicer load_package, pointed out by Steve Majewski.
808 # Revision 1.11  2001/08/03 00:59:34  richard
809 # Instance import now imports the instance using imp.load_module so that
810 # we can have instance homes of "roundup" or other existing python package
811 # names.
813 # Revision 1.10  2001/07/30 08:12:17  richard
814 # Added time logging and file uploading to the templates.
816 # Revision 1.9  2001/07/30 03:52:55  richard
817 # init help now lists templates and backends
819 # Revision 1.8  2001/07/30 02:37:07  richard
820 # Freshen is really broken. Commented out.
822 # Revision 1.7  2001/07/30 01:28:46  richard
823 # Bugfixes
825 # Revision 1.6  2001/07/30 00:57:51  richard
826 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
827 # better internal structure. It's just BETTER. :)
829 # Revision 1.5  2001/07/30 00:04:48  richard
830 # Made the "init" prompting more friendly.
832 # Revision 1.4  2001/07/29 07:01:39  richard
833 # Added vim command to all source so that we don't get no steenkin' tabs :)
835 # Revision 1.3  2001/07/23 08:45:28  richard
836 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
837 # workable instance_home set up :)
838 # _and_ anydbm has had its first test :)
840 # Revision 1.2  2001/07/23 08:20:44  richard
841 # Moved over to using marshal in the bsddb and anydbm backends.
842 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
843 #  retired - mod hyperdb.Class.list() so it lists retired nodes)
845 # Revision 1.1  2001/07/23 03:46:48  richard
846 # moving the bin files to facilitate out-of-the-boxness
848 # Revision 1.1  2001/07/22 11:15:45  richard
849 # More Grande Splite stuff
852 # vim: set filetype=python ts=4 sw=4 et si