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.52 2001-12-12 21:47:45 richard Exp $
21 import sys
22 if not hasattr(sys, 'version_info') or sys.version_info[:2] < (2,1):
23 print 'Roundup requires python 2.1 or later.'
24 sys.exit(1)
26 import string, os, getpass, getopt, re, UserDict
27 try:
28 import csv
29 except ImportError:
30 csv = None
31 from roundup import date, hyperdb, roundupdb, init, password
32 import roundup.instance
34 class CommandDict(UserDict.UserDict):
35 '''Simple dictionary that lets us do lookups using partial keys.
37 Original code submitted by Engelbert Gruber.
38 '''
39 _marker = []
40 def get(self, key, default=_marker):
41 if self.data.has_key(key):
42 return [(key, self.data[key])]
43 keylist = self.data.keys()
44 keylist.sort()
45 l = []
46 for ki in keylist:
47 if ki.startswith(key):
48 l.append((ki, self.data[ki]))
49 if not l and default is self._marker:
50 raise KeyError, key
51 return l
53 class UsageError(ValueError):
54 pass
56 class AdminTool:
58 def __init__(self):
59 self.commands = CommandDict()
60 for k in AdminTool.__dict__.keys():
61 if k[:3] == 'do_':
62 self.commands[k[3:]] = getattr(self, k)
63 self.help = {}
64 for k in AdminTool.__dict__.keys():
65 if k[:5] == 'help_':
66 self.help[k[5:]] = getattr(self, k)
67 self.instance_home = ''
68 self.db = None
70 def usage(self, message=''):
71 if message: message = 'Problem: '+message+'\n\n'
72 print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments>
74 Help:
75 roundup-admin -h
76 roundup-admin help -- this help
77 roundup-admin help <command> -- command-specific help
78 roundup-admin help all -- all available help
79 Options:
80 -i instance home -- specify the issue tracker "home directory" to administer
81 -u -- the user[:password] to use for commands
82 -c -- when outputting lists of data, just comma-separate them'''%message
83 self.help_commands()
85 def help_commands(self):
86 print 'Commands:',
87 commands = ['']
88 for command in self.commands.values():
89 h = command.__doc__.split('\n')[0]
90 commands.append(' '+h[7:])
91 commands.sort()
92 commands.append(
93 'Commands may be abbreviated as long as the abbreviation matches only one')
94 commands.append('command, e.g. l == li == lis == list.')
95 print '\n'.join(commands)
96 print
98 def help_all(self):
99 print '''
100 All commands (except help) require an instance specifier. This is just the path
101 to the roundup instance you're working with. A roundup instance is where
102 roundup keeps the database and configuration file that defines an issue
103 tracker. It may be thought of as the issue tracker's "home directory". It may
104 be specified in the environment variable ROUNDUP_INSTANCE or on the command
105 line as "-i instance".
107 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
109 Property values are represented as strings in command arguments and in the
110 printed results:
111 . Strings are, well, strings.
112 . Date values are printed in the full date format in the local time zone, and
113 accepted in the full format or any of the partial formats explained below.
114 . Link values are printed as node designators. When given as an argument,
115 node designators and key strings are both accepted.
116 . Multilink values are printed as lists of node designators joined by commas.
117 When given as an argument, node designators and key strings are both
118 accepted; an empty string, a single node, or a list of nodes joined by
119 commas is accepted.
121 When multiple nodes are specified to the roundup get or roundup set
122 commands, the specified properties are retrieved or set on all the listed
123 nodes.
125 When multiple results are returned by the roundup get or roundup find
126 commands, they are printed one per line (default) or joined by commas (with
127 the -c) option.
129 Where the command changes data, a login name/password is required. The
130 login may be specified as either "name" or "name:password".
131 . ROUNDUP_LOGIN environment variable
132 . the -u command-line option
133 If either the name or password is not supplied, they are obtained from the
134 command-line.
136 Date format examples:
137 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
138 "2000-04-17" means <Date 2000-04-17.00:00:00>
139 "01-25" means <Date yyyy-01-25.00:00:00>
140 "08-13.22:13" means <Date yyyy-08-14.03:13:00>
141 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
142 "14:25" means <Date yyyy-mm-dd.19:25:00>
143 "8:47:11" means <Date yyyy-mm-dd.13:47:11>
144 "." means "right now"
146 Command help:
147 '''
148 for name, command in self.commands.items():
149 print '%s:'%name
150 print ' ',command.__doc__
152 def do_help(self, args, nl_re=re.compile('[\r\n]'),
153 indent_re=re.compile(r'^(\s+)\S+')):
154 '''Usage: help topic
155 Give help about topic.
157 commands -- list commands
158 <command> -- help specific to a command
159 initopts -- init command options
160 all -- all available help
161 '''
162 topic = args[0]
164 # try help_ methods
165 if self.help.has_key(topic):
166 self.help[topic]()
167 return 0
169 # try command docstrings
170 try:
171 l = self.commands.get(topic)
172 except KeyError:
173 print 'Sorry, no help for "%s"'%topic
174 return 1
176 # display the help for each match, removing the docsring indent
177 for name, help in l:
178 lines = nl_re.split(help.__doc__)
179 print lines[0]
180 indent = indent_re.match(lines[1])
181 if indent: indent = len(indent.group(1))
182 for line in lines[1:]:
183 if indent:
184 print line[indent:]
185 else:
186 print line
187 return 0
189 def help_initopts(self):
190 import roundup.templates
191 templates = roundup.templates.listTemplates()
192 print 'Templates:', ', '.join(templates)
193 import roundup.backends
194 backends = roundup.backends.__all__
195 print 'Back ends:', ', '.join(backends)
198 def do_initialise(self, instance_home, args):
199 '''Usage: initialise [template [backend [admin password]]]
200 Initialise a new Roundup instance.
202 The command will prompt for the instance home directory (if not supplied
203 through INSTANCE_HOME or the -i option. The template, backend and admin
204 password may be specified on the command-line as arguments, in that
205 order.
207 See also initopts help.
208 '''
209 if len(args) < 1:
210 raise UsageError, 'Not enough arguments supplied'
211 # select template
212 import roundup.templates
213 templates = roundup.templates.listTemplates()
214 template = len(args) > 1 and args[1] or ''
215 if template not in templates:
216 print 'Templates:', ', '.join(templates)
217 while template not in templates:
218 template = raw_input('Select template [classic]: ').strip()
219 if not template:
220 template = 'classic'
222 import roundup.backends
223 backends = roundup.backends.__all__
224 backend = len(args) > 2 and args[2] or ''
225 if backend not in backends:
226 print 'Back ends:', ', '.join(backends)
227 while backend not in backends:
228 backend = raw_input('Select backend [anydbm]: ').strip()
229 if not backend:
230 backend = 'anydbm'
231 if len(args) > 3:
232 adminpw = confirm = args[3]
233 else:
234 adminpw = ''
235 confirm = 'x'
236 while adminpw != confirm:
237 adminpw = getpass.getpass('Admin Password: ')
238 confirm = getpass.getpass(' Confirm: ')
239 init.init(instance_home, template, backend, adminpw)
240 return 0
243 def do_get(self, args):
244 '''Usage: get property designator[,designator]*
245 Get the given property of one or more designator(s).
247 Retrieves the property value of the nodes specified by the designators.
248 '''
249 if len(args) < 2:
250 raise UsageError, 'Not enough arguments supplied'
251 propname = args[0]
252 designators = string.split(args[1], ',')
253 l = []
254 for designator in designators:
255 # decode the node designator
256 try:
257 classname, nodeid = roundupdb.splitDesignator(designator)
258 except roundupdb.DesignatorError, message:
259 raise UsageError, message
261 # get the class
262 try:
263 cl = self.db.getclass(classname)
264 except KeyError:
265 raise UsageError, 'invalid class "%s"'%classname
266 try:
267 if self.comma_sep:
268 l.append(cl.get(nodeid, propname))
269 else:
270 print cl.get(nodeid, propname)
271 except IndexError:
272 raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
273 except KeyError:
274 raise UsageError, 'no such %s property "%s"'%(classname,
275 propname)
276 if self.comma_sep:
277 print ','.join(l)
278 return 0
281 def do_set(self, args):
282 '''Usage: set designator[,designator]* propname=value ...
283 Set the given property of one or more designator(s).
285 Sets the property to the value for all designators given.
286 '''
287 if len(args) < 2:
288 raise UsageError, 'Not enough arguments supplied'
289 from roundup import hyperdb
291 designators = string.split(args[0], ',')
292 props = {}
293 for prop in args[1:]:
294 if prop.find('=') == -1:
295 raise UsageError, 'argument "%s" not propname=value'%prop
296 try:
297 key, value = prop.split('=')
298 except ValueError:
299 raise UsageError, 'argument "%s" not propname=value'%prop
300 props[key] = value
301 for designator in designators:
302 # decode the node designator
303 try:
304 classname, nodeid = roundupdb.splitDesignator(designator)
305 except roundupdb.DesignatorError, message:
306 raise UsageError, message
308 # get the class
309 try:
310 cl = self.db.getclass(classname)
311 except KeyError:
312 raise UsageError, 'invalid class "%s"'%classname
314 properties = cl.getprops()
315 for key, value in props.items():
316 proptype = properties[key]
317 if isinstance(proptype, hyperdb.String):
318 continue
319 elif isinstance(proptype, hyperdb.Password):
320 props[key] = password.Password(value)
321 elif isinstance(proptype, hyperdb.Date):
322 try:
323 props[key] = date.Date(value)
324 except ValueError, message:
325 raise UsageError, '"%s": %s'%(value, message)
326 elif isinstance(proptype, hyperdb.Interval):
327 try:
328 props[key] = date.Interval(value)
329 except ValueError, message:
330 raise UsageError, '"%s": %s'%(value, message)
331 elif isinstance(proptype, hyperdb.Link):
332 props[key] = value
333 elif isinstance(proptype, hyperdb.Multilink):
334 props[key] = value.split(',')
336 # try the set
337 try:
338 apply(cl.set, (nodeid, ), props)
339 except (TypeError, IndexError, ValueError), message:
340 raise UsageError, message
341 return 0
343 def do_find(self, args):
344 '''Usage: find classname propname=value ...
345 Find the nodes of the given class with a given link property value.
347 Find the nodes of the given class with a given link property value. The
348 value may be either the nodeid of the linked node, or its key value.
349 '''
350 if len(args) < 1:
351 raise UsageError, 'Not enough arguments supplied'
352 classname = args[0]
353 # get the class
354 try:
355 cl = self.db.getclass(classname)
356 except KeyError:
357 raise UsageError, 'invalid class "%s"'%classname
359 # TODO: handle > 1 argument
360 # handle the propname=value argument
361 if args[1].find('=') == -1:
362 raise UsageError, 'argument "%s" not propname=value'%prop
363 try:
364 propname, value = args[1].split('=')
365 except ValueError:
366 raise UsageError, 'argument "%s" not propname=value'%prop
368 # if the value isn't a number, look up the linked class to get the
369 # number
370 num_re = re.compile('^\d+$')
371 if not num_re.match(value):
372 # get the property
373 try:
374 property = cl.properties[propname]
375 except KeyError:
376 raise UsageError, '%s has no property "%s"'%(classname,
377 propname)
379 # make sure it's a link
380 if (not isinstance(property, hyperdb.Link) and not
381 isinstance(property, hyperdb.Multilink)):
382 raise UsageError, 'You may only "find" link properties'
384 # get the linked-to class and look up the key property
385 link_class = self.db.getclass(property.classname)
386 try:
387 value = link_class.lookup(value)
388 except TypeError:
389 raise UsageError, '%s has no key property"'%link_class.classname
390 except KeyError:
391 raise UsageError, '%s has no entry "%s"'%(link_class.classname,
392 propname)
394 # now do the find
395 try:
396 if self.comma_sep:
397 print ','.join(apply(cl.find, (), {propname: value}))
398 else:
399 print apply(cl.find, (), {propname: value})
400 except KeyError:
401 raise UsageError, '%s has no property "%s"'%(classname,
402 propname)
403 except (ValueError, TypeError), message:
404 raise UsageError, message
405 return 0
407 def do_specification(self, args):
408 '''Usage: specification classname
409 Show the properties for a classname.
411 This lists the properties for a given class.
412 '''
413 if len(args) < 1:
414 raise UsageError, 'Not enough arguments supplied'
415 classname = args[0]
416 # get the class
417 try:
418 cl = self.db.getclass(classname)
419 except KeyError:
420 raise UsageError, 'invalid class "%s"'%classname
422 # get the key property
423 keyprop = cl.getkey()
424 for key, value in cl.properties.items():
425 if keyprop == key:
426 print '%s: %s (key property)'%(key, value)
427 else:
428 print '%s: %s'%(key, value)
430 def do_display(self, args):
431 '''Usage: display designator
432 Show the property values for the given node.
434 This lists the properties and their associated values for the given
435 node.
436 '''
437 if len(args) < 1:
438 raise UsageError, 'Not enough arguments supplied'
440 # decode the node designator
441 try:
442 classname, nodeid = roundupdb.splitDesignator(args[0])
443 except roundupdb.DesignatorError, message:
444 raise UsageError, message
446 # get the class
447 try:
448 cl = self.db.getclass(classname)
449 except KeyError:
450 raise UsageError, 'invalid class "%s"'%classname
452 # display the values
453 for key in cl.properties.keys():
454 value = cl.get(nodeid, key)
455 print '%s: %s'%(key, value)
457 def do_create(self, args):
458 '''Usage: create classname property=value ...
459 Create a new entry of a given class.
461 This creates a new entry of the given class using the property
462 name=value arguments provided on the command line after the "create"
463 command.
464 '''
465 if len(args) < 1:
466 raise UsageError, 'Not enough arguments supplied'
467 from roundup import hyperdb
469 classname = args[0]
471 # get the class
472 try:
473 cl = self.db.getclass(classname)
474 except KeyError:
475 raise UsageError, 'invalid class "%s"'%classname
477 # now do a create
478 props = {}
479 properties = cl.getprops(protected = 0)
480 if len(args) == 1:
481 # ask for the properties
482 for key, value in properties.items():
483 if key == 'id': continue
484 name = value.__class__.__name__
485 if isinstance(value , hyperdb.Password):
486 again = None
487 while value != again:
488 value = getpass.getpass('%s (Password): '%key.capitalize())
489 again = getpass.getpass(' %s (Again): '%key.capitalize())
490 if value != again: print 'Sorry, try again...'
491 if value:
492 props[key] = value
493 else:
494 value = raw_input('%s (%s): '%(key.capitalize(), name))
495 if value:
496 props[key] = value
497 else:
498 # use the args
499 for prop in args[1:]:
500 if prop.find('=') == -1:
501 raise UsageError, 'argument "%s" not propname=value'%prop
502 try:
503 key, value = prop.split('=')
504 except ValueError:
505 raise UsageError, 'argument "%s" not propname=value'%prop
506 props[key] = value
508 # convert types
509 for key in props.keys():
510 # get the property
511 try:
512 proptype = properties[key]
513 except KeyError:
514 raise UsageError, '%s has no property "%s"'%(classname, key)
516 if isinstance(proptype, hyperdb.Date):
517 try:
518 props[key] = date.Date(value)
519 except ValueError, message:
520 raise UsageError, '"%s": %s'%(value, message)
521 elif isinstance(proptype, hyperdb.Interval):
522 try:
523 props[key] = date.Interval(value)
524 except ValueError, message:
525 raise UsageError, '"%s": %s'%(value, message)
526 elif isinstance(proptype, hyperdb.Password):
527 props[key] = password.Password(value)
528 elif isinstance(proptype, hyperdb.Multilink):
529 props[key] = value.split(',')
531 # check for the key property
532 if cl.getkey() and not props.has_key(cl.getkey()):
533 raise UsageError, "you must provide the '%s' property."%cl.getkey()
535 # do the actual create
536 try:
537 print apply(cl.create, (), props)
538 except (TypeError, IndexError, ValueError), message:
539 raise UsageError, message
540 return 0
542 def do_list(self, args):
543 '''Usage: list classname [property]
544 List the instances of a class.
546 Lists all instances of the given class. If the property is not
547 specified, the "label" property is used. The label property is tried
548 in order: the key, "name", "title" and then the first property,
549 alphabetically.
550 '''
551 if len(args) < 1:
552 raise UsageError, 'Not enough arguments supplied'
553 classname = args[0]
555 # get the class
556 try:
557 cl = self.db.getclass(classname)
558 except KeyError:
559 raise UsageError, 'invalid class "%s"'%classname
561 # figure the property
562 if len(args) > 1:
563 key = args[1]
564 else:
565 key = cl.labelprop()
567 if self.comma_sep:
568 print ','.join(cl.list())
569 else:
570 for nodeid in cl.list():
571 try:
572 value = cl.get(nodeid, key)
573 except KeyError:
574 raise UsageError, '%s has no property "%s"'%(classname, key)
575 print "%4s: %s"%(nodeid, value)
576 return 0
578 def do_table(self, args):
579 '''Usage: table classname [property[,property]*]
580 List the instances of a class in tabular form.
582 Lists all instances of the given class. If the properties are not
583 specified, all properties are displayed. By default, the column widths
584 are the width of the property names. The width may be explicitly defined
585 by defining the property as "name:width". For example::
586 roundup> table priority id,name:10
587 Id Name
588 1 fatal-bug
589 2 bug
590 3 usability
591 4 feature
592 '''
593 if len(args) < 1:
594 raise UsageError, 'Not enough arguments supplied'
595 classname = args[0]
597 # get the class
598 try:
599 cl = self.db.getclass(classname)
600 except KeyError:
601 raise UsageError, 'invalid class "%s"'%classname
603 # figure the property names to display
604 if len(args) > 1:
605 prop_names = args[1].split(',')
606 all_props = cl.getprops()
607 for prop_name in prop_names:
608 if not all_props.has_key(prop_name):
609 raise UsageError, '%s has no property "%s"'%(classname,
610 prop_name)
611 else:
612 prop_names = cl.getprops().keys()
614 # now figure column widths
615 props = []
616 for spec in prop_names:
617 if ':' in spec:
618 try:
619 name, width = spec.split(':')
620 except (ValueError, TypeError):
621 raise UsageError, '"%s" not name:width'%spec
622 props.append((spec, int(width)))
623 else:
624 props.append((spec, len(spec)))
626 # now display the heading
627 print ' '.join([string.capitalize(name) for name, width in props])
629 # and the table data
630 for nodeid in cl.list():
631 l = []
632 for name, width in props:
633 if name != 'id':
634 try:
635 value = str(cl.get(nodeid, name))
636 except KeyError:
637 # we already checked if the property is valid - a
638 # KeyError here means the node just doesn't have a
639 # value for it
640 value = ''
641 else:
642 value = str(nodeid)
643 f = '%%-%ds'%width
644 l.append(f%value[:width])
645 print ' '.join(l)
646 return 0
648 def do_history(self, args):
649 '''Usage: history designator
650 Show the history entries of a designator.
652 Lists the journal entries for the node identified by the designator.
653 '''
654 if len(args) < 1:
655 raise UsageError, 'Not enough arguments supplied'
656 try:
657 classname, nodeid = roundupdb.splitDesignator(args[0])
658 except roundupdb.DesignatorError, message:
659 raise UsageError, message
661 # TODO: handle the -c option?
662 try:
663 print self.db.getclass(classname).history(nodeid)
664 except KeyError:
665 raise UsageError, 'no such class "%s"'%classname
666 except IndexError:
667 raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
668 return 0
670 def do_commit(self, args):
671 '''Usage: commit
672 Commit all changes made to the database.
674 The changes made during an interactive session are not
675 automatically written to the database - they must be committed
676 using this command.
678 One-off commands on the command-line are automatically committed if
679 they are successful.
680 '''
681 self.db.commit()
682 return 0
684 def do_rollback(self, args):
685 '''Usage: rollback
686 Undo all changes that are pending commit to the database.
688 The changes made during an interactive session are not
689 automatically written to the database - they must be committed
690 manually. This command undoes all those changes, so a commit
691 immediately after would make no changes to the database.
692 '''
693 self.db.rollback()
694 return 0
696 def do_retire(self, args):
697 '''Usage: retire designator[,designator]*
698 Retire the node specified by designator.
700 This action indicates that a particular node is not to be retrieved by
701 the list or find commands, and its key value may be re-used.
702 '''
703 if len(args) < 1:
704 raise UsageError, 'Not enough arguments supplied'
705 designators = string.split(args[0], ',')
706 for designator in designators:
707 try:
708 classname, nodeid = roundupdb.splitDesignator(designator)
709 except roundupdb.DesignatorError, message:
710 raise UsageError, message
711 try:
712 self.db.getclass(classname).retire(nodeid)
713 except KeyError:
714 raise UsageError, 'no such class "%s"'%classname
715 except IndexError:
716 raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
717 return 0
719 def do_export(self, args):
720 '''Usage: export class[,class] destination_dir
721 Export the database to tab-separated-value files.
723 This action exports the current data from the database into
724 tab-separated-value files that are placed in the nominated destination
725 directory. The journals are not exported.
726 '''
727 if len(args) < 2:
728 raise UsageError, 'Not enough arguments supplied'
729 classes = string.split(args[0], ',')
730 dir = args[1]
732 # use the csv parser if we can - it's faster
733 if csv is not None:
734 p = csv.parser(field_sep=':')
736 # do all the classes specified
737 for classname in classes:
738 try:
739 cl = self.db.getclass(classname)
740 except KeyError:
741 raise UsageError, 'no such class "%s"'%classname
742 f = open(os.path.join(dir, classname+'.csv'), 'w')
743 f.write(string.join(cl.properties.keys(), ':') + '\n')
745 # all nodes for this class
746 properties = cl.properties.items()
747 for nodeid in cl.list():
748 l = []
749 for prop, proptype in properties:
750 value = cl.get(nodeid, prop)
751 # convert data where needed
752 if isinstance(proptype, hyperdb.Date):
753 value = value.get_tuple()
754 elif isinstance(proptype, hyperdb.Interval):
755 value = value.get_tuple()
756 elif isinstance(proptype, hyperdb.Password):
757 value = str(value)
758 l.append(repr(value))
760 # now write
761 if csv is not None:
762 f.write(p.join(l) + '\n')
763 else:
764 # escape the individual entries to they're valid CSV
765 m = []
766 for entry in l:
767 if '"' in entry:
768 entry = '""'.join(entry.split('"'))
769 if ':' in entry:
770 entry = '"%s"'%entry
771 m.append(entry)
772 f.write(':'.join(m) + '\n')
773 return 0
775 def do_import(self, args):
776 '''Usage: import class file
777 Import the contents of the tab-separated-value file.
779 The file must define the same properties as the class (including having
780 a "header" line with those property names.) The new nodes are added to
781 the existing database - if you want to create a new database using the
782 imported data, then create a new database (or, tediously, retire all
783 the old data.)
784 '''
785 if len(args) < 2:
786 raise UsageError, 'Not enough arguments supplied'
787 if csv is None:
788 raise UsageError, \
789 'Sorry, you need the csv module to use this function.\n'\
790 'Get it from: http://www.object-craft.com.au/projects/csv/'
792 from roundup import hyperdb
794 # ensure that the properties and the CSV file headings match
795 classname = args[0]
796 try:
797 cl = self.db.getclass(classname)
798 except KeyError:
799 raise UsageError, 'no such class "%s"'%classname
800 f = open(args[1])
801 p = csv.parser(field_sep=':')
802 file_props = p.parse(f.readline())
803 props = cl.properties.keys()
804 m = file_props[:]
805 m.sort()
806 props.sort()
807 if m != props:
808 raise UsageError, 'Import file doesn\'t define the same '\
809 'properties as "%s".'%args[0]
811 # loop through the file and create a node for each entry
812 n = range(len(props))
813 while 1:
814 line = f.readline()
815 if not line: break
817 # parse lines until we get a complete entry
818 while 1:
819 l = p.parse(line)
820 if l: break
822 # make the new node's property map
823 d = {}
824 for i in n:
825 # Use eval to reverse the repr() used to output the CSV
826 value = eval(l[i])
827 # Figure the property for this column
828 key = file_props[i]
829 proptype = cl.properties[key]
830 # Convert for property type
831 if isinstance(proptype, hyperdb.Date):
832 value = date.Date(value)
833 elif isinstance(proptype, hyperdb.Interval):
834 value = date.Interval(value)
835 elif isinstance(proptype, hyperdb.Password):
836 pwd = password.Password()
837 pwd.unpack(value)
838 value = pwd
839 if value is not None:
840 d[key] = value
842 # and create the new node
843 apply(cl.create, (), d)
844 return 0
846 def run_command(self, args):
847 '''Run a single command
848 '''
849 command = args[0]
851 # handle help now
852 if command == 'help':
853 if len(args)>1:
854 self.do_help(args[1:])
855 return 0
856 self.do_help(['help'])
857 return 0
858 if command == 'morehelp':
859 self.do_help(['help'])
860 self.help_commands()
861 self.help_all()
862 return 0
864 # figure what the command is
865 try:
866 functions = self.commands.get(command)
867 except KeyError:
868 # not a valid command
869 print 'Unknown command "%s" ("help commands" for a list)'%command
870 return 1
872 # check for multiple matches
873 if len(functions) > 1:
874 print 'Multiple commands match "%s": %s'%(command,
875 ', '.join([i[0] for i in functions]))
876 return 1
877 command, function = functions[0]
879 # make sure we have an instance_home
880 while not self.instance_home:
881 self.instance_home = raw_input('Enter instance home: ').strip()
883 # before we open the db, we may be doing an init
884 if command == 'initialise':
885 return self.do_initialise(self.instance_home, args)
887 # get the instance
888 try:
889 instance = roundup.instance.open(self.instance_home)
890 except ValueError, message:
891 self.instance_home = ''
892 print "Couldn't open instance: %s"%message
893 return 1
895 # only open the database once!
896 if not self.db:
897 self.db = instance.open('admin')
899 # do the command
900 ret = 0
901 try:
902 ret = function(args[1:])
903 except UsageError, message:
904 print 'Error: %s'%message
905 print function.__doc__
906 ret = 1
907 except:
908 import traceback
909 traceback.print_exc()
910 ret = 1
911 return ret
913 def interactive(self, ws_re=re.compile(r'\s+')):
914 '''Run in an interactive mode
915 '''
916 print 'Roundup {version} ready for input.'
917 print 'Type "help" for help.'
918 try:
919 import readline
920 except ImportError:
921 print "Note: command history and editing not available"
923 while 1:
924 try:
925 command = raw_input('roundup> ')
926 except EOFError:
927 print 'exit...'
928 break
929 if not command: continue
930 args = ws_re.split(command)
931 if not args: continue
932 if args[0] in ('quit', 'exit'): break
933 self.run_command(args)
935 # exit.. check for transactions
936 if self.db and self.db.transactions:
937 commit = raw_input("There are unsaved changes. Commit them (y/N)? ")
938 if commit[0].lower() == 'y':
939 self.db.commit()
940 return 0
942 def main(self):
943 try:
944 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
945 except getopt.GetoptError, e:
946 self.usage(str(e))
947 return 1
949 # handle command-line args
950 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
951 name = password = ''
952 if os.environ.has_key('ROUNDUP_LOGIN'):
953 l = os.environ['ROUNDUP_LOGIN'].split(':')
954 name = l[0]
955 if len(l) > 1:
956 password = l[1]
957 self.comma_sep = 0
958 for opt, arg in opts:
959 if opt == '-h':
960 self.usage()
961 return 0
962 if opt == '-i':
963 self.instance_home = arg
964 if opt == '-c':
965 self.comma_sep = 1
967 # if no command - go interactive
968 ret = 0
969 if not args:
970 self.interactive()
971 else:
972 ret = self.run_command(args)
973 if self.db: self.db.commit()
974 return ret
977 if __name__ == '__main__':
978 tool = AdminTool()
979 sys.exit(tool.main())
981 #
982 # $Log: not supported by cvs2svn $
983 # Revision 1.51 2001/12/10 00:57:38 richard
984 # From CHANGES:
985 # . Added the "display" command to the admin tool - displays a node's values
986 # . #489760 ] [issue] only subject
987 # . fixed the doc/index.html to include the quoting in the mail alias.
988 #
989 # Also:
990 # . fixed roundup-admin so it works with transactions
991 # . disabled the back_anydbm module if anydbm tries to use dumbdbm
992 #
993 # Revision 1.50 2001/12/02 05:06:16 richard
994 # . We now use weakrefs in the Classes to keep the database reference, so
995 # the close() method on the database is no longer needed.
996 # I bumped the minimum python requirement up to 2.1 accordingly.
997 # . #487480 ] roundup-server
998 # . #487476 ] INSTALL.txt
999 #
1000 # I also cleaned up the change message / post-edit stuff in the cgi client.
1001 # There's now a clearly marked "TODO: append the change note" where I believe
1002 # the change note should be added there. The "changes" list will obviously
1003 # have to be modified to be a dict of the changes, or somesuch.
1004 #
1005 # More testing needed.
1006 #
1007 # Revision 1.49 2001/12/01 07:17:50 richard
1008 # . We now have basic transaction support! Information is only written to
1009 # the database when the commit() method is called. Only the anydbm
1010 # backend is modified in this way - neither of the bsddb backends have been.
1011 # The mail, admin and cgi interfaces all use commit (except the admin tool
1012 # doesn't have a commit command, so interactive users can't commit...)
1013 # . Fixed login/registration forwarding the user to the right page (or not,
1014 # on a failure)
1015 #
1016 # Revision 1.48 2001/11/27 22:32:03 richard
1017 # typo
1018 #
1019 # Revision 1.47 2001/11/26 22:55:56 richard
1020 # Feature:
1021 # . Added INSTANCE_NAME to configuration - used in web and email to identify
1022 # the instance.
1023 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1024 # signature info in e-mails.
1025 # . Some more flexibility in the mail gateway and more error handling.
1026 # . Login now takes you to the page you back to the were denied access to.
1027 #
1028 # Fixed:
1029 # . Lots of bugs, thanks Roché and others on the devel mailing list!
1030 #
1031 # Revision 1.46 2001/11/21 03:40:54 richard
1032 # more new property handling
1033 #
1034 # Revision 1.45 2001/11/12 22:51:59 jhermann
1035 # Fixed option & associated error handling
1036 #
1037 # Revision 1.44 2001/11/12 22:01:06 richard
1038 # Fixed issues with nosy reaction and author copies.
1039 #
1040 # Revision 1.43 2001/11/09 22:33:28 richard
1041 # More error handling fixes.
1042 #
1043 # Revision 1.42 2001/11/09 10:11:08 richard
1044 # . roundup-admin now handles all hyperdb exceptions
1045 #
1046 # Revision 1.41 2001/11/09 01:25:40 richard
1047 # Should parse with python 1.5.2 now.
1048 #
1049 # Revision 1.40 2001/11/08 04:42:00 richard
1050 # Expanded the already-abbreviated "initialise" and "specification" commands,
1051 # and added a comment to the command help about the abbreviation.
1052 #
1053 # Revision 1.39 2001/11/08 04:29:59 richard
1054 # roundup-admin now accepts abbreviated commands (eg. l = li = lis = list)
1055 # [thanks Engelbert Gruber for the inspiration]
1056 #
1057 # Revision 1.38 2001/11/05 23:45:40 richard
1058 # Fixed newuser_action so it sets the cookie with the unencrypted password.
1059 # Also made it present nicer error messages (not tracebacks).
1060 #
1061 # Revision 1.37 2001/10/23 01:00:18 richard
1062 # Re-enabled login and registration access after lopping them off via
1063 # disabling access for anonymous users.
1064 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1065 # a couple of bugs while I was there. Probably introduced a couple, but
1066 # things seem to work OK at the moment.
1067 #
1068 # Revision 1.36 2001/10/21 00:45:15 richard
1069 # Added author identification to e-mail messages from roundup.
1070 #
1071 # Revision 1.35 2001/10/20 11:58:48 richard
1072 # Catch errors in login - no username or password supplied.
1073 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
1074 #
1075 # Revision 1.34 2001/10/18 02:16:42 richard
1076 # Oops, committed the admin script with the wierd #! line.
1077 # Also, made the thing into a class to reduce parameter passing.
1078 # Nuked the leading whitespace from the help __doc__ displays too.
1079 #
1080 # Revision 1.33 2001/10/17 23:13:19 richard
1081 # Did a fair bit of work on the admin tool. Now has an extra command "table"
1082 # which displays node information in a tabular format. Also fixed import and
1083 # export so they work. Removed freshen.
1084 # Fixed quopri usage in mailgw from bug reports.
1085 #
1086 # Revision 1.32 2001/10/17 06:57:29 richard
1087 # Interactive startup blurb - need to figure how to get the version in there.
1088 #
1089 # Revision 1.31 2001/10/17 06:17:26 richard
1090 # Now with readline support :)
1091 #
1092 # Revision 1.30 2001/10/17 06:04:00 richard
1093 # Beginnings of an interactive mode for roundup-admin
1094 #
1095 # Revision 1.29 2001/10/16 03:48:01 richard
1096 # admin tool now complains if a "find" is attempted with a non-link property.
1097 #
1098 # Revision 1.28 2001/10/13 00:07:39 richard
1099 # More help in admin tool.
1100 #
1101 # Revision 1.27 2001/10/11 23:43:04 richard
1102 # Implemented the comma-separated printing option in the admin tool.
1103 # Fixed a typo (more of a vim-o actually :) in mailgw.
1104 #
1105 # Revision 1.26 2001/10/11 05:03:51 richard
1106 # Marked the roundup-admin import/export as experimental since they're not fully
1107 # operational.
1108 #
1109 # Revision 1.25 2001/10/10 04:12:32 richard
1110 # The setup.cfg file is just causing pain. Away it goes.
1111 #
1112 # Revision 1.24 2001/10/10 03:54:57 richard
1113 # Added database importing and exporting through CSV files.
1114 # Uses the csv module from object-craft for exporting if it's available.
1115 # Requires the csv module for importing.
1116 #
1117 # Revision 1.23 2001/10/09 23:36:25 richard
1118 # Spit out command help if roundup-admin command doesn't get an argument.
1119 #
1120 # Revision 1.22 2001/10/09 07:25:59 richard
1121 # Added the Password property type. See "pydoc roundup.password" for
1122 # implementation details. Have updated some of the documentation too.
1123 #
1124 # Revision 1.21 2001/10/05 02:23:24 richard
1125 # . roundup-admin create now prompts for property info if none is supplied
1126 # on the command-line.
1127 # . hyperdb Class getprops() method may now return only the mutable
1128 # properties.
1129 # . Login now uses cookies, which makes it a whole lot more flexible. We can
1130 # now support anonymous user access (read-only, unless there's an
1131 # "anonymous" user, in which case write access is permitted). Login
1132 # handling has been moved into cgi_client.Client.main()
1133 # . The "extended" schema is now the default in roundup init.
1134 # . The schemas have had their page headings modified to cope with the new
1135 # login handling. Existing installations should copy the interfaces.py
1136 # file from the roundup lib directory to their instance home.
1137 # . Incorrectly had a Bizar Software copyright on the cgitb.py module from
1138 # Ping - has been removed.
1139 # . Fixed a whole bunch of places in the CGI interface where we should have
1140 # been returning Not Found instead of throwing an exception.
1141 # . Fixed a deviation from the spec: trying to modify the 'id' property of
1142 # an item now throws an exception.
1143 #
1144 # Revision 1.20 2001/10/04 02:12:42 richard
1145 # Added nicer command-line item adding: passing no arguments will enter an
1146 # interactive more which asks for each property in turn. While I was at it, I
1147 # fixed an implementation problem WRT the spec - I wasn't raising a
1148 # ValueError if the key property was missing from a create(). Also added a
1149 # protected=boolean argument to getprops() so we can list only the mutable
1150 # properties (defaults to yes, which lists the immutables).
1151 #
1152 # Revision 1.19 2001/10/01 06:40:43 richard
1153 # made do_get have the args in the correct order
1154 #
1155 # Revision 1.18 2001/09/18 22:58:37 richard
1156 #
1157 # Added some more help to roundu-admin
1158 #
1159 # Revision 1.17 2001/08/28 05:58:33 anthonybaxter
1160 # added missing 'import' statements.
1161 #
1162 # Revision 1.16 2001/08/12 06:32:36 richard
1163 # using isinstance(blah, Foo) now instead of isFooType
1164 #
1165 # Revision 1.15 2001/08/07 00:24:42 richard
1166 # stupid typo
1167 #
1168 # Revision 1.14 2001/08/07 00:15:51 richard
1169 # Added the copyright/license notice to (nearly) all files at request of
1170 # Bizar Software.
1171 #
1172 # Revision 1.13 2001/08/05 07:44:13 richard
1173 # Instances are now opened by a special function that generates a unique
1174 # module name for the instances on import time.
1175 #
1176 # Revision 1.12 2001/08/03 01:28:33 richard
1177 # Used the much nicer load_package, pointed out by Steve Majewski.
1178 #
1179 # Revision 1.11 2001/08/03 00:59:34 richard
1180 # Instance import now imports the instance using imp.load_module so that
1181 # we can have instance homes of "roundup" or other existing python package
1182 # names.
1183 #
1184 # Revision 1.10 2001/07/30 08:12:17 richard
1185 # Added time logging and file uploading to the templates.
1186 #
1187 # Revision 1.9 2001/07/30 03:52:55 richard
1188 # init help now lists templates and backends
1189 #
1190 # Revision 1.8 2001/07/30 02:37:07 richard
1191 # Freshen is really broken. Commented out.
1192 #
1193 # Revision 1.7 2001/07/30 01:28:46 richard
1194 # Bugfixes
1195 #
1196 # Revision 1.6 2001/07/30 00:57:51 richard
1197 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
1198 # better internal structure. It's just BETTER. :)
1199 #
1200 # Revision 1.5 2001/07/30 00:04:48 richard
1201 # Made the "init" prompting more friendly.
1202 #
1203 # Revision 1.4 2001/07/29 07:01:39 richard
1204 # Added vim command to all source so that we don't get no steenkin' tabs :)
1205 #
1206 # Revision 1.3 2001/07/23 08:45:28 richard
1207 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
1208 # workable instance_home set up :)
1209 # _and_ anydbm has had its first test :)
1210 #
1211 # Revision 1.2 2001/07/23 08:20:44 richard
1212 # Moved over to using marshal in the bsddb and anydbm backends.
1213 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
1214 # retired - mod hyperdb.Class.list() so it lists retired nodes)
1215 #
1216 # Revision 1.1 2001/07/23 03:46:48 richard
1217 # moving the bin files to facilitate out-of-the-boxness
1218 #
1219 # Revision 1.1 2001/07/22 11:15:45 richard
1220 # More Grande Splite stuff
1221 #
1222 #
1223 # vim: set filetype=python ts=4 sw=4 et si