f04f62786270dfa8b95255fd866c42eb69cb0b70
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.58 2001-12-31 05:12:52 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 prop_name in prop_names:
640 if not all_props.has_key(prop_name):
641 raise UsageError, '%s has no property "%s"'%(classname,
642 prop_name)
643 else:
644 prop_names = cl.getprops().keys()
646 # now figure column widths
647 props = []
648 for spec in prop_names:
649 if ':' in spec:
650 try:
651 name, width = spec.split(':')
652 except (ValueError, TypeError):
653 raise UsageError, '"%s" not name:width'%spec
654 props.append((spec, int(width)))
655 else:
656 props.append((spec, len(spec)))
658 # now display the heading
659 print ' '.join([name.capitalize() for name, width in props])
661 # and the table data
662 for nodeid in cl.list():
663 l = []
664 for name, width in props:
665 if name != 'id':
666 try:
667 value = str(cl.get(nodeid, name))
668 except KeyError:
669 # we already checked if the property is valid - a
670 # KeyError here means the node just doesn't have a
671 # value for it
672 value = ''
673 else:
674 value = str(nodeid)
675 f = '%%-%ds'%width
676 l.append(f%value[:width])
677 print ' '.join(l)
678 return 0
680 def do_history(self, args):
681 '''Usage: history designator
682 Show the history entries of a designator.
684 Lists the journal entries for the node identified by the designator.
685 '''
686 if len(args) < 1:
687 raise UsageError, 'Not enough arguments supplied'
688 try:
689 classname, nodeid = roundupdb.splitDesignator(args[0])
690 except roundupdb.DesignatorError, message:
691 raise UsageError, message
693 # TODO: handle the -c option?
694 try:
695 print self.db.getclass(classname).history(nodeid)
696 except KeyError:
697 raise UsageError, 'no such class "%s"'%classname
698 except IndexError:
699 raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
700 return 0
702 def do_commit(self, args):
703 '''Usage: commit
704 Commit all changes made to the database.
706 The changes made during an interactive session are not
707 automatically written to the database - they must be committed
708 using this command.
710 One-off commands on the command-line are automatically committed if
711 they are successful.
712 '''
713 self.db.commit()
714 return 0
716 def do_rollback(self, args):
717 '''Usage: rollback
718 Undo all changes that are pending commit to the database.
720 The changes made during an interactive session are not
721 automatically written to the database - they must be committed
722 manually. This command undoes all those changes, so a commit
723 immediately after would make no changes to the database.
724 '''
725 self.db.rollback()
726 return 0
728 def do_retire(self, args):
729 '''Usage: retire designator[,designator]*
730 Retire the node specified by designator.
732 This action indicates that a particular node is not to be retrieved by
733 the list or find commands, and its key value may be re-used.
734 '''
735 if len(args) < 1:
736 raise UsageError, 'Not enough arguments supplied'
737 designators = args[0].split(',')
738 for designator in designators:
739 try:
740 classname, nodeid = roundupdb.splitDesignator(designator)
741 except roundupdb.DesignatorError, message:
742 raise UsageError, message
743 try:
744 self.db.getclass(classname).retire(nodeid)
745 except KeyError:
746 raise UsageError, 'no such class "%s"'%classname
747 except IndexError:
748 raise UsageError, 'no such %s node "%s"'%(classname, nodeid)
749 return 0
751 def do_export(self, args):
752 '''Usage: export class[,class] destination_dir
753 Export the database to tab-separated-value files.
755 This action exports the current data from the database into
756 tab-separated-value files that are placed in the nominated destination
757 directory. The journals are not exported.
758 '''
759 if len(args) < 2:
760 raise UsageError, 'Not enough arguments supplied'
761 classes = args[0].split(',')
762 dir = args[1]
764 # use the csv parser if we can - it's faster
765 if csv is not None:
766 p = csv.parser(field_sep=':')
768 # do all the classes specified
769 for classname in classes:
770 try:
771 cl = self.db.getclass(classname)
772 except KeyError:
773 raise UsageError, 'no such class "%s"'%classname
774 f = open(os.path.join(dir, classname+'.csv'), 'w')
775 f.write(':'.join(cl.properties.keys()) + '\n')
777 # all nodes for this class
778 properties = cl.properties.items()
779 for nodeid in cl.list():
780 l = []
781 for prop, proptype in properties:
782 value = cl.get(nodeid, prop)
783 # convert data where needed
784 if isinstance(proptype, hyperdb.Date):
785 value = value.get_tuple()
786 elif isinstance(proptype, hyperdb.Interval):
787 value = value.get_tuple()
788 elif isinstance(proptype, hyperdb.Password):
789 value = str(value)
790 l.append(repr(value))
792 # now write
793 if csv is not None:
794 f.write(p.join(l) + '\n')
795 else:
796 # escape the individual entries to they're valid CSV
797 m = []
798 for entry in l:
799 if '"' in entry:
800 entry = '""'.join(entry.split('"'))
801 if ':' in entry:
802 entry = '"%s"'%entry
803 m.append(entry)
804 f.write(':'.join(m) + '\n')
805 return 0
807 def do_import(self, args):
808 '''Usage: import class file
809 Import the contents of the tab-separated-value file.
811 The file must define the same properties as the class (including having
812 a "header" line with those property names.) The new nodes are added to
813 the existing database - if you want to create a new database using the
814 imported data, then create a new database (or, tediously, retire all
815 the old data.)
816 '''
817 if len(args) < 2:
818 raise UsageError, 'Not enough arguments supplied'
819 if csv is None:
820 raise UsageError, \
821 'Sorry, you need the csv module to use this function.\n'\
822 'Get it from: http://www.object-craft.com.au/projects/csv/'
824 from roundup import hyperdb
826 # ensure that the properties and the CSV file headings match
827 classname = args[0]
828 try:
829 cl = self.db.getclass(classname)
830 except KeyError:
831 raise UsageError, 'no such class "%s"'%classname
832 f = open(args[1])
833 p = csv.parser(field_sep=':')
834 file_props = p.parse(f.readline())
835 props = cl.properties.keys()
836 m = file_props[:]
837 m.sort()
838 props.sort()
839 if m != props:
840 raise UsageError, 'Import file doesn\'t define the same '\
841 'properties as "%s".'%args[0]
843 # loop through the file and create a node for each entry
844 n = range(len(props))
845 while 1:
846 line = f.readline()
847 if not line: break
849 # parse lines until we get a complete entry
850 while 1:
851 l = p.parse(line)
852 if l: break
854 # make the new node's property map
855 d = {}
856 for i in n:
857 # Use eval to reverse the repr() used to output the CSV
858 value = eval(l[i])
859 # Figure the property for this column
860 key = file_props[i]
861 proptype = cl.properties[key]
862 # Convert for property type
863 if isinstance(proptype, hyperdb.Date):
864 value = date.Date(value)
865 elif isinstance(proptype, hyperdb.Interval):
866 value = date.Interval(value)
867 elif isinstance(proptype, hyperdb.Password):
868 pwd = password.Password()
869 pwd.unpack(value)
870 value = pwd
871 if value is not None:
872 d[key] = value
874 # and create the new node
875 apply(cl.create, (), d)
876 return 0
878 def run_command(self, args):
879 '''Run a single command
880 '''
881 command = args[0]
883 # handle help now
884 if command == 'help':
885 if len(args)>1:
886 self.do_help(args[1:])
887 return 0
888 self.do_help(['help'])
889 return 0
890 if command == 'morehelp':
891 self.do_help(['help'])
892 self.help_commands()
893 self.help_all()
894 return 0
896 # figure what the command is
897 try:
898 functions = self.commands.get(command)
899 except KeyError:
900 # not a valid command
901 print 'Unknown command "%s" ("help commands" for a list)'%command
902 return 1
904 # check for multiple matches
905 if len(functions) > 1:
906 print 'Multiple commands match "%s": %s'%(command,
907 ', '.join([i[0] for i in functions]))
908 return 1
909 command, function = functions[0]
911 # make sure we have an instance_home
912 while not self.instance_home:
913 self.instance_home = raw_input('Enter instance home: ').strip()
915 # before we open the db, we may be doing an init
916 if command == 'initialise':
917 return self.do_initialise(self.instance_home, args)
919 # get the instance
920 try:
921 instance = roundup.instance.open(self.instance_home)
922 except ValueError, message:
923 self.instance_home = ''
924 print "Couldn't open instance: %s"%message
925 return 1
927 # only open the database once!
928 if not self.db:
929 self.db = instance.open('admin')
931 # do the command
932 ret = 0
933 try:
934 ret = function(args[1:])
935 except UsageError, message:
936 print 'Error: %s'%message
937 print function.__doc__
938 ret = 1
939 except:
940 import traceback
941 traceback.print_exc()
942 ret = 1
943 return ret
945 def interactive(self):
946 '''Run in an interactive mode
947 '''
948 print 'Roundup {version} ready for input.'
949 print 'Type "help" for help.'
950 try:
951 import readline
952 except ImportError:
953 print "Note: command history and editing not available"
955 while 1:
956 try:
957 command = raw_input('roundup> ')
958 except EOFError:
959 print 'exit...'
960 break
961 if not command: continue
962 args = token.token_split(command)
963 if not args: continue
964 if args[0] in ('quit', 'exit'): break
965 self.run_command(args)
967 # exit.. check for transactions
968 if self.db and self.db.transactions:
969 commit = raw_input("There are unsaved changes. Commit them (y/N)? ")
970 if commit and commit[0].lower() == 'y':
971 self.db.commit()
972 return 0
974 def main(self):
975 try:
976 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
977 except getopt.GetoptError, e:
978 self.usage(str(e))
979 return 1
981 # handle command-line args
982 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
983 name = password = ''
984 if os.environ.has_key('ROUNDUP_LOGIN'):
985 l = os.environ['ROUNDUP_LOGIN'].split(':')
986 name = l[0]
987 if len(l) > 1:
988 password = l[1]
989 self.comma_sep = 0
990 for opt, arg in opts:
991 if opt == '-h':
992 self.usage()
993 return 0
994 if opt == '-i':
995 self.instance_home = arg
996 if opt == '-c':
997 self.comma_sep = 1
999 # if no command - go interactive
1000 ret = 0
1001 if not args:
1002 self.interactive()
1003 else:
1004 ret = self.run_command(args)
1005 if self.db: self.db.commit()
1006 return ret
1009 if __name__ == '__main__':
1010 tool = AdminTool()
1011 sys.exit(tool.main())
1013 #
1014 # $Log: not supported by cvs2svn $
1015 # Revision 1.57 2001/12/31 05:12:01 richard
1016 # added some quoting instructions to roundup-admin
1017 #
1018 # Revision 1.56 2001/12/31 05:09:20 richard
1019 # Added better tokenising to roundup-admin - handles spaces and stuff. Can
1020 # use quoting or backslashes. See the roundup.token pydoc.
1021 #
1022 # Revision 1.55 2001/12/17 03:52:47 richard
1023 # Implemented file store rollback. As a bonus, the hyperdb is now capable of
1024 # storing more than one file per node - if a property name is supplied,
1025 # the file is called designator.property.
1026 # I decided not to migrate the existing files stored over to the new naming
1027 # scheme - the FileClass just doesn't specify the property name.
1028 #
1029 # Revision 1.54 2001/12/15 23:09:23 richard
1030 # Some cleanups in roundup-admin, also made it work again...
1031 #
1032 # Revision 1.53 2001/12/13 00:20:00 richard
1033 # . Centralised the python version check code, bumped version to 2.1.1 (really
1034 # needs to be 2.1.2, but that isn't released yet :)
1035 #
1036 # Revision 1.52 2001/12/12 21:47:45 richard
1037 # . Message author's name appears in From: instead of roundup instance name
1038 # (which still appears in the Reply-To:)
1039 # . envelope-from is now set to the roundup-admin and not roundup itself so
1040 # delivery reports aren't sent to roundup (thanks Patrick Ohly)
1041 #
1042 # Revision 1.51 2001/12/10 00:57:38 richard
1043 # From CHANGES:
1044 # . Added the "display" command to the admin tool - displays a node's values
1045 # . #489760 ] [issue] only subject
1046 # . fixed the doc/index.html to include the quoting in the mail alias.
1047 #
1048 # Also:
1049 # . fixed roundup-admin so it works with transactions
1050 # . disabled the back_anydbm module if anydbm tries to use dumbdbm
1051 #
1052 # Revision 1.50 2001/12/02 05:06:16 richard
1053 # . We now use weakrefs in the Classes to keep the database reference, so
1054 # the close() method on the database is no longer needed.
1055 # I bumped the minimum python requirement up to 2.1 accordingly.
1056 # . #487480 ] roundup-server
1057 # . #487476 ] INSTALL.txt
1058 #
1059 # I also cleaned up the change message / post-edit stuff in the cgi client.
1060 # There's now a clearly marked "TODO: append the change note" where I believe
1061 # the change note should be added there. The "changes" list will obviously
1062 # have to be modified to be a dict of the changes, or somesuch.
1063 #
1064 # More testing needed.
1065 #
1066 # Revision 1.49 2001/12/01 07:17:50 richard
1067 # . We now have basic transaction support! Information is only written to
1068 # the database when the commit() method is called. Only the anydbm
1069 # backend is modified in this way - neither of the bsddb backends have been.
1070 # The mail, admin and cgi interfaces all use commit (except the admin tool
1071 # doesn't have a commit command, so interactive users can't commit...)
1072 # . Fixed login/registration forwarding the user to the right page (or not,
1073 # on a failure)
1074 #
1075 # Revision 1.48 2001/11/27 22:32:03 richard
1076 # typo
1077 #
1078 # Revision 1.47 2001/11/26 22:55:56 richard
1079 # Feature:
1080 # . Added INSTANCE_NAME to configuration - used in web and email to identify
1081 # the instance.
1082 # . Added EMAIL_SIGNATURE_POSITION to indicate where to place the roundup
1083 # signature info in e-mails.
1084 # . Some more flexibility in the mail gateway and more error handling.
1085 # . Login now takes you to the page you back to the were denied access to.
1086 #
1087 # Fixed:
1088 # . Lots of bugs, thanks Roché and others on the devel mailing list!
1089 #
1090 # Revision 1.46 2001/11/21 03:40:54 richard
1091 # more new property handling
1092 #
1093 # Revision 1.45 2001/11/12 22:51:59 jhermann
1094 # Fixed option & associated error handling
1095 #
1096 # Revision 1.44 2001/11/12 22:01:06 richard
1097 # Fixed issues with nosy reaction and author copies.
1098 #
1099 # Revision 1.43 2001/11/09 22:33:28 richard
1100 # More error handling fixes.
1101 #
1102 # Revision 1.42 2001/11/09 10:11:08 richard
1103 # . roundup-admin now handles all hyperdb exceptions
1104 #
1105 # Revision 1.41 2001/11/09 01:25:40 richard
1106 # Should parse with python 1.5.2 now.
1107 #
1108 # Revision 1.40 2001/11/08 04:42:00 richard
1109 # Expanded the already-abbreviated "initialise" and "specification" commands,
1110 # and added a comment to the command help about the abbreviation.
1111 #
1112 # Revision 1.39 2001/11/08 04:29:59 richard
1113 # roundup-admin now accepts abbreviated commands (eg. l = li = lis = list)
1114 # [thanks Engelbert Gruber for the inspiration]
1115 #
1116 # Revision 1.38 2001/11/05 23:45:40 richard
1117 # Fixed newuser_action so it sets the cookie with the unencrypted password.
1118 # Also made it present nicer error messages (not tracebacks).
1119 #
1120 # Revision 1.37 2001/10/23 01:00:18 richard
1121 # Re-enabled login and registration access after lopping them off via
1122 # disabling access for anonymous users.
1123 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
1124 # a couple of bugs while I was there. Probably introduced a couple, but
1125 # things seem to work OK at the moment.
1126 #
1127 # Revision 1.36 2001/10/21 00:45:15 richard
1128 # Added author identification to e-mail messages from roundup.
1129 #
1130 # Revision 1.35 2001/10/20 11:58:48 richard
1131 # Catch errors in login - no username or password supplied.
1132 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
1133 #
1134 # Revision 1.34 2001/10/18 02:16:42 richard
1135 # Oops, committed the admin script with the wierd #! line.
1136 # Also, made the thing into a class to reduce parameter passing.
1137 # Nuked the leading whitespace from the help __doc__ displays too.
1138 #
1139 # Revision 1.33 2001/10/17 23:13:19 richard
1140 # Did a fair bit of work on the admin tool. Now has an extra command "table"
1141 # which displays node information in a tabular format. Also fixed import and
1142 # export so they work. Removed freshen.
1143 # Fixed quopri usage in mailgw from bug reports.
1144 #
1145 # Revision 1.32 2001/10/17 06:57:29 richard
1146 # Interactive startup blurb - need to figure how to get the version in there.
1147 #
1148 # Revision 1.31 2001/10/17 06:17:26 richard
1149 # Now with readline support :)
1150 #
1151 # Revision 1.30 2001/10/17 06:04:00 richard
1152 # Beginnings of an interactive mode for roundup-admin
1153 #
1154 # Revision 1.29 2001/10/16 03:48:01 richard
1155 # admin tool now complains if a "find" is attempted with a non-link property.
1156 #
1157 # Revision 1.28 2001/10/13 00:07:39 richard
1158 # More help in admin tool.
1159 #
1160 # Revision 1.27 2001/10/11 23:43:04 richard
1161 # Implemented the comma-separated printing option in the admin tool.
1162 # Fixed a typo (more of a vim-o actually :) in mailgw.
1163 #
1164 # Revision 1.26 2001/10/11 05:03:51 richard
1165 # Marked the roundup-admin import/export as experimental since they're not fully
1166 # operational.
1167 #
1168 # Revision 1.25 2001/10/10 04:12:32 richard
1169 # The setup.cfg file is just causing pain. Away it goes.
1170 #
1171 # Revision 1.24 2001/10/10 03:54:57 richard
1172 # Added database importing and exporting through CSV files.
1173 # Uses the csv module from object-craft for exporting if it's available.
1174 # Requires the csv module for importing.
1175 #
1176 # Revision 1.23 2001/10/09 23:36:25 richard
1177 # Spit out command help if roundup-admin command doesn't get an argument.
1178 #
1179 # Revision 1.22 2001/10/09 07:25:59 richard
1180 # Added the Password property type. See "pydoc roundup.password" for
1181 # implementation details. Have updated some of the documentation too.
1182 #
1183 # Revision 1.21 2001/10/05 02:23:24 richard
1184 # . roundup-admin create now prompts for property info if none is supplied
1185 # on the command-line.
1186 # . hyperdb Class getprops() method may now return only the mutable
1187 # properties.
1188 # . Login now uses cookies, which makes it a whole lot more flexible. We can
1189 # now support anonymous user access (read-only, unless there's an
1190 # "anonymous" user, in which case write access is permitted). Login
1191 # handling has been moved into cgi_client.Client.main()
1192 # . The "extended" schema is now the default in roundup init.
1193 # . The schemas have had their page headings modified to cope with the new
1194 # login handling. Existing installations should copy the interfaces.py
1195 # file from the roundup lib directory to their instance home.
1196 # . Incorrectly had a Bizar Software copyright on the cgitb.py module from
1197 # Ping - has been removed.
1198 # . Fixed a whole bunch of places in the CGI interface where we should have
1199 # been returning Not Found instead of throwing an exception.
1200 # . Fixed a deviation from the spec: trying to modify the 'id' property of
1201 # an item now throws an exception.
1202 #
1203 # Revision 1.20 2001/10/04 02:12:42 richard
1204 # Added nicer command-line item adding: passing no arguments will enter an
1205 # interactive more which asks for each property in turn. While I was at it, I
1206 # fixed an implementation problem WRT the spec - I wasn't raising a
1207 # ValueError if the key property was missing from a create(). Also added a
1208 # protected=boolean argument to getprops() so we can list only the mutable
1209 # properties (defaults to yes, which lists the immutables).
1210 #
1211 # Revision 1.19 2001/10/01 06:40:43 richard
1212 # made do_get have the args in the correct order
1213 #
1214 # Revision 1.18 2001/09/18 22:58:37 richard
1215 #
1216 # Added some more help to roundu-admin
1217 #
1218 # Revision 1.17 2001/08/28 05:58:33 anthonybaxter
1219 # added missing 'import' statements.
1220 #
1221 # Revision 1.16 2001/08/12 06:32:36 richard
1222 # using isinstance(blah, Foo) now instead of isFooType
1223 #
1224 # Revision 1.15 2001/08/07 00:24:42 richard
1225 # stupid typo
1226 #
1227 # Revision 1.14 2001/08/07 00:15:51 richard
1228 # Added the copyright/license notice to (nearly) all files at request of
1229 # Bizar Software.
1230 #
1231 # Revision 1.13 2001/08/05 07:44:13 richard
1232 # Instances are now opened by a special function that generates a unique
1233 # module name for the instances on import time.
1234 #
1235 # Revision 1.12 2001/08/03 01:28:33 richard
1236 # Used the much nicer load_package, pointed out by Steve Majewski.
1237 #
1238 # Revision 1.11 2001/08/03 00:59:34 richard
1239 # Instance import now imports the instance using imp.load_module so that
1240 # we can have instance homes of "roundup" or other existing python package
1241 # names.
1242 #
1243 # Revision 1.10 2001/07/30 08:12:17 richard
1244 # Added time logging and file uploading to the templates.
1245 #
1246 # Revision 1.9 2001/07/30 03:52:55 richard
1247 # init help now lists templates and backends
1248 #
1249 # Revision 1.8 2001/07/30 02:37:07 richard
1250 # Freshen is really broken. Commented out.
1251 #
1252 # Revision 1.7 2001/07/30 01:28:46 richard
1253 # Bugfixes
1254 #
1255 # Revision 1.6 2001/07/30 00:57:51 richard
1256 # Now uses getopt, much improved command-line parsing. Much fuller help. Much
1257 # better internal structure. It's just BETTER. :)
1258 #
1259 # Revision 1.5 2001/07/30 00:04:48 richard
1260 # Made the "init" prompting more friendly.
1261 #
1262 # Revision 1.4 2001/07/29 07:01:39 richard
1263 # Added vim command to all source so that we don't get no steenkin' tabs :)
1264 #
1265 # Revision 1.3 2001/07/23 08:45:28 richard
1266 # ok, so now "./roundup-admin init" will ask questions in an attempt to get a
1267 # workable instance_home set up :)
1268 # _and_ anydbm has had its first test :)
1269 #
1270 # Revision 1.2 2001/07/23 08:20:44 richard
1271 # Moved over to using marshal in the bsddb and anydbm backends.
1272 # roundup-admin now has a "freshen" command that'll load/save all nodes (not
1273 # retired - mod hyperdb.Class.list() so it lists retired nodes)
1274 #
1275 # Revision 1.1 2001/07/23 03:46:48 richard
1276 # moving the bin files to facilitate out-of-the-boxness
1277 #
1278 # Revision 1.1 2001/07/22 11:15:45 richard
1279 # More Grande Splite stuff
1280 #
1281 #
1282 # vim: set filetype=python ts=4 sw=4 et si