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