From 9907c698d1c364005fee47168a076aedc470859f Mon Sep 17 00:00:00 2001 From: Mark Goldsmith Date: Thu, 7 Jun 2018 12:33:13 -0400 Subject: [PATCH 01/10] [DEV-2871] Added DocumentProgramFormatter to format program data by step, group and field metadata --- src/document/DocumentProgramFormatter.js | 230 ++++++++++++++++ src/server/daemon/controller.js | 24 ++ test/document/DocumentProgramFormatterTest.js | 247 ++++++++++++++++++ 3 files changed, 501 insertions(+) create mode 100644 src/document/DocumentProgramFormatter.js create mode 100644 test/document/DocumentProgramFormatterTest.js diff --git a/src/document/DocumentProgramFormatter.js b/src/document/DocumentProgramFormatter.js new file mode 100644 index 0000000..dedb49e --- /dev/null +++ b/src/document/DocumentProgramFormatter.js @@ -0,0 +1,230 @@ +/** + * Formats program bucket data + * + * Copyright (C) 2018 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 Affero 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 Affero General Public License + * along with this program. If not, see . + */ + +"use strict"; + +const Class = require( 'easejs' ).Class; + + +/** + * Formats program bucket data + * + * This takes a document and formats the data in a + * structured manner. + */ +module.exports = Class( 'DocumentProgramFormatter', +{ + /** + * Current program + * + * @type {Program} + */ + 'private _program': null, + + /** + * Performs classification matching on fields + * + * A field will have a positive match for a given index if all of its + * classes match + * + * @type {FieldClassMatcher} + */ + 'private _class_matcher': null, + + + /** + * Initialize document formatter + * + * @param {Program} program active program + * @param {FieldClassMatcher} class_matcher class/field matcher + */ + constructor( program, class_matcher ) + { + this._program = program; + this._class_matcher = class_matcher; + }, + + + /** + * Returns formatted document bucket data + * Calls FieldClassMatcher.match to retrieve + * index of show/hide values for each field + * + * @param {Bucket} bucket document bucket + * + * @return {Object} JSON object + */ + 'public format'( bucket ) + { + return new Promise( ( resolve, reject ) => + { + const cmatch = this._program.classify( bucket.getData() ); + + this._class_matcher.match( cmatch, ( field_matches ) => + { + const len = this._program.steps.length; + const data = this._parseSteps( len, bucket, field_matches ); + + resolve( data ); + } ); + + } ); + }, + + + /** + * Parses step data + * + * @param {Integer} len step length + * @param {Bucket} bucket document bucket + * @param {Array} field_matches array of matches for fields + * + * @return {Object} step data + */ + 'private _parseSteps'( len, bucket, field_matches ) + { + const data = { steps: [] }; + + for ( let i = 1; i < len; i++ ) + { + const step = {}; + const step_groups = this._program.steps[ i ].groups; + + const groups = this._parseGroups( step_groups, bucket, field_matches ); + + step.title = this._program.steps[ i ].title; + step.groups = groups; + + data.steps.push( step ); + } + + return data; + }, + + + /** + * Parses group data + * + * @param {Array} step_groups array of group data + * @param {Bucket} bucket document bucket + * @param {Array} field_matches array of matches for fields + * + * @return {Array} array of groups + */ + 'private _parseGroups'( step_groups, bucket, field_matches ) + { + const groups = []; + + for ( let group in step_groups ) + { + const step_group = {}; + const group_id = step_groups[ group ]; + const group_title = this._program.groups[ group_id ].title || ""; + const fields = this._program.groupExclusiveFields[ group_id ]; + + const questions = this._parseFields( fields, bucket, field_matches ); + + step_group.title = group_title; + step_group.questions = questions; + groups.push( step_group ); + } + + return groups; + }, + + + /** + * Parses fields/question data + * + * @param {Array} fields array of field data + * @param {Bucket} bucket document bucket + * @param {Array} field_matches array of matches for fields + * + * @return {Array} array of questions + */ + 'private _parseFields'( fields, bucket, field_matches ) + { + const questions = []; + + const classes = field_matches.__classes; + + for ( let field in fields ) + { + const field_id = fields[ field ]; + + // Don't include fields that are not in program.fields + if ( typeof this._program.fields[ field_id ] === "undefined" ) + { + continue; + } + + const field_value = bucket.getDataByName( field_id ); + const field_label = this._program.fields[ field_id ].label; + const question = {}; + + question.id = field_id; + question.label = field_label; + question.value = field_value; + question.applicable = this._getApplicable( + classes, + field_id, + field_value + ); + + questions.push( question ); + } + + return questions; + }, + + + /** + * Determine when a field is shown by index + * Map boolean values of [0, 1] to [true, false] + * + * @param {Object} classes object of visibility classes + * @param {String} field_id id of field + * @param {Object} field_value field object + * + * @return {Array.} array of booleans + */ + 'private _getApplicable'( classes, field_id, field_value ) + { + // If object is undefined, default to array of true + if ( typeof this._program.whens[ field_id ] === "undefined" ) + { + return field_value.map( _ => true ); + } + + const class_id = this._program.whens[ field_id ][ 0 ]; + const indexes = classes[ class_id ].indexes; + + // Map indexes of 0, 1 to true, false + if ( Array.isArray( indexes ) ) + { + return indexes.map( x => !!x ); + } + else + { + return field_value.map( _ => !!indexes ); + } + }, +} ); diff --git a/src/server/daemon/controller.js b/src/server/daemon/controller.js index 17fda41..f494162 100644 --- a/src/server/daemon/controller.js +++ b/src/server/daemon/controller.js @@ -53,6 +53,14 @@ const { DataApiManager, }, + document: { + DocumentProgramFormatter, + }, + + field: { + FieldClassMatcher, + }, + server: { DocumentServer, @@ -450,6 +458,22 @@ function doRoute( program, request, data, resolve, reject ) } ); } ); } + else if ( cmd == 'progdata' ) + { + acquireReadLock( quote_id, request, function() + { + handleRequest( function( quote ) + { + const response = UserResponse( request ); + const bucket = quote.getBucket(); + const class_matcher = FieldClassMatcher( program.whens ); + + DocumentProgramFormatter( program, class_matcher ).format( bucket ) + .then( data => response.ok( data ) ) + .catch( e => response.error( e ) ); + } ); + } ); + } else if ( cmd === 'mkrev' ) { // the database operation for this is atomic and disjoint from diff --git a/test/document/DocumentProgramFormatterTest.js b/test/document/DocumentProgramFormatterTest.js new file mode 100644 index 0000000..630e894 --- /dev/null +++ b/test/document/DocumentProgramFormatterTest.js @@ -0,0 +1,247 @@ +/** + * Test of DocumentProgramFormatter + * + * Copyright (C) 2018 R-T Specialty, LLC. + * + * This file is part of liza. + * + * 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 . + * + */ + +'use strict'; + +const chai = require( 'chai' ); +const expect = chai.expect; +const sinon = require( 'sinon' ); + +const { + document: { + DocumentProgramFormatter: Sut, + }, + + field: { + FieldClassMatcher, + }, + +} = require( '../../' ); + +chai.use( require( 'chai-as-promised' ) ); + + +describe( 'DocumentProgramFormatter', () => +{ + it( "formats bucket data", () => + { + const bucket_data = { + sell_alcohol: [ "foo", "" ], + serve_alcohol: [ "" ], + sell_ecigs: [ "", "bar" ], + dist_ecigs: [ "" ], + field_no_label: [ "" ], + field_no_array: [ "bar" ], + field_no_vis: [ "true" ] + }; + + const expected_object = { + steps: [ + { + title: "Manage Quote", + groups: [] + }, + { + title: "General Information", + groups: [ + { + title: "Group One", + questions: [ + { + id: "sell_alcohol", + label: "Does the insured sell alcohol?", + value: [ "foo", "" ], + applicable: [ true, false ] + }, + { + id: "serve_alcohol", + label: "Does the insured serve alcohol?", + value: [ "" ], + applicable: [ false ] + }, + { + id: "field_no_vis", + label: "Does this field have a visibility class?", + value: [ "true" ], + applicable: [ true ] + } + ] + }, + { + title: "", + questions: [ + { + id: "sell_ecigs", + label: "Does the insured sell e-cigarettes?", + value: [ "", "bar" ], + applicable: [ false, true ] + }, + { + id: "dist_ecigs", + label: "Does the Insured distribute Electronic Cigarette products?", + value: [ "" ], + applicable: [ false ] + }, + { + id: "field_no_array", + label: "Does this field have an array for the visibility class?", + value: [ "bar" ], + applicable: [ true ] + } + ] + } + ] + } + ] + }; + + const bucket = createStubBucket( bucket_data ); + const program = createStubProgram(); + const class_matcher = createStubClassMatcher(); + + return expect( + Sut( program, class_matcher ).format( bucket ) + ).to.eventually.deep.equal( expected_object ); + } ); + +} ); + + +function createStubClassMatcher() +{ + return { + match( _, callback ) + { + callback({ + __classes: + { + '--vis-sell-alcohol': { is: true, indexes: [1,0] }, + '--vis-serve-alcohol': { is: false, indexes: [0] }, + '--vis-sell-ecigs': { is: false, indexes: [0,1] }, + '--vis-dist-ecigs': { is: true, indexes: [0] }, + '--vis-no-array': { is: true, indexes: 1 }, + } + }) ; + } + } +} + + +function createStubBucket( metadata ) +{ + return { + getDataByName: name => metadata[ name ], + getData() + { + return metadata; + }, + }; +} + + +function createStubProgram() +{ + return { + steps: [ + { + title: "Index 0", + groups: [] + }, + { + title: "Manage Quote", + groups: [] + }, + { + title: "General Information", + groups: [ 'group_one', 'group_two' ] + } + ], + classify( bucket_data ) + { + return {} + }, + groups: + { + 'group_one': + { + title: "Group One" + }, + 'group_two': {}, + }, + fields: + { + sell_alcohol: + { + label: "Does the insured sell alcohol?", + type: "noyes", + required: "true", + }, + serve_alcohol: + { + label: "Does the insured serve alcohol?", + type: "noyes", + required: "true" + }, + sell_ecigs: + { + label: "Does the insured sell e-cigarettes?", + type: "noyes", + required: "true" + }, + dist_ecigs: + { + label: "Does the Insured distribute Electronic Cigarette products?", + type: "noyes", + required: "true" + }, + field_no_array: + { + label: "Does this field have an array for the visibility class?", + type: "noyes", + required: "true" + }, + field_no_vis: + { + label: "Does this field have a visibility class?", + type: "noyes", + required: "true" + } + }, + groupExclusiveFields: + { + 'group_one': [ "sell_alcohol", "serve_alcohol", "field_no_label", "field_no_vis" ], + 'group_two': [ "sell_ecigs", "dist_ecigs", "field_no_array" ], + + }, + whens: + { + sell_alcohol: [ "--vis-sell-alcohol" ], + serve_alcohol: [ "--vis-serve-alcohol" ], + sell_ecigs: [ "--vis-sell-ecigs" ], + dist_ecigs: [ "--vis-dist-ecigs" ], + field_no_array: [ "--vis-no-array" ], + }, + }; +} + + + From 856c9a1a83837c2bcb6e091b740f439f299613db Mon Sep 17 00:00:00 2001 From: Mark Goldsmith Date: Thu, 7 Jun 2018 12:55:45 -0400 Subject: [PATCH 02/10] [DEV-2871] Passed __classes directly into private functions, updated documentation --- src/document/DocumentProgramFormatter.js | 45 +++++++++---------- test/document/DocumentProgramFormatterTest.js | 16 +------ 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/document/DocumentProgramFormatter.js b/src/document/DocumentProgramFormatter.js index dedb49e..6c20fd5 100644 --- a/src/document/DocumentProgramFormatter.js +++ b/src/document/DocumentProgramFormatter.js @@ -27,8 +27,8 @@ const Class = require( 'easejs' ).Class; /** * Formats program bucket data * - * This takes a document and formats the data in a - * structured manner. + * This takes a document and formats the bucket data in a + * structured manner of steps, groups and fields metadata */ module.exports = Class( 'DocumentProgramFormatter', { @@ -65,12 +65,12 @@ module.exports = Class( 'DocumentProgramFormatter', /** * Returns formatted document bucket data - * Calls FieldClassMatcher.match to retrieve - * index of show/hide values for each field + * Calls FieldClassMatcher.match to retrieve index of show/hide + * values for each field * * @param {Bucket} bucket document bucket * - * @return {Object} JSON object + * @return {Promise.} a promise of a data object */ 'public format'( bucket ) { @@ -80,8 +80,9 @@ module.exports = Class( 'DocumentProgramFormatter', this._class_matcher.match( cmatch, ( field_matches ) => { - const len = this._program.steps.length; - const data = this._parseSteps( len, bucket, field_matches ); + const len = this._program.steps.length; + const classes = field_matches.__classes; + const data = this._parseSteps( len, bucket, classes ); resolve( data ); } ); @@ -93,13 +94,13 @@ module.exports = Class( 'DocumentProgramFormatter', /** * Parses step data * - * @param {Integer} len step length - * @param {Bucket} bucket document bucket - * @param {Array} field_matches array of matches for fields + * @param {Integer} len step length + * @param {Bucket} bucket document bucket + * @param {Object} classes class/field matches * * @return {Object} step data */ - 'private _parseSteps'( len, bucket, field_matches ) + 'private _parseSteps'( len, bucket, classes ) { const data = { steps: [] }; @@ -108,7 +109,7 @@ module.exports = Class( 'DocumentProgramFormatter', const step = {}; const step_groups = this._program.steps[ i ].groups; - const groups = this._parseGroups( step_groups, bucket, field_matches ); + const groups = this._parseGroups( step_groups, bucket, classes ); step.title = this._program.steps[ i ].title; step.groups = groups; @@ -123,13 +124,13 @@ module.exports = Class( 'DocumentProgramFormatter', /** * Parses group data * - * @param {Array} step_groups array of group data - * @param {Bucket} bucket document bucket - * @param {Array} field_matches array of matches for fields + * @param {Array} step_groups array of group data + * @param {Bucket} bucket document bucket + * @param {Object} classes class/field matches * * @return {Array} array of groups */ - 'private _parseGroups'( step_groups, bucket, field_matches ) + 'private _parseGroups'( step_groups, bucket, classes ) { const groups = []; @@ -140,7 +141,7 @@ module.exports = Class( 'DocumentProgramFormatter', const group_title = this._program.groups[ group_id ].title || ""; const fields = this._program.groupExclusiveFields[ group_id ]; - const questions = this._parseFields( fields, bucket, field_matches ); + const questions = this._parseFields( fields, bucket, classes ); step_group.title = group_title; step_group.questions = questions; @@ -154,18 +155,16 @@ module.exports = Class( 'DocumentProgramFormatter', /** * Parses fields/question data * - * @param {Array} fields array of field data - * @param {Bucket} bucket document bucket - * @param {Array} field_matches array of matches for fields + * @param {Array} fields array of field data + * @param {Bucket} bucket document bucket + * @param {Object} classes class/field matches * * @return {Array} array of questions */ - 'private _parseFields'( fields, bucket, field_matches ) + 'private _parseFields'( fields, bucket, classes ) { const questions = []; - const classes = field_matches.__classes; - for ( let field in fields ) { const field_id = fields[ field ]; diff --git a/test/document/DocumentProgramFormatterTest.js b/test/document/DocumentProgramFormatterTest.js index 630e894..86aec98 100644 --- a/test/document/DocumentProgramFormatterTest.js +++ b/test/document/DocumentProgramFormatterTest.js @@ -24,17 +24,11 @@ const chai = require( 'chai' ); const expect = chai.expect; -const sinon = require( 'sinon' ); const { document: { DocumentProgramFormatter: Sut, }, - - field: { - FieldClassMatcher, - }, - } = require( '../../' ); chai.use( require( 'chai-as-promised' ) ); @@ -42,7 +36,7 @@ chai.use( require( 'chai-as-promised' ) ); describe( 'DocumentProgramFormatter', () => { - it( "formats bucket data", () => + it( "formats bucket data by steps, groups and fields", () => { const bucket_data = { sell_alcohol: [ "foo", "" ], @@ -122,7 +116,6 @@ describe( 'DocumentProgramFormatter', () => Sut( program, class_matcher ).format( bucket ) ).to.eventually.deep.equal( expected_object ); } ); - } ); @@ -163,8 +156,7 @@ function createStubProgram() return { steps: [ { - title: "Index 0", - groups: [] + title: "Index 0" }, { title: "Manage Quote", @@ -230,7 +222,6 @@ function createStubProgram() { 'group_one': [ "sell_alcohol", "serve_alcohol", "field_no_label", "field_no_vis" ], 'group_two': [ "sell_ecigs", "dist_ecigs", "field_no_array" ], - }, whens: { @@ -242,6 +233,3 @@ function createStubProgram() }, }; } - - - From e2461a023c60c343283f0d0bb311ef24ad6f239c Mon Sep 17 00:00:00 2001 From: Mark Goldsmith Date: Thu, 7 Jun 2018 15:54:09 -0400 Subject: [PATCH 03/10] [DEV-2871] Added field type to DocumentProgramFormatter --- src/document/DocumentProgramFormatter.js | 2 ++ test/document/DocumentProgramFormatterTest.js | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/document/DocumentProgramFormatter.js b/src/document/DocumentProgramFormatter.js index 6c20fd5..9e73737 100644 --- a/src/document/DocumentProgramFormatter.js +++ b/src/document/DocumentProgramFormatter.js @@ -177,11 +177,13 @@ module.exports = Class( 'DocumentProgramFormatter', const field_value = bucket.getDataByName( field_id ); const field_label = this._program.fields[ field_id ].label; + const field_type = this._program.fields[ field_id ].type; const question = {}; question.id = field_id; question.label = field_label; question.value = field_value; + question.type = field_type; question.applicable = this._getApplicable( classes, field_id, diff --git a/test/document/DocumentProgramFormatterTest.js b/test/document/DocumentProgramFormatterTest.js index 86aec98..5adc649 100644 --- a/test/document/DocumentProgramFormatterTest.js +++ b/test/document/DocumentProgramFormatterTest.js @@ -64,18 +64,21 @@ describe( 'DocumentProgramFormatter', () => id: "sell_alcohol", label: "Does the insured sell alcohol?", value: [ "foo", "" ], + type: "noyes", applicable: [ true, false ] }, { id: "serve_alcohol", label: "Does the insured serve alcohol?", value: [ "" ], + type: "noyes", applicable: [ false ] }, { id: "field_no_vis", label: "Does this field have a visibility class?", value: [ "true" ], + type: "noyes", applicable: [ true ] } ] @@ -87,18 +90,21 @@ describe( 'DocumentProgramFormatter', () => id: "sell_ecigs", label: "Does the insured sell e-cigarettes?", value: [ "", "bar" ], + type: "noyes", applicable: [ false, true ] }, { id: "dist_ecigs", label: "Does the Insured distribute Electronic Cigarette products?", value: [ "" ], + type: "noyes", applicable: [ false ] }, { id: "field_no_array", label: "Does this field have an array for the visibility class?", value: [ "bar" ], + type: "noyes", applicable: [ true ] } ] From 14a8af2282fa964f336b63013e27cdd8af9155db Mon Sep 17 00:00:00 2001 From: Mark Goldsmith Date: Thu, 7 Jun 2018 17:04:36 -0400 Subject: [PATCH 04/10] [DEV-2871] Added link and id to group in DocumentProgramFormatter --- src/document/DocumentProgramFormatter.js | 5 ++++- test/document/DocumentProgramFormatterTest.js | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/document/DocumentProgramFormatter.js b/src/document/DocumentProgramFormatter.js index 9e73737..30cfb4f 100644 --- a/src/document/DocumentProgramFormatter.js +++ b/src/document/DocumentProgramFormatter.js @@ -139,11 +139,14 @@ module.exports = Class( 'DocumentProgramFormatter', const step_group = {}; const group_id = step_groups[ group ]; const group_title = this._program.groups[ group_id ].title || ""; + const group_link = this._program.groups[ group_id ].link || ""; const fields = this._program.groupExclusiveFields[ group_id ]; const questions = this._parseFields( fields, bucket, classes ); - step_group.title = group_title; + step_group.id = group_id; + step_group.title = group_title; + step_group.link = group_link; step_group.questions = questions; groups.push( step_group ); } diff --git a/test/document/DocumentProgramFormatterTest.js b/test/document/DocumentProgramFormatterTest.js index 5adc649..1b64161 100644 --- a/test/document/DocumentProgramFormatterTest.js +++ b/test/document/DocumentProgramFormatterTest.js @@ -58,7 +58,9 @@ describe( 'DocumentProgramFormatter', () => title: "General Information", groups: [ { + id: "group_one", title: "Group One", + link: "locations", questions: [ { id: "sell_alcohol", @@ -84,7 +86,9 @@ describe( 'DocumentProgramFormatter', () => ] }, { + id: "group_two", title: "", + link: "", questions: [ { id: "sell_ecigs", @@ -181,7 +185,8 @@ function createStubProgram() { 'group_one': { - title: "Group One" + title: "Group One", + link: "locations" }, 'group_two': {}, }, From 28fef169718a0aea5d1b6a8a7c338f65ddd91123 Mon Sep 17 00:00:00 2001 From: Andrew Elbaneh Date: Mon, 11 Jun 2018 10:33:03 -0400 Subject: [PATCH 05/10] [DEV-2871] cleaned up some of the code based on the merge request --- src/document/DocumentProgramFormatter.js | 57 +++++++++--------------- src/server/daemon/controller.js | 3 +- 2 files changed, 23 insertions(+), 37 deletions(-) diff --git a/src/document/DocumentProgramFormatter.js b/src/document/DocumentProgramFormatter.js index 30cfb4f..7cd4b3b 100644 --- a/src/document/DocumentProgramFormatter.js +++ b/src/document/DocumentProgramFormatter.js @@ -64,9 +64,7 @@ module.exports = Class( 'DocumentProgramFormatter', /** - * Returns formatted document bucket data - * Calls FieldClassMatcher.match to retrieve index of show/hide - * values for each field + * Formats document data into structure similar to program * * @param {Bucket} bucket document bucket * @@ -94,7 +92,7 @@ module.exports = Class( 'DocumentProgramFormatter', /** * Parses step data * - * @param {Integer} len step length + * @param {Number} len step length * @param {Bucket} bucket document bucket * @param {Object} classes class/field matches * @@ -132,26 +130,18 @@ module.exports = Class( 'DocumentProgramFormatter', */ 'private _parseGroups'( step_groups, bucket, classes ) { - const groups = []; - - for ( let group in step_groups ) + return step_groups.map( group_id => { - const step_group = {}; - const group_id = step_groups[ group ]; - const group_title = this._program.groups[ group_id ].title || ""; - const group_link = this._program.groups[ group_id ].link || ""; - const fields = this._program.groupExclusiveFields[ group_id ]; + const fields = this._program.groupExclusiveFields[ group_id ]; + const { title, link } = this._program.groups[ group_id ]; - const questions = this._parseFields( fields, bucket, classes ); - - step_group.id = group_id; - step_group.title = group_title; - step_group.link = group_link; - step_group.questions = questions; - groups.push( step_group ); - } - - return groups; + return { + id: group_id, + title: title || "", + link: link || "", + questions: this._parseFields( fields, bucket, classes ), + }; + } ); }, @@ -178,22 +168,17 @@ module.exports = Class( 'DocumentProgramFormatter', continue; } - const field_value = bucket.getDataByName( field_id ); - const field_label = this._program.fields[ field_id ].label; - const field_type = this._program.fields[ field_id ].type; - const question = {}; + const value = bucket.getDataByName( field_id ); + const { label, type } = this._program.fields[ field_id ]; - question.id = field_id; - question.label = field_label; - question.value = field_value; - question.type = field_type; - question.applicable = this._getApplicable( - classes, - field_id, - field_value - ); + questions.push( { + id: field_id, + label: label || "", + value: value, + type: type || "", + applicable: this._getApplicable( classes, field_id, value ), + } ); - questions.push( question ); } return questions; diff --git a/src/server/daemon/controller.js b/src/server/daemon/controller.js index f494162..1229829 100644 --- a/src/server/daemon/controller.js +++ b/src/server/daemon/controller.js @@ -468,7 +468,8 @@ function doRoute( program, request, data, resolve, reject ) const bucket = quote.getBucket(); const class_matcher = FieldClassMatcher( program.whens ); - DocumentProgramFormatter( program, class_matcher ).format( bucket ) + DocumentProgramFormatter( program, class_matcher ) + .format( bucket ) .then( data => response.ok( data ) ) .catch( e => response.error( e ) ); } ); From 6d6e80aac7800f5708918e077a26142c053681bc Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Tue, 12 Jun 2018 15:09:30 -0400 Subject: [PATCH 06/10] FieldClassMatcherTest: Add tests for existing implementation This is just to make sure that the current system is both well-understood and does not break with changes. This is a very important class, as it drives the display of the entire UI. * test/field/FieldClassMatcherTest.js: New file. --- test/field/FieldClassMatcherTest.js | 481 ++++++++++++++++++++++++++++ 1 file changed, 481 insertions(+) create mode 100644 test/field/FieldClassMatcherTest.js diff --git a/test/field/FieldClassMatcherTest.js b/test/field/FieldClassMatcherTest.js new file mode 100644 index 0000000..00940c9 --- /dev/null +++ b/test/field/FieldClassMatcherTest.js @@ -0,0 +1,481 @@ +/** + * Test case for FieldVisibilityEventHandler + * + * Copyright (C) 2018 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 . + */ + +const expect = require( 'chai' ).expect; + +const { FieldClassMatcher: Sut } = require( '../../' ).field; + + +describe( 'FieldClassMatcher', () => +{ + it( "echoes provided classes as __classes", done => + { + const classes = { foo: [ 1 ], bar: 0 }; + + Sut( {} ).match( classes, result => + { + expect( result.__classes ).to.deep.equal( classes ); + done(); + } ); + } ); + + + [ + { + label: "does nothing with no fields or classes", + fields: {}, + classes: {}, + expected: {}, + }, + + { + label: "sets all on >0 scalar cmatch", + fields: { foo: [ 'foowhen' ] }, + classes: { foowhen: { indexes: 1 } }, + expected: { + foo: { + all: true, + any: true, + indexes: [], + }, + }, + }, + + { + label: "sets none on 0 scalar cmatch", + fields: { foo: [ 'foowhen' ] }, + classes: { foowhen: { indexes: 0 } }, + expected: { + foo: { + all: false, + any: false, + indexes: [], + }, + }, + }, + + { + label: "sets all on all vector cmatch", + fields: { foo: [ 'foowhen' ] }, + classes: { foowhen: { indexes: [ 1, 1 ] } }, + expected: { + foo: { + all: true, + any: true, + indexes: [ 1, 1 ], + }, + }, + }, + + { + label: "sets any on partial vector cmatch", + fields: { foo: [ 'foowhen' ] }, + classes: { foowhen: { indexes: [ 0, 1 ] } }, + expected: { + foo: { + all: false, + any: true, + indexes: [ 0, 1 ], + }, + }, + }, + + { + label: "sets nothing on empty vector cmatch", + fields: { foo: [ 'foowhen' ] }, + classes: { foowhen: { indexes: [ 0, 0 ] } }, + expected: { + foo: { + all: false, + any: false, + indexes: [ 0, 0 ], + }, + }, + }, + + + // bad class data + { + label: "handles missing class as if scalar 0", + fields: { foo: [ 'noexist' ] }, + classes: {}, + expected: { + foo: { + all: false, + any: false, + indexes: [], + }, + }, + }, + + // multiple classes + { + label: "logical ANDs multiple vector classes (any)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: [ 1, 0 ] }, + foowhen2: { indexes: [ 1, 1 ] }, + }, + expected: { + foo: { + all: false, + any: true, + indexes: [ 1, 0 ], + }, + }, + }, + { + label: "logical ANDs multiple vector classes (all)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: [ 1, 1 ] }, + foowhen2: { indexes: [ 1, 1 ] }, + }, + expected: { + foo: { + all: true, + any: true, + indexes: [ 1, 1 ], + }, + }, + }, + { + label: "logical ANDs multiple vector classes (none)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: [ 0, 0 ] }, + foowhen2: { indexes: [ 0, 0 ] }, + }, + expected: { + foo: { + all: false, + any: false, + indexes: [ 0, 0 ], + }, + }, + }, + + { + label: "assumes match when ANDing varying lengths (any)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: [ 0 ] }, + foowhen2: { indexes: [ 0, 1 ] }, + }, + expected: { + foo: { + all: false, + any: true, + indexes: [ 0, 1 ], + }, + }, + }, + { + label: "assumes match when ANDing varying lengths (all)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: [ 1 ] }, + foowhen2: { indexes: [ 1, 1 ] }, + }, + expected: { + foo: { + all: true, + any: true, + indexes: [ 1, 1 ], + }, + }, + }, + { + label: "assumes match when ANDing varying lengths (none)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: [ 0 ] }, + foowhen2: { indexes: [ 0, 0 ] }, + }, + expected: { + foo: { + all: false, + any: false, + indexes: [ 0, 0 ], + }, + }, + }, + + { + label: "logically ANDs scalar with vector (any)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: 1 }, + foowhen2: { indexes: [ 0, 1, 0 ] }, + }, + expected: { + foo: { + all: false, + any: true, + indexes: [ 0, 1, 0 ], + }, + }, + }, + { + label: "logically ANDs scalar with vector (all)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: 1 }, + foowhen2: { indexes: [ 1, 1 ] }, + }, + expected: { + foo: { + all: true, + any: true, + indexes: [ 1, 1 ], + }, + }, + }, + { + label: "logically ANDs scalar with vector (none)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: 0 }, + foowhen2: { indexes: [ 1, 1 ] }, + }, + expected: { + foo: { + all: false, + any: false, + // FIXME: should be [ 0, 0 ] + indexes: [ false, false ], + }, + }, + }, + + { + label: "logically ANDs scalar with vector (rev, any)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: [ 0, 1, 0 ] }, + foowhen2: { indexes: 1 }, + }, + expected: { + foo: { + all: false, + any: true, + indexes: [ 0, 1, 0 ], + }, + }, + }, + { + label: "logically ANDs scalar with vector (rev, all)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: [ 1, 1 ] }, + foowhen2: { indexes: 1 }, + }, + expected: { + foo: { + all: true, + any: true, + indexes: [ 1, 1 ], + }, + }, + }, + { + label: "logically ANDs scalar with vector (rev, none)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: [ 1, 1 ] }, + foowhen2: { indexes: 0 }, + }, + expected: { + foo: { + all: false, + any: false, + indexes: [ 0, 0 ], + }, + }, + }, + + { + label: "logically ANDs scalar with scalar (all)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: 1 }, + foowhen2: { indexes: 1 }, + }, + expected: { + foo: { + all: true, + any: true, + indexes: [], + }, + }, + }, + { + label: "logically ANDs scalar with scalar (none 0)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: 0 }, + foowhen2: { indexes: 0 }, + }, + expected: { + foo: { + all: false, + any: false, + indexes: [], + }, + }, + }, + { + label: "logically ANDs scalar with scalar (none 1)", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: 0 }, + foowhen2: { indexes: 1 }, + }, + expected: { + foo: { + all: false, + any: false, + indexes: [], + }, + }, + }, + + + // matrix cmatch + { + label: "logically ORs matrix cmatch (single)", + fields: { foo: [ 'foowhen' ] }, + classes: { + foowhen: { indexes: [ [ 1 ] ] }, + }, + expected: { + foo: { + all: true, + any: true, + indexes: [ 1 ], + }, + }, + }, + { + label: "logically ORs matrix cmatch (any)", + fields: { foo: [ 'foowhen' ] }, + classes: { + foowhen: { indexes: [ [ 1 ], [ 0, 1 ], [ 0, 1 ], [ 0, 0 ] ] }, + }, + expected: { + foo: { + all: false, + any: true, + indexes: [ 1, 1, 1, 0 ], + }, + }, + }, + { + label: "logically ORs matrix cmatch (all)", + fields: { foo: [ 'foowhen' ] }, + classes: { + foowhen: { indexes: [ [ 1 ], [ 0, 1 ], [ 0, 1 ] ] }, + }, + expected: { + foo: { + all: true, + any: true, + indexes: [ 1, 1, 1 ], + }, + }, + }, + { + label: "logically AND's logically OR'd matrix cmatches", + fields: { foo: [ 'foowhen1', 'foowhen2' ] }, + classes: { + foowhen1: { indexes: [ [ 0 ], [ 0, 1 ], [ 1 ] ] }, + foowhen2: { indexes: [ [ 0 ], [ 0, 1 ], [ 0 ] ] }, + }, + expected: { + foo: { + all: false, + any: true, + indexes: [ 0, 1, 0 ], + }, + }, + }, + + // arbitrary nesting + { + label: "logically ORs arbitrarily nested vectors", + fields: { foo: [ 'foowhen' ] }, + classes: { + foowhen: { indexes: [ [ [ 0 ], [ 0, 1 ] ], [ [ [ 0 ] ] ] ] }, + }, + expected: { + foo: { + all: false, + any: true, + indexes: [ 1, 0 ], + }, + }, + }, + + + // multiple fields + { + label: "sets multiple fields", + fields: { foo: [ 'foowhen' ], bar: [ 'barwhen' ] }, + classes: { + foowhen: { indexes: [ 0, 0 ] }, + barwhen: { indexes: [ 1, 1 ] }, + }, + expected: { + foo: { + all: false, + any: false, + indexes: [ 0, 0 ], + }, + bar: { + all: true, + any: true, + indexes: [ 1, 1 ], + }, + }, + }, + + // TODO: perhaps this should default to all: true? + { + label: "ignores fields with no predicates", + fields: { foo: [] }, + classes: {}, + expected: {}, + }, + ].forEach( ( { label, fields, classes, expected } ) => + { + it( label, done => + { + // no use in specifying this above every time + expected.__classes = classes; + + Sut( fields ).match( classes, result => + { + expect( result ).to.deep.equal( expected ); + done(); + } ); + } ); + } ); +} ); From 5164c9dcbb244eb45a2b3baa79994f17b98f7cf3 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Tue, 12 Jun 2018 15:11:24 -0400 Subject: [PATCH 07/10] FieldClassMatcher: Always yield integer indexes * src/field/FieldClassMatcher.js (match): Cast `vis' to number. * test/field/FieldClassMatcherTest.js: Remove FIXME and change assertion. --- src/field/FieldClassMatcher.js | 2 +- test/field/FieldClassMatcherTest.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/field/FieldClassMatcher.js b/src/field/FieldClassMatcher.js index 1d0153c..6ecc9ef 100644 --- a/src/field/FieldClassMatcher.js +++ b/src/field/FieldClassMatcher.js @@ -102,7 +102,7 @@ module.exports = Class( 'FieldClassMatcher', // default to visible; note that, if we've encountered any // "all index" situations (scalars), then we must only be // true if the scalar value was true - vis[ i ] = ( + vis[ i ] = +( ( !hasall || all ) && ( ( vis[ i ] === undefined ) ? 1 diff --git a/test/field/FieldClassMatcherTest.js b/test/field/FieldClassMatcherTest.js index 00940c9..1196bdd 100644 --- a/test/field/FieldClassMatcherTest.js +++ b/test/field/FieldClassMatcherTest.js @@ -260,8 +260,7 @@ describe( 'FieldClassMatcher', () => foo: { all: false, any: false, - // FIXME: should be [ 0, 0 ] - indexes: [ false, false ], + indexes: [ 0, 0 ], }, }, }, From de931cf91b65139ca09eec2a8a70ea0fe4610d4c Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Tue, 12 Jun 2018 16:52:14 -0400 Subject: [PATCH 08/10] FieldClassMatcher: Minor refactoring/cleanup This does not go all the way, but helps improve the readability of the algorithm a little bit and modernizes the code. * src/field/FieldClassMatcher.js (constructor): Renamed from `__constructor'. (__constructor): Remove method. (match): Extract most code into `#_reduceFieldMatches'. (_reduceFieldMatches): New method, simplifying the algorithm slightly. (_reduceMatch): Simplify. * test/field/FieldClassMatcherTest.js: Update accordingly. --- src/field/FieldClassMatcher.js | 213 ++++++++++++++-------------- test/field/FieldClassMatcherTest.js | 11 +- 2 files changed, 113 insertions(+), 111 deletions(-) diff --git a/src/field/FieldClassMatcher.js b/src/field/FieldClassMatcher.js index 6ecc9ef..0c015a6 100644 --- a/src/field/FieldClassMatcher.js +++ b/src/field/FieldClassMatcher.js @@ -1,7 +1,7 @@ /** - * Contains FieldClassMatcher class + * Reduce field predicate results into vectors and flags * - * Copyright (C) 2017 R-T Specialty, LLC. + * Copyright (C) 2018 R-T Specialty, LLC. * * This file is part of the Liza Data Collection Framework. * @@ -19,12 +19,21 @@ * along with this program. If not, see . */ -var Class = require( 'easejs' ).Class; +'use strict'; + +const { Class } = require( 'easejs' ); /** - * Generates match sets for field based on their classifications and a given - * classification set + * Generate match vector for fields given field predicates and + * classification results + * + * TODO: Support for multiple predicates on fields is for + * backwards-compatibility with older classification systems; newer systems + * generate a single classification representing the visibility of the + * field, allowing the classification reduction complexity and logic to stay + * within TAME. Much of the complexity in this class can therefore be + * removed in the future. */ module.exports = Class( 'FieldClassMatcher', { @@ -40,7 +49,7 @@ module.exports = Class( 'FieldClassMatcher', * * @param {Object.>} fields field names and their classes */ - __construct: function( fields ) + constructor( fields ) { this._fields = fields; }, @@ -58,97 +67,99 @@ module.exports = Class( 'FieldClassMatcher', * * @return {FieldClassMatcher} self */ - 'public match': function( classes, callback ) + 'public match'( classes, callback ) { - var cmatch = {}; - cmatch.__classes = classes; - - for ( var field in this._fields ) - { - var cur = this._fields[ field ], - vis = [], - all = true, - hasall = false; - - if ( cur.length === 0 ) - { - continue; - } - - // determine if we have a match based on the given classifications - for ( var c in cur ) - { - // if the indexes property is a scalar, then it applies to all - // indexes - var data = ( classes[ cur[ c ] ] || {} ), - thisall = ( typeof data.indexes !== 'object' ), - alltrue = ( !thisall || data.indexes === 1 ); - - // if no indexes apply for the given classification (it may be a - // pure boolean), then this variable will be true if any only if - // all of them are true. Note that we only want to take the - // value of thisall if we're on our first index, as if hasall is - // empty thereafter, then all of them certainly aren't true! - hasall = ( hasall || ( thisall && +c === 0 ) ); - - // this will ensure that, if we've already determined some sort - // of visibility, that encountering a scalar will still manage - // to affect previous results even if it is the last - // classification that we are checking - var indexes = ( thisall ) ? vis : data.indexes; - - for ( var i in indexes ) - { - // default to visible; note that, if we've encountered any - // "all index" situations (scalars), then we must only be - // true if the scalar value was true - vis[ i ] = +( - ( !hasall || all ) - && ( ( vis[ i ] === undefined ) - ? 1 - : vis[ i ] - ) - && this._reduceMatch( - ( thisall ) ? data.indexes : data.indexes[ i ] - ) - ); - - // all are true unless one is false (duh?) - alltrue = !!( alltrue && vis[ i ] ); - } - - all = ( all && alltrue ); - } - - // default 'any' to 'all'; this will have the effect of saying "yes - // there are matches, but we don't care what" if there are no - // indexes associated with the match, implying that all indexes - // should match - var any = all; - for ( var i = 0, len = vis.length; i < len; i++ ) - { - if ( vis[ i ] ) - { - any = true; - break; - } - } - - // store the classification match data for assertions, etc - cmatch[ field ] = { - all: all, - any: any, - indexes: vis - }; - } - // currently not asynchronous, but leaves open the possibility - callback( cmatch ); + callback( + Object.keys( this._fields ).reduce( + ( cmatch, id ) => + { + cmatch[ id ] = this._reduceFieldMatches( + this._fields[ id ], + classes + ), cmatch; + + return cmatch; + }, + { __classes: classes } + ) + ); return this; }, + /** + * Reduce field class matches to a vector + * + * All field predicates in FIELDC will be reduced and combined into a + * single vector representing the visibility of each index. + * + * @param {Array} fieldc field predicate class names + * @param {Object} classes cmatch results + * + * @return {Object} all, any, indexes + */ + 'private _reduceFieldMatches'( fieldc, classes ) + { + const vis = []; + + let all = true; + let hasall = false; + + // determine if we have a match based on the given classifications + for ( let c in fieldc ) + { + // if the indexes property is a scalar, then it applies to all + // indexes + const data = ( classes[ fieldc[ c ] ] || {} ); + const thisall = !Array.isArray( data.indexes ); + + let alltrue = ( !thisall || data.indexes === 1 ); + + // if no indexes apply for the given classification (it may be a + // pure boolean), then this variable will be true if any only if + // all of them are true. Note that we only want to take the + // value of thisall if we're on our first index, as if hasall is + // empty thereafter, then all of them certainly aren't true! + hasall = ( hasall || ( thisall && +c === 0 ) ); + + // this will ensure that, if we've already determined some sort + // of visibility, that encountering a scalar will still manage + // to affect previous results even if it is the last + // classification that we are checking + const indexes = ( thisall ) ? vis : data.indexes; + + for ( let i in indexes ) + { + // default to visible; note that, if we've encountered any + // "all index" situations (scalars), then we must only be + // true if the scalar value was true + vis[ i ] = +( + ( !hasall || all ) + && ( ( vis[ i ] === undefined ) + ? 1 + : vis[ i ] + ) + && this._reduceMatch( + ( thisall ) ? data.indexes : data.indexes[ i ] + ) + ); + } + + alltrue = alltrue && vis.every( x => x ); + all = ( all && alltrue ); + } + + // store the classification match data for assertions, etc + return { + all: all, + any: all || vis.some( x => !!x ), + indexes: vis + }; + }, + + /** * Reduces the given scalar or vector * @@ -164,27 +175,13 @@ module.exports = Class( 'FieldClassMatcher', * * @return {number} 0 if false otherwise 1 for true */ - 'private _reduceMatch': function( result ) + 'private _reduceMatch'( result ) { - if ( ( result === undefined ) - || ( result === null ) - || ( result.length === undefined ) - ) + if ( !Array.isArray( result ) ) { - return result; + return !!result; } - var ret = false, - i = result.length; - - // reduce with logical or - while( i-- ) - { - // recurse just in case we have another array of values - ret = ret || this._reduceMatch( result[ i ] ); - } - - return +ret; + return +result.some( x => this._reduceMatch( x ) ); } } ); - diff --git a/test/field/FieldClassMatcherTest.js b/test/field/FieldClassMatcherTest.js index 1196bdd..fb0190f 100644 --- a/test/field/FieldClassMatcherTest.js +++ b/test/field/FieldClassMatcherTest.js @@ -456,12 +456,17 @@ describe( 'FieldClassMatcher', () => }, }, - // TODO: perhaps this should default to all: true? { - label: "ignores fields with no predicates", + label: "sets all for fields with no predicates", fields: { foo: [] }, classes: {}, - expected: {}, + expected: { + foo: { + all: true, + any: true, + indexes: [], + } + }, }, ].forEach( ( { label, fields, classes, expected } ) => { From 7935c699dead980294fc3d176e6f1acde6346c01 Mon Sep 17 00:00:00 2001 From: Mark Goldsmith Date: Wed, 13 Jun 2018 15:42:45 -0400 Subject: [PATCH 09/10] [DEV-2871] DocumentProgramFormatter: Match on fields from FieldClassMatcher instead of __classes and program.whens --- src/document/DocumentProgramFormatter.js | 41 +++--- test/document/DocumentProgramFormatterTest.js | 137 ++++++++++++------ 2 files changed, 109 insertions(+), 69 deletions(-) diff --git a/src/document/DocumentProgramFormatter.js b/src/document/DocumentProgramFormatter.js index 7cd4b3b..5bcc191 100644 --- a/src/document/DocumentProgramFormatter.js +++ b/src/document/DocumentProgramFormatter.js @@ -76,11 +76,10 @@ module.exports = Class( 'DocumentProgramFormatter', { const cmatch = this._program.classify( bucket.getData() ); - this._class_matcher.match( cmatch, ( field_matches ) => + this._class_matcher.match( cmatch, ( matches ) => { const len = this._program.steps.length; - const classes = field_matches.__classes; - const data = this._parseSteps( len, bucket, classes ); + const data = this._parseSteps( len, bucket, matches ); resolve( data ); } ); @@ -93,12 +92,12 @@ module.exports = Class( 'DocumentProgramFormatter', * Parses step data * * @param {Number} len step length - * @param {Bucket} bucket document bucket - * @param {Object} classes class/field matches + * @param {Bucket} bucket document bucket + * @param {Object} matches field matches * * @return {Object} step data */ - 'private _parseSteps'( len, bucket, classes ) + 'private _parseSteps'( len, bucket, matches ) { const data = { steps: [] }; @@ -107,7 +106,7 @@ module.exports = Class( 'DocumentProgramFormatter', const step = {}; const step_groups = this._program.steps[ i ].groups; - const groups = this._parseGroups( step_groups, bucket, classes ); + const groups = this._parseGroups( step_groups, bucket, matches ); step.title = this._program.steps[ i ].title; step.groups = groups; @@ -124,11 +123,11 @@ module.exports = Class( 'DocumentProgramFormatter', * * @param {Array} step_groups array of group data * @param {Bucket} bucket document bucket - * @param {Object} classes class/field matches + * @param {Object} matches field matches * * @return {Array} array of groups */ - 'private _parseGroups'( step_groups, bucket, classes ) + 'private _parseGroups'( step_groups, bucket, matches ) { return step_groups.map( group_id => { @@ -139,7 +138,7 @@ module.exports = Class( 'DocumentProgramFormatter', id: group_id, title: title || "", link: link || "", - questions: this._parseFields( fields, bucket, classes ), + questions: this._parseFields( fields, bucket, matches ), }; } ); }, @@ -150,11 +149,11 @@ module.exports = Class( 'DocumentProgramFormatter', * * @param {Array} fields array of field data * @param {Bucket} bucket document bucket - * @param {Object} classes class/field matches + * @param {Object} matches field matches * * @return {Array} array of questions */ - 'private _parseFields'( fields, bucket, classes ) + 'private _parseFields'( fields, bucket, matches ) { const questions = []; @@ -176,7 +175,7 @@ module.exports = Class( 'DocumentProgramFormatter', label: label || "", value: value, type: type || "", - applicable: this._getApplicable( classes, field_id, value ), + applicable: this._getApplicable( matches, field_id, value ), } ); } @@ -189,31 +188,31 @@ module.exports = Class( 'DocumentProgramFormatter', * Determine when a field is shown by index * Map boolean values of [0, 1] to [true, false] * - * @param {Object} classes object of visibility classes + * @param {Object} matches field matches * @param {String} field_id id of field * @param {Object} field_value field object * * @return {Array.} array of booleans */ - 'private _getApplicable'( classes, field_id, field_value ) + 'private _getApplicable'( matches, field_id, field_value ) { // If object is undefined, default to array of true - if ( typeof this._program.whens[ field_id ] === "undefined" ) + if ( typeof matches[ field_id ] === "undefined" ) { return field_value.map( _ => true ); } - const class_id = this._program.whens[ field_id ][ 0 ]; - const indexes = classes[ class_id ].indexes; + const indexes = matches[ field_id ].indexes; + const any_match = matches[ field_id ].any; - // Map indexes of 0, 1 to true, false - if ( Array.isArray( indexes ) ) + if ( indexes.length > 0 ) { + // Map indexes of 0, 1 to true, false return indexes.map( x => !!x ); } else { - return field_value.map( _ => !!indexes ); + return field_value.map( _ => any_match ); } }, } ); diff --git a/test/document/DocumentProgramFormatterTest.js b/test/document/DocumentProgramFormatterTest.js index 1b64161..88af3d2 100644 --- a/test/document/DocumentProgramFormatterTest.js +++ b/test/document/DocumentProgramFormatterTest.js @@ -39,13 +39,14 @@ describe( 'DocumentProgramFormatter', () => it( "formats bucket data by steps, groups and fields", () => { const bucket_data = { - sell_alcohol: [ "foo", "" ], - serve_alcohol: [ "" ], - sell_ecigs: [ "", "bar" ], - dist_ecigs: [ "" ], + alcohol_shown: [ "foo", "" ], + alcohol_not_shown: [ "" ], + ecigs_shown_twice: [ "foo", "bar" ], + ecigs_not_shown: [ "" ], field_no_label: [ "" ], - field_no_array: [ "bar" ], - field_no_vis: [ "true" ] + field_no_vis: [ "true" ], + field_empty_array_any_true: [ "bar" ], + field_empty_array_any_false: [ "" ] }; const expected_object = { @@ -63,22 +64,22 @@ describe( 'DocumentProgramFormatter', () => link: "locations", questions: [ { - id: "sell_alcohol", - label: "Does the insured sell alcohol?", + id: "alcohol_shown", + label: "Alcohol?", value: [ "foo", "" ], type: "noyes", applicable: [ true, false ] }, { - id: "serve_alcohol", - label: "Does the insured serve alcohol?", + id: "alcohol_not_shown", + label: "No alcohol?", value: [ "" ], type: "noyes", applicable: [ false ] }, { id: "field_no_vis", - label: "Does this field have a visibility class?", + label: "Is this field found in FieldMatcher?", value: [ "true" ], type: "noyes", applicable: [ true ] @@ -91,25 +92,32 @@ describe( 'DocumentProgramFormatter', () => link: "", questions: [ { - id: "sell_ecigs", - label: "Does the insured sell e-cigarettes?", - value: [ "", "bar" ], + id: "ecigs_shown_twice", + label: "Two ecig answers?", + value: [ "foo", "bar" ], type: "noyes", - applicable: [ false, true ] + applicable: [ true, true ] }, { - id: "dist_ecigs", - label: "Does the Insured distribute Electronic Cigarette products?", + id: "ecigs_not_shown", + label: "No ecigs?", value: [ "" ], type: "noyes", applicable: [ false ] }, { - id: "field_no_array", - label: "Does this field have an array for the visibility class?", + id: "field_empty_array_any_true", + label: "Empty match array?", value: [ "bar" ], type: "noyes", applicable: [ true ] + }, + { + id: "field_empty_array_any_false", + label: "Empty match array and 'any' is false?", + value: [ "" ], + type: "noyes", + applicable: [ false ] } ] } @@ -135,14 +143,39 @@ function createStubClassMatcher() match( _, callback ) { callback({ - __classes: - { - '--vis-sell-alcohol': { is: true, indexes: [1,0] }, - '--vis-serve-alcohol': { is: false, indexes: [0] }, - '--vis-sell-ecigs': { is: false, indexes: [0,1] }, - '--vis-dist-ecigs': { is: true, indexes: [0] }, - '--vis-no-array': { is: true, indexes: 1 }, - } + "__classes": { + '--vis-foo': { is: true, indexes: [1,0] }, + }, + "alcohol_shown": { + "all": false, + "any": true, + "indexes": [1, 0] + }, + "alcohol_not_shown": { + "all": false, + "any": false, + "indexes": [0] + }, + "ecigs_shown_twice": { + "all": false, + "any": true, + "indexes": [1, 1] + }, + "ecigs_not_shown": { + "all": false, + "any": false, + "indexes": [] + }, + "field_empty_array_any_false": { + "all": false, + "any": false, + "indexes": [] + }, + "field_empty_array_any_true": { + "all": true, + "any": true, + "indexes": [] + }, }) ; } } @@ -192,55 +225,63 @@ function createStubProgram() }, fields: { - sell_alcohol: + alcohol_shown: { - label: "Does the insured sell alcohol?", + label: "Alcohol?", type: "noyes", required: "true", }, - serve_alcohol: + alcohol_not_shown: { - label: "Does the insured serve alcohol?", + label: "No alcohol?", type: "noyes", required: "true" }, - sell_ecigs: + ecigs_shown_twice: { - label: "Does the insured sell e-cigarettes?", + label: "Two ecig answers?", type: "noyes", required: "true" }, - dist_ecigs: + ecigs_not_shown: { - label: "Does the Insured distribute Electronic Cigarette products?", + label: "No ecigs?", type: "noyes", required: "true" }, - field_no_array: + field_empty_array_any_true: { - label: "Does this field have an array for the visibility class?", + label: "Empty match array?", + type: "noyes", + required: "true" + }, + field_empty_array_any_false: + { + label: "Empty match array and 'any' is false?", type: "noyes", required: "true" }, field_no_vis: { - label: "Does this field have a visibility class?", + label: "Is this field found in FieldMatcher?", type: "noyes", required: "true" } }, groupExclusiveFields: { - 'group_one': [ "sell_alcohol", "serve_alcohol", "field_no_label", "field_no_vis" ], - 'group_two': [ "sell_ecigs", "dist_ecigs", "field_no_array" ], - }, - whens: - { - sell_alcohol: [ "--vis-sell-alcohol" ], - serve_alcohol: [ "--vis-serve-alcohol" ], - sell_ecigs: [ "--vis-sell-ecigs" ], - dist_ecigs: [ "--vis-dist-ecigs" ], - field_no_array: [ "--vis-no-array" ], + 'group_one': [ + "alcohol_shown", + "alcohol_not_shown", + "field_no_label", + "field_no_vis" + ], + 'group_two': [ + "ecigs_shown_twice", + "ecigs_not_shown", + "field_empty_array_any_true", + "field_empty_array_any_false" + ], }, }; } From bb9bb68a0fe23c88e294ac9bf7892defed2724a6 Mon Sep 17 00:00:00 2001 From: Mark Goldsmith Date: Fri, 15 Jun 2018 09:02:16 -0400 Subject: [PATCH 10/10] [DEV-2871] DocumentProgramFormatter: changed field match from any to all --- src/document/DocumentProgramFormatter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/document/DocumentProgramFormatter.js b/src/document/DocumentProgramFormatter.js index 5bcc191..56141c7 100644 --- a/src/document/DocumentProgramFormatter.js +++ b/src/document/DocumentProgramFormatter.js @@ -203,7 +203,7 @@ module.exports = Class( 'DocumentProgramFormatter', } const indexes = matches[ field_id ].indexes; - const any_match = matches[ field_id ].any; + const all_match = matches[ field_id ].all; if ( indexes.length > 0 ) { @@ -212,7 +212,7 @@ module.exports = Class( 'DocumentProgramFormatter', } else { - return field_value.map( _ => any_match ); + return field_value.map( _ => all_match ); } }, } );