8f086f35e8a508b362e932b87cbde49c08daa0c2
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.14 2002-06-11 06:41: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 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]
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 # make sure the instance home is installed
337 if not os.path.exists(instance_home):
338 raise UsageError, _('Instance home does not exist')%locals()
339 if not os.path.exists(os.path.join(instance_home, 'html')):
340 raise UsageError, _('Instance has not been installed')%locals()
342 # is there already a database?
343 if os.path.exists(os.path.join(instance_home, 'db')):
344 print _('WARNING: The database is already initialised!')
345 print _('If you re-initialise it, you will lose all the data!')
346 ok = raw_input(_('Erase it? Y/[N]: ')).strip()
347 if ok.lower() != 'y':
348 return 0
350 # nuke it
351 shutil.rmtree(os.path.join(instance_home, 'db'))
353 # GO
354 init.initialise(instance_home, adminpw)
356 return 0
359 def do_get(self, args):
360 '''Usage: get property designator[,designator]*
361 Get the given property of one or more designator(s).
363 Retrieves the property value of the nodes specified by the designators.
364 '''
365 if len(args) < 2:
366 raise UsageError, _('Not enough arguments supplied')
367 propname = args[0]
368 designators = args[1].split(',')
369 l = []
370 for designator in designators:
371 # decode the node designator
372 try:
373 classname, nodeid = roundupdb.splitDesignator(designator)
374 except roundupdb.DesignatorError, message:
375 raise UsageError, message
377 # get the class
378 cl = self.get_class(classname)
379 try:
380 if self.comma_sep:
381 l.append(cl.get(nodeid, propname))
382 else:
383 print cl.get(nodeid, propname)
384 except IndexError:
385 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
386 except KeyError:
387 raise UsageError, _('no such %(classname)s property '
388 '"%(propname)s"')%locals()
389 if self.comma_sep:
390 print ','.join(l)
391 return 0
394 def do_set(self, args):
395 '''Usage: set designator[,designator]* propname=value ...
396 Set the given property of one or more designator(s).
398 Sets the property to the value for all designators given.
399 '''
400 if len(args) < 2:
401 raise UsageError, _('Not enough arguments supplied')
402 from roundup import hyperdb
404 designators = args[0].split(',')
406 # get the props from the args
407 props = self.props_from_args(args[1:])
409 # now do the set for all the nodes
410 for designator in designators:
411 # decode the node designator
412 try:
413 classname, nodeid = roundupdb.splitDesignator(designator)
414 except roundupdb.DesignatorError, message:
415 raise UsageError, message
417 # get the class
418 cl = self.get_class(classname)
420 properties = cl.getprops()
421 for key, value in props.items():
422 proptype = properties[key]
423 if isinstance(proptype, hyperdb.String):
424 continue
425 elif isinstance(proptype, hyperdb.Password):
426 props[key] = password.Password(value)
427 elif isinstance(proptype, hyperdb.Date):
428 try:
429 props[key] = date.Date(value)
430 except ValueError, message:
431 raise UsageError, '"%s": %s'%(value, message)
432 elif isinstance(proptype, hyperdb.Interval):
433 try:
434 props[key] = date.Interval(value)
435 except ValueError, message:
436 raise UsageError, '"%s": %s'%(value, message)
437 elif isinstance(proptype, hyperdb.Link):
438 props[key] = value
439 elif isinstance(proptype, hyperdb.Multilink):
440 props[key] = value.split(',')
442 # try the set
443 try:
444 apply(cl.set, (nodeid, ), props)
445 except (TypeError, IndexError, ValueError), message:
446 raise UsageError, message
447 return 0
449 def do_find(self, args):
450 '''Usage: find classname propname=value ...
451 Find the nodes of the given class with a given link property value.
453 Find the nodes of the given class with a given link property value. The
454 value may be either the nodeid of the linked node, or its key value.
455 '''
456 if len(args) < 1:
457 raise UsageError, _('Not enough arguments supplied')
458 classname = args[0]
459 # get the class
460 cl = self.get_class(classname)
462 # handle the propname=value argument
463 props = self.props_from_args(args[1:])
465 # if the value isn't a number, look up the linked class to get the
466 # number
467 for propname, value in props.items():
468 num_re = re.compile('^\d+$')
469 if not num_re.match(value):
470 # get the property
471 try:
472 property = cl.properties[propname]
473 except KeyError:
474 raise UsageError, _('%(classname)s has no property '
475 '"%(propname)s"')%locals()
477 # make sure it's a link
478 if (not isinstance(property, hyperdb.Link) and not
479 isinstance(property, hyperdb.Multilink)):
480 raise UsageError, _('You may only "find" link properties')
482 # get the linked-to class and look up the key property
483 link_class = self.db.getclass(property.classname)
484 try:
485 props[propname] = link_class.lookup(value)
486 except TypeError:
487 raise UsageError, _('%(classname)s has no key property"')%{
488 'classname': link_class.classname}
490 # now do the find
491 try:
492 if self.comma_sep:
493 print ','.join(apply(cl.find, (), props))
494 else:
495 print apply(cl.find, (), props)
496 except KeyError:
497 raise UsageError, _('%(classname)s has no property '
498 '"%(propname)s"')%locals()
499 except (ValueError, TypeError), message:
500 raise UsageError, message
501 return 0
503 def do_specification(self, args):
504 '''Usage: specification classname
505 Show the properties for a classname.
507 This lists the properties for a given class.
508 '''
509 if len(args) < 1:
510 raise UsageError, _('Not enough arguments supplied')
511 classname = args[0]
512 # get the class
513 cl = self.get_class(classname)
515 # get the key property
516 keyprop = cl.getkey()
517 for key, value in cl.properties.items():
518 if keyprop == key:
519 print _('%(key)s: %(value)s (key property)')%locals()
520 else:
521 print _('%(key)s: %(value)s')%locals()
523 def do_display(self, args):
524 '''Usage: display designator
525 Show the property values for the given node.
527 This lists the properties and their associated values for the given
528 node.
529 '''
530 if len(args) < 1:
531 raise UsageError, _('Not enough arguments supplied')
533 # decode the node designator
534 try:
535 classname, nodeid = roundupdb.splitDesignator(args[0])
536 except roundupdb.DesignatorError, message:
537 raise UsageError, message
539 # get the class
540 cl = self.get_class(classname)
542 # display the values
543 for key in cl.properties.keys():
544 value = cl.get(nodeid, key)
545 print _('%(key)s: %(value)s')%locals()
547 def do_create(self, args):
548 '''Usage: create classname property=value ...
549 Create a new entry of a given class.
551 This creates a new entry of the given class using the property
552 name=value arguments provided on the command line after the "create"
553 command.
554 '''
555 if len(args) < 1:
556 raise UsageError, _('Not enough arguments supplied')
557 from roundup import hyperdb
559 classname = args[0]
561 # get the class
562 cl = self.get_class(classname)
564 # now do a create
565 props = {}
566 properties = cl.getprops(protected = 0)
567 if len(args) == 1:
568 # ask for the properties
569 for key, value in properties.items():
570 if key == 'id': continue
571 name = value.__class__.__name__
572 if isinstance(value , hyperdb.Password):
573 again = None
574 while value != again:
575 value = getpass.getpass(_('%(propname)s (Password): ')%{
576 'propname': key.capitalize()})
577 again = getpass.getpass(_(' %(propname)s (Again): ')%{
578 'propname': key.capitalize()})
579 if value != again: print _('Sorry, try again...')
580 if value:
581 props[key] = value
582 else:
583 value = raw_input(_('%(propname)s (%(proptype)s): ')%{
584 'propname': key.capitalize(), 'proptype': name})
585 if value:
586 props[key] = value
587 else:
588 props = self.props_from_args(args[1:])
590 # convert types
591 for propname, value in props.items():
592 # get the property
593 try:
594 proptype = properties[propname]
595 except KeyError:
596 raise UsageError, _('%(classname)s has no property '
597 '"%(propname)s"')%locals()
599 if isinstance(proptype, hyperdb.Date):
600 try:
601 props[propname] = date.Date(value)
602 except ValueError, message:
603 raise UsageError, _('"%(value)s": %(message)s')%locals()
604 elif isinstance(proptype, hyperdb.Interval):
605 try:
606 props[propname] = date.Interval(value)
607 except ValueError, message:
608 raise UsageError, _('"%(value)s": %(message)s')%locals()
609 elif isinstance(proptype, hyperdb.Password):
610 props[propname] = password.Password(value)
611 elif isinstance(proptype, hyperdb.Multilink):
612 props[propname] = value.split(',')
614 # check for the key property
615 propname = cl.getkey()
616 if propname and not props.has_key(propname):
617 raise UsageError, _('you must provide the "%(propname)s" '
618 'property.')%locals()
620 # do the actual create
621 try:
622 print apply(cl.create, (), props)
623 except (TypeError, IndexError, ValueError), message:
624 raise UsageError, message
625 return 0
627 def do_list(self, args):
628 '''Usage: list classname [property]
629 List the instances of a class.
631 Lists all instances of the given class. If the property is not
632 specified, the "label" property is used. The label property is tried
633 in order: the key, "name", "title" and then the first property,
634 alphabetically.
635 '''
636 if len(args) < 1:
637 raise UsageError, _('Not enough arguments supplied')
638 classname = args[0]
640 # get the class
641 cl = self.get_class(classname)
643 # figure the property
644 if len(args) > 1:
645 propname = args[1]
646 else:
647 propname = cl.labelprop()
649 if self.comma_sep:
650 print ','.join(cl.list())
651 else:
652 for nodeid in cl.list():
653 try:
654 value = cl.get(nodeid, propname)
655 except KeyError:
656 raise UsageError, _('%(classname)s has no property '
657 '"%(propname)s"')%locals()
658 print _('%(nodeid)4s: %(value)s')%locals()
659 return 0
661 def do_table(self, args):
662 '''Usage: table classname [property[,property]*]
663 List the instances of a class in tabular form.
665 Lists all instances of the given class. If the properties are not
666 specified, all properties are displayed. By default, the column widths
667 are the width of the property names. The width may be explicitly defined
668 by defining the property as "name:width". For example::
669 roundup> table priority id,name:10
670 Id Name
671 1 fatal-bug
672 2 bug
673 3 usability
674 4 feature
675 '''
676 if len(args) < 1:
677 raise UsageError, _('Not enough arguments supplied')
678 classname = args[0]
680 # get the class
681 cl = self.get_class(classname)
683 # figure the property names to display
684 if len(args) > 1:
685 prop_names = args[1].split(',')
686 all_props = cl.getprops()
687 for spec in prop_names:
688 if ':' in spec:
689 try:
690 propname, width = spec.split(':')
691 except (ValueError, TypeError):
692 raise UsageError, _('"%(spec)s" not name:width')%locals()
693 else:
694 propname = spec
695 if not all_props.has_key(propname):
696 raise UsageError, _('%(classname)s has no property '
697 '"%(propname)s"')%locals()
698 else:
699 prop_names = cl.getprops().keys()
701 # now figure column widths
702 props = []
703 for spec in prop_names:
704 if ':' in spec:
705 name, width = spec.split(':')
706 props.append((name, int(width)))
707 else:
708 props.append((spec, len(spec)))
710 # now display the heading
711 print ' '.join([name.capitalize().ljust(width) for name,width in props])
713 # and the table data
714 for nodeid in cl.list():
715 l = []
716 for name, width in props:
717 if name != 'id':
718 try:
719 value = str(cl.get(nodeid, name))
720 except KeyError:
721 # we already checked if the property is valid - a
722 # KeyError here means the node just doesn't have a
723 # value for it
724 value = ''
725 else:
726 value = str(nodeid)
727 f = '%%-%ds'%width
728 l.append(f%value[:width])
729 print ' '.join(l)
730 return 0
732 def do_history(self, args):
733 '''Usage: history designator
734 Show the history entries of a designator.
736 Lists the journal entries for the node identified by the designator.
737 '''
738 if len(args) < 1:
739 raise UsageError, _('Not enough arguments supplied')
740 try:
741 classname, nodeid = roundupdb.splitDesignator(args[0])
742 except roundupdb.DesignatorError, message:
743 raise UsageError, message
745 try:
746 print self.db.getclass(classname).history(nodeid)
747 except KeyError:
748 raise UsageError, _('no such class "%(classname)s"')%locals()
749 except IndexError:
750 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
751 return 0
753 def do_commit(self, args):
754 '''Usage: commit
755 Commit all changes made to the database.
757 The changes made during an interactive session are not
758 automatically written to the database - they must be committed
759 using this command.
761 One-off commands on the command-line are automatically committed if
762 they are successful.
763 '''
764 self.db.commit()
765 return 0
767 def do_rollback(self, args):
768 '''Usage: rollback
769 Undo all changes that are pending commit to the database.
771 The changes made during an interactive session are not
772 automatically written to the database - they must be committed
773 manually. This command undoes all those changes, so a commit
774 immediately after would make no changes to the database.
775 '''
776 self.db.rollback()
777 return 0
779 def do_retire(self, args):
780 '''Usage: retire designator[,designator]*
781 Retire the node specified by designator.
783 This action indicates that a particular node is not to be retrieved by
784 the list or find commands, and its key value may be re-used.
785 '''
786 if len(args) < 1:
787 raise UsageError, _('Not enough arguments supplied')
788 designators = args[0].split(',')
789 for designator in designators:
790 try:
791 classname, nodeid = roundupdb.splitDesignator(designator)
792 except roundupdb.DesignatorError, message:
793 raise UsageError, message
794 try:
795 self.db.getclass(classname).retire(nodeid)
796 except KeyError:
797 raise UsageError, _('no such class "%(classname)s"')%locals()
798 except IndexError:
799 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
800 return 0
802 def do_export(self, args):
803 '''Usage: export class[,class] destination_dir
804 Export the database to tab-separated-value files.
806 This action exports the current data from the database into
807 tab-separated-value files that are placed in the nominated destination
808 directory. The journals are not exported.
809 '''
810 if len(args) < 2:
811 raise UsageError, _('Not enough arguments supplied')
812 classes = args[0].split(',')
813 dir = args[1]
815 # use the csv parser if we can - it's faster
816 if csv is not None:
817 p = csv.parser(field_sep=':')
819 # do all the classes specified
820 for classname in classes:
821 cl = self.get_class(classname)
822 f = open(os.path.join(dir, classname+'.csv'), 'w')
823 f.write(':'.join(cl.properties.keys()) + '\n')
825 # all nodes for this class
826 properties = cl.properties.items()
827 for nodeid in cl.list():
828 l = []
829 for prop, proptype in properties:
830 value = cl.get(nodeid, prop)
831 # convert data where needed
832 if isinstance(proptype, hyperdb.Date):
833 value = value.get_tuple()
834 elif isinstance(proptype, hyperdb.Interval):
835 value = value.get_tuple()
836 elif isinstance(proptype, hyperdb.Password):
837 value = str(value)
838 l.append(repr(value))
840 # now write
841 if csv is not None:
842 f.write(p.join(l) + '\n')
843 else:
844 # escape the individual entries to they're valid CSV
845 m = []
846 for entry in l:
847 if '"' in entry:
848 entry = '""'.join(entry.split('"'))
849 if ':' in entry:
850 entry = '"%s"'%entry
851 m.append(entry)
852 f.write(':'.join(m) + '\n')
853 return 0
855 def do_import(self, args):
856 '''Usage: import class file
857 Import the contents of the tab-separated-value file.
859 The file must define the same properties as the class (including having
860 a "header" line with those property names.) The new nodes are added to
861 the existing database - if you want to create a new database using the
862 imported data, then create a new database (or, tediously, retire all
863 the old data.)
864 '''
865 if len(args) < 2:
866 raise UsageError, _('Not enough arguments supplied')
867 if csv is None:
868 raise UsageError, \
869 _('Sorry, you need the csv module to use this function.\n'
870 'Get it from: http://www.object-craft.com.au/projects/csv/')
872 from roundup import hyperdb
874 # ensure that the properties and the CSV file headings match
875 classname = args[0]
876 cl = self.get_class(classname)
877 f = open(args[1])
878 p = csv.parser(field_sep=':')
879 file_props = p.parse(f.readline())
880 props = cl.properties.keys()
881 m = file_props[:]
882 m.sort()
883 props.sort()
884 if m != props:
885 raise UsageError, _('Import file doesn\'t define the same '
886 'properties as "%(arg0)s".')%{'arg0': args[0]}
888 # loop through the file and create a node for each entry
889 n = range(len(props))
890 while 1:
891 line = f.readline()
892 if not line: break
894 # parse lines until we get a complete entry
895 while 1:
896 l = p.parse(line)
897 if l: break
898 line = f.readline()
899 if not line:
900 raise ValueError, "Unexpected EOF during CSV parse"
902 # make the new node's property map
903 d = {}
904 for i in n:
905 # Use eval to reverse the repr() used to output the CSV
906 value = eval(l[i])
907 # Figure the property for this column
908 key = file_props[i]
909 proptype = cl.properties[key]
910 # Convert for property type
911 if isinstance(proptype, hyperdb.Date):
912 value = date.Date(value)
913 elif isinstance(proptype, hyperdb.Interval):
914 value = date.Interval(value)
915 elif isinstance(proptype, hyperdb.Password):
916 pwd = password.Password()
917 pwd.unpack(value)
918 value = pwd
919 if value is not None:
920 d[key] = value
922 # and create the new node
923 apply(cl.create, (), d)
924 return 0
926 def do_pack(self, args):
927 '''Usage: pack period | date
929 Remove journal entries older than a period of time specified or
930 before a certain date.
932 A period is specified using the suffixes "y", "m", and "d". The
933 suffix "w" (for "week") means 7 days.
935 "3y" means three years
936 "2y 1m" means two years and one month
937 "1m 25d" means one month and 25 days
938 "2w 3d" means two weeks and three days
940 Date format is "YYYY-MM-DD" eg:
941 2001-01-01
943 '''
944 if len(args) <> 1:
945 raise UsageError, _('Not enough arguments supplied')
947 # are we dealing with a period or a date
948 value = args[0]
949 date_re = re.compile(r'''
950 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
951 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
952 ''', re.VERBOSE)
953 m = date_re.match(value)
954 if not m:
955 raise ValueError, _('Invalid format')
956 m = m.groupdict()
957 if m['period']:
958 # TODO: need to fix date module. one should be able to say
959 # pack_before = date.Date(". - %s"%value)
960 pack_before = date.Date(".") + date.Interval("- %s"%value)
961 elif m['date']:
962 pack_before = date.Date(value)
963 self.db.pack(pack_before)
964 return 0
966 def run_command(self, args):
967 '''Run a single command
968 '''
969 command = args[0]
971 # handle help now
972 if command == 'help':
973 if len(args)>1:
974 self.do_help(args[1:])
975 return 0
976 self.do_help(['help'])
977 return 0
978 if command == 'morehelp':
979 self.do_help(['help'])
980 self.help_commands()
981 self.help_all()
982 return 0
984 # figure what the command is
985 try:
986 functions = self.commands.get(command)
987 except KeyError:
988 # not a valid command
989 print _('Unknown command "%(command)s" ("help commands" for a '
990 'list)')%locals()
991 return 1
993 # check for multiple matches
994 if len(functions) > 1:
995 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
996 command, 'list': ', '.join([i[0] for i in functions])}
997 return 1
998 command, function = functions[0]
1000 # make sure we have an instance_home
1001 while not self.instance_home:
1002 self.instance_home = raw_input(_('Enter instance home: ')).strip()
1004 # before we open the db, we may be doing an install or init
1005 if command == 'initialise':
1006 try:
1007 return self.do_initialise(self.instance_home, args)
1008 except UsageError, message:
1009 print _('Error: %(message)s')%locals()
1010 return 1
1011 elif command == 'install':
1012 try:
1013 return self.do_install(self.instance_home, args)
1014 except UsageError, message:
1015 print _('Error: %(message)s')%locals()
1016 return 1
1018 # get the instance
1019 try:
1020 instance = roundup.instance.open(self.instance_home)
1021 except ValueError, message:
1022 self.instance_home = ''
1023 print _("Error: Couldn't open instance: %(message)s")%locals()
1024 return 1
1026 # only open the database once!
1027 if not self.db:
1028 self.db = instance.open('admin')
1030 # do the command
1031 ret = 0
1032 try:
1033 ret = function(args[1:])
1034 except UsageError, message:
1035 print _('Error: %(message)s')%locals()
1036 print
1037 print function.__doc__
1038 ret = 1
1039 except:
1040 import traceback
1041 traceback.print_exc()
1042 ret = 1
1043 return ret
1045 def interactive(self):
1046 '''Run in an interactive mode
1047 '''
1048 print _('Roundup {version} ready for input.')
1049 print _('Type "help" for help.')
1050 try:
1051 import readline
1052 except ImportError:
1053 print _('Note: command history and editing not available')
1055 while 1:
1056 try:
1057 command = raw_input(_('roundup> '))
1058 except EOFError:
1059 print _('exit...')
1060 break
1061 if not command: continue
1062 args = token.token_split(command)
1063 if not args: continue
1064 if args[0] in ('quit', 'exit'): break
1065 self.run_command(args)
1067 # exit.. check for transactions
1068 if self.db and self.db.transactions:
1069 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1070 if commit and commit[0].lower() == 'y':
1071 self.db.commit()
1072 return 0
1074 def main(self):
1075 try:
1076 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1077 except getopt.GetoptError, e:
1078 self.usage(str(e))
1079 return 1
1081 # handle command-line args
1082 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
1083 # TODO: reinstate the user/password stuff (-u arg too)
1084 name = password = ''
1085 if os.environ.has_key('ROUNDUP_LOGIN'):
1086 l = os.environ['ROUNDUP_LOGIN'].split(':')
1087 name = l[0]
1088 if len(l) > 1:
1089 password = l[1]
1090 self.comma_sep = 0
1091 for opt, arg in opts:
1092 if opt == '-h':
1093 self.usage()
1094 return 0
1095 if opt == '-i':
1096 self.instance_home = arg
1097 if opt == '-c':
1098 self.comma_sep = 1
1100 # if no command - go interactive
1101 ret = 0
1102 if not args:
1103 self.interactive()
1104 else:
1105 ret = self.run_command(args)
1106 if self.db: self.db.commit()
1107 return ret
1110 if __name__ == '__main__':
1111 tool = AdminTool()
1112 sys.exit(tool.main())
1114 #
1115 # $Log: not supported by cvs2svn $
1116 # Revision 1.13 2002/05/30 23:58:14 richard
1117 # oops
1118 #
1119 # Revision 1.12 2002/05/26 09:04:42 richard
1120 # out by one in the init args
1121 #
1122 # Revision 1.11 2002/05/23 01:14:20 richard
1123 # . split instance initialisation into two steps, allowing config changes
1124 # before the database is initialised.
1125 #
1126 # Revision 1.10 2002/04/27 10:07:23 richard
1127 # minor fix to error message
1128 #
1129 # Revision 1.9 2002/03/12 22:51:47 richard
1130 # . #527416 ] roundup-admin uses undefined value
1131 # . #527503 ] unfriendly init blowup when parent dir
1132 # (also handles UsageError correctly now in init)
1133 #
1134 # Revision 1.8 2002/02/27 03:28:21 richard
1135 # Ran it through pychecker, made fixes
1136 #
1137 # Revision 1.7 2002/02/20 05:04:32 richard
1138 # Wasn't handling the cvs parser feeding properly.
1139 #
1140 # Revision 1.6 2002/01/23 07:27:19 grubert
1141 # . allow abbreviation of "help" in admin tool too.
1142 #
1143 # Revision 1.5 2002/01/21 16:33:19 rochecompaan
1144 # You can now use the roundup-admin tool to pack the database
1145 #
1146 # Revision 1.4 2002/01/14 06:51:09 richard
1147 # . #503164 ] create and passwords
1148 #
1149 # Revision 1.3 2002/01/08 05:26:32 rochecompaan
1150 # Missing "self" in props_from_args
1151 #
1152 # Revision 1.2 2002/01/07 10:41:44 richard
1153 # #500140 ] AdminTool.get_class() returns nothing
1154 #
1155 # Revision 1.1 2002/01/05 02:11:22 richard
1156 # I18N'ed roundup admin - and split the code off into a module so it can be used
1157 # elsewhere.
1158 # Big issue with this is the doc strings - that's the help. We're probably going to
1159 # have to switch to not use docstrings, which will suck a little :(
1160 #
1161 #
1162 #
1163 # vim: set filetype=python ts=4 sw=4 et si