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