Skip to content

Commit

Permalink
Merge pull request #1125 from JGreenlee/fix-processed-surveys-matching
Browse files Browse the repository at this point in the history
🚑 Fix Processed Trip Survey Responses Not Matching
  • Loading branch information
shankari authored Feb 2, 2024
2 parents d8379c2 + e5ef8da commit 9ddcf31
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 5 deletions.
215 changes: 215 additions & 0 deletions www/__tests__/inputMatcher.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { mockBEMUserCache } from '../__mocks__/cordovaMocks';
import { mockLogger } from '../__mocks__/globalMocks';
import { unprocessedLabels, updateLocalUnprocessedInputs } from '../js/diary/timelineHelper';
import { EnketoUserInputEntry } from '../js/survey/enketo/enketoHelper';
import {
fmtTs,
Expand All @@ -8,9 +11,14 @@ import {
getUserInputForTimelineEntry,
getAdditionsForTimelineEntry,
getUniqueEntries,
mapInputsToTimelineEntries,
} from '../js/survey/inputMatcher';
import { AppConfig } from '../js/types/appConfigTypes';
import { CompositeTrip, TimelineEntry, UserInputEntry } from '../js/types/diaryTypes';

mockLogger();
mockBEMUserCache();

describe('input-matcher', () => {
let userTrip: UserInputEntry;
let trip: TimelineEntry;
Expand Down Expand Up @@ -267,3 +275,210 @@ describe('input-matcher', () => {
expect(uniqueEntires).toMatchObject([]);
});
});

describe('mapInputsToTimelineEntries on a MULTILABEL configuration', () => {
const fakeConfigMultilabel = {
intro: {},
survey_info: {
'trip-labels': 'MULTILABEL',
},
} as AppConfig;

const timelineEntriesMultilabel = [
{
_id: { $oid: 'trip1' },
origin_key: 'analysis/confirmed_trip',
start_ts: 1000,
end_ts: 3000,
user_input: {
mode_confirm: 'walk',
},
},
{
_id: { $oid: 'placeA' },
origin_key: 'analysis/confirmed_place',
enter_ts: 3000,
exit_ts: 5000,
// no user input
additions: [{ data: 'foo', metadata: 'bar' }],
},
{
_id: { $oid: 'trip2' },
origin_key: 'analysis/confirmed_trip',
start_ts: 5000,
end_ts: 7000,
// no user input
},
] as any as TimelineEntry[];
it('creates a map that has the processed labels and notes', () => {
const [labelMap, notesMap] = mapInputsToTimelineEntries(
timelineEntriesMultilabel,
fakeConfigMultilabel,
);
expect(labelMap).toMatchObject({
trip1: {
MODE: { data: { label: 'walk' } },
},
});
});
it('creates a map that combines processed and unprocessed labels and notes', async () => {
// insert some unprocessed data
await window['cordova'].plugins.BEMUserCache.putMessage('manual/purpose_confirm', {
label: 'recreation',
start_ts: 1000,
end_ts: 3000,
});
await window['cordova'].plugins.BEMUserCache.putMessage('manual/mode_confirm', {
label: 'bike',
start_ts: 5000,
end_ts: 7000,
});
await updateLocalUnprocessedInputs({ start_ts: 1000, end_ts: 5000 }, fakeConfigMultilabel);

// check that both processed and unprocessed data are returned
const [labelMap, notesMap] = mapInputsToTimelineEntries(
timelineEntriesMultilabel,
fakeConfigMultilabel,
);

expect(labelMap).toMatchObject({
trip1: {
MODE: { data: { label: 'walk' } },
PURPOSE: { data: { label: 'recreation' } },
},
trip2: {
MODE: { data: { label: 'bike' } },
},
});
});
});

describe('mapInputsToTimelineEntries on an ENKETO configuration', () => {
const fakeConfigEnketo = {
intro: {},
survey_info: {
'trip-labels': 'ENKETO',
buttons: {
'trip-notes': { surveyName: 'TimeSurvey' },
},
surveys: { TripConfirmSurvey: { compatibleWith: 1 } },
},
} as any as AppConfig;
const timelineEntriesEnketo = [
{
_id: { $oid: 'trip1' },
origin_key: 'analysis/confirmed_trip',
start_ts: 1000,
end_ts: 3000,
user_input: {
trip_user_input: {
data: {
name: 'TripConfirmSurvey',
version: 1,
xmlResponse: '<processed TripConfirmSurvey response>',
start_ts: 1000,
end_ts: 3000,
},
metadata: 'foo',
},
},
additions: [
{
data: {
name: 'TimeSurvey',
xmlResponse: '<processed TimeSurvey response>',
start_ts: 1000,
end_ts: 2000,
},
metadata: 'foo',
},
],
},
{
_id: { $oid: 'trip2' },
origin_key: 'analysis/confirmed_trip',
start_ts: 5000,
end_ts: 7000,
// no user input
additions: [
{
data: {
name: 'TimeSurvey',
xmlResponse: '<processed TimeSurvey response>',
match_id: 'foo',
start_ts: 5000,
end_ts: 7000,
},
metadata: 'foo',
},
],
},
] as any as TimelineEntry[];
it('creates a map that has the processed responses and notes', () => {
const [labelMap, notesMap] = mapInputsToTimelineEntries(
timelineEntriesEnketo,
fakeConfigEnketo,
);
expect(labelMap).toMatchObject({
trip1: {
SURVEY: {
data: { xmlResponse: '<processed TripConfirmSurvey response>' },
},
},
});
expect(notesMap['trip1'].length).toBe(1);
expect(notesMap['trip1'][0]).toMatchObject({
data: { xmlResponse: '<processed TimeSurvey response>' },
});
});
it('creates a map that combines processed and unprocessed responses and notes', async () => {
// insert some unprocessed data
await window['cordova'].plugins.BEMUserCache.putMessage('manual/trip_user_input', {
name: 'TripConfirmSurvey',
version: 1,
xmlResponse: '<unprocessed TripConfirmSurvey response>',
start_ts: 5000,
end_ts: 7000,
});
await window['cordova'].plugins.BEMUserCache.putMessage('manual/trip_addition_input', {
name: 'TimeSurvey',
xmlResponse: '<unprocessed TimeSurvey response>',
match_id: 'bar',
start_ts: 6000,
end_ts: 7000,
});
await updateLocalUnprocessedInputs({ start_ts: 1000, end_ts: 5000 }, fakeConfigEnketo);

// check that both processed and unprocessed data are returned
const [labelMap, notesMap] = mapInputsToTimelineEntries(
timelineEntriesEnketo,
fakeConfigEnketo,
);

expect(labelMap).toMatchObject({
trip1: {
SURVEY: {
data: { xmlResponse: '<processed TripConfirmSurvey response>' },
},
},
trip2: {
SURVEY: {
data: { xmlResponse: '<unprocessed TripConfirmSurvey response>' },
},
},
});

expect(notesMap['trip1'].length).toBe(1);
expect(notesMap['trip1'][0]).toMatchObject({
data: { xmlResponse: '<processed TimeSurvey response>' },
});

expect(notesMap['trip2'].length).toBe(2);
expect(notesMap['trip2'][0]).toMatchObject({
data: { xmlResponse: '<unprocessed TimeSurvey response>' },
});
expect(notesMap['trip2'][1]).toMatchObject({
data: { xmlResponse: '<processed TimeSurvey response>' },
});
});
});
11 changes: 8 additions & 3 deletions www/js/survey/inputMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { logDebug, displayErrorMsg } from '../plugin/logger';
import { DateTime } from 'luxon';
import { CompositeTrip, ConfirmedPlace, TimelineEntry, UserInputEntry } from '../types/diaryTypes';
import { keysForLabelInputs, unprocessedLabels, unprocessedNotes } from '../diary/timelineHelper';
import { getLabelInputDetails, inputType2retKey } from './multilabel/confirmHelper';
import {
getLabelInputDetails,
inputType2retKey,
removeManualPrefix,
} from './multilabel/confirmHelper';
import { TimelineLabelMap, TimelineNotesMap } from '../diary/LabelTabContext';
import { MultilabelKey } from '../types/labelTypes';
import { EnketoUserInputEntry } from './enketo/enketoHelper';
Expand Down Expand Up @@ -281,7 +285,8 @@ export function mapInputsToTimelineEntries(
timelineLabelMap[tlEntry._id.$oid] = { SURVEY: userInputForTrip };
} else {
let processedSurveyResponse;
for (const key of keysForLabelInputs(appConfig)) {
for (const dataKey of keysForLabelInputs(appConfig)) {
const key = removeManualPrefix(dataKey);
if (tlEntry.user_input?.[key]) {
processedSurveyResponse = tlEntry.user_input[key];
break;
Expand All @@ -293,7 +298,7 @@ export function mapInputsToTimelineEntries(
// MULTILABEL configuration: use the label inputs from the labelOptions to determine which
// keys to look for in the unprocessedInputs
const labelsForTrip: { [k: string]: UserInputEntry | undefined } = {};
Object.keys(getLabelInputDetails()).forEach((label: MultilabelKey) => {
Object.keys(getLabelInputDetails(appConfig)).forEach((label: MultilabelKey) => {
// Check unprocessed labels first since they are more recent
const userInputForTrip = getUserInputForTimelineEntry(
tlEntry,
Expand Down
7 changes: 5 additions & 2 deletions www/js/survey/multilabel/confirmHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,11 @@ export const getFakeEntry = (otherValue): Partial<LabelOption> | undefined => {
export const labelKeyToRichMode = (labelKey: string) =>
labelOptionByValue(labelKey, 'MODE')?.text || labelKeyToReadable(labelKey);

/* manual/mode_confirm becomes mode_confirm */
export const inputType2retKey = (inputType) => getLabelInputDetails()[inputType].key.split('/')[1];
/** @description e.g. manual/mode_confirm becomes mode_confirm */
export const removeManualPrefix = (key: string) => key.split('/')[1];
/** @description e.g. 'MODE' gets looked up, its key is 'manual/mode_confirm'. Returns without prefix as 'mode_confirm' */
export const inputType2retKey = (inputType: string) =>
removeManualPrefix(getLabelInputDetails()[inputType].key);

export function verifiabilityForTrip(trip: CompositeTrip, userInputForTrip?: UserInputMap) {
let allConfirmed = true;
Expand Down

0 comments on commit 9ddcf31

Please sign in to comment.