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.34 2002-09-26 07:41:54 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.tracker_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 if value:
85 props[key] = value
86 else:
87 props[key] = None
88 return props
90 def usage(self, message=''):
91 if message:
92 message = _('Problem: %(message)s)\n\n')%locals()
93 print _('''%(message)sUsage: roundup-admin [options] <command> <arguments>
95 Options:
96 -i instance home -- specify the issue tracker "home directory" to administer
97 -u -- the user[:password] to use for commands
98 -c -- when outputting lists of data, just comma-separate them
100 Help:
101 roundup-admin -h
102 roundup-admin help -- this help
103 roundup-admin help <command> -- command-specific help
104 roundup-admin help all -- all available help
105 ''')%locals()
106 self.help_commands()
108 def help_commands(self):
109 print _('Commands:'),
110 commands = ['']
111 for command in self.commands.values():
112 h = command.__doc__.split('\n')[0]
113 commands.append(' '+h[7:])
114 commands.sort()
115 commands.append(_('Commands may be abbreviated as long as the abbreviation matches only one'))
116 commands.append(_('command, e.g. l == li == lis == list.'))
117 print '\n'.join(commands)
118 print
120 def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
121 commands = self.commands.values()
122 def sortfun(a, b):
123 return cmp(a.__name__, b.__name__)
124 commands.sort(sortfun)
125 for command in commands:
126 h = command.__doc__.split('\n')
127 name = command.__name__[3:]
128 usage = h[0]
129 print _('''
130 <tr><td valign=top><strong>%(name)s</strong></td>
131 <td><tt>%(usage)s</tt><p>
132 <pre>''')%locals()
133 indent = indent_re.match(h[3])
134 if indent: indent = len(indent.group(1))
135 for line in h[3:]:
136 if indent:
137 print line[indent:]
138 else:
139 print line
140 print _('</pre></td></tr>\n')
142 def help_all(self):
143 print _('''
144 All commands (except help) require a tracker specifier. This is just the path
145 to the roundup tracker you're working with. A roundup tracker is where
146 roundup keeps the database and configuration file that defines an issue
147 tracker. It may be thought of as the issue tracker's "home directory". It may
148 be specified in the environment variable TRACKER_HOME or on the command
149 line as "-i tracker".
151 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
153 Property values are represented as strings in command arguments and in the
154 printed results:
155 . Strings are, well, strings.
156 . Date values are printed in the full date format in the local time zone, and
157 accepted in the full format or any of the partial formats explained below.
158 . Link values are printed as node designators. When given as an argument,
159 node designators and key strings are both accepted.
160 . Multilink values are printed as lists of node designators joined by commas.
161 When given as an argument, node designators and key strings are both
162 accepted; an empty string, a single node, or a list of nodes joined by
163 commas is accepted.
165 When property values must contain spaces, just surround the value with
166 quotes, either ' or ". A single space may also be backslash-quoted. If a
167 valuu must contain a quote character, it must be backslash-quoted or inside
168 quotes. Examples:
169 hello world (2 tokens: hello, world)
170 "hello world" (1 token: hello world)
171 "Roch'e" Compaan (2 tokens: Roch'e Compaan)
172 Roch\'e Compaan (2 tokens: Roch'e Compaan)
173 address="1 2 3" (1 token: address=1 2 3)
174 \\ (1 token: \)
175 \n\r\t (1 token: a newline, carriage-return and tab)
177 When multiple nodes are specified to the roundup get or roundup set
178 commands, the specified properties are retrieved or set on all the listed
179 nodes.
181 When multiple results are returned by the roundup get or roundup find
182 commands, they are printed one per line (default) or joined by commas (with
183 the -c) option.
185 Where the command changes data, a login name/password is required. The
186 login may be specified as either "name" or "name:password".
187 . ROUNDUP_LOGIN environment variable
188 . the -u command-line option
189 If either the name or password is not supplied, they are obtained from the
190 command-line.
192 Date format examples:
193 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
194 "2000-04-17" means <Date 2000-04-17.00:00:00>
195 "01-25" means <Date yyyy-01-25.00:00:00>
196 "08-13.22:13" means <Date yyyy-08-14.03:13:00>
197 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
198 "14:25" means <Date yyyy-mm-dd.19:25:00>
199 "8:47:11" means <Date yyyy-mm-dd.13:47:11>
200 "." means "right now"
202 Command help:
203 ''')
204 for name, command in self.commands.items():
205 print _('%s:')%name
206 print _(' '), command.__doc__
208 def do_help(self, args, nl_re=re.compile('[\r\n]'),
209 indent_re=re.compile(r'^(\s+)\S+')):
210 '''Usage: help topic
211 Give help about topic.
213 commands -- list commands
214 <command> -- help specific to a command
215 initopts -- init command options
216 all -- all available help
217 '''
218 if len(args)>0:
219 topic = args[0]
220 else:
221 topic = 'help'
224 # try help_ methods
225 if self.help.has_key(topic):
226 self.help[topic]()
227 return 0
229 # try command docstrings
230 try:
231 l = self.commands.get(topic)
232 except KeyError:
233 print _('Sorry, no help for "%(topic)s"')%locals()
234 return 1
236 # display the help for each match, removing the docsring indent
237 for name, help in l:
238 lines = nl_re.split(help.__doc__)
239 print lines[0]
240 indent = indent_re.match(lines[1])
241 if indent: indent = len(indent.group(1))
242 for line in lines[1:]:
243 if indent:
244 print line[indent:]
245 else:
246 print line
247 return 0
249 def help_initopts(self):
250 import roundup.templates
251 templates = roundup.templates.listTemplates()
252 print _('Templates:'), ', '.join(templates)
253 import roundup.backends
254 backends = roundup.backends.__all__
255 print _('Back ends:'), ', '.join(backends)
257 def do_install(self, tracker_home, args):
258 '''Usage: install [template [backend [admin password]]]
259 Install a new Roundup tracker.
261 The command will prompt for the tracker home directory (if not supplied
262 through TRACKER_HOME or the -i option). The template, backend and admin
263 password may be specified on the command-line as arguments, in that
264 order.
266 The initialise command must be called after this command in order
267 to initialise the tracker's database. You may edit the tracker's
268 initial database contents before running that command by editing
269 the tracker's dbinit.py module init() function.
271 See also initopts help.
272 '''
273 if len(args) < 1:
274 raise UsageError, _('Not enough arguments supplied')
276 # make sure the tracker home can be created
277 parent = os.path.split(tracker_home)[0]
278 if not os.path.exists(parent):
279 raise UsageError, _('Instance home parent directory "%(parent)s"'
280 ' does not exist')%locals()
282 # select template
283 import roundup.templates
284 templates = roundup.templates.listTemplates()
285 template = len(args) > 1 and args[1] or ''
286 if template not in templates:
287 print _('Templates:'), ', '.join(templates)
288 while template not in templates:
289 template = raw_input(_('Select template [classic]: ')).strip()
290 if not template:
291 template = 'classic'
293 # select hyperdb backend
294 import roundup.backends
295 backends = roundup.backends.__all__
296 backend = len(args) > 2 and args[2] or ''
297 if backend not in backends:
298 print _('Back ends:'), ', '.join(backends)
299 while backend not in backends:
300 backend = raw_input(_('Select backend [anydbm]: ')).strip()
301 if not backend:
302 backend = 'anydbm'
303 # XXX perform a unit test based on the user's selections
305 # install!
306 init.install(tracker_home, template, backend)
308 print _('''
309 You should now edit the tracker configuration file:
310 %(config_file)s
311 ... at a minimum, you must set MAILHOST, MAIL_DOMAIN and ADMIN_EMAIL.
313 If you wish to modify the default schema, you should also edit the database
314 initialisation file:
315 %(database_config_file)s
316 ... see the documentation on customizing for more information.
317 ''')%{
318 'config_file': os.path.join(tracker_home, 'config.py'),
319 'database_config_file': os.path.join(tracker_home, 'dbinit.py')
320 }
321 return 0
324 def do_initialise(self, tracker_home, args):
325 '''Usage: initialise [adminpw]
326 Initialise a new Roundup tracker.
328 The administrator details will be set at this step.
330 Execute the tracker's initialisation function dbinit.init()
331 '''
332 # password
333 if len(args) > 1:
334 adminpw = args[1]
335 else:
336 adminpw = ''
337 confirm = 'x'
338 while adminpw != confirm:
339 adminpw = getpass.getpass(_('Admin Password: '))
340 confirm = getpass.getpass(_(' Confirm: '))
342 # make sure the tracker home is installed
343 if not os.path.exists(tracker_home):
344 raise UsageError, _('Instance home does not exist')%locals()
345 if not os.path.exists(os.path.join(tracker_home, 'html')):
346 raise UsageError, _('Instance has not been installed')%locals()
348 # is there already a database?
349 if os.path.exists(os.path.join(tracker_home, 'db')):
350 print _('WARNING: The database is already initialised!')
351 print _('If you re-initialise it, you will lose all the data!')
352 ok = raw_input(_('Erase it? Y/[N]: ')).strip()
353 if ok.lower() != 'y':
354 return 0
356 # nuke it
357 shutil.rmtree(os.path.join(tracker_home, 'db'))
359 # GO
360 init.initialise(tracker_home, adminpw)
362 return 0
365 def do_get(self, args):
366 '''Usage: get property designator[,designator]*
367 Get the given property of one or more designator(s).
369 Retrieves the property value of the nodes specified by the designators.
370 '''
371 if len(args) < 2:
372 raise UsageError, _('Not enough arguments supplied')
373 propname = args[0]
374 designators = args[1].split(',')
375 l = []
376 for designator in designators:
377 # decode the node designator
378 try:
379 classname, nodeid = hyperdb.splitDesignator(designator)
380 except hyperdb.DesignatorError, message:
381 raise UsageError, message
383 # get the class
384 cl = self.get_class(classname)
385 try:
386 if self.comma_sep:
387 l.append(cl.get(nodeid, propname))
388 else:
389 print cl.get(nodeid, propname)
390 except IndexError:
391 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
392 except KeyError:
393 raise UsageError, _('no such %(classname)s property '
394 '"%(propname)s"')%locals()
395 if self.comma_sep:
396 print ','.join(l)
397 return 0
400 def do_set(self, args, pwre = re.compile(r'{(\w+)}(.+)')):
401 '''Usage: set [items] property=value property=value ...
402 Set the given properties of one or more items(s).
404 The items may be specified as a class or as a comma-separeted
405 list of item designators (ie "designator[,designator,...]").
407 This command sets the properties to the values for all designators
408 given. If the value is missing (ie. "property=") then the property is
409 un-set.
410 '''
411 if len(args) < 2:
412 raise UsageError, _('Not enough arguments supplied')
413 from roundup import hyperdb
415 designators = args[0].split(',')
416 if len(designators) == 1:
417 designator = designators[0]
418 try:
419 designator = hyperdb.splitDesignator(designator)
420 designators = [designator]
421 except hyperdb.DesignatorError:
422 cl = self.get_class(designator)
423 designators = [(designator, x) for x in cl.list()]
424 else:
425 try:
426 designators = [hyperdb.splitDesignator(x) for x in designators]
427 except hyperdb.DesignatorError, message:
428 raise UsageError, message
430 # get the props from the args
431 props = self.props_from_args(args[1:])
433 # now do the set for all the nodes
434 for classname, itemid in designators:
435 cl = self.get_class(classname)
437 properties = cl.getprops()
438 for key, value in props.items():
439 proptype = properties[key]
440 if isinstance(proptype, hyperdb.Multilink):
441 if value is None:
442 props[key] = []
443 else:
444 props[key] = value.split(',')
445 elif value is None:
446 continue
447 elif isinstance(proptype, hyperdb.String):
448 continue
449 elif isinstance(proptype, hyperdb.Password):
450 m = pwre.match(value)
451 if m:
452 # password is being given to us encrypted
453 p = password.Password()
454 p.scheme = m.group(1)
455 p.password = m.group(2)
456 props[key] = p
457 else:
458 props[key] = password.Password(value)
459 elif isinstance(proptype, hyperdb.Date):
460 try:
461 props[key] = date.Date(value)
462 except ValueError, message:
463 raise UsageError, '"%s": %s'%(value, message)
464 elif isinstance(proptype, hyperdb.Interval):
465 try:
466 props[key] = date.Interval(value)
467 except ValueError, message:
468 raise UsageError, '"%s": %s'%(value, message)
469 elif isinstance(proptype, hyperdb.Link):
470 props[key] = value
471 elif isinstance(proptype, hyperdb.Boolean):
472 props[key] = value.lower() in ('yes', 'true', 'on', '1')
473 elif isinstance(proptype, hyperdb.Number):
474 props[key] = int(value)
476 # try the set
477 try:
478 apply(cl.set, (itemid, ), props)
479 except (TypeError, IndexError, ValueError), message:
480 import traceback; traceback.print_exc()
481 raise UsageError, message
482 return 0
484 def do_find(self, args):
485 '''Usage: find classname propname=value ...
486 Find the nodes of the given class with a given link property value.
488 Find the nodes of the given class with a given link property value. The
489 value may be either the nodeid of the linked node, or its key value.
490 '''
491 if len(args) < 1:
492 raise UsageError, _('Not enough arguments supplied')
493 classname = args[0]
494 # get the class
495 cl = self.get_class(classname)
497 # handle the propname=value argument
498 props = self.props_from_args(args[1:])
500 # if the value isn't a number, look up the linked class to get the
501 # number
502 for propname, value in props.items():
503 num_re = re.compile('^\d+$')
504 if not num_re.match(value):
505 # get the property
506 try:
507 property = cl.properties[propname]
508 except KeyError:
509 raise UsageError, _('%(classname)s has no property '
510 '"%(propname)s"')%locals()
512 # make sure it's a link
513 if (not isinstance(property, hyperdb.Link) and not
514 isinstance(property, hyperdb.Multilink)):
515 raise UsageError, _('You may only "find" link properties')
517 # get the linked-to class and look up the key property
518 link_class = self.db.getclass(property.classname)
519 try:
520 props[propname] = link_class.lookup(value)
521 except TypeError:
522 raise UsageError, _('%(classname)s has no key property"')%{
523 'classname': link_class.classname}
525 # now do the find
526 try:
527 if self.comma_sep:
528 print ','.join(apply(cl.find, (), props))
529 else:
530 print apply(cl.find, (), props)
531 except KeyError:
532 raise UsageError, _('%(classname)s has no property '
533 '"%(propname)s"')%locals()
534 except (ValueError, TypeError), message:
535 raise UsageError, message
536 return 0
538 def do_specification(self, args):
539 '''Usage: specification classname
540 Show the properties for a classname.
542 This lists the properties for a given class.
543 '''
544 if len(args) < 1:
545 raise UsageError, _('Not enough arguments supplied')
546 classname = args[0]
547 # get the class
548 cl = self.get_class(classname)
550 # get the key property
551 keyprop = cl.getkey()
552 for key, value in cl.properties.items():
553 if keyprop == key:
554 print _('%(key)s: %(value)s (key property)')%locals()
555 else:
556 print _('%(key)s: %(value)s')%locals()
558 def do_display(self, args):
559 '''Usage: display designator
560 Show the property values for the given node.
562 This lists the properties and their associated values for the given
563 node.
564 '''
565 if len(args) < 1:
566 raise UsageError, _('Not enough arguments supplied')
568 # decode the node designator
569 try:
570 classname, nodeid = hyperdb.splitDesignator(args[0])
571 except hyperdb.DesignatorError, message:
572 raise UsageError, message
574 # get the class
575 cl = self.get_class(classname)
577 # display the values
578 for key in cl.properties.keys():
579 value = cl.get(nodeid, key)
580 print _('%(key)s: %(value)s')%locals()
582 def do_create(self, args, pwre = re.compile(r'{(\w+)}(.+)')):
583 '''Usage: create classname property=value ...
584 Create a new entry of a given class.
586 This creates a new entry of the given class using the property
587 name=value arguments provided on the command line after the "create"
588 command.
589 '''
590 if len(args) < 1:
591 raise UsageError, _('Not enough arguments supplied')
592 from roundup import hyperdb
594 classname = args[0]
596 # get the class
597 cl = self.get_class(classname)
599 # now do a create
600 props = {}
601 properties = cl.getprops(protected = 0)
602 if len(args) == 1:
603 # ask for the properties
604 for key, value in properties.items():
605 if key == 'id': continue
606 name = value.__class__.__name__
607 if isinstance(value , hyperdb.Password):
608 again = None
609 while value != again:
610 value = getpass.getpass(_('%(propname)s (Password): ')%{
611 'propname': key.capitalize()})
612 again = getpass.getpass(_(' %(propname)s (Again): ')%{
613 'propname': key.capitalize()})
614 if value != again: print _('Sorry, try again...')
615 if value:
616 props[key] = value
617 else:
618 value = raw_input(_('%(propname)s (%(proptype)s): ')%{
619 'propname': key.capitalize(), 'proptype': name})
620 if value:
621 props[key] = value
622 else:
623 props = self.props_from_args(args[1:])
625 # convert types
626 for propname, value in props.items():
627 # get the property
628 try:
629 proptype = properties[propname]
630 except KeyError:
631 raise UsageError, _('%(classname)s has no property '
632 '"%(propname)s"')%locals()
634 if isinstance(proptype, hyperdb.Date):
635 try:
636 props[propname] = date.Date(value)
637 except ValueError, message:
638 raise UsageError, _('"%(value)s": %(message)s')%locals()
639 elif isinstance(proptype, hyperdb.Interval):
640 try:
641 props[propname] = date.Interval(value)
642 except ValueError, message:
643 raise UsageError, _('"%(value)s": %(message)s')%locals()
644 elif isinstance(proptype, hyperdb.Password):
645 m = pwre.match(value)
646 if m:
647 # password is being given to us encrypted
648 p = password.Password()
649 p.scheme = m.group(1)
650 p.password = m.group(2)
651 props[propname] = p
652 else:
653 props[propname] = password.Password(value)
654 elif isinstance(proptype, hyperdb.Multilink):
655 props[propname] = value.split(',')
656 elif isinstance(proptype, hyperdb.Boolean):
657 props[propname] = value.lower() in ('yes', 'true', 'on', '1')
658 elif isinstance(proptype, hyperdb.Number):
659 props[propname] = int(value)
661 # check for the key property
662 propname = cl.getkey()
663 if propname and not props.has_key(propname):
664 raise UsageError, _('you must provide the "%(propname)s" '
665 'property.')%locals()
667 # do the actual create
668 try:
669 print apply(cl.create, (), props)
670 except (TypeError, IndexError, ValueError), message:
671 raise UsageError, message
672 return 0
674 def do_list(self, args):
675 '''Usage: list classname [property]
676 List the instances of a class.
678 Lists all instances of the given class. If the property is not
679 specified, the "label" property is used. The label property is tried
680 in order: the key, "name", "title" and then the first property,
681 alphabetically.
682 '''
683 if len(args) < 1:
684 raise UsageError, _('Not enough arguments supplied')
685 classname = args[0]
687 # get the class
688 cl = self.get_class(classname)
690 # figure the property
691 if len(args) > 1:
692 propname = args[1]
693 else:
694 propname = cl.labelprop()
696 if self.comma_sep:
697 print ','.join(cl.list())
698 else:
699 for nodeid in cl.list():
700 try:
701 value = cl.get(nodeid, propname)
702 except KeyError:
703 raise UsageError, _('%(classname)s has no property '
704 '"%(propname)s"')%locals()
705 print _('%(nodeid)4s: %(value)s')%locals()
706 return 0
708 def do_table(self, args):
709 '''Usage: table classname [property[,property]*]
710 List the instances of a class in tabular form.
712 Lists all instances of the given class. If the properties are not
713 specified, all properties are displayed. By default, the column widths
714 are the width of the property names. The width may be explicitly defined
715 by defining the property as "name:width". For example::
716 roundup> table priority id,name:10
717 Id Name
718 1 fatal-bug
719 2 bug
720 3 usability
721 4 feature
722 '''
723 if len(args) < 1:
724 raise UsageError, _('Not enough arguments supplied')
725 classname = args[0]
727 # get the class
728 cl = self.get_class(classname)
730 # figure the property names to display
731 if len(args) > 1:
732 prop_names = args[1].split(',')
733 all_props = cl.getprops()
734 for spec in prop_names:
735 if ':' in spec:
736 try:
737 propname, width = spec.split(':')
738 except (ValueError, TypeError):
739 raise UsageError, _('"%(spec)s" not name:width')%locals()
740 else:
741 propname = spec
742 if not all_props.has_key(propname):
743 raise UsageError, _('%(classname)s has no property '
744 '"%(propname)s"')%locals()
745 else:
746 prop_names = cl.getprops().keys()
748 # now figure column widths
749 props = []
750 for spec in prop_names:
751 if ':' in spec:
752 name, width = spec.split(':')
753 props.append((name, int(width)))
754 else:
755 props.append((spec, len(spec)))
757 # now display the heading
758 print ' '.join([name.capitalize().ljust(width) for name,width in props])
760 # and the table data
761 for nodeid in cl.list():
762 l = []
763 for name, width in props:
764 if name != 'id':
765 try:
766 value = str(cl.get(nodeid, name))
767 except KeyError:
768 # we already checked if the property is valid - a
769 # KeyError here means the node just doesn't have a
770 # value for it
771 value = ''
772 else:
773 value = str(nodeid)
774 f = '%%-%ds'%width
775 l.append(f%value[:width])
776 print ' '.join(l)
777 return 0
779 def do_history(self, args):
780 '''Usage: history designator
781 Show the history entries of a designator.
783 Lists the journal entries for the node identified by the designator.
784 '''
785 if len(args) < 1:
786 raise UsageError, _('Not enough arguments supplied')
787 try:
788 classname, nodeid = hyperdb.splitDesignator(args[0])
789 except hyperdb.DesignatorError, message:
790 raise UsageError, message
792 try:
793 print self.db.getclass(classname).history(nodeid)
794 except KeyError:
795 raise UsageError, _('no such class "%(classname)s"')%locals()
796 except IndexError:
797 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
798 return 0
800 def do_commit(self, args):
801 '''Usage: commit
802 Commit all changes made to the database.
804 The changes made during an interactive session are not
805 automatically written to the database - they must be committed
806 using this command.
808 One-off commands on the command-line are automatically committed if
809 they are successful.
810 '''
811 self.db.commit()
812 return 0
814 def do_rollback(self, args):
815 '''Usage: rollback
816 Undo all changes that are pending commit to the database.
818 The changes made during an interactive session are not
819 automatically written to the database - they must be committed
820 manually. This command undoes all those changes, so a commit
821 immediately after would make no changes to the database.
822 '''
823 self.db.rollback()
824 return 0
826 def do_retire(self, args):
827 '''Usage: retire designator[,designator]*
828 Retire the node specified by designator.
830 This action indicates that a particular node is not to be retrieved by
831 the list or find commands, and its key value may be re-used.
832 '''
833 if len(args) < 1:
834 raise UsageError, _('Not enough arguments supplied')
835 designators = args[0].split(',')
836 for designator in designators:
837 try:
838 classname, nodeid = hyperdb.splitDesignator(designator)
839 except hyperdb.DesignatorError, message:
840 raise UsageError, message
841 try:
842 self.db.getclass(classname).retire(nodeid)
843 except KeyError:
844 raise UsageError, _('no such class "%(classname)s"')%locals()
845 except IndexError:
846 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
847 return 0
849 def do_export(self, args):
850 '''Usage: export [class[,class]] export_dir
851 Export the database to colon-separated-value files.
853 This action exports the current data from the database into
854 colon-separated-value files that are placed in the nominated
855 destination directory. The journals are not exported.
856 '''
857 # we need the CSV module
858 if csv is None:
859 raise UsageError, \
860 _('Sorry, you need the csv module to use this function.\n'
861 'Get it from: http://www.object-craft.com.au/projects/csv/')
863 # grab the directory to export to
864 if len(args) < 1:
865 raise UsageError, _('Not enough arguments supplied')
866 dir = args[-1]
868 # get the list of classes to export
869 if len(args) == 2:
870 classes = args[0].split(',')
871 else:
872 classes = self.db.classes.keys()
874 # use the csv parser if we can - it's faster
875 p = csv.parser(field_sep=':')
877 # do all the classes specified
878 for classname in classes:
879 cl = self.get_class(classname)
880 f = open(os.path.join(dir, classname+'.csv'), 'w')
881 properties = cl.getprops()
882 propnames = properties.keys()
883 propnames.sort()
884 print >> f, p.join(propnames)
886 # all nodes for this class
887 for nodeid in cl.list():
888 print >>f, p.join(cl.export_list(propnames, nodeid))
889 return 0
891 def do_import(self, args):
892 '''Usage: import import_dir
893 Import a database from the directory containing CSV files, one per
894 class to import.
896 The files must define the same properties as the class (including having
897 a "header" line with those property names.)
899 The imported nodes will have the same nodeid as defined in the
900 import file, thus replacing any existing content.
902 The new nodes are added to the existing database - if you want to
903 create a new database using the imported data, then create a new
904 database (or, tediously, retire all the old data.)
905 '''
906 if len(args) < 1:
907 raise UsageError, _('Not enough arguments supplied')
908 if csv is None:
909 raise UsageError, \
910 _('Sorry, you need the csv module to use this function.\n'
911 'Get it from: http://www.object-craft.com.au/projects/csv/')
913 from roundup import hyperdb
915 for file in os.listdir(args[0]):
916 f = open(os.path.join(args[0], file))
918 # get the classname
919 classname = os.path.splitext(file)[0]
921 # ensure that the properties and the CSV file headings match
922 cl = self.get_class(classname)
923 p = csv.parser(field_sep=':')
924 file_props = p.parse(f.readline())
925 properties = cl.getprops()
926 propnames = properties.keys()
927 propnames.sort()
928 m = file_props[:]
929 m.sort()
930 if m != propnames:
931 raise UsageError, _('Import file doesn\'t define the same '
932 'properties as "%(arg0)s".')%{'arg0': args[0]}
934 # loop through the file and create a node for each entry
935 maxid = 1
936 while 1:
937 line = f.readline()
938 if not line: break
940 # parse lines until we get a complete entry
941 while 1:
942 l = p.parse(line)
943 if l: break
944 line = f.readline()
945 if not line:
946 raise ValueError, "Unexpected EOF during CSV parse"
948 # do the import and figure the current highest nodeid
949 maxid = max(maxid, int(cl.import_list(propnames, l)))
951 print 'setting', classname, maxid+1
952 self.db.setid(classname, str(maxid+1))
953 return 0
955 def do_pack(self, args):
956 '''Usage: pack period | date
958 Remove journal entries older than a period of time specified or
959 before a certain date.
961 A period is specified using the suffixes "y", "m", and "d". The
962 suffix "w" (for "week") means 7 days.
964 "3y" means three years
965 "2y 1m" means two years and one month
966 "1m 25d" means one month and 25 days
967 "2w 3d" means two weeks and three days
969 Date format is "YYYY-MM-DD" eg:
970 2001-01-01
972 '''
973 if len(args) <> 1:
974 raise UsageError, _('Not enough arguments supplied')
976 # are we dealing with a period or a date
977 value = args[0]
978 date_re = re.compile(r'''
979 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
980 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
981 ''', re.VERBOSE)
982 m = date_re.match(value)
983 if not m:
984 raise ValueError, _('Invalid format')
985 m = m.groupdict()
986 if m['period']:
987 pack_before = date.Date(". - %s"%value)
988 elif m['date']:
989 pack_before = date.Date(value)
990 self.db.pack(pack_before)
991 return 0
993 def do_reindex(self, args):
994 '''Usage: reindex
995 Re-generate a tracker's search indexes.
997 This will re-generate the search indexes for a tracker. This will
998 typically happen automatically.
999 '''
1000 self.db.indexer.force_reindex()
1001 self.db.reindex()
1002 return 0
1004 def do_security(self, args):
1005 '''Usage: security [Role name]
1006 Display the Permissions available to one or all Roles.
1007 '''
1008 if len(args) == 1:
1009 role = args[0]
1010 try:
1011 roles = [(args[0], self.db.security.role[args[0]])]
1012 except KeyError:
1013 print _('No such Role "%(role)s"')%locals()
1014 return 1
1015 else:
1016 roles = self.db.security.role.items()
1017 role = self.db.config.NEW_WEB_USER_ROLES
1018 if ',' in role:
1019 print _('New Web users get the Roles "%(role)s"')%locals()
1020 else:
1021 print _('New Web users get the Role "%(role)s"')%locals()
1022 role = self.db.config.NEW_EMAIL_USER_ROLES
1023 if ',' in role:
1024 print _('New Email users get the Roles "%(role)s"')%locals()
1025 else:
1026 print _('New Email users get the Role "%(role)s"')%locals()
1027 roles.sort()
1028 for rolename, role in roles:
1029 print _('Role "%(name)s":')%role.__dict__
1030 for permission in role.permissions:
1031 if permission.klass:
1032 print _(' %(description)s (%(name)s for "%(klass)s" '
1033 'only)')%permission.__dict__
1034 else:
1035 print _(' %(description)s (%(name)s)')%permission.__dict__
1036 return 0
1038 def run_command(self, args):
1039 '''Run a single command
1040 '''
1041 command = args[0]
1043 # handle help now
1044 if command == 'help':
1045 if len(args)>1:
1046 self.do_help(args[1:])
1047 return 0
1048 self.do_help(['help'])
1049 return 0
1050 if command == 'morehelp':
1051 self.do_help(['help'])
1052 self.help_commands()
1053 self.help_all()
1054 return 0
1056 # figure what the command is
1057 try:
1058 functions = self.commands.get(command)
1059 except KeyError:
1060 # not a valid command
1061 print _('Unknown command "%(command)s" ("help commands" for a '
1062 'list)')%locals()
1063 return 1
1065 # check for multiple matches
1066 if len(functions) > 1:
1067 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1068 command, 'list': ', '.join([i[0] for i in functions])}
1069 return 1
1070 command, function = functions[0]
1072 # make sure we have a tracker_home
1073 while not self.tracker_home:
1074 self.tracker_home = raw_input(_('Enter tracker home: ')).strip()
1076 # before we open the db, we may be doing an install or init
1077 if command == 'initialise':
1078 try:
1079 return self.do_initialise(self.tracker_home, args)
1080 except UsageError, message:
1081 print _('Error: %(message)s')%locals()
1082 return 1
1083 elif command == 'install':
1084 try:
1085 return self.do_install(self.tracker_home, args)
1086 except UsageError, message:
1087 print _('Error: %(message)s')%locals()
1088 return 1
1090 # get the tracker
1091 try:
1092 tracker = roundup.instance.open(self.tracker_home)
1093 except ValueError, message:
1094 self.tracker_home = ''
1095 print _("Error: Couldn't open tracker: %(message)s")%locals()
1096 return 1
1098 # only open the database once!
1099 if not self.db:
1100 self.db = tracker.open('admin')
1102 # do the command
1103 ret = 0
1104 try:
1105 ret = function(args[1:])
1106 except UsageError, message:
1107 print _('Error: %(message)s')%locals()
1108 print
1109 print function.__doc__
1110 ret = 1
1111 except:
1112 import traceback
1113 traceback.print_exc()
1114 ret = 1
1115 return ret
1117 def interactive(self):
1118 '''Run in an interactive mode
1119 '''
1120 print _('Roundup %s ready for input.'%roundup_version)
1121 print _('Type "help" for help.')
1122 try:
1123 import readline
1124 except ImportError:
1125 print _('Note: command history and editing not available')
1127 while 1:
1128 try:
1129 command = raw_input(_('roundup> '))
1130 except EOFError:
1131 print _('exit...')
1132 break
1133 if not command: continue
1134 args = token.token_split(command)
1135 if not args: continue
1136 if args[0] in ('quit', 'exit'): break
1137 self.run_command(args)
1139 # exit.. check for transactions
1140 if self.db and self.db.transactions:
1141 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1142 if commit and commit[0].lower() == 'y':
1143 self.db.commit()
1144 return 0
1146 def main(self):
1147 try:
1148 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1149 except getopt.GetoptError, e:
1150 self.usage(str(e))
1151 return 1
1153 # handle command-line args
1154 self.tracker_home = os.environ.get('TRACKER_HOME', '')
1155 # TODO: reinstate the user/password stuff (-u arg too)
1156 name = password = ''
1157 if os.environ.has_key('ROUNDUP_LOGIN'):
1158 l = os.environ['ROUNDUP_LOGIN'].split(':')
1159 name = l[0]
1160 if len(l) > 1:
1161 password = l[1]
1162 self.comma_sep = 0
1163 for opt, arg in opts:
1164 if opt == '-h':
1165 self.usage()
1166 return 0
1167 if opt == '-i':
1168 self.tracker_home = arg
1169 if opt == '-c':
1170 self.comma_sep = 1
1172 # if no command - go interactive
1173 # wrap in a try/finally so we always close off the db
1174 ret = 0
1175 try:
1176 if not args:
1177 self.interactive()
1178 else:
1179 ret = self.run_command(args)
1180 if self.db: self.db.commit()
1181 return ret
1182 finally:
1183 if self.db:
1184 self.db.close()
1186 if __name__ == '__main__':
1187 tool = AdminTool()
1188 sys.exit(tool.main())
1190 # vim: set filetype=python ts=4 sw=4 et si