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.31 2002-09-18 05:07:47 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):
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 props[key] = password.Password(value)
451 elif isinstance(proptype, hyperdb.Date):
452 try:
453 props[key] = date.Date(value)
454 except ValueError, message:
455 raise UsageError, '"%s": %s'%(value, message)
456 elif isinstance(proptype, hyperdb.Interval):
457 try:
458 props[key] = date.Interval(value)
459 except ValueError, message:
460 raise UsageError, '"%s": %s'%(value, message)
461 elif isinstance(proptype, hyperdb.Link):
462 props[key] = value
463 elif isinstance(proptype, hyperdb.Boolean):
464 props[key] = value.lower() in ('yes', 'true', 'on', '1')
465 elif isinstance(proptype, hyperdb.Number):
466 props[key] = int(value)
468 # try the set
469 try:
470 apply(cl.set, (itemid, ), props)
471 except (TypeError, IndexError, ValueError), message:
472 raise UsageError, message
473 return 0
475 def do_find(self, args):
476 '''Usage: find classname propname=value ...
477 Find the nodes of the given class with a given link property value.
479 Find the nodes of the given class with a given link property value. The
480 value may be either the nodeid of the linked node, or its key value.
481 '''
482 if len(args) < 1:
483 raise UsageError, _('Not enough arguments supplied')
484 classname = args[0]
485 # get the class
486 cl = self.get_class(classname)
488 # handle the propname=value argument
489 props = self.props_from_args(args[1:])
491 # if the value isn't a number, look up the linked class to get the
492 # number
493 for propname, value in props.items():
494 num_re = re.compile('^\d+$')
495 if not num_re.match(value):
496 # get the property
497 try:
498 property = cl.properties[propname]
499 except KeyError:
500 raise UsageError, _('%(classname)s has no property '
501 '"%(propname)s"')%locals()
503 # make sure it's a link
504 if (not isinstance(property, hyperdb.Link) and not
505 isinstance(property, hyperdb.Multilink)):
506 raise UsageError, _('You may only "find" link properties')
508 # get the linked-to class and look up the key property
509 link_class = self.db.getclass(property.classname)
510 try:
511 props[propname] = link_class.lookup(value)
512 except TypeError:
513 raise UsageError, _('%(classname)s has no key property"')%{
514 'classname': link_class.classname}
516 # now do the find
517 try:
518 if self.comma_sep:
519 print ','.join(apply(cl.find, (), props))
520 else:
521 print apply(cl.find, (), props)
522 except KeyError:
523 raise UsageError, _('%(classname)s has no property '
524 '"%(propname)s"')%locals()
525 except (ValueError, TypeError), message:
526 raise UsageError, message
527 return 0
529 def do_specification(self, args):
530 '''Usage: specification classname
531 Show the properties for a classname.
533 This lists the properties for a given class.
534 '''
535 if len(args) < 1:
536 raise UsageError, _('Not enough arguments supplied')
537 classname = args[0]
538 # get the class
539 cl = self.get_class(classname)
541 # get the key property
542 keyprop = cl.getkey()
543 for key, value in cl.properties.items():
544 if keyprop == key:
545 print _('%(key)s: %(value)s (key property)')%locals()
546 else:
547 print _('%(key)s: %(value)s')%locals()
549 def do_display(self, args):
550 '''Usage: display designator
551 Show the property values for the given node.
553 This lists the properties and their associated values for the given
554 node.
555 '''
556 if len(args) < 1:
557 raise UsageError, _('Not enough arguments supplied')
559 # decode the node designator
560 try:
561 classname, nodeid = hyperdb.splitDesignator(args[0])
562 except hyperdb.DesignatorError, message:
563 raise UsageError, message
565 # get the class
566 cl = self.get_class(classname)
568 # display the values
569 for key in cl.properties.keys():
570 value = cl.get(nodeid, key)
571 print _('%(key)s: %(value)s')%locals()
573 def do_create(self, args):
574 '''Usage: create classname property=value ...
575 Create a new entry of a given class.
577 This creates a new entry of the given class using the property
578 name=value arguments provided on the command line after the "create"
579 command.
580 '''
581 if len(args) < 1:
582 raise UsageError, _('Not enough arguments supplied')
583 from roundup import hyperdb
585 classname = args[0]
587 # get the class
588 cl = self.get_class(classname)
590 # now do a create
591 props = {}
592 properties = cl.getprops(protected = 0)
593 if len(args) == 1:
594 # ask for the properties
595 for key, value in properties.items():
596 if key == 'id': continue
597 name = value.__class__.__name__
598 if isinstance(value , hyperdb.Password):
599 again = None
600 while value != again:
601 value = getpass.getpass(_('%(propname)s (Password): ')%{
602 'propname': key.capitalize()})
603 again = getpass.getpass(_(' %(propname)s (Again): ')%{
604 'propname': key.capitalize()})
605 if value != again: print _('Sorry, try again...')
606 if value:
607 props[key] = value
608 else:
609 value = raw_input(_('%(propname)s (%(proptype)s): ')%{
610 'propname': key.capitalize(), 'proptype': name})
611 if value:
612 props[key] = value
613 else:
614 props = self.props_from_args(args[1:])
616 # convert types
617 for propname, value in props.items():
618 # get the property
619 try:
620 proptype = properties[propname]
621 except KeyError:
622 raise UsageError, _('%(classname)s has no property '
623 '"%(propname)s"')%locals()
625 if isinstance(proptype, hyperdb.Date):
626 try:
627 props[propname] = date.Date(value)
628 except ValueError, message:
629 raise UsageError, _('"%(value)s": %(message)s')%locals()
630 elif isinstance(proptype, hyperdb.Interval):
631 try:
632 props[propname] = date.Interval(value)
633 except ValueError, message:
634 raise UsageError, _('"%(value)s": %(message)s')%locals()
635 elif isinstance(proptype, hyperdb.Password):
636 props[propname] = password.Password(value)
637 elif isinstance(proptype, hyperdb.Multilink):
638 props[propname] = value.split(',')
639 elif isinstance(proptype, hyperdb.Boolean):
640 props[propname] = value.lower() in ('yes', 'true', 'on', '1')
641 elif isinstance(proptype, hyperdb.Number):
642 props[propname] = int(value)
644 # check for the key property
645 propname = cl.getkey()
646 if propname and not props.has_key(propname):
647 raise UsageError, _('you must provide the "%(propname)s" '
648 'property.')%locals()
650 # do the actual create
651 try:
652 print apply(cl.create, (), props)
653 except (TypeError, IndexError, ValueError), message:
654 raise UsageError, message
655 return 0
657 def do_list(self, args):
658 '''Usage: list classname [property]
659 List the instances of a class.
661 Lists all instances of the given class. If the property is not
662 specified, the "label" property is used. The label property is tried
663 in order: the key, "name", "title" and then the first property,
664 alphabetically.
665 '''
666 if len(args) < 1:
667 raise UsageError, _('Not enough arguments supplied')
668 classname = args[0]
670 # get the class
671 cl = self.get_class(classname)
673 # figure the property
674 if len(args) > 1:
675 propname = args[1]
676 else:
677 propname = cl.labelprop()
679 if self.comma_sep:
680 print ','.join(cl.list())
681 else:
682 for nodeid in cl.list():
683 try:
684 value = cl.get(nodeid, propname)
685 except KeyError:
686 raise UsageError, _('%(classname)s has no property '
687 '"%(propname)s"')%locals()
688 print _('%(nodeid)4s: %(value)s')%locals()
689 return 0
691 def do_table(self, args):
692 '''Usage: table classname [property[,property]*]
693 List the instances of a class in tabular form.
695 Lists all instances of the given class. If the properties are not
696 specified, all properties are displayed. By default, the column widths
697 are the width of the property names. The width may be explicitly defined
698 by defining the property as "name:width". For example::
699 roundup> table priority id,name:10
700 Id Name
701 1 fatal-bug
702 2 bug
703 3 usability
704 4 feature
705 '''
706 if len(args) < 1:
707 raise UsageError, _('Not enough arguments supplied')
708 classname = args[0]
710 # get the class
711 cl = self.get_class(classname)
713 # figure the property names to display
714 if len(args) > 1:
715 prop_names = args[1].split(',')
716 all_props = cl.getprops()
717 for spec in prop_names:
718 if ':' in spec:
719 try:
720 propname, width = spec.split(':')
721 except (ValueError, TypeError):
722 raise UsageError, _('"%(spec)s" not name:width')%locals()
723 else:
724 propname = spec
725 if not all_props.has_key(propname):
726 raise UsageError, _('%(classname)s has no property '
727 '"%(propname)s"')%locals()
728 else:
729 prop_names = cl.getprops().keys()
731 # now figure column widths
732 props = []
733 for spec in prop_names:
734 if ':' in spec:
735 name, width = spec.split(':')
736 props.append((name, int(width)))
737 else:
738 props.append((spec, len(spec)))
740 # now display the heading
741 print ' '.join([name.capitalize().ljust(width) for name,width in props])
743 # and the table data
744 for nodeid in cl.list():
745 l = []
746 for name, width in props:
747 if name != 'id':
748 try:
749 value = str(cl.get(nodeid, name))
750 except KeyError:
751 # we already checked if the property is valid - a
752 # KeyError here means the node just doesn't have a
753 # value for it
754 value = ''
755 else:
756 value = str(nodeid)
757 f = '%%-%ds'%width
758 l.append(f%value[:width])
759 print ' '.join(l)
760 return 0
762 def do_history(self, args):
763 '''Usage: history designator
764 Show the history entries of a designator.
766 Lists the journal entries for the node identified by the designator.
767 '''
768 if len(args) < 1:
769 raise UsageError, _('Not enough arguments supplied')
770 try:
771 classname, nodeid = hyperdb.splitDesignator(args[0])
772 except hyperdb.DesignatorError, message:
773 raise UsageError, message
775 try:
776 print self.db.getclass(classname).history(nodeid)
777 except KeyError:
778 raise UsageError, _('no such class "%(classname)s"')%locals()
779 except IndexError:
780 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
781 return 0
783 def do_commit(self, args):
784 '''Usage: commit
785 Commit all changes made to the database.
787 The changes made during an interactive session are not
788 automatically written to the database - they must be committed
789 using this command.
791 One-off commands on the command-line are automatically committed if
792 they are successful.
793 '''
794 self.db.commit()
795 return 0
797 def do_rollback(self, args):
798 '''Usage: rollback
799 Undo all changes that are pending commit to the database.
801 The changes made during an interactive session are not
802 automatically written to the database - they must be committed
803 manually. This command undoes all those changes, so a commit
804 immediately after would make no changes to the database.
805 '''
806 self.db.rollback()
807 return 0
809 def do_retire(self, args):
810 '''Usage: retire designator[,designator]*
811 Retire the node specified by designator.
813 This action indicates that a particular node is not to be retrieved by
814 the list or find commands, and its key value may be re-used.
815 '''
816 if len(args) < 1:
817 raise UsageError, _('Not enough arguments supplied')
818 designators = args[0].split(',')
819 for designator in designators:
820 try:
821 classname, nodeid = hyperdb.splitDesignator(designator)
822 except hyperdb.DesignatorError, message:
823 raise UsageError, message
824 try:
825 self.db.getclass(classname).retire(nodeid)
826 except KeyError:
827 raise UsageError, _('no such class "%(classname)s"')%locals()
828 except IndexError:
829 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
830 return 0
832 def do_export(self, args):
833 '''Usage: export [class[,class]] export_dir
834 Export the database to colon-separated-value files.
836 This action exports the current data from the database into
837 colon-separated-value files that are placed in the nominated
838 destination directory. The journals are not exported.
839 '''
840 # we need the CSV module
841 if csv is None:
842 raise UsageError, \
843 _('Sorry, you need the csv module to use this function.\n'
844 'Get it from: http://www.object-craft.com.au/projects/csv/')
846 # grab the directory to export to
847 if len(args) < 1:
848 raise UsageError, _('Not enough arguments supplied')
849 dir = args[-1]
851 # get the list of classes to export
852 if len(args) == 2:
853 classes = args[0].split(',')
854 else:
855 classes = self.db.classes.keys()
857 # use the csv parser if we can - it's faster
858 p = csv.parser(field_sep=':')
860 # do all the classes specified
861 for classname in classes:
862 cl = self.get_class(classname)
863 f = open(os.path.join(dir, classname+'.csv'), 'w')
864 properties = cl.getprops()
865 propnames = properties.keys()
866 propnames.sort()
867 print >> f, p.join(propnames)
869 # all nodes for this class
870 for nodeid in cl.list():
871 print >>f, p.join(cl.export_list(propnames, nodeid))
872 return 0
874 def do_import(self, args):
875 '''Usage: import import_dir
876 Import a database from the directory containing CSV files, one per
877 class to import.
879 The files must define the same properties as the class (including having
880 a "header" line with those property names.)
882 The imported nodes will have the same nodeid as defined in the
883 import file, thus replacing any existing content.
885 The new nodes are added to the existing database - if you want to
886 create a new database using the imported data, then create a new
887 database (or, tediously, retire all the old data.)
888 '''
889 if len(args) < 1:
890 raise UsageError, _('Not enough arguments supplied')
891 if csv is None:
892 raise UsageError, \
893 _('Sorry, you need the csv module to use this function.\n'
894 'Get it from: http://www.object-craft.com.au/projects/csv/')
896 from roundup import hyperdb
898 for file in os.listdir(args[0]):
899 f = open(os.path.join(args[0], file))
901 # get the classname
902 classname = os.path.splitext(file)[0]
904 # ensure that the properties and the CSV file headings match
905 cl = self.get_class(classname)
906 p = csv.parser(field_sep=':')
907 file_props = p.parse(f.readline())
908 properties = cl.getprops()
909 propnames = properties.keys()
910 propnames.sort()
911 m = file_props[:]
912 m.sort()
913 if m != propnames:
914 raise UsageError, _('Import file doesn\'t define the same '
915 'properties as "%(arg0)s".')%{'arg0': args[0]}
917 # loop through the file and create a node for each entry
918 maxid = 1
919 while 1:
920 line = f.readline()
921 if not line: break
923 # parse lines until we get a complete entry
924 while 1:
925 l = p.parse(line)
926 if l: break
927 line = f.readline()
928 if not line:
929 raise ValueError, "Unexpected EOF during CSV parse"
931 # do the import and figure the current highest nodeid
932 maxid = max(maxid, int(cl.import_list(propnames, l)))
934 print 'setting', classname, maxid
935 self.db.setid(classname, str(maxid))
936 return 0
938 def do_pack(self, args):
939 '''Usage: pack period | date
941 Remove journal entries older than a period of time specified or
942 before a certain date.
944 A period is specified using the suffixes "y", "m", and "d". The
945 suffix "w" (for "week") means 7 days.
947 "3y" means three years
948 "2y 1m" means two years and one month
949 "1m 25d" means one month and 25 days
950 "2w 3d" means two weeks and three days
952 Date format is "YYYY-MM-DD" eg:
953 2001-01-01
955 '''
956 if len(args) <> 1:
957 raise UsageError, _('Not enough arguments supplied')
959 # are we dealing with a period or a date
960 value = args[0]
961 date_re = re.compile(r'''
962 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
963 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
964 ''', re.VERBOSE)
965 m = date_re.match(value)
966 if not m:
967 raise ValueError, _('Invalid format')
968 m = m.groupdict()
969 if m['period']:
970 pack_before = date.Date(". - %s"%value)
971 elif m['date']:
972 pack_before = date.Date(value)
973 self.db.pack(pack_before)
974 return 0
976 def do_reindex(self, args):
977 '''Usage: reindex
978 Re-generate a tracker's search indexes.
980 This will re-generate the search indexes for a tracker. This will
981 typically happen automatically.
982 '''
983 self.db.indexer.force_reindex()
984 self.db.reindex()
985 return 0
987 def do_security(self, args):
988 '''Usage: security [Role name]
989 Display the Permissions available to one or all Roles.
990 '''
991 if len(args) == 1:
992 role = args[0]
993 try:
994 roles = [(args[0], self.db.security.role[args[0]])]
995 except KeyError:
996 print _('No such Role "%(role)s"')%locals()
997 return 1
998 else:
999 roles = self.db.security.role.items()
1000 role = self.db.config.NEW_WEB_USER_ROLES
1001 if ',' in role:
1002 print _('New Web users get the Roles "%(role)s"')%locals()
1003 else:
1004 print _('New Web users get the Role "%(role)s"')%locals()
1005 role = self.db.config.NEW_EMAIL_USER_ROLES
1006 if ',' in role:
1007 print _('New Email users get the Roles "%(role)s"')%locals()
1008 else:
1009 print _('New Email users get the Role "%(role)s"')%locals()
1010 roles.sort()
1011 for rolename, role in roles:
1012 print _('Role "%(name)s":')%role.__dict__
1013 for permission in role.permissions:
1014 if permission.klass:
1015 print _(' %(description)s (%(name)s for "%(klass)s" '
1016 'only)')%permission.__dict__
1017 else:
1018 print _(' %(description)s (%(name)s)')%permission.__dict__
1019 return 0
1021 def run_command(self, args):
1022 '''Run a single command
1023 '''
1024 command = args[0]
1026 # handle help now
1027 if command == 'help':
1028 if len(args)>1:
1029 self.do_help(args[1:])
1030 return 0
1031 self.do_help(['help'])
1032 return 0
1033 if command == 'morehelp':
1034 self.do_help(['help'])
1035 self.help_commands()
1036 self.help_all()
1037 return 0
1039 # figure what the command is
1040 try:
1041 functions = self.commands.get(command)
1042 except KeyError:
1043 # not a valid command
1044 print _('Unknown command "%(command)s" ("help commands" for a '
1045 'list)')%locals()
1046 return 1
1048 # check for multiple matches
1049 if len(functions) > 1:
1050 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1051 command, 'list': ', '.join([i[0] for i in functions])}
1052 return 1
1053 command, function = functions[0]
1055 # make sure we have a tracker_home
1056 while not self.tracker_home:
1057 self.tracker_home = raw_input(_('Enter tracker home: ')).strip()
1059 # before we open the db, we may be doing an install or init
1060 if command == 'initialise':
1061 try:
1062 return self.do_initialise(self.tracker_home, args)
1063 except UsageError, message:
1064 print _('Error: %(message)s')%locals()
1065 return 1
1066 elif command == 'install':
1067 try:
1068 return self.do_install(self.tracker_home, args)
1069 except UsageError, message:
1070 print _('Error: %(message)s')%locals()
1071 return 1
1073 # get the tracker
1074 try:
1075 tracker = roundup.instance.open(self.tracker_home)
1076 except ValueError, message:
1077 self.tracker_home = ''
1078 print _("Error: Couldn't open tracker: %(message)s")%locals()
1079 return 1
1081 # only open the database once!
1082 if not self.db:
1083 self.db = tracker.open('admin')
1085 # do the command
1086 ret = 0
1087 try:
1088 ret = function(args[1:])
1089 except UsageError, message:
1090 print _('Error: %(message)s')%locals()
1091 print
1092 print function.__doc__
1093 ret = 1
1094 except:
1095 import traceback
1096 traceback.print_exc()
1097 ret = 1
1098 return ret
1100 def interactive(self):
1101 '''Run in an interactive mode
1102 '''
1103 print _('Roundup %s ready for input.'%roundup_version)
1104 print _('Type "help" for help.')
1105 try:
1106 import readline
1107 except ImportError:
1108 print _('Note: command history and editing not available')
1110 while 1:
1111 try:
1112 command = raw_input(_('roundup> '))
1113 except EOFError:
1114 print _('exit...')
1115 break
1116 if not command: continue
1117 args = token.token_split(command)
1118 if not args: continue
1119 if args[0] in ('quit', 'exit'): break
1120 self.run_command(args)
1122 # exit.. check for transactions
1123 if self.db and self.db.transactions:
1124 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1125 if commit and commit[0].lower() == 'y':
1126 self.db.commit()
1127 return 0
1129 def main(self):
1130 try:
1131 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1132 except getopt.GetoptError, e:
1133 self.usage(str(e))
1134 return 1
1136 # handle command-line args
1137 self.tracker_home = os.environ.get('TRACKER_HOME', '')
1138 # TODO: reinstate the user/password stuff (-u arg too)
1139 name = password = ''
1140 if os.environ.has_key('ROUNDUP_LOGIN'):
1141 l = os.environ['ROUNDUP_LOGIN'].split(':')
1142 name = l[0]
1143 if len(l) > 1:
1144 password = l[1]
1145 self.comma_sep = 0
1146 for opt, arg in opts:
1147 if opt == '-h':
1148 self.usage()
1149 return 0
1150 if opt == '-i':
1151 self.tracker_home = arg
1152 if opt == '-c':
1153 self.comma_sep = 1
1155 # if no command - go interactive
1156 # wrap in a try/finally so we always close off the db
1157 ret = 0
1158 try:
1159 if not args:
1160 self.interactive()
1161 else:
1162 ret = self.run_command(args)
1163 if self.db: self.db.commit()
1164 return ret
1165 finally:
1166 if self.db:
1167 self.db.close()
1169 if __name__ == '__main__':
1170 tool = AdminTool()
1171 sys.exit(tool.main())
1173 # vim: set filetype=python ts=4 sw=4 et si