Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/python
      2 """
      3 Module used to parse the autotest job results and generate an HTML report.
      4 
      5 @copyright: (c)2005-2007 Matt Kruse (javascripttoolbox.com)
      6 @copyright: Red Hat 2008-2009
      7 @author: Dror Russo (drusso (at] redhat.com)
      8 """
      9 
     10 import os, sys, re, getopt, time, datetime, commands
     11 import common
     12 
     13 
     14 format_css = """
     15 html,body {
     16     padding:0;
     17     color:#222;
     18     background:#FFFFFF;
     19 }
     20 
     21 body {
     22     padding:0px;
     23     font:76%/150% "Lucida Grande", "Lucida Sans Unicode", Lucida, Verdana, Geneva, Arial, Helvetica, sans-serif;
     24 }
     25 
     26 #page_title{
     27     text-decoration:none;
     28     font:bold 2em/2em Arial, Helvetica, sans-serif;
     29     text-transform:none;
     30     text-align: left;
     31     color:#555555;
     32     border-bottom: 1px solid #555555;
     33 }
     34 
     35 #page_sub_title{
     36         text-decoration:none;
     37         font:bold 16px Arial, Helvetica, sans-serif;
     38         text-transform:uppercase;
     39         text-align: left;
     40         color:#555555;
     41     margin-bottom:0;
     42 }
     43 
     44 #comment{
     45         text-decoration:none;
     46         font:bold 10px Arial, Helvetica, sans-serif;
     47         text-transform:none;
     48         text-align: left;
     49         color:#999999;
     50     margin-top:0;
     51 }
     52 
     53 
     54 #meta_headline{
     55                 text-decoration:none;
     56                 font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ;
     57                 text-align: left;
     58                 color:black;
     59                 font-weight: bold;
     60                 font-size: 14px;
     61         }
     62 
     63 
     64 table.meta_table
     65 {text-align: center;
     66 font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ;
     67 width: 90%;
     68 background-color: #FFFFFF;
     69 border: 0px;
     70 border-top: 1px #003377 solid;
     71 border-bottom: 1px #003377 solid;
     72 border-right: 1px #003377 solid;
     73 border-left: 1px #003377 solid;
     74 border-collapse: collapse;
     75 border-spacing: 0px;}
     76 
     77 table.meta_table td
     78 {background-color: #FFFFFF;
     79 color: #000;
     80 padding: 4px;
     81 border-top: 1px #BBBBBB solid;
     82 border-bottom: 1px #BBBBBB solid;
     83 font-weight: normal;
     84 font-size: 13px;}
     85 
     86 
     87 table.stats
     88 {text-align: center;
     89 font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ;
     90 width: 100%;
     91 background-color: #FFFFFF;
     92 border: 0px;
     93 border-top: 1px #003377 solid;
     94 border-bottom: 1px #003377 solid;
     95 border-right: 1px #003377 solid;
     96 border-left: 1px #003377 solid;
     97 border-collapse: collapse;
     98 border-spacing: 0px;}
     99 
    100 table.stats td{
    101 background-color: #FFFFFF;
    102 color: #000;
    103 padding: 4px;
    104 border-top: 1px #BBBBBB solid;
    105 border-bottom: 1px #BBBBBB solid;
    106 font-weight: normal;
    107 font-size: 11px;}
    108 
    109 table.stats th{
    110 background: #dcdcdc;
    111 color: #000;
    112 padding: 6px;
    113 font-size: 12px;
    114 border-bottom: 1px #003377 solid;
    115 font-weight: bold;}
    116 
    117 table.stats td.top{
    118 background-color: #dcdcdc;
    119 color: #000;
    120 padding: 6px;
    121 text-align: center;
    122 border: 0px;
    123 border-bottom: 1px #003377 solid;
    124 font-size: 10px;
    125 font-weight: bold;}
    126 
    127 table.stats th.table-sorted-asc{
    128         background-image: url(ascending.gif);
    129         background-position: top left  ;
    130         background-repeat: no-repeat;
    131 }
    132 
    133 table.stats th.table-sorted-desc{
    134         background-image: url(descending.gif);
    135         background-position: top left;
    136         background-repeat: no-repeat;
    137 }
    138 
    139 table.stats2
    140 {text-align: left;
    141 font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ;
    142 width: 100%;
    143 background-color: #FFFFFF;
    144 border: 0px;
    145 }
    146 
    147 table.stats2 td{
    148 background-color: #FFFFFF;
    149 color: #000;
    150 padding: 0px;
    151 font-weight: bold;
    152 font-size: 13px;}
    153 
    154 
    155 
    156 /* Put this inside a @media qualifier so Netscape 4 ignores it */
    157 @media screen, print {
    158         /* Turn off list bullets */
    159         ul.mktree  li { list-style: none; }
    160         /* Control how "spaced out" the tree is */
    161         ul.mktree, ul.mktree ul , ul.mktree li { margin-left:10px; padding:0px; }
    162         /* Provide space for our own "bullet" inside the LI */
    163         ul.mktree  li           .bullet { padding-left: 15px; }
    164         /* Show "bullets" in the links, depending on the class of the LI that the link's in */
    165         ul.mktree  li.liOpen    .bullet { cursor: pointer; }
    166         ul.mktree  li.liClosed  .bullet { cursor: pointer;  }
    167         ul.mktree  li.liBullet  .bullet { cursor: default; }
    168         /* Sublists are visible or not based on class of parent LI */
    169         ul.mktree  li.liOpen    ul { display: block; }
    170         ul.mktree  li.liClosed  ul { display: none; }
    171 
    172         /* Format menu items differently depending on what level of the tree they are in */
    173         /* Uncomment this if you want your fonts to decrease in size the deeper they are in the tree */
    174 /*
    175         ul.mktree  li ul li { font-size: 90% }
    176 */
    177 }
    178 """
    179 
    180 
    181 table_js = """
    182 /**
    183  * Copyright (c)2005-2007 Matt Kruse (javascripttoolbox.com)
    184  *
    185  * Dual licensed under the MIT and GPL licenses.
    186  * This basically means you can use this code however you want for
    187  * free, but don't claim to have written it yourself!
    188  * Donations always accepted: http://www.JavascriptToolbox.com/donate/
    189  *
    190  * Please do not link to the .js files on javascripttoolbox.com from
    191  * your site. Copy the files locally to your server instead.
    192  *
    193  */
    194 /**
    195  * Table.js
    196  * Functions for interactive Tables
    197  *
    198  * Copyright (c) 2007 Matt Kruse (javascripttoolbox.com)
    199  * Dual licensed under the MIT and GPL licenses.
    200  *
    201  * @version 0.981
    202  *
    203  * @history 0.981 2007-03-19 Added Sort.numeric_comma, additional date parsing formats
    204  * @history 0.980 2007-03-18 Release new BETA release pending some testing. Todo: Additional docs, examples, plus jQuery plugin.
    205  * @history 0.959 2007-03-05 Added more "auto" functionality, couple bug fixes
    206  * @history 0.958 2007-02-28 Added auto functionality based on class names
    207  * @history 0.957 2007-02-21 Speed increases, more code cleanup, added Auto Sort functionality
    208  * @history 0.956 2007-02-16 Cleaned up the code and added Auto Filter functionality.
    209  * @history 0.950 2006-11-15 First BETA release.
    210  *
    211  * @todo Add more date format parsers
    212  * @todo Add style classes to colgroup tags after sorting/filtering in case the user wants to highlight the whole column
    213  * @todo Correct for colspans in data rows (this may slow it down)
    214  * @todo Fix for IE losing form control values after sort?
    215  */
    216 
    217 /**
    218  * Sort Functions
    219  */
    220 var Sort = (function(){
    221         var sort = {};
    222         // Default alpha-numeric sort
    223         // --------------------------
    224         sort.alphanumeric = function(a,b) {
    225                 return (a==b)?0:(a<b)?-1:1;
    226         };
    227         sort.alphanumeric_rev = function(a,b) {
    228                 return (a==b)?0:(a<b)?1:-1;
    229         };
    230         sort['default'] = sort.alphanumeric; // IE chokes on sort.default
    231 
    232         // This conversion is generalized to work for either a decimal separator of , or .
    233         sort.numeric_converter = function(separator) {
    234                 return function(val) {
    235                         if (typeof(val)=="string") {
    236                                 val = parseFloat(val.replace(/^[^\d\.]*([\d., ]+).*/g,"$1").replace(new RegExp("[^\\\d"+separator+"]","g"),'').replace(/,/,'.')) || 0;
    237                         }
    238                         return val || 0;
    239                 };
    240         };
    241 
    242         // Numeric Reversed Sort
    243         // ------------
    244         sort.numeric_rev = function(a,b) {
    245                 if (sort.numeric.convert(a)>sort.numeric.convert(b)) {
    246                         return (-1);
    247                 }
    248                 if (sort.numeric.convert(a)==sort.numeric.convert(b)) {
    249                         return 0;
    250                 }
    251                 if (sort.numeric.convert(a)<sort.numeric.convert(b)) {
    252                         return 1;
    253                 }
    254         };
    255 
    256 
    257         // Numeric Sort
    258         // ------------
    259         sort.numeric = function(a,b) {
    260                 return sort.numeric.convert(a)-sort.numeric.convert(b);
    261         };
    262         sort.numeric.convert = sort.numeric_converter(".");
    263 
    264         // Numeric Sort - comma decimal separator
    265         // --------------------------------------
    266         sort.numeric_comma = function(a,b) {
    267                 return sort.numeric_comma.convert(a)-sort.numeric_comma.convert(b);
    268         };
    269         sort.numeric_comma.convert = sort.numeric_converter(",");
    270 
    271         // Case-insensitive Sort
    272         // ---------------------
    273         sort.ignorecase = function(a,b) {
    274                 return sort.alphanumeric(sort.ignorecase.convert(a),sort.ignorecase.convert(b));
    275         };
    276         sort.ignorecase.convert = function(val) {
    277                 if (val==null) { return ""; }
    278                 return (""+val).toLowerCase();
    279         };
    280 
    281         // Currency Sort
    282         // -------------
    283         sort.currency = sort.numeric; // Just treat it as numeric!
    284         sort.currency_comma = sort.numeric_comma;
    285 
    286         // Date sort
    287         // ---------
    288         sort.date = function(a,b) {
    289                 return sort.numeric(sort.date.convert(a),sort.date.convert(b));
    290         };
    291         // Convert 2-digit years to 4
    292         sort.date.fixYear=function(yr) {
    293                 yr = +yr;
    294                 if (yr<50) { yr += 2000; }
    295                 else if (yr<100) { yr += 1900; }
    296                 return yr;
    297         };
    298         sort.date.formats = [
    299                 // YY[YY]-MM-DD
    300                 { re:/(\d{2,4})-(\d{1,2})-(\d{1,2})/ , f:function(x){ return (new Date(sort.date.fixYear(x[1]),+x[2],+x[3])).getTime(); } }
    301                 // MM/DD/YY[YY] or MM-DD-YY[YY]
    302                 ,{ re:/(\d{1,2})[\/-](\d{1,2})[\/-](\d{2,4})/ , f:function(x){ return (new Date(sort.date.fixYear(x[3]),+x[1],+x[2])).getTime(); } }
    303                 // Any catch-all format that new Date() can handle. This is not reliable except for long formats, for example: 31 Jan 2000 01:23:45 GMT
    304                 ,{ re:/(.*\d{4}.*\d+:\d+\d+.*)/, f:function(x){ var d=new Date(x[1]); if(d){return d.getTime();} } }
    305         ];
    306         sort.date.convert = function(val) {
    307                 var m,v, f = sort.date.formats;
    308                 for (var i=0,L=f.length; i<L; i++) {
    309                         if (m=val.match(f[i].re)) {
    310                                 v=f[i].f(m);
    311                                 if (typeof(v)!="undefined") { return v; }
    312                         }
    313                 }
    314                 return 9999999999999; // So non-parsed dates will be last, not first
    315         };
    316 
    317         return sort;
    318 })();
    319 
    320 /**
    321  * The main Table namespace
    322  */
    323 var Table = (function(){
    324 
    325         /**
    326          * Determine if a reference is defined
    327          */
    328         function def(o) {return (typeof o!="undefined");};
    329 
    330         /**
    331          * Determine if an object or class string contains a given class.
    332          */
    333         function hasClass(o,name) {
    334                 return new RegExp("(^|\\\s)"+name+"(\\\s|$)").test(o.className);
    335         };
    336 
    337         /**
    338          * Add a class to an object
    339          */
    340         function addClass(o,name) {
    341                 var c = o.className || "";
    342                 if (def(c) && !hasClass(o,name)) {
    343                         o.className += (c?" ":"") + name;
    344                 }
    345         };
    346 
    347         /**
    348          * Remove a class from an object
    349          */
    350         function removeClass(o,name) {
    351                 var c = o.className || "";
    352                 o.className = c.replace(new RegExp("(^|\\\s)"+name+"(\\\s|$)"),"$1");
    353         };
    354 
    355         /**
    356          * For classes that match a given substring, return the rest
    357          */
    358         function classValue(o,prefix) {
    359                 var c = o.className;
    360                 if (c.match(new RegExp("(^|\\\s)"+prefix+"([^ ]+)"))) {
    361                         return RegExp.$2;
    362                 }
    363                 return null;
    364         };
    365 
    366         /**
    367          * Return true if an object is hidden.
    368          * This uses the "russian doll" technique to unwrap itself to the most efficient
    369          * function after the first pass. This avoids repeated feature detection that
    370          * would always fall into the same block of code.
    371          */
    372          function isHidden(o) {
    373                 if (window.getComputedStyle) {
    374                         var cs = window.getComputedStyle;
    375                         return (isHidden = function(o) {
    376                                 return 'none'==cs(o,null).getPropertyValue('display');
    377                         })(o);
    378                 }
    379                 else if (window.currentStyle) {
    380                         return(isHidden = function(o) {
    381                                 return 'none'==o.currentStyle['display'];
    382                         })(o);
    383                 }
    384                 return (isHidden = function(o) {
    385                         return 'none'==o.style['display'];
    386                 })(o);
    387         };
    388 
    389         /**
    390          * Get a parent element by tag name, or the original element if it is of the tag type
    391          */
    392         function getParent(o,a,b) {
    393                 if (o!=null && o.nodeName) {
    394                         if (o.nodeName==a || (b && o.nodeName==b)) {
    395                                 return o;
    396                         }
    397                         while (o=o.parentNode) {
    398                                 if (o.nodeName && (o.nodeName==a || (b && o.nodeName==b))) {
    399                                         return o;
    400                                 }
    401                         }
    402                 }
    403                 return null;
    404         };
    405 
    406         /**
    407          * Utility function to copy properties from one object to another
    408          */
    409         function copy(o1,o2) {
    410                 for (var i=2;i<arguments.length; i++) {
    411                         var a = arguments[i];
    412                         if (def(o1[a])) {
    413                                 o2[a] = o1[a];
    414                         }
    415                 }
    416         }
    417 
    418         // The table object itself
    419         var table = {
    420                 //Class names used in the code
    421                 AutoStripeClassName:"table-autostripe",
    422                 StripeClassNamePrefix:"table-stripeclass:",
    423 
    424                 AutoSortClassName:"table-autosort",
    425                 AutoSortColumnPrefix:"table-autosort:",
    426                 AutoSortTitle:"Click to sort",
    427                 SortedAscendingClassName:"table-sorted-asc",
    428                 SortedDescendingClassName:"table-sorted-desc",
    429                 SortableClassName:"table-sortable",
    430                 SortableColumnPrefix:"table-sortable:",
    431                 NoSortClassName:"table-nosort",
    432 
    433                 AutoFilterClassName:"table-autofilter",
    434                 FilteredClassName:"table-filtered",
    435                 FilterableClassName:"table-filterable",
    436                 FilteredRowcountPrefix:"table-filtered-rowcount:",
    437                 RowcountPrefix:"table-rowcount:",
    438                 FilterAllLabel:"Filter: All",
    439 
    440                 AutoPageSizePrefix:"table-autopage:",
    441                 AutoPageJumpPrefix:"table-page:",
    442                 PageNumberPrefix:"table-page-number:",
    443                 PageCountPrefix:"table-page-count:"
    444         };
    445 
    446         /**
    447          * A place to store misc table information, rather than in the table objects themselves
    448          */
    449         table.tabledata = {};
    450 
    451         /**
    452          * Resolve a table given an element reference, and make sure it has a unique ID
    453          */
    454         table.uniqueId=1;
    455         table.resolve = function(o,args) {
    456                 if (o!=null && o.nodeName && o.nodeName!="TABLE") {
    457                         o = getParent(o,"TABLE");
    458                 }
    459                 if (o==null) { return null; }
    460                 if (!o.id) {
    461                         var id = null;
    462                         do { var id = "TABLE_"+(table.uniqueId++); }
    463                                 while (document.getElementById(id)!=null);
    464                         o.id = id;
    465                 }
    466                 this.tabledata[o.id] = this.tabledata[o.id] || {};
    467                 if (args) {
    468                         copy(args,this.tabledata[o.id],"stripeclass","ignorehiddenrows","useinnertext","sorttype","col","desc","page","pagesize");
    469                 }
    470                 return o;
    471         };
    472 
    473 
    474         /**
    475          * Run a function against each cell in a table header or footer, usually
    476          * to add or remove css classes based on sorting, filtering, etc.
    477          */
    478         table.processTableCells = function(t, type, func, arg) {
    479                 t = this.resolve(t);
    480                 if (t==null) { return; }
    481                 if (type!="TFOOT") {
    482                         this.processCells(t.tHead, func, arg);
    483                 }
    484                 if (type!="THEAD") {
    485                         this.processCells(t.tFoot, func, arg);
    486                 }
    487         };
    488 
    489         /**
    490          * Internal method used to process an arbitrary collection of cells.
    491          * Referenced by processTableCells.
    492          * It's done this way to avoid getElementsByTagName() which would also return nested table cells.
    493          */
    494         table.processCells = function(section,func,arg) {
    495                 if (section!=null) {
    496                         if (section.rows && section.rows.length && section.rows.length>0) {
    497                                 var rows = section.rows;
    498                                 for (var j=0,L2=rows.length; j<L2; j++) {
    499                                         var row = rows[j];
    500                                         if (row.cells && row.cells.length && row.cells.length>0) {
    501                                                 var cells = row.cells;
    502                                                 for (var k=0,L3=cells.length; k<L3; k++) {
    503                                                         var cellsK = cells[k];
    504                                                         func.call(this,cellsK,arg);
    505                                                 }
    506                                         }
    507                                 }
    508                         }
    509                 }
    510         };
    511 
    512         /**
    513          * Get the cellIndex value for a cell. This is only needed because of a Safari
    514          * bug that causes cellIndex to exist but always be 0.
    515          * Rather than feature-detecting each time it is called, the function will
    516          * re-write itself the first time it is called.
    517          */
    518         table.getCellIndex = function(td) {
    519                 var tr = td.parentNode;
    520                 var cells = tr.cells;
    521                 if (cells && cells.length) {
    522                         if (cells.length>1 && cells[cells.length-1].cellIndex>0) {
    523                                 // Define the new function, overwrite the one we're running now, and then run the new one
    524                                 (this.getCellIndex = function(td) {
    525                                         return td.cellIndex;
    526                                 })(td);
    527                         }
    528                         // Safari will always go through this slower block every time. Oh well.
    529                         for (var i=0,L=cells.length; i<L; i++) {
    530                                 if (tr.cells[i]==td) {
    531                                         return i;
    532                                 }
    533                         }
    534                 }
    535                 return 0;
    536         };
    537 
    538         /**
    539          * A map of node names and how to convert them into their "value" for sorting, filtering, etc.
    540          * These are put here so it is extensible.
    541          */
    542         table.nodeValue = {
    543                 'INPUT':function(node) {
    544                         if (def(node.value) && node.type && ((node.type!="checkbox" && node.type!="radio") || node.checked)) {
    545                                 return node.value;
    546                         }
    547                         return "";
    548                 },
    549                 'SELECT':function(node) {
    550                         if (node.selectedIndex>=0 && node.options) {
    551                                 // Sort select elements by the visible text
    552                                 return node.options[node.selectedIndex].text;
    553                         }
    554                         return "";
    555                 },
    556                 'IMG':function(node) {
    557                         return node.name || "";
    558                 }
    559         };
    560 
    561         /**
    562          * Get the text value of a cell. Only use innerText if explicitly told to, because
    563          * otherwise we want to be able to handle sorting on inputs and other types
    564          */
    565         table.getCellValue = function(td,useInnerText) {
    566                 if (useInnerText && def(td.innerText)) {
    567                         return td.innerText;
    568                 }
    569                 if (!td.childNodes) {
    570                         return "";
    571                 }
    572                 var childNodes=td.childNodes;
    573                 var ret = "";
    574                 for (var i=0,L=childNodes.length; i<L; i++) {
    575                         var node = childNodes[i];
    576                         var type = node.nodeType;
    577                         // In order to get realistic sort results, we need to treat some elements in a special way.
    578                         // These behaviors are defined in the nodeValue() object, keyed by node name
    579                         if (type==1) {
    580                                 var nname = node.nodeName;
    581                                 if (this.nodeValue[nname]) {
    582                                         ret += this.nodeValue[nname](node);
    583                                 }
    584                                 else {
    585                                         ret += this.getCellValue(node);
    586                                 }
    587                         }
    588                         else if (type==3) {
    589                                 if (def(node.innerText)) {
    590                                         ret += node.innerText;
    591                                 }
    592                                 else if (def(node.nodeValue)) {
    593                                         ret += node.nodeValue;
    594                                 }
    595                         }
    596                 }
    597                 return ret;
    598         };
    599 
    600         /**
    601          * Consider colspan and rowspan values in table header cells to calculate the actual cellIndex
    602          * of a given cell. This is necessary because if the first cell in row 0 has a rowspan of 2,
    603          * then the first cell in row 1 will have a cellIndex of 0 rather than 1, even though it really
    604          * starts in the second column rather than the first.
    605          * See: http://www.javascripttoolbox.com/temp/table_cellindex.html
    606          */
    607         table.tableHeaderIndexes = {};
    608         table.getActualCellIndex = function(tableCellObj) {
    609                 if (!def(tableCellObj.cellIndex)) { return null; }
    610                 var tableObj = getParent(tableCellObj,"TABLE");
    611                 var cellCoordinates = tableCellObj.parentNode.rowIndex+"-"+this.getCellIndex(tableCellObj);
    612 
    613                 // If it has already been computed, return the answer from the lookup table
    614                 if (def(this.tableHeaderIndexes[tableObj.id])) {
    615                         return this.tableHeaderIndexes[tableObj.id][cellCoordinates];
    616                 }
    617 
    618                 var matrix = [];
    619                 this.tableHeaderIndexes[tableObj.id] = {};
    620                 var thead = getParent(tableCellObj,"THEAD");
    621                 var trs = thead.getElementsByTagName('TR');
    622 
    623                 // Loop thru every tr and every cell in the tr, building up a 2-d array "grid" that gets
    624                 // populated with an "x" for each space that a cell takes up. If the first cell is colspan
    625                 // 2, it will fill in values [0] and [1] in the first array, so that the second cell will
    626                 // find the first empty cell in the first row (which will be [2]) and know that this is
    627                 // where it sits, rather than its internal .cellIndex value of [1].
    628                 for (var i=0; i<trs.length; i++) {
    629                         var cells = trs[i].cells;
    630                         for (var j=0; j<cells.length; j++) {
    631                                 var c = cells[j];
    632                                 var rowIndex = c.parentNode.rowIndex;
    633                                 var cellId = rowIndex+"-"+this.getCellIndex(c);
    634                                 var rowSpan = c.rowSpan || 1;
    635                                 var colSpan = c.colSpan || 1;
    636                                 var firstAvailCol;
    637                                 if(!def(matrix[rowIndex])) {
    638                                         matrix[rowIndex] = [];
    639                                 }
    640                                 var m = matrix[rowIndex];
    641                                 // Find first available column in the first row
    642                                 for (var k=0; k<m.length+1; k++) {
    643                                         if (!def(m[k])) {
    644                                                 firstAvailCol = k;
    645                                                 break;
    646                                         }
    647                                 }
    648                                 this.tableHeaderIndexes[tableObj.id][cellId] = firstAvailCol;
    649                                 for (var k=rowIndex; k<rowIndex+rowSpan; k++) {
    650                                         if(!def(matrix[k])) {
    651                                                 matrix[k] = [];
    652                                         }
    653                                         var matrixrow = matrix[k];
    654                                         for (var l=firstAvailCol; l<firstAvailCol+colSpan; l++) {
    655                                                 matrixrow[l] = "x";
    656                                         }
    657                                 }
    658                         }
    659                 }
    660                 // Store the map so future lookups are fast.
    661                 return this.tableHeaderIndexes[tableObj.id][cellCoordinates];
    662         };
    663 
    664         /**
    665          * Sort all rows in each TBODY (tbodies are sorted independent of each other)
    666          */
    667         table.sort = function(o,args) {
    668                 var t, tdata, sortconvert=null;
    669                 // Allow for a simple passing of sort type as second parameter
    670                 if (typeof(args)=="function") {
    671                         args={sorttype:args};
    672                 }
    673                 args = args || {};
    674 
    675                 // If no col is specified, deduce it from the object sent in
    676                 if (!def(args.col)) {
    677                         args.col = this.getActualCellIndex(o) || 0;
    678                 }
    679                 // If no sort type is specified, default to the default sort
    680                 args.sorttype = args.sorttype || Sort['default'];
    681 
    682                 // Resolve the table
    683                 t = this.resolve(o,args);
    684                 tdata = this.tabledata[t.id];
    685 
    686                 // If we are sorting on the same column as last time, flip the sort direction
    687                 if (def(tdata.lastcol) && tdata.lastcol==tdata.col && def(tdata.lastdesc)) {
    688                         tdata.desc = !tdata.lastdesc;
    689                 }
    690                 else {
    691                         tdata.desc = !!args.desc;
    692                 }
    693 
    694                 // Store the last sorted column so clicking again will reverse the sort order
    695                 tdata.lastcol=tdata.col;
    696                 tdata.lastdesc=!!tdata.desc;
    697 
    698                 // If a sort conversion function exists, pre-convert cell values and then use a plain alphanumeric sort
    699                 var sorttype = tdata.sorttype;
    700                 if (typeof(sorttype.convert)=="function") {
    701                         sortconvert=tdata.sorttype.convert;
    702                         sorttype=Sort.alphanumeric;
    703                 }
    704 
    705                 // Loop through all THEADs and remove sorted class names, then re-add them for the col
    706                 // that is being sorted
    707                 this.processTableCells(t,"THEAD",
    708                         function(cell) {
    709                                 if (hasClass(cell,this.SortableClassName)) {
    710                                         removeClass(cell,this.SortedAscendingClassName);
    711                                         removeClass(cell,this.SortedDescendingClassName);
    712                                         // If the computed colIndex of the cell equals the sorted colIndex, flag it as sorted
    713                                         if (tdata.col==table.getActualCellIndex(cell) && (classValue(cell,table.SortableClassName))) {
    714                                                 addClass(cell,tdata.desc?this.SortedAscendingClassName:this.SortedDescendingClassName);
    715                                         }
    716                                 }
    717                         }
    718                 );
    719 
    720                 // Sort each tbody independently
    721                 var bodies = t.tBodies;
    722                 if (bodies==null || bodies.length==0) { return; }
    723 
    724                 // Define a new sort function to be called to consider descending or not
    725                 var newSortFunc = (tdata.desc)?
    726                         function(a,b){return sorttype(b[0],a[0]);}
    727                         :function(a,b){return sorttype(a[0],b[0]);};
    728 
    729                 var useinnertext=!!tdata.useinnertext;
    730                 var col = tdata.col;
    731 
    732                 for (var i=0,L=bodies.length; i<L; i++) {
    733                         var tb = bodies[i], tbrows = tb.rows, rows = [];
    734 
    735                         // Allow tbodies to request that they not be sorted
    736                         if(!hasClass(tb,table.NoSortClassName)) {
    737                                 // Create a separate array which will store the converted values and refs to the
    738                                 // actual rows. This is the array that will be sorted.
    739                                 var cRow, cRowIndex=0;
    740                                 if (cRow=tbrows[cRowIndex]){
    741                                         // Funky loop style because it's considerably faster in IE
    742                                         do {
    743                                                 if (rowCells = cRow.cells) {
    744                                                         var cellValue = (col<rowCells.length)?this.getCellValue(rowCells[col],useinnertext):null;
    745                                                         if (sortconvert) cellValue = sortconvert(cellValue);
    746                                                         rows[cRowIndex] = [cellValue,tbrows[cRowIndex]];
    747                                                 }
    748                                         } while (cRow=tbrows[++cRowIndex])
    749                                 }
    750 
    751                                 // Do the actual sorting
    752                                 rows.sort(newSortFunc);
    753 
    754                                 // Move the rows to the correctly sorted order. Appending an existing DOM object just moves it!
    755                                 cRowIndex=0;
    756                                 var displayedCount=0;
    757                                 var f=[removeClass,addClass];
    758                                 if (cRow=rows[cRowIndex]){
    759                                         do {
    760                                                 tb.appendChild(cRow[1]);
    761                                         } while (cRow=rows[++cRowIndex])
    762                                 }
    763                         }
    764                 }
    765 
    766                 // If paging is enabled on the table, then we need to re-page because the order of rows has changed!
    767                 if (tdata.pagesize) {
    768                         this.page(t); // This will internally do the striping
    769                 }
    770                 else {
    771                         // Re-stripe if a class name was supplied
    772                         if (tdata.stripeclass) {
    773                                 this.stripe(t,tdata.stripeclass,!!tdata.ignorehiddenrows);
    774                         }
    775                 }
    776         };
    777 
    778         /**
    779         * Apply a filter to rows in a table and hide those that do not match.
    780         */
    781         table.filter = function(o,filters,args) {
    782                 var cell;
    783                 args = args || {};
    784 
    785                 var t = this.resolve(o,args);
    786                 var tdata = this.tabledata[t.id];
    787 
    788                 // If new filters were passed in, apply them to the table's list of filters
    789                 if (!filters) {
    790                         // If a null or blank value was sent in for 'filters' then that means reset the table to no filters
    791                         tdata.filters = null;
    792                 }
    793                 else {
    794                         // Allow for passing a select list in as the filter, since this is common design
    795                         if (filters.nodeName=="SELECT" && filters.type=="select-one" && filters.selectedIndex>-1) {
    796                                 filters={ 'filter':filters.options[filters.selectedIndex].value };
    797                         }
    798                         // Also allow for a regular input
    799                         if (filters.nodeName=="INPUT" && filters.type=="text") {
    800                                 filters={ 'filter':"/"+filters.value+"/" };
    801                         }
    802                         // Force filters to be an array
    803                         if (typeof(filters)=="object" && !filters.length) {
    804                                 filters = [filters];
    805                         }
    806 
    807                         // Convert regular expression strings to RegExp objects and function strings to function objects
    808                         for (var i=0,L=filters.length; i<L; i++) {
    809                                 var filter = filters[i];
    810                                 if (typeof(filter.filter)=="string") {
    811                                         // If a filter string is like "/expr/" then turn it into a Regex
    812                                         if (filter.filter.match(/^\/(.*)\/$/)) {
    813                                                 filter.filter = new RegExp(RegExp.$1);
    814                                                 filter.filter.regex=true;
    815                                         }
    816                                         // If filter string is like "function (x) { ... }" then turn it into a function
    817                                         else if (filter.filter.match(/^function\s*\(([^\)]*)\)\s*\{(.*)}\s*$/)) {
    818                                                 filter.filter = Function(RegExp.$1,RegExp.$2);
    819                                         }
    820                                 }
    821                                 // If some non-table object was passed in rather than a 'col' value, resolve it
    822                                 // and assign it's column index to the filter if it doesn't have one. This way,
    823                                 // passing in a cell reference or a select object etc instead of a table object
    824                                 // will automatically set the correct column to filter.
    825                                 if (filter && !def(filter.col) && (cell=getParent(o,"TD","TH"))) {
    826                                         filter.col = this.getCellIndex(cell);
    827                                 }
    828 
    829                                 // Apply the passed-in filters to the existing list of filters for the table, removing those that have a filter of null or ""
    830                                 if ((!filter || !filter.filter) && tdata.filters) {
    831                                         delete tdata.filters[filter.col];
    832                                 }
    833                                 else {
    834                                         tdata.filters = tdata.filters || {};
    835                                         tdata.filters[filter.col] = filter.filter;
    836                                 }
    837                         }
    838                         // If no more filters are left, then make sure to empty out the filters object
    839                         for (var j in tdata.filters) { var keep = true; }
    840                         if (!keep) {
    841                                 tdata.filters = null;
    842                         }
    843                 }
    844                 // Everything's been setup, so now scrape the table rows
    845                 return table.scrape(o);
    846         };
    847 
    848         /**
    849          * "Page" a table by showing only a subset of the rows
    850          */
    851         table.page = function(t,page,args) {
    852                 args = args || {};
    853                 if (def(page)) { args.page = page; }
    854                 return table.scrape(t,args);
    855         };
    856 
    857         /**
    858          * Jump forward or back any number of pages
    859          */
    860         table.pageJump = function(t,count,args) {
    861                 t = this.resolve(t,args);
    862                 return this.page(t,(table.tabledata[t.id].page||0)+count,args);
    863         };
    864 
    865         /**
    866          * Go to the next page of a paged table
    867          */
    868         table.pageNext = function(t,args) {
    869                 return this.pageJump(t,1,args);
    870         };
    871 
    872         /**
    873          * Go to the previous page of a paged table
    874          */
    875         table.pagePrevious = function(t,args) {
    876                 return this.pageJump(t,-1,args);
    877         };
    878 
    879         /**
    880         * Scrape a table to either hide or show each row based on filters and paging
    881         */
    882         table.scrape = function(o,args) {
    883                 var col,cell,filterList,filterReset=false,filter;
    884                 var page,pagesize,pagestart,pageend;
    885                 var unfilteredrows=[],unfilteredrowcount=0,totalrows=0;
    886                 var t,tdata,row,hideRow;
    887                 args = args || {};
    888 
    889                 // Resolve the table object
    890                 t = this.resolve(o,args);
    891                 tdata = this.tabledata[t.id];
    892 
    893                 // Setup for Paging
    894                 var page = tdata.page;
    895                 if (def(page)) {
    896                         // Don't let the page go before the beginning
    897                         if (page<0) { tdata.page=page=0; }
    898                         pagesize = tdata.pagesize || 25; // 25=arbitrary default
    899                         pagestart = page*pagesize+1;
    900                         pageend = pagestart + pagesize - 1;
    901                 }
    902 
    903                 // Scrape each row of each tbody
    904                 var bodies = t.tBodies;
    905                 if (bodies==null || bodies.length==0) { return; }
    906                 for (var i=0,L=bodies.length; i<L; i++) {
    907                         var tb = bodies[i];
    908                         for (var j=0,L2=tb.rows.length; j<L2; j++) {
    909                                 row = tb.rows[j];
    910                                 hideRow = false;
    911 
    912                                 // Test if filters will hide the row
    913                                 if (tdata.filters && row.cells) {
    914                                         var cells = row.cells;
    915                                         var cellsLength = cells.length;
    916                                         // Test each filter
    917                                         for (col in tdata.filters) {
    918                                                 if (!hideRow) {
    919                                                         filter = tdata.filters[col];
    920                                                         if (filter && col<cellsLength) {
    921                                                                 var val = this.getCellValue(cells[col]);
    922                                                                 if (filter.regex && val.search) {
    923                                                                         hideRow=(val.search(filter)<0);
    924                                                                 }
    925                                                                 else if (typeof(filter)=="function") {
    926                                                                         hideRow=!filter(val,cells[col]);
    927                                                                 }
    928                                                                 else {
    929                                                                         hideRow = (val!=filter);
    930                                                                 }
    931                                                         }
    932                                                 }
    933                                         }
    934                                 }
    935 
    936                                 // Keep track of the total rows scanned and the total runs _not_ filtered out
    937                                 totalrows++;
    938                                 if (!hideRow) {
    939                                         unfilteredrowcount++;
    940                                         if (def(page)) {
    941                                                 // Temporarily keep an array of unfiltered rows in case the page we're on goes past
    942                                                 // the last page and we need to back up. Don't want to filter again!
    943                                                 unfilteredrows.push(row);
    944                                                 if (unfilteredrowcount<pagestart || unfilteredrowcount>pageend) {
    945                                                         hideRow = true;
    946                                                 }
    947                                         }
    948                                 }
    949 
    950                                 row.style.display = hideRow?"none":"";
    951                         }
    952                 }
    953 
    954                 if (def(page)) {
    955                         // Check to see if filtering has put us past the requested page index. If it has,
    956                         // then go back to the last page and show it.
    957                         if (pagestart>=unfilteredrowcount) {
    958                                 pagestart = unfilteredrowcount-(unfilteredrowcount%pagesize);
    959                                 tdata.page = page = pagestart/pagesize;
    960                                 for (var i=pagestart,L=unfilteredrows.length; i<L; i++) {
    961                                         unfilteredrows[i].style.display="";
    962                                 }
    963                         }
    964                 }
    965 
    966                 // Loop through all THEADs and add/remove filtered class names
    967                 this.processTableCells(t,"THEAD",
    968                         function(c) {
    969                                 ((tdata.filters && def(tdata.filters[table.getCellIndex(c)]) && hasClass(c,table.FilterableClassName))?addClass:removeClass)(c,table.FilteredClassName);
    970                         }
    971                 );
    972 
    973                 // Stripe the table if necessary
    974                 if (tdata.stripeclass) {
    975                         this.stripe(t);
    976                 }
    977 
    978                 // Calculate some values to be returned for info and updating purposes
    979                 var pagecount = Math.floor(unfilteredrowcount/pagesize)+1;
    980                 if (def(page)) {
    981                         // Update the page number/total containers if they exist
    982                         if (tdata.container_number) {
    983                                 tdata.container_number.innerHTML = page+1;
    984                         }
    985                         if (tdata.container_count) {
    986                                 tdata.container_count.innerHTML = pagecount;
    987                         }
    988                 }
    989 
    990                 // Update the row count containers if they exist
    991                 if (tdata.container_filtered_count) {
    992                         tdata.container_filtered_count.innerHTML = unfilteredrowcount;
    993                 }
    994                 if (tdata.container_all_count) {
    995                         tdata.container_all_count.innerHTML = totalrows;
    996                 }
    997                 return { 'data':tdata, 'unfilteredcount':unfilteredrowcount, 'total':totalrows, 'pagecount':pagecount, 'page':page, 'pagesize':pagesize };
    998         };
    999 
   1000         /**
   1001          * Shade alternate rows, aka Stripe the table.
   1002          */
   1003         table.stripe = function(t,className,args) {
   1004                 args = args || {};
   1005                 args.stripeclass = className;
   1006 
   1007                 t = this.resolve(t,args);
   1008                 var tdata = this.tabledata[t.id];
   1009 
   1010                 var bodies = t.tBodies;
   1011                 if (bodies==null || bodies.length==0) {
   1012                         return;
   1013                 }
   1014 
   1015                 className = tdata.stripeclass;
   1016                 // Cache a shorter, quicker reference to either the remove or add class methods
   1017                 var f=[removeClass,addClass];
   1018                 for (var i=0,L=bodies.length; i<L; i++) {
   1019                         var tb = bodies[i], tbrows = tb.rows, cRowIndex=0, cRow, displayedCount=0;
   1020                         if (cRow=tbrows[cRowIndex]){
   1021                                 // The ignorehiddenrows test is pulled out of the loop for a slight speed increase.
   1022                                 // Makes a bigger difference in FF than in IE.
   1023                                 // In this case, speed always wins over brevity!
   1024                                 if (tdata.ignoreHiddenRows) {
   1025                                         do {
   1026                                                 f[displayedCount++%2](cRow,className);
   1027                                         } while (cRow=tbrows[++cRowIndex])
   1028                                 }
   1029                                 else {
   1030                                         do {
   1031                                                 if (!isHidden(cRow)) {
   1032                                                         f[displayedCount++%2](cRow,className);
   1033                                                 }
   1034                                         } while (cRow=tbrows[++cRowIndex])
   1035                                 }
   1036                         }
   1037                 }
   1038         };
   1039 
   1040         /**
   1041          * Build up a list of unique values in a table column
   1042          */
   1043         table.getUniqueColValues = function(t,col) {
   1044                 var values={}, bodies = this.resolve(t).tBodies;
   1045                 for (var i=0,L=bodies.length; i<L; i++) {
   1046                         var tbody = bodies[i];
   1047                         for (var r=0,L2=tbody.rows.length; r<L2; r++) {
   1048                                 values[this.getCellValue(tbody.rows[r].cells[col])] = true;
   1049                         }
   1050                 }
   1051                 var valArray = [];
   1052                 for (var val in values) {
   1053                         valArray.push(val);
   1054                 }
   1055                 return valArray.sort();
   1056         };
   1057 
   1058         /**
   1059          * Scan the document on load and add sorting, filtering, paging etc ability automatically
   1060          * based on existence of class names on the table and cells.
   1061          */
   1062         table.auto = function(args) {
   1063                 var cells = [], tables = document.getElementsByTagName("TABLE");
   1064                 var val,tdata;
   1065                 if (tables!=null) {
   1066                         for (var i=0,L=tables.length; i<L; i++) {
   1067                                 var t = table.resolve(tables[i]);
   1068                                 tdata = table.tabledata[t.id];
   1069                                 if (val=classValue(t,table.StripeClassNamePrefix)) {
   1070                                         tdata.stripeclass=val;
   1071                                 }
   1072                                 // Do auto-filter if necessary
   1073                                 if (hasClass(t,table.AutoFilterClassName)) {
   1074                                         table.autofilter(t);
   1075                                 }
   1076                                 // Do auto-page if necessary
   1077                                 if (val = classValue(t,table.AutoPageSizePrefix)) {
   1078                                         table.autopage(t,{'pagesize':+val});
   1079                                 }
   1080                                 // Do auto-sort if necessary
   1081                                 if ((val = classValue(t,table.AutoSortColumnPrefix)) || (hasClass(t,table.AutoSortClassName))) {
   1082                                         table.autosort(t,{'col':(val==null)?null:+val});
   1083                                 }
   1084                                 // Do auto-stripe if necessary
   1085                                 if (tdata.stripeclass && hasClass(t,table.AutoStripeClassName)) {
   1086                                         table.stripe(t);
   1087                                 }
   1088                         }
   1089                 }
   1090         };
   1091 
   1092         /**
   1093          * Add sorting functionality to a table header cell
   1094          */
   1095         table.autosort = function(t,args) {
   1096                 t = this.resolve(t,args);
   1097                 var tdata = this.tabledata[t.id];
   1098                 this.processTableCells(t, "THEAD", function(c) {
   1099                         var type = classValue(c,table.SortableColumnPrefix);
   1100                         if (type!=null) {
   1101                                 type = type || "default";
   1102                                 c.title =c.title || table.AutoSortTitle;
   1103                                 addClass(c,table.SortableClassName);
   1104                                 c.onclick = Function("","Table.sort(this,{'sorttype':Sort['"+type+"']})");
   1105                                 // If we are going to auto sort on a column, we need to keep track of what kind of sort it will be
   1106                                 if (args.col!=null) {
   1107                                         if (args.col==table.getActualCellIndex(c)) {
   1108                                                 tdata.sorttype=Sort['"+type+"'];
   1109                                         }
   1110                                 }
   1111                         }
   1112                 } );
   1113                 if (args.col!=null) {
   1114                         table.sort(t,args);
   1115                 }
   1116         };
   1117 
   1118         /**
   1119          * Add paging functionality to a table
   1120          */
   1121         table.autopage = function(t,args) {
   1122                 t = this.resolve(t,args);
   1123                 var tdata = this.tabledata[t.id];
   1124                 if (tdata.pagesize) {
   1125                         this.processTableCells(t, "THEAD,TFOOT", function(c) {
   1126                                 var type = classValue(c,table.AutoPageJumpPrefix);
   1127                                 if (type=="next") { type = 1; }
   1128                                 else if (type=="previous") { type = -1; }
   1129                                 if (type!=null) {
   1130                                         c.onclick = Function("","Table.pageJump(this,"+type+")");
   1131                                 }
   1132                         } );
   1133                         if (val = classValue(t,table.PageNumberPrefix)) {
   1134                                 tdata.container_number = document.getElementById(val);
   1135                         }
   1136                         if (val = classValue(t,table.PageCountPrefix)) {
   1137                                 tdata.container_count = document.getElementById(val);
   1138                         }
   1139                         return table.page(t,0,args);
   1140                 }
   1141         };
   1142 
   1143         /**
   1144          * A util function to cancel bubbling of clicks on filter dropdowns
   1145          */
   1146         table.cancelBubble = function(e) {
   1147                 e = e || window.event;
   1148                 if (typeof(e.stopPropagation)=="function") { e.stopPropagation(); }
   1149                 if (def(e.cancelBubble)) { e.cancelBubble = true; }
   1150         };
   1151 
   1152         /**
   1153          * Auto-filter a table
   1154          */
   1155         table.autofilter = function(t,args) {
   1156                 args = args || {};
   1157                 t = this.resolve(t,args);
   1158                 var tdata = this.tabledata[t.id],val;
   1159                 table.processTableCells(t, "THEAD", function(cell) {
   1160                         if (hasClass(cell,table.FilterableClassName)) {
   1161                                 var cellIndex = table.getCellIndex(cell);
   1162                                 var colValues = table.getUniqueColValues(t,cellIndex);
   1163                                 if (colValues.length>0) {
   1164                                         if (typeof(args.insert)=="function") {
   1165                                                 func.insert(cell,colValues);
   1166                                         }
   1167                                         else {
   1168                                                 var sel = '<select onchange="Table.filter(this,this)" onclick="Table.cancelBubble(event)" class="'+table.AutoFilterClassName+'"><option value="">'+table.FilterAllLabel+'</option>';
   1169                                                 for (var i=0; i<colValues.length; i++) {
   1170                                                         sel += '<option value="'+colValues[i]+'">'+colValues[i]+'</option>';
   1171                                                 }
   1172                                                 sel += '</select>';
   1173                                                 cell.innerHTML += "<br>"+sel;
   1174                                         }
   1175                                 }
   1176                         }
   1177                 });
   1178                 if (val = classValue(t,table.FilteredRowcountPrefix)) {
   1179                         tdata.container_filtered_count = document.getElementById(val);
   1180                 }
   1181                 if (val = classValue(t,table.RowcountPrefix)) {
   1182                         tdata.container_all_count = document.getElementById(val);
   1183                 }
   1184         };
   1185 
   1186         /**
   1187          * Attach the auto event so it happens on load.
   1188          * use jQuery's ready() function if available
   1189          */
   1190         if (typeof(jQuery)!="undefined") {
   1191                 jQuery(table.auto);
   1192         }
   1193         else if (window.addEventListener) {
   1194                 window.addEventListener( "load", table.auto, false );
   1195         }
   1196         else if (window.attachEvent) {
   1197                 window.attachEvent( "onload", table.auto );
   1198         }
   1199 
   1200         return table;
   1201 })();
   1202 """
   1203 
   1204 
   1205 maketree_js = """/**
   1206  * Copyright (c)2005-2007 Matt Kruse (javascripttoolbox.com)
   1207  *
   1208  * Dual licensed under the MIT and GPL licenses.
   1209  * This basically means you can use this code however you want for
   1210  * free, but don't claim to have written it yourself!
   1211  * Donations always accepted: http://www.JavascriptToolbox.com/donate/
   1212  *
   1213  * Please do not link to the .js files on javascripttoolbox.com from
   1214  * your site. Copy the files locally to your server instead.
   1215  *
   1216  */
   1217 /*
   1218 This code is inspired by and extended from Stuart Langridge's aqlist code:
   1219     http://www.kryogenix.org/code/browser/aqlists/
   1220     Stuart Langridge, November 2002
   1221     sil (at] kryogenix.org
   1222     Inspired by Aaron's labels.js (http://youngpup.net/demos/labels/)
   1223     and Dave Lindquist's menuDropDown.js (http://www.gazingus.org/dhtml/?id=109)
   1224 */
   1225 
   1226 // Automatically attach a listener to the window onload, to convert the trees
   1227 addEvent(window,"load",convertTrees);
   1228 
   1229 // Utility function to add an event listener
   1230 function addEvent(o,e,f){
   1231   if (o.addEventListener){ o.addEventListener(e,f,false); return true; }
   1232   else if (o.attachEvent){ return o.attachEvent("on"+e,f); }
   1233   else { return false; }
   1234 }
   1235 
   1236 // utility function to set a global variable if it is not already set
   1237 function setDefault(name,val) {
   1238   if (typeof(window[name])=="undefined" || window[name]==null) {
   1239     window[name]=val;
   1240   }
   1241 }
   1242 
   1243 // Full expands a tree with a given ID
   1244 function expandTree(treeId) {
   1245   var ul = document.getElementById(treeId);
   1246   if (ul == null) { return false; }
   1247   expandCollapseList(ul,nodeOpenClass);
   1248 }
   1249 
   1250 // Fully collapses a tree with a given ID
   1251 function collapseTree(treeId) {
   1252   var ul = document.getElementById(treeId);
   1253   if (ul == null) { return false; }
   1254   expandCollapseList(ul,nodeClosedClass);
   1255 }
   1256 
   1257 // Expands enough nodes to expose an LI with a given ID
   1258 function expandToItem(treeId,itemId) {
   1259   var ul = document.getElementById(treeId);
   1260   if (ul == null) { return false; }
   1261   var ret = expandCollapseList(ul,nodeOpenClass,itemId);
   1262   if (ret) {
   1263     var o = document.getElementById(itemId);
   1264     if (o.scrollIntoView) {
   1265       o.scrollIntoView(false);
   1266     }
   1267   }
   1268 }
   1269 
   1270 // Performs 3 functions:
   1271 // a) Expand all nodes
   1272 // b) Collapse all nodes
   1273 // c) Expand all nodes to reach a certain ID
   1274 function expandCollapseList(ul,cName,itemId) {
   1275   if (!ul.childNodes || ul.childNodes.length==0) { return false; }
   1276   // Iterate LIs
   1277   for (var itemi=0;itemi<ul.childNodes.length;itemi++) {
   1278     var item = ul.childNodes[itemi];
   1279     if (itemId!=null && item.id==itemId) { return true; }
   1280     if (item.nodeName == "LI") {
   1281       // Iterate things in this LI
   1282       var subLists = false;
   1283       for (var sitemi=0;sitemi<item.childNodes.length;sitemi++) {
   1284         var sitem = item.childNodes[sitemi];
   1285         if (sitem.nodeName=="UL") {
   1286           subLists = true;
   1287           var ret = expandCollapseList(sitem,cName,itemId);
   1288           if (itemId!=null && ret) {
   1289             item.className=cName;
   1290             return true;
   1291           }
   1292         }
   1293       }
   1294       if (subLists && itemId==null) {
   1295         item.className = cName;
   1296       }
   1297     }
   1298   }
   1299 }
   1300 
   1301 // Search the document for UL elements with the correct CLASS name, then process them
   1302 function convertTrees() {
   1303   setDefault("treeClass","mktree");
   1304   setDefault("nodeClosedClass","liClosed");
   1305   setDefault("nodeOpenClass","liOpen");
   1306   setDefault("nodeBulletClass","liBullet");
   1307   setDefault("nodeLinkClass","bullet");
   1308   setDefault("preProcessTrees",true);
   1309   if (preProcessTrees) {
   1310     if (!document.createElement) { return; } // Without createElement, we can't do anything
   1311     var uls = document.getElementsByTagName("ul");
   1312     if (uls==null) { return; }
   1313     var uls_length = uls.length;
   1314     for (var uli=0;uli<uls_length;uli++) {
   1315       var ul=uls[uli];
   1316       if (ul.nodeName=="UL" && ul.className==treeClass) {
   1317         processList(ul);
   1318       }
   1319     }
   1320   }
   1321 }
   1322 
   1323 function treeNodeOnclick() {
   1324   this.parentNode.className = (this.parentNode.className==nodeOpenClass) ? nodeClosedClass : nodeOpenClass;
   1325   return false;
   1326 }
   1327 function retFalse() {
   1328   return false;
   1329 }
   1330 // Process a UL tag and all its children, to convert to a tree
   1331 function processList(ul) {
   1332   if (!ul.childNodes || ul.childNodes.length==0) { return; }
   1333   // Iterate LIs
   1334   var childNodesLength = ul.childNodes.length;
   1335   for (var itemi=0;itemi<childNodesLength;itemi++) {
   1336     var item = ul.childNodes[itemi];
   1337     if (item.nodeName == "LI") {
   1338       // Iterate things in this LI
   1339       var subLists = false;
   1340       var itemChildNodesLength = item.childNodes.length;
   1341       for (var sitemi=0;sitemi<itemChildNodesLength;sitemi++) {
   1342         var sitem = item.childNodes[sitemi];
   1343         if (sitem.nodeName=="UL") {
   1344           subLists = true;
   1345           processList(sitem);
   1346         }
   1347       }
   1348       var s= document.createElement("SPAN");
   1349       var t= '\u00A0'; // &nbsp;
   1350       s.className = nodeLinkClass;
   1351       if (subLists) {
   1352         // This LI has UL's in it, so it's a +/- node
   1353         if (item.className==null || item.className=="") {
   1354           item.className = nodeClosedClass;
   1355         }
   1356         // If it's just text, make the text work as the link also
   1357         if (item.firstChild.nodeName=="#text") {
   1358           t = t+item.firstChild.nodeValue;
   1359           item.removeChild(item.firstChild);
   1360         }
   1361         s.onclick = treeNodeOnclick;
   1362       }
   1363       else {
   1364         // No sublists, so it's just a bullet node
   1365         item.className = nodeBulletClass;
   1366         s.onclick = retFalse;
   1367       }
   1368       s.appendChild(document.createTextNode(t));
   1369       item.insertBefore(s,item.firstChild);
   1370     }
   1371   }
   1372 }
   1373 """
   1374 
   1375 
   1376 
   1377 
   1378 def make_html_file(metadata, results, tag, host, output_file_name, dirname):
   1379     """
   1380     Create HTML file contents for the job report, to stdout or filesystem.
   1381 
   1382     @param metadata: Dictionary with Job metadata (tests, exec time, etc).
   1383     @param results: List with testcase results.
   1384     @param tag: Job tag.
   1385     @param host: Client hostname.
   1386     @param output_file_name: Output file name. If empty string, prints to
   1387             stdout.
   1388     @param dirname: Prefix for HTML links. If empty string, the HTML links
   1389             will be relative to the results dir.
   1390     """
   1391     html_prefix = """
   1392 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
   1393 <html>
   1394 <head>
   1395 <title>Autotest job execution results</title>
   1396 <style type="text/css">
   1397 %s
   1398 </style>
   1399 <script type="text/javascript">
   1400 %s
   1401 %s
   1402 function popup(tag,text) {
   1403 var w = window.open('', tag, 'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes, copyhistory=no,width=600,height=300,top=20,left=100');
   1404 w.document.open("text/html", "replace");
   1405 w.document.write(text);
   1406 w.document.close();
   1407 return true;
   1408 }
   1409 </script>
   1410 </head>
   1411 <body>
   1412 """ % (format_css, table_js, maketree_js)
   1413 
   1414     if output_file_name:
   1415         output = open(output_file_name, "w")
   1416     else:   #if no output file defined, print html file to console
   1417         output = sys.stdout
   1418     # create html page
   1419     print >> output, html_prefix
   1420     print >> output, '<h2 id=\"page_title\">Autotest job execution report</h2>'
   1421 
   1422     # formating date and time to print
   1423     t = datetime.datetime.now()
   1424 
   1425     epoch_sec = time.mktime(t.timetuple())
   1426     now = datetime.datetime.fromtimestamp(epoch_sec)
   1427 
   1428     # basic statistics
   1429     total_executed = 0
   1430     total_failed = 0
   1431     total_passed = 0
   1432     for res in results:
   1433         if results[res][2] != None:
   1434             total_executed += 1
   1435             if results[res][2]['status'] == 'GOOD':
   1436                 total_passed += 1
   1437             else:
   1438                 total_failed += 1
   1439     stat_str = 'No test cases executed'
   1440     if total_executed > 0:
   1441         failed_perct = int(float(total_failed)/float(total_executed)*100)
   1442         stat_str = ('From %d tests executed, %d have passed (%d%% failures)' %
   1443                     (total_executed, total_passed, failed_perct))
   1444 
   1445     kvm_ver_str = metadata.get('kvmver', None)
   1446 
   1447     print >> output, '<table class="stats2">'
   1448     print >> output, '<tr><td>HOST</td><td>:</td><td>%s</td></tr>' % host
   1449     print >> output, '<tr><td>RESULTS DIR</td><td>:</td><td>%s</td></tr>'  % tag
   1450     print >> output, '<tr><td>DATE</td><td>:</td><td>%s</td></tr>' % now.ctime()
   1451     print >> output, '<tr><td>STATS</td><td>:</td><td>%s</td></tr>'% stat_str
   1452     print >> output, '<tr><td></td><td></td><td></td></tr>'
   1453     if kvm_ver_str is not None:
   1454         print >> output, '<tr><td>KVM VERSION</td><td>:</td><td>%s</td></tr>' % kvm_ver_str
   1455     print >> output, '</table>'
   1456 
   1457     ## print test results
   1458     print >> output, '<br>'
   1459     print >> output, '<h2 id=\"page_sub_title\">Test Results</h2>'
   1460     print >> output, '<h2 id=\"comment\">click on table headers to asc/desc sort</h2>'
   1461     result_table_prefix = """<table
   1462 id="t1" class="stats table-autosort:4 table-autofilter table-stripeclass:alternate table-page-number:t1page table-page-count:t1pages table-filtered-rowcount:t1filtercount table-rowcount:t1allcount">
   1463 <thead class="th table-sorted-asc table-sorted-desc">
   1464 <tr>
   1465 <th align="left" class="table-sortable:alphanumeric">Date/Time</th>
   1466 <th align="left" class="filterable table-sortable:alphanumeric">Test Case<br><input name="tc_filter" size="10" onkeyup="Table.filter(this,this)" onclick="Table.cancelBubble(event)"></th>
   1467 <th align="left" class="table-filterable table-sortable:alphanumeric">Status</th>
   1468 <th align="left">Time (sec)</th>
   1469 <th align="left">Info</th>
   1470 <th align="left">Debug</th>
   1471 </tr></thead>
   1472 <tbody>
   1473 """
   1474     print >> output, result_table_prefix
   1475     def print_result(result, indent):
   1476         while result != []:
   1477             r = result.pop(0)
   1478             res = results[r][2]
   1479             print >> output, '<tr>'
   1480             print >> output, '<td align="left">%s</td>' % res['time']
   1481             print >> output, '<td align="left" style="padding-left:%dpx">%s</td>' % (indent * 20, res['title'])
   1482             if res['status'] == 'GOOD':
   1483                 print >> output, '<td align=\"left\"><b><font color="#00CC00">PASS</font></b></td>'
   1484             elif res['status'] == 'FAIL':
   1485                 print >> output, '<td align=\"left\"><b><font color="red">FAIL</font></b></td>'
   1486             elif res['status'] == 'ERROR':
   1487                 print >> output, '<td align=\"left\"><b><font color="red">ERROR!</font></b></td>'
   1488             else:
   1489                 print >> output, '<td align=\"left\">%s</td>' % res['status']
   1490             # print exec time (seconds)
   1491             print >> output, '<td align="left">%s</td>' % res['exec_time_sec']
   1492             # print log only if test failed..
   1493             if res['log']:
   1494                 #chop all '\n' from log text (to prevent html errors)
   1495                 rx1 = re.compile('(\s+)')
   1496                 log_text = rx1.sub(' ', res['log'])
   1497 
   1498                 # allow only a-zA-Z0-9_ in html title name
   1499                 # (due to bug in MS-explorer)
   1500                 rx2 = re.compile('([^a-zA-Z_0-9])')
   1501                 updated_tag = rx2.sub('_', res['title'])
   1502 
   1503                 html_body_text = '<html><head><title>%s</title></head><body>%s</body></html>' % (str(updated_tag), log_text)
   1504                 print >> output, '<td align=\"left\"><A HREF=\"#\" onClick=\"popup(\'%s\',\'%s\')\">Info</A></td>' % (str(updated_tag), str(html_body_text))
   1505             else:
   1506                 print >> output, '<td align=\"left\"></td>'
   1507             # print execution time
   1508             print >> output, '<td align="left"><A HREF=\"%s\">Debug</A></td>' % os.path.join(dirname, res['subdir'], "debug")
   1509 
   1510             print >> output, '</tr>'
   1511             print_result(results[r][1], indent + 1)
   1512 
   1513     print_result(results[""][1], 0)
   1514     print >> output, "</tbody></table>"
   1515 
   1516 
   1517     print >> output, '<h2 id=\"page_sub_title\">Host Info</h2>'
   1518     print >> output, '<h2 id=\"comment\">click on each item to expend/collapse</h2>'
   1519     ## Meta list comes here..
   1520     print >> output, '<p>'
   1521     print >> output, '<A href="#" class="button" onClick="expandTree(\'meta_tree\');return false;">Expand All</A>'
   1522     print >> output, '&nbsp;&nbsp;&nbsp'
   1523     print >> output, '<A class="button" href="#" onClick="collapseTree(\'meta_tree\'); return false;">Collapse All</A>'
   1524     print >> output, '</p>'
   1525 
   1526     print >> output, '<ul class="mktree" id="meta_tree">'
   1527     counter = 0
   1528     keys = metadata.keys()
   1529     keys.sort()
   1530     for key in keys:
   1531         val = metadata[key]
   1532         print >> output, '<li id=\"meta_headline\">%s' % key
   1533         print >> output, '<ul><table class="meta_table"><tr><td align="left">%s</td></tr></table></ul></li>' % val
   1534     print >> output, '</ul>'
   1535 
   1536     print >> output, "</body></html>"
   1537     if output_file_name:
   1538         output.close()
   1539 
   1540 
   1541 def parse_result(dirname, line, results_data):
   1542     """
   1543     Parse job status log line.
   1544 
   1545     @param dirname: Job results dir
   1546     @param line: Status log line.
   1547     @param results_data: Dictionary with for results.
   1548     """
   1549     parts = line.split()
   1550     if len(parts) < 4:
   1551         return None
   1552     global tests
   1553     if parts[0] == 'START':
   1554         pair = parts[3].split('=')
   1555         stime = int(pair[1])
   1556         results_data[parts[1]] = [stime, [], None]
   1557         try:
   1558             parent_test = re.findall(r".*/", parts[1])[0][:-1]
   1559             results_data[parent_test][1].append(parts[1])
   1560         except IndexError:
   1561             results_data[""][1].append(parts[1])
   1562 
   1563     elif (parts[0] == 'END'):
   1564         result = {}
   1565         exec_time = ''
   1566         # fetch time stamp
   1567         if len(parts) > 7:
   1568             temp = parts[5].split('=')
   1569             exec_time = temp[1] + ' ' + parts[6] + ' ' + parts[7]
   1570         # assign default values
   1571         result['time'] = exec_time
   1572         result['testcase'] = 'na'
   1573         result['status'] = 'na'
   1574         result['log'] = None
   1575         result['exec_time_sec'] = 'na'
   1576         tag = parts[3]
   1577 
   1578         result['subdir'] = parts[2]
   1579         # assign actual values
   1580         rx = re.compile('^(\w+)\.(.*)$')
   1581         m1 = rx.findall(parts[3])
   1582         if len(m1):
   1583             result['testcase'] = m1[0][1]
   1584         else:
   1585             result['testcase'] = parts[3]
   1586         result['title'] = str(tag)
   1587         result['status'] = parts[1]
   1588         if result['status'] != 'GOOD':
   1589             result['log'] = get_exec_log(dirname, tag)
   1590         if len(results_data)>0:
   1591             pair = parts[4].split('=')
   1592             etime = int(pair[1])
   1593             stime = results_data[parts[2]][0]
   1594             total_exec_time_sec = etime - stime
   1595             result['exec_time_sec'] = total_exec_time_sec
   1596         results_data[parts[2]][2] = result
   1597     return None
   1598 
   1599 
   1600 def get_exec_log(resdir, tag):
   1601     """
   1602     Get job execution summary.
   1603 
   1604     @param resdir: Job results dir.
   1605     @param tag: Job tag.
   1606     """
   1607     stdout_file = os.path.join(resdir, tag, 'debug', 'stdout')
   1608     stderr_file = os.path.join(resdir, tag, 'debug', 'stderr')
   1609     status_file = os.path.join(resdir, tag, 'status')
   1610     dmesg_file = os.path.join(resdir, tag, 'sysinfo', 'dmesg')
   1611     log = ''
   1612     log += '<br><b>STDERR:</b><br>'
   1613     log += get_info_file(stderr_file)
   1614     log += '<br><b>STDOUT:</b><br>'
   1615     log += get_info_file(stdout_file)
   1616     log += '<br><b>STATUS:</b><br>'
   1617     log += get_info_file(status_file)
   1618     log += '<br><b>DMESG:</b><br>'
   1619     log += get_info_file(dmesg_file)
   1620     return log
   1621 
   1622 
   1623 def get_info_file(filename):
   1624     """
   1625     Gets the contents of an autotest info file.
   1626 
   1627     It also and highlights the file contents with possible problems.
   1628 
   1629     @param filename: Info file path.
   1630     """
   1631     data = ''
   1632     errors = re.compile(r"\b(error|fail|failed)\b", re.IGNORECASE)
   1633     if os.path.isfile(filename):
   1634         f = open('%s' % filename, "r")
   1635         lines = f.readlines()
   1636         f.close()
   1637         rx = re.compile('(\'|\")')
   1638         for line in lines:
   1639             new_line = rx.sub('', line)
   1640             errors_found = errors.findall(new_line)
   1641             if len(errors_found) > 0:
   1642                 data += '<font color=red>%s</font><br>' % str(new_line)
   1643             else:
   1644                 data += '%s<br>' % str(new_line)
   1645         if not data:
   1646             data = 'No Information Found.<br>'
   1647     else:
   1648         data = 'File not found.<br>'
   1649     return data
   1650 
   1651 
   1652 def usage():
   1653     """
   1654     Print stand alone program usage.
   1655     """
   1656     print 'usage:',
   1657     print 'make_html_report.py -r <result_directory> [-f output_file] [-R]'
   1658     print '(e.g. make_html_reporter.py -r '\
   1659           '/usr/local/autotest/client/results/default -f /tmp/myreport.html)'
   1660     print 'add "-R" for an html report with relative-paths (relative '\
   1661           'to results directory)'
   1662     print ''
   1663     sys.exit(1)
   1664 
   1665 
   1666 def get_keyval_value(result_dir, key):
   1667     """
   1668     Return the value of the first appearance of key in any keyval file in
   1669     result_dir. If no appropriate line is found, return 'Unknown'.
   1670 
   1671     @param result_dir: Path that holds the keyval files.
   1672     @param key: Specific key we're retrieving.
   1673     """
   1674     keyval_pattern = os.path.join(result_dir, "kvm.*", "keyval")
   1675     keyval_lines = commands.getoutput(r"grep -h '\b%s\b.*=' %s"
   1676                                       % (key, keyval_pattern))
   1677     if not keyval_lines:
   1678         return "Unknown"
   1679     keyval_line = keyval_lines.splitlines()[0]
   1680     if key in keyval_line and "=" in keyval_line:
   1681         return keyval_line.split("=")[1].strip()
   1682     else:
   1683         return "Unknown"
   1684 
   1685 
   1686 def get_kvm_version(result_dir):
   1687     """
   1688     Return an HTML string describing the KVM version.
   1689 
   1690     @param result_dir: An Autotest job result dir.
   1691     """
   1692     kvm_version = get_keyval_value(result_dir, "kvm_version")
   1693     kvm_userspace_version = get_keyval_value(result_dir,
   1694                                              "kvm_userspace_version")
   1695     if kvm_version == "Unknown" or kvm_userspace_version == "Unknown":
   1696         return None
   1697     return "Kernel: %s<br>Userspace: %s" % (kvm_version, kvm_userspace_version)
   1698 
   1699 
   1700 def create_report(dirname, html_path='', output_file_name=None):
   1701     """
   1702     Create an HTML report with info about an autotest client job.
   1703 
   1704     If no relative path (html_path) or output file name provided, an HTML
   1705     file in the toplevel job results dir called 'job_report.html' will be
   1706     created, with relative links.
   1707 
   1708     @param html_path: Prefix for the HTML links. Useful to specify absolute
   1709             in the report (not wanted most of the time).
   1710     @param output_file_name: Path to the report file.
   1711     """
   1712     res_dir = os.path.abspath(dirname)
   1713     tag = res_dir
   1714     status_file_name = os.path.join(dirname, 'status')
   1715     sysinfo_dir = os.path.join(dirname, 'sysinfo')
   1716     host = get_info_file(os.path.join(sysinfo_dir, 'hostname'))
   1717     rx = re.compile('^\s+[END|START].*$')
   1718     # create the results set dict
   1719     results_data = {}
   1720     results_data[""] = [0, [], None]
   1721     if os.path.exists(status_file_name):
   1722         f = open(status_file_name, "r")
   1723         lines = f.readlines()
   1724         f.close()
   1725         for line in lines:
   1726             if rx.match(line):
   1727                 parse_result(dirname, line, results_data)
   1728     # create the meta info dict
   1729     metalist = {
   1730                 'uname': get_info_file(os.path.join(sysinfo_dir, 'uname')),
   1731                 'cpuinfo':get_info_file(os.path.join(sysinfo_dir, 'cpuinfo')),
   1732                 'meminfo':get_info_file(os.path.join(sysinfo_dir, 'meminfo')),
   1733                 'df':get_info_file(os.path.join(sysinfo_dir, 'df')),
   1734                 'modules':get_info_file(os.path.join(sysinfo_dir, 'modules')),
   1735                 'gcc':get_info_file(os.path.join(sysinfo_dir, 'gcc_--version')),
   1736                 'dmidecode':get_info_file(os.path.join(sysinfo_dir, 'dmidecode')),
   1737                 'dmesg':get_info_file(os.path.join(sysinfo_dir, 'dmesg')),
   1738     }
   1739     if get_kvm_version(dirname) is not None:
   1740         metalist['kvm_ver'] = get_kvm_version(dirname)
   1741 
   1742     if output_file_name is None:
   1743         output_file_name = os.path.join(dirname, 'job_report.html')
   1744     make_html_file(metalist, results_data, tag, host, output_file_name,
   1745                    html_path)
   1746 
   1747 
   1748 def main(argv):
   1749     """
   1750     Parses the arguments and executes the stand alone program.
   1751     """
   1752     dirname = None
   1753     output_file_name = None
   1754     relative_path = False
   1755     try:
   1756         opts, args = getopt.getopt(argv, "r:f:h:R", ['help'])
   1757     except getopt.GetoptError:
   1758         usage()
   1759         sys.exit(2)
   1760     for opt, arg in opts:
   1761         if opt in ("-h", "--help"):
   1762             usage()
   1763             sys.exit()
   1764         elif opt == '-r':
   1765             dirname =  arg
   1766         elif opt == '-f':
   1767             output_file_name =  arg
   1768         elif opt == '-R':
   1769             relative_path = True
   1770         else:
   1771             usage()
   1772             sys.exit(1)
   1773 
   1774     html_path = dirname
   1775     # don't use absolute path in html output if relative flag passed
   1776     if relative_path:
   1777         html_path = ''
   1778 
   1779     if dirname:
   1780         if os.path.isdir(dirname): # TBD: replace it with a validation of
   1781                                    # autotest result dir
   1782             create_report(dirname, html_path, output_file_name)
   1783             sys.exit(0)
   1784         else:
   1785             print 'Invalid result directory <%s>' % dirname
   1786             sys.exit(1)
   1787     else:
   1788         usage()
   1789         sys.exit(1)
   1790 
   1791 
   1792 if __name__ == "__main__":
   1793     main(sys.argv[1:])
   1794