d7b702a5f1914db73ed0015005c16a0b954155f4
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.3 2002-01-08 05:26:32 rochecompaan 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 topic = args[0]
214 # try help_ methods
215 if self.help.has_key(topic):
216 self.help[topic]()
217 return 0
219 # try command docstrings
220 try:
221 l = self.commands.get(topic)
222 except KeyError:
223 print _('Sorry, no help for "%(topic)s"')%locals()
224 return 1
226 # display the help for each match, removing the docsring indent
227 for name, help in l:
228 lines = nl_re.split(help.__doc__)
229 print lines[0]
230 indent = indent_re.match(lines[1])
231 if indent: indent = len(indent.group(1))
232 for line in lines[1:]:
233 if indent:
234 print line[indent:]
235 else:
236 print line
237 return 0
239 def help_initopts(self):
240 import roundup.templates
241 templates = roundup.templates.listTemplates()
242 print _('Templates:'), ', '.join(templates)
243 import roundup.backends
244 backends = roundup.backends.__all__
245 print _('Back ends:'), ', '.join(backends)
248 def do_initialise(self, instance_home, args):
249 '''Usage: initialise [template [backend [admin password]]]
250 Initialise a new Roundup instance.
252 The command will prompt for the instance home directory (if not supplied
253 through INSTANCE_HOME or the -i option). The template, backend and admin
254 password may be specified on the command-line as arguments, in that
255 order.
257 See also initopts help.
258 '''
259 if len(args) < 1:
260 raise UsageError, _('Not enough arguments supplied')
261 # select template
262 import roundup.templates
263 templates = roundup.templates.listTemplates()
264 template = len(args) > 1 and args[1] or ''
265 if template not in templates:
266 print _('Templates:'), ', '.join(templates)
267 while template not in templates:
268 template = raw_input(_('Select template [classic]: ')).strip()
269 if not template:
270 template = 'classic'
272 import roundup.backends
273 backends = roundup.backends.__all__
274 backend = len(args) > 2 and args[2] or ''
275 if backend not in backends:
276 print _('Back ends:'), ', '.join(backends)
277 while backend not in backends:
278 backend = raw_input(_('Select backend [anydbm]: ')).strip()
279 if not backend:
280 backend = 'anydbm'
281 if len(args) > 3:
282 adminpw = confirm = args[3]
283 else:
284 adminpw = ''
285 confirm = 'x'
286 while adminpw != confirm:
287 adminpw = getpass.getpass(_('Admin Password: '))
288 confirm = getpass.getpass(_(' Confirm: '))
289 init.init(instance_home, template, backend, adminpw)
290 return 0
293 def do_get(self, args):
294 '''Usage: get property designator[,designator]*
295 Get the given property of one or more designator(s).
297 Retrieves the property value of the nodes specified by the designators.
298 '''
299 if len(args) < 2:
300 raise UsageError, _('Not enough arguments supplied')
301 propname = args[0]
302 designators = args[1].split(',')
303 l = []
304 for designator in designators:
305 # decode the node designator
306 try:
307 classname, nodeid = roundupdb.splitDesignator(designator)
308 except roundupdb.DesignatorError, message:
309 raise UsageError, message
311 # get the class
312 cl = self.get_class(classname)
313 try:
314 if self.comma_sep:
315 l.append(cl.get(nodeid, propname))
316 else:
317 print cl.get(nodeid, propname)
318 except IndexError:
319 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
320 except KeyError:
321 raise UsageError, _('no such %(classname)s property '
322 '"%(propname)s"')%locals()
323 if self.comma_sep:
324 print ','.join(l)
325 return 0
328 def do_set(self, args):
329 '''Usage: set designator[,designator]* propname=value ...
330 Set the given property of one or more designator(s).
332 Sets the property to the value for all designators given.
333 '''
334 if len(args) < 2:
335 raise UsageError, _('Not enough arguments supplied')
336 from roundup import hyperdb
338 designators = args[0].split(',')
340 # get the props from the args
341 props = self.props_from_args(args[1:])
343 # now do the set for all the nodes
344 for designator in designators:
345 # decode the node designator
346 try:
347 classname, nodeid = roundupdb.splitDesignator(designator)
348 except roundupdb.DesignatorError, message:
349 raise UsageError, message
351 # get the class
352 cl = self.get_class(classname)
354 properties = cl.getprops()
355 for key, value in props.items():
356 proptype = properties[key]
357 if isinstance(proptype, hyperdb.String):
358 continue
359 elif isinstance(proptype, hyperdb.Password):
360 props[key] = password.Password(value)
361 elif isinstance(proptype, hyperdb.Date):
362 try:
363 props[key] = date.Date(value)
364 except ValueError, message:
365 raise UsageError, '"%s": %s'%(value, message)
366 elif isinstance(proptype, hyperdb.Interval):
367 try:
368 props[key] = date.Interval(value)
369 except ValueError, message:
370 raise UsageError, '"%s": %s'%(value, message)
371 elif isinstance(proptype, hyperdb.Link):
372 props[key] = value
373 elif isinstance(proptype, hyperdb.Multilink):
374 props[key] = value.split(',')
376 # try the set
377 try:
378 apply(cl.set, (nodeid, ), props)
379 except (TypeError, IndexError, ValueError), message:
380 raise UsageError, message
381 return 0
383 def do_find(self, args):
384 '''Usage: find classname propname=value ...
385 Find the nodes of the given class with a given link property value.
387 Find the nodes of the given class with a given link property value. The
388 value may be either the nodeid of the linked node, or its key value.
389 '''
390 if len(args) < 1:
391 raise UsageError, _('Not enough arguments supplied')
392 classname = args[0]
393 # get the class
394 cl = self.get_class(classname)
396 # handle the propname=value argument
397 props = self.props_from_args(args[1:])
399 # if the value isn't a number, look up the linked class to get the
400 # number
401 for propname, value in props.items():
402 num_re = re.compile('^\d+$')
403 if not num_re.match(value):
404 # get the property
405 try:
406 property = cl.properties[propname]
407 except KeyError:
408 raise UsageError, _('%(classname)s has no property '
409 '"%(propname)s"')%locals()
411 # make sure it's a link
412 if (not isinstance(property, hyperdb.Link) and not
413 isinstance(property, hyperdb.Multilink)):
414 raise UsageError, _('You may only "find" link properties')
416 # get the linked-to class and look up the key property
417 link_class = self.db.getclass(property.classname)
418 try:
419 props[propname] = link_class.lookup(value)
420 except TypeError:
421 raise UsageError, _('%(classname)s has no key property"')%{
422 'classname': link_class.classname}
423 except KeyError:
424 raise UsageError, _('%(classname)s has no entry "%(propname)s"')%{
425 'classname': link_class.classname, 'propname': propname}
427 # now do the find
428 try:
429 if self.comma_sep:
430 print ','.join(apply(cl.find, (), props))
431 else:
432 print apply(cl.find, (), props)
433 except KeyError:
434 raise UsageError, _('%(classname)s has no property '
435 '"%(propname)s"')%locals()
436 except (ValueError, TypeError), message:
437 raise UsageError, message
438 return 0
440 def do_specification(self, args):
441 '''Usage: specification classname
442 Show the properties for a classname.
444 This lists the properties for a given class.
445 '''
446 if len(args) < 1:
447 raise UsageError, _('Not enough arguments supplied')
448 classname = args[0]
449 # get the class
450 cl = self.get_class(classname)
452 # get the key property
453 keyprop = cl.getkey()
454 for key, value in cl.properties.items():
455 if keyprop == key:
456 print _('%(key)s: %(value)s (key property)')%locals()
457 else:
458 print _('%(key)s: %(value)s')%locals()
460 def do_display(self, args):
461 '''Usage: display designator
462 Show the property values for the given node.
464 This lists the properties and their associated values for the given
465 node.
466 '''
467 if len(args) < 1:
468 raise UsageError, _('Not enough arguments supplied')
470 # decode the node designator
471 try:
472 classname, nodeid = roundupdb.splitDesignator(args[0])
473 except roundupdb.DesignatorError, message:
474 raise UsageError, message
476 # get the class
477 cl = self.get_class(classname)
479 # display the values
480 for key in cl.properties.keys():
481 value = cl.get(nodeid, key)
482 print _('%(key)s: %(value)s')%locals()
484 def do_create(self, args):
485 '''Usage: create classname property=value ...
486 Create a new entry of a given class.
488 This creates a new entry of the given class using the property
489 name=value arguments provided on the command line after the "create"
490 command.
491 '''
492 if len(args) < 1:
493 raise UsageError, _('Not enough arguments supplied')
494 from roundup import hyperdb
496 classname = args[0]
498 # get the class
499 cl = self.get_class(classname)
501 # now do a create
502 props = {}
503 properties = cl.getprops(protected = 0)
504 if len(args) == 1:
505 # ask for the properties
506 for key, value in properties.items():
507 if key == 'id': continue
508 name = value.__class__.__name__
509 if isinstance(value , hyperdb.Password):
510 again = None
511 while value != again:
512 value = getpass.getpass(_('%(propname)s (Password): ')%{
513 'propname': key.capitalize()})
514 again = getpass.getpass(_(' %(propname)s (Again): ')%{
515 'propname': key.capitalize()})
516 if value != again: print _('Sorry, try again...')
517 if value:
518 props[key] = value
519 else:
520 value = raw_input(_('%(propname)s (%(proptype)s): ')%{
521 'propname': key.capitalize(), 'proptype': name})
522 if value:
523 props[key] = value
524 else:
525 props = self.props_from_args(args[1:])
527 # convert types
528 for propname in props.keys():
529 # get the property
530 try:
531 proptype = properties[propname]
532 except KeyError:
533 raise UsageError, _('%(classname)s has no property '
534 '"%(propname)s"')%locals()
536 if isinstance(proptype, hyperdb.Date):
537 try:
538 props[key] = date.Date(value)
539 except ValueError, message:
540 raise UsageError, _('"%(value)s": %(message)s')%locals()
541 elif isinstance(proptype, hyperdb.Interval):
542 try:
543 props[key] = date.Interval(value)
544 except ValueError, message:
545 raise UsageError, _('"%(value)s": %(message)s')%locals()
546 elif isinstance(proptype, hyperdb.Password):
547 props[key] = password.Password(value)
548 elif isinstance(proptype, hyperdb.Multilink):
549 props[key] = value.split(',')
551 # check for the key property
552 propname = cl.getkey()
553 if propname and not props.has_key(propname):
554 raise UsageError, _('you must provide the "%(propname)s" '
555 'property.')%locals()
557 # do the actual create
558 try:
559 print apply(cl.create, (), props)
560 except (TypeError, IndexError, ValueError), message:
561 raise UsageError, message
562 return 0
564 def do_list(self, args):
565 '''Usage: list classname [property]
566 List the instances of a class.
568 Lists all instances of the given class. If the property is not
569 specified, the "label" property is used. The label property is tried
570 in order: the key, "name", "title" and then the first property,
571 alphabetically.
572 '''
573 if len(args) < 1:
574 raise UsageError, _('Not enough arguments supplied')
575 classname = args[0]
577 # get the class
578 cl = self.get_class(classname)
580 # figure the property
581 if len(args) > 1:
582 propname = args[1]
583 else:
584 propname = cl.labelprop()
586 if self.comma_sep:
587 print ','.join(cl.list())
588 else:
589 for nodeid in cl.list():
590 try:
591 value = cl.get(nodeid, propname)
592 except KeyError:
593 raise UsageError, _('%(classname)s has no property '
594 '"%(propname)s"')%locals()
595 print _('%(nodeid)4s: %(value)s')%locals()
596 return 0
598 def do_table(self, args):
599 '''Usage: table classname [property[,property]*]
600 List the instances of a class in tabular form.
602 Lists all instances of the given class. If the properties are not
603 specified, all properties are displayed. By default, the column widths
604 are the width of the property names. The width may be explicitly defined
605 by defining the property as "name:width". For example::
606 roundup> table priority id,name:10
607 Id Name
608 1 fatal-bug
609 2 bug
610 3 usability
611 4 feature
612 '''
613 if len(args) < 1:
614 raise UsageError, _('Not enough arguments supplied')
615 classname = args[0]
617 # get the class
618 cl = self.get_class(classname)
620 # figure the property names to display
621 if len(args) > 1:
622 prop_names = args[1].split(',')
623 all_props = cl.getprops()
624 for spec in prop_names:
625 if ':' in spec:
626 try:
627 propname, width = spec.split(':')
628 except (ValueError, TypeError):
629 raise UsageError, _('"%(spec)s" not name:width')%locals()
630 else:
631 propname = spec
632 if not all_props.has_key(propname):
633 raise UsageError, _('%(classname)s has no property '
634 '"%(propname)s"')%locals()
635 else:
636 prop_names = cl.getprops().keys()
638 # now figure column widths
639 props = []
640 for spec in prop_names:
641 if ':' in spec:
642 name, width = spec.split(':')
643 props.append((name, int(width)))
644 else:
645 props.append((spec, len(spec)))
647 # now display the heading
648 print ' '.join([name.capitalize().ljust(width) for name,width in props])
650 # and the table data
651 for nodeid in cl.list():
652 l = []
653 for name, width in props:
654 if name != 'id':
655 try:
656 value = str(cl.get(nodeid, name))
657 except KeyError:
658 # we already checked if the property is valid - a
659 # KeyError here means the node just doesn't have a
660 # value for it
661 value = ''
662 else:
663 value = str(nodeid)
664 f = '%%-%ds'%width
665 l.append(f%value[:width])
666 print ' '.join(l)
667 return 0
669 def do_history(self, args):
670 '''Usage: history designator
671 Show the history entries of a designator.
673 Lists the journal entries for the node identified by the designator.
674 '''
675 if len(args) < 1:
676 raise UsageError, _('Not enough arguments supplied')
677 try:
678 classname, nodeid = roundupdb.splitDesignator(args[0])
679 except roundupdb.DesignatorError, message:
680 raise UsageError, message
682 try:
683 print self.db.getclass(classname).history(nodeid)
684 except KeyError:
685 raise UsageError, _('no such class "%(classname)s"')%locals()
686 except IndexError:
687 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
688 return 0
690 def do_commit(self, args):
691 '''Usage: commit
692 Commit all changes made to the database.
694 The changes made during an interactive session are not
695 automatically written to the database - they must be committed
696 using this command.
698 One-off commands on the command-line are automatically committed if
699 they are successful.
700 '''
701 self.db.commit()
702 return 0
704 def do_rollback(self, args):
705 '''Usage: rollback
706 Undo all changes that are pending commit to the database.
708 The changes made during an interactive session are not
709 automatically written to the database - they must be committed
710 manually. This command undoes all those changes, so a commit
711 immediately after would make no changes to the database.
712 '''
713 self.db.rollback()
714 return 0
716 def do_retire(self, args):
717 '''Usage: retire designator[,designator]*
718 Retire the node specified by designator.
720 This action indicates that a particular node is not to be retrieved by
721 the list or find commands, and its key value may be re-used.
722 '''
723 if len(args) < 1:
724 raise UsageError, _('Not enough arguments supplied')
725 designators = args[0].split(',')
726 for designator in designators:
727 try:
728 classname, nodeid = roundupdb.splitDesignator(designator)
729 except roundupdb.DesignatorError, message:
730 raise UsageError, message
731 try:
732 self.db.getclass(classname).retire(nodeid)
733 except KeyError:
734 raise UsageError, _('no such class "%(classname)s"')%locals()
735 except IndexError:
736 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
737 return 0
739 def do_export(self, args):
740 '''Usage: export class[,class] destination_dir
741 Export the database to tab-separated-value files.
743 This action exports the current data from the database into
744 tab-separated-value files that are placed in the nominated destination
745 directory. The journals are not exported.
746 '''
747 if len(args) < 2:
748 raise UsageError, _('Not enough arguments supplied')
749 classes = args[0].split(',')
750 dir = args[1]
752 # use the csv parser if we can - it's faster
753 if csv is not None:
754 p = csv.parser(field_sep=':')
756 # do all the classes specified
757 for classname in classes:
758 cl = self.get_class(classname)
759 f = open(os.path.join(dir, classname+'.csv'), 'w')
760 f.write(':'.join(cl.properties.keys()) + '\n')
762 # all nodes for this class
763 properties = cl.properties.items()
764 for nodeid in cl.list():
765 l = []
766 for prop, proptype in properties:
767 value = cl.get(nodeid, prop)
768 # convert data where needed
769 if isinstance(proptype, hyperdb.Date):
770 value = value.get_tuple()
771 elif isinstance(proptype, hyperdb.Interval):
772 value = value.get_tuple()
773 elif isinstance(proptype, hyperdb.Password):
774 value = str(value)
775 l.append(repr(value))
777 # now write
778 if csv is not None:
779 f.write(p.join(l) + '\n')
780 else:
781 # escape the individual entries to they're valid CSV
782 m = []
783 for entry in l:
784 if '"' in entry:
785 entry = '""'.join(entry.split('"'))
786 if ':' in entry:
787 entry = '"%s"'%entry
788 m.append(entry)
789 f.write(':'.join(m) + '\n')
790 return 0
792 def do_import(self, args):
793 '''Usage: import class file
794 Import the contents of the tab-separated-value file.
796 The file must define the same properties as the class (including having
797 a "header" line with those property names.) The new nodes are added to
798 the existing database - if you want to create a new database using the
799 imported data, then create a new database (or, tediously, retire all
800 the old data.)
801 '''
802 if len(args) < 2:
803 raise UsageError, _('Not enough arguments supplied')
804 if csv is None:
805 raise UsageError, \
806 _('Sorry, you need the csv module to use this function.\n'
807 'Get it from: http://www.object-craft.com.au/projects/csv/')
809 from roundup import hyperdb
811 # ensure that the properties and the CSV file headings match
812 classname = args[0]
813 cl = self.get_class(classname)
814 f = open(args[1])
815 p = csv.parser(field_sep=':')
816 file_props = p.parse(f.readline())
817 props = cl.properties.keys()
818 m = file_props[:]
819 m.sort()
820 props.sort()
821 if m != props:
822 raise UsageError, _('Import file doesn\'t define the same '
823 'properties as "%(arg0)s".')%{'arg0': args[0]}
825 # loop through the file and create a node for each entry
826 n = range(len(props))
827 while 1:
828 line = f.readline()
829 if not line: break
831 # parse lines until we get a complete entry
832 while 1:
833 l = p.parse(line)
834 if l: break
836 # make the new node's property map
837 d = {}
838 for i in n:
839 # Use eval to reverse the repr() used to output the CSV
840 value = eval(l[i])
841 # Figure the property for this column
842 key = file_props[i]
843 proptype = cl.properties[key]
844 # Convert for property type
845 if isinstance(proptype, hyperdb.Date):
846 value = date.Date(value)
847 elif isinstance(proptype, hyperdb.Interval):
848 value = date.Interval(value)
849 elif isinstance(proptype, hyperdb.Password):
850 pwd = password.Password()
851 pwd.unpack(value)
852 value = pwd
853 if value is not None:
854 d[key] = value
856 # and create the new node
857 apply(cl.create, (), d)
858 return 0
860 def run_command(self, args):
861 '''Run a single command
862 '''
863 command = args[0]
865 # handle help now
866 if command == 'help':
867 if len(args)>1:
868 self.do_help(args[1:])
869 return 0
870 self.do_help(['help'])
871 return 0
872 if command == 'morehelp':
873 self.do_help(['help'])
874 self.help_commands()
875 self.help_all()
876 return 0
878 # figure what the command is
879 try:
880 functions = self.commands.get(command)
881 except KeyError:
882 # not a valid command
883 print _('Unknown command "%(command)s" ("help commands" for a '
884 'list)')%locals()
885 return 1
887 # check for multiple matches
888 if len(functions) > 1:
889 print _('Multiple commands match "%(command)s": %(list)s')%{'command':
890 command, 'list': ', '.join([i[0] for i in functions])}
891 return 1
892 command, function = functions[0]
894 # make sure we have an instance_home
895 while not self.instance_home:
896 self.instance_home = raw_input(_('Enter instance home: ')).strip()
898 # before we open the db, we may be doing an init
899 if command == 'initialise':
900 return self.do_initialise(self.instance_home, args)
902 # get the instance
903 try:
904 instance = roundup.instance.open(self.instance_home)
905 except ValueError, message:
906 self.instance_home = ''
907 print _("Couldn't open instance: %(message)s")%locals()
908 return 1
910 # only open the database once!
911 if not self.db:
912 self.db = instance.open('admin')
914 # do the command
915 ret = 0
916 try:
917 ret = function(args[1:])
918 except UsageError, message:
919 print _('Error: %(message)s')%locals()
920 print function.__doc__
921 ret = 1
922 except:
923 import traceback
924 traceback.print_exc()
925 ret = 1
926 return ret
928 def interactive(self):
929 '''Run in an interactive mode
930 '''
931 print _('Roundup {version} ready for input.')
932 print _('Type "help" for help.')
933 try:
934 import readline
935 except ImportError:
936 print _('Note: command history and editing not available')
938 while 1:
939 try:
940 command = raw_input(_('roundup> '))
941 except EOFError:
942 print _('exit...')
943 break
944 if not command: continue
945 args = token.token_split(command)
946 if not args: continue
947 if args[0] in ('quit', 'exit'): break
948 self.run_command(args)
950 # exit.. check for transactions
951 if self.db and self.db.transactions:
952 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
953 if commit and commit[0].lower() == 'y':
954 self.db.commit()
955 return 0
957 def main(self):
958 try:
959 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc')
960 except getopt.GetoptError, e:
961 self.usage(str(e))
962 return 1
964 # handle command-line args
965 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '')
966 name = password = ''
967 if os.environ.has_key('ROUNDUP_LOGIN'):
968 l = os.environ['ROUNDUP_LOGIN'].split(':')
969 name = l[0]
970 if len(l) > 1:
971 password = l[1]
972 self.comma_sep = 0
973 for opt, arg in opts:
974 if opt == '-h':
975 self.usage()
976 return 0
977 if opt == '-i':
978 self.instance_home = arg
979 if opt == '-c':
980 self.comma_sep = 1
982 # if no command - go interactive
983 ret = 0
984 if not args:
985 self.interactive()
986 else:
987 ret = self.run_command(args)
988 if self.db: self.db.commit()
989 return ret
992 if __name__ == '__main__':
993 tool = AdminTool()
994 sys.exit(tool.main())
996 #
997 # $Log: not supported by cvs2svn $
998 # Revision 1.2 2002/01/07 10:41:44 richard
999 # #500140 ] AdminTool.get_class() returns nothing
1000 #
1001 # Revision 1.1 2002/01/05 02:11:22 richard
1002 # I18N'ed roundup admin - and split the code off into a module so it can be used
1003 # elsewhere.
1004 # Big issue with this is the doc strings - that's the help. We're probably going to
1005 # have to switch to not use docstrings, which will suck a little :(
1006 #
1007 #
1008 #
1009 # vim: set filetype=python ts=4 sw=4 et si