8ea917329913e3c944b989b9f62ca58c1c6ebe61
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())
688 #
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.
696 #
697 # Revision 1.36 2001/10/21 00:45:15 richard
698 # Added author identification to e-mail messages from roundup.
699 #
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.
703 #
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.
708 #
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.
714 #
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.
717 #
718 # Revision 1.31 2001/10/17 06:17:26 richard
719 # Now with readline support :)
720 #
721 # Revision 1.30 2001/10/17 06:04:00 richard
722 # Beginnings of an interactive mode for roundup-admin
723 #
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.
726 #
727 # Revision 1.28 2001/10/13 00:07:39 richard
728 # More help in admin tool.
729 #
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.
733 #
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.
737 #
738 # Revision 1.25 2001/10/10 04:12:32 richard
739 # The setup.cfg file is just causing pain. Away it goes.
740 #
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.
745 #
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.
748 #
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.
752 #
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.
772 #
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).
780 #
781 # Revision 1.19 2001/10/01 06:40:43 richard
782 # made do_get have the args in the correct order
783 #
784 # Revision 1.18 2001/09/18 22:58:37 richard
785 #
786 # Added some more help to roundu-admin
787 #
788 # Revision 1.17 2001/08/28 05:58:33 anthonybaxter
789 # added missing 'import' statements.
790 #
791 # Revision 1.16 2001/08/12 06:32:36 richard
792 # using isinstance(blah, Foo) now instead of isFooType
793 #
794 # Revision 1.15 2001/08/07 00:24:42 richard
795 # stupid typo
796 #
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.
800 #
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.
804 #
805 # Revision 1.12 2001/08/03 01:28:33 richard
806 # Used the much nicer load_package, pointed out by Steve Majewski.
807 #
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.
812 #
813 # Revision 1.10 2001/07/30 08:12:17 richard
814 # Added time logging and file uploading to the templates.
815 #
816 # Revision 1.9 2001/07/30 03:52:55 richard
817 # init help now lists templates and backends
818 #
819 # Revision 1.8 2001/07/30 02:37:07 richard
820 # Freshen is really broken. Commented out.
821 #
822 # Revision 1.7 2001/07/30 01:28:46 richard
823 # Bugfixes
824 #
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. :)
828 #
829 # Revision 1.5 2001/07/30 00:04:48 richard
830 # Made the "init" prompting more friendly.
831 #
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 :)
834 #
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 :)
839 #
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)
844 #
845 # Revision 1.1 2001/07/23 03:46:48 richard
846 # moving the bin files to facilitate out-of-the-boxness
847 #
848 # Revision 1.1 2001/07/22 11:15:45 richard
849 # More Grande Splite stuff
850 #
851 #
852 # vim: set filetype=python ts=4 sw=4 et si