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: admin.py,v 1.10 2002-04-27 10:07:23 richard Exp $
21 import sys, os, getpass, getopt, re, UserDict, shlex
22 try:
23 import csv
24 except ImportError:
25 csv = None
26 from roundup import date, hyperdb, roundupdb, init, password, token
27 import roundup.instance
28 from roundup.i18n import _
30 class CommandDict(UserDict.UserDict):
31 '''Simple dictionary that lets us do lookups using partial keys.
33 Original code submitted by Engelbert Gruber.
34 '''
35 _marker = []
36 def get(self, key, default=_marker):
37 if self.data.has_key(key):
38 return [(key, self.data[key])]
39 keylist = self.data.keys()
40 keylist.sort()
41 l = []
42 for ki in keylist:
43 if ki.startswith(key):
44 l.append((ki, self.data[ki]))
45 if not l and default is self._marker:
46 raise KeyError, key
47 return l
49 class UsageError(ValueError):
50 pass
52 class AdminTool:
54 def __init__(self):
55 self.commands = CommandDict()
56 for k in AdminTool.__dict__.keys():
57 if k[:3] == 'do_':
58 self.commands[k[3:]] = getattr(self, k)
59 self.help = {}
60 for k in AdminTool.__dict__.keys():
61 if k[:5] == 'help_':
62 self.help[k[5:]] = getattr(self, k)
63 self.instance_home = ''
64 self.db = None
66 def get_class(self, classname):
67 '''Get the class - raise an exception if it doesn't exist.
68 '''
69 try:
70 return self.db.getclass(classname)
71 except KeyError:
72 raise UsageError, _('no such class "%(classname)s"')%locals()
74 def props_from_args(self, args):
75 props = {}
76 for arg in args:
77 if arg.find('=') == -1:
78 raise UsageError, _('argument "%(arg)s" not propname=value')%locals()
79 try:
80 key, value = arg.split('=')
81 except ValueError:
82 raise UsageError, _('argument "%(arg)s" not propname=value')%locals()
83 props[key] = value
84 return props
86 def usage(self, message=''):
87 if message:
88 message = _('Problem: %(message)s)\n\n')%locals()
89 print _('''%(message)sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments>
91 Help:
92 roundup-admin -h
93 roundup-admin help -- this help
94 roundup-admin help <command> -- command-specific help
95 roundup-admin help all -- all available help
96 Options:
97 -i instance home -- specify the issue tracker "home directory" to administer
98 -u -- the user[:password] to use for commands
99 -c -- when outputting lists of data, just comma-separate them''')%locals()
100 self.help_commands()
102 def help_commands(self):
103 print _('Commands:'),
104 commands = ['']
105 for command in self.commands.values():
106 h = command.__doc__.split('\n')[0]
107 commands.append(' '+h[7:])
108 commands.sort()
109 commands.append(_('Commands may be abbreviated as long as the abbreviation matches only one'))
110 commands.append(_('command, e.g. l == li == lis == list.'))
111 print '\n'.join(commands)
112 print
114 def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
115 commands = self.commands.values()
116 def sortfun(a, b):
117 return cmp(a.__name__, b.__name__)
118 commands.sort(sortfun)
119 for command in commands:
120 h = command.__doc__.split('\n')
121 name = command.__name__[3:]
122 usage = h[0]
123 print _('''
124 <tr><td valign=top><strong>%(name)s</strong></td>
125 <td><tt>%(usage)s</tt><p>
126 <pre>''')%locals()
127 indent = indent_re.match(h[3])
128 if indent: indent = len(indent.group(1))
129 for line in h[3:]:
130 if indent:
131 print line[indent:]
132 else:
133 print line
134 print _('</pre></td></tr>\n')
136 def help_all(self):
137 print _('''
138 All commands (except help) require an instance specifier. This is just the path
139 to the roundup instance you're working with. A roundup instance is where
140 roundup keeps the database and configuration file that defines an issue
141 tracker. It may be thought of as the issue tracker's "home directory". It may
142 be specified in the environment variable ROUNDUP_INSTANCE or on the command
143 line as "-i instance".
145 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
147 Property values are represented as strings in command arguments and in the
148 printed results:
149 . Strings are, well, strings.
150 . Date values are printed in the full date format in the local time zone, and
151 accepted in the full format or any of the partial formats explained below.
152 . Link values are printed as node designators. When given as an argument,
153 node designators and key strings are both accepted.
154 . Multilink values are printed as lists of node designators joined by commas.
155 When given as an argument, node designators and key strings are both
156 accepted; an empty string, a single node, or a list of nodes joined by
157 commas is accepted.
159 When property values must contain spaces, just surround the value with
160 quotes, either ' or ". A single space may also be backslash-quoted. If a
161 valuu must contain a quote character, it must be backslash-quoted or inside
162 quotes. Examples:
163 hello world (2 tokens: hello, world)
164 "hello world" (1 token: hello world)
165 "Roch'e" Compaan (2 tokens: Roch'e Compaan)
166 Roch\'e Compaan (2 tokens: Roch'e Compaan)
167 address="1 2 3" (1 token: address=1 2 3)
168 \\ (1 token: \)
169 \n\r\t (1 token: a newline, carriage-return and tab)
171 When multiple nodes are specified to the roundup get or roundup set
172 commands, the specified properties are retrieved or set on all the listed
173 nodes.
175 When multiple results are returned by the roundup get or roundup find
176 commands, they are printed one per line (default) or joined by commas (with
177 the -c) option.
179 Where the command changes data, a login name/password is required. The
180 login may be specified as either "name" or "name:password".
181 . ROUNDUP_LOGIN environment variable
182 . the -u command-line option
183 If either the name or password is not supplied, they are obtained from the
184 command-line.
186 Date format examples:
187 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
188 "2000-04-17" means <Date 2000-04-17.00:00:00>
189 "01-25" means <Date yyyy-01-25.00:00:00>
190 "08-13.22:13" means <Date yyyy-08-14.03:13:00>
191 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
192 "14:25" means <Date yyyy-mm-dd.19:25:00>
193 "8:47:11" means <Date yyyy-mm-dd.13:47:11>
194 "." means "right now"
196 Command help:
197 ''')
198 for name, command in self.commands.items():
199 print _('%s:')%name
200 print _(' '), command.__doc__
202 def do_help(self, args, nl_re=re.compile('[\r\n]'),
203 indent_re=re.compile(r'^(\s+)\S+')):
204 '''Usage: help topic
205 Give help about topic.
207 commands -- list commands
208 <command> -- help specific to a command
209 initopts -- init command options
210 all -- all available help
211 '''
212 if len(args)>0:
213 topic = args[0]
214 else:
215 topic = 'help'
218 # try help_ methods
219 if self.help.has_key(topic):
220 self.help[topic]()
221 return 0
223 # try command docstrings
224 try:
225 l = self.commands.get(topic)
226 except KeyError:
227 print _('Sorry, no help for "%(topic)s"')%locals()
228 return 1
230 # display the help for each match, removing the docsring indent
231 for name, help in l:
232 lines = nl_re.split(help.__doc__)
233 print lines[0]
234 indent = indent_re.match(lines[1])
235 if indent: indent = len(indent.group(1))
236 for line in lines[1:]:
237 if indent:
238 print line[indent:]
239 else:
240 print line
241 return 0
243 def help_initopts(self):
244 import roundup.templates
245 templates = roundup.templates.listTemplates()
246 print _('Templates:'), ', '.join(templates)
247 import roundup.backends
248 backends = roundup.backends.__all__
249 print _('Back ends:'), ', '.join(backends)
252 def do_initialise(self, instance_home, args):
253 '''Usage: initialise [template [backend [admin password]]]
254 Initialise a new Roundup instance.
256 The command will prompt for the instance home directory (if not supplied
257 through INSTANCE_HOME or the -i option). The template, backend and admin
258 password may be specified on the command-line as arguments, in that
259 order.
261 See also initopts help.
262 '''
263 if len(args) < 1:
264 raise UsageError, _('Not enough arguments supplied')
266 # make sure the instance home can be created
267 parent = os.path.split(instance_home)[0]
268 if not os.path.exists(parent):
269 raise UsageError, _('Instance home parent directory "%(parent)s"'
270 ' does not exist')%locals()
272 # select template
273 import roundup.templates
274 templates = roundup.templates.listTemplates()
275 template = len(args) > 1 and args[1] or ''
276 if template not in templates:
277 print _('Templates:'), ', '.join(templates)
278 while template not in templates:
279 template = raw_input(_('Select template [classic]: ')).strip()
280 if not template:
281 template = 'classic'
283 # select hyperdb backend
284 import roundup.backends
285 backends = roundup.backends.__all__
286 backend = len(args) > 2 and args[2] or ''
287 if backend not in backends:
288 print _('Back ends:'), ', '.join(backends)
289 while backend not in backends:
290 backend = raw_input(_('Select backend [anydbm]: ')).strip()
291 if not backend:
292 backend = 'anydbm'
294 # admin password
295 if len(args) > 3:
296 adminpw = confirm = args[3]
297 else:
298 adminpw = ''
299 confirm = 'x'
300 while adminpw != confirm:
301 adminpw = getpass.getpass(_('Admin Password: '))
302 confirm = getpass.getpass(_(' Confirm: '))
304 # create!
305 init.init(instance_home, template, backend, adminpw)
307 return 0
310 def do_get(self, args):
311 '''Usage: get property designator[,designator]*
312 Get the given property of one or more designator(s).
314 Retrieves the property value of the nodes specified by the designators.
315 '''
316 if len(args) < 2:
317 raise UsageError, _('Not enough arguments supplied')
318 propname = args[0]
319 designators = args[1].split(',')
320 l = []
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 cl = self.get_class(classname)
330 try:
331 if self.comma_sep:
332 l.append(cl.get(nodeid, propname))
333 else:
334 print cl.get(nodeid, propname)
335 except IndexError:
336 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
337 except KeyError:
338 raise UsageError, _('no such %(classname)s property '
339 '"%(propname)s"')%locals()
340 if self.comma_sep:
341 print ','.join(l)
342 return 0
345 def do_set(self, args):
346 '''Usage: set designator[,designator]* propname=value ...
347 Set the given property of one or more designator(s).
349 Sets the property to the value for all designators given.
350 '''
351 if len(args) < 2:
352 raise UsageError, _('Not enough arguments supplied')
353 from roundup import hyperdb
355 designators = args[0].split(',')
357 # get the props from the args
358 props = self.props_from_args(args[1:])
360 # now do the set for all the nodes
361 for designator in designators:
362 # decode the node designator
363 try:
364 classname, nodeid = roundupdb.splitDesignator(designator)
365 except roundupdb.DesignatorError, message:
366 raise UsageError, message
368 # get the class
369 cl = self.get_class(classname)
371 properties = cl.getprops()
372 for key, value in props.items():
373 proptype = properties[key]
374 if isinstance(proptype, hyperdb.String):
375 continue
376 elif isinstance(proptype, hyperdb.Password):
377 props[key] = password.Password(value)
378 elif isinstance(proptype, hyperdb.Date):
379 try:
380 props[key] = date.Date(value)
381 except ValueError, message:
382 raise UsageError, '"%s": %s'%(value, message)
383 elif isinstance(proptype, hyperdb.Interval):
384 try:
385 props[key] = date.Interval(value)
386 except ValueError, message:
387 raise UsageError, '"%s": %s'%(value, message)
388 elif isinstance(proptype, hyperdb.Link):
389 props[key] = value
390 elif isinstance(proptype, hyperdb.Multilink):
391 props[key] = value.split(',')
393 # try the set
394 try:
395 apply(cl.set, (nodeid, ), props)
396 except (TypeError, IndexError, ValueError), message:
397 raise UsageError, message
398 return 0
400 def do_find(self, args):
401 '''Usage: find classname propname=value ...
402 Find the nodes of the given class with a given link property value.
404 Find the nodes of the given class with a given link property value. The
405 value may be either the nodeid of the linked node, or its key value.
406 '''
407 if len(args) < 1:
408 raise UsageError, _('Not enough arguments supplied')
409 classname = args[0]
410 # get the class
411 cl = self.get_class(classname)
413 # handle the propname=value argument
414 props = self.props_from_args(args[1:])
416 # if the value isn't a number, look up the linked class to get the
417 # number
418 for propname, value in props.items():
419 num_re = re.compile('^\d+$')
420 if not num_re.match(value):
421 # get the property
422 try:
423 property = cl.properties[propname]
424 except KeyError:
425 raise UsageError, _('%(classname)s has no property '
426 '"%(propname)s"')%locals()
428 # make sure it's a link
429 if (not isinstance(property, hyperdb.Link) and not
430 isinstance(property, hyperdb.Multilink)):
431 raise UsageError, _('You may only "find" link properties')
433 # get the linked-to class and look up the key property
434 link_class = self.db.getclass(property.classname)
435 try:
436 props[propname] = link_class.lookup(value)
437 except TypeError:
438 raise UsageError, _('%(classname)s has no key property"')%{
439 'classname': link_class.classname}
440 except KeyError:
441 raise UsageError, _('%(classname)s has no entry "%(propname)s"')%{
442 'classname': link_class.classname, 'propname': propname}
444 # now do the find
445 try:
446 if self.comma_sep:
447 print ','.join(apply(cl.find, (), props))
448 else:
449 print apply(cl.find, (), props)
450 except KeyError:
451 raise UsageError, _('%(classname)s has no property '
452 '"%(propname)s"')%locals()
453 except (ValueError, TypeError), message:
454 raise UsageError, message
455 return 0
457 def do_specification(self, args):
458 '''Usage: specification classname
459 Show the properties for a classname.
461 This lists the properties for a given class.
462 '''
463 if len(args) < 1:
464 raise UsageError, _('Not enough arguments supplied')
465 classname = args[0]
466 # get the class
467 cl = self.get_class(classname)
469 # get the key property
470 keyprop = cl.getkey()
471 for key, value in cl.properties.items():
472 if keyprop == key:
473 print _('%(key)s: %(value)s (key property)')%locals()
474 else:
475 print _('%(key)s: %(value)s')%locals()
477 def do_display(self, args):
478 '''Usage: display designator
479 Show the property values for the given node.
481 This lists the properties and their associated values for the given
482 node.
483 '''
484 if len(args) < 1:
485 raise UsageError, _('Not enough arguments supplied')
487 # decode the node designator
488 try:
489 classname, nodeid = roundupdb.splitDesignator(args[0])
490 except roundupdb.DesignatorError, message:
491 raise UsageError, message
493 # get the class
494 cl = self.get_class(classname)
496 # display the values
497 for key in cl.properties.keys():
498 value = cl.get(nodeid, key)
499 print _('%(key)s: %(value)s')%locals()
501 def do_create(self, args):
502 '''Usage: create classname property=value ...
503 Create a new entry of a given class.
505 This creates a new entry of the given class using the property
506 name=value arguments provided on the command line after the "create"
507 command.
508 '''
509 if len(args) < 1:
510 raise UsageError, _('Not enough arguments supplied')
511 from roundup import hyperdb
513 classname = args[0]
515 # get the class
516 cl = self.get_class(classname)
518 # now do a create
519 props = {}
520 properties = cl.getprops(protected = 0)
521 if len(args) == 1:
522 # ask for the properties
523 for key, value in properties.items():
524 if key == 'id': continue
525 name = value.__class__.__name__
526 if isinstance(value , hyperdb.Password):
527 again = None
528 while value != again:
529 value = getpass.getpass(_('%(propname)s (Password): ')%{
530 'propname': key.capitalize()})
531 again = getpass.getpass(_(' %(propname)s (Again): ')%{
532 'propname': key.capitalize()})
533 if value != again: print _('Sorry, try again...')
534 if value:
535 props[key] = value
536 else:
537 value = raw_input(_('%(propname)s (%(proptype)s): ')%{
538 'propname': key.capitalize(), 'proptype': name})
539 if value:
540 props[key] = value
541 else:
542 props = self.props_from_args(args[1:])
544 # convert types
545 for propname, value in props.items():
546 # get the property
547 try:
548 proptype = properties[propname]
549 except KeyError:
550 raise UsageError, _('%(classname)s has no property '
551 '"%(propname)s"')%locals()
553 if isinstance(proptype, hyperdb.Date):
554 try:
555 props[propname] = date.Date(value)
556 except ValueError, message:
557 raise UsageError, _('"%(value)s": %(message)s')%locals()
558 elif isinstance(proptype, hyperdb.Interval):
559 try:
560 props[propname] = date.Interval(value)
561 except ValueError, message:
562 raise UsageError, _('"%(value)s": %(message)s')%locals()
563 elif isinstance(proptype, hyperdb.Password):
564 props[propname] = password.Password(value)
565 elif isinstance(proptype, hyperdb.Multilink):
566 props[propname] = value.split(',')
568 # check for the key property
569 propname = cl.getkey()
570 if propname and not props.has_key(propname):
571 raise UsageError, _('you must provide the "%(propname)s" '
572 'property.')%locals()
574 # do the actual create
575 try:
576 print apply(cl.create, (), props)
577 except (TypeError, IndexError, ValueError), message:
578 raise UsageError, message
579 return 0
581 def do_list(self, args):
582 '''Usage: list classname [property]
583 List the instances of a class.
585 Lists all instances of the given class. If the property is not
586 specified, the "label" property is used. The label property is tried
587 in order: the key, "name", "title" and then the first property,
588 alphabetically.
589 '''
590 if len(args) < 1:
591 raise UsageError, _('Not enough arguments supplied')
592 classname = args[0]
594 # get the class
595 cl = self.get_class(classname)
597 # figure the property
598 if len(args) > 1:
599 propname = args[1]
600 else:
601 propname = cl.labelprop()
603 if self.comma_sep:
604 print ','.join(cl.list())
605 else:
606 for nodeid in cl.list():
607 try:
608 value = cl.get(nodeid, propname)
609 except KeyError:
610 raise UsageError, _('%(classname)s has no property '
611 '"%(propname)s"')%locals()
612 print _('%(nodeid)4s: %(value)s')%locals()
613 return 0
615 def do_table(self, args):
616 '''Usage: table classname [property[,property]*]
617 List the instances of a class in tabular form.
619 Lists all instances of the given class. If the properties are not
620 specified, all properties are displayed. By default, the column widths
621 are the width of the property names. The width may be explicitly defined
622 by defining the property as "name:width". For example::
623 roundup> table priority id,name:10
624 Id Name
625 1 fatal-bug
626 2 bug
627 3 usability
628 4 feature
629 '''
630 if len(args) < 1:
631 raise UsageError, _('Not enough arguments supplied')
632 classname = args[0]
634 # get the class
635 cl = self.get_class(classname)
637 # figure the property names to display
638 if len(args) > 1:
639 prop_names = args[1].split(',')
640 all_props = cl.getprops()
641 for spec in prop_names:
642 if ':' in spec:
643 try:
644 propname, width = spec.split(':')
645 except (ValueError, TypeError):
646 raise UsageError, _('"%(spec)s" not name:width')%locals()
647 else:
648 propname = spec
649 if not all_props.has_key(propname):
650 raise UsageError, _('%(classname)s has no property '
651 '"%(propname)s"')%locals()
652 else:
653 prop_names = cl.getprops().keys()
655 # now figure column widths
656 props = []
657 for spec in prop_names:
658 if ':' in spec:
659 name, width = spec.split(':')
660 props.append((name, int(width)))
661 else:
662 props.append((spec, len(spec)))
664 # now display the heading
665 print ' '.join([name.capitalize().ljust(width) for name,width in props])
667 # and the table data
668 for nodeid in cl.list():
669 l = []
670 for name, width in props:
671 if name != 'id':
672 try:
673 value = str(cl.get(nodeid, name))
674 except KeyError:
675 # we already checked if the property is valid - a
676 # KeyError here means the node just doesn't have a
677 # value for it
678 value = ''
679 else:
680 value = str(nodeid)
681 f = '%%-%ds'%width
682 l.append(f%value[:width])
683 print ' '.join(l)
684 return 0
686 def do_history(self, args):
687 '''Usage: history designator
688 Show the history entries of a designator.
690 Lists the journal entries for the node identified by the designator.
691 '''
692 if len(args) < 1:
693 raise UsageError, _('Not enough arguments supplied')
694 try:
695 classname, nodeid = roundupdb.splitDesignator(args[0])
696 except roundupdb.DesignatorError, message:
697 raise UsageError, message
699 try:
700 print self.db.getclass(classname).history(nodeid)
701 except KeyError:
702 raise UsageError, _('no such class "%(classname)s"')%locals()
703 except IndexError:
704 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
705 return 0
707 def do_commit(self, args):
708 '''Usage: commit
709 Commit all changes made to the database.
711 The changes made during an interactive session are not
712 automatically written to the database - they must be committed
713 using this command.
715 One-off commands on the command-line are automatically committed if
716 they are successful.
717 '''
718 self.db.commit()
719 return 0
721 def do_rollback(self, args):
722 '''Usage: rollback
723 Undo all changes that are pending commit to the database.
725 The changes made during an interactive session are not
726 automatically written to the database - they must be committed
727 manually. This command undoes all those changes, so a commit
728 immediately after would make no changes to the database.
729 '''
730 self.db.rollback()
731 return 0
733 def do_retire(self, args):
734 '''Usage: retire designator[,designator]*
735 Retire the node specified by designator.
737 This action indicates that a particular node is not to be retrieved by
738 the list or find commands, and its key value may be re-used.
739 '''
740 if len(args) < 1:
741 raise UsageError, _('Not enough arguments supplied')
742 designators = args[0].split(',')
743 for designator in designators:
744 try:
745 classname, nodeid = roundupdb.splitDesignator(designator)
746 except roundupdb.DesignatorError, message:
747 raise UsageError, message
748 try:
749 self.db.getclass(classname).retire(nodeid)
750 except KeyError:
751 raise UsageError, _('no such class "%(classname)s"')%locals()
752 except IndexError:
753 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
754 return 0
756 def do_export(self, args):
757 '''Usage: export class[,class] destination_dir
758 Export the database to tab-separated-value files.
760 This action exports the current data from the database into
761 tab-separated-value files that are placed in the nominated destination
762 directory. The journals are not exported.
763 '''
764 if len(args) < 2:
765 raise UsageError, _('Not enough arguments supplied')
766 classes = args[0].split(',')
767 dir = args[1]
769 # use the csv parser if we can - it's faster
770 if csv is not None:
771 p = csv.parser(field_sep=':')
773 # do all the classes specified
774 for classname in classes:
775 cl = self.get_class(classname)
776 f = open(os.path.join(dir, classname+'.csv'), 'w')
777 f.write(':'.join(cl.properties.keys()) + '\n')
779 # all nodes for this class
780 properties = cl.properties.items()
781 for nodeid in cl.list():
782 l = []
783 for prop, proptype in properties:
784 value = cl.get(nodeid, prop)
785 # convert data where needed
786 if isinstance(proptype, hyperdb.Date):
787 value = value.get_tuple()
788 elif isinstance(proptype, hyperdb.Interval):
789 value = value.get_tuple()
790 elif isinstance(proptype, hyperdb.Password):
791 value = str(value)
792 l.append(repr(value))
794 # now write
795 if csv is not None:
796 f.write(p.join(l) + '\n')
797 else:
798 # escape the individual entries to they're valid CSV
799 m = []
800 for entry in l:
801 if '"' in entry:
802 entry = '""'.join(entry.split('"'))
803 if ':' in entry:
804 entry = '"%s"'%entry
805 m.append(entry)
806 f.write(':'.join(m) + '\n')
807 return 0
809 def do_import(self, args):
810 '''Usage: import class file
811 Import the contents of the tab-separated-value file.
813 The file must define the same properties as the class (including having
814 a "header" line with those property names.) The new nodes are added to
815 the existing database - if you want to create a new database using the
816 imported data, then create a new database (or, tediously, retire all
817 the old data.)
818 '''
819 if len(args) < 2:
820 raise UsageError, _('Not enough arguments supplied')
821 if csv is None:
822 raise UsageError, \
823 _('Sorry, you need the csv module to use this function.\n'
824 'Get it from: http://www.object-craft.com.au/projects/csv/')
826 from roundup import hyperdb
828 # ensure that the properties and the CSV file headings match
829 classname = args[0]
830 cl = self.get_class(classname)
831 f = open(args[1])
832 p = csv.parser(field_sep=':')
833 file_props = p.parse(f.readline())
834 props = cl.properties.keys()
835 m = file_props[:]
836 m.sort()
837 props.sort()
838 if m != props:
839 raise UsageError, _('Import file doesn\'t define the same '
840 'properties as "%(arg0)s".')%{'arg0': args[0]}
842 # loop through the file and create a node for each entry
843 n = range(len(props))
844 while 1:
845 line = f.readline()
846 if not line: break
848 # parse lines until we get a complete entry
849 while 1:
850 l = p.parse(line)
851 if l: break
852 line = f.readline()
853 if not line:
854 raise ValueError, "Unexpected EOF during CSV parse"
856 # make the new node's property map
857 d = {}
858 for i in n:
859 # Use eval to reverse the repr() used to output the CSV
860 value = eval(l[i])
861 # Figure the property for this column
862 key = file_props[i]
863 proptype = cl.properties[key]
864 # Convert for property type
865 if isinstance(proptype, hyperdb.Date):
866 value = date.Date(value)
867 elif isinstance(proptype, hyperdb.Interval):
868 value = date.Interval(value)
869 elif isinstance(proptype, hyperdb.Password):
870 pwd = password.Password()
871 pwd.unpack(value)
872 value = pwd
873 if value is not None:
874 d[key] = value
876 # and create the new node
877 apply(cl.create, (), d)
878 return 0
880 def do_pack(self, args):
881 '''Usage: pack period | date
883 Remove journal entries older than a period of time specified or
884 before a certain date.
886 A period is specified using the suffixes "y", "m", and "d". The
887 suffix "w" (for "week") means 7 days.
889 "3y" means three years
890 "2y 1m" means two years and one month
891 "1m 25d" means one month and 25 days
892 "2w 3d" means two weeks and three days
894 Date format is "YYYY-MM-DD" eg:
895 2001-01-01
897 '''
898 if len(args) <> 1:
899 raise UsageError, _('Not enough arguments supplied')
901 # are we dealing with a period or a date
902 value = args[0]
903 date_re = re.compile(r'''
904 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
905 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
906 ''', re.VERBOSE)
907 m = date_re.match(value)
908 if not m:
909 raise ValueError, _('Invalid format')
910 m = m.groupdict()
911 if m['period']:
912 # TODO: need to fix date module. one should be able to say
913 # pack_before = date.Date(". - %s"%value)
914 pack_before = date.Date(".") + date.Interval("- %s"%value)
915 elif m['date']:
916 pack_before = date.Date(value)
917 self.db.pack(pack_before)
918 return 0
920 def run_command(self, args):
921 '''Run a single command
922 '''
923 command = args[0]
925 # handle help now
926 if command == 'help':
927 if len(args)>1:
928 self.do_help(args[1:])
929 return 0
930 self.do_help(['help'])
931 return 0
932 if command == 'morehelp':
933 self.do_help(['help'])
934 self.help_commands()
935 self.help_all()
936 return 0
938 # figure what the command is
939 try:
940 functions = self.commands.get(command)
941 except KeyError:
942 # not a valid command
943 print _('Unknown command "%(command)s" ("help commands" for a '
944 'list)')%locals()
945 return 1
947 # check for multiple matches
948 if len(functions) > 1:
949 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
950 command, 'list': ', '.join([i[0] for i in functions])}
951 return 1
952 command, function = functions[0]
954 # make sure we have an instance_home
955 while not self.instance_home:
956 self.instance_home = raw_input(_('Enter instance home: ')).strip()
958 # before we open the db, we may be doing an init
959 if command == 'initialise':
960 try:
961 return self.do_initialise(self.instance_home, args)
962 except UsageError, message:
963 print _('Error: %(message)s')%locals()
964 return 1
966 # get the instance
967 try:
968 instance = roundup.instance.open(self.instance_home)
969 except ValueError, message:
970 self.instance_home = ''
971 print _("Error: Couldn't open instance: %(message)s")%locals()
972 return 1
974 # only open the database once!
975 if not self.db:
976 self.db = instance.open('admin')
978 # do the command
979 ret = 0
980 try:
981 ret = function(args[1:])
982 except UsageError, message:
983 print _('Error: %(message)s')%locals()
984 print
985 print function.__doc__
986 ret = 1
987 except:
988 import traceback
989 traceback.print_exc()
990 ret = 1
991 return ret
993 def interactive(self):
994 '''Run in an interactive mode
995 '''
996 print _('Roundup {version} ready for input.')
997 print _('Type "help" for help.')
998 try:
999 import readline
1000 except ImportError:
1001 print _('Note: command history and editing not available')
1003 while 1:
1004 try:
1005 command = raw_input(_('roundup> '))
1006 except EOFError:
1007 print _('exit...')
1008 break
1009 if not command: continue
1010 args = token.token_split(command)
1011 if not args: continue
1012 if args[0] in ('quit', 'exit'): break
1013 self.run_command(args)
1015 # exit.. check for transactions
1016 if self.db and self.db.transactions:
1017 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1018 if commit and commit[0].lower() == 'y':
1019 self.db.commit()
1020 return 0
1022 def main(self):
1023 try:
1024 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1025 except getopt.GetoptError, e:
1026 self.usage(str(e))
1027 return 1
1029 # handle command-line args
1030 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
1031 # TODO: reinstate the user/password stuff (-u arg too)
1032 name = password = ''
1033 if os.environ.has_key('ROUNDUP_LOGIN'):
1034 l = os.environ['ROUNDUP_LOGIN'].split(':')
1035 name = l[0]
1036 if len(l) > 1:
1037 password = l[1]
1038 self.comma_sep = 0
1039 for opt, arg in opts:
1040 if opt == '-h':
1041 self.usage()
1042 return 0
1043 if opt == '-i':
1044 self.instance_home = arg
1045 if opt == '-c':
1046 self.comma_sep = 1
1048 # if no command - go interactive
1049 ret = 0
1050 if not args:
1051 self.interactive()
1052 else:
1053 ret = self.run_command(args)
1054 if self.db: self.db.commit()
1055 return ret
1058 if __name__ == '__main__':
1059 tool = AdminTool()
1060 sys.exit(tool.main())
1062 #
1063 # $Log: not supported by cvs2svn $
1064 # Revision 1.9 2002/03/12 22:51:47 richard
1065 # . #527416 ] roundup-admin uses undefined value
1066 # . #527503 ] unfriendly init blowup when parent dir
1067 # (also handles UsageError correctly now in init)
1068 #
1069 # Revision 1.8 2002/02/27 03:28:21 richard
1070 # Ran it through pychecker, made fixes
1071 #
1072 # Revision 1.7 2002/02/20 05:04:32 richard
1073 # Wasn't handling the cvs parser feeding properly.
1074 #
1075 # Revision 1.6 2002/01/23 07:27:19 grubert
1076 # . allow abbreviation of "help" in admin tool too.
1077 #
1078 # Revision 1.5 2002/01/21 16:33:19 rochecompaan
1079 # You can now use the roundup-admin tool to pack the database
1080 #
1081 # Revision 1.4 2002/01/14 06:51:09 richard
1082 # . #503164 ] create and passwords
1083 #
1084 # Revision 1.3 2002/01/08 05:26:32 rochecompaan
1085 # Missing "self" in props_from_args
1086 #
1087 # Revision 1.2 2002/01/07 10:41:44 richard
1088 # #500140 ] AdminTool.get_class() returns nothing
1089 #
1090 # Revision 1.1 2002/01/05 02:11:22 richard
1091 # I18N'ed roundup admin - and split the code off into a module so it can be used
1092 # elsewhere.
1093 # Big issue with this is the doc strings - that's the help. We're probably going to
1094 # have to switch to not use docstrings, which will suck a little :(
1095 #
1096 #
1097 #
1098 # vim: set filetype=python ts=4 sw=4 et si