/** * Summary page program * * Copyright (C) 2016, 2017 LoVullo Associates, Inc. * * This file is part of the Liza Data Collection Framework * * liza is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file is used for direct interaction with the rater for testing purposes. * As such, much of it is a rushed implementation; it's a bit of a kluge and * could use some refactoring. * * Also, it is terriby stateful and difficult to work with. I have the utmost * confidence in your ability to look away and pretend you never saw this * script. */ // intentionally global; developers can override var program = document.location.pathname.match( '/raters/(.*?)/' )[1], submit_url = '/raters/submit-test.php?program=' + program, supplier = rater.supplier, prior_url = '/raters/submit-test.php?retrieve=' + supplier + '&program=' + program, qdata_host = 'dev'; var client = ( function() { // URL to which quote/result submissions should be POSTed var form = document.querySelector( 'form.entry-form' ), final_prem = form.querySelector( '.final-premium' ), final_accept = form.querySelector( '.final-accept' ), final_comments = form.querySelector( '.final-comments' ), voi = document.getElementById( 'voi-list' ), coview = document.getElementById( 'class-overview-list' ), final_good = document.getElementById( 'final-accept-good' ), final_bad = document.getElementById( 'final-accept-bad' ), load_prior = document.getElementById( 'load-prior' ), workstatus = null, valspan = {}, bucket = {}, rate_result = {}, // used to overwrite existing test cases rather than create a new save_id = '', prior_result, rate_callback = function() {}, // whether to ignore user input (do not put in bucket) ignore_input = false; populateBucket(); function setWorkStatus( message ) { if ( workstatus === null ) { workstatus = document.createElement( 'div' ); workstatus.id = 'workstatus'; document.body.appendChild( workstatus ); } workstatus.innerHTML = message; workstatus.className = ( message ) ? 'show' : ''; } function populateBucket() { Array.prototype.slice.call( form.querySelectorAll( '[name]' ) ).forEach( function( field ) { var name = field.name.replace( /\[\]$/, '' ); if ( !name ) { return; } // if the name does not match, then we removed the square // brackets, meaning that this is a set bucket[ name ] = ( name === field.name ) ? field.value : [ field.value ]; } ); } function overrideBucket( boverride ) { for ( var name in boverride ) { bucket[ name ] = boverride[ name ]; } emptyBucket(); } function removeEntryFocus() { form.className = form.className.replace( /\bfocus\b/, '' ); } document.body.addEventListener( 'mouseup', function( e ) { var overform = hasParent( form, e.target ); if ( overform === false ) { removeEntryFocus(); } } ); form.addEventListener( 'reset', function() { if ( ignore_input ) { return; } // wait until *after* reset setTimeout( function() { clearTestCase(); bucket = {}; populateBucket(); clearSummaryPremium(); setWorkStatus(); }, 0 ); } ); form.addEventListener( 'mouseover', function() { showEntryForm(); } ); function clearTestCase() { save_id = ''; prior_result = undefined; // clear prior message Prior.setPriorMessage( null ); // clear prior class from body document.body.className = document.body.className.replace( /\bprior\b/, '' ); } function setTestCase( id, result ) { save_id = ''+( id ); prior_result = result; // this really should be set... if ( !( prior_result.vars ) ) { prior_result.vars = {}; } // add prior class name to body document.body.className += ' prior'; } function showEntryForm() { if ( form.className.match( /\bfocus\b/ ) ) { return; } form.className += ' focus'; } // on field change, update bucket form.addEventListener( 'change', function( e ) { if ( ignore_input ) { return; } // if we changed something, then the displayed premium (if any) must be // invalidated clearSummaryPremium(); var target = e.target, name = target.name.replace( /\[\]$/, '' ), value = target.value.trim(); if ( !name ) { return; } // if this is a set, we want to store every value if ( name !== target.name ) { var toarr = Array.prototype.slice; // retrieve all the rows var rows = toarr.call( target.parentElement.parentElement.parentElement .querySelectorAll( '.entry-row' ) ); // determine if we're working with a matrix var matrix = /\bmatrix\b/.test( rows[ 0 ].className ); value = []; rows.forEach( function( row, i ) { var ref = value; // for matricies, add value to a sub-array; vectors, just keep // appending to the original array if ( matrix ) { ref = value[ i ] = []; } // add each value toarr.call( row.querySelectorAll( '[name]' ) ).forEach( function( node ) { ref.push( node.value.trim() ); } ); } ); } bucket[ name ] = value; } ); // update screen on submit form.addEventListener( 'submit', function( e ) { // do not submit the form e.preventDefault(); rate( bucket ); } ); form.addEventListener( 'click', function( e ) { if ( e.target.className === 'entry-add' ) { addRow( e.target.parentElement, e.target ); e.preventDefault(); } else if ( e.target.className === 'entry-rm' ) { removeColumn( e.target ); e.preventDefault(); } else if ( e.target.className === 'entry-add-matrix' ) { addColumn( e.target ); e.preventDefault(); } } ); final_good.addEventListener( 'click', function( e ) { e.preventDefault(); showFinalComments( true, function( comment, _, waiting ) { var prem = rate_result.premium; hideFinalAccept(); submitQuote( bucket, rate_result, comment, true, waiting, prem, save_id ); } ); } ); final_bad.addEventListener( 'click', function( e ) { e.preventDefault(); showFinalComments( false, function( comment, expect, waiting ) { hideFinalAccept(); submitQuote( bucket, rate_result, comment, false, waiting, expect, save_id ); } ); } ); function showFinalComments( looksgood, callback ) { final_comments.className += ' show'; var submit = document.getElementById( 'final-submit' ), cancel = document.getElementById( 'final-cancel' ), expect_container = document.getElementById( 'final-expect-container' ), submit_new = document.getElementById( 'final-submit-new' ), listener; // we do not care about the expected value if the premium looks good expect_container.style.display = ( looksgood ) ? 'none' : 'inline'; // if a test case is set, give them the option to clear it and submit it // as a new test case submit_new.style.display = ( save_id ) ? 'inline' : 'none'; // make it very clear what the user is about to do submit.innerHTML = ( save_id ) ? 'Update Existing Test Case' : 'Submit'; // we won't use addEventListener becuase we only want one event to be // attached submit.onclick = function( e ) { e.preventDefault(); var comments = document.getElementById( 'final-comments' ), expected = document.getElementById( 'final-expected' ), waiting = document.getElementById( 'final-waiting' ); callback( comments.value, +( expected.value.replace( /^\$/, '' ) ), !!waiting.checked ); rmclass( final_comments, 'show' ); }; submit_new.onclick = function( e ) { e.preventDefault(); // clear save id and trigger normal submit save_id = ''; submit.onclick( e ); }; cancel.onclick = function( e ) { e.preventDefault(); rmclass( final_comments, 'show' ); }; // give focus to final comments document.getElementById( 'final-comments' ).focus(); } function hideFinalAccept() { // replace all shows since there may be multiple final_accept.className = final_accept.className.replace( /\bshow\b/, '' ); } function getXhrJsonSync( method, url, data ) { var xhttp = new XMLHttpRequest(); xhttp.open( method, url, false ); if ( method.toLowerCase() === 'post' ) { xhttp.setRequestHeader( 'Content-type', 'application/x-www-form-urlencoded' ); } xhttp.send( ( data ) ? 'data=' + JSON.stringify( data ) : null ); if ( xhttp.status !== 200 ) { throw Error( 'Submit failed; status: ' + xhttp.status ); } // this will fail if the response is crap, but will be caught by the // exception return JSON.parse( xhttp.responseText ); } function getXhrJson( method, url, data, callback ) { var xhttp = new XMLHttpRequest(); xhttp.open( method, url, true ); if ( method.toLowerCase() === 'post' ) { xhttp.setRequestHeader( 'Content-type', 'application/x-www-form-urlencoded' ); } xhttp.onload = function() { if ( xhttp.status !== 200 ) { callback( null, Error( 'Submit failed; status: ' + xhttp.status ) ); return; } callback( JSON.parse( xhttp.responseText ) ); } xhttp.send( ( data ) ? 'data=' + JSON.stringify( data ) : null ); } function submitQuote( bucket, result, comment, looksgood, waiting, expected, caseid, success_callback ) { // we don't want to modify the original result (could use // Object.create() here, but they may be using IE) var tmpresult = function() {}; tmpresult.prototype = result; // it is absolutely pointless to store debug information since the ids // change at any time and are dependent on the XSL processor var submit_result = new tmpresult(); // so that it's property serialized for ( var name in result ) { submit_result[ name ] = result[ name ]; } // we do not need the debug information (there's a lot of it and it // changes frequently) submit_result.debug = undefined; // nor do we need constants (especially large ones)! submit_result.consts = undefined; var data = { bucket: bucket, result: submit_result, looksgood: !!looksgood, waiting: !!waiting, comment: encodeURIComponent( comment ), expected: expected, supplier: supplier, // will cause an existing test case to be overwritten, if set caseid: caseid, }; getXhrJson( 'POST', submit_url, data, function( response, err ) { // check the response of the actual save (just because we // got a HTTP 200 doesn't mean we successfully saved to the // server; we could have also hit the wrong page // (misconfigured)!) if ( err ) { alert( 'Ah, crap! Quote submission failed! Contact IT before ' + 'you do anything else.\n\n' + 'Here are the boring details:\n' + ' ' + err.message ); return; } success_callback && success_callback(); } ); } function addRow( parent, before ) { before = before || parent.querySelector( '.entry-add' ); // get the row to duplicate var dup = parent.querySelector( '.entry-row' ) .cloneNode( true ); parent.insertBefore( dup, before ); // trigger change so that its value can be recorded triggerChange( dup.querySelector( '[name]' ) ); } function addColumn( event_target ) { // get the field to duplicate var dup = event_target.parentElement.querySelector( '.entry-field' ) .cloneNode( true ); event_target.parentElement.insertBefore( dup, event_target ); // trigger change so that its value can be recorded triggerChange( dup.querySelector( '[name]' ) ); } function removeColumn( event_target ) { var rm = event_target.parentElement, parent = rm.parentElement, rowParent = parent.parentElement, rows = rowParent.querySelectorAll( '.entry-row' ).length, cols = parent.querySelectorAll( '.entry-field' ).length; // do not remove last column of last row if ( ( rows + cols ) === 2 ) { return; } // remove the element parent.removeChild( rm ); // if there are no more columns, remove the row if ( cols === 1 ) { rowParent.removeChild( parent ); } // re-gather values in bucket to accomodate missing value (we can do so // simply by triggering a change on one of the elements of the same // name) triggerChange( rowParent.querySelector( '[name]' ) ); } function triggerChange( element ) { if ( !element ) { return; } // create change event var event = document.createEvent( 'Event' ); event.initEvent( 'change', true, true ); // trigger event element.dispatchEvent( event ); } function rate( args, showresults, exception ) { showresults = ( showresults === undefined ) ? true : !!showresults; exception = !!exception; var rater = window.rater; if ( !( window.rater ) ) { alert( 'fatal: rater unavailable.' ); return; } setWorkStatus( 'Performing rating...' ); try { var result = rater( args ); // XXX: ewwww rate_result = result; if ( !( showresults ) ) { return; } // log result to the console in case we want to peeky peeky console.log( result ); rate_callback( result ); setWorkStatus( 'Updating premium...' ); updateSummaryPremium( result.premium ); // VOIs are referenced immediately, so render them first updateVois( result.vars, function() { // classes are faster to process than the other summary values updateSummaryClasses( result.classes, result.vars, undefined, function() { updateSummaryValues( result.vars ); } ); } ); } catch ( e ) { setWorkStatus( 'Rating error occurred.' ); console && console.log( e ); if ( exception ) { throw e; } else { alert( 'fatal: ' + e.message ); } } } function updateSummaryPremium( premium ) { final_prem.innerHTML = premium; final_prem.className += ' show'; final_accept.className += ' show'; setPlaceholderValue( 'yields_premium', '', premium ); } function clearSummaryPremium() { final_prem.innerHTML = ''; rmclass( final_prem, 'show' ); rmclass( final_accept, 'show' ); } function getValueDisplay( value ) { if ( Array.isArray( value ) ) { return joinValues( value ); } return ( value === undefined ) ? '' : ''+( value ); } function updateVois( vars, callback ) { setWorkStatus( 'Processing VOIs...' ); voi.innerHTML = ''; var queue = []; for ( var name in vars ) { queue.push( name ); } var vois = {}, qlen = queue.length, i = qlen; dequeueSetsOf( 10, function( c ) { if ( i-- === 0 ) { // display the VOIs processVois( vois ); document.getElementById( 'voi-container' ).className += ' show'; setWorkStatus(); callback && callback(); return; } setWorkStatus( 'Processing VOIs (' + Math.floor( ( ( qlen - i ) / qlen ) * 100 ) + '%)...' ); var name = queue[ i ], value = vars[ name ]; if ( value && /^prem|^min|^surcharge|^cov(erage)?|^credit|^factor|^rate|Prem|[tT]otal/ .test( name ) && !( /^_/.test( name ) ) ) { var display = getValueDisplay( value ), prior = ( prior_result && prior_result.vars[ name ] ) ? getValueDisplay( prior_result.vars[ name ] ) : ''; // update values of interest (voi) if ( display !== '[]' ) { vois[ name ] = [ display, prior ]; } } // continue c(); } )(); } function updateSummaryValues( vars, placeid, callback ) { var queue = []; for ( var name in vars ) { queue.push( name ); } var qlen = queue.length; // repaint frequently; this is intensive dequeueSetsOf( 10, function( c ) { if ( queue.length === 0 ) { setWorkStatus(); return; } name = queue.pop(); setWorkStatus( 'Formatting summary values (' + Math.floor( ( ( qlen - queue.length ) / qlen ) * 100 ) + '%)...' ); var value = vars[ name ], display = getValueDisplay( value ), prior = ( prior_result && prior_result.vars[ name ] ) ? getValueDisplay( prior_result.vars[ name ] ) : ''; setPlaceholderValue( name, '', display ); if ( prior ) { setPlaceholderValue( name, '-prior', prior ); } setLetListPlaceholders( name, display, prior ); // continue c(); } )(); } function dequeueSetsOf( n, c ) { return function dq( i ) { i = i || 0; c( function() { if ( i === 0 ) { setTimeout( function() { dq( n ) }, 0 ); } else { dq( i - 1 ); } } ); } } function processVois( vois ) { // add the vois to the screen in the proper order (reversed) var i = window.voi_order.length; while ( i-- ) { var data = window.voi_order[ i ], name = data[ 0 ], depth = data[ 1 ], href = data[ 2 ]; if ( vois[ name ] ) { var voi = vois[ name ]; addVoi( name, voi[ 0 ], voi[ 1 ], href, depth ); } } } function addVoi( name, value, prior, href, depth ) { depth = depth || 0; // if the VOI has a value other than 0 (our poor-man check is using a // regex to remove anything and see if we have a non-empty string left) if ( ( value.replace( /[\[\]0,]/g, '' ) === '' ) && ( prior.replace( /[\[\]0,]/g, '' ) === '' ) ) { return; } var depthstr = '', i = depth; while ( i-- ) { if ( i === 0 ) { depthstr += '|-'; } depthstr += '  '; } // if href is not given, then use name href = href || name; // got lazy. var tr = document.createElement( 'tr' ); tr.className = 'depth' + depth; tr.innerHTML = ( '' + '' + depthstr + name + '' + '' + '' + depthstr.replace( /-/, '' ) + value + '' + ( ( !prior ) ? '' : '' + prior + '' ) ); tr.addEventListener( 'click', function( e ) { // ignore link clicks if ( e.target.nodeName === 'A' ) { return; } var val = JSON.parse( value ); if ( Array.isArray( val ) ) { var t = 0; for ( var i in val ) { t += val[ i ]; } val = t; } voiPainterAdd( tr, val ); } ); voi.appendChild( tr ); } function addClassOverview( name, value ) { var prior = ( prior_result && prior_result.vars[ name ] ) ? getValueDisplay( prior_result.vars[ name ] ) : ''; // got lazy. var tr = document.createElement( 'tr' ); tr.innerHTML = ( '' + '' + name + '' + '' ); coview.appendChild( tr ); } function joinValues( values ) { var ret = '['; if ( Array.isArray( values[ 0 ] ) ) { var subvals = []; for ( var i in values ) { subvals.push( joinValues( values[ i ] ) ); } ret += subvals.join( ', ' ); } else { ret += ( Array.isArray( values ) ) ? values.join( ', ' ) : values; } return ret + ']'; } function updateSummaryClasses( classes, vars, placeid, callback ) { coview.innerHTML = ''; var queue = []; for ( var name in classes ) { queue.push( name ); } var qlen = queue.length; dequeueSetsOf( 10, function( c ) { if ( queue.length === 0 ) { setWorkStatus(); callback && callback(); return; } var name = queue.pop(); // hide internal classes ("-" prefix) if ( /^-/.test( name ) ) { return c(); } setWorkStatus( 'Formatting class summary values (' + Math.floor( ( ( qlen - queue.length ) / qlen ) * 100 ) + '%)...' ); // output the classification and the total premium for the class setPlaceholderValue( 'class-' + name, placeid, ( ''+( classes[ name ] ) + ' -> ' + vars[ name ] ), classes[ name ] ); if ( prior_result && prior_result.classes && prior_result.classes[ name ] ) { // XXX: duplicate setPlaceholderValue( 'class-' + name, '-prior', ( ''+( prior_result.classes[ name ] ) + ' -> ' + prior_result.vars[ name ] ), classes[ name ] ); } // if this class was a match, add it to the overview with its // accumulator value if ( classes[ name ] ) { addClassOverview( name, vars[ name ] ); } c(); } )(); document.getElementById( 'class-overview' ).className += ' show'; } function updateSummaryDebug( debug, parent, callback ) { var queue = []; // do nothing if debug data is not yet available if ( !debug ) { return; } // loop through each element on the DOM, *not* each debug id returned to // us, since we want to clear any that may be missing Array.prototype.slice.call( parent.querySelectorAll( '.debugid' ) ) .forEach( function( element ) { queue.push( element.id ); } ); var qlen = queue.length; dequeueSetsOf( 10, function( c ) { if ( queue.length === 0 ) { setWorkStatus(); callback && callback(); return; } setWorkStatus( 'Processing breakdown values (' + Math.floor( ( ( qlen - queue.length ) / qlen ) * 100 ) + '%)...' ); var id = queue.pop(), did = id.replace( /^ubd-/, '' ); try { setPlaceholderValue( id, '', ( debug[ did ] ) ? JSON.stringify( debug[ did ] ) : '' ); } catch ( e ) { console.error( 'Debug (stringify debug ' + did + ' ): ' + e.message ); } c(); } )(); } var getPlaceholder = ( function() { var domcache = {}; function getPlaceholder( name, placeid ) { var classname = ( 'entry-value' + ( placeid || '' ) ); var current = domcache[ name + placeid ]; if ( current ) { return current; } // ignore system vars if ( name.match( /^___/ ) ) { return null; } var parent = document.getElementById( name ); if ( !parent ) { return null; } var legend = parent.getElementsByTagName( 'legend' ), dest = ( legend.length ) ? legend[ 0 ] : parent; var element = document.createElement( 'span' ); element.className = classname; dest.appendChild( element ); // rather than re-scanning the DOM each time domcache[ name + placeid ] = element; return element; } return getPlaceholder; } )(); function setPlaceholderValue( name, placeid, value, hasval ) { var p = getPlaceholder( name, placeid ); if ( p === null ) { return; } p.innerHTML = value; // do not handle prior flagging if ( placeid === '-prior' ) { return; } // get fieldset var fs = p.parentNode.parentNode; if ( fs.nodeName === 'FIELDSET' ) { fs.className = fs.className.replace( /\Bhasval\B/, '' ); if ( ( hasval !== undefined && hasval ) // progressively more time-consuming checks || ( ( hasval === undefined ) && value && +value !== 0 && value.replace( /[\[\],0]/g, '' ) ) ) { fs.className += ' hasval'; } } } function setLetListPlaceholders( name, value, prior ) { if ( name.match( /^___/ ) ) { return; } // certainly room for improvement here (especially performance-wise), // but this is a quick implementation var elements = document.querySelectorAll( '.letlist-' + name ); Array.prototype.slice.call( elements ).forEach( function( element ) { if ( !( element.id ) ) { // prefix with alpha so as not to cause a syntax error on query element.id = 'll' + Math.floor( ( new Date() ).getTime() * Math.random() ); } setPlaceholderValue( element.id, '', value ); // include prior values, if available if ( prior ) { setPlaceholderValue( element.id, '-prior', prior ); } } ); } function rmclass( element, name ) { element.className = element.className.replace( new RegExp( '\\b' + name + '\\b', 'g' ), '' ); } function hasParent( parent, element ) { var parentElement = element.parentElement; if ( parentElement === parent ) { return true; } return ( parentElement ) ? hasParent( parent, parentElement ) : false; } // XXX: This is a mess. THIS IS WHAT TIME CONSTRAINTS DO TO CODE QUALITY! // LET ME HACK IN PEACE! >:@ (What? Unconstrained development is a fantasy? // Is unlimited time unreasonable? Phf. Maybe that Time Weaver frog person // knows how to help with that. If you don't know that reference and you're // in here hacking this code, then that implies that you are new; it then // begs the question: why has it persisted for so long!!! Of course it has, // though. That's how TODOs/XXXs work: they don't get fixed; they just turn // text in your editor pretty [obnoxious] colors.) function resetFields() { // XXX: gahhhhhh!!!!!!! ignore_input = true; function rowquery( name ) { return document.querySelectorAll( '#param-input-' + name + ' > .entry-row' ); } for ( var field in bucket ) { // may happen if we're loading data from another source if ( bucket[ field ] === undefined ) { continue; } var fdata = bucket[ field ], elements = rowquery( field ), length = ( fdata.length > elements.length ) ? fdata.length : elements.length; if ( elements.length === 0 ) { continue; } // not everything is an array of values if ( Array.isArray( fdata ) ) { // add/clear fields on the form as necessary to accomdate bucket // data for ( var i = 0; i < length; i++ ) { // field exists in bucket but not on the form if ( ( fdata[ i ] !== undefined ) && !( elements[ i ] ) ) { addRow( elements[ 0 ].parentNode ); } // field exists on form but not in the bucket else if ( elements[ i ] && ( fdata[ i ] === undefined ) ) { // TODO: remove field instead elements[ i ].querySelector( '[name]' ).value = ''; } // if we have a matrix of values, we must also add columns // for each if ( Array.isArray( fdata[ i ] ) ) { var element = rowquery( field )[ i ], cols = element.querySelectorAll( '.entry-field' ), len = ( fdata[ i ].length > cols.length ) ? fdata[ i ].length : cols.length; // check each column for ( var j = 0; j < len; j++ ) { if ( ( fdata[ i ][ j ] !== undefined ) && !( cols[ j ] ) ) { // re-query in case we just added a row addColumn( element.querySelector( '.entry-add-matrix' ) ); } else if ( cols[ j ] && ( fdata[ i ][ j ] === undefined ) ) { // TODO: remove field instead cols[ i ].querySelector( '[name]' ).value = ''; } } } } } } form.reset(); ignore_input = false; } function emptyBucket() { resetFields(); // prevent form updates from propagating to the bucket ignore_input = true; for ( var field in bucket ) { var fdata = bucket[ field ]; // not everything is an array; if not, simply set the value and move // on if ( !( Array.isArray( fdata ) ) ) { var element = document.querySelector( '[name="' + field + '"]' ); if ( element ) { element.value = fdata; } continue; } var elements = document.querySelectorAll( '[name="' + field + '[]"]' ); var total = 0; for ( var i = 0, l = fdata.length; i < l; i++ ) { if ( !( elements[ i ] ) ) { continue; } // if a matrix, update each value if ( Array.isArray( fdata[ i ] ) ) { for ( var j = 0, jl = fdata[ i ].length; j < jl; j++ ) { elements[ total++ ].value = fdata[ i ][ j ]; } } else { // not a matrix elements[ total++ ].value = fdata[ i ]; } } } // re-allow input ignore_input = false; } function getUserFromHostname( hostname ) { // strip off any domain, remove number from username and strip anything // after a dash (e.g. gerwitm-ubuntu2.lovullo.local => gerwitm) return hostname.split( '.' )[ 0 ].replace( /[0-9]+$/, '' ) .split( '-' )[ 0 ]; } /** * Prior module: load prior quotes (test cases) * * Not to be confused in speech with the Friar module, which would have your * premiums divinely calculated and communicated through a deep meditation. */ var Prior = ( function ___loadprior( dom ) { var exports = {}, // current set of loaded test cases curset = {}; var getLoadDialog = function() { // URL with fragment to automatically display this dialog var url = document.location.href.replace( /#.*$/, '' ) + '#prior'; var dialog = dom.createElement( 'div' ); dialog.id = 'prior'; dialog.className = 'load-dialog'; dialog.innerHTML = "

