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.11 2002-05-23 01:14:20 richard Exp $
21 import sys, os, getpass, getopt, re, UserDict, shlex, shutil
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_install(self, instance_home, args):
253 '''Usage: install [template [backend [admin password]]]
254 Install 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 The initialise command must be called after this command in order
262 to initialise the instance's database. You may edit the instance's
263 initial database contents before running that command by editing
264 the instance's dbinit.py module init() function.
266 See also initopts help.
267 '''
268 if len(args) < 1:
269 raise UsageError, _('Not enough arguments supplied')
271 # make sure the instance home can be created
272 parent = os.path.split(instance_home)[0]
273 if not os.path.exists(parent):
274 raise UsageError, _('Instance home parent directory "%(parent)s"'
275 ' does not exist')%locals()
277 # select template
278 import roundup.templates
279 templates = roundup.templates.listTemplates()
280 template = len(args) > 1 and args[1] or ''
281 if template not in templates:
282 print _('Templates:'), ', '.join(templates)
283 while template not in templates:
284 template = raw_input(_('Select template [classic]: ')).strip()
285 if not template:
286 template = 'classic'
288 # select hyperdb backend
289 import roundup.backends
290 backends = roundup.backends.__all__
291 backend = len(args) > 2 and args[2] or ''
292 if backend not in backends:
293 print _('Back ends:'), ', '.join(backends)
294 while backend not in backends:
295 backend = raw_input(_('Select backend [anydbm]: ')).strip()
296 if not backend:
297 backend = 'anydbm'
299 # install!
300 init.install(instance_home, template, backend)
302 print _('''
303 You should now edit the instance configuration file:
304 %(instance_config_file)s
305 ... at a minimum, you must set MAILHOST, MAIL_DOMAIN and ADMIN_EMAIL.
307 If you wish to modify the default schema, you should also edit the database
308 initialisation file:
309 %(database_config_file)s
310 ... see the documentation on customizing for more information.
311 ''')%{
312 'instance_config_file': os.path.join(instance_home, 'instance_config.py'),
313 'database_config_file': os.path.join(instance_home, 'dbinit.py')
314 }
315 return 0
318 def do_initialise(self, instance_home, args):
319 '''Usage: initialise [adminpw [adminemail]]
320 Initialise a new Roundup instance.
322 The administrator details will be set at this step.
324 Execute the instance's initialisation function dbinit.init()
325 '''
326 # password
327 if len(args) > 0:
328 adminpw = args[0]
329 else:
330 adminpw = ''
331 confirm = 'x'
332 while adminpw != confirm:
333 adminpw = getpass.getpass(_('Admin Password: '))
334 confirm = getpass.getpass(_(' Confirm: '))
336 # email
337 if len(args) > 1:
338 adminemail = args[1]
339 else:
340 adminemail = ''
341 while not adminemail:
342 adminemail = raw_input(_(' Admin Email: ')).strip()
344 # make sure the instance home is installed
345 if not os.path.exists(instance_home):
346 raise UsageError, _('Instance home does not exist')%locals()
347 if not os.path.exists(os.path.join(instance_home, 'html')):
348 raise UsageError, _('Instance has not been installed')%locals()
350 # is there already a database?
351 if os.path.exists(os.path.join(instance_home, 'db')):
352 print _('WARNING: The database is already initialised!')
353 print _('If you re-initialise it, you will lose all the data!')
354 ok = raw_input(_('Erase it? Y/[N]: ')).strip()
355 if ok.lower() != 'y':
356 return 0
358 # nuke it
359 shutil.rmtree(os.path.join(instance_home, 'db'))
361 # GO
362 init.initialise(instance_home, adminpw)
364 return 0
367 def do_get(self, args):
368 '''Usage: get property designator[,designator]*
369 Get the given property of one or more designator(s).
371 Retrieves the property value of the nodes specified by the designators.
372 '''
373 if len(args) < 2:
374 raise UsageError, _('Not enough arguments supplied')
375 propname = args[0]
376 designators = args[1].split(',')
377 l = []
378 for designator in designators:
379 # decode the node designator
380 try:
381 classname, nodeid = roundupdb.splitDesignator(designator)
382 except roundupdb.DesignatorError, message:
383 raise UsageError, message
385 # get the class
386 cl = self.get_class(classname)
387 try:
388 if self.comma_sep:
389 l.append(cl.get(nodeid, propname))
390 else:
391 print cl.get(nodeid, propname)
392 except IndexError:
393 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
394 except KeyError:
395 raise UsageError, _('no such %(classname)s property '
396 '"%(propname)s"')%locals()
397 if self.comma_sep:
398 print ','.join(l)
399 return 0
402 def do_set(self, args):
403 '''Usage: set designator[,designator]* propname=value ...
404 Set the given property of one or more designator(s).
406 Sets the property to the value for all designators given.
407 '''
408 if len(args) < 2:
409 raise UsageError, _('Not enough arguments supplied')
410 from roundup import hyperdb
412 designators = args[0].split(',')
414 # get the props from the args
415 props = self.props_from_args(args[1:])
417 # now do the set for all the nodes
418 for designator in designators:
419 # decode the node designator
420 try:
421 classname, nodeid = roundupdb.splitDesignator(designator)
422 except roundupdb.DesignatorError, message:
423 raise UsageError, message
425 # get the class
426 cl = self.get_class(classname)
428 properties = cl.getprops()
429 for key, value in props.items():
430 proptype = properties[key]
431 if isinstance(proptype, hyperdb.String):
432 continue
433 elif isinstance(proptype, hyperdb.Password):
434 props[key] = password.Password(value)
435 elif isinstance(proptype, hyperdb.Date):
436 try:
437 props[key] = date.Date(value)
438 except ValueError, message:
439 raise UsageError, '"%s": %s'%(value, message)
440 elif isinstance(proptype, hyperdb.Interval):
441 try:
442 props[key] = date.Interval(value)
443 except ValueError, message:
444 raise UsageError, '"%s": %s'%(value, message)
445 elif isinstance(proptype, hyperdb.Link):
446 props[key] = value
447 elif isinstance(proptype, hyperdb.Multilink):
448 props[key] = value.split(',')
450 # try the set
451 try:
452 apply(cl.set, (nodeid, ), props)
453 except (TypeError, IndexError, ValueError), message:
454 raise UsageError, message
455 return 0
457 def do_find(self, args):
458 '''Usage: find classname propname=value ...
459 Find the nodes of the given class with a given link property value.
461 Find the nodes of the given class with a given link property value. The
462 value may be either the nodeid of the linked node, or its key value.
463 '''
464 if len(args) < 1:
465 raise UsageError, _('Not enough arguments supplied')
466 classname = args[0]
467 # get the class
468 cl = self.get_class(classname)
470 # handle the propname=value argument
471 props = self.props_from_args(args[1:])
473 # if the value isn't a number, look up the linked class to get the
474 # number
475 for propname, value in props.items():
476 num_re = re.compile('^\d+$')
477 if not num_re.match(value):
478 # get the property
479 try:
480 property = cl.properties[propname]
481 except KeyError:
482 raise UsageError, _('%(classname)s has no property '
483 '"%(propname)s"')%locals()
485 # make sure it's a link
486 if (not isinstance(property, hyperdb.Link) and not
487 isinstance(property, hyperdb.Multilink)):
488 raise UsageError, _('You may only "find" link properties')
490 # get the linked-to class and look up the key property
491 link_class = self.db.getclass(property.classname)
492 try:
493 props[propname] = link_class.lookup(value)
494 except TypeError:
495 raise UsageError, _('%(classname)s has no key property"')%{
496 'classname': link_class.classname}
497 except KeyError:
498 raise UsageError, _('%(classname)s has no entry "%(propname)s"')%{
499 'classname': link_class.classname, 'propname': propname}
501 # now do the find
502 try:
503 if self.comma_sep:
504 print ','.join(apply(cl.find, (), props))
505 else:
506 print apply(cl.find, (), props)
507 except KeyError:
508 raise UsageError, _('%(classname)s has no property '
509 '"%(propname)s"')%locals()
510 except (ValueError, TypeError), message:
511 raise UsageError, message
512 return 0
514 def do_specification(self, args):
515 '''Usage: specification classname
516 Show the properties for a classname.
518 This lists the properties for a given class.
519 '''
520 if len(args) < 1:
521 raise UsageError, _('Not enough arguments supplied')
522 classname = args[0]
523 # get the class
524 cl = self.get_class(classname)
526 # get the key property
527 keyprop = cl.getkey()
528 for key, value in cl.properties.items():
529 if keyprop == key:
530 print _('%(key)s: %(value)s (key property)')%locals()
531 else:
532 print _('%(key)s: %(value)s')%locals()
534 def do_display(self, args):
535 '''Usage: display designator
536 Show the property values for the given node.
538 This lists the properties and their associated values for the given
539 node.
540 '''
541 if len(args) < 1:
542 raise UsageError, _('Not enough arguments supplied')
544 # decode the node designator
545 try:
546 classname, nodeid = roundupdb.splitDesignator(args[0])
547 except roundupdb.DesignatorError, message:
548 raise UsageError, message
550 # get the class
551 cl = self.get_class(classname)
553 # display the values
554 for key in cl.properties.keys():
555 value = cl.get(nodeid, key)
556 print _('%(key)s: %(value)s')%locals()
558 def do_create(self, args):
559 '''Usage: create classname property=value ...
560 Create a new entry of a given class.
562 This creates a new entry of the given class using the property
563 name=value arguments provided on the command line after the "create"
564 command.
565 '''
566 if len(args) < 1:
567 raise UsageError, _('Not enough arguments supplied')
568 from roundup import hyperdb
570 classname = args[0]
572 # get the class
573 cl = self.get_class(classname)
575 # now do a create
576 props = {}
577 properties = cl.getprops(protected = 0)
578 if len(args) == 1:
579 # ask for the properties
580 for key, value in properties.items():
581 if key == 'id': continue
582 name = value.__class__.__name__
583 if isinstance(value , hyperdb.Password):
584 again = None
585 while value != again:
586 value = getpass.getpass(_('%(propname)s (Password): ')%{
587 'propname': key.capitalize()})
588 again = getpass.getpass(_(' %(propname)s (Again): ')%{
589 'propname': key.capitalize()})
590 if value != again: print _('Sorry, try again...')
591 if value:
592 props[key] = value
593 else:
594 value = raw_input(_('%(propname)s (%(proptype)s): ')%{
595 'propname': key.capitalize(), 'proptype': name})
596 if value:
597 props[key] = value
598 else:
599 props = self.props_from_args(args[1:])
601 # convert types
602 for propname, value in props.items():
603 # get the property
604 try:
605 proptype = properties[propname]
606 except KeyError:
607 raise UsageError, _('%(classname)s has no property '
608 '"%(propname)s"')%locals()
610 if isinstance(proptype, hyperdb.Date):
611 try:
612 props[propname] = date.Date(value)
613 except ValueError, message:
614 raise UsageError, _('"%(value)s": %(message)s')%locals()
615 elif isinstance(proptype, hyperdb.Interval):
616 try:
617 props[propname] = date.Interval(value)
618 except ValueError, message:
619 raise UsageError, _('"%(value)s": %(message)s')%locals()
620 elif isinstance(proptype, hyperdb.Password):
621 props[propname] = password.Password(value)
622 elif isinstance(proptype, hyperdb.Multilink):
623 props[propname] = value.split(',')
625 # check for the key property
626 propname = cl.getkey()
627 if propname and not props.has_key(propname):
628 raise UsageError, _('you must provide the "%(propname)s" '
629 'property.')%locals()
631 # do the actual create
632 try:
633 print apply(cl.create, (), props)
634 except (TypeError, IndexError, ValueError), message:
635 raise UsageError, message
636 return 0
638 def do_list(self, args):
639 '''Usage: list classname [property]
640 List the instances of a class.
642 Lists all instances of the given class. If the property is not
643 specified, the "label" property is used. The label property is tried
644 in order: the key, "name", "title" and then the first property,
645 alphabetically.
646 '''
647 if len(args) < 1:
648 raise UsageError, _('Not enough arguments supplied')
649 classname = args[0]
651 # get the class
652 cl = self.get_class(classname)
654 # figure the property
655 if len(args) > 1:
656 propname = args[1]
657 else:
658 propname = cl.labelprop()
660 if self.comma_sep:
661 print ','.join(cl.list())
662 else:
663 for nodeid in cl.list():
664 try:
665 value = cl.get(nodeid, propname)
666 except KeyError:
667 raise UsageError, _('%(classname)s has no property '
668 '"%(propname)s"')%locals()
669 print _('%(nodeid)4s: %(value)s')%locals()
670 return 0
672 def do_table(self, args):
673 '''Usage: table classname [property[,property]*]
674 List the instances of a class in tabular form.
676 Lists all instances of the given class. If the properties are not
677 specified, all properties are displayed. By default, the column widths
678 are the width of the property names. The width may be explicitly defined
679 by defining the property as "name:width". For example::
680 roundup> table priority id,name:10
681 Id Name
682 1 fatal-bug
683 2 bug
684 3 usability
685 4 feature
686 '''
687 if len(args) < 1:
688 raise UsageError, _('Not enough arguments supplied')
689 classname = args[0]
691 # get the class
692 cl = self.get_class(classname)
694 # figure the property names to display
695 if len(args) > 1:
696 prop_names = args[1].split(',')
697 all_props = cl.getprops()
698 for spec in prop_names:
699 if ':' in spec:
700 try:
701 propname, width = spec.split(':')
702 except (ValueError, TypeError):
703 raise UsageError, _('"%(spec)s" not name:width')%locals()
704 else:
705 propname = spec
706 if not all_props.has_key(propname):
707 raise UsageError, _('%(classname)s has no property '
708 '"%(propname)s"')%locals()
709 else:
710 prop_names = cl.getprops().keys()
712 # now figure column widths
713 props = []
714 for spec in prop_names:
715 if ':' in spec:
716 name, width = spec.split(':')
717 props.append((name, int(width)))
718 else:
719 props.append((spec, len(spec)))
721 # now display the heading
722 print ' '.join([name.capitalize().ljust(width) for name,width in props])
724 # and the table data
725 for nodeid in cl.list():
726 l = []
727 for name, width in props:
728 if name != 'id':
729 try:
730 value = str(cl.get(nodeid, name))
731 except KeyError:
732 # we already checked if the property is valid - a
733 # KeyError here means the node just doesn't have a
734 # value for it
735 value = ''
736 else:
737 value = str(nodeid)
738 f = '%%-%ds'%width
739 l.append(f%value[:width])
740 print ' '.join(l)
741 return 0
743 def do_history(self, args):
744 '''Usage: history designator
745 Show the history entries of a designator.
747 Lists the journal entries for the node identified by the designator.
748 '''
749 if len(args) < 1:
750 raise UsageError, _('Not enough arguments supplied')
751 try:
752 classname, nodeid = roundupdb.splitDesignator(args[0])
753 except roundupdb.DesignatorError, message:
754 raise UsageError, message
756 try:
757 print self.db.getclass(classname).history(nodeid)
758 except KeyError:
759 raise UsageError, _('no such class "%(classname)s"')%locals()
760 except IndexError:
761 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
762 return 0
764 def do_commit(self, args):
765 '''Usage: commit
766 Commit all changes made to the database.
768 The changes made during an interactive session are not
769 automatically written to the database - they must be committed
770 using this command.
772 One-off commands on the command-line are automatically committed if
773 they are successful.
774 '''
775 self.db.commit()
776 return 0
778 def do_rollback(self, args):
779 '''Usage: rollback
780 Undo all changes that are pending commit to the database.
782 The changes made during an interactive session are not
783 automatically written to the database - they must be committed
784 manually. This command undoes all those changes, so a commit
785 immediately after would make no changes to the database.
786 '''
787 self.db.rollback()
788 return 0
790 def do_retire(self, args):
791 '''Usage: retire designator[,designator]*
792 Retire the node specified by designator.
794 This action indicates that a particular node is not to be retrieved by
795 the list or find commands, and its key value may be re-used.
796 '''
797 if len(args) < 1:
798 raise UsageError, _('Not enough arguments supplied')
799 designators = args[0].split(',')
800 for designator in designators:
801 try:
802 classname, nodeid = roundupdb.splitDesignator(designator)
803 except roundupdb.DesignatorError, message:
804 raise UsageError, message
805 try:
806 self.db.getclass(classname).retire(nodeid)
807 except KeyError:
808 raise UsageError, _('no such class "%(classname)s"')%locals()
809 except IndexError:
810 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
811 return 0
813 def do_export(self, args):
814 '''Usage: export class[,class] destination_dir
815 Export the database to tab-separated-value files.
817 This action exports the current data from the database into
818 tab-separated-value files that are placed in the nominated destination
819 directory. The journals are not exported.
820 '''
821 if len(args) < 2:
822 raise UsageError, _('Not enough arguments supplied')
823 classes = args[0].split(',')
824 dir = args[1]
826 # use the csv parser if we can - it's faster
827 if csv is not None:
828 p = csv.parser(field_sep=':')
830 # do all the classes specified
831 for classname in classes:
832 cl = self.get_class(classname)
833 f = open(os.path.join(dir, classname+'.csv'), 'w')
834 f.write(':'.join(cl.properties.keys()) + '\n')
836 # all nodes for this class
837 properties = cl.properties.items()
838 for nodeid in cl.list():
839 l = []
840 for prop, proptype in properties:
841 value = cl.get(nodeid, prop)
842 # convert data where needed
843 if isinstance(proptype, hyperdb.Date):
844 value = value.get_tuple()
845 elif isinstance(proptype, hyperdb.Interval):
846 value = value.get_tuple()
847 elif isinstance(proptype, hyperdb.Password):
848 value = str(value)
849 l.append(repr(value))
851 # now write
852 if csv is not None:
853 f.write(p.join(l) + '\n')
854 else:
855 # escape the individual entries to they're valid CSV
856 m = []
857 for entry in l:
858 if '"' in entry:
859 entry = '""'.join(entry.split('"'))
860 if ':' in entry:
861 entry = '"%s"'%entry
862 m.append(entry)
863 f.write(':'.join(m) + '\n')
864 return 0
866 def do_import(self, args):
867 '''Usage: import class file
868 Import the contents of the tab-separated-value file.
870 The file must define the same properties as the class (including having
871 a "header" line with those property names.) The new nodes are added to
872 the existing database - if you want to create a new database using the
873 imported data, then create a new database (or, tediously, retire all
874 the old data.)
875 '''
876 if len(args) < 2:
877 raise UsageError, _('Not enough arguments supplied')
878 if csv is None:
879 raise UsageError, \
880 _('Sorry, you need the csv module to use this function.\n'
881 'Get it from: http://www.object-craft.com.au/projects/csv/')
883 from roundup import hyperdb
885 # ensure that the properties and the CSV file headings match
886 classname = args[0]
887 cl = self.get_class(classname)
888 f = open(args[1])
889 p = csv.parser(field_sep=':')
890 file_props = p.parse(f.readline())
891 props = cl.properties.keys()
892 m = file_props[:]
893 m.sort()
894 props.sort()
895 if m != props:
896 raise UsageError, _('Import file doesn\'t define the same '
897 'properties as "%(arg0)s".')%{'arg0': args[0]}
899 # loop through the file and create a node for each entry
900 n = range(len(props))
901 while 1:
902 line = f.readline()
903 if not line: break
905 # parse lines until we get a complete entry
906 while 1:
907 l = p.parse(line)
908 if l: break
909 line = f.readline()
910 if not line:
911 raise ValueError, "Unexpected EOF during CSV parse"
913 # make the new node's property map
914 d = {}
915 for i in n:
916 # Use eval to reverse the repr() used to output the CSV
917 value = eval(l[i])
918 # Figure the property for this column
919 key = file_props[i]
920 proptype = cl.properties[key]
921 # Convert for property type
922 if isinstance(proptype, hyperdb.Date):
923 value = date.Date(value)
924 elif isinstance(proptype, hyperdb.Interval):
925 value = date.Interval(value)
926 elif isinstance(proptype, hyperdb.Password):
927 pwd = password.Password()
928 pwd.unpack(value)
929 value = pwd
930 if value is not None:
931 d[key] = value
933 # and create the new node
934 apply(cl.create, (), d)
935 return 0
937 def do_pack(self, args):
938 '''Usage: pack period | date
940 Remove journal entries older than a period of time specified or
941 before a certain date.
943 A period is specified using the suffixes "y", "m", and "d". The
944 suffix "w" (for "week") means 7 days.
946 "3y" means three years
947 "2y 1m" means two years and one month
948 "1m 25d" means one month and 25 days
949 "2w 3d" means two weeks and three days
951 Date format is "YYYY-MM-DD" eg:
952 2001-01-01
954 '''
955 if len(args) <> 1:
956 raise UsageError, _('Not enough arguments supplied')
958 # are we dealing with a period or a date
959 value = args[0]
960 date_re = re.compile(r'''
961 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
962 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
963 ''', re.VERBOSE)
964 m = date_re.match(value)
965 if not m:
966 raise ValueError, _('Invalid format')
967 m = m.groupdict()
968 if m['period']:
969 # TODO: need to fix date module. one should be able to say
970 # pack_before = date.Date(". - %s"%value)
971 pack_before = date.Date(".") + date.Interval("- %s"%value)
972 elif m['date']:
973 pack_before = date.Date(value)
974 self.db.pack(pack_before)
975 return 0
977 def run_command(self, args):
978 '''Run a single command
979 '''
980 command = args[0]
982 # handle help now
983 if command == 'help':
984 if len(args)>1:
985 self.do_help(args[1:])
986 return 0
987 self.do_help(['help'])
988 return 0
989 if command == 'morehelp':
990 self.do_help(['help'])
991 self.help_commands()
992 self.help_all()
993 return 0
995 # figure what the command is
996 try:
997 functions = self.commands.get(command)
998 except KeyError:
999 # not a valid command
1000 print _('Unknown command "%(command)s" ("help commands" for a '
1001 'list)')%locals()
1002 return 1
1004 # check for multiple matches
1005 if len(functions) > 1:
1006 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1007 command, 'list': ', '.join([i[0] for i in functions])}
1008 return 1
1009 command, function = functions[0]
1011 # make sure we have an instance_home
1012 while not self.instance_home:
1013 self.instance_home = raw_input(_('Enter instance home: ')).strip()
1015 # before we open the db, we may be doing an install or init
1016 if command == 'initialise':
1017 try:
1018 return self.do_initialise(self.instance_home, args)
1019 except UsageError, message:
1020 print _('Error: %(message)s')%locals()
1021 return 1
1022 elif command == 'install':
1023 try:
1024 return self.do_install(self.instance_home, args)
1025 except UsageError, message:
1026 print _('Error: %(message)s')%locals()
1027 return 1
1029 # get the instance
1030 try:
1031 instance = roundup.instance.open(self.instance_home)
1032 except ValueError, message:
1033 self.instance_home = ''
1034 print _("Error: Couldn't open instance: %(message)s")%locals()
1035 return 1
1037 # only open the database once!
1038 if not self.db:
1039 self.db = instance.open('admin')
1041 # do the command
1042 ret = 0
1043 try:
1044 ret = function(args[1:])
1045 except UsageError, message:
1046 print _('Error: %(message)s')%locals()
1047 print
1048 print function.__doc__
1049 ret = 1
1050 except:
1051 import traceback
1052 traceback.print_exc()
1053 ret = 1
1054 return ret
1056 def interactive(self):
1057 '''Run in an interactive mode
1058 '''
1059 print _('Roundup {version} ready for input.')
1060 print _('Type "help" for help.')
1061 try:
1062 import readline
1063 except ImportError:
1064 print _('Note: command history and editing not available')
1066 while 1:
1067 try:
1068 command = raw_input(_('roundup> '))
1069 except EOFError:
1070 print _('exit...')
1071 break
1072 if not command: continue
1073 args = token.token_split(command)
1074 if not args: continue
1075 if args[0] in ('quit', 'exit'): break
1076 self.run_command(args)
1078 # exit.. check for transactions
1079 if self.db and self.db.transactions:
1080 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1081 if commit and commit[0].lower() == 'y':
1082 self.db.commit()
1083 return 0
1085 def main(self):
1086 try:
1087 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1088 except getopt.GetoptError, e:
1089 self.usage(str(e))
1090 return 1
1092 # handle command-line args
1093 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
1094 # TODO: reinstate the user/password stuff (-u arg too)
1095 name = password = ''
1096 if os.environ.has_key('ROUNDUP_LOGIN'):
1097 l = os.environ['ROUNDUP_LOGIN'].split(':')
1098 name = l[0]
1099 if len(l) > 1:
1100 password = l[1]
1101 self.comma_sep = 0
1102 for opt, arg in opts:
1103 if opt == '-h':
1104 self.usage()
1105 return 0
1106 if opt == '-i':
1107 self.instance_home = arg
1108 if opt == '-c':
1109 self.comma_sep = 1
1111 # if no command - go interactive
1112 ret = 0
1113 if not args:
1114 self.interactive()
1115 else:
1116 ret = self.run_command(args)
1117 if self.db: self.db.commit()
1118 return ret
1121 if __name__ == '__main__':
1122 tool = AdminTool()
1123 sys.exit(tool.main())
1125 #
1126 # $Log: not supported by cvs2svn $
1127 # Revision 1.10 2002/04/27 10:07:23 richard
1128 # minor fix to error message
1129 #
1130 # Revision 1.9 2002/03/12 22:51:47 richard
1131 # . #527416 ] roundup-admin uses undefined value
1132 # . #527503 ] unfriendly init blowup when parent dir
1133 # (also handles UsageError correctly now in init)
1134 #
1135 # Revision 1.8 2002/02/27 03:28:21 richard
1136 # Ran it through pychecker, made fixes
1137 #
1138 # Revision 1.7 2002/02/20 05:04:32 richard
1139 # Wasn't handling the cvs parser feeding properly.
1140 #
1141 # Revision 1.6 2002/01/23 07:27:19 grubert
1142 # . allow abbreviation of "help" in admin tool too.
1143 #
1144 # Revision 1.5 2002/01/21 16:33:19 rochecompaan
1145 # You can now use the roundup-admin tool to pack the database
1146 #
1147 # Revision 1.4 2002/01/14 06:51:09 richard
1148 # . #503164 ] create and passwords
1149 #
1150 # Revision 1.3 2002/01/08 05:26:32 rochecompaan
1151 # Missing "self" in props_from_args
1152 #
1153 # Revision 1.2 2002/01/07 10:41:44 richard
1154 # #500140 ] AdminTool.get_class() returns nothing
1155 #
1156 # Revision 1.1 2002/01/05 02:11:22 richard
1157 # I18N'ed roundup admin - and split the code off into a module so it can be used
1158 # elsewhere.
1159 # Big issue with this is the doc strings - that's the help. We're probably going to
1160 # have to switch to not use docstrings, which will suck a little :(
1161 #
1162 #
1163 #
1164 # vim: set filetype=python ts=4 sw=4 et si