Mercurial > p > roundup > code
comparison roundup/admin.py @ 601:912029653c1c config-0-4-0-branch
[[Metadata associated with this commit was garbled during conversion from CVS
to Subversion. The actual author of these changes was probably either Richard
Jones or Titus Brown.]]
| author | No Author <no-author@users.sourceforge.net> |
|---|---|
| date | Wed, 06 Feb 2002 03:47:17 +0000 |
| parents | |
| children | c242455d9b46 |
comparison
equal
deleted
inserted
replaced
| 482:fdee2ff82b40 | 601:912029653c1c |
|---|---|
| 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.6 2002-01-23 07:27:19 grubert 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 return self.db.getclass(classname) | |
| 71 except KeyError: | |
| 72 raise UsageError, _('no such class "%(classname)s"')%locals() | |
| 73 | |
| 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 | |
| 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 if len(args)>0: | |
| 213 topic = args[0] | |
| 214 else: | |
| 215 topic = 'help' | |
| 216 | |
| 217 | |
| 218 # try help_ methods | |
| 219 if self.help.has_key(topic): | |
| 220 self.help[topic]() | |
| 221 return 0 | |
| 222 | |
| 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 | |
| 229 | |
| 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 | |
| 242 | |
| 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) | |
| 250 | |
| 251 | |
| 252 def do_initialise(self, instance_home, args): | |
| 253 '''Usage: initialise [template [backend [admin password]]] | |
| 254 Initialise a new Roundup instance. | |
| 255 | |
| 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. | |
| 260 | |
| 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' | |
| 275 | |
| 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 | |
| 295 | |
| 296 | |
| 297 def do_get(self, args): | |
| 298 '''Usage: get property designator[,designator]* | |
| 299 Get the given property of one or more designator(s). | |
| 300 | |
| 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 | |
| 314 | |
| 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 | |
| 330 | |
| 331 | |
| 332 def do_set(self, args): | |
| 333 '''Usage: set designator[,designator]* propname=value ... | |
| 334 Set the given property of one or more designator(s). | |
| 335 | |
| 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 | |
| 341 | |
| 342 designators = args[0].split(',') | |
| 343 | |
| 344 # get the props from the args | |
| 345 props = self.props_from_args(args[1:]) | |
| 346 | |
| 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 | |
| 354 | |
| 355 # get the class | |
| 356 cl = self.get_class(classname) | |
| 357 | |
| 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(',') | |
| 379 | |
| 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 | |
| 386 | |
| 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. | |
| 390 | |
| 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) | |
| 399 | |
| 400 # handle the propname=value argument | |
| 401 props = self.props_from_args(args[1:]) | |
| 402 | |
| 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() | |
| 414 | |
| 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') | |
| 419 | |
| 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} | |
| 430 | |
| 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 | |
| 443 | |
| 444 def do_specification(self, args): | |
| 445 '''Usage: specification classname | |
| 446 Show the properties for a classname. | |
| 447 | |
| 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) | |
| 455 | |
| 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() | |
| 463 | |
| 464 def do_display(self, args): | |
| 465 '''Usage: display designator | |
| 466 Show the property values for the given node. | |
| 467 | |
| 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') | |
| 473 | |
| 474 # decode the node designator | |
| 475 try: | |
| 476 classname, nodeid = roundupdb.splitDesignator(args[0]) | |
| 477 except roundupdb.DesignatorError, message: | |
| 478 raise UsageError, message | |
| 479 | |
| 480 # get the class | |
| 481 cl = self.get_class(classname) | |
| 482 | |
| 483 # display the values | |
| 484 for key in cl.properties.keys(): | |
| 485 value = cl.get(nodeid, key) | |
| 486 print _('%(key)s: %(value)s')%locals() | |
| 487 | |
| 488 def do_create(self, args): | |
| 489 '''Usage: create classname property=value ... | |
| 490 Create a new entry of a given class. | |
| 491 | |
| 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 | |
| 499 | |
| 500 classname = args[0] | |
| 501 | |
| 502 # get the class | |
| 503 cl = self.get_class(classname) | |
| 504 | |
| 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:]) | |
| 530 | |
| 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() | |
| 539 | |
| 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(',') | |
| 554 | |
| 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() | |
| 560 | |
| 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 | |
| 567 | |
| 568 def do_list(self, args): | |
| 569 '''Usage: list classname [property] | |
| 570 List the instances of a class. | |
| 571 | |
| 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] | |
| 580 | |
| 581 # get the class | |
| 582 cl = self.get_class(classname) | |
| 583 | |
| 584 # figure the property | |
| 585 if len(args) > 1: | |
| 586 propname = args[1] | |
| 587 else: | |
| 588 propname = cl.labelprop() | |
| 589 | |
| 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 | |
| 601 | |
| 602 def do_table(self, args): | |
| 603 '''Usage: table classname [property[,property]*] | |
| 604 List the instances of a class in tabular form. | |
| 605 | |
| 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] | |
| 620 | |
| 621 # get the class | |
| 622 cl = self.get_class(classname) | |
| 623 | |
| 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() | |
| 641 | |
| 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))) | |
| 650 | |
| 651 # now display the heading | |
| 652 print ' '.join([name.capitalize().ljust(width) for name,width in props]) | |
| 653 | |
| 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 | |
| 672 | |
| 673 def do_history(self, args): | |
| 674 '''Usage: history designator | |
| 675 Show the history entries of a designator. | |
| 676 | |
| 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 | |
| 685 | |
| 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 | |
| 693 | |
| 694 def do_commit(self, args): | |
| 695 '''Usage: commit | |
| 696 Commit all changes made to the database. | |
| 697 | |
| 698 The changes made during an interactive session are not | |
| 699 automatically written to the database - they must be committed | |
| 700 using this command. | |
| 701 | |
| 702 One-off commands on the command-line are automatically committed if | |
| 703 they are successful. | |
| 704 ''' | |
| 705 self.db.commit() | |
| 706 return 0 | |
| 707 | |
| 708 def do_rollback(self, args): | |
| 709 '''Usage: rollback | |
| 710 Undo all changes that are pending commit to the database. | |
| 711 | |
| 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 | |
| 719 | |
| 720 def do_retire(self, args): | |
| 721 '''Usage: retire designator[,designator]* | |
| 722 Retire the node specified by designator. | |
| 723 | |
| 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 | |
| 742 | |
| 743 def do_export(self, args): | |
| 744 '''Usage: export class[,class] destination_dir | |
| 745 Export the database to tab-separated-value files. | |
| 746 | |
| 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] | |
| 755 | |
| 756 # use the csv parser if we can - it's faster | |
| 757 if csv is not None: | |
| 758 p = csv.parser(field_sep=':') | |
| 759 | |
| 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') | |
| 765 | |
| 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)) | |
| 780 | |
| 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 | |
| 795 | |
| 796 def do_import(self, args): | |
| 797 '''Usage: import class file | |
| 798 Import the contents of the tab-separated-value file. | |
| 799 | |
| 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/') | |
| 812 | |
| 813 from roundup import hyperdb | |
| 814 | |
| 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]} | |
| 828 | |
| 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 | |
| 834 | |
| 835 # parse lines until we get a complete entry | |
| 836 while 1: | |
| 837 l = p.parse(line) | |
| 838 if l: break | |
| 839 | |
| 840 # make the new node's property map | |
| 841 d = {} | |
| 842 for i in n: | |
| 843 # Use eval to reverse the repr() used to output the CSV | |
| 844 value = eval(l[i]) | |
| 845 # Figure the property for this column | |
| 846 key = file_props[i] | |
| 847 proptype = cl.properties[key] | |
| 848 # Convert for property type | |
| 849 if isinstance(proptype, hyperdb.Date): | |
| 850 value = date.Date(value) | |
| 851 elif isinstance(proptype, hyperdb.Interval): | |
| 852 value = date.Interval(value) | |
| 853 elif isinstance(proptype, hyperdb.Password): | |
| 854 pwd = password.Password() | |
| 855 pwd.unpack(value) | |
| 856 value = pwd | |
| 857 if value is not None: | |
| 858 d[key] = value | |
| 859 | |
| 860 # and create the new node | |
| 861 apply(cl.create, (), d) | |
| 862 return 0 | |
| 863 | |
| 864 def do_pack(self, args): | |
| 865 '''Usage: pack period | date | |
| 866 | |
| 867 Remove journal entries older than a period of time specified or | |
| 868 before a certain date. | |
| 869 | |
| 870 A period is specified using the suffixes "y", "m", and "d". The | |
| 871 suffix "w" (for "week") means 7 days. | |
| 872 | |
| 873 "3y" means three years | |
| 874 "2y 1m" means two years and one month | |
| 875 "1m 25d" means one month and 25 days | |
| 876 "2w 3d" means two weeks and three days | |
| 877 | |
| 878 Date format is "YYYY-MM-DD" eg: | |
| 879 2001-01-01 | |
| 880 | |
| 881 ''' | |
| 882 if len(args) <> 1: | |
| 883 raise UsageError, _('Not enough arguments supplied') | |
| 884 | |
| 885 # are we dealing with a period or a date | |
| 886 value = args[0] | |
| 887 date_re = re.compile(r''' | |
| 888 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd | |
| 889 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)? | |
| 890 ''', re.VERBOSE) | |
| 891 m = date_re.match(value) | |
| 892 if not m: | |
| 893 raise ValueError, _('Invalid format') | |
| 894 m = m.groupdict() | |
| 895 if m['period']: | |
| 896 # TODO: need to fix date module. one should be able to say | |
| 897 # pack_before = date.Date(". - %s"%value) | |
| 898 pack_before = date.Date(".") + date.Interval("- %s"%value) | |
| 899 elif m['date']: | |
| 900 pack_before = date.Date(value) | |
| 901 self.db.pack(pack_before) | |
| 902 return 0 | |
| 903 | |
| 904 def run_command(self, args): | |
| 905 '''Run a single command | |
| 906 ''' | |
| 907 command = args[0] | |
| 908 | |
| 909 # handle help now | |
| 910 if command == 'help': | |
| 911 if len(args)>1: | |
| 912 self.do_help(args[1:]) | |
| 913 return 0 | |
| 914 self.do_help(['help']) | |
| 915 return 0 | |
| 916 if command == 'morehelp': | |
| 917 self.do_help(['help']) | |
| 918 self.help_commands() | |
| 919 self.help_all() | |
| 920 return 0 | |
| 921 | |
| 922 # figure what the command is | |
| 923 try: | |
| 924 functions = self.commands.get(command) | |
| 925 except KeyError: | |
| 926 # not a valid command | |
| 927 print _('Unknown command "%(command)s" ("help commands" for a ' | |
| 928 'list)')%locals() | |
| 929 return 1 | |
| 930 | |
| 931 # check for multiple matches | |
| 932 if len(functions) > 1: | |
| 933 print _('Multiple commands match "%(command)s": %(list)s')%{'command': | |
| 934 command, 'list': ', '.join([i[0] for i in functions])} | |
| 935 return 1 | |
| 936 command, function = functions[0] | |
| 937 | |
| 938 # make sure we have an instance_home | |
| 939 while not self.instance_home: | |
| 940 self.instance_home = raw_input(_('Enter instance home: ')).strip() | |
| 941 | |
| 942 # before we open the db, we may be doing an init | |
| 943 if command == 'initialise': | |
| 944 return self.do_initialise(self.instance_home, args) | |
| 945 | |
| 946 # get the instance | |
| 947 try: | |
| 948 instance = roundup.instance.open(self.instance_home) | |
| 949 except ValueError, message: | |
| 950 self.instance_home = '' | |
| 951 print _("Couldn't open instance: %(message)s")%locals() | |
| 952 return 1 | |
| 953 | |
| 954 # only open the database once! | |
| 955 if not self.db: | |
| 956 self.db = instance.open('admin') | |
| 957 | |
| 958 # do the command | |
| 959 ret = 0 | |
| 960 try: | |
| 961 ret = function(args[1:]) | |
| 962 except UsageError, message: | |
| 963 print _('Error: %(message)s')%locals() | |
| 964 print function.__doc__ | |
| 965 ret = 1 | |
| 966 except: | |
| 967 import traceback | |
| 968 traceback.print_exc() | |
| 969 ret = 1 | |
| 970 return ret | |
| 971 | |
| 972 def interactive(self): | |
| 973 '''Run in an interactive mode | |
| 974 ''' | |
| 975 print _('Roundup {version} ready for input.') | |
| 976 print _('Type "help" for help.') | |
| 977 try: | |
| 978 import readline | |
| 979 except ImportError: | |
| 980 print _('Note: command history and editing not available') | |
| 981 | |
| 982 while 1: | |
| 983 try: | |
| 984 command = raw_input(_('roundup> ')) | |
| 985 except EOFError: | |
| 986 print _('exit...') | |
| 987 break | |
| 988 if not command: continue | |
| 989 args = token.token_split(command) | |
| 990 if not args: continue | |
| 991 if args[0] in ('quit', 'exit'): break | |
| 992 self.run_command(args) | |
| 993 | |
| 994 # exit.. check for transactions | |
| 995 if self.db and self.db.transactions: | |
| 996 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? ')) | |
| 997 if commit and commit[0].lower() == 'y': | |
| 998 self.db.commit() | |
| 999 return 0 | |
| 1000 | |
| 1001 def main(self): | |
| 1002 try: | |
| 1003 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc') | |
| 1004 except getopt.GetoptError, e: | |
| 1005 self.usage(str(e)) | |
| 1006 return 1 | |
| 1007 | |
| 1008 # handle command-line args | |
| 1009 self.instance_home = os.environ.get('ROUNDUP_INSTANCE', '') | |
| 1010 name = password = '' | |
| 1011 if os.environ.has_key('ROUNDUP_LOGIN'): | |
| 1012 l = os.environ['ROUNDUP_LOGIN'].split(':') | |
| 1013 name = l[0] | |
| 1014 if len(l) > 1: | |
| 1015 password = l[1] | |
| 1016 self.comma_sep = 0 | |
| 1017 for opt, arg in opts: | |
| 1018 if opt == '-h': | |
| 1019 self.usage() | |
| 1020 return 0 | |
| 1021 if opt == '-i': | |
| 1022 self.instance_home = arg | |
| 1023 if opt == '-c': | |
| 1024 self.comma_sep = 1 | |
| 1025 | |
| 1026 # if no command - go interactive | |
| 1027 ret = 0 | |
| 1028 if not args: | |
| 1029 self.interactive() | |
| 1030 else: | |
| 1031 ret = self.run_command(args) | |
| 1032 if self.db: self.db.commit() | |
| 1033 return ret | |
| 1034 | |
| 1035 | |
| 1036 if __name__ == '__main__': | |
| 1037 tool = AdminTool() | |
| 1038 sys.exit(tool.main()) | |
| 1039 | |
| 1040 # | |
| 1041 # $Log: not supported by cvs2svn $ | |
| 1042 # Revision 1.5 2002/01/21 16:33:19 rochecompaan | |
| 1043 # You can now use the roundup-admin tool to pack the database | |
| 1044 # | |
| 1045 # Revision 1.4 2002/01/14 06:51:09 richard | |
| 1046 # . #503164 ] create and passwords | |
| 1047 # | |
| 1048 # Revision 1.3 2002/01/08 05:26:32 rochecompaan | |
| 1049 # Missing "self" in props_from_args | |
| 1050 # | |
| 1051 # Revision 1.2 2002/01/07 10:41:44 richard | |
| 1052 # #500140 ] AdminTool.get_class() returns nothing | |
| 1053 # | |
| 1054 # Revision 1.1 2002/01/05 02:11:22 richard | |
| 1055 # I18N'ed roundup admin - and split the code off into a module so it can be used | |
| 1056 # elsewhere. | |
| 1057 # Big issue with this is the doc strings - that's the help. We're probably going to | |
| 1058 # have to switch to not use docstrings, which will suck a little :( | |
| 1059 # | |
| 1060 # | |
| 1061 # | |
| 1062 # vim: set filetype=python ts=4 sw=4 et si |
