Mercurial > p > roundup > code
comparison roundup/admin.py @ 1356:83f33642d220 maint-0.5
[[Metadata associated with this commit was garbled during conversion from CVS
to Subversion.]]
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Thu, 09 Jan 2003 22:59:22 +0000 |
| parents | |
| children | 3a853f1c20b5 |
comparison
equal
deleted
inserted
replaced
| 1242:3d0158c8c32b | 1356:83f33642d220 |
|---|---|
| 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.35 2002-10-03 06:56:28 richard Exp $ | |
| 20 | |
| 21 '''Administration commands for maintaining Roundup trackers. | |
| 22 ''' | |
| 23 | |
| 24 import sys, os, getpass, getopt, re, UserDict, shlex, shutil | |
| 25 try: | |
| 26 import csv | |
| 27 except ImportError: | |
| 28 csv = None | |
| 29 from roundup import date, hyperdb, roundupdb, init, password, token | |
| 30 from roundup import __version__ as roundup_version | |
| 31 import roundup.instance | |
| 32 from roundup.i18n import _ | |
| 33 | |
| 34 class CommandDict(UserDict.UserDict): | |
| 35 '''Simple dictionary that lets us do lookups using partial keys. | |
| 36 | |
| 37 Original code submitted by Engelbert Gruber. | |
| 38 ''' | |
| 39 _marker = [] | |
| 40 def get(self, key, default=_marker): | |
| 41 if self.data.has_key(key): | |
| 42 return [(key, self.data[key])] | |
| 43 keylist = self.data.keys() | |
| 44 keylist.sort() | |
| 45 l = [] | |
| 46 for ki in keylist: | |
| 47 if ki.startswith(key): | |
| 48 l.append((ki, self.data[ki])) | |
| 49 if not l and default is self._marker: | |
| 50 raise KeyError, key | |
| 51 return l | |
| 52 | |
| 53 class UsageError(ValueError): | |
| 54 pass | |
| 55 | |
| 56 class AdminTool: | |
| 57 ''' A collection of methods used in maintaining Roundup trackers. | |
| 58 | |
| 59 Typically these methods are accessed through the roundup-admin | |
| 60 script. The main() method provided on this class gives the main | |
| 61 loop for the roundup-admin script. | |
| 62 | |
| 63 Actions are defined by do_*() methods, with help for the action | |
| 64 given in the method docstring. | |
| 65 | |
| 66 Additional help may be supplied by help_*() methods. | |
| 67 ''' | |
| 68 def __init__(self): | |
| 69 self.commands = CommandDict() | |
| 70 for k in AdminTool.__dict__.keys(): | |
| 71 if k[:3] == 'do_': | |
| 72 self.commands[k[3:]] = getattr(self, k) | |
| 73 self.help = {} | |
| 74 for k in AdminTool.__dict__.keys(): | |
| 75 if k[:5] == 'help_': | |
| 76 self.help[k[5:]] = getattr(self, k) | |
| 77 self.tracker_home = '' | |
| 78 self.db = None | |
| 79 | |
| 80 def get_class(self, classname): | |
| 81 '''Get the class - raise an exception if it doesn't exist. | |
| 82 ''' | |
| 83 try: | |
| 84 return self.db.getclass(classname) | |
| 85 except KeyError: | |
| 86 raise UsageError, _('no such class "%(classname)s"')%locals() | |
| 87 | |
| 88 def props_from_args(self, args): | |
| 89 ''' Produce a dictionary of prop: value from the args list. | |
| 90 | |
| 91 The args list is specified as ``prop=value prop=value ...``. | |
| 92 ''' | |
| 93 props = {} | |
| 94 for arg in args: | |
| 95 if arg.find('=') == -1: | |
| 96 raise UsageError, _('argument "%(arg)s" not propname=value' | |
| 97 )%locals() | |
| 98 try: | |
| 99 key, value = arg.split('=') | |
| 100 except ValueError: | |
| 101 raise UsageError, _('argument "%(arg)s" not propname=value' | |
| 102 )%locals() | |
| 103 if value: | |
| 104 props[key] = value | |
| 105 else: | |
| 106 props[key] = None | |
| 107 return props | |
| 108 | |
| 109 def usage(self, message=''): | |
| 110 ''' Display a simple usage message. | |
| 111 ''' | |
| 112 if message: | |
| 113 message = _('Problem: %(message)s)\n\n')%locals() | |
| 114 print _('''%(message)sUsage: roundup-admin [options] <command> <arguments> | |
| 115 | |
| 116 Options: | |
| 117 -i instance home -- specify the issue tracker "home directory" to administer | |
| 118 -u -- the user[:password] to use for commands | |
| 119 -c -- when outputting lists of data, just comma-separate them | |
| 120 | |
| 121 Help: | |
| 122 roundup-admin -h | |
| 123 roundup-admin help -- this help | |
| 124 roundup-admin help <command> -- command-specific help | |
| 125 roundup-admin help all -- all available help | |
| 126 ''')%locals() | |
| 127 self.help_commands() | |
| 128 | |
| 129 def help_commands(self): | |
| 130 ''' List the commands available with their precis help. | |
| 131 ''' | |
| 132 print _('Commands:'), | |
| 133 commands = [''] | |
| 134 for command in self.commands.values(): | |
| 135 h = command.__doc__.split('\n')[0] | |
| 136 commands.append(' '+h[7:]) | |
| 137 commands.sort() | |
| 138 commands.append(_('Commands may be abbreviated as long as the abbreviation matches only one')) | |
| 139 commands.append(_('command, e.g. l == li == lis == list.')) | |
| 140 print '\n'.join(commands) | |
| 141 print | |
| 142 | |
| 143 def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')): | |
| 144 ''' Produce an HTML command list. | |
| 145 ''' | |
| 146 commands = self.commands.values() | |
| 147 def sortfun(a, b): | |
| 148 return cmp(a.__name__, b.__name__) | |
| 149 commands.sort(sortfun) | |
| 150 for command in commands: | |
| 151 h = command.__doc__.split('\n') | |
| 152 name = command.__name__[3:] | |
| 153 usage = h[0] | |
| 154 print _(''' | |
| 155 <tr><td valign=top><strong>%(name)s</strong></td> | |
| 156 <td><tt>%(usage)s</tt><p> | |
| 157 <pre>''')%locals() | |
| 158 indent = indent_re.match(h[3]) | |
| 159 if indent: indent = len(indent.group(1)) | |
| 160 for line in h[3:]: | |
| 161 if indent: | |
| 162 print line[indent:] | |
| 163 else: | |
| 164 print line | |
| 165 print _('</pre></td></tr>\n') | |
| 166 | |
| 167 def help_all(self): | |
| 168 print _(''' | |
| 169 All commands (except help) require a tracker specifier. This is just the path | |
| 170 to the roundup tracker you're working with. A roundup tracker is where | |
| 171 roundup keeps the database and configuration file that defines an issue | |
| 172 tracker. It may be thought of as the issue tracker's "home directory". It may | |
| 173 be specified in the environment variable TRACKER_HOME or on the command | |
| 174 line as "-i tracker". | |
| 175 | |
| 176 A designator is a classname and a nodeid concatenated, eg. bug1, user10, ... | |
| 177 | |
| 178 Property values are represented as strings in command arguments and in the | |
| 179 printed results: | |
| 180 . Strings are, well, strings. | |
| 181 . Date values are printed in the full date format in the local time zone, and | |
| 182 accepted in the full format or any of the partial formats explained below. | |
| 183 . Link values are printed as node designators. When given as an argument, | |
| 184 node designators and key strings are both accepted. | |
| 185 . Multilink values are printed as lists of node designators joined by commas. | |
| 186 When given as an argument, node designators and key strings are both | |
| 187 accepted; an empty string, a single node, or a list of nodes joined by | |
| 188 commas is accepted. | |
| 189 | |
| 190 When property values must contain spaces, just surround the value with | |
| 191 quotes, either ' or ". A single space may also be backslash-quoted. If a | |
| 192 valuu must contain a quote character, it must be backslash-quoted or inside | |
| 193 quotes. Examples: | |
| 194 hello world (2 tokens: hello, world) | |
| 195 "hello world" (1 token: hello world) | |
| 196 "Roch'e" Compaan (2 tokens: Roch'e Compaan) | |
| 197 Roch\'e Compaan (2 tokens: Roch'e Compaan) | |
| 198 address="1 2 3" (1 token: address=1 2 3) | |
| 199 \\ (1 token: \) | |
| 200 \n\r\t (1 token: a newline, carriage-return and tab) | |
| 201 | |
| 202 When multiple nodes are specified to the roundup get or roundup set | |
| 203 commands, the specified properties are retrieved or set on all the listed | |
| 204 nodes. | |
| 205 | |
| 206 When multiple results are returned by the roundup get or roundup find | |
| 207 commands, they are printed one per line (default) or joined by commas (with | |
| 208 the -c) option. | |
| 209 | |
| 210 Where the command changes data, a login name/password is required. The | |
| 211 login may be specified as either "name" or "name:password". | |
| 212 . ROUNDUP_LOGIN environment variable | |
| 213 . the -u command-line option | |
| 214 If either the name or password is not supplied, they are obtained from the | |
| 215 command-line. | |
| 216 | |
| 217 Date format examples: | |
| 218 "2000-04-17.03:45" means <Date 2000-04-17.08:45:00> | |
| 219 "2000-04-17" means <Date 2000-04-17.00:00:00> | |
| 220 "01-25" means <Date yyyy-01-25.00:00:00> | |
| 221 "08-13.22:13" means <Date yyyy-08-14.03:13:00> | |
| 222 "11-07.09:32:43" means <Date yyyy-11-07.14:32:43> | |
| 223 "14:25" means <Date yyyy-mm-dd.19:25:00> | |
| 224 "8:47:11" means <Date yyyy-mm-dd.13:47:11> | |
| 225 "." means "right now" | |
| 226 | |
| 227 Command help: | |
| 228 ''') | |
| 229 for name, command in self.commands.items(): | |
| 230 print _('%s:')%name | |
| 231 print _(' '), command.__doc__ | |
| 232 | |
| 233 def do_help(self, args, nl_re=re.compile('[\r\n]'), | |
| 234 indent_re=re.compile(r'^(\s+)\S+')): | |
| 235 '''Usage: help topic | |
| 236 Give help about topic. | |
| 237 | |
| 238 commands -- list commands | |
| 239 <command> -- help specific to a command | |
| 240 initopts -- init command options | |
| 241 all -- all available help | |
| 242 ''' | |
| 243 if len(args)>0: | |
| 244 topic = args[0] | |
| 245 else: | |
| 246 topic = 'help' | |
| 247 | |
| 248 | |
| 249 # try help_ methods | |
| 250 if self.help.has_key(topic): | |
| 251 self.help[topic]() | |
| 252 return 0 | |
| 253 | |
| 254 # try command docstrings | |
| 255 try: | |
| 256 l = self.commands.get(topic) | |
| 257 except KeyError: | |
| 258 print _('Sorry, no help for "%(topic)s"')%locals() | |
| 259 return 1 | |
| 260 | |
| 261 # display the help for each match, removing the docsring indent | |
| 262 for name, help in l: | |
| 263 lines = nl_re.split(help.__doc__) | |
| 264 print lines[0] | |
| 265 indent = indent_re.match(lines[1]) | |
| 266 if indent: indent = len(indent.group(1)) | |
| 267 for line in lines[1:]: | |
| 268 if indent: | |
| 269 print line[indent:] | |
| 270 else: | |
| 271 print line | |
| 272 return 0 | |
| 273 | |
| 274 def help_initopts(self): | |
| 275 import roundup.templates | |
| 276 templates = roundup.templates.listTemplates() | |
| 277 print _('Templates:'), ', '.join(templates) | |
| 278 import roundup.backends | |
| 279 backends = roundup.backends.__all__ | |
| 280 print _('Back ends:'), ', '.join(backends) | |
| 281 | |
| 282 def do_install(self, tracker_home, args): | |
| 283 '''Usage: install [template [backend [admin password]]] | |
| 284 Install a new Roundup tracker. | |
| 285 | |
| 286 The command will prompt for the tracker home directory (if not supplied | |
| 287 through TRACKER_HOME or the -i option). The template, backend and admin | |
| 288 password may be specified on the command-line as arguments, in that | |
| 289 order. | |
| 290 | |
| 291 The initialise command must be called after this command in order | |
| 292 to initialise the tracker's database. You may edit the tracker's | |
| 293 initial database contents before running that command by editing | |
| 294 the tracker's dbinit.py module init() function. | |
| 295 | |
| 296 See also initopts help. | |
| 297 ''' | |
| 298 if len(args) < 1: | |
| 299 raise UsageError, _('Not enough arguments supplied') | |
| 300 | |
| 301 # make sure the tracker home can be created | |
| 302 parent = os.path.split(tracker_home)[0] | |
| 303 if not os.path.exists(parent): | |
| 304 raise UsageError, _('Instance home parent directory "%(parent)s"' | |
| 305 ' does not exist')%locals() | |
| 306 | |
| 307 # select template | |
| 308 import roundup.templates | |
| 309 templates = roundup.templates.listTemplates() | |
| 310 template = len(args) > 1 and args[1] or '' | |
| 311 if template not in templates: | |
| 312 print _('Templates:'), ', '.join(templates) | |
| 313 while template not in templates: | |
| 314 template = raw_input(_('Select template [classic]: ')).strip() | |
| 315 if not template: | |
| 316 template = 'classic' | |
| 317 | |
| 318 # select hyperdb backend | |
| 319 import roundup.backends | |
| 320 backends = roundup.backends.__all__ | |
| 321 backend = len(args) > 2 and args[2] or '' | |
| 322 if backend not in backends: | |
| 323 print _('Back ends:'), ', '.join(backends) | |
| 324 while backend not in backends: | |
| 325 backend = raw_input(_('Select backend [anydbm]: ')).strip() | |
| 326 if not backend: | |
| 327 backend = 'anydbm' | |
| 328 # XXX perform a unit test based on the user's selections | |
| 329 | |
| 330 # install! | |
| 331 init.install(tracker_home, template, backend) | |
| 332 | |
| 333 print _(''' | |
| 334 You should now edit the tracker configuration file: | |
| 335 %(config_file)s | |
| 336 ... at a minimum, you must set MAILHOST, TRACKER_WEB, MAIL_DOMAIN and | |
| 337 ADMIN_EMAIL. | |
| 338 | |
| 339 If you wish to modify the default schema, you should also edit the database | |
| 340 initialisation file: | |
| 341 %(database_config_file)s | |
| 342 ... see the documentation on customizing for more information. | |
| 343 ''')%{ | |
| 344 'config_file': os.path.join(tracker_home, 'config.py'), | |
| 345 'database_config_file': os.path.join(tracker_home, 'dbinit.py') | |
| 346 } | |
| 347 return 0 | |
| 348 | |
| 349 | |
| 350 def do_initialise(self, tracker_home, args): | |
| 351 '''Usage: initialise [adminpw] | |
| 352 Initialise a new Roundup tracker. | |
| 353 | |
| 354 The administrator details will be set at this step. | |
| 355 | |
| 356 Execute the tracker's initialisation function dbinit.init() | |
| 357 ''' | |
| 358 # password | |
| 359 if len(args) > 1: | |
| 360 adminpw = args[1] | |
| 361 else: | |
| 362 adminpw = '' | |
| 363 confirm = 'x' | |
| 364 while adminpw != confirm: | |
| 365 adminpw = getpass.getpass(_('Admin Password: ')) | |
| 366 confirm = getpass.getpass(_(' Confirm: ')) | |
| 367 | |
| 368 # make sure the tracker home is installed | |
| 369 if not os.path.exists(tracker_home): | |
| 370 raise UsageError, _('Instance home does not exist')%locals() | |
| 371 if not os.path.exists(os.path.join(tracker_home, 'html')): | |
| 372 raise UsageError, _('Instance has not been installed')%locals() | |
| 373 | |
| 374 # is there already a database? | |
| 375 if os.path.exists(os.path.join(tracker_home, 'db')): | |
| 376 print _('WARNING: The database is already initialised!') | |
| 377 print _('If you re-initialise it, you will lose all the data!') | |
| 378 ok = raw_input(_('Erase it? Y/[N]: ')).strip() | |
| 379 if ok.lower() != 'y': | |
| 380 return 0 | |
| 381 | |
| 382 # nuke it | |
| 383 shutil.rmtree(os.path.join(tracker_home, 'db')) | |
| 384 | |
| 385 # GO | |
| 386 init.initialise(tracker_home, adminpw) | |
| 387 | |
| 388 return 0 | |
| 389 | |
| 390 | |
| 391 def do_get(self, args): | |
| 392 '''Usage: get property designator[,designator]* | |
| 393 Get the given property of one or more designator(s). | |
| 394 | |
| 395 Retrieves the property value of the nodes specified by the designators. | |
| 396 ''' | |
| 397 if len(args) < 2: | |
| 398 raise UsageError, _('Not enough arguments supplied') | |
| 399 propname = args[0] | |
| 400 designators = args[1].split(',') | |
| 401 l = [] | |
| 402 for designator in designators: | |
| 403 # decode the node designator | |
| 404 try: | |
| 405 classname, nodeid = hyperdb.splitDesignator(designator) | |
| 406 except hyperdb.DesignatorError, message: | |
| 407 raise UsageError, message | |
| 408 | |
| 409 # get the class | |
| 410 cl = self.get_class(classname) | |
| 411 try: | |
| 412 if self.comma_sep: | |
| 413 l.append(cl.get(nodeid, propname)) | |
| 414 else: | |
| 415 print cl.get(nodeid, propname) | |
| 416 except IndexError: | |
| 417 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals() | |
| 418 except KeyError: | |
| 419 raise UsageError, _('no such %(classname)s property ' | |
| 420 '"%(propname)s"')%locals() | |
| 421 if self.comma_sep: | |
| 422 print ','.join(l) | |
| 423 return 0 | |
| 424 | |
| 425 | |
| 426 def do_set(self, args, pwre = re.compile(r'{(\w+)}(.+)')): | |
| 427 '''Usage: set [items] property=value property=value ... | |
| 428 Set the given properties of one or more items(s). | |
| 429 | |
| 430 The items may be specified as a class or as a comma-separeted | |
| 431 list of item designators (ie "designator[,designator,...]"). | |
| 432 | |
| 433 This command sets the properties to the values for all designators | |
| 434 given. If the value is missing (ie. "property=") then the property is | |
| 435 un-set. | |
| 436 ''' | |
| 437 if len(args) < 2: | |
| 438 raise UsageError, _('Not enough arguments supplied') | |
| 439 from roundup import hyperdb | |
| 440 | |
| 441 designators = args[0].split(',') | |
| 442 if len(designators) == 1: | |
| 443 designator = designators[0] | |
| 444 try: | |
| 445 designator = hyperdb.splitDesignator(designator) | |
| 446 designators = [designator] | |
| 447 except hyperdb.DesignatorError: | |
| 448 cl = self.get_class(designator) | |
| 449 designators = [(designator, x) for x in cl.list()] | |
| 450 else: | |
| 451 try: | |
| 452 designators = [hyperdb.splitDesignator(x) for x in designators] | |
| 453 except hyperdb.DesignatorError, message: | |
| 454 raise UsageError, message | |
| 455 | |
| 456 # get the props from the args | |
| 457 props = self.props_from_args(args[1:]) | |
| 458 | |
| 459 # now do the set for all the nodes | |
| 460 for classname, itemid in designators: | |
| 461 cl = self.get_class(classname) | |
| 462 | |
| 463 properties = cl.getprops() | |
| 464 for key, value in props.items(): | |
| 465 proptype = properties[key] | |
| 466 if isinstance(proptype, hyperdb.Multilink): | |
| 467 if value is None: | |
| 468 props[key] = [] | |
| 469 else: | |
| 470 props[key] = value.split(',') | |
| 471 elif value is None: | |
| 472 continue | |
| 473 elif isinstance(proptype, hyperdb.String): | |
| 474 continue | |
| 475 elif isinstance(proptype, hyperdb.Password): | |
| 476 m = pwre.match(value) | |
| 477 if m: | |
| 478 # password is being given to us encrypted | |
| 479 p = password.Password() | |
| 480 p.scheme = m.group(1) | |
| 481 p.password = m.group(2) | |
| 482 props[key] = p | |
| 483 else: | |
| 484 props[key] = password.Password(value) | |
| 485 elif isinstance(proptype, hyperdb.Date): | |
| 486 try: | |
| 487 props[key] = date.Date(value) | |
| 488 except ValueError, message: | |
| 489 raise UsageError, '"%s": %s'%(value, message) | |
| 490 elif isinstance(proptype, hyperdb.Interval): | |
| 491 try: | |
| 492 props[key] = date.Interval(value) | |
| 493 except ValueError, message: | |
| 494 raise UsageError, '"%s": %s'%(value, message) | |
| 495 elif isinstance(proptype, hyperdb.Link): | |
| 496 props[key] = value | |
| 497 elif isinstance(proptype, hyperdb.Boolean): | |
| 498 props[key] = value.lower() in ('yes', 'true', 'on', '1') | |
| 499 elif isinstance(proptype, hyperdb.Number): | |
| 500 props[key] = int(value) | |
| 501 | |
| 502 # try the set | |
| 503 try: | |
| 504 apply(cl.set, (itemid, ), props) | |
| 505 except (TypeError, IndexError, ValueError), message: | |
| 506 import traceback; traceback.print_exc() | |
| 507 raise UsageError, message | |
| 508 return 0 | |
| 509 | |
| 510 def do_find(self, args): | |
| 511 '''Usage: find classname propname=value ... | |
| 512 Find the nodes of the given class with a given link property value. | |
| 513 | |
| 514 Find the nodes of the given class with a given link property value. The | |
| 515 value may be either the nodeid of the linked node, or its key value. | |
| 516 ''' | |
| 517 if len(args) < 1: | |
| 518 raise UsageError, _('Not enough arguments supplied') | |
| 519 classname = args[0] | |
| 520 # get the class | |
| 521 cl = self.get_class(classname) | |
| 522 | |
| 523 # handle the propname=value argument | |
| 524 props = self.props_from_args(args[1:]) | |
| 525 | |
| 526 # if the value isn't a number, look up the linked class to get the | |
| 527 # number | |
| 528 for propname, value in props.items(): | |
| 529 num_re = re.compile('^\d+$') | |
| 530 if not num_re.match(value): | |
| 531 # get the property | |
| 532 try: | |
| 533 property = cl.properties[propname] | |
| 534 except KeyError: | |
| 535 raise UsageError, _('%(classname)s has no property ' | |
| 536 '"%(propname)s"')%locals() | |
| 537 | |
| 538 # make sure it's a link | |
| 539 if (not isinstance(property, hyperdb.Link) and not | |
| 540 isinstance(property, hyperdb.Multilink)): | |
| 541 raise UsageError, _('You may only "find" link properties') | |
| 542 | |
| 543 # get the linked-to class and look up the key property | |
| 544 link_class = self.db.getclass(property.classname) | |
| 545 try: | |
| 546 props[propname] = link_class.lookup(value) | |
| 547 except TypeError: | |
| 548 raise UsageError, _('%(classname)s has no key property"')%{ | |
| 549 'classname': link_class.classname} | |
| 550 | |
| 551 # now do the find | |
| 552 try: | |
| 553 if self.comma_sep: | |
| 554 print ','.join(apply(cl.find, (), props)) | |
| 555 else: | |
| 556 print apply(cl.find, (), props) | |
| 557 except KeyError: | |
| 558 raise UsageError, _('%(classname)s has no property ' | |
| 559 '"%(propname)s"')%locals() | |
| 560 except (ValueError, TypeError), message: | |
| 561 raise UsageError, message | |
| 562 return 0 | |
| 563 | |
| 564 def do_specification(self, args): | |
| 565 '''Usage: specification classname | |
| 566 Show the properties for a classname. | |
| 567 | |
| 568 This lists the properties for a given class. | |
| 569 ''' | |
| 570 if len(args) < 1: | |
| 571 raise UsageError, _('Not enough arguments supplied') | |
| 572 classname = args[0] | |
| 573 # get the class | |
| 574 cl = self.get_class(classname) | |
| 575 | |
| 576 # get the key property | |
| 577 keyprop = cl.getkey() | |
| 578 for key, value in cl.properties.items(): | |
| 579 if keyprop == key: | |
| 580 print _('%(key)s: %(value)s (key property)')%locals() | |
| 581 else: | |
| 582 print _('%(key)s: %(value)s')%locals() | |
| 583 | |
| 584 def do_display(self, args): | |
| 585 '''Usage: display designator | |
| 586 Show the property values for the given node. | |
| 587 | |
| 588 This lists the properties and their associated values for the given | |
| 589 node. | |
| 590 ''' | |
| 591 if len(args) < 1: | |
| 592 raise UsageError, _('Not enough arguments supplied') | |
| 593 | |
| 594 # decode the node designator | |
| 595 try: | |
| 596 classname, nodeid = hyperdb.splitDesignator(args[0]) | |
| 597 except hyperdb.DesignatorError, message: | |
| 598 raise UsageError, message | |
| 599 | |
| 600 # get the class | |
| 601 cl = self.get_class(classname) | |
| 602 | |
| 603 # display the values | |
| 604 for key in cl.properties.keys(): | |
| 605 value = cl.get(nodeid, key) | |
| 606 print _('%(key)s: %(value)s')%locals() | |
| 607 | |
| 608 def do_create(self, args, pwre = re.compile(r'{(\w+)}(.+)')): | |
| 609 '''Usage: create classname property=value ... | |
| 610 Create a new entry of a given class. | |
| 611 | |
| 612 This creates a new entry of the given class using the property | |
| 613 name=value arguments provided on the command line after the "create" | |
| 614 command. | |
| 615 ''' | |
| 616 if len(args) < 1: | |
| 617 raise UsageError, _('Not enough arguments supplied') | |
| 618 from roundup import hyperdb | |
| 619 | |
| 620 classname = args[0] | |
| 621 | |
| 622 # get the class | |
| 623 cl = self.get_class(classname) | |
| 624 | |
| 625 # now do a create | |
| 626 props = {} | |
| 627 properties = cl.getprops(protected = 0) | |
| 628 if len(args) == 1: | |
| 629 # ask for the properties | |
| 630 for key, value in properties.items(): | |
| 631 if key == 'id': continue | |
| 632 name = value.__class__.__name__ | |
| 633 if isinstance(value , hyperdb.Password): | |
| 634 again = None | |
| 635 while value != again: | |
| 636 value = getpass.getpass(_('%(propname)s (Password): ')%{ | |
| 637 'propname': key.capitalize()}) | |
| 638 again = getpass.getpass(_(' %(propname)s (Again): ')%{ | |
| 639 'propname': key.capitalize()}) | |
| 640 if value != again: print _('Sorry, try again...') | |
| 641 if value: | |
| 642 props[key] = value | |
| 643 else: | |
| 644 value = raw_input(_('%(propname)s (%(proptype)s): ')%{ | |
| 645 'propname': key.capitalize(), 'proptype': name}) | |
| 646 if value: | |
| 647 props[key] = value | |
| 648 else: | |
| 649 props = self.props_from_args(args[1:]) | |
| 650 | |
| 651 # convert types | |
| 652 for propname, value in props.items(): | |
| 653 # get the property | |
| 654 try: | |
| 655 proptype = properties[propname] | |
| 656 except KeyError: | |
| 657 raise UsageError, _('%(classname)s has no property ' | |
| 658 '"%(propname)s"')%locals() | |
| 659 | |
| 660 if isinstance(proptype, hyperdb.Date): | |
| 661 try: | |
| 662 props[propname] = date.Date(value) | |
| 663 except ValueError, message: | |
| 664 raise UsageError, _('"%(value)s": %(message)s')%locals() | |
| 665 elif isinstance(proptype, hyperdb.Interval): | |
| 666 try: | |
| 667 props[propname] = date.Interval(value) | |
| 668 except ValueError, message: | |
| 669 raise UsageError, _('"%(value)s": %(message)s')%locals() | |
| 670 elif isinstance(proptype, hyperdb.Password): | |
| 671 m = pwre.match(value) | |
| 672 if m: | |
| 673 # password is being given to us encrypted | |
| 674 p = password.Password() | |
| 675 p.scheme = m.group(1) | |
| 676 p.password = m.group(2) | |
| 677 props[propname] = p | |
| 678 else: | |
| 679 props[propname] = password.Password(value) | |
| 680 elif isinstance(proptype, hyperdb.Multilink): | |
| 681 props[propname] = value.split(',') | |
| 682 elif isinstance(proptype, hyperdb.Boolean): | |
| 683 props[propname] = value.lower() in ('yes', 'true', 'on', '1') | |
| 684 elif isinstance(proptype, hyperdb.Number): | |
| 685 props[propname] = int(value) | |
| 686 | |
| 687 # check for the key property | |
| 688 propname = cl.getkey() | |
| 689 if propname and not props.has_key(propname): | |
| 690 raise UsageError, _('you must provide the "%(propname)s" ' | |
| 691 'property.')%locals() | |
| 692 | |
| 693 # do the actual create | |
| 694 try: | |
| 695 print apply(cl.create, (), props) | |
| 696 except (TypeError, IndexError, ValueError), message: | |
| 697 raise UsageError, message | |
| 698 return 0 | |
| 699 | |
| 700 def do_list(self, args): | |
| 701 '''Usage: list classname [property] | |
| 702 List the instances of a class. | |
| 703 | |
| 704 Lists all instances of the given class. If the property is not | |
| 705 specified, the "label" property is used. The label property is tried | |
| 706 in order: the key, "name", "title" and then the first property, | |
| 707 alphabetically. | |
| 708 ''' | |
| 709 if len(args) < 1: | |
| 710 raise UsageError, _('Not enough arguments supplied') | |
| 711 classname = args[0] | |
| 712 | |
| 713 # get the class | |
| 714 cl = self.get_class(classname) | |
| 715 | |
| 716 # figure the property | |
| 717 if len(args) > 1: | |
| 718 propname = args[1] | |
| 719 else: | |
| 720 propname = cl.labelprop() | |
| 721 | |
| 722 if self.comma_sep: | |
| 723 print ','.join(cl.list()) | |
| 724 else: | |
| 725 for nodeid in cl.list(): | |
| 726 try: | |
| 727 value = cl.get(nodeid, propname) | |
| 728 except KeyError: | |
| 729 raise UsageError, _('%(classname)s has no property ' | |
| 730 '"%(propname)s"')%locals() | |
| 731 print _('%(nodeid)4s: %(value)s')%locals() | |
| 732 return 0 | |
| 733 | |
| 734 def do_table(self, args): | |
| 735 '''Usage: table classname [property[,property]*] | |
| 736 List the instances of a class in tabular form. | |
| 737 | |
| 738 Lists all instances of the given class. If the properties are not | |
| 739 specified, all properties are displayed. By default, the column widths | |
| 740 are the width of the property names. The width may be explicitly defined | |
| 741 by defining the property as "name:width". For example:: | |
| 742 roundup> table priority id,name:10 | |
| 743 Id Name | |
| 744 1 fatal-bug | |
| 745 2 bug | |
| 746 3 usability | |
| 747 4 feature | |
| 748 ''' | |
| 749 if len(args) < 1: | |
| 750 raise UsageError, _('Not enough arguments supplied') | |
| 751 classname = args[0] | |
| 752 | |
| 753 # get the class | |
| 754 cl = self.get_class(classname) | |
| 755 | |
| 756 # figure the property names to display | |
| 757 if len(args) > 1: | |
| 758 prop_names = args[1].split(',') | |
| 759 all_props = cl.getprops() | |
| 760 for spec in prop_names: | |
| 761 if ':' in spec: | |
| 762 try: | |
| 763 propname, width = spec.split(':') | |
| 764 except (ValueError, TypeError): | |
| 765 raise UsageError, _('"%(spec)s" not name:width')%locals() | |
| 766 else: | |
| 767 propname = spec | |
| 768 if not all_props.has_key(propname): | |
| 769 raise UsageError, _('%(classname)s has no property ' | |
| 770 '"%(propname)s"')%locals() | |
| 771 else: | |
| 772 prop_names = cl.getprops().keys() | |
| 773 | |
| 774 # now figure column widths | |
| 775 props = [] | |
| 776 for spec in prop_names: | |
| 777 if ':' in spec: | |
| 778 name, width = spec.split(':') | |
| 779 props.append((name, int(width))) | |
| 780 else: | |
| 781 props.append((spec, len(spec))) | |
| 782 | |
| 783 # now display the heading | |
| 784 print ' '.join([name.capitalize().ljust(width) for name,width in props]) | |
| 785 | |
| 786 # and the table data | |
| 787 for nodeid in cl.list(): | |
| 788 l = [] | |
| 789 for name, width in props: | |
| 790 if name != 'id': | |
| 791 try: | |
| 792 value = str(cl.get(nodeid, name)) | |
| 793 except KeyError: | |
| 794 # we already checked if the property is valid - a | |
| 795 # KeyError here means the node just doesn't have a | |
| 796 # value for it | |
| 797 value = '' | |
| 798 else: | |
| 799 value = str(nodeid) | |
| 800 f = '%%-%ds'%width | |
| 801 l.append(f%value[:width]) | |
| 802 print ' '.join(l) | |
| 803 return 0 | |
| 804 | |
| 805 def do_history(self, args): | |
| 806 '''Usage: history designator | |
| 807 Show the history entries of a designator. | |
| 808 | |
| 809 Lists the journal entries for the node identified by the designator. | |
| 810 ''' | |
| 811 if len(args) < 1: | |
| 812 raise UsageError, _('Not enough arguments supplied') | |
| 813 try: | |
| 814 classname, nodeid = hyperdb.splitDesignator(args[0]) | |
| 815 except hyperdb.DesignatorError, message: | |
| 816 raise UsageError, message | |
| 817 | |
| 818 try: | |
| 819 print self.db.getclass(classname).history(nodeid) | |
| 820 except KeyError: | |
| 821 raise UsageError, _('no such class "%(classname)s"')%locals() | |
| 822 except IndexError: | |
| 823 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals() | |
| 824 return 0 | |
| 825 | |
| 826 def do_commit(self, args): | |
| 827 '''Usage: commit | |
| 828 Commit all changes made to the database. | |
| 829 | |
| 830 The changes made during an interactive session are not | |
| 831 automatically written to the database - they must be committed | |
| 832 using this command. | |
| 833 | |
| 834 One-off commands on the command-line are automatically committed if | |
| 835 they are successful. | |
| 836 ''' | |
| 837 self.db.commit() | |
| 838 return 0 | |
| 839 | |
| 840 def do_rollback(self, args): | |
| 841 '''Usage: rollback | |
| 842 Undo all changes that are pending commit to the database. | |
| 843 | |
| 844 The changes made during an interactive session are not | |
| 845 automatically written to the database - they must be committed | |
| 846 manually. This command undoes all those changes, so a commit | |
| 847 immediately after would make no changes to the database. | |
| 848 ''' | |
| 849 self.db.rollback() | |
| 850 return 0 | |
| 851 | |
| 852 def do_retire(self, args): | |
| 853 '''Usage: retire designator[,designator]* | |
| 854 Retire the node specified by designator. | |
| 855 | |
| 856 This action indicates that a particular node is not to be retrieved by | |
| 857 the list or find commands, and its key value may be re-used. | |
| 858 ''' | |
| 859 if len(args) < 1: | |
| 860 raise UsageError, _('Not enough arguments supplied') | |
| 861 designators = args[0].split(',') | |
| 862 for designator in designators: | |
| 863 try: | |
| 864 classname, nodeid = hyperdb.splitDesignator(designator) | |
| 865 except hyperdb.DesignatorError, message: | |
| 866 raise UsageError, message | |
| 867 try: | |
| 868 self.db.getclass(classname).retire(nodeid) | |
| 869 except KeyError: | |
| 870 raise UsageError, _('no such class "%(classname)s"')%locals() | |
| 871 except IndexError: | |
| 872 raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals() | |
| 873 return 0 | |
| 874 | |
| 875 def do_export(self, args): | |
| 876 '''Usage: export [class[,class]] export_dir | |
| 877 Export the database to colon-separated-value files. | |
| 878 | |
| 879 This action exports the current data from the database into | |
| 880 colon-separated-value files that are placed in the nominated | |
| 881 destination directory. The journals are not exported. | |
| 882 ''' | |
| 883 # we need the CSV module | |
| 884 if csv is None: | |
| 885 raise UsageError, \ | |
| 886 _('Sorry, you need the csv module to use this function.\n' | |
| 887 'Get it from: http://www.object-craft.com.au/projects/csv/') | |
| 888 | |
| 889 # grab the directory to export to | |
| 890 if len(args) < 1: | |
| 891 raise UsageError, _('Not enough arguments supplied') | |
| 892 dir = args[-1] | |
| 893 | |
| 894 # get the list of classes to export | |
| 895 if len(args) == 2: | |
| 896 classes = args[0].split(',') | |
| 897 else: | |
| 898 classes = self.db.classes.keys() | |
| 899 | |
| 900 # use the csv parser if we can - it's faster | |
| 901 p = csv.parser(field_sep=':') | |
| 902 | |
| 903 # do all the classes specified | |
| 904 for classname in classes: | |
| 905 cl = self.get_class(classname) | |
| 906 f = open(os.path.join(dir, classname+'.csv'), 'w') | |
| 907 properties = cl.getprops() | |
| 908 propnames = properties.keys() | |
| 909 propnames.sort() | |
| 910 print >> f, p.join(propnames) | |
| 911 | |
| 912 # all nodes for this class | |
| 913 for nodeid in cl.list(): | |
| 914 print >>f, p.join(cl.export_list(propnames, nodeid)) | |
| 915 return 0 | |
| 916 | |
| 917 def do_import(self, args): | |
| 918 '''Usage: import import_dir | |
| 919 Import a database from the directory containing CSV files, one per | |
| 920 class to import. | |
| 921 | |
| 922 The files must define the same properties as the class (including having | |
| 923 a "header" line with those property names.) | |
| 924 | |
| 925 The imported nodes will have the same nodeid as defined in the | |
| 926 import file, thus replacing any existing content. | |
| 927 | |
| 928 The new nodes are added to the existing database - if you want to | |
| 929 create a new database using the imported data, then create a new | |
| 930 database (or, tediously, retire all the old data.) | |
| 931 ''' | |
| 932 if len(args) < 1: | |
| 933 raise UsageError, _('Not enough arguments supplied') | |
| 934 if csv is None: | |
| 935 raise UsageError, \ | |
| 936 _('Sorry, you need the csv module to use this function.\n' | |
| 937 'Get it from: http://www.object-craft.com.au/projects/csv/') | |
| 938 | |
| 939 from roundup import hyperdb | |
| 940 | |
| 941 for file in os.listdir(args[0]): | |
| 942 f = open(os.path.join(args[0], file)) | |
| 943 | |
| 944 # get the classname | |
| 945 classname = os.path.splitext(file)[0] | |
| 946 | |
| 947 # ensure that the properties and the CSV file headings match | |
| 948 cl = self.get_class(classname) | |
| 949 p = csv.parser(field_sep=':') | |
| 950 file_props = p.parse(f.readline()) | |
| 951 properties = cl.getprops() | |
| 952 propnames = properties.keys() | |
| 953 propnames.sort() | |
| 954 m = file_props[:] | |
| 955 m.sort() | |
| 956 if m != propnames: | |
| 957 raise UsageError, _('Import file doesn\'t define the same ' | |
| 958 'properties as "%(arg0)s".')%{'arg0': args[0]} | |
| 959 | |
| 960 # loop through the file and create a node for each entry | |
| 961 maxid = 1 | |
| 962 while 1: | |
| 963 line = f.readline() | |
| 964 if not line: break | |
| 965 | |
| 966 # parse lines until we get a complete entry | |
| 967 while 1: | |
| 968 l = p.parse(line) | |
| 969 if l: break | |
| 970 line = f.readline() | |
| 971 if not line: | |
| 972 raise ValueError, "Unexpected EOF during CSV parse" | |
| 973 | |
| 974 # do the import and figure the current highest nodeid | |
| 975 maxid = max(maxid, int(cl.import_list(propnames, l))) | |
| 976 | |
| 977 print 'setting', classname, maxid+1 | |
| 978 self.db.setid(classname, str(maxid+1)) | |
| 979 return 0 | |
| 980 | |
| 981 def do_pack(self, args): | |
| 982 '''Usage: pack period | date | |
| 983 | |
| 984 Remove journal entries older than a period of time specified or | |
| 985 before a certain date. | |
| 986 | |
| 987 A period is specified using the suffixes "y", "m", and "d". The | |
| 988 suffix "w" (for "week") means 7 days. | |
| 989 | |
| 990 "3y" means three years | |
| 991 "2y 1m" means two years and one month | |
| 992 "1m 25d" means one month and 25 days | |
| 993 "2w 3d" means two weeks and three days | |
| 994 | |
| 995 Date format is "YYYY-MM-DD" eg: | |
| 996 2001-01-01 | |
| 997 | |
| 998 ''' | |
| 999 if len(args) <> 1: | |
| 1000 raise UsageError, _('Not enough arguments supplied') | |
| 1001 | |
| 1002 # are we dealing with a period or a date | |
| 1003 value = args[0] | |
| 1004 date_re = re.compile(r''' | |
| 1005 (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd | |
| 1006 (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)? | |
| 1007 ''', re.VERBOSE) | |
| 1008 m = date_re.match(value) | |
| 1009 if not m: | |
| 1010 raise ValueError, _('Invalid format') | |
| 1011 m = m.groupdict() | |
| 1012 if m['period']: | |
| 1013 pack_before = date.Date(". - %s"%value) | |
| 1014 elif m['date']: | |
| 1015 pack_before = date.Date(value) | |
| 1016 self.db.pack(pack_before) | |
| 1017 return 0 | |
| 1018 | |
| 1019 def do_reindex(self, args): | |
| 1020 '''Usage: reindex | |
| 1021 Re-generate a tracker's search indexes. | |
| 1022 | |
| 1023 This will re-generate the search indexes for a tracker. This will | |
| 1024 typically happen automatically. | |
| 1025 ''' | |
| 1026 self.db.indexer.force_reindex() | |
| 1027 self.db.reindex() | |
| 1028 return 0 | |
| 1029 | |
| 1030 def do_security(self, args): | |
| 1031 '''Usage: security [Role name] | |
| 1032 Display the Permissions available to one or all Roles. | |
| 1033 ''' | |
| 1034 if len(args) == 1: | |
| 1035 role = args[0] | |
| 1036 try: | |
| 1037 roles = [(args[0], self.db.security.role[args[0]])] | |
| 1038 except KeyError: | |
| 1039 print _('No such Role "%(role)s"')%locals() | |
| 1040 return 1 | |
| 1041 else: | |
| 1042 roles = self.db.security.role.items() | |
| 1043 role = self.db.config.NEW_WEB_USER_ROLES | |
| 1044 if ',' in role: | |
| 1045 print _('New Web users get the Roles "%(role)s"')%locals() | |
| 1046 else: | |
| 1047 print _('New Web users get the Role "%(role)s"')%locals() | |
| 1048 role = self.db.config.NEW_EMAIL_USER_ROLES | |
| 1049 if ',' in role: | |
| 1050 print _('New Email users get the Roles "%(role)s"')%locals() | |
| 1051 else: | |
| 1052 print _('New Email users get the Role "%(role)s"')%locals() | |
| 1053 roles.sort() | |
| 1054 for rolename, role in roles: | |
| 1055 print _('Role "%(name)s":')%role.__dict__ | |
| 1056 for permission in role.permissions: | |
| 1057 if permission.klass: | |
| 1058 print _(' %(description)s (%(name)s for "%(klass)s" ' | |
| 1059 'only)')%permission.__dict__ | |
| 1060 else: | |
| 1061 print _(' %(description)s (%(name)s)')%permission.__dict__ | |
| 1062 return 0 | |
| 1063 | |
| 1064 def run_command(self, args): | |
| 1065 '''Run a single command | |
| 1066 ''' | |
| 1067 command = args[0] | |
| 1068 | |
| 1069 # handle help now | |
| 1070 if command == 'help': | |
| 1071 if len(args)>1: | |
| 1072 self.do_help(args[1:]) | |
| 1073 return 0 | |
| 1074 self.do_help(['help']) | |
| 1075 return 0 | |
| 1076 if command == 'morehelp': | |
| 1077 self.do_help(['help']) | |
| 1078 self.help_commands() | |
| 1079 self.help_all() | |
| 1080 return 0 | |
| 1081 | |
| 1082 # figure what the command is | |
| 1083 try: | |
| 1084 functions = self.commands.get(command) | |
| 1085 except KeyError: | |
| 1086 # not a valid command | |
| 1087 print _('Unknown command "%(command)s" ("help commands" for a ' | |
| 1088 'list)')%locals() | |
| 1089 return 1 | |
| 1090 | |
| 1091 # check for multiple matches | |
| 1092 if len(functions) > 1: | |
| 1093 print _('Multiple commands match "%(command)s": %(list)s')%{'command': | |
| 1094 command, 'list': ', '.join([i[0] for i in functions])} | |
| 1095 return 1 | |
| 1096 command, function = functions[0] | |
| 1097 | |
| 1098 # make sure we have a tracker_home | |
| 1099 while not self.tracker_home: | |
| 1100 self.tracker_home = raw_input(_('Enter tracker home: ')).strip() | |
| 1101 | |
| 1102 # before we open the db, we may be doing an install or init | |
| 1103 if command == 'initialise': | |
| 1104 try: | |
| 1105 return self.do_initialise(self.tracker_home, args) | |
| 1106 except UsageError, message: | |
| 1107 print _('Error: %(message)s')%locals() | |
| 1108 return 1 | |
| 1109 elif command == 'install': | |
| 1110 try: | |
| 1111 return self.do_install(self.tracker_home, args) | |
| 1112 except UsageError, message: | |
| 1113 print _('Error: %(message)s')%locals() | |
| 1114 return 1 | |
| 1115 | |
| 1116 # get the tracker | |
| 1117 try: | |
| 1118 tracker = roundup.instance.open(self.tracker_home) | |
| 1119 except ValueError, message: | |
| 1120 self.tracker_home = '' | |
| 1121 print _("Error: Couldn't open tracker: %(message)s")%locals() | |
| 1122 return 1 | |
| 1123 | |
| 1124 # only open the database once! | |
| 1125 if not self.db: | |
| 1126 self.db = tracker.open('admin') | |
| 1127 | |
| 1128 # do the command | |
| 1129 ret = 0 | |
| 1130 try: | |
| 1131 ret = function(args[1:]) | |
| 1132 except UsageError, message: | |
| 1133 print _('Error: %(message)s')%locals() | |
| 1134 print | |
| 1135 print function.__doc__ | |
| 1136 ret = 1 | |
| 1137 except: | |
| 1138 import traceback | |
| 1139 traceback.print_exc() | |
| 1140 ret = 1 | |
| 1141 return ret | |
| 1142 | |
| 1143 def interactive(self): | |
| 1144 '''Run in an interactive mode | |
| 1145 ''' | |
| 1146 print _('Roundup %s ready for input.'%roundup_version) | |
| 1147 print _('Type "help" for help.') | |
| 1148 try: | |
| 1149 import readline | |
| 1150 except ImportError: | |
| 1151 print _('Note: command history and editing not available') | |
| 1152 | |
| 1153 while 1: | |
| 1154 try: | |
| 1155 command = raw_input(_('roundup> ')) | |
| 1156 except EOFError: | |
| 1157 print _('exit...') | |
| 1158 break | |
| 1159 if not command: continue | |
| 1160 args = token.token_split(command) | |
| 1161 if not args: continue | |
| 1162 if args[0] in ('quit', 'exit'): break | |
| 1163 self.run_command(args) | |
| 1164 | |
| 1165 # exit.. check for transactions | |
| 1166 if self.db and self.db.transactions: | |
| 1167 commit = raw_input(_('There are unsaved changes. Commit them (y/N)? ')) | |
| 1168 if commit and commit[0].lower() == 'y': | |
| 1169 self.db.commit() | |
| 1170 return 0 | |
| 1171 | |
| 1172 def main(self): | |
| 1173 try: | |
| 1174 opts, args = getopt.getopt(sys.argv[1:], 'i:u:hc') | |
| 1175 except getopt.GetoptError, e: | |
| 1176 self.usage(str(e)) | |
| 1177 return 1 | |
| 1178 | |
| 1179 # handle command-line args | |
| 1180 self.tracker_home = os.environ.get('TRACKER_HOME', '') | |
| 1181 # TODO: reinstate the user/password stuff (-u arg too) | |
| 1182 name = password = '' | |
| 1183 if os.environ.has_key('ROUNDUP_LOGIN'): | |
| 1184 l = os.environ['ROUNDUP_LOGIN'].split(':') | |
| 1185 name = l[0] | |
| 1186 if len(l) > 1: | |
| 1187 password = l[1] | |
| 1188 self.comma_sep = 0 | |
| 1189 for opt, arg in opts: | |
| 1190 if opt == '-h': | |
| 1191 self.usage() | |
| 1192 return 0 | |
| 1193 if opt == '-i': | |
| 1194 self.tracker_home = arg | |
| 1195 if opt == '-c': | |
| 1196 self.comma_sep = 1 | |
| 1197 | |
| 1198 # if no command - go interactive | |
| 1199 # wrap in a try/finally so we always close off the db | |
| 1200 ret = 0 | |
| 1201 try: | |
| 1202 if not args: | |
| 1203 self.interactive() | |
| 1204 else: | |
| 1205 ret = self.run_command(args) | |
| 1206 if self.db: self.db.commit() | |
| 1207 return ret | |
| 1208 finally: | |
| 1209 if self.db: | |
| 1210 self.db.close() | |
| 1211 | |
| 1212 if __name__ == '__main__': | |
| 1213 tool = AdminTool() | |
| 1214 sys.exit(tool.main()) | |
| 1215 | |
| 1216 # vim: set filetype=python ts=4 sw=4 et si |