Load Prior Quotes

" + "

" + "Below is a list of all prior saved quotes; choose one " + "to load it into the test area." + "

" + "

" + "To load this dialog automatically on page load, you " + "may use the following link: " + url + "" "

"; // re-test button var retest = dom.createElement( 'button' ); retest.innerHTML = 'Regression Test'; retest.addEventListener( 'click', function( e ) { e.preventDefault(); e.target.disabled = 'disabled'; retestAll( function() { e.target.disabled = ''; } ); } ); // load quote number var loadquote = dom.createElement( 'button' ); loadquote.innerHTML = 'Load Quote #'; loadquote.addEventListener( 'click', function( e ) { e.preventDefault(); var qid = prompt( 'Enter quote #:' ); if ( !qid ) { return; } exports.hideLoad(); loadQuote( qid, qdata_host ); } ); dialog.appendChild( retest ); dialog.appendChild( loadquote ); dialog.appendChild( getPriorTable() ); dom.body.appendChild( dialog ); // reassign the function to always return the instance getLoadDialog = function() { return dialog; }; return getLoadDialog(); }; var getPriorTable = function() { var table = dom.createElement( 'table' ), headings = [ "Date", "Description", "User", "Premium", "Expected" ]; // add headings for ( var head in headings ) { var th = dom.createElement( 'th' ); th.innerHTML = headings[ head ]; th.className = headings[ head ].toLowerCase(); table.appendChild( th ); } // add count var count = dom.createElement( 'caption' ); count.innerHTML = 'Total Count: 0'; table.appendChild( count ); table.clear = function() { var rows = table.querySelectorAll( 'tr' ); for ( var i = 0; i < rows.length; i++ ) { table.removeChild( rows[ i ] ); } }; table.addRow = function( looksgood, waiting /*, ... */ ) { var tr = dom.createElement( 'tr' ); // the first argument is the id var id = arguments[ 0 ]; tr.id = '_testcase_' + id; // the second argument will determine the row color (looksgood) tr.className = ( ( arguments[ 1 ] ) ? 'good' : 'bad' ) + ( ( arguments[ 2 ] ) ? ' waiting' : '' ); // all other arguments will be cells for ( var i = 3; i < arguments.length; i++ ) { var td = dom.createElement( 'td' ); td.innerHTML = arguments[ i ]; td.className = headings[ i - 3 ].toLowerCase(); // first cell will contain a hyperlink for auto-loading on // visit if ( i === 3 ) { var a = dom.createElement( 'a' ); a.href = ( '#prior/' + id ); a.innerHTML = td.innerHTML; td.innerHTML = ''; td.appendChild( a ); a.addEventListener( 'click', function( e ) { doLoad( id ); } ); } tr.appendChild( td ); } table.appendChild( tr ); }; table.setCount = function( count ) { table.querySelector( '.count' ).innerHTML = +count; }; table.mark = function( id, type ) { table.querySelector( '#_testcase_' + id ).className = type; }; table.changePremium = function( id, premium ) { var element = table.querySelector( '#_testcase_' + id + ' .premium' ); // add the value and retain the previous value element.innerHTML = '$' + premium + '
(was ' + element.innerHTML + ')
'; }; table.changeComment = function( id, comment ) { var element = table.querySelector( '#_testcase_' + id + ' .description' ); element.innerHTML = comment.replace( /\n/g, '
' ); }; function doLoad( id ) { exports.hideLoad(); // give them an indication that something is happening setTimeout( function() { loadPriorTestCase( id ); }, 0 ); } // when a row is clicked, trigger the load table.addEventListener( 'click', function( e ) { // we care only of row clicks if ( e.target.nodeName.toLowerCase() !== 'td' ) { return; } // get the unique id for this test case var id = e.target.parentNode.id.replace( /^_testcase_/, '' ); doLoad( id ); } ); getPriorTable = function() { return table; }; return getPriorTable(); }; function loadPrior() { // first, clear out any existing results getPriorTable().clear(); // load prior data from server var response = getXhrJsonSync( 'GET', prior_url ), table = getPriorTable(), results = response.results; // store the current set of test cases curset = results; // add test test case to the table for ( testcase in results ) { var result = results[ testcase ]; table.addRow( result.id, result.looksgood, result.waiting, result.date, // comment ( result.comment.replace( /\n/g, '
' ) || '(no comment)' ), // username (from hostname) getUserFromHostname( result.hostname ), // premium ( '$' + ( result.premium || 0.00 ) ), // expected premium ( ( result.expected ) ? '$' + result.expected : ( result.looksgood ) ? '$' + result.premium : '-' ) ); } table.setCount( results.length ); } function getTestCaseData( id ) { return getXhrJsonSync( 'GET', prior_url + '&id=' + id ); } function getQuoteData( id, qdata_host ) { try { return getXhrJsonSync( 'GET', prior_url + '&host=' + qdata_host + '&qid=' + id ); } catch ( e ) { return { error: 'Invalid response from server.' }; } } function showRatingResultPage() { dom.location.hash = 'test-data'; } function loadQuote( qid, host, bucket_override ) { var data = getQuoteData( qid, host ); if ( data.error !== 'OK' ) { alert( data.error ); return; } rater.fromMap( data.results.bucket, function( data ) { bucket = data; emptyBucket(); if ( bucket_override ) { overrideBucket( bucket_override ); } showRatingResultPage(); rate( bucket ); } ); } function loadPriorTestCase( id ) { var casedata = getTestCaseData( id ); if ( casedata.status !== 200 ) { alert( 'Could not load test case.\n\n' + casedata.error ); } var data = casedata.results; // display the message so that they know what they're looking at exports.setPriorMessage( data.hostname, data.comment, data.looksgood, id ); // overwrite the bucket bucket = data.bucket; emptyBucket(); // set this test case so that our next save overwrites it setTestCase( id, data.result ); // make it obvious to the user that the data has been loaded clearSummaryPremium(); showEntryForm(); // prefill the comment and expected data on the submission form, // leaving room at the top for additional comments document.getElementById( 'final-comments' ) .innerHTML = ( "\n\n\n" + getPrevSubmitCommentText( data.hostname, data.comment ) ); document.getElementById( 'final-expected' ).value = data.expected; // let the browser catch up and then perform rating setTimeout( function() { // switch to test data and rate document.location.hash = '#test-data'; rate( bucket ); }, 0 ); } function getPrevSubmitCommentText( hostname, comment ) { return "Previously submitted by " + getUserFromHostname( hostname ) + ": " + comment; } function retestAll( callback ) { var queue = [], skipped = 0, // regression test results, which may or may not be submitted to // the server history = {}; // queue each of the test cases for ( var testcase in curset ) { queue.push( curset[ testcase ] ); } var count = failures = changed = 0, start = ( new Date() ).getTime(); var run = function() { // do not pop(); we want to do them in order so it doesn't look // too odd to the user var test = queue.shift(); // all done if ( !( test ) ) { var time = ( new Date() ).getTime() - start; var msg = ( 'Test complete. Re-ran ' + count + ' test(s) with ' + failures + ' failure(s) in ' + ( time / 1000 ) + 's.' + "\n\n" + ( ( skipped ) ? skipped + " test(s) premium checks " + "were skipped because they " + "have no expected premium; please aid in the " + "automated testing of these by selecting " + "them and entering an expected premium when " + "re-submitting it (by clicking Incorrect). " + "These skipped tests are still noted if " + "their premiums changed (in italics), but " + "their success statuses are left untouched." : '' ) + ( ( failures === 0 ) ? ( !skipped ) ? "\n\nYou should feel pretty sweet right now." : '' : "\n\nSomeone's got some splainin' to do." ) + ( ( !changed ) ? "\n\nNo test cases have changed." : "\n\n" + changed + " case(s) changed." + "\n\nWould you like the results of this regression " + "to be recorded? This will cause the status of each " + "test case to be updated as shown. If unsure, click " + "'Cancel'." ) ); // if we have changes, show a box asking if the changes // should be uploaded to the server; otheriwse, just alert // (which will return undefined and cast to false) var submit = !!( changed && confirm || alert ) .call( window, msg ); if ( submit ) { saveRegression( history ); } callback && callback( count, failures, time ); return; } var table = getPriorTable(); table.mark( test.id, 'testing' ); setTimeout( function() { var testdata = getTestCaseData( test.id ).results; try { // rate, but do not update the screen rate( testdata.bucket, false, true ); } catch ( e ) { console.log( e ); table.mark( test.id, 'skip' ); // abort! abort! //alert( 'An error occurred. Aborting.' ); run(); return; } // determine what premium we're expecting (default to // existing premium) var expected = testdata.expected || testdata.result.premium, skipme = !( testdata.looksgood || testdata.expected ); var correct = ( rate_result.premium && ( rate_result.premium == expected ) ); // add to changed count if the status changed var has_changed = ( testdata.looksgood !== correct ); changed += ( +has_changed && !skipme ); skipped += +skipme; // only add to the total count if the premium was actually // compared if ( !skipme ) { count++; if ( !( correct ) ) { failures++; } // store in case the user decides to save to the server if ( has_changed ) { history[ test.id ] = { looksgood: correct, bucket: testdata.bucket, result: rate_result, expected: expected, comment: testdata.comment, hostname: testdata.hostname, previous: testdata.result, }; // show the comment that would be saved to the // server, should they choose to do so table.changeComment( test.id, genRegressionComment( history[ test.id ] ) ); } } // update table table.changePremium( test.id, rate_result.premium ); table.mark( test.id, ( ( ( correct ) ? 'good' : 'bad' ) + ( ( has_changed ) ? ' changed' : '' ) + ( ( rate_result.premium !== testdata.result.premium ) ? ' premchanged' : '' ) + ( ( skipme ) ? ' skipped' : '' ) ) ); // continue run(); }, 0 ); } // run 'em one by one setTimeout( run, 0 ); } function genRegressionComment( item ) { return "[Regression Test: " + ( ( item.looksgood ) ? "Pass" : "Fail" ) + "] Expected $" + item.expected + "; calculated $" + item.result.premium + "; previously $" + item.previous.premium + "\n\n" + getPrevSubmitCommentText( item.hostname, item.comment ); } function saveRegression( history ) { for ( var id in history ) { var item = history[ id ]; // generate comment var comment = genRegressionComment( item ); submitQuote( item.bucket, item.result, comment, item.looksgood, ( item.waiting || false ), item.expected, id, function() {} ); } } exports.initHtml = function() { getLoadDialog(); } exports.showLoad = function() { removeEntryFocus(); getLoadDialog().className += ' show'; loadPrior(); } exports.hideLoad = function() { var dialog = getLoadDialog(); dialog.className = dialog.className.replace( /\bshow\b/g, '' ); } exports.setPriorMessage = function( host, message, good, id ) { var container = document.getElementById( 'prior-message' ); if ( !container ) { return; } container.style.display = ( message ) ? 'inline-block' : 'none'; container.className = ( good ) ? 'good' : 'bad'; container.innerHTML = ( '' + getUserFromHostname( host ) + ': ' + message .replace( /^\n+|\n+$/g, '' ) .replace( / /g, '  ' ) .replace( /\t/g, '    ' ) .replace( /\n/g, '
' ) .replace( /(Previously submitted by [^:]+:)/g, '$1' ) + '

