ProgramQuoteCleaner: Clean all groups (not just linked)
* src/server/quote/ProgramQuoteCleaner.js (clean): Add docblock. Replace previous linked group cleaning with call to `_fixGroup'. (_fixGroup): New method. Similar logic to previous linked group cleaning, except that fields are never truncated. (_fixLinkedGroups, _getLinkedIndexLength): Remove methods. (_getGroupLength): New method determining group size from leader length, which also accounts for linked groups. * test/server/quote/ProgramQuoteCleanerTest.js: New test case.master
parent
27cc3d2c63
commit
2bc1b96a15
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Contains ProgramQuoteCleaner
|
||||
*
|
||||
* Copyright (C) 2017 R-T Specialty, LLC.
|
||||
* Copyright (C) 2017, 2018 R-T Specialty, LLC.
|
||||
*
|
||||
* This file is part of the Liza Data Collection Framework.
|
||||
*
|
||||
|
@ -39,6 +39,16 @@ module.exports = Class( 'ProgramQuoteCleaner',
|
|||
},
|
||||
|
||||
|
||||
/**
|
||||
* "Clean" quote, getting it into a stable state
|
||||
*
|
||||
* Quote cleaning will ensure that all group fields share at least the
|
||||
* same number of indexes as its leader, and that meta fields are
|
||||
* initialized. This is useful when questions or meta fields are added.
|
||||
*
|
||||
* @param {Quote} quote target quote
|
||||
* @param {Function} callback continuation
|
||||
*/
|
||||
'public clean': function( quote, callback )
|
||||
{
|
||||
// consider it an error to attempt cleaning a quote with the incorrect
|
||||
|
@ -47,116 +57,102 @@ module.exports = Class( 'ProgramQuoteCleaner',
|
|||
{
|
||||
callback( null );
|
||||
return;
|
||||
|
||||
// TODO: once we move the program redirect before this check
|
||||
// callback( Error( 'Program mismatch' ) );
|
||||
}
|
||||
|
||||
// fix any problems with linked groups
|
||||
this._fixLinkedGroups( quote, err =>
|
||||
{
|
||||
if ( err )
|
||||
{
|
||||
callback( err );
|
||||
return;
|
||||
}
|
||||
// correct group indexes
|
||||
Object.keys( this._program.groupIndexField || {} ).forEach(
|
||||
group_id => this._fixGroup( group_id, quote )
|
||||
);
|
||||
|
||||
this._fixMeta( quote );
|
||||
callback( null );
|
||||
} );
|
||||
},
|
||||
this._fixMeta( quote );
|
||||
|
||||
|
||||
'private _fixLinkedGroups': function( quote, callback )
|
||||
{
|
||||
var links = this._program.links,
|
||||
update = {};
|
||||
|
||||
for ( var link in links )
|
||||
{
|
||||
var len = this._getLinkedIndexLength( link, quote ),
|
||||
cur = links[ link ];
|
||||
|
||||
// for each field less than the given length, correct it by adding
|
||||
// the necessary number of indexes and filling them with their
|
||||
// default values
|
||||
for ( var i in cur )
|
||||
{
|
||||
var field = cur[ i ];
|
||||
|
||||
if ( !field )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var data = quote.getDataByName( field ),
|
||||
flen = data.length;
|
||||
|
||||
//varnity check
|
||||
if ( !( Array.isArray( data ) ) )
|
||||
{
|
||||
data = [];
|
||||
flen = 0;
|
||||
}
|
||||
|
||||
// if the length matches, continue
|
||||
if ( flen === len )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if ( flen > len )
|
||||
{
|
||||
// length is greater; cut it off
|
||||
data = data.slice( 0, len );
|
||||
}
|
||||
|
||||
var d = this._program.defaults[ field ] || '';
|
||||
for ( var j = flen; j < len; j++ )
|
||||
{
|
||||
data[ j ] = d;
|
||||
}
|
||||
|
||||
update[ field ] = data;
|
||||
}
|
||||
}
|
||||
|
||||
// perform quote update a single time once we have decided what needs to
|
||||
// be done
|
||||
quote.setData( update );
|
||||
|
||||
// we're not async, but we'll keep with the callback to simplify such a
|
||||
// possibility in the future
|
||||
callback( null );
|
||||
},
|
||||
|
||||
|
||||
'private _getLinkedIndexLength': function( link, quote )
|
||||
/**
|
||||
* Correct group fields to be at least the length of the leader
|
||||
*
|
||||
* If a group is part of a link, then its leader may be part of another
|
||||
* group, and the length of the fields of all linked groups will match
|
||||
* be at least the length of the leader.
|
||||
*
|
||||
* Unlike previous implementations, this _does not_ truncate fields,
|
||||
* since that risks data loss. Instead, field length should be
|
||||
* validated on save.
|
||||
*
|
||||
* @param {string} group_id group identifier
|
||||
* @param {Quote} quote target quote
|
||||
*
|
||||
* @return {undefined} data are set on QUOTE
|
||||
*/
|
||||
'private _fixGroup'( group_id, quote )
|
||||
{
|
||||
var fields = this._program.links[ link ],
|
||||
chklen = 20,
|
||||
len = 0;
|
||||
const length = +this._getGroupLength( group_id, quote );
|
||||
|
||||
// loop through the first N fields, take the largest index length and
|
||||
// consider that to be the length of the group
|
||||
for ( var i = 0; i < chklen; i++ )
|
||||
// if we cannot accurately determine the length then it's too
|
||||
// dangerous to proceed and risk screwing up the data; abort
|
||||
// processing this group (this should never happen unless a program
|
||||
// is either not properly compiled or is out of date)
|
||||
if ( isNaN( length ) )
|
||||
{
|
||||
var field = fields[ i ];
|
||||
if ( !field )
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var data = quote.getDataByName( field );
|
||||
if ( !( Array.isArray( data ) ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// increaes the length if a larger field was found
|
||||
len = ( len > data.length ) ? len : data.length;
|
||||
return;
|
||||
}
|
||||
|
||||
return len;
|
||||
const update = {};
|
||||
|
||||
const group_fields = this._program.groupExclusiveFields[ group_id ];
|
||||
|
||||
group_fields.forEach( field =>
|
||||
{
|
||||
const flen = ( quote.getDataByName( field ) || [] ).length;
|
||||
|
||||
if ( flen >= length )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const data = [];
|
||||
const field_default = this._program.defaults[ field ] || '';
|
||||
|
||||
for ( var i = flen; i < length; i++ )
|
||||
{
|
||||
data[ i ] = field_default;
|
||||
}
|
||||
|
||||
update[ field ] = data;
|
||||
} );
|
||||
|
||||
quote.setData( update );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine length of group GROUP_ID
|
||||
*
|
||||
* The length of a group is the length of its leader, which may be part
|
||||
* of another group (if the group is linked).
|
||||
*
|
||||
* @param {string} group_id group identifier
|
||||
* @param {Quote} quote target quote
|
||||
*
|
||||
* @return {number} length of group GROUP_ID
|
||||
*/
|
||||
'private _getGroupLength'( group_id, quote )
|
||||
{
|
||||
const index_field = this._program.groupIndexField[ group_id ];
|
||||
|
||||
// we don't want to give the wrong answer, so just abort
|
||||
if ( !index_field )
|
||||
{
|
||||
return NaN;
|
||||
}
|
||||
|
||||
const data = quote.getDataByName( index_field );
|
||||
|
||||
return ( Array.isArray( data ) )
|
||||
? data.length
|
||||
: NaN;
|
||||
},
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Tests ProgramQuoteCleaner
|
||||
*
|
||||
* Copyright (C) 2017 R-T Specialty, LLC.
|
||||
* Copyright (C) 2017, 2018 R-T Specialty, LLC.
|
||||
*
|
||||
* This file is part of the Liza Data Collection Framework.
|
||||
*
|
||||
|
@ -27,6 +27,71 @@ const Sut = require( '../../../' ).server.quote.ProgramQuoteCleaner;
|
|||
|
||||
describe( 'ProgramQuoteCleaner', () =>
|
||||
{
|
||||
describe( "group cleaning", () =>
|
||||
{
|
||||
[
|
||||
{
|
||||
label: "expands indexes of linked and non-linked groups",
|
||||
|
||||
group_index: {
|
||||
one: 'field11', // linked
|
||||
two: 'field11', // linked
|
||||
three: 'field31',
|
||||
},
|
||||
|
||||
exclusive: {
|
||||
one: [ "field11", "field12" ],
|
||||
two: [ "field21", "field22" ],
|
||||
three: [ "field31", "field32" ],
|
||||
},
|
||||
|
||||
defaults: {
|
||||
field12: "12default",
|
||||
},
|
||||
|
||||
existing: {
|
||||
"field11": [ "1", "", "3" ], // leader one, two
|
||||
"field12": [ "a", "b" ],
|
||||
"field21": [ "e" ],
|
||||
"field22": [ "I", "II" ],
|
||||
"field31": [ "i", "ii" ], // leader three
|
||||
"field32": [ "x" ],
|
||||
},
|
||||
|
||||
expected: {
|
||||
"field12": [ , , "12default" ],
|
||||
"field21": [ , "", "" ],
|
||||
"field22": [ , , "" ],
|
||||
"field32": [ , "" ],
|
||||
},
|
||||
},
|
||||
].forEach( test =>
|
||||
it( test.label, done =>
|
||||
{
|
||||
const quote = createStubQuote( test.existing, {} );
|
||||
const program = createStubProgram( {} );
|
||||
|
||||
program.defaults = test.defaults;
|
||||
program.groupIndexField = test.group_index;
|
||||
program.groupExclusiveFields = test.exclusive;
|
||||
|
||||
const updates = {};
|
||||
|
||||
quote.setData = given =>
|
||||
Object.keys( given ).forEach( k => updates[ k ] = given[ k ] );
|
||||
|
||||
Sut( program ).clean( quote, err =>
|
||||
{
|
||||
expect( err ).to.deep.equal( null );
|
||||
expect( updates ).to.deep.equal( test.expected );
|
||||
|
||||
done();
|
||||
} );
|
||||
} )
|
||||
);
|
||||
} );
|
||||
|
||||
|
||||
describe( "metadata cleaning", () =>
|
||||
{
|
||||
[
|
||||
|
@ -51,7 +116,7 @@ describe( 'ProgramQuoteCleaner', () =>
|
|||
].forEach( ( { label, existing, fields, expected } ) =>
|
||||
it( label, done =>
|
||||
{
|
||||
const quote = createStubQuote( existing );
|
||||
const quote = createStubQuote( {}, existing );
|
||||
const program = createStubProgram( fields );
|
||||
|
||||
Sut( program ).clean( quote, err =>
|
||||
|
@ -68,11 +133,12 @@ describe( 'ProgramQuoteCleaner', () =>
|
|||
} );
|
||||
|
||||
|
||||
function createStubQuote( metadata )
|
||||
function createStubQuote( data, metadata )
|
||||
{
|
||||
return {
|
||||
getProgramId: () => 'foo',
|
||||
setData: () => {},
|
||||
getDataByName: name => data[ name ],
|
||||
getMetabucket: () => ( {
|
||||
getDataByName: name => metadata[ name ],
|
||||
getData: () => metadata,
|
||||
|
@ -92,5 +158,6 @@ function createStubProgram( meta_fields )
|
|||
return {
|
||||
getId: () => 'foo',
|
||||
meta: { fields: meta_fields },
|
||||
defaults: {},
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue