1
0
Fork 0

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
Mike Gerwitz 2018-10-21 23:41:58 -04:00
parent 27cc3d2c63
commit 2bc1b96a15
2 changed files with 164 additions and 101 deletions

View File

@ -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;
},

View File

@ -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: {},
};
}