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.15 2002-06-17 23:14:44 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 # TODO: need to fix date module. one should be able to say
960 # pack_before = date.Date(". - %s"%value)
961 pack_before = date.Date(".") + date.Interval("- %s"%value)
962 elif m['date']:
963 pack_before = date.Date(value)
964 self.db.pack(pack_before)
965 return 0
967 def run_command(self, args):
968 '''Run a single command
969 '''
970 command = args[0]
972 # handle help now
973 if command == 'help':
974 if len(args)>1:
975 self.do_help(args[1:])
976 return 0
977 self.do_help(['help'])
978 return 0
979 if command == 'morehelp':
980 self.do_help(['help'])
981 self.help_commands()
982 self.help_all()
983 return 0
985 # figure what the command is
986 try:
987 functions = self.commands.get(command)
988 except KeyError:
989 # not a valid command
990 print _('Unknown command "%(command)s" ("help commands" for a '
991 'list)')%locals()
992 return 1
994 # check for multiple matches
995 if len(functions) > 1:
996 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
997 command, 'list': ', '.join([i[0] for i in functions])}
998 return 1
999 command, function = functions[0]
1001 # make sure we have an instance_home
1002 while not self.instance_home:
1003 self.instance_home = raw_input(_('Enter instance home: ')).strip()
1005 # before we open the db, we may be doing an install or init
1006 if command == 'initialise':
1007 try:
1008 return self.do_initialise(self.instance_home, args)
1009 except UsageError, message:
1010 print _('Error: %(message)s')%locals()
1011 return 1
1012 elif command == 'install':
1013 try:
1014 return self.do_install(self.instance_home, args)
1015 except UsageError, message:
1016 print _('Error: %(message)s')%locals()
1017 return 1
1019 # get the instance
1020 try:
1021 instance = roundup.instance.open(self.instance_home)
1022 except ValueError, message:
1023 self.instance_home = ''
1024 print _("Error: Couldn't open instance: %(message)s")%locals()
1025 return 1
1027 # only open the database once!
1028 if not self.db:
1029 self.db = instance.open('admin')
1031 # do the command
1032 ret = 0
1033 try:
1034 ret = function(args[1:])
1035 except UsageError, message:
1036 print _('Error: %(message)s')%locals()
1037 print
1038 print function.__doc__
1039 ret = 1
1040 except:
1041 import traceback
1042 traceback.print_exc()
1043 ret = 1
1044 return ret
1046 def interactive(self):
1047 '''Run in an interactive mode
1048 '''
1049 print _('Roundup %s ready for input.'%roundup_version)
1050 print _('Type "help" for help.')
1051 try:
1052 import readline
1053 except ImportError:
1054 print _('Note: command history and editing not available')
1056 while 1:
1057 try:
1058 command = raw_input(_('roundup> '))
1059 except EOFError:
1060 print _('exit...')
1061 break
1062 if not command: continue
1063 args = token.token_split(command)
1064 if not args: continue
1065 if args[0] in ('quit', 'exit'): break
1066 self.run_command(args)
1068 # exit.. check for transactions
1069 if self.db and self.db.transactions:
1070 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1071 if commit and commit[0].lower() == 'y':
1072 self.db.commit()
1073 return 0
1075 def main(self):
1076 try:
1077 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1078 except getopt.GetoptError, e:
1079 self.usage(str(e))
1080 return 1
1082 # handle command-line args
1083 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
1084 # TODO: reinstate the user/password stuff (-u arg too)
1085 name = password = ''
1086 if os.environ.has_key('ROUNDUP_LOGIN'):
1087 l = os.environ['ROUNDUP_LOGIN'].split(':')
1088 name = l[0]
1089 if len(l) > 1:
1090 password = l[1]
1091 self.comma_sep = 0
1092 for opt, arg in opts:
1093 if opt == '-h':
1094 self.usage()
1095 return 0
1096 if opt == '-i':
1097 self.instance_home = arg
1098 if opt == '-c':
1099 self.comma_sep = 1
1101 # if no command - go interactive
1102 ret = 0
1103 if not args:
1104 self.interactive()
1105 else:
1106 ret = self.run_command(args)
1107 if self.db: self.db.commit()
1108 return ret
1111 if __name__ == '__main__':
1112 tool = AdminTool()
1113 sys.exit(tool.main())
1115 #
1116 # $Log: not supported by cvs2svn $
1117 # Revision 1.14 2002/06/11 06:41:50 richard
1118 # Removed prompt for admin email in initialisation.
1119 #
1120 # Revision 1.13 2002/05/30 23:58:14 richard
1121 # oops
1122 #
1123 # Revision 1.12 2002/05/26 09:04:42 richard
1124 # out by one in the init args
1125 #
1126 # Revision 1.11 2002/05/23 01:14:20 richard
1127 # . split instance initialisation into two steps, allowing config changes
1128 # before the database is initialised.
1129 #
1130 # Revision 1.10 2002/04/27 10:07:23 richard
1131 # minor fix to error message
1132 #
1133 # Revision 1.9 2002/03/12 22:51:47 richard
1134 # . #527416 ] roundup-admin uses undefined value
1135 # . #527503 ] unfriendly init blowup when parent dir
1136 # (also handles UsageError correctly now in init)
1137 #
1138 # Revision 1.8 2002/02/27 03:28:21 richard
1139 # Ran it through pychecker, made fixes
1140 #
1141 # Revision 1.7 2002/02/20 05:04:32 richard
1142 # Wasn't handling the cvs parser feeding properly.
1143 #
1144 # Revision 1.6 2002/01/23 07:27:19 grubert
1145 # . allow abbreviation of "help" in admin tool too.
1146 #
1147 # Revision 1.5 2002/01/21 16:33:19 rochecompaan
1148 # You can now use the roundup-admin tool to pack the database
1149 #
1150 # Revision 1.4 2002/01/14 06:51:09 richard
1151 # . #503164 ] create and passwords
1152 #
1153 # Revision 1.3 2002/01/08 05:26:32 rochecompaan
1154 # Missing "self" in props_from_args
1155 #
1156 # Revision 1.2 2002/01/07 10:41:44 richard
1157 # #500140 ] AdminTool.get_class() returns nothing
1158 #
1159 # Revision 1.1 2002/01/05 02:11:22 richard
1160 # I18N'ed roundup admin - and split the code off into a module so it can be used
1161 # elsewhere.
1162 # Big issue with this is the doc strings - that's the help. We're probably going to
1163 # have to switch to not use docstrings, which will suck a little :(
1164 #
1165 #
1166 #
1167 # vim: set filetype=python ts=4 sw=4 et si