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