[Direct Link]' ); }; exports.loadQuote = loadQuote; exports.loadPriorTestCase = loadPriorTestCase; return exports; } )( document ); function begin() { // initialize prior div Prior.initHtml(); // allow linking to test cases var pmatch; if ( pmatch = document.location.href.match( /#prior(?:\/(.*))?$/ ) ) { var id = pmatch[ 1 ]; if ( !( id ) ) { // no id given; let them choose Prior.showLoad(); } else { // we were given an id; load it! console.log( 'Loading ' + id + '...' ); Prior.loadPriorTestCase( id ); } } // allow settings params from the URL (very basic parsing; barely used); we // use a colon rather than ? because ? is not included in the location // object var pdata; var bucket_override = []; if ( pdata = document.location.hash.match( /:(.*)$/ ) ) { try { // params delimited by & var params = pdata[1].split( '&' ); for ( var param in params ) { // values delimited from the name by = var valdata = params[ param ].split( '=' ), val = JSON.parse( valdata[ 1 ] ); bucket_override[ valdata[ 0 ] ] = val; bucket[ valdata[ 0 ] ] = val; console.log( 'Bucket override: ' + valdata[ 0 ] + '=' + val ); } overrideBucket( bucket_override ); } catch ( e ) { alert( 'Failed setting param values from URL.\n\n' + e.message ); } } // allow loading of quote ids var mdata; if ( mdata = document.location.hash.match( /#load\/([a-z]+)\/([0-9]+)/ ) ) { var host = mdata[1], id = mdata[2]; // load the quote Prior.loadQuote( id, host, bucket_override ); } } var vpt = [ 0, 0, 0 ], vpt_cur = 0; function voiPainterAdd( tr, value ) { var c = 'sel' + vpt_cur; tr.classList.toggle( c ); vpt[ vpt_cur ] += ( value * ( tr.classList.contains( c ) ? 1 : -1 ) ); vpt[ vpt_cur ] = +( vpt[ vpt_cur ].toFixed( 6 ) ); showVoiPainter( vpt[ vpt_cur ] ); } var vp_element = null, vpt_dest = []; function showVoiPainter( val ) { if ( !vp_element ) { vp_element = document.createElement( 'div' ); vp_element.id = 'voi-painter'; for ( var i in vpt ) { vpt_dest[ i ] = document.createElement( 'div' ); vpt_dest[ i ].classList.add( 'sel' + i ); vpt_dest[ i ].innerHTML = '0'; vp_element.appendChild( vpt_dest[ i ] ); ( function( i ) { vpt_dest[ i ].addEventListener( 'click', function() { vpt_cur = i; } ); } )( i ); } document.getElementById( 'test-data' ).appendChild( vp_element ); } vpt_dest[ vpt_cur ] .innerHTML = val; } return { updateSummaryDebug: updateSummaryDebug, onRate: function( callback ) { rate_callback = callback; }, begin: begin, Prior: Prior, }; } )();