Mercurial > p > roundup > code
comparison roundup-admin @ 299:fd9835c1e58d
Did a fair bit of work on the admin tool.
Now has an extra command "table" which displays node information in a
tabular format. Also fixed import and export so they work. Removed
freshen. Fixed quopri usage in mailgw from bug reports.
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Wed, 17 Oct 2001 23:13:19 +0000 |
| parents | 07a64ec2a79d |
| children | 7901b58cfae8 |
comparison
equal
deleted
inserted
replaced
| 298:07a64ec2a79d | 299:fd9835c1e58d |
|---|---|
| 1 #! /usr/bin/python | 1 #! /Users/builder/bin/python |
| 2 # | 2 # |
| 3 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/) | 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 | 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 | 5 # under the same terms as Python, so long as this copyright message and |
| 6 # disclaimer are retained in their original form. | 6 # disclaimer are retained in their original form. |
| 14 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | 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" | 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, | 16 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, |
| 17 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. | 17 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. |
| 18 # | 18 # |
| 19 # $Id: roundup-admin,v 1.32 2001-10-17 06:57:29 richard Exp $ | 19 # $Id: roundup-admin,v 1.33 2001-10-17 23:13:19 richard Exp $ |
| 20 | 20 |
| 21 import sys | 21 import sys |
| 22 if int(sys.version[0]) < 2: | 22 if int(sys.version[0]) < 2: |
| 23 print 'Roundup requires python 2.0 or later.' | 23 print 'Roundup requires python 2.0 or later.' |
| 24 sys.exit(1) | 24 sys.exit(1) |
| 26 import string, os, getpass, getopt, re | 26 import string, os, getpass, getopt, re |
| 27 try: | 27 try: |
| 28 import csv | 28 import csv |
| 29 except ImportError: | 29 except ImportError: |
| 30 csv = None | 30 csv = None |
| 31 from roundup import date, roundupdb, init, password | 31 from roundup import date, hyperdb, roundupdb, init, password |
| 32 import roundup.instance | 32 import roundup.instance |
| 33 | 33 |
| 34 def usage(message=''): | 34 def usage(message=''): |
| 35 if message: message = 'Problem: '+message+'\n' | 35 if message: message = 'Problem: '+message+'\n' |
| 36 print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments> | 36 print '''%sUsage: roundup-admin [-i instance home] [-u login] [-c] <command> <arguments> |
| 335 | 335 |
| 336 def do_list(db, args, comma_sep=0): | 336 def do_list(db, args, comma_sep=0): |
| 337 '''Usage: list classname [property] | 337 '''Usage: list classname [property] |
| 338 List the instances of a class. | 338 List the instances of a class. |
| 339 | 339 |
| 340 Lists all instances of the given class along. If the property is not | 340 Lists all instances of the given class. If the property is not |
| 341 specified, the "label" property is used. The label property is tried | 341 specified, the "label" property is used. The label property is tried |
| 342 in order: the key, "name", "title" and then the first property, | 342 in order: the key, "name", "title" and then the first property, |
| 343 alphabetically. | 343 alphabetically. |
| 344 ''' | 344 ''' |
| 345 classname = args[0] | 345 classname = args[0] |
| 354 for nodeid in cl.list(): | 354 for nodeid in cl.list(): |
| 355 value = cl.get(nodeid, key) | 355 value = cl.get(nodeid, key) |
| 356 print "%4s: %s"%(nodeid, value) | 356 print "%4s: %s"%(nodeid, value) |
| 357 return 0 | 357 return 0 |
| 358 | 358 |
| 359 def do_table(db, args, comma_sep=None): | |
| 360 '''Usage: table classname [property[,property]*] | |
| 361 List the instances of a class in tabular form. | |
| 362 | |
| 363 Lists all instances of the given class. If the properties are not | |
| 364 specified, all properties are displayed. By default, the column widths | |
| 365 are the width of the property names. The width may be explicitly defined | |
| 366 by defining the property as "name:width". For example:: | |
| 367 roundup> table priority id,name:10 | |
| 368 Id Name | |
| 369 1 fatal-bug | |
| 370 2 bug | |
| 371 3 usability | |
| 372 4 feature | |
| 373 ''' | |
| 374 classname = args[0] | |
| 375 cl = db.getclass(classname) | |
| 376 if len(args) > 1: | |
| 377 prop_names = args[1].split(',') | |
| 378 else: | |
| 379 prop_names = cl.getprops().keys() | |
| 380 props = [] | |
| 381 for name in prop_names: | |
| 382 if ':' in name: | |
| 383 name, width = name.split(':') | |
| 384 props.append((name, int(width))) | |
| 385 else: | |
| 386 props.append((name, len(name))) | |
| 387 | |
| 388 print ' '.join([string.capitalize(name) for name, width in props]) | |
| 389 for nodeid in cl.list(): | |
| 390 l = [] | |
| 391 for name, width in props: | |
| 392 if name != 'id': | |
| 393 value = str(cl.get(nodeid, name)) | |
| 394 else: | |
| 395 value = str(nodeid) | |
| 396 f = '%%-%ds'%width | |
| 397 l.append(f%value[:width]) | |
| 398 print ' '.join(l) | |
| 399 return 0 | |
| 400 | |
| 359 def do_history(db, args, comma_sep=0): | 401 def do_history(db, args, comma_sep=0): |
| 360 '''Usage: history designator | 402 '''Usage: history designator |
| 361 Show the history entries of a designator. | 403 Show the history entries of a designator. |
| 362 | 404 |
| 363 Lists the journal entries for the node identified by the designator. | 405 Lists the journal entries for the node identified by the designator. |
| 380 db.getclass(classname).retire(nodeid) | 422 db.getclass(classname).retire(nodeid) |
| 381 return 0 | 423 return 0 |
| 382 | 424 |
| 383 def do_export(db, args, comma_sep=0): | 425 def do_export(db, args, comma_sep=0): |
| 384 '''Usage: export class[,class] destination_dir | 426 '''Usage: export class[,class] destination_dir |
| 385 ** EXPERIMENTAL ** | 427 Export the database to tab-separated-value files. |
| 386 Export the database to CSV files by class in the given directory. | |
| 387 | 428 |
| 388 This action exports the current data from the database into | 429 This action exports the current data from the database into |
| 389 comma-separated files that are placed in the nominated destination | 430 tab-separated-value files that are placed in the nominated destination |
| 390 directory. The journals are not exported. | 431 directory. The journals are not exported. |
| 391 ''' | 432 ''' |
| 392 if len(args) < 2: | 433 if len(args) < 2: |
| 393 print do_export.__doc__ | 434 print do_export.__doc__ |
| 394 return 1 | 435 return 1 |
| 395 classes = string.split(args[0], ',') | 436 classes = string.split(args[0], ',') |
| 396 dir = args[1] | 437 dir = args[1] |
| 397 | 438 |
| 398 # use the csv parser if we can - it's faster | 439 # use the csv parser if we can - it's faster |
| 399 if csv is not None: | 440 if csv is not None: |
| 400 p = csv.parser() | 441 p = csv.parser(field_sep=':') |
| 401 | 442 |
| 402 # do all the classes specified | 443 # do all the classes specified |
| 403 for classname in classes: | 444 for classname in classes: |
| 404 cl = db.getclass(classname) | 445 cl = db.getclass(classname) |
| 405 f = open(os.path.join(dir, classname+'.csv'), 'w') | 446 f = open(os.path.join(dir, classname+'.csv'), 'w') |
| 406 f.write(string.join(cl.properties.keys(), ',') + '\n') | 447 f.write(string.join(cl.properties.keys(), ':') + '\n') |
| 407 | 448 |
| 408 # all nodes for this class | 449 # all nodes for this class |
| 450 properties = cl.properties.items() | |
| 409 for nodeid in cl.list(): | 451 for nodeid in cl.list(): |
| 452 l = [] | |
| 453 for prop, type in properties: | |
| 454 value = cl.get(nodeid, prop) | |
| 455 # convert data where needed | |
| 456 if isinstance(type, hyperdb.Date): | |
| 457 value = value.get_tuple() | |
| 458 elif isinstance(type, hyperdb.Interval): | |
| 459 value = value.get_tuple() | |
| 460 elif isinstance(type, hyperdb.Password): | |
| 461 value = str(value) | |
| 462 l.append(repr(value)) | |
| 463 | |
| 464 # now write | |
| 410 if csv is not None: | 465 if csv is not None: |
| 411 s = p.join(map(str, cl.getnode(nodeid).values(protected=0))) | 466 f.write(p.join(l) + '\n') |
| 412 f.write(s + '\n') | |
| 413 else: | 467 else: |
| 414 l = [] | |
| 415 # escape the individual entries to they're valid CSV | 468 # escape the individual entries to they're valid CSV |
| 416 for entry in map(str, cl.getnode(nodeid).values(protected=0)): | 469 m = [] |
| 470 for entry in l: | |
| 417 if '"' in entry: | 471 if '"' in entry: |
| 418 entry = '""'.join(entry.split('"')) | 472 entry = '""'.join(entry.split('"')) |
| 419 if ',' in entry: | 473 if ':' in entry: |
| 420 entry = '"%s"'%entry | 474 entry = '"%s"'%entry |
| 421 l.append(entry) | 475 m.append(entry) |
| 422 f.write(','.join(l) + '\n') | 476 f.write(':'.join(m) + '\n') |
| 423 return 0 | 477 return 0 |
| 424 | 478 |
| 425 def do_import(db, args, comma_sep=0): | 479 def do_import(db, args, comma_sep=0): |
| 426 '''Usage: import class file | 480 '''Usage: import class file |
| 427 ** EXPERIMENTAL ** | 481 Import the contents of the tab-separated-value file. |
| 428 Import the contents of the CSV file as new nodes for the given class. | |
| 429 | 482 |
| 430 The file must define the same properties as the class (including having | 483 The file must define the same properties as the class (including having |
| 431 a "header" line with those property names.) The new nodes are added to | 484 a "header" line with those property names.) The new nodes are added to |
| 432 the existing database - if you want to create a new database using the | 485 the existing database - if you want to create a new database using the |
| 433 imported data, then create a new database (or, tediously, retire all | 486 imported data, then create a new database (or, tediously, retire all |
| 434 the old data.) | 487 the old data.) |
| 435 ''' | 488 ''' |
| 436 if len(args) < 2: | 489 if len(args) < 2: |
| 437 print do_export.__doc__ | 490 print do_import.__doc__ |
| 438 return 1 | 491 return 1 |
| 439 if csv is None: | 492 if csv is None: |
| 440 print 'Sorry, you need the csv module to use this function.' | 493 print 'Sorry, you need the csv module to use this function.' |
| 441 print 'Get it from: http://www.object-craft.com.au/projects/csv/' | 494 print 'Get it from: http://www.object-craft.com.au/projects/csv/' |
| 442 return 1 | 495 return 1 |
| 444 from roundup import hyperdb | 497 from roundup import hyperdb |
| 445 | 498 |
| 446 # ensure that the properties and the CSV file headings match | 499 # ensure that the properties and the CSV file headings match |
| 447 cl = db.getclass(args[0]) | 500 cl = db.getclass(args[0]) |
| 448 f = open(args[1]) | 501 f = open(args[1]) |
| 449 p = csv.parser() | 502 p = csv.parser(field_sep=':') |
| 450 file_props = p.parse(f.readline()) | 503 file_props = p.parse(f.readline()) |
| 451 props = cl.properties.keys() | 504 props = cl.properties.keys() |
| 452 m = file_props[:] | 505 m = file_props[:] |
| 453 m.sort() | 506 m.sort() |
| 454 props.sort() | 507 props.sort() |
| 455 if m != props: | 508 if m != props: |
| 456 print do_export.__doc__ | 509 print 'Import file doesn\'t define the same properties as "%s".'%args[0] |
| 457 print "\n\nFile doesn't define the same properties" | |
| 458 return 1 | 510 return 1 |
| 459 | 511 |
| 460 # loop through the file and create a node for each entry | 512 # loop through the file and create a node for each entry |
| 461 n = range(len(props)) | 513 n = range(len(props)) |
| 462 while 1: | 514 while 1: |
| 469 if l: break | 521 if l: break |
| 470 | 522 |
| 471 # make the new node's property map | 523 # make the new node's property map |
| 472 d = {} | 524 d = {} |
| 473 for i in n: | 525 for i in n: |
| 474 value = l[i] | 526 # Use eval to reverse the repr() used to output the CSV |
| 527 value = eval(l[i]) | |
| 528 # Figure the property for this column | |
| 475 key = file_props[i] | 529 key = file_props[i] |
| 476 type = cl.properties[key] | 530 type = cl.properties[key] |
| 531 # Convert for property type | |
| 477 if isinstance(type, hyperdb.Date): | 532 if isinstance(type, hyperdb.Date): |
| 478 value = date.Date(value) | 533 value = date.Date(value) |
| 479 elif isinstance(type, hyperdb.Interval): | 534 elif isinstance(type, hyperdb.Interval): |
| 480 value = date.Interval(value) | 535 value = date.Interval(value) |
| 481 elif isinstance(type, hyperdb.Password): | 536 elif isinstance(type, hyperdb.Password): |
| 482 pwd = password.Password() | 537 pwd = password.Password() |
| 483 pwd.unpack(value) | 538 pwd.unpack(value) |
| 484 value = pwd | 539 value = pwd |
| 485 elif isinstance(type, hyperdb.Multilink): | 540 if value is not None: |
| 486 value = value.split(',') | 541 d[key] = value |
| 487 d[key] = value | |
| 488 | 542 |
| 489 # and create the new node | 543 # and create the new node |
| 490 apply(cl.create, (), d) | 544 apply(cl.create, (), d) |
| 491 return 0 | 545 return 0 |
| 492 | |
| 493 def do_freshen(db, args, comma_sep=0): | |
| 494 '''Usage: freshen | |
| 495 Freshen an existing instance. **DO NOT USE** | |
| 496 | |
| 497 This currently kills databases!!!! | |
| 498 | |
| 499 This action should generally not be used. It reads in an instance | |
| 500 database and writes it again. In the future, is may also update | |
| 501 instance code to account for changes in templates. It's probably wise | |
| 502 not to use it anyway. Until we're sure it won't break things... | |
| 503 ''' | |
| 504 # for classname, cl in db.classes.items(): | |
| 505 # properties = cl.properties.items() | |
| 506 # for nodeid in cl.list(): | |
| 507 # node = {} | |
| 508 # for name, type in properties: | |
| 509 # isinstance( if type, hyperdb.Multilink): | |
| 510 # node[name] = cl.get(nodeid, name, []) | |
| 511 # else: | |
| 512 # node[name] = cl.get(nodeid, name, None) | |
| 513 # db.setnode(classname, nodeid, node) | |
| 514 return 1 | |
| 515 | 546 |
| 516 def figureCommands(): | 547 def figureCommands(): |
| 517 d = {} | 548 d = {} |
| 518 for k, v in globals().items(): | 549 for k, v in globals().items(): |
| 519 if k[:3] == 'do_': | 550 if k[:3] == 'do_': |
| 526 if k[:5] == 'help_': | 557 if k[:5] == 'help_': |
| 527 d[k[5:]] = v | 558 d[k[5:]] = v |
| 528 return d | 559 return d |
| 529 | 560 |
| 530 class AdminTool: | 561 class AdminTool: |
| 531 | |
| 532 def run_command(self, args): | 562 def run_command(self, args): |
| 563 '''Run a single command | |
| 564 ''' | |
| 533 command = args[0] | 565 command = args[0] |
| 534 | 566 |
| 535 # handle help now | 567 # handle help now |
| 536 if command == 'help': | 568 if command == 'help': |
| 537 if len(args)>1: | 569 if len(args)>1: |
| 555 | 587 |
| 556 function = figureCommands().get(command, None) | 588 function = figureCommands().get(command, None) |
| 557 | 589 |
| 558 # not a valid command | 590 # not a valid command |
| 559 if function is None: | 591 if function is None: |
| 560 usage('Unknown command "%s"'%command) | 592 print 'Unknown command "%s" ("help commands" for a list)'%command |
| 561 return 1 | 593 return 1 |
| 562 | 594 |
| 563 # get the instance | 595 # get the instance |
| 564 instance = roundup.instance.open(self.instance_home) | 596 instance = roundup.instance.open(self.instance_home) |
| 565 db = instance.open('admin') | 597 db = instance.open('admin') |
| 629 tool = AdminTool() | 661 tool = AdminTool() |
| 630 sys.exit(tool.main()) | 662 sys.exit(tool.main()) |
| 631 | 663 |
| 632 # | 664 # |
| 633 # $Log: not supported by cvs2svn $ | 665 # $Log: not supported by cvs2svn $ |
| 666 # Revision 1.32 2001/10/17 06:57:29 richard | |
| 667 # Interactive startup blurb - need to figure how to get the version in there. | |
| 668 # | |
| 634 # Revision 1.31 2001/10/17 06:17:26 richard | 669 # Revision 1.31 2001/10/17 06:17:26 richard |
| 635 # Now with readline support :) | 670 # Now with readline support :) |
| 636 # | 671 # |
| 637 # Revision 1.30 2001/10/17 06:04:00 richard | 672 # Revision 1.30 2001/10/17 06:04:00 richard |
| 638 # Beginnings of an interactive mode for roundup-admin | 673 # Beginnings of an interactive mode for roundup-admin |
