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.30 2002-09-13 00:08:43 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'
304 # install!
305 init.install(tracker_home, template, backend)
307 print _('''
308 You should now edit the tracker configuration file:
309 %(config_file)s
310 ... at a minimum, you must set MAILHOST, MAIL_DOMAIN and ADMIN_EMAIL.
312 If you wish to modify the default schema, you should also edit the database
313 initialisation file:
314 %(database_config_file)s
315 ... see the documentation on customizing for more information.
316 ''')%{
317 'config_file': os.path.join(tracker_home, 'config.py'),
318 'database_config_file': os.path.join(tracker_home, 'dbinit.py')
319 }
320 return 0
323 def do_initialise(self, tracker_home, args):
324 '''Usage: initialise [adminpw]
325 Initialise a new Roundup tracker.
327 The administrator details will be set at this step.
329 Execute the tracker's initialisation function dbinit.init()
330 '''
331 # password
332 if len(args) > 1:
333 adminpw = args[1]
334 else:
335 adminpw = ''
336 confirm = 'x'
337 while adminpw != confirm:
338 adminpw = getpass.getpass(_('Admin Password: '))
339 confirm = getpass.getpass(_(' Confirm: '))
341 # make sure the tracker home is installed
342 if not os.path.exists(tracker_home):
343 raise UsageError, _('Instance home does not exist')%locals()
344 if not os.path.exists(os.path.join(tracker_home, 'html')):
345 raise UsageError, _('Instance has not been installed')%locals()
347 # is there already a database?
348 if os.path.exists(os.path.join(tracker_home, 'db')):
349 print _('WARNING: The database is already initialised!')
350 print _('If you re-initialise it, you will lose all the data!')
351 ok = raw_input(_('Erase it? Y/[N]: ')).strip()
352 if ok.lower() != 'y':
353 return 0
355 # nuke it
356 shutil.rmtree(os.path.join(tracker_home, 'db'))
358 # GO
359 init.initialise(tracker_home, adminpw)
361 return 0
364 def do_get(self, args):
365 '''Usage: get property designator[,designator]*
366 Get the given property of one or more designator(s).
368 Retrieves the property value of the nodes specified by the designators.
369 '''
370 if len(args) < 2:
371 raise UsageError, _('Not enough arguments supplied')
372 propname = args[0]
373 designators = args[1].split(',')
374 l = []
375 for designator in designators:
376 # decode the node designator
377 try:
378 classname, nodeid = hyperdb.splitDesignator(designator)
379 except hyperdb.DesignatorError, message:
380 raise UsageError, message
382 # get the class
383 cl = self.get_class(classname)
384 try:
385 if self.comma_sep:
386 l.append(cl.get(nodeid, propname))
387 else:
388 print cl.get(nodeid, propname)
389 except IndexError:
390 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
391 except KeyError:
392 raise UsageError, _('no such %(classname)s property '
393 '"%(propname)s"')%locals()
394 if self.comma_sep:
395 print ','.join(l)
396 return 0
399 def do_set(self, args):
400 '''Usage: set [items] property=value property=value ...
401 Set the given properties of one or more items(s).
403 The items may be specified as a class or as a comma-separeted
404 list of item designators (ie "designator[,designator,...]").
406 This command sets the properties to the values for all designators
407 given. If the value is missing (ie. "property=") then the property is
408 un-set.
409 '''
410 if len(args) < 2:
411 raise UsageError, _('Not enough arguments supplied')
412 from roundup import hyperdb
414 designators = args[0].split(',')
415 if len(designators) == 1:
416 designator = designators[0]
417 try:
418 designator = hyperdb.splitDesignator(designator)
419 designators = [designator]
420 except hyperdb.DesignatorError:
421 cl = self.get_class(designator)
422 designators = [(designator, x) for x in cl.list()]
423 else:
424 try:
425 designators = [hyperdb.splitDesignator(x) for x in designators]
426 except hyperdb.DesignatorError, message:
427 raise UsageError, message
429 # get the props from the args
430 props = self.props_from_args(args[1:])
432 # now do the set for all the nodes
433 for classname, itemid in designators:
434 cl = self.get_class(classname)
436 properties = cl.getprops()
437 for key, value in props.items():
438 proptype = properties[key]
439 if isinstance(proptype, hyperdb.Multilink):
440 if value is None:
441 props[key] = []
442 else:
443 props[key] = value.split(',')
444 elif value is None:
445 continue
446 elif isinstance(proptype, hyperdb.String):
447 continue
448 elif isinstance(proptype, hyperdb.Password):
449 props[key] = password.Password(value)
450 elif isinstance(proptype, hyperdb.Date):
451 try:
452 props[key] = date.Date(value)
453 except ValueError, message:
454 raise UsageError, '"%s": %s'%(value, message)
455 elif isinstance(proptype, hyperdb.Interval):
456 try:
457 props[key] = date.Interval(value)
458 except ValueError, message:
459 raise UsageError, '"%s": %s'%(value, message)
460 elif isinstance(proptype, hyperdb.Link):
461 props[key] = value
462 elif isinstance(proptype, hyperdb.Boolean):
463 props[key] = value.lower() in ('yes', 'true', 'on', '1')
464 elif isinstance(proptype, hyperdb.Number):
465 props[key] = int(value)
467 # try the set
468 try:
469 apply(cl.set, (itemid, ), props)
470 except (TypeError, IndexError, ValueError), message:
471 raise UsageError, message
472 return 0
474 def do_find(self, args):
475 '''Usage: find classname propname=value ...
476 Find the nodes of the given class with a given link property value.
478 Find the nodes of the given class with a given link property value. The
479 value may be either the nodeid of the linked node, or its key value.
480 '''
481 if len(args) < 1:
482 raise UsageError, _('Not enough arguments supplied')
483 classname = args[0]
484 # get the class
485 cl = self.get_class(classname)
487 # handle the propname=value argument
488 props = self.props_from_args(args[1:])
490 # if the value isn't a number, look up the linked class to get the
491 # number
492 for propname, value in props.items():
493 num_re = re.compile('^\d+$')
494 if not num_re.match(value):
495 # get the property
496 try:
497 property = cl.properties[propname]
498 except KeyError:
499 raise UsageError, _('%(classname)s has no property '
500 '"%(propname)s"')%locals()
502 # make sure it's a link
503 if (not isinstance(property, hyperdb.Link) and not
504 isinstance(property, hyperdb.Multilink)):
505 raise UsageError, _('You may only "find" link properties')
507 # get the linked-to class and look up the key property
508 link_class = self.db.getclass(property.classname)
509 try:
510 props[propname] = link_class.lookup(value)
511 except TypeError:
512 raise UsageError, _('%(classname)s has no key property"')%{
513 'classname': link_class.classname}
515 # now do the find
516 try:
517 if self.comma_sep:
518 print ','.join(apply(cl.find, (), props))
519 else:
520 print apply(cl.find, (), props)
521 except KeyError:
522 raise UsageError, _('%(classname)s has no property '
523 '"%(propname)s"')%locals()
524 except (ValueError, TypeError), message:
525 raise UsageError, message
526 return 0
528 def do_specification(self, args):
529 '''Usage: specification classname
530 Show the properties for a classname.
532 This lists the properties for a given class.
533 '''
534 if len(args) < 1:
535 raise UsageError, _('Not enough arguments supplied')
536 classname = args[0]
537 # get the class
538 cl = self.get_class(classname)
540 # get the key property
541 keyprop = cl.getkey()
542 for key, value in cl.properties.items():
543 if keyprop == key:
544 print _('%(key)s: %(value)s (key property)')%locals()
545 else:
546 print _('%(key)s: %(value)s')%locals()
548 def do_display(self, args):
549 '''Usage: display designator
550 Show the property values for the given node.
552 This lists the properties and their associated values for the given
553 node.
554 '''
555 if len(args) < 1:
556 raise UsageError, _('Not enough arguments supplied')
558 # decode the node designator
559 try:
560 classname, nodeid = hyperdb.splitDesignator(args[0])
561 except hyperdb.DesignatorError, message:
562 raise UsageError, message
564 # get the class
565 cl = self.get_class(classname)
567 # display the values
568 for key in cl.properties.keys():
569 value = cl.get(nodeid, key)
570 print _('%(key)s: %(value)s')%locals()
572 def do_create(self, args):
573 '''Usage: create classname property=value ...
574 Create a new entry of a given class.
576 This creates a new entry of the given class using the property
577 name=value arguments provided on the command line after the "create"
578 command.
579 '''
580 if len(args) < 1:
581 raise UsageError, _('Not enough arguments supplied')
582 from roundup import hyperdb
584 classname = args[0]
586 # get the class
587 cl = self.get_class(classname)
589 # now do a create
590 props = {}
591 properties = cl.getprops(protected = 0)
592 if len(args) == 1:
593 # ask for the properties
594 for key, value in properties.items():
595 if key == 'id': continue
596 name = value.__class__.__name__
597 if isinstance(value , hyperdb.Password):
598 again = None
599 while value != again:
600 value = getpass.getpass(_('%(propname)s (Password): ')%{
601 'propname': key.capitalize()})
602 again = getpass.getpass(_(' %(propname)s (Again): ')%{
603 'propname': key.capitalize()})
604 if value != again: print _('Sorry, try again...')
605 if value:
606 props[key] = value
607 else:
608 value = raw_input(_('%(propname)s (%(proptype)s): ')%{
609 'propname': key.capitalize(), 'proptype': name})
610 if value:
611 props[key] = value
612 else:
613 props = self.props_from_args(args[1:])
615 # convert types
616 for propname, value in props.items():
617 # get the property
618 try:
619 proptype = properties[propname]
620 except KeyError:
621 raise UsageError, _('%(classname)s has no property '
622 '"%(propname)s"')%locals()
624 if isinstance(proptype, hyperdb.Date):
625 try:
626 props[propname] = date.Date(value)
627 except ValueError, message:
628 raise UsageError, _('"%(value)s": %(message)s')%locals()
629 elif isinstance(proptype, hyperdb.Interval):
630 try:
631 props[propname] = date.Interval(value)
632 except ValueError, message:
633 raise UsageError, _('"%(value)s": %(message)s')%locals()
634 elif isinstance(proptype, hyperdb.Password):
635 props[propname] = password.Password(value)
636 elif isinstance(proptype, hyperdb.Multilink):
637 props[propname] = value.split(',')
638 elif isinstance(proptype, hyperdb.Boolean):
639 props[propname] = value.lower() in ('yes', 'true', 'on', '1')
640 elif isinstance(proptype, hyperdb.Number):
641 props[propname] = int(value)
643 # check for the key property
644 propname = cl.getkey()
645 if propname and not props.has_key(propname):
646 raise UsageError, _('you must provide the "%(propname)s" '
647 'property.')%locals()
649 # do the actual create
650 try:
651 print apply(cl.create, (), props)
652 except (TypeError, IndexError, ValueError), message:
653 raise UsageError, message
654 return 0
656 def do_list(self, args):
657 '''Usage: list classname [property]
658 List the instances of a class.
660 Lists all instances of the given class. If the property is not
661 specified, the "label" property is used. The label property is tried
662 in order: the key, "name", "title" and then the first property,
663 alphabetically.
664 '''
665 if len(args) < 1:
666 raise UsageError, _('Not enough arguments supplied')
667 classname = args[0]
669 # get the class
670 cl = self.get_class(classname)
672 # figure the property
673 if len(args) > 1:
674 propname = args[1]
675 else:
676 propname = cl.labelprop()
678 if self.comma_sep:
679 print ','.join(cl.list())
680 else:
681 for nodeid in cl.list():
682 try:
683 value = cl.get(nodeid, propname)
684 except KeyError:
685 raise UsageError, _('%(classname)s has no property '
686 '"%(propname)s"')%locals()
687 print _('%(nodeid)4s: %(value)s')%locals()
688 return 0
690 def do_table(self, args):
691 '''Usage: table classname [property[,property]*]
692 List the instances of a class in tabular form.
694 Lists all instances of the given class. If the properties are not
695 specified, all properties are displayed. By default, the column widths
696 are the width of the property names. The width may be explicitly defined
697 by defining the property as "name:width". For example::
698 roundup> table priority id,name:10
699 Id Name
700 1 fatal-bug
701 2 bug
702 3 usability
703 4 feature
704 '''
705 if len(args) < 1:
706 raise UsageError, _('Not enough arguments supplied')
707 classname = args[0]
709 # get the class
710 cl = self.get_class(classname)
712 # figure the property names to display
713 if len(args) > 1:
714 prop_names = args[1].split(',')
715 all_props = cl.getprops()
716 for spec in prop_names:
717 if ':' in spec:
718 try:
719 propname, width = spec.split(':')
720 except (ValueError, TypeError):
721 raise UsageError, _('"%(spec)s" not name:width')%locals()
722 else:
723 propname = spec
724 if not all_props.has_key(propname):
725 raise UsageError, _('%(classname)s has no property '
726 '"%(propname)s"')%locals()
727 else:
728 prop_names = cl.getprops().keys()
730 # now figure column widths
731 props = []
732 for spec in prop_names:
733 if ':' in spec:
734 name, width = spec.split(':')
735 props.append((name, int(width)))
736 else:
737 props.append((spec, len(spec)))
739 # now display the heading
740 print ' '.join([name.capitalize().ljust(width) for name,width in props])
742 # and the table data
743 for nodeid in cl.list():
744 l = []
745 for name, width in props:
746 if name != 'id':
747 try:
748 value = str(cl.get(nodeid, name))
749 except KeyError:
750 # we already checked if the property is valid - a
751 # KeyError here means the node just doesn't have a
752 # value for it
753 value = ''
754 else:
755 value = str(nodeid)
756 f = '%%-%ds'%width
757 l.append(f%value[:width])
758 print ' '.join(l)
759 return 0
761 def do_history(self, args):
762 '''Usage: history designator
763 Show the history entries of a designator.
765 Lists the journal entries for the node identified by the designator.
766 '''
767 if len(args) < 1:
768 raise UsageError, _('Not enough arguments supplied')
769 try:
770 classname, nodeid = hyperdb.splitDesignator(args[0])
771 except hyperdb.DesignatorError, message:
772 raise UsageError, message
774 try:
775 print self.db.getclass(classname).history(nodeid)
776 except KeyError:
777 raise UsageError, _('no such class "%(classname)s"')%locals()
778 except IndexError:
779 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
780 return 0
782 def do_commit(self, args):
783 '''Usage: commit
784 Commit all changes made to the database.
786 The changes made during an interactive session are not
787 automatically written to the database - they must be committed
788 using this command.
790 One-off commands on the command-line are automatically committed if
791 they are successful.
792 '''
793 self.db.commit()
794 return 0
796 def do_rollback(self, args):
797 '''Usage: rollback
798 Undo all changes that are pending commit to the database.
800 The changes made during an interactive session are not
801 automatically written to the database - they must be committed
802 manually. This command undoes all those changes, so a commit
803 immediately after would make no changes to the database.
804 '''
805 self.db.rollback()
806 return 0
808 def do_retire(self, args):
809 '''Usage: retire designator[,designator]*
810 Retire the node specified by designator.
812 This action indicates that a particular node is not to be retrieved by
813 the list or find commands, and its key value may be re-used.
814 '''
815 if len(args) < 1:
816 raise UsageError, _('Not enough arguments supplied')
817 designators = args[0].split(',')
818 for designator in designators:
819 try:
820 classname, nodeid = hyperdb.splitDesignator(designator)
821 except hyperdb.DesignatorError, message:
822 raise UsageError, message
823 try:
824 self.db.getclass(classname).retire(nodeid)
825 except KeyError:
826 raise UsageError, _('no such class "%(classname)s"')%locals()
827 except IndexError:
828 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
829 return 0
831 def do_export(self, args):
832 '''Usage: export [class[,class]] export_dir
833 Export the database to colon-separated-value files.
835 This action exports the current data from the database into
836 colon-separated-value files that are placed in the nominated
837 destination directory. The journals are not exported.
838 '''
839 # we need the CSV module
840 if csv is None:
841 raise UsageError, \
842 _('Sorry, you need the csv module to use this function.\n'
843 'Get it from: http://www.object-craft.com.au/projects/csv/')
845 # grab the directory to export to
846 if len(args) < 1:
847 raise UsageError, _('Not enough arguments supplied')
848 dir = args[-1]
850 # get the list of classes to export
851 if len(args) == 2:
852 classes = args[0].split(',')
853 else:
854 classes = self.db.classes.keys()
856 # use the csv parser if we can - it's faster
857 p = csv.parser(field_sep=':')
859 # do all the classes specified
860 for classname in classes:
861 cl = self.get_class(classname)
862 f = open(os.path.join(dir, classname+'.csv'), 'w')
863 properties = cl.getprops()
864 propnames = properties.keys()
865 propnames.sort()
866 print >> f, p.join(propnames)
868 # all nodes for this class
869 for nodeid in cl.list():
870 print >>f, p.join(cl.export_list(propnames, nodeid))
871 return 0
873 def do_import(self, args):
874 '''Usage: import import_dir
875 Import a database from the directory containing CSV files, one per
876 class to import.
878 The files must define the same properties as the class (including having
879 a "header" line with those property names.)
881 The imported nodes will have the same nodeid as defined in the
882 import file, thus replacing any existing content.
884 The new nodes are added to the existing database - if you want to
885 create a new database using the imported data, then create a new
886 database (or, tediously, retire all the old data.)
887 '''
888 if len(args) < 1:
889 raise UsageError, _('Not enough arguments supplied')
890 if csv is None:
891 raise UsageError, \
892 _('Sorry, you need the csv module to use this function.\n'
893 'Get it from: http://www.object-craft.com.au/projects/csv/')
895 from roundup import hyperdb
897 for file in os.listdir(args[0]):
898 f = open(os.path.join(args[0], file))
900 # get the classname
901 classname = os.path.splitext(file)[0]
903 # ensure that the properties and the CSV file headings match
904 cl = self.get_class(classname)
905 p = csv.parser(field_sep=':')
906 file_props = p.parse(f.readline())
907 properties = cl.getprops()
908 propnames = properties.keys()
909 propnames.sort()
910 m = file_props[:]
911 m.sort()
912 if m != propnames:
913 raise UsageError, _('Import file doesn\'t define the same '
914 'properties as "%(arg0)s".')%{'arg0': args[0]}
916 # loop through the file and create a node for each entry
917 maxid = 1
918 while 1:
919 line = f.readline()
920 if not line: break
922 # parse lines until we get a complete entry
923 while 1:
924 l = p.parse(line)
925 if l: break
926 line = f.readline()
927 if not line:
928 raise ValueError, "Unexpected EOF during CSV parse"
930 # do the import and figure the current highest nodeid
931 maxid = max(maxid, int(cl.import_list(propnames, l)))
933 print 'setting', classname, maxid
934 self.db.setid(classname, str(maxid))
935 return 0
937 def do_pack(self, args):
938 '''Usage: pack period | date
940 Remove journal entries older than a period of time specified or
941 before a certain date.
943 A period is specified using the suffixes "y", "m", and "d". The
944 suffix "w" (for "week") means 7 days.
946 "3y" means three years
947 "2y 1m" means two years and one month
948 "1m 25d" means one month and 25 days
949 "2w 3d" means two weeks and three days
951 Date format is "YYYY-MM-DD" eg:
952 2001-01-01
954 '''
955 if len(args) <> 1:
956 raise UsageError, _('Not enough arguments supplied')
958 # are we dealing with a period or a date
959 value = args[0]
960 date_re = re.compile(r'''
961 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
962 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
963 ''', re.VERBOSE)
964 m = date_re.match(value)
965 if not m:
966 raise ValueError, _('Invalid format')
967 m = m.groupdict()
968 if m['period']:
969 pack_before = date.Date(". - %s"%value)
970 elif m['date']:
971 pack_before = date.Date(value)
972 self.db.pack(pack_before)
973 return 0
975 def do_reindex(self, args):
976 '''Usage: reindex
977 Re-generate a tracker's search indexes.
979 This will re-generate the search indexes for a tracker. This will
980 typically happen automatically.
981 '''
982 self.db.indexer.force_reindex()
983 self.db.reindex()
984 return 0
986 def do_security(self, args):
987 '''Usage: security [Role name]
988 Display the Permissions available to one or all Roles.
989 '''
990 if len(args) == 1:
991 role = args[0]
992 try:
993 roles = [(args[0], self.db.security.role[args[0]])]
994 except KeyError:
995 print _('No such Role "%(role)s"')%locals()
996 return 1
997 else:
998 roles = self.db.security.role.items()
999 role = self.db.config.NEW_WEB_USER_ROLES
1000 if ',' in role:
1001 print _('New Web users get the Roles "%(role)s"')%locals()
1002 else:
1003 print _('New Web users get the Role "%(role)s"')%locals()
1004 role = self.db.config.NEW_EMAIL_USER_ROLES
1005 if ',' in role:
1006 print _('New Email users get the Roles "%(role)s"')%locals()
1007 else:
1008 print _('New Email users get the Role "%(role)s"')%locals()
1009 roles.sort()
1010 for rolename, role in roles:
1011 print _('Role "%(name)s":')%role.__dict__
1012 for permission in role.permissions:
1013 if permission.klass:
1014 print _(' %(description)s (%(name)s for "%(klass)s" '
1015 'only)')%permission.__dict__
1016 else:
1017 print _(' %(description)s (%(name)s)')%permission.__dict__
1018 return 0
1020 def run_command(self, args):
1021 '''Run a single command
1022 '''
1023 command = args[0]
1025 # handle help now
1026 if command == 'help':
1027 if len(args)>1:
1028 self.do_help(args[1:])
1029 return 0
1030 self.do_help(['help'])
1031 return 0
1032 if command == 'morehelp':
1033 self.do_help(['help'])
1034 self.help_commands()
1035 self.help_all()
1036 return 0
1038 # figure what the command is
1039 try:
1040 functions = self.commands.get(command)
1041 except KeyError:
1042 # not a valid command
1043 print _('Unknown command "%(command)s" ("help commands" for a '
1044 'list)')%locals()
1045 return 1
1047 # check for multiple matches
1048 if len(functions) > 1:
1049 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1050 command, 'list': ', '.join([i[0] for i in functions])}
1051 return 1
1052 command, function = functions[0]
1054 # make sure we have a tracker_home
1055 while not self.tracker_home:
1056 self.tracker_home = raw_input(_('Enter tracker home: ')).strip()
1058 # before we open the db, we may be doing an install or init
1059 if command == 'initialise':
1060 try:
1061 return self.do_initialise(self.tracker_home, args)
1062 except UsageError, message:
1063 print _('Error: %(message)s')%locals()
1064 return 1
1065 elif command == 'install':
1066 try:
1067 return self.do_install(self.tracker_home, args)
1068 except UsageError, message:
1069 print _('Error: %(message)s')%locals()
1070 return 1
1072 # get the tracker
1073 try:
1074 tracker = roundup.instance.open(self.tracker_home)
1075 except ValueError, message:
1076 self.tracker_home = ''
1077 print _("Error: Couldn't open tracker: %(message)s")%locals()
1078 return 1
1080 # only open the database once!
1081 if not self.db:
1082 self.db = tracker.open('admin')
1084 # do the command
1085 ret = 0
1086 try:
1087 ret = function(args[1:])
1088 except UsageError, message:
1089 print _('Error: %(message)s')%locals()
1090 print
1091 print function.__doc__
1092 ret = 1
1093 except:
1094 import traceback
1095 traceback.print_exc()
1096 ret = 1
1097 return ret
1099 def interactive(self):
1100 '''Run in an interactive mode
1101 '''
1102 print _('Roundup %s ready for input.'%roundup_version)
1103 print _('Type "help" for help.')
1104 try:
1105 import readline
1106 except ImportError:
1107 print _('Note: command history and editing not available')
1109 while 1:
1110 try:
1111 command = raw_input(_('roundup> '))
1112 except EOFError:
1113 print _('exit...')
1114 break
1115 if not command: continue
1116 args = token.token_split(command)
1117 if not args: continue
1118 if args[0] in ('quit', 'exit'): break
1119 self.run_command(args)
1121 # exit.. check for transactions
1122 if self.db and self.db.transactions:
1123 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1124 if commit and commit[0].lower() == 'y':
1125 self.db.commit()
1126 return 0
1128 def main(self):
1129 try:
1130 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1131 except getopt.GetoptError, e:
1132 self.usage(str(e))
1133 return 1
1135 # handle command-line args
1136 self.tracker_home = os.environ.get('TRACKER_HOME', '')
1137 # TODO: reinstate the user/password stuff (-u arg too)
1138 name = password = ''
1139 if os.environ.has_key('ROUNDUP_LOGIN'):
1140 l = os.environ['ROUNDUP_LOGIN'].split(':')
1141 name = l[0]
1142 if len(l) > 1:
1143 password = l[1]
1144 self.comma_sep = 0
1145 for opt, arg in opts:
1146 if opt == '-h':
1147 self.usage()
1148 return 0
1149 if opt == '-i':
1150 self.tracker_home = arg
1151 if opt == '-c':
1152 self.comma_sep = 1
1154 # if no command - go interactive
1155 # wrap in a try/finally so we always close off the db
1156 ret = 0
1157 try:
1158 if not args:
1159 self.interactive()
1160 else:
1161 ret = self.run_command(args)
1162 if self.db: self.db.commit()
1163 return ret
1164 finally:
1165 if self.db:
1166 self.db.close()
1168 if __name__ == '__main__':
1169 tool = AdminTool()
1170 sys.exit(tool.main())
1172 # vim: set filetype=python ts=4 sw=4 et si