c2c8840e4cde7b1b39c00cccbd7cb5b38ee5bf58
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.7 2002-02-20 05:04:32 richard Exp $
21 import sys, os, getpass, getopt, re, UserDict, shlex
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, klass=None):
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_initialise(self, instance_home, args):
253 '''Usage: initialise [template [backend [admin password]]]
254 Initialise 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 See also initopts help.
262 '''
263 if len(args) < 1:
264 raise UsageError, _('Not enough arguments supplied')
265 # select template
266 import roundup.templates
267 templates = roundup.templates.listTemplates()
268 template = len(args) > 1 and args[1] or ''
269 if template not in templates:
270 print _('Templates:'), ', '.join(templates)
271 while template not in templates:
272 template = raw_input(_('Select template [classic]: ')).strip()
273 if not template:
274 template = 'classic'
276 import roundup.backends
277 backends = roundup.backends.__all__
278 backend = len(args) > 2 and args[2] or ''
279 if backend not in backends:
280 print _('Back ends:'), ', '.join(backends)
281 while backend not in backends:
282 backend = raw_input(_('Select backend [anydbm]: ')).strip()
283 if not backend:
284 backend = 'anydbm'
285 if len(args) > 3:
286 adminpw = confirm = args[3]
287 else:
288 adminpw = ''
289 confirm = 'x'
290 while adminpw != confirm:
291 adminpw = getpass.getpass(_('Admin Password: '))
292 confirm = getpass.getpass(_(' Confirm: '))
293 init.init(instance_home, template, backend, adminpw)
294 return 0
297 def do_get(self, args):
298 '''Usage: get property designator[,designator]*
299 Get the given property of one or more designator(s).
301 Retrieves the property value of the nodes specified by the designators.
302 '''
303 if len(args) < 2:
304 raise UsageError, _('Not enough arguments supplied')
305 propname = args[0]
306 designators = args[1].split(',')
307 l = []
308 for designator in designators:
309 # decode the node designator
310 try:
311 classname, nodeid = roundupdb.splitDesignator(designator)
312 except roundupdb.DesignatorError, message:
313 raise UsageError, message
315 # get the class
316 cl = self.get_class(classname)
317 try:
318 if self.comma_sep:
319 l.append(cl.get(nodeid, propname))
320 else:
321 print cl.get(nodeid, propname)
322 except IndexError:
323 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
324 except KeyError:
325 raise UsageError, _('no such %(classname)s property '
326 '"%(propname)s"')%locals()
327 if self.comma_sep:
328 print ','.join(l)
329 return 0
332 def do_set(self, args):
333 '''Usage: set designator[,designator]* propname=value ...
334 Set the given property of one or more designator(s).
336 Sets the property to the value for all designators given.
337 '''
338 if len(args) < 2:
339 raise UsageError, _('Not enough arguments supplied')
340 from roundup import hyperdb
342 designators = args[0].split(',')
344 # get the props from the args
345 props = self.props_from_args(args[1:])
347 # now do the set for all the nodes
348 for designator in designators:
349 # decode the node designator
350 try:
351 classname, nodeid = roundupdb.splitDesignator(designator)
352 except roundupdb.DesignatorError, message:
353 raise UsageError, message
355 # get the class
356 cl = self.get_class(classname)
358 properties = cl.getprops()
359 for key, value in props.items():
360 proptype = properties[key]
361 if isinstance(proptype, hyperdb.String):
362 continue
363 elif isinstance(proptype, hyperdb.Password):
364 props[key] = password.Password(value)
365 elif isinstance(proptype, hyperdb.Date):
366 try:
367 props[key] = date.Date(value)
368 except ValueError, message:
369 raise UsageError, '"%s": %s'%(value, message)
370 elif isinstance(proptype, hyperdb.Interval):
371 try:
372 props[key] = date.Interval(value)
373 except ValueError, message:
374 raise UsageError, '"%s": %s'%(value, message)
375 elif isinstance(proptype, hyperdb.Link):
376 props[key] = value
377 elif isinstance(proptype, hyperdb.Multilink):
378 props[key] = value.split(',')
380 # try the set
381 try:
382 apply(cl.set, (nodeid, ), props)
383 except (TypeError, IndexError, ValueError), message:
384 raise UsageError, message
385 return 0
387 def do_find(self, args):
388 '''Usage: find classname propname=value ...
389 Find the nodes of the given class with a given link property value.
391 Find the nodes of the given class with a given link property value. The
392 value may be either the nodeid of the linked node, or its key value.
393 '''
394 if len(args) < 1:
395 raise UsageError, _('Not enough arguments supplied')
396 classname = args[0]
397 # get the class
398 cl = self.get_class(classname)
400 # handle the propname=value argument
401 props = self.props_from_args(args[1:])
403 # if the value isn't a number, look up the linked class to get the
404 # number
405 for propname, value in props.items():
406 num_re = re.compile('^\d+$')
407 if not num_re.match(value):
408 # get the property
409 try:
410 property = cl.properties[propname]
411 except KeyError:
412 raise UsageError, _('%(classname)s has no property '
413 '"%(propname)s"')%locals()
415 # make sure it's a link
416 if (not isinstance(property, hyperdb.Link) and not
417 isinstance(property, hyperdb.Multilink)):
418 raise UsageError, _('You may only "find" link properties')
420 # get the linked-to class and look up the key property
421 link_class = self.db.getclass(property.classname)
422 try:
423 props[propname] = link_class.lookup(value)
424 except TypeError:
425 raise UsageError, _('%(classname)s has no key property"')%{
426 'classname': link_class.classname}
427 except KeyError:
428 raise UsageError, _('%(classname)s has no entry "%(propname)s"')%{
429 'classname': link_class.classname, 'propname': propname}
431 # now do the find
432 try:
433 if self.comma_sep:
434 print ','.join(apply(cl.find, (), props))
435 else:
436 print apply(cl.find, (), props)
437 except KeyError:
438 raise UsageError, _('%(classname)s has no property '
439 '"%(propname)s"')%locals()
440 except (ValueError, TypeError), message:
441 raise UsageError, message
442 return 0
444 def do_specification(self, args):
445 '''Usage: specification classname
446 Show the properties for a classname.
448 This lists the properties for a given class.
449 '''
450 if len(args) < 1:
451 raise UsageError, _('Not enough arguments supplied')
452 classname = args[0]
453 # get the class
454 cl = self.get_class(classname)
456 # get the key property
457 keyprop = cl.getkey()
458 for key, value in cl.properties.items():
459 if keyprop == key:
460 print _('%(key)s: %(value)s (key property)')%locals()
461 else:
462 print _('%(key)s: %(value)s')%locals()
464 def do_display(self, args):
465 '''Usage: display designator
466 Show the property values for the given node.
468 This lists the properties and their associated values for the given
469 node.
470 '''
471 if len(args) < 1:
472 raise UsageError, _('Not enough arguments supplied')
474 # decode the node designator
475 try:
476 classname, nodeid = roundupdb.splitDesignator(args[0])
477 except roundupdb.DesignatorError, message:
478 raise UsageError, message
480 # get the class
481 cl = self.get_class(classname)
483 # display the values
484 for key in cl.properties.keys():
485 value = cl.get(nodeid, key)
486 print _('%(key)s: %(value)s')%locals()
488 def do_create(self, args):
489 '''Usage: create classname property=value ...
490 Create a new entry of a given class.
492 This creates a new entry of the given class using the property
493 name=value arguments provided on the command line after the "create"
494 command.
495 '''
496 if len(args) < 1:
497 raise UsageError, _('Not enough arguments supplied')
498 from roundup import hyperdb
500 classname = args[0]
502 # get the class
503 cl = self.get_class(classname)
505 # now do a create
506 props = {}
507 properties = cl.getprops(protected = 0)
508 if len(args) == 1:
509 # ask for the properties
510 for key, value in properties.items():
511 if key == 'id': continue
512 name = value.__class__.__name__
513 if isinstance(value , hyperdb.Password):
514 again = None
515 while value != again:
516 value = getpass.getpass(_('%(propname)s (Password): ')%{
517 'propname': key.capitalize()})
518 again = getpass.getpass(_(' %(propname)s (Again): ')%{
519 'propname': key.capitalize()})
520 if value != again: print _('Sorry, try again...')
521 if value:
522 props[key] = value
523 else:
524 value = raw_input(_('%(propname)s (%(proptype)s): ')%{
525 'propname': key.capitalize(), 'proptype': name})
526 if value:
527 props[key] = value
528 else:
529 props = self.props_from_args(args[1:])
531 # convert types
532 for propname in props.keys():
533 # get the property
534 try:
535 proptype = properties[propname]
536 except KeyError:
537 raise UsageError, _('%(classname)s has no property '
538 '"%(propname)s"')%locals()
540 if isinstance(proptype, hyperdb.Date):
541 try:
542 props[propname] = date.Date(value)
543 except ValueError, message:
544 raise UsageError, _('"%(value)s": %(message)s')%locals()
545 elif isinstance(proptype, hyperdb.Interval):
546 try:
547 props[propname] = date.Interval(value)
548 except ValueError, message:
549 raise UsageError, _('"%(value)s": %(message)s')%locals()
550 elif isinstance(proptype, hyperdb.Password):
551 props[propname] = password.Password(value)
552 elif isinstance(proptype, hyperdb.Multilink):
553 props[propname] = value.split(',')
555 # check for the key property
556 propname = cl.getkey()
557 if propname and not props.has_key(propname):
558 raise UsageError, _('you must provide the "%(propname)s" '
559 'property.')%locals()
561 # do the actual create
562 try:
563 print apply(cl.create, (), props)
564 except (TypeError, IndexError, ValueError), message:
565 raise UsageError, message
566 return 0
568 def do_list(self, args):
569 '''Usage: list classname [property]
570 List the instances of a class.
572 Lists all instances of the given class. If the property is not
573 specified, the "label" property is used. The label property is tried
574 in order: the key, "name", "title" and then the first property,
575 alphabetically.
576 '''
577 if len(args) < 1:
578 raise UsageError, _('Not enough arguments supplied')
579 classname = args[0]
581 # get the class
582 cl = self.get_class(classname)
584 # figure the property
585 if len(args) > 1:
586 propname = args[1]
587 else:
588 propname = cl.labelprop()
590 if self.comma_sep:
591 print ','.join(cl.list())
592 else:
593 for nodeid in cl.list():
594 try:
595 value = cl.get(nodeid, propname)
596 except KeyError:
597 raise UsageError, _('%(classname)s has no property '
598 '"%(propname)s"')%locals()
599 print _('%(nodeid)4s: %(value)s')%locals()
600 return 0
602 def do_table(self, args):
603 '''Usage: table classname [property[,property]*]
604 List the instances of a class in tabular form.
606 Lists all instances of the given class. If the properties are not
607 specified, all properties are displayed. By default, the column widths
608 are the width of the property names. The width may be explicitly defined
609 by defining the property as "name:width". For example::
610 roundup> table priority id,name:10
611 Id Name
612 1 fatal-bug
613 2 bug
614 3 usability
615 4 feature
616 '''
617 if len(args) < 1:
618 raise UsageError, _('Not enough arguments supplied')
619 classname = args[0]
621 # get the class
622 cl = self.get_class(classname)
624 # figure the property names to display
625 if len(args) > 1:
626 prop_names = args[1].split(',')
627 all_props = cl.getprops()
628 for spec in prop_names:
629 if ':' in spec:
630 try:
631 propname, width = spec.split(':')
632 except (ValueError, TypeError):
633 raise UsageError, _('"%(spec)s" not name:width')%locals()
634 else:
635 propname = spec
636 if not all_props.has_key(propname):
637 raise UsageError, _('%(classname)s has no property '
638 '"%(propname)s"')%locals()
639 else:
640 prop_names = cl.getprops().keys()
642 # now figure column widths
643 props = []
644 for spec in prop_names:
645 if ':' in spec:
646 name, width = spec.split(':')
647 props.append((name, int(width)))
648 else:
649 props.append((spec, len(spec)))
651 # now display the heading
652 print ' '.join([name.capitalize().ljust(width) for name,width in props])
654 # and the table data
655 for nodeid in cl.list():
656 l = []
657 for name, width in props:
658 if name != 'id':
659 try:
660 value = str(cl.get(nodeid, name))
661 except KeyError:
662 # we already checked if the property is valid - a
663 # KeyError here means the node just doesn't have a
664 # value for it
665 value = ''
666 else:
667 value = str(nodeid)
668 f = '%%-%ds'%width
669 l.append(f%value[:width])
670 print ' '.join(l)
671 return 0
673 def do_history(self, args):
674 '''Usage: history designator
675 Show the history entries of a designator.
677 Lists the journal entries for the node identified by the designator.
678 '''
679 if len(args) < 1:
680 raise UsageError, _('Not enough arguments supplied')
681 try:
682 classname, nodeid = roundupdb.splitDesignator(args[0])
683 except roundupdb.DesignatorError, message:
684 raise UsageError, message
686 try:
687 print self.db.getclass(classname).history(nodeid)
688 except KeyError:
689 raise UsageError, _('no such class "%(classname)s"')%locals()
690 except IndexError:
691 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
692 return 0
694 def do_commit(self, args):
695 '''Usage: commit
696 Commit all changes made to the database.
698 The changes made during an interactive session are not
699 automatically written to the database - they must be committed
700 using this command.
702 One-off commands on the command-line are automatically committed if
703 they are successful.
704 '''
705 self.db.commit()
706 return 0
708 def do_rollback(self, args):
709 '''Usage: rollback
710 Undo all changes that are pending commit to the database.
712 The changes made during an interactive session are not
713 automatically written to the database - they must be committed
714 manually. This command undoes all those changes, so a commit
715 immediately after would make no changes to the database.
716 '''
717 self.db.rollback()
718 return 0
720 def do_retire(self, args):
721 '''Usage: retire designator[,designator]*
722 Retire the node specified by designator.
724 This action indicates that a particular node is not to be retrieved by
725 the list or find commands, and its key value may be re-used.
726 '''
727 if len(args) < 1:
728 raise UsageError, _('Not enough arguments supplied')
729 designators = args[0].split(',')
730 for designator in designators:
731 try:
732 classname, nodeid = roundupdb.splitDesignator(designator)
733 except roundupdb.DesignatorError, message:
734 raise UsageError, message
735 try:
736 self.db.getclass(classname).retire(nodeid)
737 except KeyError:
738 raise UsageError, _('no such class "%(classname)s"')%locals()
739 except IndexError:
740 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
741 return 0
743 def do_export(self, args):
744 '''Usage: export class[,class] destination_dir
745 Export the database to tab-separated-value files.
747 This action exports the current data from the database into
748 tab-separated-value files that are placed in the nominated destination
749 directory. The journals are not exported.
750 '''
751 if len(args) < 2:
752 raise UsageError, _('Not enough arguments supplied')
753 classes = args[0].split(',')
754 dir = args[1]
756 # use the csv parser if we can - it's faster
757 if csv is not None:
758 p = csv.parser(field_sep=':')
760 # do all the classes specified
761 for classname in classes:
762 cl = self.get_class(classname)
763 f = open(os.path.join(dir, classname+'.csv'), 'w')
764 f.write(':'.join(cl.properties.keys()) + '\n')
766 # all nodes for this class
767 properties = cl.properties.items()
768 for nodeid in cl.list():
769 l = []
770 for prop, proptype in properties:
771 value = cl.get(nodeid, prop)
772 # convert data where needed
773 if isinstance(proptype, hyperdb.Date):
774 value = value.get_tuple()
775 elif isinstance(proptype, hyperdb.Interval):
776 value = value.get_tuple()
777 elif isinstance(proptype, hyperdb.Password):
778 value = str(value)
779 l.append(repr(value))
781 # now write
782 if csv is not None:
783 f.write(p.join(l) + '\n')
784 else:
785 # escape the individual entries to they're valid CSV
786 m = []
787 for entry in l:
788 if '"' in entry:
789 entry = '""'.join(entry.split('"'))
790 if ':' in entry:
791 entry = '"%s"'%entry
792 m.append(entry)
793 f.write(':'.join(m) + '\n')
794 return 0
796 def do_import(self, args):
797 '''Usage: import class file
798 Import the contents of the tab-separated-value file.
800 The file must define the same properties as the class (including having
801 a "header" line with those property names.) The new nodes are added to
802 the existing database - if you want to create a new database using the
803 imported data, then create a new database (or, tediously, retire all
804 the old data.)
805 '''
806 if len(args) < 2:
807 raise UsageError, _('Not enough arguments supplied')
808 if csv is None:
809 raise UsageError, \
810 _('Sorry, you need the csv module to use this function.\n'
811 'Get it from: http://www.object-craft.com.au/projects/csv/')
813 from roundup import hyperdb
815 # ensure that the properties and the CSV file headings match
816 classname = args[0]
817 cl = self.get_class(classname)
818 f = open(args[1])
819 p = csv.parser(field_sep=':')
820 file_props = p.parse(f.readline())
821 props = cl.properties.keys()
822 m = file_props[:]
823 m.sort()
824 props.sort()
825 if m != props:
826 raise UsageError, _('Import file doesn\'t define the same '
827 'properties as "%(arg0)s".')%{'arg0': args[0]}
829 # loop through the file and create a node for each entry
830 n = range(len(props))
831 while 1:
832 line = f.readline()
833 if not line: break
835 # parse lines until we get a complete entry
836 while 1:
837 l = p.parse(line)
838 if l: break
839 line = f.readline()
840 if not line:
841 raise ValueError, "Unexpected EOF during CSV parse"
843 # make the new node's property map
844 d = {}
845 for i in n:
846 # Use eval to reverse the repr() used to output the CSV
847 value = eval(l[i])
848 # Figure the property for this column
849 key = file_props[i]
850 proptype = cl.properties[key]
851 # Convert for property type
852 if isinstance(proptype, hyperdb.Date):
853 value = date.Date(value)
854 elif isinstance(proptype, hyperdb.Interval):
855 value = date.Interval(value)
856 elif isinstance(proptype, hyperdb.Password):
857 pwd = password.Password()
858 pwd.unpack(value)
859 value = pwd
860 if value is not None:
861 d[key] = value
863 # and create the new node
864 apply(cl.create, (), d)
865 return 0
867 def do_pack(self, args):
868 '''Usage: pack period | date
870 Remove journal entries older than a period of time specified or
871 before a certain date.
873 A period is specified using the suffixes "y", "m", and "d". The
874 suffix "w" (for "week") means 7 days.
876 "3y" means three years
877 "2y 1m" means two years and one month
878 "1m 25d" means one month and 25 days
879 "2w 3d" means two weeks and three days
881 Date format is "YYYY-MM-DD" eg:
882 2001-01-01
884 '''
885 if len(args) <> 1:
886 raise UsageError, _('Not enough arguments supplied')
888 # are we dealing with a period or a date
889 value = args[0]
890 date_re = re.compile(r'''
891 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
892 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
893 ''', re.VERBOSE)
894 m = date_re.match(value)
895 if not m:
896 raise ValueError, _('Invalid format')
897 m = m.groupdict()
898 if m['period']:
899 # TODO: need to fix date module. one should be able to say
900 # pack_before = date.Date(". - %s"%value)
901 pack_before = date.Date(".") + date.Interval("- %s"%value)
902 elif m['date']:
903 pack_before = date.Date(value)
904 self.db.pack(pack_before)
905 return 0
907 def run_command(self, args):
908 '''Run a single command
909 '''
910 command = args[0]
912 # handle help now
913 if command == 'help':
914 if len(args)>1:
915 self.do_help(args[1:])
916 return 0
917 self.do_help(['help'])
918 return 0
919 if command == 'morehelp':
920 self.do_help(['help'])
921 self.help_commands()
922 self.help_all()
923 return 0
925 # figure what the command is
926 try:
927 functions = self.commands.get(command)
928 except KeyError:
929 # not a valid command
930 print _('Unknown command "%(command)s" ("help commands" for a '
931 'list)')%locals()
932 return 1
934 # check for multiple matches
935 if len(functions) > 1:
936 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
937 command, 'list': ', '.join([i[0] for i in functions])}
938 return 1
939 command, function = functions[0]
941 # make sure we have an instance_home
942 while not self.instance_home:
943 self.instance_home = raw_input(_('Enter instance home: ')).strip()
945 # before we open the db, we may be doing an init
946 if command == 'initialise':
947 return self.do_initialise(self.instance_home, args)
949 # get the instance
950 try:
951 instance = roundup.instance.open(self.instance_home)
952 except ValueError, message:
953 self.instance_home = ''
954 print _("Couldn't open instance: %(message)s")%locals()
955 return 1
957 # only open the database once!
958 if not self.db:
959 self.db = instance.open('admin')
961 # do the command
962 ret = 0
963 try:
964 ret = function(args[1:])
965 except UsageError, message:
966 print _('Error: %(message)s')%locals()
967 print function.__doc__
968 ret = 1
969 except:
970 import traceback
971 traceback.print_exc()
972 ret = 1
973 return ret
975 def interactive(self):
976 '''Run in an interactive mode
977 '''
978 print _('Roundup {version} ready for input.')
979 print _('Type "help" for help.')
980 try:
981 import readline
982 except ImportError:
983 print _('Note: command history and editing not available')
985 while 1:
986 try:
987 command = raw_input(_('roundup> '))
988 except EOFError:
989 print _('exit...')
990 break
991 if not command: continue
992 args = token.token_split(command)
993 if not args: continue
994 if args[0] in ('quit', 'exit'): break
995 self.run_command(args)
997 # exit.. check for transactions
998 if self.db and self.db.transactions:
999 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
1000 if commit and commit[0].lower() == 'y':
1001 self.db.commit()
1002 return 0
1004 def main(self):
1005 try:
1006 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
1007 except getopt.GetoptError, e:
1008 self.usage(str(e))
1009 return 1
1011 # handle command-line args
1012 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
1013 name = password = ''
1014 if os.environ.has_key('ROUNDUP_LOGIN'):
1015 l = os.environ['ROUNDUP_LOGIN'].split(':')
1016 name = l[0]
1017 if len(l) > 1:
1018 password = l[1]
1019 self.comma_sep = 0
1020 for opt, arg in opts:
1021 if opt == '-h':
1022 self.usage()
1023 return 0
1024 if opt == '-i':
1025 self.instance_home = arg
1026 if opt == '-c':
1027 self.comma_sep = 1
1029 # if no command - go interactive
1030 ret = 0
1031 if not args:
1032 self.interactive()
1033 else:
1034 ret = self.run_command(args)
1035 if self.db: self.db.commit()
1036 return ret
1039 if __name__ == '__main__':
1040 tool = AdminTool()
1041 sys.exit(tool.main())
1043 #
1044 # $Log: not supported by cvs2svn $
1045 # Revision 1.6 2002/01/23 07:27:19 grubert
1046 # . allow abbreviation of "help" in admin tool too.
1047 #
1048 # Revision 1.5 2002/01/21 16:33:19 rochecompaan
1049 # You can now use the roundup-admin tool to pack the database
1050 #
1051 # Revision 1.4 2002/01/14 06:51:09 richard
1052 # . #503164 ] create and passwords
1053 #
1054 # Revision 1.3 2002/01/08 05:26:32 rochecompaan
1055 # Missing "self" in props_from_args
1056 #
1057 # Revision 1.2 2002/01/07 10:41:44 richard
1058 # #500140 ] AdminTool.get_class() returns nothing
1059 #
1060 # Revision 1.1 2002/01/05 02:11:22 richard
1061 # I18N'ed roundup admin - and split the code off into a module so it can be used
1062 # elsewhere.
1063 # Big issue with this is the doc strings - that's the help. We're probably going to
1064 # have to switch to not use docstrings, which will suck a little :(
1065 #
1066 #
1067 #
1068 # vim: set filetype=python ts=4 sw=4 et si