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