/** * Contains Program base class * * Copyright (C) 2017 R-T Specialty, LLC. * * 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 . * * @todo This is one of the system's oldest relics; evolve! */ var AbstractClass = require( 'easejs' ).AbstractClass EventEmitter = require( 'events' ).EventEmitter, // XXX coupling Failure = require( '../validate/Failure' ), BucketField = require( '../field/BucketField' ); exports.Program = AbstractClass( 'Program' ) .extend( EventEmitter, { /** * Program id * @type {string} */ id: 'undefined', /** * Program title * @type {string} */ title: 'LoVullo Rater', eventData: [], /** * Stores program metadata * @type {Object} */ meta: {}, /** * Array of step titles * @type {Array.} */ steps: [], /** * Contains sidebar data * @type {Object} */ sidebar: { overview: {}, static_content: {} }, /** * Questions that should only be visible internally * @type {Array.} */ internal: [], /** * Default values for questions * @type {Object.} */ defaults: {}, /** * Fields contained within groups * @type {Object.>} */ groupFields: {}, /** * API descriptions * @type {Object} */ 'protected apis': {}, 'public secureFields': [], 'private _assertDepth': 0, 'protected classifier': '', 'private _classify': null, 'private _classifyKnownFields': {}, /** * Id of the first valid step * * Useful if early steps are used for, say, management purposes. * * @type {number} */ 'protected firstStepId': 1, /** * Data API manager * @type {DataApiManager} */ 'protected dapiManager': null, /** * Initialize program * * @param {DataApiManager} dapi_manager */ __construct: function( dapi_manager ) { if ( dapi_manager ) { this.dapiManager = dapi_manager; this.dapiManager.setApis( this.apis ); } try { this._classify = ( this.classifier ) ? require( this.classifier ) : function() { return {}; }; } catch ( e ) { throw Error( "Failed to load global classifier: " + e.message ); } this._initClasses(); }, 'private _initClasses': function() { var fieldc = ( this._classify.fieldClasses || {} ); for ( var field in fieldc ) { // UI is considered to completely override (may change) this.whens[ field ] = this.whens[ field ] || [ fieldc[ field ] ]; } this._initKnownClassFields(); }, 'private _initKnownClassFields': function() { var known = this._classifyKnownFields; // maintain BC for the time being (old and new API, respectively) var cfields = this._classify.knownFields || this._classify.rater.knownFields; // from global classifier for ( var f in cfields ) { known[ f ] = true; } // from whens that reference questions in the UI directly for ( var f in ( this.qwhens || {} ) ) { known[ f ] = true; } }, /** * Returns the program id * * @return String program id */ getId: function() { return this.id; }, 'abstract public initQuote': [ 'bucket', 'store_only' ], submit: function( step_id, bucket, cmatch, trigger_callback ) { // if there are any pending data api calls, do not // bother validating the rest of the step data var pending_request = this._getPendingApiCall( step_id ); if ( pending_request !== null ) { return pending_request; } trigger_callback = trigger_callback || function() {}; var callback = this.eventData[ step_id ].submit; // if there was no callback for this step, then we haven't anything // to do if ( callback === undefined ) { return true; } return callback.call( this, bucket, {}, cmatch, trigger_callback ); }, postSubmit: function( step_id, bucket, trigger_callback ) { trigger_callback = trigger_callback || function() {}; // make sure that the step they're trying to load actually exists if ( this.eventData[ step_id ] === undefined ) { return false; } var callback = this.eventData[ step_id ].postSubmit; if ( callback === undefined ) { return false; } callback.call( this, trigger_callback, bucket ); return true; }, /** * Trigger processing of `forward' event * * @param {number} step_id step id to trigger event on * @param {function()} trigger_callback callback for event triggers * * @return {Object} failures */ forward: function( step_id, bucket, cmatch, trigger_callback ) { trigger_callback = trigger_callback || function() {}; var callback = ( this.eventData[ step_id ] || {} ).forward; if ( callback === undefined ) { return null; } return callback.call( this, bucket, {}, cmatch, trigger_callback ); }, 'public change': function( step_id, name, bucket, diff, cmatch, trigger_callback ) { var change = ( this.eventData[ step_id ] || {} ).change; if ( !change || !( change[ name ] ) ) { return null; } return change[ name ].call( this, bucket, diff, cmatch, trigger_callback ); }, 'public dapi': function( step_id, name, bucket, diff, cmatch, trigger_callback ) { var dapi = ( this.eventData[ step_id ] || {} ).dapi; if ( !dapi || !( dapi[ name ] ) ) { return null; } return dapi[ name ].call( this, bucket, diff, cmatch, trigger_callback ); }, 'public addFailure': function( dest, name, indexes, message, cause_names ) { var to = dest[ name ] = dest[ name ] || []; for ( var i in indexes ) { var index = indexes[ i ], field = BucketField( name, index ), causes = []; for ( var cause_i in cause_names ) { causes.push( BucketField( cause_names[ cause_i ], index ) ); } to[ index ] = Failure( field, message, causes ); } }, 'public action': function( step_id, type, ref, index, bucket, trigger_callback ) { var action = ( this.eventData[ step_id ] || {} ).action; // the double-negative-or prevents the latter from being executed // (yielding an error) if the former fails if ( !action || !action[ ref ] ) { return this; } // attempt to locate this type of action for the given ref var action = action[ ref ][ type ]; if ( action === undefined ) { return this; } // found it! action.call( this, trigger_callback, bucket, index ); return this; }, /** * Determine if any Api Calls are still pending * * @param {integer} step id to get pending api calls for * * @return {object|null} null if none are pending otherwise message */ 'private _getPendingApiCall': function( step_id ) { if ( !this.dapiManager ) { return null; } var changes = this.eventData[ step_id ].change, pending = this.dapiManager.getPendingApiCalls(); for ( var id in pending ) { if ( pending[ id ] === undefined ) { continue; } if ( pending[ id ].uid !== undefined ) { // no reason to check any further, return first pending // api call var ret_val = {}, failed = [], name = pending[ id ].name, index = pending[ id ].index; // we only care if this data api request is associated // to this step if ( changes[ name ] !== undefined ) { this.addFailure( failed, name, [ index ], 'Question is still loading; please wait...', [ name ] ); ret_val[ name ] = failed[ name ]; return ( ret_val ); } } } // no pending requests return null; }, eachChangeById: function( step_id, callback, trigger_callback ) { trigger_callback = trigger_callback || function() {}; var change = this.eventData[ step_id ].change; // if there's no change events, we don't need to do anything if ( change === undefined ) { return this; } // call the callback for each element that has a change function var program = this; for ( name in change ) { // use a closure to ensure that the variable we pass in will not be // changed (remember, we're in a loop) ( function( change_callback ) { // call the callback, passing in a callback of our own to be // called when the change event is triggered callback.call( program, name, function( bucket, diff, cmatch ) { // run the assertions and return the result return change_callback.call( program, bucket, diff, cmatch, trigger_callback ); }, +step_id ); } )( change[name] ); } return this; }, beforeLoadStep: function( step_id, bucket, trigger_callback ) { trigger_callback = trigger_callback || function() {}; // make sure that the step they're trying to load actually exists if ( this.eventData[ step_id ] === undefined ) { return false; } var callback = this.eventData[ step_id ].beforeLoad; if ( callback === undefined ) { return false; } callback.call( this, trigger_callback, bucket ); return true; }, visitStep: function( step_id, bucket ) { // make sure that the step they're trying to load actually exists if ( this.eventData[ step_id ] === undefined ) { return false; } var callback = this.eventData[ step_id ].visit; if ( callback === undefined ) { return false; } callback.call( this, function() {}, bucket ); return true; }, doAssertion: function( assertion, qid, expected, given, success, failure, record ) { var thisresult = false, result = false; this._assertDepth++; if ( assertion.assert( expected, given ) ) { thisresult = true; result = ( success ) ? success.call( this ) : true; } else { thisresult = false; result = ( failure ) ? failure.call( this ) : false; } this._assertDepth--; this.emit( 'assert', assertion, qid, expected, given, thisresult, result, record, this._assertDepth ); return result; }, /** * Classify the given bucket data * * @param {Object} data bucket data to classify * * @return {Object.>} classification results */ 'public classify': function( data ) { // maintain BC for the time being (new and old respectively); can be // cleaned up to be less verbose once we remove compatibility var result = ( this._classify.rater ) ? this._classify.rater.classify.fromMap( data, false ) : this._classify( data ); // add qwhens (TODO: let's not do this every single time; use a diff) this.qwhens = this.qwhens || {}; for ( var f in this.qwhens ) { var values = data[ f ], match = [], is = true, expect = this.qwhens[ f ]; for ( var i in values ) { match[ i ] = +( !!( ( values[ i ] !== '0' ) && !!values[ i ] ) === expect ); is = is && !!match[ i ]; } result[ 'q:' + f ] = { indexes: match, is: is }; } return result; }, /** * Checks the given indexes against classification matches * * If the cmatch array indicates that the given index does not match its * required classification, then the index will be removed. This has the * effect of ignoring indexes for fields that do not match their required * classifications. * * The index array should be an array of index numbers. The values of the * index array will be used to check the associated index of the cmatch * array for a boolean value. * * @param {Array.} cmatch match array * @param {Array.} indexes indexes to check * * @return {Array.} cmatch-filtered index array */ 'protected cmatchCheck': function( cmatch, indexes ) { // if there's no cmatch data for this field, or if the cmatch data // exists but is empty (indicating a true match for any number of // indexes) then simply return what we were given if ( !cmatch || ( cmatch.length === 0 ) ) { return indexes; } var ret = [], len = indexes.length; // return the indexes of only the available cmatch indexes for ( var i = 0; i < len; i++ ) { var index = indexes[ i ]; if ( cmatch[ index ] ) { ret.push( index ); } } return ret; }, 'public getClassifierKnownFields': function() { return this._classifyKnownFields; }, 'public getFirstStepId': function() { return this.firstStepId; } } );