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.23 2002-08-19 02:53:27 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)
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]
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) > 1:
328 adminpw = args[1]
329 else:
330 adminpw = ''
331 confirm = 'x'
332 while adminpw != confirm:
333 adminpw = getpass.getpass(_('Admin Password: '))
334 confirm = getpass.getpass(_(' Confirm: '))
336 # make sure the instance home is installed
337 if not os.path.exists(instance_home):
338 raise UsageError, _('Instance home does not exist')%locals()
339 if not os.path.exists(os.path.join(instance_home, 'html')):
340 raise UsageError, _('Instance has not been installed')%locals()
342 # is there already a database?
343 if os.path.exists(os.path.join(instance_home, 'db')):
344 print _('WARNING: The database is already initialised!')
345 print _('If you re-initialise it, you will lose all the data!')
346 ok = raw_input(_('Erase it? Y/[N]: ')).strip()
347 if ok.lower() != 'y':
348 return 0
350 # nuke it
351 shutil.rmtree(os.path.join(instance_home, 'db'))
353 # GO
354 init.initialise(instance_home, adminpw)
356 return 0
359 def do_get(self, args):
360 '''Usage: get property designator[,designator]*
361 Get the given property of one or more designator(s).
363 Retrieves the property value of the nodes specified by the designators.
364 '''
365 if len(args) < 2:
366 raise UsageError, _('Not enough arguments supplied')
367 propname = args[0]
368 designators = args[1].split(',')
369 l = []
370 for designator in designators:
371 # decode the node designator
372 try:
373 classname, nodeid = hyperdb.splitDesignator(designator)
374 except hyperdb.DesignatorError, message:
375 raise UsageError, message
377 # get the class
378 cl = self.get_class(classname)
379 try:
380 if self.comma_sep:
381 l.append(cl.get(nodeid, propname))
382 else:
383 print cl.get(nodeid, propname)
384 except IndexError:
385 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
386 except KeyError:
387 raise UsageError, _('no such %(classname)s property '
388 '"%(propname)s"')%locals()
389 if self.comma_sep:
390 print ','.join(l)
391 return 0
394 def do_set(self, args):
395 '''Usage: set designator[,designator]* propname=value ...
396 Set the given property of one or more designator(s).
398 Sets the property to the value for all designators given.
399 '''
400 if len(args) < 2:
401 raise UsageError, _('Not enough arguments supplied')
402 from roundup import hyperdb
404 designators = args[0].split(',')
406 # get the props from the args
407 props = self.props_from_args(args[1:])
409 # now do the set for all the nodes
410 for designator in designators:
411 # decode the node designator
412 try:
413 classname, nodeid = hyperdb.splitDesignator(designator)
414 except hyperdb.DesignatorError, message:
415 raise UsageError, message
417 # get the class
418 cl = self.get_class(classname)
420 properties = cl.getprops()
421 for key, value in props.items():
422 proptype = properties[key]
423 if isinstance(proptype, hyperdb.String):
424 continue
425 elif isinstance(proptype, hyperdb.Password):
426 props[key] = password.Password(value)
427 elif isinstance(proptype, hyperdb.Date):
428 try:
429 props[key] = date.Date(value)
430 except ValueError, message:
431 raise UsageError, '"%s": %s'%(value, message)
432 elif isinstance(proptype, hyperdb.Interval):
433 try:
434 props[key] = date.Interval(value)
435 except ValueError, message:
436 raise UsageError, '"%s": %s'%(value, message)
437 elif isinstance(proptype, hyperdb.Link):
438 props[key] = value
439 elif isinstance(proptype, hyperdb.Multilink):
440 props[key] = value.split(',')
441 elif isinstance(proptype, hyperdb.Boolean):
442 props[key] = value.lower() in ('yes', 'true', 'on', '1')
443 elif isinstance(proptype, hyperdb.Number):
444 props[key] = int(value)
446 # try the set
447 try:
448 apply(cl.set, (nodeid, ), props)
449 except (TypeError, IndexError, ValueError), message:
450 raise UsageError, message
451 return 0
453 def do_find(self, args):
454 '''Usage: find classname propname=value ...
455 Find the nodes of the given class with a given link property value.
457 Find the nodes of the given class with a given link property value. The
458 value may be either the nodeid of the linked node, or its key value.
459 '''
460 if len(args) < 1:
461 raise UsageError, _('Not enough arguments supplied')
462 classname = args[0]
463 # get the class
464 cl = self.get_class(classname)
466 # handle the propname=value argument
467 props = self.props_from_args(args[1:])
469 # if the value isn't a number, look up the linked class to get the
470 # number
471 for propname, value in props.items():
472 num_re = re.compile('^\d+$')
473 if not num_re.match(value):
474 # get the property
475 try:
476 property = cl.properties[propname]
477 except KeyError:
478 raise UsageError, _('%(classname)s has no property '
479 '"%(propname)s"')%locals()
481 # make sure it's a link
482 if (not isinstance(property, hyperdb.Link) and not
483 isinstance(property, hyperdb.Multilink)):
484 raise UsageError, _('You may only "find" link properties')
486 # get the linked-to class and look up the key property
487 link_class = self.db.getclass(property.classname)
488 try:
489 props[propname] = link_class.lookup(value)
490 except TypeError:
491 raise UsageError, _('%(classname)s has no key property"')%{
492 'classname': link_class.classname}
494 # now do the find
495 try:
496 if self.comma_sep:
497 print ','.join(apply(cl.find, (), props))
498 else:
499 print apply(cl.find, (), props)
500 except KeyError:
501 raise UsageError, _('%(classname)s has no property '
502 '"%(propname)s"')%locals()
503 except (ValueError, TypeError), message:
504 raise UsageError, message
505 return 0
507 def do_specification(self, args):
508 '''Usage: specification classname
509 Show the properties for a classname.
511 This lists the properties for a given class.
512 '''
513 if len(args) < 1:
514 raise UsageError, _('Not enough arguments supplied')
515 classname = args[0]
516 # get the class
517 cl = self.get_class(classname)
519 # get the key property
520 keyprop = cl.getkey()
521 for key, value in cl.properties.items():
522 if keyprop == key:
523 print _('%(key)s: %(value)s (key property)')%locals()
524 else:
525 print _('%(key)s: %(value)s')%locals()
527 def do_display(self, args):
528 '''Usage: display designator
529 Show the property values for the given node.
531 This lists the properties and their associated values for the given
532 node.
533 '''
534 if len(args) < 1:
535 raise UsageError, _('Not enough arguments supplied')
537 # decode the node designator
538 try:
539 classname, nodeid = hyperdb.splitDesignator(args[0])
540 except hyperdb.DesignatorError, message:
541 raise UsageError, message
543 # get the class
544 cl = self.get_class(classname)
546 # display the values
547 for key in cl.properties.keys():
548 value = cl.get(nodeid, key)
549 print _('%(key)s: %(value)s')%locals()
551 def do_create(self, args):
552 '''Usage: create classname property=value ...
553 Create a new entry of a given class.
555 This creates a new entry of the given class using the property
556 name=value arguments provided on the command line after the "create"
557 command.
558 '''
559 if len(args) < 1:
560 raise UsageError, _('Not enough arguments supplied')
561 from roundup import hyperdb
563 classname = args[0]
565 # get the class
566 cl = self.get_class(classname)
568 # now do a create
569 props = {}
570 properties = cl.getprops(protected = 0)
571 if len(args) == 1:
572 # ask for the properties
573 for key, value in properties.items():
574 if key == 'id': continue
575 name = value.__class__.__name__
576 if isinstance(value , hyperdb.Password):
577 again = None
578 while value != again:
579 value = getpass.getpass(_('%(propname)s (Password): ')%{
580 'propname': key.capitalize()})
581 again = getpass.getpass(_(' %(propname)s (Again): ')%{
582 'propname': key.capitalize()})
583 if value != again: print _('Sorry, try again...')
584 if value:
585 props[key] = value
586 else:
587 value = raw_input(_('%(propname)s (%(proptype)s): ')%{
588 'propname': key.capitalize(), 'proptype': name})
589 if value:
590 props[key] = value
591 else:
592 props = self.props_from_args(args[1:])
594 # convert types
595 for propname, value in props.items():
596 # get the property
597 try:
598 proptype = properties[propname]
599 except KeyError:
600 raise UsageError, _('%(classname)s has no property '
601 '"%(propname)s"')%locals()
603 if isinstance(proptype, hyperdb.Date):
604 try:
605 props[propname] = date.Date(value)
606 except ValueError, message:
607 raise UsageError, _('"%(value)s": %(message)s')%locals()
608 elif isinstance(proptype, hyperdb.Interval):
609 try:
610 props[propname] = date.Interval(value)
611 except ValueError, message:
612 raise UsageError, _('"%(value)s": %(message)s')%locals()
613 elif isinstance(proptype, hyperdb.Password):
614 props[propname] = password.Password(value)
615 elif isinstance(proptype, hyperdb.Multilink):
616 props[propname] = value.split(',')
617 elif isinstance(proptype, hyperdb.Boolean):
618 props[propname] = value.lower() in ('yes', 'true', 'on', '1')
619 elif isinstance(proptype, hyperdb.Number):
620 props[propname] = int(value)
622 # check for the key property
623 propname = cl.getkey()
624 if propname and not props.has_key(propname):
625 raise UsageError, _('you must provide the "%(propname)s" '
626 'property.')%locals()
628 # do the actual create
629 try:
630 print apply(cl.create, (), props)
631 except (TypeError, IndexError, ValueError), message:
632 raise UsageError, message
633 return 0
635 def do_list(self, args):
636 '''Usage: list classname [property]
637 List the instances of a class.
639 Lists all instances of the given class. If the property is not
640 specified, the "label" property is used. The label property is tried
641 in order: the key, "name", "title" and then the first property,
642 alphabetically.
643 '''
644 if len(args) < 1:
645 raise UsageError, _('Not enough arguments supplied')
646 classname = args[0]
648 # get the class
649 cl = self.get_class(classname)
651 # figure the property
652 if len(args) > 1:
653 propname = args[1]
654 else:
655 propname = cl.labelprop()
657 if self.comma_sep:
658 print ','.join(cl.list())
659 else:
660 for nodeid in cl.list():
661 try:
662 value = cl.get(nodeid, propname)
663 except KeyError:
664 raise UsageError, _('%(classname)s has no property '
665 '"%(propname)s"')%locals()
666 print _('%(nodeid)4s: %(value)s')%locals()
667 return 0
669 def do_table(self, args):
670 '''Usage: table classname [property[,property]*]
671 List the instances of a class in tabular form.
673 Lists all instances of the given class. If the properties are not
674 specified, all properties are displayed. By default, the column widths
675 are the width of the property names. The width may be explicitly defined
676 by defining the property as "name:width". For example::
677 roundup> table priority id,name:10
678 Id Name
679 1 fatal-bug
680 2 bug
681 3 usability
682 4 feature
683 '''
684 if len(args) < 1:
685 raise UsageError, _('Not enough arguments supplied')
686 classname = args[0]
688 # get the class
689 cl = self.get_class(classname)
691 # figure the property names to display
692 if len(args) > 1:
693 prop_names = args[1].split(',')
694 all_props = cl.getprops()
695 for spec in prop_names:
696 if ':' in spec:
697 try:
698 propname, width = spec.split(':')
699 except (ValueError, TypeError):
700 raise UsageError, _('"%(spec)s" not name:width')%locals()
701 else:
702 propname = spec
703 if not all_props.has_key(propname):
704 raise UsageError, _('%(classname)s has no property '
705 '"%(propname)s"')%locals()
706 else:
707 prop_names = cl.getprops().keys()
709 # now figure column widths
710 props = []
711 for spec in prop_names:
712 if ':' in spec:
713 name, width = spec.split(':')
714 props.append((name, int(width)))
715 else:
716 props.append((spec, len(spec)))
718 # now display the heading
719 print ' '.join([name.capitalize().ljust(width) for name,width in props])
721 # and the table data
722 for nodeid in cl.list():
723 l = []
724 for name, width in props:
725 if name != 'id':
726 try:
727 value = str(cl.get(nodeid, name))
728 except KeyError:
729 # we already checked if the property is valid - a
730 # KeyError here means the node just doesn't have a
731 # value for it
732 value = ''
733 else:
734 value = str(nodeid)
735 f = '%%-%ds'%width
736 l.append(f%value[:width])
737 print ' '.join(l)
738 return 0
740 def do_history(self, args):
741 '''Usage: history designator
742 Show the history entries of a designator.
744 Lists the journal entries for the node identified by the designator.
745 '''
746 if len(args) < 1:
747 raise UsageError, _('Not enough arguments supplied')
748 try:
749 classname, nodeid = hyperdb.splitDesignator(args[0])
750 except hyperdb.DesignatorError, message:
751 raise UsageError, message
753 try:
754 print self.db.getclass(classname).history(nodeid)
755 except KeyError:
756 raise UsageError, _('no such class "%(classname)s"')%locals()
757 except IndexError:
758 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
759 return 0
761 def do_commit(self, args):
762 '''Usage: commit
763 Commit all changes made to the database.
765 The changes made during an interactive session are not
766 automatically written to the database - they must be committed
767 using this command.
769 One-off commands on the command-line are automatically committed if
770 they are successful.
771 '''
772 self.db.commit()
773 return 0
775 def do_rollback(self, args):
776 '''Usage: rollback
777 Undo all changes that are pending commit to the database.
779 The changes made during an interactive session are not
780 automatically written to the database - they must be committed
781 manually. This command undoes all those changes, so a commit
782 immediately after would make no changes to the database.
783 '''
784 self.db.rollback()
785 return 0
787 def do_retire(self, args):
788 '''Usage: retire designator[,designator]*
789 Retire the node specified by designator.
791 This action indicates that a particular node is not to be retrieved by
792 the list or find commands, and its key value may be re-used.
793 '''
794 if len(args) < 1:
795 raise UsageError, _('Not enough arguments supplied')
796 designators = args[0].split(',')
797 for designator in designators:
798 try:
799 classname, nodeid = hyperdb.splitDesignator(designator)
800 except hyperdb.DesignatorError, message:
801 raise UsageError, message
802 try:
803 self.db.getclass(classname).retire(nodeid)
804 except KeyError:
805 raise UsageError, _('no such class "%(classname)s"')%locals()
806 except IndexError:
807 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
808 return 0
810 def do_export(self, args):
811 '''Usage: export [class[,class]] export_dir
812 Export the database to tab-separated-value files.
814 This action exports the current data from the database into
815 tab-separated-value files that are placed in the nominated destination
816 directory. The journals are not exported.
817 '''
818 # we need the CSV module
819 if csv is None:
820 raise UsageError, \
821 _('Sorry, you need the csv module to use this function.\n'
822 'Get it from: http://www.object-craft.com.au/projects/csv/')
824 # grab the directory to export to
825 if len(args) < 1:
826 raise UsageError, _('Not enough arguments supplied')
827 dir = args[-1]
829 # get the list of classes to export
830 if len(args) == 2:
831 classes = args[0].split(',')
832 else:
833 classes = self.db.classes.keys()
835 # use the csv parser if we can - it's faster
836 p = csv.parser(field_sep=':')
838 # do all the classes specified
839 for classname in classes:
840 cl = self.get_class(classname)
841 f = open(os.path.join(dir, classname+'.csv'), 'w')
842 properties = cl.getprops()
843 propnames = properties.keys()
844 propnames.sort()
845 print >> f, p.join(propnames)
847 # all nodes for this class
848 for nodeid in cl.list():
849 print >>f, p.join(cl.export_list(propnames, nodeid))
850 return 0
852 def do_import(self, args):
853 '''Usage: import import_dir
854 Import a database from the directory containing CSV files, one per
855 class to import.
857 The files must define the same properties as the class (including having
858 a "header" line with those property names.)
860 The imported nodes will have the same nodeid as defined in the
861 import file, thus replacing any existing content.
863 XXX The new nodes are added to the existing database - if you want to
864 XXX create a new database using the imported data, then create a new
865 XXX database (or, tediously, retire all the old data.)
866 '''
867 if len(args) < 1:
868 raise UsageError, _('Not enough arguments supplied')
869 if csv is None:
870 raise UsageError, \
871 _('Sorry, you need the csv module to use this function.\n'
872 'Get it from: http://www.object-craft.com.au/projects/csv/')
874 from roundup import hyperdb
876 for file in os.listdir(args[0]):
877 f = open(os.path.join(args[0], file))
879 # get the classname
880 classname = os.path.splitext(file)[0]
882 # ensure that the properties and the CSV file headings match
883 cl = self.get_class(classname)
884 p = csv.parser(field_sep=':')
885 file_props = p.parse(f.readline())
886 properties = cl.getprops()
887 propnames = properties.keys()
888 propnames.sort()
889 m = file_props[:]
890 m.sort()
891 if m != propnames:
892 raise UsageError, _('Import file doesn\'t define the same '
893 'properties as "%(arg0)s".')%{'arg0': args[0]}
895 # loop through the file and create a node for each entry
896 maxid = 1
897 while 1:
898 line = f.readline()
899 if not line: break
901 # parse lines until we get a complete entry
902 while 1:
903 l = p.parse(line)
904 if l: break
905 line = f.readline()
906 if not line:
907 raise ValueError, "Unexpected EOF during CSV parse"
909 # do the import and figure the current highest nodeid
910 maxid = max(maxid, int(cl.import_list(propnames, l)))
912 print 'setting', classname, maxid
913 self.db.setid(classname, str(maxid))
914 return 0
916 def do_pack(self, args):
917 '''Usage: pack period | date
919 Remove journal entries older than a period of time specified or
920 before a certain date.
922 A period is specified using the suffixes "y", "m", and "d". The
923 suffix "w" (for "week") means 7 days.
925 "3y" means three years
926 "2y 1m" means two years and one month
927 "1m 25d" means one month and 25 days
928 "2w 3d" means two weeks and three days
930 Date format is "YYYY-MM-DD" eg:
931 2001-01-01
933 '''
934 if len(args) <> 1:
935 raise UsageError, _('Not enough arguments supplied')
937 # are we dealing with a period or a date
938 value = args[0]
939 date_re = re.compile(r'''
940 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
941 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
942 ''', re.VERBOSE)
943 m = date_re.match(value)
944 if not m:
945 raise ValueError, _('Invalid format')
946 m = m.groupdict()
947 if m['period']:
948 pack_before = date.Date(". - %s"%value)
949 elif m['date']:
950 pack_before = date.Date(value)
951 self.db.pack(pack_before)
952 return 0
954 def do_reindex(self, args):
955 '''Usage: reindex
956 Re-generate an instance's search indexes.
958 This will re-generate the search indexes for an instance. This will
959 typically happen automatically.
960 '''
961 self.db.indexer.force_reindex()
962 self.db.reindex()
963 return 0
965 def do_security(self, args):
966 '''Usage: security [Role name]
967 Display the Permissions available to one or all Roles.
968 '''
969 if len(args) == 1:
970 role = args[0]
971 try:
972 roles = [(args[0], self.db.security.role[args[0]])]
973 except KeyError:
974 print _('No such Role "%(role)s"')%locals()
975 return 1
976 else:
977 roles = self.db.security.role.items()
978 role = self.db.config.NEW_WEB_USER_ROLES
979 if ',' in role:
980 print _('New Web users get the Roles "%(role)s"')%locals()
981 else:
982 print _('New Web users get the Role "%(role)s"')%locals()
983 role = self.db.config.NEW_EMAIL_USER_ROLES
984 if ',' in role:
985 print _('New Email users get the Roles "%(role)s"')%locals()
986 else:
987 print _('New Email users get the Role "%(role)s"')%locals()
988 roles.sort()
989 for rolename, role in roles:
990 print _('Role "%(name)s":')%role.__dict__
991 for permission in role.permissions:
992 if permission.klass:
993 print _(' %(description)s (%(name)s for "%(klass)s" '
994 'only)')%permission.__dict__
995 else:
996 print _(' %(description)s (%(name)s)')%permission.__dict__
997 return 0
999 def run_command(self, args):
1000 '''Run a single command
1001 '''
1002 command = args[0]
1004 # handle help now
1005 if command == 'help':
1006 if len(args)>1:
1007 self.do_help(args[1:])
1008 return 0
1009 self.do_help(['help'])
1010 return 0
1011 if command == 'morehelp':
1012 self.do_help(['help'])
1013 self.help_commands()
1014 self.help_all()
1015 return 0
1017 # figure what the command is
1018 try:
1019 functions = self.commands.get(command)
1020 except KeyError:
1021 # not a valid command
1022 print _('Unknown command "%(command)s" ("help commands" for a '
1023 'list)')%locals()
1024 return 1
1026 # check for multiple matches
1027 if len(functions) > 1:
1028 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1029 command, 'list': ', '.join([i[0] for i in functions])}
1030 return 1
1031 command, function = functions[0]
1033 # make sure we have an instance_home
1034 while not self.instance_home:
1035 self.instance_home = raw_input(_('Enter instance home: ')).strip()
1037 # before we open the db, we may be doing an install or init
1038 if command == 'initialise':
1039 try:
1040 return self.do_initialise(self.instance_home, args)
1041 except UsageError, message:
1042 print _('Error: %(message)s')%locals()
1043 return 1
1044 elif command == 'install':
1045 try:
1046 return self.do_install(self.instance_home, args)
1047 except UsageError, message:
1048 print _('Error: %(message)s')%locals()
1049 return 1
1051 # get the instance
1052 try:
1053 instance = roundup.instance.open(self.instance_home)
1054 except ValueError, message:
1055 self.instance_home = ''
1056 print _("Error: Couldn't open instance: %(message)s")%locals()
1057 return 1
1059 # only open the database once!
1060 if not self.db:
1061 self.db = instance.open('admin')
1063 # do the command
1064 ret = 0
1065 try:
1066 ret = function(args[1:])
1067 except UsageError, message:
1068 print _('Error: %(message)s')%locals()
1069 print
1070 print function.__doc__
1071 ret = 1
1072 except:
1073 import traceback
1074 traceback.print_exc()
1075 ret = 1
1076 return ret
1078 def interactive(self):
1079 '''Run in an interactive mode
1080 '''
1081 print _('Roundup %s ready for input.'%roundup_version)
1082 print _('Type "help" for help.')
1083 try:
1084 import readline
1085 except ImportError:
1086 print _('Note: command history and editing not available')
1088 while 1:
1089 try:
1090 command = raw_input(_('roundup> '))
1091 except EOFError:
1092 print _('exit...')
1093 break
1094 if not command: continue
1095 args = token.token_split(command)
1096 if not args: continue
1097 if args[0] in ('quit', 'exit'): break
1098 self.run_command(args)
1100 # exit.. check for transactions
1101 if self.db and self.db.transactions:
1102 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1103 if commit and commit[0].lower() == 'y':
1104 self.db.commit()
1105 return 0
1107 def main(self):
1108 try:
1109 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1110 except getopt.GetoptError, e:
1111 self.usage(str(e))
1112 return 1
1114 # handle command-line args
1115 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
1116 # TODO: reinstate the user/password stuff (-u arg too)
1117 name = password = ''
1118 if os.environ.has_key('ROUNDUP_LOGIN'):
1119 l = os.environ['ROUNDUP_LOGIN'].split(':')
1120 name = l[0]
1121 if len(l) > 1:
1122 password = l[1]
1123 self.comma_sep = 0
1124 for opt, arg in opts:
1125 if opt == '-h':
1126 self.usage()
1127 return 0
1128 if opt == '-i':
1129 self.instance_home = arg
1130 if opt == '-c':
1131 self.comma_sep = 1
1133 # if no command - go interactive
1134 ret = 0
1135 if not args:
1136 self.interactive()
1137 else:
1138 ret = self.run_command(args)
1139 if self.db: self.db.commit()
1140 return ret
1143 if __name__ == '__main__':
1144 tool = AdminTool()
1145 sys.exit(tool.main())
1147 #
1148 # $Log: not supported by cvs2svn $
1149 # Revision 1.22 2002/08/16 04:26:42 richard
1150 # moving towards full database export
1151 #
1152 # Revision 1.21 2002/08/01 01:07:37 richard
1153 # include info about new user roles
1154 #
1155 # Revision 1.20 2002/08/01 00:56:22 richard
1156 # Added the web access and email access permissions, so people can restrict
1157 # access to users who register through the email interface (for example).
1158 # Also added "security" command to the roundup-admin interface to display the
1159 # Role/Permission config for an instance.
1160 #
1161 # Revision 1.19 2002/07/25 07:14:05 richard
1162 # Bugger it. Here's the current shape of the new security implementation.
1163 # Still to do:
1164 # . call the security funcs from cgi and mailgw
1165 # . change shipped templates to include correct initialisation and remove
1166 # the old config vars
1167 # ... that seems like a lot. The bulk of the work has been done though. Honest :)
1168 #
1169 # Revision 1.18 2002/07/18 11:17:30 gmcm
1170 # Add Number and Boolean types to hyperdb.
1171 # Add conversion cases to web, mail & admin interfaces.
1172 # Add storage/serialization cases to back_anydbm & back_metakit.
1173 #
1174 # Revision 1.17 2002/07/14 06:05:50 richard
1175 # . fixed the date module so that Date(". - 2d") works
1176 #
1177 # Revision 1.16 2002/07/09 04:19:09 richard
1178 # Added reindex command to roundup-admin.
1179 # Fixed reindex on first access.
1180 # Also fixed reindexing of entries that change.
1181 #
1182 # Revision 1.15 2002/06/17 23:14:44 richard
1183 # . #569415 ] {version}
1184 #
1185 # Revision 1.14 2002/06/11 06:41:50 richard
1186 # Removed prompt for admin email in initialisation.
1187 #
1188 # Revision 1.13 2002/05/30 23:58:14 richard
1189 # oops
1190 #
1191 # Revision 1.12 2002/05/26 09:04:42 richard
1192 # out by one in the init args
1193 #
1194 # Revision 1.11 2002/05/23 01:14:20 richard
1195 # . split instance initialisation into two steps, allowing config changes
1196 # before the database is initialised.
1197 #
1198 # Revision 1.10 2002/04/27 10:07:23 richard
1199 # minor fix to error message
1200 #
1201 # Revision 1.9 2002/03/12 22:51:47 richard
1202 # . #527416 ] roundup-admin uses undefined value
1203 # . #527503 ] unfriendly init blowup when parent dir
1204 # (also handles UsageError correctly now in init)
1205 #
1206 # Revision 1.8 2002/02/27 03:28:21 richard
1207 # Ran it through pychecker, made fixes
1208 #
1209 # Revision 1.7 2002/02/20 05:04:32 richard
1210 # Wasn't handling the cvs parser feeding properly.
1211 #
1212 # Revision 1.6 2002/01/23 07:27:19 grubert
1213 # . allow abbreviation of "help" in admin tool too.
1214 #
1215 # Revision 1.5 2002/01/21 16:33:19 rochecompaan
1216 # You can now use the roundup-admin tool to pack the database
1217 #
1218 # Revision 1.4 2002/01/14 06:51:09 richard
1219 # . #503164 ] create and passwords
1220 #
1221 # Revision 1.3 2002/01/08 05:26:32 rochecompaan
1222 # Missing "self" in props_from_args
1223 #
1224 # Revision 1.2 2002/01/07 10:41:44 richard
1225 # #500140 ] AdminTool.get_class() returns nothing
1226 #
1227 # Revision 1.1 2002/01/05 02:11:22 richard
1228 # I18N'ed roundup admin - and split the code off into a module so it can be used
1229 # elsewhere.
1230 # Big issue with this is the doc strings - that's the help. We're probably going to
1231 # have to switch to not use docstrings, which will suck a little :(
1232 #
1233 #
1234 #
1235 # vim: set filetype=python ts=4 sw=4 et si