86047930343885667ef2f73bb4c923bfb73a25f8
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.33 2002-09-26 07:39:21 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):
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 props[propname] = password.Password(value)
646 elif isinstance(proptype, hyperdb.Multilink):
647 props[propname] = value.split(',')
648 elif isinstance(proptype, hyperdb.Boolean):
649 props[propname] = value.lower() in ('yes', 'true', 'on', '1')
650 elif isinstance(proptype, hyperdb.Number):
651 props[propname] = int(value)
653 # check for the key property
654 propname = cl.getkey()
655 if propname and not props.has_key(propname):
656 raise UsageError, _('you must provide the "%(propname)s" '
657 'property.')%locals()
659 # do the actual create
660 try:
661 print apply(cl.create, (), props)
662 except (TypeError, IndexError, ValueError), message:
663 raise UsageError, message
664 return 0
666 def do_list(self, args):
667 '''Usage: list classname [property]
668 List the instances of a class.
670 Lists all instances of the given class. If the property is not
671 specified, the "label" property is used. The label property is tried
672 in order: the key, "name", "title" and then the first property,
673 alphabetically.
674 '''
675 if len(args) < 1:
676 raise UsageError, _('Not enough arguments supplied')
677 classname = args[0]
679 # get the class
680 cl = self.get_class(classname)
682 # figure the property
683 if len(args) > 1:
684 propname = args[1]
685 else:
686 propname = cl.labelprop()
688 if self.comma_sep:
689 print ','.join(cl.list())
690 else:
691 for nodeid in cl.list():
692 try:
693 value = cl.get(nodeid, propname)
694 except KeyError:
695 raise UsageError, _('%(classname)s has no property '
696 '"%(propname)s"')%locals()
697 print _('%(nodeid)4s: %(value)s')%locals()
698 return 0
700 def do_table(self, args):
701 '''Usage: table classname [property[,property]*]
702 List the instances of a class in tabular form.
704 Lists all instances of the given class. If the properties are not
705 specified, all properties are displayed. By default, the column widths
706 are the width of the property names. The width may be explicitly defined
707 by defining the property as "name:width". For example::
708 roundup> table priority id,name:10
709 Id Name
710 1 fatal-bug
711 2 bug
712 3 usability
713 4 feature
714 '''
715 if len(args) < 1:
716 raise UsageError, _('Not enough arguments supplied')
717 classname = args[0]
719 # get the class
720 cl = self.get_class(classname)
722 # figure the property names to display
723 if len(args) > 1:
724 prop_names = args[1].split(',')
725 all_props = cl.getprops()
726 for spec in prop_names:
727 if ':' in spec:
728 try:
729 propname, width = spec.split(':')
730 except (ValueError, TypeError):
731 raise UsageError, _('"%(spec)s" not name:width')%locals()
732 else:
733 propname = spec
734 if not all_props.has_key(propname):
735 raise UsageError, _('%(classname)s has no property '
736 '"%(propname)s"')%locals()
737 else:
738 prop_names = cl.getprops().keys()
740 # now figure column widths
741 props = []
742 for spec in prop_names:
743 if ':' in spec:
744 name, width = spec.split(':')
745 props.append((name, int(width)))
746 else:
747 props.append((spec, len(spec)))
749 # now display the heading
750 print ' '.join([name.capitalize().ljust(width) for name,width in props])
752 # and the table data
753 for nodeid in cl.list():
754 l = []
755 for name, width in props:
756 if name != 'id':
757 try:
758 value = str(cl.get(nodeid, name))
759 except KeyError:
760 # we already checked if the property is valid - a
761 # KeyError here means the node just doesn't have a
762 # value for it
763 value = ''
764 else:
765 value = str(nodeid)
766 f = '%%-%ds'%width
767 l.append(f%value[:width])
768 print ' '.join(l)
769 return 0
771 def do_history(self, args):
772 '''Usage: history designator
773 Show the history entries of a designator.
775 Lists the journal entries for the node identified by the designator.
776 '''
777 if len(args) < 1:
778 raise UsageError, _('Not enough arguments supplied')
779 try:
780 classname, nodeid = hyperdb.splitDesignator(args[0])
781 except hyperdb.DesignatorError, message:
782 raise UsageError, message
784 try:
785 print self.db.getclass(classname).history(nodeid)
786 except KeyError:
787 raise UsageError, _('no such class "%(classname)s"')%locals()
788 except IndexError:
789 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
790 return 0
792 def do_commit(self, args):
793 '''Usage: commit
794 Commit all changes made to the database.
796 The changes made during an interactive session are not
797 automatically written to the database - they must be committed
798 using this command.
800 One-off commands on the command-line are automatically committed if
801 they are successful.
802 '''
803 self.db.commit()
804 return 0
806 def do_rollback(self, args):
807 '''Usage: rollback
808 Undo all changes that are pending commit to the database.
810 The changes made during an interactive session are not
811 automatically written to the database - they must be committed
812 manually. This command undoes all those changes, so a commit
813 immediately after would make no changes to the database.
814 '''
815 self.db.rollback()
816 return 0
818 def do_retire(self, args):
819 '''Usage: retire designator[,designator]*
820 Retire the node specified by designator.
822 This action indicates that a particular node is not to be retrieved by
823 the list or find commands, and its key value may be re-used.
824 '''
825 if len(args) < 1:
826 raise UsageError, _('Not enough arguments supplied')
827 designators = args[0].split(',')
828 for designator in designators:
829 try:
830 classname, nodeid = hyperdb.splitDesignator(designator)
831 except hyperdb.DesignatorError, message:
832 raise UsageError, message
833 try:
834 self.db.getclass(classname).retire(nodeid)
835 except KeyError:
836 raise UsageError, _('no such class "%(classname)s"')%locals()
837 except IndexError:
838 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
839 return 0
841 def do_export(self, args):
842 '''Usage: export [class[,class]] export_dir
843 Export the database to colon-separated-value files.
845 This action exports the current data from the database into
846 colon-separated-value files that are placed in the nominated
847 destination directory. The journals are not exported.
848 '''
849 # we need the CSV module
850 if csv is None:
851 raise UsageError, \
852 _('Sorry, you need the csv module to use this function.\n'
853 'Get it from: http://www.object-craft.com.au/projects/csv/')
855 # grab the directory to export to
856 if len(args) < 1:
857 raise UsageError, _('Not enough arguments supplied')
858 dir = args[-1]
860 # get the list of classes to export
861 if len(args) == 2:
862 classes = args[0].split(',')
863 else:
864 classes = self.db.classes.keys()
866 # use the csv parser if we can - it's faster
867 p = csv.parser(field_sep=':')
869 # do all the classes specified
870 for classname in classes:
871 cl = self.get_class(classname)
872 f = open(os.path.join(dir, classname+'.csv'), 'w')
873 properties = cl.getprops()
874 propnames = properties.keys()
875 propnames.sort()
876 print >> f, p.join(propnames)
878 # all nodes for this class
879 for nodeid in cl.list():
880 print >>f, p.join(cl.export_list(propnames, nodeid))
881 return 0
883 def do_import(self, args):
884 '''Usage: import import_dir
885 Import a database from the directory containing CSV files, one per
886 class to import.
888 The files must define the same properties as the class (including having
889 a "header" line with those property names.)
891 The imported nodes will have the same nodeid as defined in the
892 import file, thus replacing any existing content.
894 The new nodes are added to the existing database - if you want to
895 create a new database using the imported data, then create a new
896 database (or, tediously, retire all the old data.)
897 '''
898 if len(args) < 1:
899 raise UsageError, _('Not enough arguments supplied')
900 if csv is None:
901 raise UsageError, \
902 _('Sorry, you need the csv module to use this function.\n'
903 'Get it from: http://www.object-craft.com.au/projects/csv/')
905 from roundup import hyperdb
907 for file in os.listdir(args[0]):
908 f = open(os.path.join(args[0], file))
910 # get the classname
911 classname = os.path.splitext(file)[0]
913 # ensure that the properties and the CSV file headings match
914 cl = self.get_class(classname)
915 p = csv.parser(field_sep=':')
916 file_props = p.parse(f.readline())
917 properties = cl.getprops()
918 propnames = properties.keys()
919 propnames.sort()
920 m = file_props[:]
921 m.sort()
922 if m != propnames:
923 raise UsageError, _('Import file doesn\'t define the same '
924 'properties as "%(arg0)s".')%{'arg0': args[0]}
926 # loop through the file and create a node for each entry
927 maxid = 1
928 while 1:
929 line = f.readline()
930 if not line: break
932 # parse lines until we get a complete entry
933 while 1:
934 l = p.parse(line)
935 if l: break
936 line = f.readline()
937 if not line:
938 raise ValueError, "Unexpected EOF during CSV parse"
940 # do the import and figure the current highest nodeid
941 maxid = max(maxid, int(cl.import_list(propnames, l)))
943 print 'setting', classname, maxid+1
944 self.db.setid(classname, str(maxid+1))
945 return 0
947 def do_pack(self, args):
948 '''Usage: pack period | date
950 Remove journal entries older than a period of time specified or
951 before a certain date.
953 A period is specified using the suffixes "y", "m", and "d". The
954 suffix "w" (for "week") means 7 days.
956 "3y" means three years
957 "2y 1m" means two years and one month
958 "1m 25d" means one month and 25 days
959 "2w 3d" means two weeks and three days
961 Date format is "YYYY-MM-DD" eg:
962 2001-01-01
964 '''
965 if len(args) <> 1:
966 raise UsageError, _('Not enough arguments supplied')
968 # are we dealing with a period or a date
969 value = args[0]
970 date_re = re.compile(r'''
971 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
972 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
973 ''', re.VERBOSE)
974 m = date_re.match(value)
975 if not m:
976 raise ValueError, _('Invalid format')
977 m = m.groupdict()
978 if m['period']:
979 pack_before = date.Date(". - %s"%value)
980 elif m['date']:
981 pack_before = date.Date(value)
982 self.db.pack(pack_before)
983 return 0
985 def do_reindex(self, args):
986 '''Usage: reindex
987 Re-generate a tracker's search indexes.
989 This will re-generate the search indexes for a tracker. This will
990 typically happen automatically.
991 '''
992 self.db.indexer.force_reindex()
993 self.db.reindex()
994 return 0
996 def do_security(self, args):
997 '''Usage: security [Role name]
998 Display the Permissions available to one or all Roles.
999 '''
1000 if len(args) == 1:
1001 role = args[0]
1002 try:
1003 roles = [(args[0], self.db.security.role[args[0]])]
1004 except KeyError:
1005 print _('No such Role "%(role)s"')%locals()
1006 return 1
1007 else:
1008 roles = self.db.security.role.items()
1009 role = self.db.config.NEW_WEB_USER_ROLES
1010 if ',' in role:
1011 print _('New Web users get the Roles "%(role)s"')%locals()
1012 else:
1013 print _('New Web users get the Role "%(role)s"')%locals()
1014 role = self.db.config.NEW_EMAIL_USER_ROLES
1015 if ',' in role:
1016 print _('New Email users get the Roles "%(role)s"')%locals()
1017 else:
1018 print _('New Email users get the Role "%(role)s"')%locals()
1019 roles.sort()
1020 for rolename, role in roles:
1021 print _('Role "%(name)s":')%role.__dict__
1022 for permission in role.permissions:
1023 if permission.klass:
1024 print _(' %(description)s (%(name)s for "%(klass)s" '
1025 'only)')%permission.__dict__
1026 else:
1027 print _(' %(description)s (%(name)s)')%permission.__dict__
1028 return 0
1030 def run_command(self, args):
1031 '''Run a single command
1032 '''
1033 command = args[0]
1035 # handle help now
1036 if command == 'help':
1037 if len(args)>1:
1038 self.do_help(args[1:])
1039 return 0
1040 self.do_help(['help'])
1041 return 0
1042 if command == 'morehelp':
1043 self.do_help(['help'])
1044 self.help_commands()
1045 self.help_all()
1046 return 0
1048 # figure what the command is
1049 try:
1050 functions = self.commands.get(command)
1051 except KeyError:
1052 # not a valid command
1053 print _('Unknown command "%(command)s" ("help commands" for a '
1054 'list)')%locals()
1055 return 1
1057 # check for multiple matches
1058 if len(functions) > 1:
1059 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1060 command, 'list': ', '.join([i[0] for i in functions])}
1061 return 1
1062 command, function = functions[0]
1064 # make sure we have a tracker_home
1065 while not self.tracker_home:
1066 self.tracker_home = raw_input(_('Enter tracker home: ')).strip()
1068 # before we open the db, we may be doing an install or init
1069 if command == 'initialise':
1070 try:
1071 return self.do_initialise(self.tracker_home, args)
1072 except UsageError, message:
1073 print _('Error: %(message)s')%locals()
1074 return 1
1075 elif command == 'install':
1076 try:
1077 return self.do_install(self.tracker_home, args)
1078 except UsageError, message:
1079 print _('Error: %(message)s')%locals()
1080 return 1
1082 # get the tracker
1083 try:
1084 tracker = roundup.instance.open(self.tracker_home)
1085 except ValueError, message:
1086 self.tracker_home = ''
1087 print _("Error: Couldn't open tracker: %(message)s")%locals()
1088 return 1
1090 # only open the database once!
1091 if not self.db:
1092 self.db = tracker.open('admin')
1094 # do the command
1095 ret = 0
1096 try:
1097 ret = function(args[1:])
1098 except UsageError, message:
1099 print _('Error: %(message)s')%locals()
1100 print
1101 print function.__doc__
1102 ret = 1
1103 except:
1104 import traceback
1105 traceback.print_exc()
1106 ret = 1
1107 return ret
1109 def interactive(self):
1110 '''Run in an interactive mode
1111 '''
1112 print _('Roundup %s ready for input.'%roundup_version)
1113 print _('Type "help" for help.')
1114 try:
1115 import readline
1116 except ImportError:
1117 print _('Note: command history and editing not available')
1119 while 1:
1120 try:
1121 command = raw_input(_('roundup> '))
1122 except EOFError:
1123 print _('exit...')
1124 break
1125 if not command: continue
1126 args = token.token_split(command)
1127 if not args: continue
1128 if args[0] in ('quit', 'exit'): break
1129 self.run_command(args)
1131 # exit.. check for transactions
1132 if self.db and self.db.transactions:
1133 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1134 if commit and commit[0].lower() == 'y':
1135 self.db.commit()
1136 return 0
1138 def main(self):
1139 try:
1140 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1141 except getopt.GetoptError, e:
1142 self.usage(str(e))
1143 return 1
1145 # handle command-line args
1146 self.tracker_home = os.environ.get('TRACKER_HOME', '')
1147 # TODO: reinstate the user/password stuff (-u arg too)
1148 name = password = ''
1149 if os.environ.has_key('ROUNDUP_LOGIN'):
1150 l = os.environ['ROUNDUP_LOGIN'].split(':')
1151 name = l[0]
1152 if len(l) > 1:
1153 password = l[1]
1154 self.comma_sep = 0
1155 for opt, arg in opts:
1156 if opt == '-h':
1157 self.usage()
1158 return 0
1159 if opt == '-i':
1160 self.tracker_home = arg
1161 if opt == '-c':
1162 self.comma_sep = 1
1164 # if no command - go interactive
1165 # wrap in a try/finally so we always close off the db
1166 ret = 0
1167 try:
1168 if not args:
1169 self.interactive()
1170 else:
1171 ret = self.run_command(args)
1172 if self.db: self.db.commit()
1173 return ret
1174 finally:
1175 if self.db:
1176 self.db.close()
1178 if __name__ == '__main__':
1179 tool = AdminTool()
1180 sys.exit(tool.main())
1182 # vim: set filetype=python ts=4 sw=4 et si