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