Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🧹 Enketo cleanup #1113

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 81 additions & 18 deletions www/__tests__/enketoHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
resolveLabel,
loadPreviousResponseForSurvey,
saveResponse,
fetchSurvey,
EnketoUserInputEntry,
} from '../js/survey/enketo/enketoHelper';
import { mockBEMUserCache } from '../__mocks__/cordovaMocks';
Expand Down Expand Up @@ -80,6 +81,7 @@ it('resolves the timestamps', () => {
const xmlParser = new window.DOMParser();
const timelineEntry = {
end_local_dt: { timezone: 'America/Los_Angeles' },
start_local_dt: { timezone: 'America/Los_Angeles' },
start_ts: 1469492672.928242,
end_ts: 1469493031,
} as CompositeTrip;
Expand Down Expand Up @@ -198,14 +200,12 @@ it('resolves the label, if no labelVars, returns template', async () => {
);
});

/**
* @param surveyName the name of the survey (e.g. "TimeUseSurvey")
* @param enketoForm the Form object from enketo-core that contains this survey
* @param appConfig the dynamic config file for the app
* @param opts object with SurveyOptions like 'timelineEntry' or 'dataKey'
* @returns Promise of the saved result, or an Error if there was a problem
*/
// export function saveResponse(surveyName: string, enketoForm: Form, appConfig, opts: SurveyOptions) {
/* cases tested here:
1. returns the label with options timestamps
2. returns the label with fallback timestamps
3. returns error about the invalid timestamps
4. errors out on invalid label vars
*/
it('gets the saved result or throws an error', async () => {
const surveyName = 'TimeUseSurvey';
const form = {
Expand All @@ -227,18 +227,21 @@ it('gets the saved result or throws an error', async () => {
formPath:
'https://raw.githubusercontent.com/sebastianbarry/nrel-openpath-deploy-configs/surveys-info-and-surveys-data/survey-resources/data-json/time-use-survey-form-v9.json',
labelTemplate: {
en: '{ erea, plural, =0 {} other {# Employment/Education, } }{ da, plural, =0 {} other {# Domestic, } }',
es: '{ erea, plural, =0 {} other {# Empleo/Educación, } }{ da, plural, =0 {} other {# Actividades domesticas, }}',
en: '{ erea, plural, =0 {} other {# Employment/Education, } }{ da, plural, =0 {} other {# Domestic, } }{ pca, plural, =0 {} other {# Personal Care, } }',
es: '{ erea, plural, =0 {} other {# Empleo/Educación, } }{ da, plural, =0 {} other {# Actividades domesticas, }}{ pca, plural, =0 {} other {# Cuidado, } }',
},
labelVars: {
da: { key: 'Domestic_activities', type: 'length' },
erea: { key: 'Employment_related_a_Education_activities', type: 'length' },
pca: { key: 'Personal_Care_activities', type: 'length' },
},
version: 9,
},
},
},
} as unknown as AppConfig;
mockBEMUserCache(config);

const opts = {
timelineEntry: {
end_local_dt: { timezone: 'America/Los_Angeles' },
Expand All @@ -247,11 +250,44 @@ it('gets the saved result or throws an error', async () => {
} as CompositeTrip,
};

console.log(config);
expect(saveResponse(surveyName, form, config, opts)).resolves.toMatchObject({
await expect(saveResponse(surveyName, form, config, opts)).resolves.toMatchObject({
label: '1 Personal Care',
name: 'TimeUseSurvey',
});
expect(async () => await saveResponse(surveyName, form, config, {})).resolves.toMatchObject({
label: '1 Personal Care',
name: 'TimeUseSurvey',
});
expect(async () => await saveResponse(surveyName, badForm, config, opts)).rejects.toThrowError(
'The times you entered are invalid. Please ensure that the start time is before the end time.',
);

//wrong label format
const bad_config = {
survey_info: {
surveys: {
TimeUseSurvey: {
compatibleWith: 1,
formPath:
'https://raw.githubusercontent.com/sebastianbarry/nrel-openpath-deploy-configs/surveys-info-and-surveys-data/survey-resources/data-json/time-use-survey-form-v9.json',
labelTemplate: {
en: '{ da, plural, =0 {} other {# Domestic, } }',
es: '{ da, plural, =0 {} other {# Actividades domesticas, }}',
},
labelVars: {
da: { key: 'Domestic_activities', type: 'width' },
},
version: 9,
},
},
},
} as unknown as AppConfig;

_test_resetStoredConfig();
mockBEMUserCache(bad_config);

expect(async () => await saveResponse(surveyName, form, bad_config, opts)).rejects.toThrow(
'labelVar type width is not supported!',);
expect(async () => await saveResponse(surveyName, badForm, config, opts)).rejects.toEqual(
'The times you entered are invalid. Please ensure that the start time is before the end time.',
);
Expand All @@ -264,8 +300,8 @@ it('gets the saved result or throws an error', async () => {
* Loading it on demand seems like the way to go. If we choose to experiment
* with incremental updates, we may want to revisit this.
*/
it('loads the previous response to a given survey', () => {
expect(loadPreviousResponseForSurvey('manual/demographic_survey')).resolves.toMatchObject({
it('loads the previous response to a given survey', async () => {
await expect(loadPreviousResponseForSurvey('manual/demographic_survey')).resolves.toMatchObject({
data: 'completed',
time: '01/01/2001',
});
Expand All @@ -276,7 +312,7 @@ it('loads the previous response to a given survey', () => {
* The version for filtering is specified in enketo survey `compatibleWith` config.
* The stored survey response version must be greater than or equal to `compatibleWith` to be included.
*/
it('filters the survey responses by their name and version', () => {
it('filters the survey responses by their name and version', async () => {
//no response -> no filtered responses
expect(filterByNameAndVersion('TimeUseSurvey', [], fakeConfig)).toStrictEqual([]);

Expand All @@ -296,7 +332,7 @@ it('filters the survey responses by their name and version', () => {
];

//one response -> that response
expect(filterByNameAndVersion('TimeUseSurvey', response, fakeConfig)).toStrictEqual(response);
await expect(filterByNameAndVersion('TimeUseSurvey', response, fakeConfig)).resolves.toStrictEqual(response);

const responses = [
{
Expand Down Expand Up @@ -337,6 +373,33 @@ it('filters the survey responses by their name and version', () => {
},
];

//several responses -> only the one that has a name match
expect(filterByNameAndVersion('TimeUseSurvey', responses, fakeConfig)).toStrictEqual(response);
//several responses -> only the one that has a name & version match
await expect(filterByNameAndVersion('TimeUseSurvey', responses, fakeConfig)).resolves.toStrictEqual(response);
});

it('fetches the survey', async () => {
global.fetch = (url: string) =>
new Promise((rs, rj) => {
setTimeout(() =>
rs({
text: () =>
new Promise((rs, rj) => {
let urlList = url.split('.');
let urlEnd = urlList[urlList.length - 1];
if (urlEnd === 'json') {
setTimeout(() => rs('{ "data": "is_json" }'), 100);
} else {
setTimeout(() => rs('not json'), 100);
}
}),
}),
);
}) as any;
await expect(
fetchSurvey(
'https://raw.githubusercontent.com/e-mission/nrel-openpath-deploy-configs/main/label_options/example-study-label-options.json',
),
).resolves.toMatchObject({ data: 'is_json' });

//test for the xml transformer?
});
20 changes: 14 additions & 6 deletions www/js/survey/enketo/enketoHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { transform } from 'enketo-transformer/web';
import { XMLParser } from 'fast-xml-parser';
import i18next from 'i18next';
import MessageFormat from '@messageformat/core';
import { logDebug, logInfo } from '../../plugin/logger';
import { logDebug } from '../../plugin/logger';
import { getConfig } from '../../config/dynamicConfig';
import { DateTime } from 'luxon';
import { fetchUrlCached } from '../../services/commHelper';
Expand Down Expand Up @@ -188,19 +188,20 @@ export function resolveTimestamps(
// if any of the fields are missing, return null
if (!startDate || !startTime || !endDate || !endTime) return null;

const timezone =
const start_timezone =
(timelineEntry as CompositeTrip).start_local_dt?.timezone ||
(timelineEntry as ConfirmedPlace).enter_local_dt?.timezone ||
(timelineEntry as ConfirmedPlace).enter_local_dt?.timezone;
const end_timezone =
(timelineEntry as CompositeTrip).end_local_dt?.timezone ||
(timelineEntry as ConfirmedPlace).exit_local_dt?.timezone;
// split by + or - to get time without offset
startTime = startTime.split(/\-|\+/)[0];
endTime = endTime.split(/\-|\+/)[0];

let additionStartTs = DateTime.fromISO(startDate + 'T' + startTime, {
zone: timezone,
zone: start_timezone,
}).toSeconds();
let additionEndTs = DateTime.fromISO(endDate + 'T' + endTime, { zone: timezone }).toSeconds();
let additionEndTs = DateTime.fromISO(endDate + 'T' + endTime, { zone: end_timezone }).toSeconds();

if (additionStartTs > additionEndTs) {
onFail(new Error(i18next.t('survey.enketo-timestamps-invalid'))); //"Timestamps are invalid. Please ensure that the start time is before the end time.");
Expand Down Expand Up @@ -247,7 +248,7 @@ export function saveResponse(
const jsonDocResponse = xml2js.parse(xmlResponse);
return resolveLabel(surveyName, xmlDoc)
.then((rsLabel) => {
let timestamps: TimestampRange | { ts: number; fmt_time: string } | undefined;
let timestamps: TimestampRange | { ts: number; fmt_time: string } | TimelineEntry | undefined;
let match_id: string | undefined;
if (opts?.timelineEntry) {
const resolvedTimestamps = resolveTimestamps(xmlDoc, opts.timelineEntry, (errOnFail) => {
Expand All @@ -266,6 +267,13 @@ export function saveResponse(
: opts.timelineEntry.exit_ts,
};
}
// if timestamps were not resolved from the survey, we will use the trip or place timestamps
Abby-Wheelis marked this conversation as resolved.
Show resolved Hide resolved
timestamps ||= opts.timelineEntry;
let time = {start_ts: 0, end_ts: 0}; // was data ... wasn't declared ... WHAT IS GOING ON HERE
time.start_ts = timestamps?.start_ts || opts.timelineEntry.enter_ts;
time.end_ts = timestamps?.end_ts || opts.timelineEntry.exit_ts;
console.log(time);

// UUID generated using this method https://stackoverflow.com/a/66332305
match_id = URL.createObjectURL(new Blob([])).slice(-36);
} else {
Expand Down
Loading