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.21 2002-08-01 01:07:37 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] destination_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 if len(args) < 2:
819 raise UsageError, _('Not enough arguments supplied')
820 classes = args[0].split(',')
821 dir = args[1]
823 # use the csv parser if we can - it's faster
824 if csv is not None:
825 p = csv.parser(field_sep=':')
827 # do all the classes specified
828 for classname in classes:
829 cl = self.get_class(classname)
830 f = open(os.path.join(dir, classname+'.csv'), 'w')
831 f.write(':'.join(cl.properties.keys()) + '\n')
833 # all nodes for this class
834 properties = cl.properties.items()
835 for nodeid in cl.list():
836 l = []
837 for prop, proptype in properties:
838 value = cl.get(nodeid, prop)
839 # convert data where needed
840 if isinstance(proptype, hyperdb.Date):
841 value = value.get_tuple()
842 elif isinstance(proptype, hyperdb.Interval):
843 value = value.get_tuple()
844 elif isinstance(proptype, hyperdb.Password):
845 value = str(value)
846 l.append(repr(value))
848 # now write
849 if csv is not None:
850 f.write(p.join(l) + '\n')
851 else:
852 # escape the individual entries to they're valid CSV
853 m = []
854 for entry in l:
855 if '"' in entry:
856 entry = '""'.join(entry.split('"'))
857 if ':' in entry:
858 entry = '"%s"'%entry
859 m.append(entry)
860 f.write(':'.join(m) + '\n')
861 return 0
863 def do_import(self, args):
864 '''Usage: import class file
865 Import the contents of the tab-separated-value file.
867 The file must define the same properties as the class (including having
868 a "header" line with those property names.) The new nodes are added to
869 the existing database - if you want to create a new database using the
870 imported data, then create a new database (or, tediously, retire all
871 the old data.)
872 '''
873 if len(args) < 2:
874 raise UsageError, _('Not enough arguments supplied')
875 if csv is None:
876 raise UsageError, \
877 _('Sorry, you need the csv module to use this function.\n'
878 'Get it from: http://www.object-craft.com.au/projects/csv/')
880 from roundup import hyperdb
882 # ensure that the properties and the CSV file headings match
883 classname = args[0]
884 cl = self.get_class(classname)
885 f = open(args[1])
886 p = csv.parser(field_sep=':')
887 file_props = p.parse(f.readline())
888 props = cl.properties.keys()
889 m = file_props[:]
890 m.sort()
891 props.sort()
892 if m != props:
893 raise UsageError, _('Import file doesn\'t define the same '
894 'properties as "%(arg0)s".')%{'arg0': args[0]}
896 # loop through the file and create a node for each entry
897 n = range(len(props))
898 while 1:
899 line = f.readline()
900 if not line: break
902 # parse lines until we get a complete entry
903 while 1:
904 l = p.parse(line)
905 if l: break
906 line = f.readline()
907 if not line:
908 raise ValueError, "Unexpected EOF during CSV parse"
910 # make the new node's property map
911 d = {}
912 for i in n:
913 # Use eval to reverse the repr() used to output the CSV
914 value = eval(l[i])
915 # Figure the property for this column
916 key = file_props[i]
917 proptype = cl.properties[key]
918 # Convert for property type
919 if isinstance(proptype, hyperdb.Date):
920 value = date.Date(value)
921 elif isinstance(proptype, hyperdb.Interval):
922 value = date.Interval(value)
923 elif isinstance(proptype, hyperdb.Password):
924 pwd = password.Password()
925 pwd.unpack(value)
926 value = pwd
927 if value is not None:
928 d[key] = value
930 # and create the new node
931 apply(cl.create, (), d)
932 return 0
934 def do_pack(self, args):
935 '''Usage: pack period | date
937 Remove journal entries older than a period of time specified or
938 before a certain date.
940 A period is specified using the suffixes "y", "m", and "d". The
941 suffix "w" (for "week") means 7 days.
943 "3y" means three years
944 "2y 1m" means two years and one month
945 "1m 25d" means one month and 25 days
946 "2w 3d" means two weeks and three days
948 Date format is "YYYY-MM-DD" eg:
949 2001-01-01
951 '''
952 if len(args) <> 1:
953 raise UsageError, _('Not enough arguments supplied')
955 # are we dealing with a period or a date
956 value = args[0]
957 date_re = re.compile(r'''
958 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
959 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
960 ''', re.VERBOSE)
961 m = date_re.match(value)
962 if not m:
963 raise ValueError, _('Invalid format')
964 m = m.groupdict()
965 if m['period']:
966 pack_before = date.Date(". - %s"%value)
967 elif m['date']:
968 pack_before = date.Date(value)
969 self.db.pack(pack_before)
970 return 0
972 def do_reindex(self, args):
973 '''Usage: reindex
974 Re-generate an instance's search indexes.
976 This will re-generate the search indexes for an instance. This will
977 typically happen automatically.
978 '''
979 self.db.indexer.force_reindex()
980 self.db.reindex()
981 return 0
983 def do_security(self, args):
984 '''Usage: security [Role name]
985 Display the Permissions available to one or all Roles.
986 '''
987 if len(args) == 1:
988 role = args[0]
989 try:
990 roles = [(args[0], self.db.security.role[args[0]])]
991 except KeyError:
992 print _('No such Role "%(role)s"')%locals()
993 return 1
994 else:
995 roles = self.db.security.role.items()
996 role = self.db.config.NEW_WEB_USER_ROLES
997 if ',' in role:
998 print _('New Web users get the Roles "%(role)s"')%locals()
999 else:
1000 print _('New Web users get the Role "%(role)s"')%locals()
1001 role = self.db.config.NEW_EMAIL_USER_ROLES
1002 if ',' in role:
1003 print _('New Email users get the Roles "%(role)s"')%locals()
1004 else:
1005 print _('New Email users get the Role "%(role)s"')%locals()
1006 roles.sort()
1007 for rolename, role in roles:
1008 print _('Role "%(name)s":')%role.__dict__
1009 for permission in role.permissions:
1010 if permission.klass:
1011 print _(' %(description)s (%(name)s for "%(klass)s" '
1012 'only)')%permission.__dict__
1013 else:
1014 print _(' %(description)s (%(name)s)')%permission.__dict__
1015 return 0
1017 def run_command(self, args):
1018 '''Run a single command
1019 '''
1020 command = args[0]
1022 # handle help now
1023 if command == 'help':
1024 if len(args)>1:
1025 self.do_help(args[1:])
1026 return 0
1027 self.do_help(['help'])
1028 return 0
1029 if command == 'morehelp':
1030 self.do_help(['help'])
1031 self.help_commands()
1032 self.help_all()
1033 return 0
1035 # figure what the command is
1036 try:
1037 functions = self.commands.get(command)
1038 except KeyError:
1039 # not a valid command
1040 print _('Unknown command "%(command)s" ("help commands" for a '
1041 'list)')%locals()
1042 return 1
1044 # check for multiple matches
1045 if len(functions) > 1:
1046 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1047 command, 'list': ', '.join([i[0] for i in functions])}
1048 return 1
1049 command, function = functions[0]
1051 # make sure we have an instance_home
1052 while not self.instance_home:
1053 self.instance_home = raw_input(_('Enter instance home: ')).strip()
1055 # before we open the db, we may be doing an install or init
1056 if command == 'initialise':
1057 try:
1058 return self.do_initialise(self.instance_home, args)
1059 except UsageError, message:
1060 print _('Error: %(message)s')%locals()
1061 return 1
1062 elif command == 'install':
1063 try:
1064 return self.do_install(self.instance_home, args)
1065 except UsageError, message:
1066 print _('Error: %(message)s')%locals()
1067 return 1
1069 # get the instance
1070 try:
1071 instance = roundup.instance.open(self.instance_home)
1072 except ValueError, message:
1073 self.instance_home = ''
1074 print _("Error: Couldn't open instance: %(message)s")%locals()
1075 return 1
1077 # only open the database once!
1078 if not self.db:
1079 self.db = instance.open('admin')
1081 # do the command
1082 ret = 0
1083 try:
1084 ret = function(args[1:])
1085 except UsageError, message:
1086 print _('Error: %(message)s')%locals()
1087 print
1088 print function.__doc__
1089 ret = 1
1090 except:
1091 import traceback
1092 traceback.print_exc()
1093 ret = 1
1094 return ret
1096 def interactive(self):
1097 '''Run in an interactive mode
1098 '''
1099 print _('Roundup %s ready for input.'%roundup_version)
1100 print _('Type "help" for help.')
1101 try:
1102 import readline
1103 except ImportError:
1104 print _('Note: command history and editing not available')
1106 while 1:
1107 try:
1108 command = raw_input(_('roundup> '))
1109 except EOFError:
1110 print _('exit...')
1111 break
1112 if not command: continue
1113 args = token.token_split(command)
1114 if not args: continue
1115 if args[0] in ('quit', 'exit'): break
1116 self.run_command(args)
1118 # exit.. check for transactions
1119 if self.db and self.db.transactions:
1120 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1121 if commit and commit[0].lower() == 'y':
1122 self.db.commit()
1123 return 0
1125 def main(self):
1126 try:
1127 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1128 except getopt.GetoptError, e:
1129 self.usage(str(e))
1130 return 1
1132 # handle command-line args
1133 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
1134 # TODO: reinstate the user/password stuff (-u arg too)
1135 name = password = ''
1136 if os.environ.has_key('ROUNDUP_LOGIN'):
1137 l = os.environ['ROUNDUP_LOGIN'].split(':')
1138 name = l[0]
1139 if len(l) > 1:
1140 password = l[1]
1141 self.comma_sep = 0
1142 for opt, arg in opts:
1143 if opt == '-h':
1144 self.usage()
1145 return 0
1146 if opt == '-i':
1147 self.instance_home = arg
1148 if opt == '-c':
1149 self.comma_sep = 1
1151 # if no command - go interactive
1152 ret = 0
1153 if not args:
1154 self.interactive()
1155 else:
1156 ret = self.run_command(args)
1157 if self.db: self.db.commit()
1158 return ret
1161 if __name__ == '__main__':
1162 tool = AdminTool()
1163 sys.exit(tool.main())
1165 #
1166 # $Log: not supported by cvs2svn $
1167 # Revision 1.20 2002/08/01 00:56:22 richard
1168 # Added the web access and email access permissions, so people can restrict
1169 # access to users who register through the email interface (for example).
1170 # Also added "security" command to the roundup-admin interface to display the
1171 # Role/Permission config for an instance.
1172 #
1173 # Revision 1.19 2002/07/25 07:14:05 richard
1174 # Bugger it. Here's the current shape of the new security implementation.
1175 # Still to do:
1176 # . call the security funcs from cgi and mailgw
1177 # . change shipped templates to include correct initialisation and remove
1178 # the old config vars
1179 # ... that seems like a lot. The bulk of the work has been done though. Honest :)
1180 #
1181 # Revision 1.18 2002/07/18 11:17:30 gmcm
1182 # Add Number and Boolean types to hyperdb.
1183 # Add conversion cases to web, mail & admin interfaces.
1184 # Add storage/serialization cases to back_anydbm & back_metakit.
1185 #
1186 # Revision 1.17 2002/07/14 06:05:50 richard
1187 # . fixed the date module so that Date(". - 2d") works
1188 #
1189 # Revision 1.16 2002/07/09 04:19:09 richard
1190 # Added reindex command to roundup-admin.
1191 # Fixed reindex on first access.
1192 # Also fixed reindexing of entries that change.
1193 #
1194 # Revision 1.15 2002/06/17 23:14:44 richard
1195 # . #569415 ] {version}
1196 #
1197 # Revision 1.14 2002/06/11 06:41:50 richard
1198 # Removed prompt for admin email in initialisation.
1199 #
1200 # Revision 1.13 2002/05/30 23:58:14 richard
1201 # oops
1202 #
1203 # Revision 1.12 2002/05/26 09:04:42 richard
1204 # out by one in the init args
1205 #
1206 # Revision 1.11 2002/05/23 01:14:20 richard
1207 # . split instance initialisation into two steps, allowing config changes
1208 # before the database is initialised.
1209 #
1210 # Revision 1.10 2002/04/27 10:07:23 richard
1211 # minor fix to error message
1212 #
1213 # Revision 1.9 2002/03/12 22:51:47 richard
1214 # . #527416 ] roundup-admin uses undefined value
1215 # . #527503 ] unfriendly init blowup when parent dir
1216 # (also handles UsageError correctly now in init)
1217 #
1218 # Revision 1.8 2002/02/27 03:28:21 richard
1219 # Ran it through pychecker, made fixes
1220 #
1221 # Revision 1.7 2002/02/20 05:04:32 richard
1222 # Wasn't handling the cvs parser feeding properly.
1223 #
1224 # Revision 1.6 2002/01/23 07:27:19 grubert
1225 # . allow abbreviation of "help" in admin tool too.
1226 #
1227 # Revision 1.5 2002/01/21 16:33:19 rochecompaan
1228 # You can now use the roundup-admin tool to pack the database
1229 #
1230 # Revision 1.4 2002/01/14 06:51:09 richard
1231 # . #503164 ] create and passwords
1232 #
1233 # Revision 1.3 2002/01/08 05:26:32 rochecompaan
1234 # Missing "self" in props_from_args
1235 #
1236 # Revision 1.2 2002/01/07 10:41:44 richard
1237 # #500140 ] AdminTool.get_class() returns nothing
1238 #
1239 # Revision 1.1 2002/01/05 02:11:22 richard
1240 # I18N'ed roundup admin - and split the code off into a module so it can be used
1241 # elsewhere.
1242 # Big issue with this is the doc strings - that's the help. We're probably going to
1243 # have to switch to not use docstrings, which will suck a little :(
1244 #
1245 #
1246 #
1247 # vim: set filetype=python ts=4 sw=4 et si