687797315566ddbe7f04a195f44fb4a2d4f8aee2
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.40 2003-02-28 03:33:46 richard Exp $
21 '''Administration commands for maintaining Roundup trackers.
22 '''
24 import sys, os, getpass, getopt, re, UserDict, shlex, shutil
25 try:
26 import csv
27 except ImportError:
28 csv = None
29 from roundup import date, hyperdb, roundupdb, init, password, token
30 from roundup import __version__ as roundup_version
31 import roundup.instance
32 from roundup.i18n import _
34 class CommandDict(UserDict.UserDict):
35 '''Simple dictionary that lets us do lookups using partial keys.
37 Original code submitted by Engelbert Gruber.
38 '''
39 _marker = []
40 def get(self, key, default=_marker):
41 if self.data.has_key(key):
42 return [(key, self.data[key])]
43 keylist = self.data.keys()
44 keylist.sort()
45 l = []
46 for ki in keylist:
47 if ki.startswith(key):
48 l.append((ki, self.data[ki]))
49 if not l and default is self._marker:
50 raise KeyError, key
51 return l
53 class UsageError(ValueError):
54 pass
56 class AdminTool:
57 ''' A collection of methods used in maintaining Roundup trackers.
59 Typically these methods are accessed through the roundup-admin
60 script. The main() method provided on this class gives the main
61 loop for the roundup-admin script.
63 Actions are defined by do_*() methods, with help for the action
64 given in the method docstring.
66 Additional help may be supplied by help_*() methods.
67 '''
68 def __init__(self):
69 self.commands = CommandDict()
70 for k in AdminTool.__dict__.keys():
71 if k[:3] == 'do_':
72 self.commands[k[3:]] = getattr(self, k)
73 self.help = {}
74 for k in AdminTool.__dict__.keys():
75 if k[:5] == 'help_':
76 self.help[k[5:]] = getattr(self, k)
77 self.tracker_home = ''
78 self.db = None
80 def get_class(self, classname):
81 '''Get the class - raise an exception if it doesn't exist.
82 '''
83 try:
84 return self.db.getclass(classname)
85 except KeyError:
86 raise UsageError, _('no such class "%(classname)s"')%locals()
88 def props_from_args(self, args):
89 ''' Produce a dictionary of prop: value from the args list.
91 The args list is specified as ``prop=value prop=value ...``.
92 '''
93 props = {}
94 for arg in args:
95 if arg.find('=') == -1:
96 raise UsageError, _('argument "%(arg)s" not propname=value'
97 )%locals()
98 try:
99 key, value = arg.split('=')
100 except ValueError:
101 raise UsageError, _('argument "%(arg)s" not propname=value'
102 )%locals()
103 if value:
104 props[key] = value
105 else:
106 props[key] = None
107 return props
109 def usage(self, message=''):
110 ''' Display a simple usage message.
111 '''
112 if message:
113 message = _('Problem: %(message)s)\n\n')%locals()
114 print _('''%(message)sUsage: roundup-admin [options] <command> <arguments>
116 Options:
117 -i instance home -- specify the issue tracker "home directory" to administer
118 -u -- the user[:password] to use for commands
119 -c -- when outputting lists of data, just comma-separate them
121 Help:
122 roundup-admin -h
123 roundup-admin help -- this help
124 roundup-admin help <command> -- command-specific help
125 roundup-admin help all -- all available help
126 ''')%locals()
127 self.help_commands()
129 def help_commands(self):
130 ''' List the commands available with their precis help.
131 '''
132 print _('Commands:'),
133 commands = ['']
134 for command in self.commands.values():
135 h = command.__doc__.split('\n')[0]
136 commands.append(' '+h[7:])
137 commands.sort()
138 commands.append(_('Commands may be abbreviated as long as the abbreviation matches only one'))
139 commands.append(_('command, e.g. l == li == lis == list.'))
140 print '\n'.join(commands)
141 print
143 def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
144 ''' Produce an HTML command list.
145 '''
146 commands = self.commands.values()
147 def sortfun(a, b):
148 return cmp(a.__name__, b.__name__)
149 commands.sort(sortfun)
150 for command in commands:
151 h = command.__doc__.split('\n')
152 name = command.__name__[3:]
153 usage = h[0]
154 print _('''
155 <tr><td valign=top><strong>%(name)s</strong></td>
156 <td><tt>%(usage)s</tt><p>
157 <pre>''')%locals()
158 indent = indent_re.match(h[3])
159 if indent: indent = len(indent.group(1))
160 for line in h[3:]:
161 if indent:
162 print line[indent:]
163 else:
164 print line
165 print _('</pre></td></tr>\n')
167 def help_all(self):
168 print _('''
169 All commands (except help) require a tracker specifier. This is just the path
170 to the roundup tracker you're working with. A roundup tracker is where
171 roundup keeps the database and configuration file that defines an issue
172 tracker. It may be thought of as the issue tracker's "home directory". It may
173 be specified in the environment variable TRACKER_HOME or on the command
174 line as "-i tracker".
176 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
178 Property values are represented as strings in command arguments and in the
179 printed results:
180 . Strings are, well, strings.
181 . Date values are printed in the full date format in the local time zone, and
182 accepted in the full format or any of the partial formats explained below.
183 . Link values are printed as node designators. When given as an argument,
184 node designators and key strings are both accepted.
185 . Multilink values are printed as lists of node designators joined by commas.
186 When given as an argument, node designators and key strings are both
187 accepted; an empty string, a single node, or a list of nodes joined by
188 commas is accepted.
190 When property values must contain spaces, just surround the value with
191 quotes, either ' or ". A single space may also be backslash-quoted. If a
192 valuu must contain a quote character, it must be backslash-quoted or inside
193 quotes. Examples:
194 hello world (2 tokens: hello, world)
195 "hello world" (1 token: hello world)
196 "Roch'e" Compaan (2 tokens: Roch'e Compaan)
197 Roch\'e Compaan (2 tokens: Roch'e Compaan)
198 address="1 2 3" (1 token: address=1 2 3)
199 \\ (1 token: \)
200 \n\r\t (1 token: a newline, carriage-return and tab)
202 When multiple nodes are specified to the roundup get or roundup set
203 commands, the specified properties are retrieved or set on all the listed
204 nodes.
206 When multiple results are returned by the roundup get or roundup find
207 commands, they are printed one per line (default) or joined by commas (with
208 the -c) option.
210 Where the command changes data, a login name/password is required. The
211 login may be specified as either "name" or "name:password".
212 . ROUNDUP_LOGIN environment variable
213 . the -u command-line option
214 If either the name or password is not supplied, they are obtained from the
215 command-line.
217 Date format examples:
218 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
219 "2000-04-17" means <Date 2000-04-17.00:00:00>
220 "01-25" means <Date yyyy-01-25.00:00:00>
221 "08-13.22:13" means <Date yyyy-08-14.03:13:00>
222 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
223 "14:25" means <Date yyyy-mm-dd.19:25:00>
224 "8:47:11" means <Date yyyy-mm-dd.13:47:11>
225 "." means "right now"
227 Command help:
228 ''')
229 for name, command in self.commands.items():
230 print _('%s:')%name
231 print _(' '), command.__doc__
233 def do_help(self, args, nl_re=re.compile('[\r\n]'),
234 indent_re=re.compile(r'^(\s+)\S+')):
235 '''Usage: help topic
236 Give help about topic.
238 commands -- list commands
239 <command> -- help specific to a command
240 initopts -- init command options
241 all -- all available help
242 '''
243 if len(args)>0:
244 topic = args[0]
245 else:
246 topic = 'help'
249 # try help_ methods
250 if self.help.has_key(topic):
251 self.help[topic]()
252 return 0
254 # try command docstrings
255 try:
256 l = self.commands.get(topic)
257 except KeyError:
258 print _('Sorry, no help for "%(topic)s"')%locals()
259 return 1
261 # display the help for each match, removing the docsring indent
262 for name, help in l:
263 lines = nl_re.split(help.__doc__)
264 print lines[0]
265 indent = indent_re.match(lines[1])
266 if indent: indent = len(indent.group(1))
267 for line in lines[1:]:
268 if indent:
269 print line[indent:]
270 else:
271 print line
272 return 0
274 def help_initopts(self):
275 import roundup.templates
276 templates = roundup.templates.listTemplates()
277 print _('Templates:'), ', '.join(templates)
278 import roundup.backends
279 backends = roundup.backends.__all__
280 print _('Back ends:'), ', '.join(backends)
282 def do_install(self, tracker_home, args):
283 '''Usage: install [template [backend [admin password]]]
284 Install a new Roundup tracker.
286 The command will prompt for the tracker home directory (if not supplied
287 through TRACKER_HOME or the -i option). The template, backend and admin
288 password may be specified on the command-line as arguments, in that
289 order.
291 The initialise command must be called after this command in order
292 to initialise the tracker's database. You may edit the tracker's
293 initial database contents before running that command by editing
294 the tracker's dbinit.py module init() function.
296 See also initopts help.
297 '''
298 if len(args) < 1:
299 raise UsageError, _('Not enough arguments supplied')
301 # make sure the tracker home can be created
302 parent = os.path.split(tracker_home)[0]
303 if not os.path.exists(parent):
304 raise UsageError, _('Instance home parent directory "%(parent)s"'
305 ' does not exist')%locals()
307 # select template
308 import roundup.templates
309 templates = roundup.templates.listTemplates()
310 template = len(args) > 1 and args[1] or ''
311 if template not in templates:
312 print _('Templates:'), ', '.join(templates)
313 while template not in templates:
314 template = raw_input(_('Select template [classic]: ')).strip()
315 if not template:
316 template = 'classic'
318 # select hyperdb backend
319 import roundup.backends
320 backends = roundup.backends.__all__
321 backend = len(args) > 2 and args[2] or ''
322 if backend not in backends:
323 print _('Back ends:'), ', '.join(backends)
324 while backend not in backends:
325 backend = raw_input(_('Select backend [anydbm]: ')).strip()
326 if not backend:
327 backend = 'anydbm'
328 # XXX perform a unit test based on the user's selections
330 # install!
331 init.install(tracker_home, template)
332 init.write_select_db(tracker_home, backend)
334 print _('''
335 You should now edit the tracker configuration file:
336 %(config_file)s
337 ... at a minimum, you must set MAILHOST, TRACKER_WEB, MAIL_DOMAIN and
338 ADMIN_EMAIL.
340 If you wish to modify the default schema, you should also edit the database
341 initialisation file:
342 %(database_config_file)s
343 ... see the documentation on customizing for more information.
344 ''')%{
345 'config_file': os.path.join(tracker_home, 'config.py'),
346 'database_config_file': os.path.join(tracker_home, 'dbinit.py')
347 }
348 return 0
351 def do_initialise(self, tracker_home, args):
352 '''Usage: initialise [adminpw]
353 Initialise a new Roundup tracker.
355 The administrator details will be set at this step.
357 Execute the tracker's initialisation function dbinit.init()
358 '''
359 # password
360 if len(args) > 1:
361 adminpw = args[1]
362 else:
363 adminpw = ''
364 confirm = 'x'
365 while adminpw != confirm:
366 adminpw = getpass.getpass(_('Admin Password: '))
367 confirm = getpass.getpass(_(' Confirm: '))
369 # make sure the tracker home is installed
370 if not os.path.exists(tracker_home):
371 raise UsageError, _('Instance home does not exist')%locals()
372 try:
373 tracker = roundup.instance.open(tracker_home)
374 except roundup.instance.TrackerError:
375 raise UsageError, _('Instance has not been installed')%locals()
377 # is there already a database?
378 try:
379 db_exists = tracker.select_db.Database.exists(tracker.config)
380 except AttributeError:
381 # TODO: move this code to exists() static method in every backend
382 db_exists = os.path.exists(os.path.join(tracker_home, 'db'))
383 if db_exists:
384 print _('WARNING: The database is already initialised!')
385 print _('If you re-initialise it, you will lose all the data!')
386 ok = raw_input(_('Erase it? Y/[N]: ')).strip()
387 if ok.lower() != 'y':
388 return 0
390 # Get a database backend in use by tracker
391 try:
392 # nuke it
393 tracker.select_db.Database.nuke(tracker.config)
394 except AttributeError:
395 # TODO: move this code to nuke() static method in every backend
396 shutil.rmtree(os.path.join(tracker_home, 'db'))
398 # GO
399 init.initialise(tracker_home, adminpw)
401 return 0
404 def do_get(self, args):
405 '''Usage: get property designator[,designator]*
406 Get the given property of one or more designator(s).
408 Retrieves the property value of the nodes specified by the designators.
409 '''
410 if len(args) < 2:
411 raise UsageError, _('Not enough arguments supplied')
412 propname = args[0]
413 designators = args[1].split(',')
414 l = []
415 for designator in designators:
416 # decode the node designator
417 try:
418 classname, nodeid = hyperdb.splitDesignator(designator)
419 except hyperdb.DesignatorError, message:
420 raise UsageError, message
422 # get the class
423 cl = self.get_class(classname)
424 try:
425 if self.comma_sep:
426 l.append(cl.get(nodeid, propname))
427 else:
428 print cl.get(nodeid, propname)
429 except IndexError:
430 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
431 except KeyError:
432 raise UsageError, _('no such %(classname)s property '
433 '"%(propname)s"')%locals()
434 if self.comma_sep:
435 print ','.join(l)
436 return 0
439 def do_set(self, args, pwre = re.compile(r'{(\w+)}(.+)')):
440 '''Usage: set [items] property=value property=value ...
441 Set the given properties of one or more items(s).
443 The items may be specified as a class or as a comma-separeted
444 list of item designators (ie "designator[,designator,...]").
446 This command sets the properties to the values for all designators
447 given. If the value is missing (ie. "property=") then the property is
448 un-set.
449 '''
450 if len(args) < 2:
451 raise UsageError, _('Not enough arguments supplied')
452 from roundup import hyperdb
454 designators = args[0].split(',')
455 if len(designators) == 1:
456 designator = designators[0]
457 try:
458 designator = hyperdb.splitDesignator(designator)
459 designators = [designator]
460 except hyperdb.DesignatorError:
461 cl = self.get_class(designator)
462 designators = [(designator, x) for x in cl.list()]
463 else:
464 try:
465 designators = [hyperdb.splitDesignator(x) for x in designators]
466 except hyperdb.DesignatorError, message:
467 raise UsageError, message
469 # get the props from the args
470 props = self.props_from_args(args[1:])
472 # now do the set for all the nodes
473 for classname, itemid in designators:
474 cl = self.get_class(classname)
476 properties = cl.getprops()
477 for key, value in props.items():
478 proptype = properties[key]
479 if isinstance(proptype, hyperdb.Multilink):
480 if value is None:
481 props[key] = []
482 else:
483 props[key] = value.split(',')
484 elif value is None:
485 continue
486 elif isinstance(proptype, hyperdb.String):
487 continue
488 elif isinstance(proptype, hyperdb.Password):
489 m = pwre.match(value)
490 if m:
491 # password is being given to us encrypted
492 p = password.Password()
493 p.scheme = m.group(1)
494 p.password = m.group(2)
495 props[key] = p
496 else:
497 props[key] = password.Password(value)
498 elif isinstance(proptype, hyperdb.Date):
499 try:
500 props[key] = date.Date(value)
501 except ValueError, message:
502 raise UsageError, '"%s": %s'%(value, message)
503 elif isinstance(proptype, hyperdb.Interval):
504 try:
505 props[key] = date.Interval(value)
506 except ValueError, message:
507 raise UsageError, '"%s": %s'%(value, message)
508 elif isinstance(proptype, hyperdb.Link):
509 props[key] = value
510 elif isinstance(proptype, hyperdb.Boolean):
511 props[key] = value.lower() in ('yes', 'true', 'on', '1')
512 elif isinstance(proptype, hyperdb.Number):
513 props[key] = float(value)
515 # try the set
516 try:
517 apply(cl.set, (itemid, ), props)
518 except (TypeError, IndexError, ValueError), message:
519 import traceback; traceback.print_exc()
520 raise UsageError, message
521 return 0
523 def do_find(self, args):
524 '''Usage: find classname propname=value ...
525 Find the nodes of the given class with a given link property value.
527 Find the nodes of the given class with a given link property value. The
528 value may be either the nodeid of the linked node, or its key value.
529 '''
530 if len(args) < 1:
531 raise UsageError, _('Not enough arguments supplied')
532 classname = args[0]
533 # get the class
534 cl = self.get_class(classname)
536 # handle the propname=value argument
537 props = self.props_from_args(args[1:])
539 # if the value isn't a number, look up the linked class to get the
540 # number
541 for propname, value in props.items():
542 num_re = re.compile('^\d+$')
543 if not num_re.match(value):
544 # get the property
545 try:
546 property = cl.properties[propname]
547 except KeyError:
548 raise UsageError, _('%(classname)s has no property '
549 '"%(propname)s"')%locals()
551 # make sure it's a link
552 if (not isinstance(property, hyperdb.Link) and not
553 isinstance(property, hyperdb.Multilink)):
554 raise UsageError, _('You may only "find" link properties')
556 # get the linked-to class and look up the key property
557 link_class = self.db.getclass(property.classname)
558 try:
559 props[propname] = link_class.lookup(value)
560 except TypeError:
561 raise UsageError, _('%(classname)s has no key property"')%{
562 'classname': link_class.classname}
564 # now do the find
565 try:
566 if self.comma_sep:
567 print ','.join(apply(cl.find, (), props))
568 else:
569 print apply(cl.find, (), props)
570 except KeyError:
571 raise UsageError, _('%(classname)s has no property '
572 '"%(propname)s"')%locals()
573 except (ValueError, TypeError), message:
574 raise UsageError, message
575 return 0
577 def do_specification(self, args):
578 '''Usage: specification classname
579 Show the properties for a classname.
581 This lists the properties for a given class.
582 '''
583 if len(args) < 1:
584 raise UsageError, _('Not enough arguments supplied')
585 classname = args[0]
586 # get the class
587 cl = self.get_class(classname)
589 # get the key property
590 keyprop = cl.getkey()
591 for key, value in cl.properties.items():
592 if keyprop == key:
593 print _('%(key)s: %(value)s (key property)')%locals()
594 else:
595 print _('%(key)s: %(value)s')%locals()
597 def do_display(self, args):
598 '''Usage: display designator
599 Show the property values for the given node.
601 This lists the properties and their associated values for the given
602 node.
603 '''
604 if len(args) < 1:
605 raise UsageError, _('Not enough arguments supplied')
607 # decode the node designator
608 try:
609 classname, nodeid = hyperdb.splitDesignator(args[0])
610 except hyperdb.DesignatorError, message:
611 raise UsageError, message
613 # get the class
614 cl = self.get_class(classname)
616 # display the values
617 for key in cl.properties.keys():
618 value = cl.get(nodeid, key)
619 print _('%(key)s: %(value)s')%locals()
621 def do_create(self, args, pwre = re.compile(r'{(\w+)}(.+)')):
622 '''Usage: create classname property=value ...
623 Create a new entry of a given class.
625 This creates a new entry of the given class using the property
626 name=value arguments provided on the command line after the "create"
627 command.
628 '''
629 if len(args) < 1:
630 raise UsageError, _('Not enough arguments supplied')
631 from roundup import hyperdb
633 classname = args[0]
635 # get the class
636 cl = self.get_class(classname)
638 # now do a create
639 props = {}
640 properties = cl.getprops(protected = 0)
641 if len(args) == 1:
642 # ask for the properties
643 for key, value in properties.items():
644 if key == 'id': continue
645 name = value.__class__.__name__
646 if isinstance(value , hyperdb.Password):
647 again = None
648 while value != again:
649 value = getpass.getpass(_('%(propname)s (Password): ')%{
650 'propname': key.capitalize()})
651 again = getpass.getpass(_(' %(propname)s (Again): ')%{
652 'propname': key.capitalize()})
653 if value != again: print _('Sorry, try again...')
654 if value:
655 props[key] = value
656 else:
657 value = raw_input(_('%(propname)s (%(proptype)s): ')%{
658 'propname': key.capitalize(), 'proptype': name})
659 if value:
660 props[key] = value
661 else:
662 props = self.props_from_args(args[1:])
664 # convert types
665 for propname, value in props.items():
666 # get the property
667 try:
668 proptype = properties[propname]
669 except KeyError:
670 raise UsageError, _('%(classname)s has no property '
671 '"%(propname)s"')%locals()
673 if isinstance(proptype, hyperdb.Date):
674 try:
675 props[propname] = date.Date(value)
676 except ValueError, message:
677 raise UsageError, _('"%(value)s": %(message)s')%locals()
678 elif isinstance(proptype, hyperdb.Interval):
679 try:
680 props[propname] = date.Interval(value)
681 except ValueError, message:
682 raise UsageError, _('"%(value)s": %(message)s')%locals()
683 elif isinstance(proptype, hyperdb.Password):
684 m = pwre.match(value)
685 if m:
686 # password is being given to us encrypted
687 p = password.Password()
688 p.scheme = m.group(1)
689 p.password = m.group(2)
690 props[propname] = p
691 else:
692 props[propname] = password.Password(value)
693 elif isinstance(proptype, hyperdb.Multilink):
694 props[propname] = value.split(',')
695 elif isinstance(proptype, hyperdb.Boolean):
696 props[propname] = value.lower() in ('yes', 'true', 'on', '1')
697 elif isinstance(proptype, hyperdb.Number):
698 props[propname] = float(value)
700 # check for the key property
701 propname = cl.getkey()
702 if propname and not props.has_key(propname):
703 raise UsageError, _('you must provide the "%(propname)s" '
704 'property.')%locals()
706 # do the actual create
707 try:
708 print apply(cl.create, (), props)
709 except (TypeError, IndexError, ValueError), message:
710 raise UsageError, message
711 return 0
713 def do_list(self, args):
714 '''Usage: list classname [property]
715 List the instances of a class.
717 Lists all instances of the given class. If the property is not
718 specified, the "label" property is used. The label property is tried
719 in order: the key, "name", "title" and then the first property,
720 alphabetically.
721 '''
722 if len(args) < 1:
723 raise UsageError, _('Not enough arguments supplied')
724 classname = args[0]
726 # get the class
727 cl = self.get_class(classname)
729 # figure the property
730 if len(args) > 1:
731 propname = args[1]
732 else:
733 propname = cl.labelprop()
735 if self.comma_sep:
736 print ','.join(cl.list())
737 else:
738 for nodeid in cl.list():
739 try:
740 value = cl.get(nodeid, propname)
741 except KeyError:
742 raise UsageError, _('%(classname)s has no property '
743 '"%(propname)s"')%locals()
744 print _('%(nodeid)4s: %(value)s')%locals()
745 return 0
747 def do_table(self, args):
748 '''Usage: table classname [property[,property]*]
749 List the instances of a class in tabular form.
751 Lists all instances of the given class. If the properties are not
752 specified, all properties are displayed. By default, the column widths
753 are the width of the property names. The width may be explicitly defined
754 by defining the property as "name:width". For example::
755 roundup> table priority id,name:10
756 Id Name
757 1 fatal-bug
758 2 bug
759 3 usability
760 4 feature
761 '''
762 if len(args) < 1:
763 raise UsageError, _('Not enough arguments supplied')
764 classname = args[0]
766 # get the class
767 cl = self.get_class(classname)
769 # figure the property names to display
770 if len(args) > 1:
771 prop_names = args[1].split(',')
772 all_props = cl.getprops()
773 for spec in prop_names:
774 if ':' in spec:
775 try:
776 propname, width = spec.split(':')
777 except (ValueError, TypeError):
778 raise UsageError, _('"%(spec)s" not name:width')%locals()
779 else:
780 propname = spec
781 if not all_props.has_key(propname):
782 raise UsageError, _('%(classname)s has no property '
783 '"%(propname)s"')%locals()
784 else:
785 prop_names = cl.getprops().keys()
787 # now figure column widths
788 props = []
789 for spec in prop_names:
790 if ':' in spec:
791 name, width = spec.split(':')
792 props.append((name, int(width)))
793 else:
794 props.append((spec, len(spec)))
796 # now display the heading
797 print ' '.join([name.capitalize().ljust(width) for name,width in props])
799 # and the table data
800 for nodeid in cl.list():
801 l = []
802 for name, width in props:
803 if name != 'id':
804 try:
805 value = str(cl.get(nodeid, name))
806 except KeyError:
807 # we already checked if the property is valid - a
808 # KeyError here means the node just doesn't have a
809 # value for it
810 value = ''
811 else:
812 value = str(nodeid)
813 f = '%%-%ds'%width
814 l.append(f%value[:width])
815 print ' '.join(l)
816 return 0
818 def do_history(self, args):
819 '''Usage: history designator
820 Show the history entries of a designator.
822 Lists the journal entries for the node identified by the designator.
823 '''
824 if len(args) < 1:
825 raise UsageError, _('Not enough arguments supplied')
826 try:
827 classname, nodeid = hyperdb.splitDesignator(args[0])
828 except hyperdb.DesignatorError, message:
829 raise UsageError, message
831 try:
832 print self.db.getclass(classname).history(nodeid)
833 except KeyError:
834 raise UsageError, _('no such class "%(classname)s"')%locals()
835 except IndexError:
836 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
837 return 0
839 def do_commit(self, args):
840 '''Usage: commit
841 Commit all changes made to the database.
843 The changes made during an interactive session are not
844 automatically written to the database - they must be committed
845 using this command.
847 One-off commands on the command-line are automatically committed if
848 they are successful.
849 '''
850 self.db.commit()
851 return 0
853 def do_rollback(self, args):
854 '''Usage: rollback
855 Undo all changes that are pending commit to the database.
857 The changes made during an interactive session are not
858 automatically written to the database - they must be committed
859 manually. This command undoes all those changes, so a commit
860 immediately after would make no changes to the database.
861 '''
862 self.db.rollback()
863 return 0
865 def do_retire(self, args):
866 '''Usage: retire designator[,designator]*
867 Retire the node specified by designator.
869 This action indicates that a particular node is not to be retrieved by
870 the list or find commands, and its key value may be re-used.
871 '''
872 if len(args) < 1:
873 raise UsageError, _('Not enough arguments supplied')
874 designators = args[0].split(',')
875 for designator in designators:
876 try:
877 classname, nodeid = hyperdb.splitDesignator(designator)
878 except hyperdb.DesignatorError, message:
879 raise UsageError, message
880 try:
881 self.db.getclass(classname).retire(nodeid)
882 except KeyError:
883 raise UsageError, _('no such class "%(classname)s"')%locals()
884 except IndexError:
885 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
886 return 0
888 def do_export(self, args):
889 '''Usage: export [class[,class]] export_dir
890 Export the database to colon-separated-value files.
892 This action exports the current data from the database into
893 colon-separated-value files that are placed in the nominated
894 destination directory. The journals are not exported.
895 '''
896 # we need the CSV module
897 if csv is None:
898 raise UsageError, \
899 _('Sorry, you need the csv module to use this function.\n'
900 'Get it from: http://www.object-craft.com.au/projects/csv/')
902 # grab the directory to export to
903 if len(args) < 1:
904 raise UsageError, _('Not enough arguments supplied')
905 dir = args[-1]
907 # get the list of classes to export
908 if len(args) == 2:
909 classes = args[0].split(',')
910 else:
911 classes = self.db.classes.keys()
913 # use the csv parser if we can - it's faster
914 p = csv.parser(field_sep=':')
916 # do all the classes specified
917 for classname in classes:
918 cl = self.get_class(classname)
919 f = open(os.path.join(dir, classname+'.csv'), 'w')
920 properties = cl.getprops()
921 propnames = properties.keys()
922 propnames.sort()
923 print >> f, p.join(propnames)
925 # all nodes for this class (not using list() 'cos it doesn't
926 # include retired nodes)
928 for nodeid in self.db.getclass(classname).getnodeids():
929 # get the regular props
930 print >>f, p.join(cl.export_list(propnames, nodeid))
931 return 0
933 def do_import(self, args):
934 '''Usage: import import_dir
935 Import a database from the directory containing CSV files, one per
936 class to import.
938 The files must define the same properties as the class (including having
939 a "header" line with those property names.)
941 The imported nodes will have the same nodeid as defined in the
942 import file, thus replacing any existing content.
944 The new nodes are added to the existing database - if you want to
945 create a new database using the imported data, then create a new
946 database (or, tediously, retire all the old data.)
947 '''
948 if len(args) < 1:
949 raise UsageError, _('Not enough arguments supplied')
950 if csv is None:
951 raise UsageError, \
952 _('Sorry, you need the csv module to use this function.\n'
953 'Get it from: http://www.object-craft.com.au/projects/csv/')
955 from roundup import hyperdb
957 for file in os.listdir(args[0]):
958 f = open(os.path.join(args[0], file))
960 # get the classname
961 classname = os.path.splitext(file)[0]
963 # ensure that the properties and the CSV file headings match
964 cl = self.get_class(classname)
965 p = csv.parser(field_sep=':')
966 file_props = p.parse(f.readline())
967 properties = cl.getprops()
968 propnames = properties.keys()
969 propnames.sort()
970 m = file_props[:]
971 m.sort()
972 if m != propnames:
973 raise UsageError, _('Import file doesn\'t define the same '
974 'properties as "%(arg0)s".')%{'arg0': args[0]}
976 # loop through the file and create a node for each entry
977 maxid = 1
978 while 1:
979 line = f.readline()
980 if not line: break
982 # parse lines until we get a complete entry
983 while 1:
984 l = p.parse(line)
985 if l: break
986 line = f.readline()
987 if not line:
988 raise ValueError, "Unexpected EOF during CSV parse"
990 # do the import and figure the current highest nodeid
991 maxid = max(maxid, int(cl.import_list(propnames, l)))
993 print 'setting', classname, maxid+1
994 self.db.setid(classname, str(maxid+1))
995 return 0
997 def do_pack(self, args):
998 '''Usage: pack period | date
1000 Remove journal entries older than a period of time specified or
1001 before a certain date.
1003 A period is specified using the suffixes "y", "m", and "d". The
1004 suffix "w" (for "week") means 7 days.
1006 "3y" means three years
1007 "2y 1m" means two years and one month
1008 "1m 25d" means one month and 25 days
1009 "2w 3d" means two weeks and three days
1011 Date format is "YYYY-MM-DD" eg:
1012 2001-01-01
1014 '''
1015 if len(args) <> 1:
1016 raise UsageError, _('Not enough arguments supplied')
1018 # are we dealing with a period or a date
1019 value = args[0]
1020 date_re = re.compile(r'''
1021 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
1022 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
1023 ''', re.VERBOSE)
1024 m = date_re.match(value)
1025 if not m:
1026 raise ValueError, _('Invalid format')
1027 m = m.groupdict()
1028 if m['period']:
1029 pack_before = date.Date(". - %s"%value)
1030 elif m['date']:
1031 pack_before = date.Date(value)
1032 self.db.pack(pack_before)
1033 return 0
1035 def do_reindex(self, args):
1036 '''Usage: reindex
1037 Re-generate a tracker's search indexes.
1039 This will re-generate the search indexes for a tracker. This will
1040 typically happen automatically.
1041 '''
1042 self.db.indexer.force_reindex()
1043 self.db.reindex()
1044 return 0
1046 def do_security(self, args):
1047 '''Usage: security [Role name]
1048 Display the Permissions available to one or all Roles.
1049 '''
1050 if len(args) == 1:
1051 role = args[0]
1052 try:
1053 roles = [(args[0], self.db.security.role[args[0]])]
1054 except KeyError:
1055 print _('No such Role "%(role)s"')%locals()
1056 return 1
1057 else:
1058 roles = self.db.security.role.items()
1059 role = self.db.config.NEW_WEB_USER_ROLES
1060 if ',' in role:
1061 print _('New Web users get the Roles "%(role)s"')%locals()
1062 else:
1063 print _('New Web users get the Role "%(role)s"')%locals()
1064 role = self.db.config.NEW_EMAIL_USER_ROLES
1065 if ',' in role:
1066 print _('New Email users get the Roles "%(role)s"')%locals()
1067 else:
1068 print _('New Email users get the Role "%(role)s"')%locals()
1069 roles.sort()
1070 for rolename, role in roles:
1071 print _('Role "%(name)s":')%role.__dict__
1072 for permission in role.permissions:
1073 if permission.klass:
1074 print _(' %(description)s (%(name)s for "%(klass)s" '
1075 'only)')%permission.__dict__
1076 else:
1077 print _(' %(description)s (%(name)s)')%permission.__dict__
1078 return 0
1080 def run_command(self, args):
1081 '''Run a single command
1082 '''
1083 command = args[0]
1085 # handle help now
1086 if command == 'help':
1087 if len(args)>1:
1088 self.do_help(args[1:])
1089 return 0
1090 self.do_help(['help'])
1091 return 0
1092 if command == 'morehelp':
1093 self.do_help(['help'])
1094 self.help_commands()
1095 self.help_all()
1096 return 0
1098 # figure what the command is
1099 try:
1100 functions = self.commands.get(command)
1101 except KeyError:
1102 # not a valid command
1103 print _('Unknown command "%(command)s" ("help commands" for a '
1104 'list)')%locals()
1105 return 1
1107 # check for multiple matches
1108 if len(functions) > 1:
1109 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1110 command, 'list': ', '.join([i[0] for i in functions])}
1111 return 1
1112 command, function = functions[0]
1114 # make sure we have a tracker_home
1115 while not self.tracker_home:
1116 self.tracker_home = raw_input(_('Enter tracker home: ')).strip()
1118 # before we open the db, we may be doing an install or init
1119 if command == 'initialise':
1120 try:
1121 return self.do_initialise(self.tracker_home, args)
1122 except UsageError, message:
1123 print _('Error: %(message)s')%locals()
1124 return 1
1125 elif command == 'install':
1126 try:
1127 return self.do_install(self.tracker_home, args)
1128 except UsageError, message:
1129 print _('Error: %(message)s')%locals()
1130 return 1
1132 # get the tracker
1133 try:
1134 tracker = roundup.instance.open(self.tracker_home)
1135 except ValueError, message:
1136 self.tracker_home = ''
1137 print _("Error: Couldn't open tracker: %(message)s")%locals()
1138 return 1
1140 # only open the database once!
1141 if not self.db:
1142 self.db = tracker.open('admin')
1144 # do the command
1145 ret = 0
1146 try:
1147 ret = function(args[1:])
1148 except UsageError, message:
1149 print _('Error: %(message)s')%locals()
1150 print
1151 print function.__doc__
1152 ret = 1
1153 except:
1154 import traceback
1155 traceback.print_exc()
1156 ret = 1
1157 return ret
1159 def interactive(self):
1160 '''Run in an interactive mode
1161 '''
1162 print _('Roundup %s ready for input.'%roundup_version)
1163 print _('Type "help" for help.')
1164 try:
1165 import readline
1166 except ImportError:
1167 print _('Note: command history and editing not available')
1169 while 1:
1170 try:
1171 command = raw_input(_('roundup> '))
1172 except EOFError:
1173 print _('exit...')
1174 break
1175 if not command: continue
1176 args = token.token_split(command)
1177 if not args: continue
1178 if args[0] in ('quit', 'exit'): break
1179 self.run_command(args)
1181 # exit.. check for transactions
1182 if self.db and self.db.transactions:
1183 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1184 if commit and commit[0].lower() == 'y':
1185 self.db.commit()
1186 return 0
1188 def main(self):
1189 try:
1190 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1191 except getopt.GetoptError, e:
1192 self.usage(str(e))
1193 return 1
1195 # handle command-line args
1196 self.tracker_home = os.environ.get('TRACKER_HOME', '')
1197 # TODO: reinstate the user/password stuff (-u arg too)
1198 name = password = ''
1199 if os.environ.has_key('ROUNDUP_LOGIN'):
1200 l = os.environ['ROUNDUP_LOGIN'].split(':')
1201 name = l[0]
1202 if len(l) > 1:
1203 password = l[1]
1204 self.comma_sep = 0
1205 for opt, arg in opts:
1206 if opt == '-h':
1207 self.usage()
1208 return 0
1209 if opt == '-i':
1210 self.tracker_home = arg
1211 if opt == '-c':
1212 self.comma_sep = 1
1214 # if no command - go interactive
1215 # wrap in a try/finally so we always close off the db
1216 ret = 0
1217 try:
1218 if not args:
1219 self.interactive()
1220 else:
1221 ret = self.run_command(args)
1222 if self.db: self.db.commit()
1223 return ret
1224 finally:
1225 if self.db:
1226 self.db.close()
1228 if __name__ == '__main__':
1229 tool = AdminTool()
1230 sys.exit(tool.main())
1232 # vim: set filetype=python ts=4 sw=4 et si