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