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.19 2002-07-25 07:14:05 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 from roundup import __version__ as roundup_version
28 import roundup.instance
29 from roundup.i18n import _
31 class CommandDict(UserDict.UserDict):
32 '''Simple dictionary that lets us do lookups using partial keys.
34 Original code submitted by Engelbert Gruber.
35 '''
36 _marker = []
37 def get(self, key, default=_marker):
38 if self.data.has_key(key):
39 return [(key, self.data[key])]
40 keylist = self.data.keys()
41 keylist.sort()
42 l = []
43 for ki in keylist:
44 if ki.startswith(key):
45 l.append((ki, self.data[ki]))
46 if not l and default is self._marker:
47 raise KeyError, key
48 return l
50 class UsageError(ValueError):
51 pass
53 class AdminTool:
55 def __init__(self):
56 self.commands = CommandDict()
57 for k in AdminTool.__dict__.keys():
58 if k[:3] == 'do_':
59 self.commands[k[3:]] = getattr(self, k)
60 self.help = {}
61 for k in AdminTool.__dict__.keys():
62 if k[:5] == 'help_':
63 self.help[k[5:]] = getattr(self, k)
64 self.instance_home = ''
65 self.db = None
67 def get_class(self, classname):
68 '''Get the class - raise an exception if it doesn't exist.
69 '''
70 try:
71 return self.db.getclass(classname)
72 except KeyError:
73 raise UsageError, _('no such class "%(classname)s"')%locals()
75 def props_from_args(self, args):
76 props = {}
77 for arg in args:
78 if arg.find('=') == -1:
79 raise UsageError, _('argument "%(arg)s" not propname=value')%locals()
80 try:
81 key, value = arg.split('=')
82 except ValueError:
83 raise UsageError, _('argument "%(arg)s" not propname=value')%locals()
84 props[key] = value
85 return props
87 def usage(self, message=''):
88 if message:
89 message = _('Problem: %(message)s)\n\n')%locals()
90 print _('''%(message)sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments>
92 Help:
93 roundup-admin -h
94 roundup-admin help -- this help
95 roundup-admin help <command> -- command-specific help
96 roundup-admin help all -- all available help
97 Options:
98 -i instance home -- specify the issue tracker "home directory" to administer
99 -u -- the user[:password] to use for commands
100 -c -- when outputting lists of data, just comma-separate them''')%locals()
101 self.help_commands()
103 def help_commands(self):
104 print _('Commands:'),
105 commands = ['']
106 for command in self.commands.values():
107 h = command.__doc__.split('\n')[0]
108 commands.append(' '+h[7:])
109 commands.sort()
110 commands.append(_('Commands may be abbreviated as long as the abbreviation matches only one'))
111 commands.append(_('command, e.g. l == li == lis == list.'))
112 print '\n'.join(commands)
113 print
115 def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
116 commands = self.commands.values()
117 def sortfun(a, b):
118 return cmp(a.__name__, b.__name__)
119 commands.sort(sortfun)
120 for command in commands:
121 h = command.__doc__.split('\n')
122 name = command.__name__[3:]
123 usage = h[0]
124 print _('''
125 <tr><td valign=top><strong>%(name)s</strong></td>
126 <td><tt>%(usage)s</tt><p>
127 <pre>''')%locals()
128 indent = indent_re.match(h[3])
129 if indent: indent = len(indent.group(1))
130 for line in h[3:]:
131 if indent:
132 print line[indent:]
133 else:
134 print line
135 print _('</pre></td></tr>\n')
137 def help_all(self):
138 print _('''
139 All commands (except help) require an instance specifier. This is just the path
140 to the roundup instance you're working with. A roundup instance is where
141 roundup keeps the database and configuration file that defines an issue
142 tracker. It may be thought of as the issue tracker's "home directory". It may
143 be specified in the environment variable ROUNDUP_INSTANCE or on the command
144 line as "-i instance".
146 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
148 Property values are represented as strings in command arguments and in the
149 printed results:
150 . Strings are, well, strings.
151 . Date values are printed in the full date format in the local time zone, and
152 accepted in the full format or any of the partial formats explained below.
153 . Link values are printed as node designators. When given as an argument,
154 node designators and key strings are both accepted.
155 . Multilink values are printed as lists of node designators joined by commas.
156 When given as an argument, node designators and key strings are both
157 accepted; an empty string, a single node, or a list of nodes joined by
158 commas is accepted.
160 When property values must contain spaces, just surround the value with
161 quotes, either ' or ". A single space may also be backslash-quoted. If a
162 valuu must contain a quote character, it must be backslash-quoted or inside
163 quotes. Examples:
164 hello world (2 tokens: hello, world)
165 "hello world" (1 token: hello world)
166 "Roch'e" Compaan (2 tokens: Roch'e Compaan)
167 Roch\'e Compaan (2 tokens: Roch'e Compaan)
168 address="1 2 3" (1 token: address=1 2 3)
169 \\ (1 token: \)
170 \n\r\t (1 token: a newline, carriage-return and tab)
172 When multiple nodes are specified to the roundup get or roundup set
173 commands, the specified properties are retrieved or set on all the listed
174 nodes.
176 When multiple results are returned by the roundup get or roundup find
177 commands, they are printed one per line (default) or joined by commas (with
178 the -c) option.
180 Where the command changes data, a login name/password is required. The
181 login may be specified as either "name" or "name:password".
182 . ROUNDUP_LOGIN environment variable
183 . the -u command-line option
184 If either the name or password is not supplied, they are obtained from the
185 command-line.
187 Date format examples:
188 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
189 "2000-04-17" means <Date 2000-04-17.00:00:00>
190 "01-25" means <Date yyyy-01-25.00:00:00>
191 "08-13.22:13" means <Date yyyy-08-14.03:13:00>
192 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
193 "14:25" means <Date yyyy-mm-dd.19:25:00>
194 "8:47:11" means <Date yyyy-mm-dd.13:47:11>
195 "." means "right now"
197 Command help:
198 ''')
199 for name, command in self.commands.items():
200 print _('%s:')%name
201 print _(' '), command.__doc__
203 def do_help(self, args, nl_re=re.compile('[\r\n]'),
204 indent_re=re.compile(r'^(\s+)\S+')):
205 '''Usage: help topic
206 Give help about topic.
208 commands -- list commands
209 <command> -- help specific to a command
210 initopts -- init command options
211 all -- all available help
212 '''
213 if len(args)>0:
214 topic = args[0]
215 else:
216 topic = 'help'
219 # try help_ methods
220 if self.help.has_key(topic):
221 self.help[topic]()
222 return 0
224 # try command docstrings
225 try:
226 l = self.commands.get(topic)
227 except KeyError:
228 print _('Sorry, no help for "%(topic)s"')%locals()
229 return 1
231 # display the help for each match, removing the docsring indent
232 for name, help in l:
233 lines = nl_re.split(help.__doc__)
234 print lines[0]
235 indent = indent_re.match(lines[1])
236 if indent: indent = len(indent.group(1))
237 for line in lines[1:]:
238 if indent:
239 print line[indent:]
240 else:
241 print line
242 return 0
244 def help_initopts(self):
245 import roundup.templates
246 templates = roundup.templates.listTemplates()
247 print _('Templates:'), ', '.join(templates)
248 import roundup.backends
249 backends = roundup.backends.__all__
250 print _('Back ends:'), ', '.join(backends)
253 def do_install(self, instance_home, args):
254 '''Usage: install [template [backend [admin password]]]
255 Install a new Roundup instance.
257 The command will prompt for the instance home directory (if not supplied
258 through INSTANCE_HOME or the -i option). The template, backend and admin
259 password may be specified on the command-line as arguments, in that
260 order.
262 The initialise command must be called after this command in order
263 to initialise the instance's database. You may edit the instance's
264 initial database contents before running that command by editing
265 the instance's dbinit.py module init() function.
267 See also initopts help.
268 '''
269 if len(args) < 1:
270 raise UsageError, _('Not enough arguments supplied')
272 # make sure the instance home can be created
273 parent = os.path.split(instance_home)[0]
274 if not os.path.exists(parent):
275 raise UsageError, _('Instance home parent directory "%(parent)s"'
276 ' does not exist')%locals()
278 # select template
279 import roundup.templates
280 templates = roundup.templates.listTemplates()
281 template = len(args) > 1 and args[1] or ''
282 if template not in templates:
283 print _('Templates:'), ', '.join(templates)
284 while template not in templates:
285 template = raw_input(_('Select template [classic]: ')).strip()
286 if not template:
287 template = 'classic'
289 # select hyperdb backend
290 import roundup.backends
291 backends = roundup.backends.__all__
292 backend = len(args) > 2 and args[2] or ''
293 if backend not in backends:
294 print _('Back ends:'), ', '.join(backends)
295 while backend not in backends:
296 backend = raw_input(_('Select backend [anydbm]: ')).strip()
297 if not backend:
298 backend = 'anydbm'
300 # install!
301 init.install(instance_home, template, backend)
303 print _('''
304 You should now edit the instance configuration file:
305 %(instance_config_file)s
306 ... at a minimum, you must set MAILHOST, MAIL_DOMAIN and ADMIN_EMAIL.
308 If you wish to modify the default schema, you should also edit the database
309 initialisation file:
310 %(database_config_file)s
311 ... see the documentation on customizing for more information.
312 ''')%{
313 'instance_config_file': os.path.join(instance_home, 'instance_config.py'),
314 'database_config_file': os.path.join(instance_home, 'dbinit.py')
315 }
316 return 0
319 def do_initialise(self, instance_home, args):
320 '''Usage: initialise [adminpw]
321 Initialise a new Roundup instance.
323 The administrator details will be set at this step.
325 Execute the instance's initialisation function dbinit.init()
326 '''
327 # password
328 if len(args) > 1:
329 adminpw = args[1]
330 else:
331 adminpw = ''
332 confirm = 'x'
333 while adminpw != confirm:
334 adminpw = getpass.getpass(_('Admin Password: '))
335 confirm = getpass.getpass(_(' Confirm: '))
337 # make sure the instance home is installed
338 if not os.path.exists(instance_home):
339 raise UsageError, _('Instance home does not exist')%locals()
340 if not os.path.exists(os.path.join(instance_home, 'html')):
341 raise UsageError, _('Instance has not been installed')%locals()
343 # is there already a database?
344 if os.path.exists(os.path.join(instance_home, 'db')):
345 print _('WARNING: The database is already initialised!')
346 print _('If you re-initialise it, you will lose all the data!')
347 ok = raw_input(_('Erase it? Y/[N]: ')).strip()
348 if ok.lower() != 'y':
349 return 0
351 # nuke it
352 shutil.rmtree(os.path.join(instance_home, 'db'))
354 # GO
355 init.initialise(instance_home, adminpw)
357 return 0
360 def do_get(self, args):
361 '''Usage: get property designator[,designator]*
362 Get the given property of one or more designator(s).
364 Retrieves the property value of the nodes specified by the designators.
365 '''
366 if len(args) < 2:
367 raise UsageError, _('Not enough arguments supplied')
368 propname = args[0]
369 designators = args[1].split(',')
370 l = []
371 for designator in designators:
372 # decode the node designator
373 try:
374 classname, nodeid = hyperdb.splitDesignator(designator)
375 except hyperdb.DesignatorError, message:
376 raise UsageError, message
378 # get the class
379 cl = self.get_class(classname)
380 try:
381 if self.comma_sep:
382 l.append(cl.get(nodeid, propname))
383 else:
384 print cl.get(nodeid, propname)
385 except IndexError:
386 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
387 except KeyError:
388 raise UsageError, _('no such %(classname)s property '
389 '"%(propname)s"')%locals()
390 if self.comma_sep:
391 print ','.join(l)
392 return 0
395 def do_set(self, args):
396 '''Usage: set designator[,designator]* propname=value ...
397 Set the given property of one or more designator(s).
399 Sets the property to the value for all designators given.
400 '''
401 if len(args) < 2:
402 raise UsageError, _('Not enough arguments supplied')
403 from roundup import hyperdb
405 designators = args[0].split(',')
407 # get the props from the args
408 props = self.props_from_args(args[1:])
410 # now do the set for all the nodes
411 for designator in designators:
412 # decode the node designator
413 try:
414 classname, nodeid = hyperdb.splitDesignator(designator)
415 except hyperdb.DesignatorError, message:
416 raise UsageError, message
418 # get the class
419 cl = self.get_class(classname)
421 properties = cl.getprops()
422 for key, value in props.items():
423 proptype = properties[key]
424 if isinstance(proptype, hyperdb.String):
425 continue
426 elif isinstance(proptype, hyperdb.Password):
427 props[key] = password.Password(value)
428 elif isinstance(proptype, hyperdb.Date):
429 try:
430 props[key] = date.Date(value)
431 except ValueError, message:
432 raise UsageError, '"%s": %s'%(value, message)
433 elif isinstance(proptype, hyperdb.Interval):
434 try:
435 props[key] = date.Interval(value)
436 except ValueError, message:
437 raise UsageError, '"%s": %s'%(value, message)
438 elif isinstance(proptype, hyperdb.Link):
439 props[key] = value
440 elif isinstance(proptype, hyperdb.Multilink):
441 props[key] = value.split(',')
442 elif isinstance(proptype, hyperdb.Boolean):
443 props[key] = value.lower() in ('yes', 'true', 'on', '1')
444 elif isinstance(proptype, hyperdb.Number):
445 props[key] = int(value)
447 # try the set
448 try:
449 apply(cl.set, (nodeid, ), props)
450 except (TypeError, IndexError, ValueError), message:
451 raise UsageError, message
452 return 0
454 def do_find(self, args):
455 '''Usage: find classname propname=value ...
456 Find the nodes of the given class with a given link property value.
458 Find the nodes of the given class with a given link property value. The
459 value may be either the nodeid of the linked node, or its key value.
460 '''
461 if len(args) < 1:
462 raise UsageError, _('Not enough arguments supplied')
463 classname = args[0]
464 # get the class
465 cl = self.get_class(classname)
467 # handle the propname=value argument
468 props = self.props_from_args(args[1:])
470 # if the value isn't a number, look up the linked class to get the
471 # number
472 for propname, value in props.items():
473 num_re = re.compile('^\d+$')
474 if not num_re.match(value):
475 # get the property
476 try:
477 property = cl.properties[propname]
478 except KeyError:
479 raise UsageError, _('%(classname)s has no property '
480 '"%(propname)s"')%locals()
482 # make sure it's a link
483 if (not isinstance(property, hyperdb.Link) and not
484 isinstance(property, hyperdb.Multilink)):
485 raise UsageError, _('You may only "find" link properties')
487 # get the linked-to class and look up the key property
488 link_class = self.db.getclass(property.classname)
489 try:
490 props[propname] = link_class.lookup(value)
491 except TypeError:
492 raise UsageError, _('%(classname)s has no key property"')%{
493 'classname': link_class.classname}
495 # now do the find
496 try:
497 if self.comma_sep:
498 print ','.join(apply(cl.find, (), props))
499 else:
500 print apply(cl.find, (), props)
501 except KeyError:
502 raise UsageError, _('%(classname)s has no property '
503 '"%(propname)s"')%locals()
504 except (ValueError, TypeError), message:
505 raise UsageError, message
506 return 0
508 def do_specification(self, args):
509 '''Usage: specification classname
510 Show the properties for a classname.
512 This lists the properties for a given class.
513 '''
514 if len(args) < 1:
515 raise UsageError, _('Not enough arguments supplied')
516 classname = args[0]
517 # get the class
518 cl = self.get_class(classname)
520 # get the key property
521 keyprop = cl.getkey()
522 for key, value in cl.properties.items():
523 if keyprop == key:
524 print _('%(key)s: %(value)s (key property)')%locals()
525 else:
526 print _('%(key)s: %(value)s')%locals()
528 def do_display(self, args):
529 '''Usage: display designator
530 Show the property values for the given node.
532 This lists the properties and their associated values for the given
533 node.
534 '''
535 if len(args) < 1:
536 raise UsageError, _('Not enough arguments supplied')
538 # decode the node designator
539 try:
540 classname, nodeid = hyperdb.splitDesignator(args[0])
541 except hyperdb.DesignatorError, message:
542 raise UsageError, message
544 # get the class
545 cl = self.get_class(classname)
547 # display the values
548 for key in cl.properties.keys():
549 value = cl.get(nodeid, key)
550 print _('%(key)s: %(value)s')%locals()
552 def do_create(self, args):
553 '''Usage: create classname property=value ...
554 Create a new entry of a given class.
556 This creates a new entry of the given class using the property
557 name=value arguments provided on the command line after the "create"
558 command.
559 '''
560 if len(args) < 1:
561 raise UsageError, _('Not enough arguments supplied')
562 from roundup import hyperdb
564 classname = args[0]
566 # get the class
567 cl = self.get_class(classname)
569 # now do a create
570 props = {}
571 properties = cl.getprops(protected = 0)
572 if len(args) == 1:
573 # ask for the properties
574 for key, value in properties.items():
575 if key == 'id': continue
576 name = value.__class__.__name__
577 if isinstance(value , hyperdb.Password):
578 again = None
579 while value != again:
580 value = getpass.getpass(_('%(propname)s (Password): ')%{
581 'propname': key.capitalize()})
582 again = getpass.getpass(_(' %(propname)s (Again): ')%{
583 'propname': key.capitalize()})
584 if value != again: print _('Sorry, try again...')
585 if value:
586 props[key] = value
587 else:
588 value = raw_input(_('%(propname)s (%(proptype)s): ')%{
589 'propname': key.capitalize(), 'proptype': name})
590 if value:
591 props[key] = value
592 else:
593 props = self.props_from_args(args[1:])
595 # convert types
596 for propname, value in props.items():
597 # get the property
598 try:
599 proptype = properties[propname]
600 except KeyError:
601 raise UsageError, _('%(classname)s has no property '
602 '"%(propname)s"')%locals()
604 if isinstance(proptype, hyperdb.Date):
605 try:
606 props[propname] = date.Date(value)
607 except ValueError, message:
608 raise UsageError, _('"%(value)s": %(message)s')%locals()
609 elif isinstance(proptype, hyperdb.Interval):
610 try:
611 props[propname] = date.Interval(value)
612 except ValueError, message:
613 raise UsageError, _('"%(value)s": %(message)s')%locals()
614 elif isinstance(proptype, hyperdb.Password):
615 props[propname] = password.Password(value)
616 elif isinstance(proptype, hyperdb.Multilink):
617 props[propname] = value.split(',')
618 elif isinstance(proptype, hyperdb.Boolean):
619 props[propname] = value.lower() in ('yes', 'true', 'on', '1')
620 elif isinstance(proptype, hyperdb.Number):
621 props[propname] = int(value)
623 # check for the key property
624 propname = cl.getkey()
625 if propname and not props.has_key(propname):
626 raise UsageError, _('you must provide the "%(propname)s" '
627 'property.')%locals()
629 # do the actual create
630 try:
631 print apply(cl.create, (), props)
632 except (TypeError, IndexError, ValueError), message:
633 raise UsageError, message
634 return 0
636 def do_list(self, args):
637 '''Usage: list classname [property]
638 List the instances of a class.
640 Lists all instances of the given class. If the property is not
641 specified, the "label" property is used. The label property is tried
642 in order: the key, "name", "title" and then the first property,
643 alphabetically.
644 '''
645 if len(args) < 1:
646 raise UsageError, _('Not enough arguments supplied')
647 classname = args[0]
649 # get the class
650 cl = self.get_class(classname)
652 # figure the property
653 if len(args) > 1:
654 propname = args[1]
655 else:
656 propname = cl.labelprop()
658 if self.comma_sep:
659 print ','.join(cl.list())
660 else:
661 for nodeid in cl.list():
662 try:
663 value = cl.get(nodeid, propname)
664 except KeyError:
665 raise UsageError, _('%(classname)s has no property '
666 '"%(propname)s"')%locals()
667 print _('%(nodeid)4s: %(value)s')%locals()
668 return 0
670 def do_table(self, args):
671 '''Usage: table classname [property[,property]*]
672 List the instances of a class in tabular form.
674 Lists all instances of the given class. If the properties are not
675 specified, all properties are displayed. By default, the column widths
676 are the width of the property names. The width may be explicitly defined
677 by defining the property as "name:width". For example::
678 roundup> table priority id,name:10
679 Id Name
680 1 fatal-bug
681 2 bug
682 3 usability
683 4 feature
684 '''
685 if len(args) < 1:
686 raise UsageError, _('Not enough arguments supplied')
687 classname = args[0]
689 # get the class
690 cl = self.get_class(classname)
692 # figure the property names to display
693 if len(args) > 1:
694 prop_names = args[1].split(',')
695 all_props = cl.getprops()
696 for spec in prop_names:
697 if ':' in spec:
698 try:
699 propname, width = spec.split(':')
700 except (ValueError, TypeError):
701 raise UsageError, _('"%(spec)s" not name:width')%locals()
702 else:
703 propname = spec
704 if not all_props.has_key(propname):
705 raise UsageError, _('%(classname)s has no property '
706 '"%(propname)s"')%locals()
707 else:
708 prop_names = cl.getprops().keys()
710 # now figure column widths
711 props = []
712 for spec in prop_names:
713 if ':' in spec:
714 name, width = spec.split(':')
715 props.append((name, int(width)))
716 else:
717 props.append((spec, len(spec)))
719 # now display the heading
720 print ' '.join([name.capitalize().ljust(width) for name,width in props])
722 # and the table data
723 for nodeid in cl.list():
724 l = []
725 for name, width in props:
726 if name != 'id':
727 try:
728 value = str(cl.get(nodeid, name))
729 except KeyError:
730 # we already checked if the property is valid - a
731 # KeyError here means the node just doesn't have a
732 # value for it
733 value = ''
734 else:
735 value = str(nodeid)
736 f = '%%-%ds'%width
737 l.append(f%value[:width])
738 print ' '.join(l)
739 return 0
741 def do_history(self, args):
742 '''Usage: history designator
743 Show the history entries of a designator.
745 Lists the journal entries for the node identified by the designator.
746 '''
747 if len(args) < 1:
748 raise UsageError, _('Not enough arguments supplied')
749 try:
750 classname, nodeid = hyperdb.splitDesignator(args[0])
751 except hyperdb.DesignatorError, message:
752 raise UsageError, message
754 try:
755 print self.db.getclass(classname).history(nodeid)
756 except KeyError:
757 raise UsageError, _('no such class "%(classname)s"')%locals()
758 except IndexError:
759 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
760 return 0
762 def do_commit(self, args):
763 '''Usage: commit
764 Commit all changes made to the database.
766 The changes made during an interactive session are not
767 automatically written to the database - they must be committed
768 using this command.
770 One-off commands on the command-line are automatically committed if
771 they are successful.
772 '''
773 self.db.commit()
774 return 0
776 def do_rollback(self, args):
777 '''Usage: rollback
778 Undo all changes that are pending commit to the database.
780 The changes made during an interactive session are not
781 automatically written to the database - they must be committed
782 manually. This command undoes all those changes, so a commit
783 immediately after would make no changes to the database.
784 '''
785 self.db.rollback()
786 return 0
788 def do_retire(self, args):
789 '''Usage: retire designator[,designator]*
790 Retire the node specified by designator.
792 This action indicates that a particular node is not to be retrieved by
793 the list or find commands, and its key value may be re-used.
794 '''
795 if len(args) < 1:
796 raise UsageError, _('Not enough arguments supplied')
797 designators = args[0].split(',')
798 for designator in designators:
799 try:
800 classname, nodeid = hyperdb.splitDesignator(designator)
801 except hyperdb.DesignatorError, message:
802 raise UsageError, message
803 try:
804 self.db.getclass(classname).retire(nodeid)
805 except KeyError:
806 raise UsageError, _('no such class "%(classname)s"')%locals()
807 except IndexError:
808 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
809 return 0
811 def do_export(self, args):
812 '''Usage: export class[,class] destination_dir
813 Export the database to tab-separated-value files.
815 This action exports the current data from the database into
816 tab-separated-value files that are placed in the nominated destination
817 directory. The journals are not exported.
818 '''
819 if len(args) < 2:
820 raise UsageError, _('Not enough arguments supplied')
821 classes = args[0].split(',')
822 dir = args[1]
824 # use the csv parser if we can - it's faster
825 if csv is not None:
826 p = csv.parser(field_sep=':')
828 # do all the classes specified
829 for classname in classes:
830 cl = self.get_class(classname)
831 f = open(os.path.join(dir, classname+'.csv'), 'w')
832 f.write(':'.join(cl.properties.keys()) + '\n')
834 # all nodes for this class
835 properties = cl.properties.items()
836 for nodeid in cl.list():
837 l = []
838 for prop, proptype in properties:
839 value = cl.get(nodeid, prop)
840 # convert data where needed
841 if isinstance(proptype, hyperdb.Date):
842 value = value.get_tuple()
843 elif isinstance(proptype, hyperdb.Interval):
844 value = value.get_tuple()
845 elif isinstance(proptype, hyperdb.Password):
846 value = str(value)
847 l.append(repr(value))
849 # now write
850 if csv is not None:
851 f.write(p.join(l) + '\n')
852 else:
853 # escape the individual entries to they're valid CSV
854 m = []
855 for entry in l:
856 if '"' in entry:
857 entry = '""'.join(entry.split('"'))
858 if ':' in entry:
859 entry = '"%s"'%entry
860 m.append(entry)
861 f.write(':'.join(m) + '\n')
862 return 0
864 def do_import(self, args):
865 '''Usage: import class file
866 Import the contents of the tab-separated-value file.
868 The file must define the same properties as the class (including having
869 a "header" line with those property names.) The new nodes are added to
870 the existing database - if you want to create a new database using the
871 imported data, then create a new database (or, tediously, retire all
872 the old data.)
873 '''
874 if len(args) < 2:
875 raise UsageError, _('Not enough arguments supplied')
876 if csv is None:
877 raise UsageError, \
878 _('Sorry, you need the csv module to use this function.\n'
879 'Get it from: http://www.object-craft.com.au/projects/csv/')
881 from roundup import hyperdb
883 # ensure that the properties and the CSV file headings match
884 classname = args[0]
885 cl = self.get_class(classname)
886 f = open(args[1])
887 p = csv.parser(field_sep=':')
888 file_props = p.parse(f.readline())
889 props = cl.properties.keys()
890 m = file_props[:]
891 m.sort()
892 props.sort()
893 if m != props:
894 raise UsageError, _('Import file doesn\'t define the same '
895 'properties as "%(arg0)s".')%{'arg0': args[0]}
897 # loop through the file and create a node for each entry
898 n = range(len(props))
899 while 1:
900 line = f.readline()
901 if not line: break
903 # parse lines until we get a complete entry
904 while 1:
905 l = p.parse(line)
906 if l: break
907 line = f.readline()
908 if not line:
909 raise ValueError, "Unexpected EOF during CSV parse"
911 # make the new node's property map
912 d = {}
913 for i in n:
914 # Use eval to reverse the repr() used to output the CSV
915 value = eval(l[i])
916 # Figure the property for this column
917 key = file_props[i]
918 proptype = cl.properties[key]
919 # Convert for property type
920 if isinstance(proptype, hyperdb.Date):
921 value = date.Date(value)
922 elif isinstance(proptype, hyperdb.Interval):
923 value = date.Interval(value)
924 elif isinstance(proptype, hyperdb.Password):
925 pwd = password.Password()
926 pwd.unpack(value)
927 value = pwd
928 if value is not None:
929 d[key] = value
931 # and create the new node
932 apply(cl.create, (), d)
933 return 0
935 def do_pack(self, args):
936 '''Usage: pack period | date
938 Remove journal entries older than a period of time specified or
939 before a certain date.
941 A period is specified using the suffixes "y", "m", and "d". The
942 suffix "w" (for "week") means 7 days.
944 "3y" means three years
945 "2y 1m" means two years and one month
946 "1m 25d" means one month and 25 days
947 "2w 3d" means two weeks and three days
949 Date format is "YYYY-MM-DD" eg:
950 2001-01-01
952 '''
953 if len(args) <> 1:
954 raise UsageError, _('Not enough arguments supplied')
956 # are we dealing with a period or a date
957 value = args[0]
958 date_re = re.compile(r'''
959 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
960 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
961 ''', re.VERBOSE)
962 m = date_re.match(value)
963 if not m:
964 raise ValueError, _('Invalid format')
965 m = m.groupdict()
966 if m['period']:
967 pack_before = date.Date(". - %s"%value)
968 elif m['date']:
969 pack_before = date.Date(value)
970 self.db.pack(pack_before)
971 return 0
973 def do_reindex(self, args):
974 '''Usage: reindex
975 Re-generate an instance's search indexes.
977 This will re-generate the search indexes for an instance. This will
978 typically happen automatically.
979 '''
980 self.db.indexer.force_reindex()
981 self.db.reindex()
982 return 0
984 def run_command(self, args):
985 '''Run a single command
986 '''
987 command = args[0]
989 # handle help now
990 if command == 'help':
991 if len(args)>1:
992 self.do_help(args[1:])
993 return 0
994 self.do_help(['help'])
995 return 0
996 if command == 'morehelp':
997 self.do_help(['help'])
998 self.help_commands()
999 self.help_all()
1000 return 0
1002 # figure what the command is
1003 try:
1004 functions = self.commands.get(command)
1005 except KeyError:
1006 # not a valid command
1007 print _('Unknown command "%(command)s" ("help commands" for a '
1008 'list)')%locals()
1009 return 1
1011 # check for multiple matches
1012 if len(functions) > 1:
1013 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1014 command, 'list': ', '.join([i[0] for i in functions])}
1015 return 1
1016 command, function = functions[0]
1018 # make sure we have an instance_home
1019 while not self.instance_home:
1020 self.instance_home = raw_input(_('Enter instance home: ')).strip()
1022 # before we open the db, we may be doing an install or init
1023 if command == 'initialise':
1024 try:
1025 return self.do_initialise(self.instance_home, args)
1026 except UsageError, message:
1027 print _('Error: %(message)s')%locals()
1028 return 1
1029 elif command == 'install':
1030 try:
1031 return self.do_install(self.instance_home, args)
1032 except UsageError, message:
1033 print _('Error: %(message)s')%locals()
1034 return 1
1036 # get the instance
1037 try:
1038 instance = roundup.instance.open(self.instance_home)
1039 except ValueError, message:
1040 self.instance_home = ''
1041 print _("Error: Couldn't open instance: %(message)s")%locals()
1042 return 1
1044 # only open the database once!
1045 if not self.db:
1046 self.db = instance.open('admin')
1048 # do the command
1049 ret = 0
1050 try:
1051 ret = function(args[1:])
1052 except UsageError, message:
1053 print _('Error: %(message)s')%locals()
1054 print
1055 print function.__doc__
1056 ret = 1
1057 except:
1058 import traceback
1059 traceback.print_exc()
1060 ret = 1
1061 return ret
1063 def interactive(self):
1064 '''Run in an interactive mode
1065 '''
1066 print _('Roundup %s ready for input.'%roundup_version)
1067 print _('Type "help" for help.')
1068 try:
1069 import readline
1070 except ImportError:
1071 print _('Note: command history and editing not available')
1073 while 1:
1074 try:
1075 command = raw_input(_('roundup> '))
1076 except EOFError:
1077 print _('exit...')
1078 break
1079 if not command: continue
1080 args = token.token_split(command)
1081 if not args: continue
1082 if args[0] in ('quit', 'exit'): break
1083 self.run_command(args)
1085 # exit.. check for transactions
1086 if self.db and self.db.transactions:
1087 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1088 if commit and commit[0].lower() == 'y':
1089 self.db.commit()
1090 return 0
1092 def main(self):
1093 try:
1094 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1095 except getopt.GetoptError, e:
1096 self.usage(str(e))
1097 return 1
1099 # handle command-line args
1100 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
1101 # TODO: reinstate the user/password stuff (-u arg too)
1102 name = password = ''
1103 if os.environ.has_key('ROUNDUP_LOGIN'):
1104 l = os.environ['ROUNDUP_LOGIN'].split(':')
1105 name = l[0]
1106 if len(l) > 1:
1107 password = l[1]
1108 self.comma_sep = 0
1109 for opt, arg in opts:
1110 if opt == '-h':
1111 self.usage()
1112 return 0
1113 if opt == '-i':
1114 self.instance_home = arg
1115 if opt == '-c':
1116 self.comma_sep = 1
1118 # if no command - go interactive
1119 ret = 0
1120 if not args:
1121 self.interactive()
1122 else:
1123 ret = self.run_command(args)
1124 if self.db: self.db.commit()
1125 return ret
1128 if __name__ == '__main__':
1129 tool = AdminTool()
1130 sys.exit(tool.main())
1132 #
1133 # $Log: not supported by cvs2svn $
1134 # Revision 1.18 2002/07/18 11:17:30 gmcm
1135 # Add Number and Boolean types to hyperdb.
1136 # Add conversion cases to web, mail & admin interfaces.
1137 # Add storage/serialization cases to back_anydbm & back_metakit.
1138 #
1139 # Revision 1.17 2002/07/14 06:05:50 richard
1140 # . fixed the date module so that Date(". - 2d") works
1141 #
1142 # Revision 1.16 2002/07/09 04:19:09 richard
1143 # Added reindex command to roundup-admin.
1144 # Fixed reindex on first access.
1145 # Also fixed reindexing of entries that change.
1146 #
1147 # Revision 1.15 2002/06/17 23:14:44 richard
1148 # . #569415 ] {version}
1149 #
1150 # Revision 1.14 2002/06/11 06:41:50 richard
1151 # Removed prompt for admin email in initialisation.
1152 #
1153 # Revision 1.13 2002/05/30 23:58:14 richard
1154 # oops
1155 #
1156 # Revision 1.12 2002/05/26 09:04:42 richard
1157 # out by one in the init args
1158 #
1159 # Revision 1.11 2002/05/23 01:14:20 richard
1160 # . split instance initialisation into two steps, allowing config changes
1161 # before the database is initialised.
1162 #
1163 # Revision 1.10 2002/04/27 10:07:23 richard
1164 # minor fix to error message
1165 #
1166 # Revision 1.9 2002/03/12 22:51:47 richard
1167 # . #527416 ] roundup-admin uses undefined value
1168 # . #527503 ] unfriendly init blowup when parent dir
1169 # (also handles UsageError correctly now in init)
1170 #
1171 # Revision 1.8 2002/02/27 03:28:21 richard
1172 # Ran it through pychecker, made fixes
1173 #
1174 # Revision 1.7 2002/02/20 05:04:32 richard
1175 # Wasn't handling the cvs parser feeding properly.
1176 #
1177 # Revision 1.6 2002/01/23 07:27:19 grubert
1178 # . allow abbreviation of "help" in admin tool too.
1179 #
1180 # Revision 1.5 2002/01/21 16:33:19 rochecompaan
1181 # You can now use the roundup-admin tool to pack the database
1182 #
1183 # Revision 1.4 2002/01/14 06:51:09 richard
1184 # . #503164 ] create and passwords
1185 #
1186 # Revision 1.3 2002/01/08 05:26:32 rochecompaan
1187 # Missing "self" in props_from_args
1188 #
1189 # Revision 1.2 2002/01/07 10:41:44 richard
1190 # #500140 ] AdminTool.get_class() returns nothing
1191 #
1192 # Revision 1.1 2002/01/05 02:11:22 richard
1193 # I18N'ed roundup admin - and split the code off into a module so it can be used
1194 # elsewhere.
1195 # Big issue with this is the doc strings - that's the help. We're probably going to
1196 # have to switch to not use docstrings, which will suck a little :(
1197 #
1198 #
1199 #
1200 # vim: set filetype=python ts=4 sw=4 et si