Mercurial > p > roundup > code
comparison roundup/admin.py @ 484:b35f229dd049
I18N'ed roundup admin...
...and split the code off into a module so it can be used elsewhere.
Big issue with this is the doc strings - that's the help. We're
probably going to have to switch to not use docstrings, which will
suck a little :(
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Sat, 05 Jan 2002 02:11:22 +0000 |
| parents | |
| children | 5db2dad23f09 |
comparison
equal
deleted
inserted
replaced
| 483:a090b3873d82 | 484:b35f229dd049 |
|---|---|
| 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.1 2002-01-05 02:11:22 richard Exp $ | |
| 20 | |
| 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 _ | |
| 29 | |
| 30 class CommandDict(UserDict.UserDict): | |
| 31 '''Simple dictionary that lets us do lookups using partial keys. | |
| 32 | |
| 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 | |
| 48 | |
| 49 class UsageError(ValueError): | |
| 50 pass | |
| 51 | |
| 52 class AdminTool: | |
| 53 | |
| 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 | |
| 65 | |
| 66 def get_class(self, classname): | |
| 67 '''Get the class - raise an exception if it doesn't exist. | |
| 68 ''' | |
| 69 try: | |
| 70 cl = self.db.getclass(classname) | |
| 71 except KeyError: | |
| 72 raise UsageError, _('no such class "%(classname)s"')%locals() | |
| 73 | |
| 74 def props_from_args(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 | |
| 85 | |
| 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> | |
| 90 | |
| 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() | |
| 101 | |
| 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 | |
| 113 | |
| 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') | |
| 135 | |
| 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". | |
| 144 | |
| 145 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ... | |
| 146 | |
| 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. | |
| 158 | |
| 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) | |
| 170 | |
| 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. | |
| 174 | |
| 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. | |
| 178 | |
| 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. | |
| 185 | |
| 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" | |
| 195 | |
| 196 Command help: | |
| 197 ''') | |
| 198 for name, command in self.commands.items(): | |
| 199 print _('%s:')%name | |
| 200 print _(' '), command.__doc__ | |
| 201 | |
| 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. | |
| 206 | |
| 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] | |
| 213 | |
| 214 # try help_ methods | |
| 215 if self.help.has_key(topic): | |
| 216 self.help[topic]() | |
| 217 return 0 | |
| 218 | |
| 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 | |
| 225 | |
| 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 | |
| 238 | |
| 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) | |
| 246 | |
| 247 | |
| 248 def do_initialise(self, instance_home, args): | |
| 249 '''Usage: initialise [template [backend [admin password]]] | |
| 250 Initialise a new Roundup instance. | |
| 251 | |
| 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. | |
| 256 | |
| 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' | |
| 271 | |
| 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 | |
| 291 | |
| 292 | |
| 293 def do_get(self, args): | |
| 294 '''Usage: get property designator[,designator]* | |
| 295 Get the given property of one or more designator(s). | |
| 296 | |
| 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 | |
| 310 | |
| 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 | |
| 326 | |
| 327 | |
| 328 def do_set(self, args): | |
| 329 '''Usage: set designator[,designator]* propname=value ... | |
| 330 Set the given property of one or more designator(s). | |
| 331 | |
| 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 | |
| 337 | |
| 338 designators = args[0].split(',') | |
| 339 | |
| 340 # get the props from the args | |
| 341 props = self.props_from_args(args[1:]) | |
| 342 | |
| 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 | |
| 350 | |
| 351 # get the class | |
| 352 cl = self.get_class(classname) | |
| 353 | |
| 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(',') | |
| 375 | |
| 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 | |
| 382 | |
| 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. | |
| 386 | |
| 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) | |
| 395 | |
| 396 # handle the propname=value argument | |
| 397 props = self.props_from_args(args[1:]) | |
| 398 | |
| 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() | |
| 410 | |
| 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') | |
| 415 | |
| 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} | |
| 426 | |
| 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 | |
| 439 | |
| 440 def do_specification(self, args): | |
| 441 '''Usage: specification classname | |
| 442 Show the properties for a classname. | |
| 443 | |
| 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) | |
| 451 | |
| 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() | |
| 459 | |
| 460 def do_display(self, args): | |
| 461 '''Usage: display designator | |
| 462 Show the property values for the given node. | |
| 463 | |
| 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') | |
| 469 | |
| 470 # decode the node designator | |
| 471 try: | |
| 472 classname, nodeid = roundupdb.splitDesignator(args[0]) | |
| 473 except roundupdb.DesignatorError, message: | |
| 474 raise UsageError, message | |
| 475 | |
| 476 # get the class | |
| 477 cl = self.get_class(classname) | |
| 478 | |
| 479 # display the values | |
| 480 for key in cl.properties.keys(): | |
| 481 value = cl.get(nodeid, key) | |
| 482 print _('%(key)s: %(value)s')%locals() | |
| 483 | |
| 484 def do_create(self, args): | |
| 485 '''Usage: create classname property=value ... | |
| 486 Create a new entry of a given class. | |
| 487 | |
| 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 | |
| 495 | |
| 496 classname = args[0] | |
| 497 | |
| 498 # get the class | |
| 499 cl = self.get_class(classname) | |
| 500 | |
| 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:]) | |
| 526 | |
| 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() | |
| 535 | |
| 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(',') | |
| 550 | |
| 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() | |
| 556 | |
| 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 | |
| 563 | |
| 564 def do_list(self, args): | |
| 565 '''Usage: list classname [property] | |
| 566 List the instances of a class. | |
| 567 | |
| 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] | |
| 576 | |
| 577 # get the class | |
| 578 cl = self.get_class(classname) | |
| 579 | |
| 580 # figure the property | |
| 581 if len(args) > 1: | |
| 582 propname = args[1] | |
| 583 else: | |
| 584 propname = cl.labelprop() | |
| 585 | |
| 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 | |
| 597 | |
| 598 def do_table(self, args): | |
| 599 '''Usage: table classname [property[,property]*] | |
| 600 List the instances of a class in tabular form. | |
| 601 | |
| 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] | |
| 616 | |
| 617 # get the class | |
| 618 cl = self.get_class(classname) | |
| 619 | |
| 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() | |
| 637 | |
| 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))) | |
| 646 | |
| 647 # now display the heading | |
| 648 print ' '.join([name.capitalize().ljust(width) for name,width in props]) | |
| 649 | |
| 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 | |
| 668 | |
| 669 def do_history(self, args): | |
| 670 '''Usage: history designator | |
| 671 Show the history entries of a designator. | |
| 672 | |
| 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 | |
| 681 | |
| 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 | |
| 689 | |
| 690 def do_commit(self, args): | |
| 691 '''Usage: commit | |
| 692 Commit all changes made to the database. | |
| 693 | |
| 694 The changes made during an interactive session are not | |
| 695 automatically written to the database - they must be committed | |
| 696 using this command. | |
| 697 | |
| 698 One-off commands on the command-line are automatically committed if | |
| 699 they are successful. | |
| 700 ''' | |
| 701 self.db.commit() | |
| 702 return 0 | |
| 703 | |
| 704 def do_rollback(self, args): | |
| 705 '''Usage: rollback | |
| 706 Undo all changes that are pending commit to the database. | |
| 707 | |
| 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 | |
| 715 | |
| 716 def do_retire(self, args): | |
| 717 '''Usage: retire designator[,designator]* | |
| 718 Retire the node specified by designator. | |
| 719 | |
| 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 | |
| 738 | |
| 739 def do_export(self, args): | |
| 740 '''Usage: export class[,class] destination_dir | |
| 741 Export the database to tab-separated-value files. | |
| 742 | |
| 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] | |
| 751 | |
| 752 # use the csv parser if we can - it's faster | |
| 753 if csv is not None: | |
| 754 p = csv.parser(field_sep=':') | |
| 755 | |
| 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') | |
| 761 | |
| 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)) | |
| 776 | |
| 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 | |
| 791 | |
| 792 def do_import(self, args): | |
| 793 '''Usage: import class file | |
| 794 Import the contents of the tab-separated-value file. | |
| 795 | |
| 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/') | |
| 808 | |
| 809 from roundup import hyperdb | |
| 810 | |
| 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]} | |
| 824 | |
| 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 | |
| 830 | |
| 831 # parse lines until we get a complete entry | |
| 832 while 1: | |
| 833 l = p.parse(line) | |
| 834 if l: break | |
| 835 | |
| 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 | |
| 855 | |
| 856 # and create the new node | |
| 857 apply(cl.create, (), d) | |
| 858 return 0 | |
| 859 | |
| 860 def run_command(self, args): | |
| 861 '''Run a single command | |
| 862 ''' | |
| 863 command = args[0] | |
| 864 | |
| 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 | |
| 877 | |
| 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 | |
| 886 | |
| 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] | |
| 893 | |
| 894 # make sure we have an instance_home | |
| 895 while not self.instance_home: | |
| 896 self.instance_home = raw_input(_('Enter instance home: ')).strip() | |
| 897 | |
| 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) | |
| 901 | |
| 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 | |
| 909 | |
| 910 # only open the database once! | |
| 911 if not self.db: | |
| 912 self.db = instance.open('admin') | |
| 913 | |
| 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 | |
| 927 | |
| 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') | |
| 937 | |
| 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) | |
| 949 | |
| 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 | |
| 956 | |
| 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 | |
| 963 | |
| 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 | |
| 981 | |
| 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 | |
| 990 | |
| 991 | |
| 992 if __name__ == '__main__': | |
| 993 tool = AdminTool() | |
| 994 sys.exit(tool.main()) | |
| 995 | |
| 996 # | |
| 997 # $Log: not supported by cvs2svn $ | |
| 998 # | |
| 999 # | |
| 1000 # vim: set filetype=python ts=4 sw=4 et si |
