b8cfb840bb81fce6cfcd55b24849d76940b65354
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.17 2002-07-14 06:05:50 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.instance_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 [-i instance home] [-u login] [-c] <command> <arguments>
92 Help:
93 roundup-admin -h
94 roundup-admin help -- this help
95 roundup-admin help <command> -- command-specific help
96 roundup-admin help all -- all available help
97 Options:
98 -i instance home -- specify the issue tracker "home directory" to administer
99 -u -- the user[:password] to use for commands
100 -c -- when outputting lists of data, just comma-separate them''')%locals()
101 self.help_commands()
103 def help_commands(self):
104 print _('Commands:'),
105 commands = ['']
106 for command in self.commands.values():
107 h = command.__doc__.split('\n')[0]
108 commands.append(' '+h[7:])
109 commands.sort()
110 commands.append(_('Commands may be abbreviated as long as the abbreviation matches only one'))
111 commands.append(_('command, e.g. l == li == lis == list.'))
112 print '\n'.join(commands)
113 print
115 def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
116 commands = self.commands.values()
117 def sortfun(a, b):
118 return cmp(a.__name__, b.__name__)
119 commands.sort(sortfun)
120 for command in commands:
121 h = command.__doc__.split('\n')
122 name = command.__name__[3:]
123 usage = h[0]
124 print _('''
125 <tr><td valign=top><strong>%(name)s</strong></td>
126 <td><tt>%(usage)s</tt><p>
127 <pre>''')%locals()
128 indent = indent_re.match(h[3])
129 if indent: indent = len(indent.group(1))
130 for line in h[3:]:
131 if indent:
132 print line[indent:]
133 else:
134 print line
135 print _('</pre></td></tr>\n')
137 def help_all(self):
138 print _('''
139 All commands (except help) require an instance specifier. This is just the path
140 to the roundup instance you're working with. A roundup instance is where
141 roundup keeps the database and configuration file that defines an issue
142 tracker. It may be thought of as the issue tracker's "home directory". It may
143 be specified in the environment variable ROUNDUP_INSTANCE or on the command
144 line as "-i instance".
146 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
148 Property values are represented as strings in command arguments and in the
149 printed results:
150 . Strings are, well, strings.
151 . Date values are printed in the full date format in the local time zone, and
152 accepted in the full format or any of the partial formats explained below.
153 . Link values are printed as node designators. When given as an argument,
154 node designators and key strings are both accepted.
155 . Multilink values are printed as lists of node designators joined by commas.
156 When given as an argument, node designators and key strings are both
157 accepted; an empty string, a single node, or a list of nodes joined by
158 commas is accepted.
160 When property values must contain spaces, just surround the value with
161 quotes, either ' or ". A single space may also be backslash-quoted. If a
162 valuu must contain a quote character, it must be backslash-quoted or inside
163 quotes. Examples:
164 hello world (2 tokens: hello, world)
165 "hello world" (1 token: hello world)
166 "Roch'e" Compaan (2 tokens: Roch'e Compaan)
167 Roch\'e Compaan (2 tokens: Roch'e Compaan)
168 address="1 2 3" (1 token: address=1 2 3)
169 \\ (1 token: \)
170 \n\r\t (1 token: a newline, carriage-return and tab)
172 When multiple nodes are specified to the roundup get or roundup set
173 commands, the specified properties are retrieved or set on all the listed
174 nodes.
176 When multiple results are returned by the roundup get or roundup find
177 commands, they are printed one per line (default) or joined by commas (with
178 the -c) option.
180 Where the command changes data, a login name/password is required. The
181 login may be specified as either "name" or "name:password".
182 . ROUNDUP_LOGIN environment variable
183 . the -u command-line option
184 If either the name or password is not supplied, they are obtained from the
185 command-line.
187 Date format examples:
188 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
189 "2000-04-17" means <Date 2000-04-17.00:00:00>
190 "01-25" means <Date yyyy-01-25.00:00:00>
191 "08-13.22:13" means <Date yyyy-08-14.03:13:00>
192 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
193 "14:25" means <Date yyyy-mm-dd.19:25:00>
194 "8:47:11" means <Date yyyy-mm-dd.13:47:11>
195 "." means "right now"
197 Command help:
198 ''')
199 for name, command in self.commands.items():
200 print _('%s:')%name
201 print _(' '), command.__doc__
203 def do_help(self, args, nl_re=re.compile('[\r\n]'),
204 indent_re=re.compile(r'^(\s+)\S+')):
205 '''Usage: help topic
206 Give help about topic.
208 commands -- list commands
209 <command> -- help specific to a command
210 initopts -- init command options
211 all -- all available help
212 '''
213 if len(args)>0:
214 topic = args[0]
215 else:
216 topic = 'help'
219 # try help_ methods
220 if self.help.has_key(topic):
221 self.help[topic]()
222 return 0
224 # try command docstrings
225 try:
226 l = self.commands.get(topic)
227 except KeyError:
228 print _('Sorry, no help for "%(topic)s"')%locals()
229 return 1
231 # display the help for each match, removing the docsring indent
232 for name, help in l:
233 lines = nl_re.split(help.__doc__)
234 print lines[0]
235 indent = indent_re.match(lines[1])
236 if indent: indent = len(indent.group(1))
237 for line in lines[1:]:
238 if indent:
239 print line[indent:]
240 else:
241 print line
242 return 0
244 def help_initopts(self):
245 import roundup.templates
246 templates = roundup.templates.listTemplates()
247 print _('Templates:'), ', '.join(templates)
248 import roundup.backends
249 backends = roundup.backends.__all__
250 print _('Back ends:'), ', '.join(backends)
253 def do_install(self, instance_home, args):
254 '''Usage: install [template [backend [admin password]]]
255 Install a new Roundup instance.
257 The command will prompt for the instance home directory (if not supplied
258 through INSTANCE_HOME or the -i option). The template, backend and admin
259 password may be specified on the command-line as arguments, in that
260 order.
262 The initialise command must be called after this command in order
263 to initialise the instance's database. You may edit the instance's
264 initial database contents before running that command by editing
265 the instance's dbinit.py module init() function.
267 See also initopts help.
268 '''
269 if len(args) < 1:
270 raise UsageError, _('Not enough arguments supplied')
272 # make sure the instance home can be created
273 parent = os.path.split(instance_home)[0]
274 if not os.path.exists(parent):
275 raise UsageError, _('Instance home parent directory "%(parent)s"'
276 ' does not exist')%locals()
278 # select template
279 import roundup.templates
280 templates = roundup.templates.listTemplates()
281 template = len(args) > 1 and args[1] or ''
282 if template not in templates:
283 print _('Templates:'), ', '.join(templates)
284 while template not in templates:
285 template = raw_input(_('Select template [classic]: ')).strip()
286 if not template:
287 template = 'classic'
289 # select hyperdb backend
290 import roundup.backends
291 backends = roundup.backends.__all__
292 backend = len(args) > 2 and args[2] or ''
293 if backend not in backends:
294 print _('Back ends:'), ', '.join(backends)
295 while backend not in backends:
296 backend = raw_input(_('Select backend [anydbm]: ')).strip()
297 if not backend:
298 backend = 'anydbm'
300 # install!
301 init.install(instance_home, template, backend)
303 print _('''
304 You should now edit the instance configuration file:
305 %(instance_config_file)s
306 ... at a minimum, you must set MAILHOST, MAIL_DOMAIN and ADMIN_EMAIL.
308 If you wish to modify the default schema, you should also edit the database
309 initialisation file:
310 %(database_config_file)s
311 ... see the documentation on customizing for more information.
312 ''')%{
313 'instance_config_file': os.path.join(instance_home, 'instance_config.py'),
314 'database_config_file': os.path.join(instance_home, 'dbinit.py')
315 }
316 return 0
319 def do_initialise(self, instance_home, args):
320 '''Usage: initialise [adminpw]
321 Initialise a new Roundup instance.
323 The administrator details will be set at this step.
325 Execute the instance's initialisation function dbinit.init()
326 '''
327 # password
328 if len(args) > 1:
329 adminpw = args[1]
330 else:
331 adminpw = ''
332 confirm = 'x'
333 while adminpw != confirm:
334 adminpw = getpass.getpass(_('Admin Password: '))
335 confirm = getpass.getpass(_(' Confirm: '))
337 # make sure the instance home is installed
338 if not os.path.exists(instance_home):
339 raise UsageError, _('Instance home does not exist')%locals()
340 if not os.path.exists(os.path.join(instance_home, 'html')):
341 raise UsageError, _('Instance has not been installed')%locals()
343 # is there already a database?
344 if os.path.exists(os.path.join(instance_home, 'db')):
345 print _('WARNING: The database is already initialised!')
346 print _('If you re-initialise it, you will lose all the data!')
347 ok = raw_input(_('Erase it? Y/[N]: ')).strip()
348 if ok.lower() != 'y':
349 return 0
351 # nuke it
352 shutil.rmtree(os.path.join(instance_home, 'db'))
354 # GO
355 init.initialise(instance_home, adminpw)
357 return 0
360 def do_get(self, args):
361 '''Usage: get property designator[,designator]*
362 Get the given property of one or more designator(s).
364 Retrieves the property value of the nodes specified by the designators.
365 '''
366 if len(args) < 2:
367 raise UsageError, _('Not enough arguments supplied')
368 propname = args[0]
369 designators = args[1].split(',')
370 l = []
371 for designator in designators:
372 # decode the node designator
373 try:
374 classname, nodeid = roundupdb.splitDesignator(designator)
375 except roundupdb.DesignatorError, message:
376 raise UsageError, message
378 # get the class
379 cl = self.get_class(classname)
380 try:
381 if self.comma_sep:
382 l.append(cl.get(nodeid, propname))
383 else:
384 print cl.get(nodeid, propname)
385 except IndexError:
386 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
387 except KeyError:
388 raise UsageError, _('no such %(classname)s property '
389 '"%(propname)s"')%locals()
390 if self.comma_sep:
391 print ','.join(l)
392 return 0
395 def do_set(self, args):
396 '''Usage: set designator[,designator]* propname=value ...
397 Set the given property of one or more designator(s).
399 Sets the property to the value for all designators given.
400 '''
401 if len(args) < 2:
402 raise UsageError, _('Not enough arguments supplied')
403 from roundup import hyperdb
405 designators = args[0].split(',')
407 # get the props from the args
408 props = self.props_from_args(args[1:])
410 # now do the set for all the nodes
411 for designator in designators:
412 # decode the node designator
413 try:
414 classname, nodeid = roundupdb.splitDesignator(designator)
415 except roundupdb.DesignatorError, message:
416 raise UsageError, message
418 # get the class
419 cl = self.get_class(classname)
421 properties = cl.getprops()
422 for key, value in props.items():
423 proptype = properties[key]
424 if isinstance(proptype, hyperdb.String):
425 continue
426 elif isinstance(proptype, hyperdb.Password):
427 props[key] = password.Password(value)
428 elif isinstance(proptype, hyperdb.Date):
429 try:
430 props[key] = date.Date(value)
431 except ValueError, message:
432 raise UsageError, '"%s": %s'%(value, message)
433 elif isinstance(proptype, hyperdb.Interval):
434 try:
435 props[key] = date.Interval(value)
436 except ValueError, message:
437 raise UsageError, '"%s": %s'%(value, message)
438 elif isinstance(proptype, hyperdb.Link):
439 props[key] = value
440 elif isinstance(proptype, hyperdb.Multilink):
441 props[key] = value.split(',')
443 # try the set
444 try:
445 apply(cl.set, (nodeid, ), props)
446 except (TypeError, IndexError, ValueError), message:
447 raise UsageError, message
448 return 0
450 def do_find(self, args):
451 '''Usage: find classname propname=value ...
452 Find the nodes of the given class with a given link property value.
454 Find the nodes of the given class with a given link property value. The
455 value may be either the nodeid of the linked node, or its key value.
456 '''
457 if len(args) < 1:
458 raise UsageError, _('Not enough arguments supplied')
459 classname = args[0]
460 # get the class
461 cl = self.get_class(classname)
463 # handle the propname=value argument
464 props = self.props_from_args(args[1:])
466 # if the value isn't a number, look up the linked class to get the
467 # number
468 for propname, value in props.items():
469 num_re = re.compile('^\d+$')
470 if not num_re.match(value):
471 # get the property
472 try:
473 property = cl.properties[propname]
474 except KeyError:
475 raise UsageError, _('%(classname)s has no property '
476 '"%(propname)s"')%locals()
478 # make sure it's a link
479 if (not isinstance(property, hyperdb.Link) and not
480 isinstance(property, hyperdb.Multilink)):
481 raise UsageError, _('You may only "find" link properties')
483 # get the linked-to class and look up the key property
484 link_class = self.db.getclass(property.classname)
485 try:
486 props[propname] = link_class.lookup(value)
487 except TypeError:
488 raise UsageError, _('%(classname)s has no key property"')%{
489 'classname': link_class.classname}
491 # now do the find
492 try:
493 if self.comma_sep:
494 print ','.join(apply(cl.find, (), props))
495 else:
496 print apply(cl.find, (), props)
497 except KeyError:
498 raise UsageError, _('%(classname)s has no property '
499 '"%(propname)s"')%locals()
500 except (ValueError, TypeError), message:
501 raise UsageError, message
502 return 0
504 def do_specification(self, args):
505 '''Usage: specification classname
506 Show the properties for a classname.
508 This lists the properties for a given class.
509 '''
510 if len(args) < 1:
511 raise UsageError, _('Not enough arguments supplied')
512 classname = args[0]
513 # get the class
514 cl = self.get_class(classname)
516 # get the key property
517 keyprop = cl.getkey()
518 for key, value in cl.properties.items():
519 if keyprop == key:
520 print _('%(key)s: %(value)s (key property)')%locals()
521 else:
522 print _('%(key)s: %(value)s')%locals()
524 def do_display(self, args):
525 '''Usage: display designator
526 Show the property values for the given node.
528 This lists the properties and their associated values for the given
529 node.
530 '''
531 if len(args) < 1:
532 raise UsageError, _('Not enough arguments supplied')
534 # decode the node designator
535 try:
536 classname, nodeid = roundupdb.splitDesignator(args[0])
537 except roundupdb.DesignatorError, message:
538 raise UsageError, message
540 # get the class
541 cl = self.get_class(classname)
543 # display the values
544 for key in cl.properties.keys():
545 value = cl.get(nodeid, key)
546 print _('%(key)s: %(value)s')%locals()
548 def do_create(self, args):
549 '''Usage: create classname property=value ...
550 Create a new entry of a given class.
552 This creates a new entry of the given class using the property
553 name=value arguments provided on the command line after the "create"
554 command.
555 '''
556 if len(args) < 1:
557 raise UsageError, _('Not enough arguments supplied')
558 from roundup import hyperdb
560 classname = args[0]
562 # get the class
563 cl = self.get_class(classname)
565 # now do a create
566 props = {}
567 properties = cl.getprops(protected = 0)
568 if len(args) == 1:
569 # ask for the properties
570 for key, value in properties.items():
571 if key == 'id': continue
572 name = value.__class__.__name__
573 if isinstance(value , hyperdb.Password):
574 again = None
575 while value != again:
576 value = getpass.getpass(_('%(propname)s (Password): ')%{
577 'propname': key.capitalize()})
578 again = getpass.getpass(_(' %(propname)s (Again): ')%{
579 'propname': key.capitalize()})
580 if value != again: print _('Sorry, try again...')
581 if value:
582 props[key] = value
583 else:
584 value = raw_input(_('%(propname)s (%(proptype)s): ')%{
585 'propname': key.capitalize(), 'proptype': name})
586 if value:
587 props[key] = value
588 else:
589 props = self.props_from_args(args[1:])
591 # convert types
592 for propname, value in props.items():
593 # get the property
594 try:
595 proptype = properties[propname]
596 except KeyError:
597 raise UsageError, _('%(classname)s has no property '
598 '"%(propname)s"')%locals()
600 if isinstance(proptype, hyperdb.Date):
601 try:
602 props[propname] = date.Date(value)
603 except ValueError, message:
604 raise UsageError, _('"%(value)s": %(message)s')%locals()
605 elif isinstance(proptype, hyperdb.Interval):
606 try:
607 props[propname] = date.Interval(value)
608 except ValueError, message:
609 raise UsageError, _('"%(value)s": %(message)s')%locals()
610 elif isinstance(proptype, hyperdb.Password):
611 props[propname] = password.Password(value)
612 elif isinstance(proptype, hyperdb.Multilink):
613 props[propname] = value.split(',')
615 # check for the key property
616 propname = cl.getkey()
617 if propname and not props.has_key(propname):
618 raise UsageError, _('you must provide the "%(propname)s" '
619 'property.')%locals()
621 # do the actual create
622 try:
623 print apply(cl.create, (), props)
624 except (TypeError, IndexError, ValueError), message:
625 raise UsageError, message
626 return 0
628 def do_list(self, args):
629 '''Usage: list classname [property]
630 List the instances of a class.
632 Lists all instances of the given class. If the property is not
633 specified, the "label" property is used. The label property is tried
634 in order: the key, "name", "title" and then the first property,
635 alphabetically.
636 '''
637 if len(args) < 1:
638 raise UsageError, _('Not enough arguments supplied')
639 classname = args[0]
641 # get the class
642 cl = self.get_class(classname)
644 # figure the property
645 if len(args) > 1:
646 propname = args[1]
647 else:
648 propname = cl.labelprop()
650 if self.comma_sep:
651 print ','.join(cl.list())
652 else:
653 for nodeid in cl.list():
654 try:
655 value = cl.get(nodeid, propname)
656 except KeyError:
657 raise UsageError, _('%(classname)s has no property '
658 '"%(propname)s"')%locals()
659 print _('%(nodeid)4s: %(value)s')%locals()
660 return 0
662 def do_table(self, args):
663 '''Usage: table classname [property[,property]*]
664 List the instances of a class in tabular form.
666 Lists all instances of the given class. If the properties are not
667 specified, all properties are displayed. By default, the column widths
668 are the width of the property names. The width may be explicitly defined
669 by defining the property as "name:width". For example::
670 roundup> table priority id,name:10
671 Id Name
672 1 fatal-bug
673 2 bug
674 3 usability
675 4 feature
676 '''
677 if len(args) < 1:
678 raise UsageError, _('Not enough arguments supplied')
679 classname = args[0]
681 # get the class
682 cl = self.get_class(classname)
684 # figure the property names to display
685 if len(args) > 1:
686 prop_names = args[1].split(',')
687 all_props = cl.getprops()
688 for spec in prop_names:
689 if ':' in spec:
690 try:
691 propname, width = spec.split(':')
692 except (ValueError, TypeError):
693 raise UsageError, _('"%(spec)s" not name:width')%locals()
694 else:
695 propname = spec
696 if not all_props.has_key(propname):
697 raise UsageError, _('%(classname)s has no property '
698 '"%(propname)s"')%locals()
699 else:
700 prop_names = cl.getprops().keys()
702 # now figure column widths
703 props = []
704 for spec in prop_names:
705 if ':' in spec:
706 name, width = spec.split(':')
707 props.append((name, int(width)))
708 else:
709 props.append((spec, len(spec)))
711 # now display the heading
712 print ' '.join([name.capitalize().ljust(width) for name,width in props])
714 # and the table data
715 for nodeid in cl.list():
716 l = []
717 for name, width in props:
718 if name != 'id':
719 try:
720 value = str(cl.get(nodeid, name))
721 except KeyError:
722 # we already checked if the property is valid - a
723 # KeyError here means the node just doesn't have a
724 # value for it
725 value = ''
726 else:
727 value = str(nodeid)
728 f = '%%-%ds'%width
729 l.append(f%value[:width])
730 print ' '.join(l)
731 return 0
733 def do_history(self, args):
734 '''Usage: history designator
735 Show the history entries of a designator.
737 Lists the journal entries for the node identified by the designator.
738 '''
739 if len(args) < 1:
740 raise UsageError, _('Not enough arguments supplied')
741 try:
742 classname, nodeid = roundupdb.splitDesignator(args[0])
743 except roundupdb.DesignatorError, message:
744 raise UsageError, message
746 try:
747 print self.db.getclass(classname).history(nodeid)
748 except KeyError:
749 raise UsageError, _('no such class "%(classname)s"')%locals()
750 except IndexError:
751 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
752 return 0
754 def do_commit(self, args):
755 '''Usage: commit
756 Commit all changes made to the database.
758 The changes made during an interactive session are not
759 automatically written to the database - they must be committed
760 using this command.
762 One-off commands on the command-line are automatically committed if
763 they are successful.
764 '''
765 self.db.commit()
766 return 0
768 def do_rollback(self, args):
769 '''Usage: rollback
770 Undo all changes that are pending commit to the database.
772 The changes made during an interactive session are not
773 automatically written to the database - they must be committed
774 manually. This command undoes all those changes, so a commit
775 immediately after would make no changes to the database.
776 '''
777 self.db.rollback()
778 return 0
780 def do_retire(self, args):
781 '''Usage: retire designator[,designator]*
782 Retire the node specified by designator.
784 This action indicates that a particular node is not to be retrieved by
785 the list or find commands, and its key value may be re-used.
786 '''
787 if len(args) < 1:
788 raise UsageError, _('Not enough arguments supplied')
789 designators = args[0].split(',')
790 for designator in designators:
791 try:
792 classname, nodeid = roundupdb.splitDesignator(designator)
793 except roundupdb.DesignatorError, message:
794 raise UsageError, message
795 try:
796 self.db.getclass(classname).retire(nodeid)
797 except KeyError:
798 raise UsageError, _('no such class "%(classname)s"')%locals()
799 except IndexError:
800 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
801 return 0
803 def do_export(self, args):
804 '''Usage: export class[,class] destination_dir
805 Export the database to tab-separated-value files.
807 This action exports the current data from the database into
808 tab-separated-value files that are placed in the nominated destination
809 directory. The journals are not exported.
810 '''
811 if len(args) < 2:
812 raise UsageError, _('Not enough arguments supplied')
813 classes = args[0].split(',')
814 dir = args[1]
816 # use the csv parser if we can - it's faster
817 if csv is not None:
818 p = csv.parser(field_sep=':')
820 # do all the classes specified
821 for classname in classes:
822 cl = self.get_class(classname)
823 f = open(os.path.join(dir, classname+'.csv'), 'w')
824 f.write(':'.join(cl.properties.keys()) + '\n')
826 # all nodes for this class
827 properties = cl.properties.items()
828 for nodeid in cl.list():
829 l = []
830 for prop, proptype in properties:
831 value = cl.get(nodeid, prop)
832 # convert data where needed
833 if isinstance(proptype, hyperdb.Date):
834 value = value.get_tuple()
835 elif isinstance(proptype, hyperdb.Interval):
836 value = value.get_tuple()
837 elif isinstance(proptype, hyperdb.Password):
838 value = str(value)
839 l.append(repr(value))
841 # now write
842 if csv is not None:
843 f.write(p.join(l) + '\n')
844 else:
845 # escape the individual entries to they're valid CSV
846 m = []
847 for entry in l:
848 if '"' in entry:
849 entry = '""'.join(entry.split('"'))
850 if ':' in entry:
851 entry = '"%s"'%entry
852 m.append(entry)
853 f.write(':'.join(m) + '\n')
854 return 0
856 def do_import(self, args):
857 '''Usage: import class file
858 Import the contents of the tab-separated-value file.
860 The file must define the same properties as the class (including having
861 a "header" line with those property names.) The new nodes are added to
862 the existing database - if you want to create a new database using the
863 imported data, then create a new database (or, tediously, retire all
864 the old data.)
865 '''
866 if len(args) < 2:
867 raise UsageError, _('Not enough arguments supplied')
868 if csv is None:
869 raise UsageError, \
870 _('Sorry, you need the csv module to use this function.\n'
871 'Get it from: http://www.object-craft.com.au/projects/csv/')
873 from roundup import hyperdb
875 # ensure that the properties and the CSV file headings match
876 classname = args[0]
877 cl = self.get_class(classname)
878 f = open(args[1])
879 p = csv.parser(field_sep=':')
880 file_props = p.parse(f.readline())
881 props = cl.properties.keys()
882 m = file_props[:]
883 m.sort()
884 props.sort()
885 if m != props:
886 raise UsageError, _('Import file doesn\'t define the same '
887 'properties as "%(arg0)s".')%{'arg0': args[0]}
889 # loop through the file and create a node for each entry
890 n = range(len(props))
891 while 1:
892 line = f.readline()
893 if not line: break
895 # parse lines until we get a complete entry
896 while 1:
897 l = p.parse(line)
898 if l: break
899 line = f.readline()
900 if not line:
901 raise ValueError, "Unexpected EOF during CSV parse"
903 # make the new node's property map
904 d = {}
905 for i in n:
906 # Use eval to reverse the repr() used to output the CSV
907 value = eval(l[i])
908 # Figure the property for this column
909 key = file_props[i]
910 proptype = cl.properties[key]
911 # Convert for property type
912 if isinstance(proptype, hyperdb.Date):
913 value = date.Date(value)
914 elif isinstance(proptype, hyperdb.Interval):
915 value = date.Interval(value)
916 elif isinstance(proptype, hyperdb.Password):
917 pwd = password.Password()
918 pwd.unpack(value)
919 value = pwd
920 if value is not None:
921 d[key] = value
923 # and create the new node
924 apply(cl.create, (), d)
925 return 0
927 def do_pack(self, args):
928 '''Usage: pack period | date
930 Remove journal entries older than a period of time specified or
931 before a certain date.
933 A period is specified using the suffixes "y", "m", and "d". The
934 suffix "w" (for "week") means 7 days.
936 "3y" means three years
937 "2y 1m" means two years and one month
938 "1m 25d" means one month and 25 days
939 "2w 3d" means two weeks and three days
941 Date format is "YYYY-MM-DD" eg:
942 2001-01-01
944 '''
945 if len(args) <> 1:
946 raise UsageError, _('Not enough arguments supplied')
948 # are we dealing with a period or a date
949 value = args[0]
950 date_re = re.compile(r'''
951 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
952 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
953 ''', re.VERBOSE)
954 m = date_re.match(value)
955 if not m:
956 raise ValueError, _('Invalid format')
957 m = m.groupdict()
958 if m['period']:
959 pack_before = date.Date(". - %s"%value)
960 elif m['date']:
961 pack_before = date.Date(value)
962 self.db.pack(pack_before)
963 return 0
965 def do_reindex(self, args):
966 '''Usage: reindex
967 Re-generate an instance's search indexes.
969 This will re-generate the search indexes for an instance. This will
970 typically happen automatically.
971 '''
972 self.db.indexer.force_reindex()
973 self.db.reindex()
974 return 0
976 def run_command(self, args):
977 '''Run a single command
978 '''
979 command = args[0]
981 # handle help now
982 if command == 'help':
983 if len(args)>1:
984 self.do_help(args[1:])
985 return 0
986 self.do_help(['help'])
987 return 0
988 if command == 'morehelp':
989 self.do_help(['help'])
990 self.help_commands()
991 self.help_all()
992 return 0
994 # figure what the command is
995 try:
996 functions = self.commands.get(command)
997 except KeyError:
998 # not a valid command
999 print _('Unknown command "%(command)s" ("help commands" for a '
1000 'list)')%locals()
1001 return 1
1003 # check for multiple matches
1004 if len(functions) > 1:
1005 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
1006 command, 'list': ', '.join([i[0] for i in functions])}
1007 return 1
1008 command, function = functions[0]
1010 # make sure we have an instance_home
1011 while not self.instance_home:
1012 self.instance_home = raw_input(_('Enter instance home: ')).strip()
1014 # before we open the db, we may be doing an install or init
1015 if command == 'initialise':
1016 try:
1017 return self.do_initialise(self.instance_home, args)
1018 except UsageError, message:
1019 print _('Error: %(message)s')%locals()
1020 return 1
1021 elif command == 'install':
1022 try:
1023 return self.do_install(self.instance_home, args)
1024 except UsageError, message:
1025 print _('Error: %(message)s')%locals()
1026 return 1
1028 # get the instance
1029 try:
1030 instance = roundup.instance.open(self.instance_home)
1031 except ValueError, message:
1032 self.instance_home = ''
1033 print _("Error: Couldn't open instance: %(message)s")%locals()
1034 return 1
1036 # only open the database once!
1037 if not self.db:
1038 self.db = instance.open('admin')
1040 # do the command
1041 ret = 0
1042 try:
1043 ret = function(args[1:])
1044 except UsageError, message:
1045 print _('Error: %(message)s')%locals()
1046 print
1047 print function.__doc__
1048 ret = 1
1049 except:
1050 import traceback
1051 traceback.print_exc()
1052 ret = 1
1053 return ret
1055 def interactive(self):
1056 '''Run in an interactive mode
1057 '''
1058 print _('Roundup %s ready for input.'%roundup_version)
1059 print _('Type "help" for help.')
1060 try:
1061 import readline
1062 except ImportError:
1063 print _('Note: command history and editing not available')
1065 while 1:
1066 try:
1067 command = raw_input(_('roundup> '))
1068 except EOFError:
1069 print _('exit...')
1070 break
1071 if not command: continue
1072 args = token.token_split(command)
1073 if not args: continue
1074 if args[0] in ('quit', 'exit'): break
1075 self.run_command(args)
1077 # exit.. check for transactions
1078 if self.db and self.db.transactions:
1079 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1080 if commit and commit[0].lower() == 'y':
1081 self.db.commit()
1082 return 0
1084 def main(self):
1085 try:
1086 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1087 except getopt.GetoptError, e:
1088 self.usage(str(e))
1089 return 1
1091 # handle command-line args
1092 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
1093 # TODO: reinstate the user/password stuff (-u arg too)
1094 name = password = ''
1095 if os.environ.has_key('ROUNDUP_LOGIN'):
1096 l = os.environ['ROUNDUP_LOGIN'].split(':')
1097 name = l[0]
1098 if len(l) > 1:
1099 password = l[1]
1100 self.comma_sep = 0
1101 for opt, arg in opts:
1102 if opt == '-h':
1103 self.usage()
1104 return 0
1105 if opt == '-i':
1106 self.instance_home = arg
1107 if opt == '-c':
1108 self.comma_sep = 1
1110 # if no command - go interactive
1111 ret = 0
1112 if not args:
1113 self.interactive()
1114 else:
1115 ret = self.run_command(args)
1116 if self.db: self.db.commit()
1117 return ret
1120 if __name__ == '__main__':
1121 tool = AdminTool()
1122 sys.exit(tool.main())
1124 #
1125 # $Log: not supported by cvs2svn $
1126 # Revision 1.16 2002/07/09 04:19:09 richard
1127 # Added reindex command to roundup-admin.
1128 # Fixed reindex on first access.
1129 # Also fixed reindexing of entries that change.
1130 #
1131 # Revision 1.15 2002/06/17 23:14:44 richard
1132 # . #569415 ] {version}
1133 #
1134 # Revision 1.14 2002/06/11 06:41:50 richard
1135 # Removed prompt for admin email in initialisation.
1136 #
1137 # Revision 1.13 2002/05/30 23:58:14 richard
1138 # oops
1139 #
1140 # Revision 1.12 2002/05/26 09:04:42 richard
1141 # out by one in the init args
1142 #
1143 # Revision 1.11 2002/05/23 01:14:20 richard
1144 # . split instance initialisation into two steps, allowing config changes
1145 # before the database is initialised.
1146 #
1147 # Revision 1.10 2002/04/27 10:07:23 richard
1148 # minor fix to error message
1149 #
1150 # Revision 1.9 2002/03/12 22:51:47 richard
1151 # . #527416 ] roundup-admin uses undefined value
1152 # . #527503 ] unfriendly init blowup when parent dir
1153 # (also handles UsageError correctly now in init)
1154 #
1155 # Revision 1.8 2002/02/27 03:28:21 richard
1156 # Ran it through pychecker, made fixes
1157 #
1158 # Revision 1.7 2002/02/20 05:04:32 richard
1159 # Wasn't handling the cvs parser feeding properly.
1160 #
1161 # Revision 1.6 2002/01/23 07:27:19 grubert
1162 # . allow abbreviation of "help" in admin tool too.
1163 #
1164 # Revision 1.5 2002/01/21 16:33:19 rochecompaan
1165 # You can now use the roundup-admin tool to pack the database
1166 #
1167 # Revision 1.4 2002/01/14 06:51:09 richard
1168 # . #503164 ] create and passwords
1169 #
1170 # Revision 1.3 2002/01/08 05:26:32 rochecompaan
1171 # Missing "self" in props_from_args
1172 #
1173 # Revision 1.2 2002/01/07 10:41:44 richard
1174 # #500140 ] AdminTool.get_class() returns nothing
1175 #
1176 # Revision 1.1 2002/01/05 02:11:22 richard
1177 # I18N'ed roundup admin - and split the code off into a module so it can be used
1178 # elsewhere.
1179 # Big issue with this is the doc strings - that's the help. We're probably going to
1180 # have to switch to not use docstrings, which will suck a little :(
1181 #
1182 #
1183 #
1184 # vim: set filetype=python ts=4 sw=4 et si