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

Roundup Issue Tracker: http://roundup-tracker.org/