From 0815334ec707ce5804a60f4b3985e7b7f6caa304 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 11 Oct 2023 15:14:02 -0600 Subject: [PATCH 001/112] draft enketoHelper tests --- www/__tests__/enketoHelper.test.ts | 70 ++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 www/__tests__/enketoHelper.test.ts diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts new file mode 100644 index 000000000..537accb79 --- /dev/null +++ b/www/__tests__/enketoHelper.test.ts @@ -0,0 +1,70 @@ +import { getInstanceStr, filterByNameAndVersion } from '../js/survey/enketo/enketoHelper'; +import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; + +mockBEMUserCache(); + + +/** + * @param xmlModel the blank XML model response for the survey + * @param opts object with options like 'prefilledSurveyResponse' or 'prefillFields' + * @returns XML string of an existing or prefilled model response, or null if no response is available + */ +it('gets the model response, if avaliable, or returns null', ()=> { + const xmlModel = '\n \n \n \n \n \n \n ;'; + const filled = '\n \n car\n \n \n \n \n ;'; + const opts = {"prefilledSurveyResponse": filled}; + const opts2 = {"prefillFields": {"travel_mode" : "car"}}; + + //if no xmlModel, returns null + expect(getInstanceStr(null, opts)).toBe(null); + + //if there is a prefilled survey, return it + expect(getInstanceStr(xmlModel, opts)).toBe(filled); + + //if there is a model and fields, return prefilled + // expect(getInstanceStr(xmlModel, opts2)).toBe(filled); + //TODO - figure out how to use the helper function with JEST -- getElementsByTagName is empty? should it be? + + //if none of those things, also return null + expect(getInstanceStr(xmlModel, {})).toBe(null); +}); + +/** + * @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) { +it('gets the saved result or throws an error', () => { + +}); + +/* +* We retrieve all the records every time instead of caching because of the +* usage pattern. We assume that the demographic survey is edited fairly +* rarely, so loading it every time will likely do a bunch of unnecessary work. +* Loading it on demand seems like the way to go. If we choose to experiment +* with incremental updates, we may want to revisit this. +*/ +// export function loadPreviousResponseForSurvey(dataKey: string) { +it('loads the previous response to a given survey', () => { + +}); + +/** + * filterByNameAndVersion filter the survey answers by survey name and their version. + * The version for filtering is specified in enketo survey `compatibleWith` config. + * The stored survey answer version must be greater than or equal to `compatibleWith` to be included. + * @param {string} name survey name (defined in enketo survey config) + * @param {EnketoAnswer[]} answers survey answers + * (usually retrieved by calling UnifiedDataLoader.getUnifiedMessagesForInterval('manual/survey_response', tq)) method. + * @return {Promise} filtered survey answers + */ +it('filters the survey answers by their name and version', () => { + const surveyName = "TimeUseSurvey"; + const answers = []; + expect(filterByNameAndVersion(surveyName, answers)).resolves.toBe([]); + +}); From 43b8386acf595c1a9296d032c691feafb7e5255f Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 11 Oct 2023 15:15:38 -0600 Subject: [PATCH 002/112] convert answer.js into enketoHelper moving the methods form answer.js into enketoHelper as a part of the services migration --- www/js/survey/enketo/enketoHelper.ts | 192 +++++++++++++++++++++++++-- 1 file changed, 184 insertions(+), 8 deletions(-) diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 6e9147cf8..84c057658 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -2,7 +2,10 @@ import { getAngularService } from "../../angular-react-helper"; import { Form } from 'enketo-core'; import { XMLParser } from 'fast-xml-parser'; import i18next from 'i18next'; -import { logDebug } from "../../plugin/logger"; +import MessageFormat from 'messageformat'; +import { logDebug, logInfo } from "../../plugin/logger"; +import { getConfig } from '../../config/dynamicConfig'; +import { DateTime } from "luxon"; export type PrefillFields = {[key: string]: string}; @@ -14,6 +17,129 @@ export type SurveyOptions = { dataKey?: string; }; +type EnketoAnswerData = { + label: string; //display label (this value is use for displaying on the button) + ts: string; //the timestamp at which the survey was filled out (in seconds) + fmt_time: string; //the formatted timestamp at which the survey was filled out + name: string; //survey name + version: string; //survey version + xmlResponse: string; //survey answer XML string + jsonDocResponse: string; //survey answer JSON object +} + +type EnketoAnswer = { + data: EnketoAnswerData; //answer data + labels: [{[labelField:string]: string}]; //virtual labels (populated by populateLabels method) +} + +type EnketoSurveyConfig = { + [surveyName:string]: { + formPath: string + labelFields: string[]; + version: number; + compatibleWith: number; + } +} + +/** @type {EnketoSurveyConfig} _config */ +//TODO find a more appropriate way to store this +let _config: EnketoSurveyConfig; + +const LABEL_FUNCTIONS = { + UseLabelTemplate : async (xmlDoc: XMLDocument, name: string) => { + let configSurveys = await _lazyLoadConfig(); + + const config = configSurveys[name]; // config for this survey + const lang = i18next.resolvedLanguage; + const labelTemplate = config.labelTemplate?.[lang]; + + if (!labelTemplate) return "Answered"; // no template given in config + if (!config.labelVars) return labelTemplate; // if no vars given, nothing to interpolate, + // so we return the unaltered template + + // gather vars that will be interpolated into the template according to the survey config + const labelVars = {} + for (let lblVar in config.labelVars) { + const fieldName = config.labelVars[lblVar].key; + let fieldStr = _getAnswerByTagName(xmlDoc, fieldName); + if (fieldStr == '') fieldStr = null; + if (config.labelVars[lblVar].type == 'length') { + const fieldMatches = fieldStr?.split(' '); + labelVars[lblVar] = fieldMatches?.length || 0; + } else { + throw new Error(`labelVar type ${config.labelVars[lblVar].type } is not supported!`) + } + } + + // use MessageFormat interpolate the label template with the label vars + const mf = new MessageFormat(lang); + const label = mf.compile(labelTemplate)(labelVars); + return label.replace(/^[ ,]+|[ ,]+$/g, ''); // trim leading and trailing spaces and commas + } +} + +/** + * _getAnswerByTagName lookup for the survey answer by tag name form the given XML document. + * @param {XMLDocument} xmlDoc survey answer object + * @param {string} tagName tag name + * @returns {string} answer string. If not found, return "\" + */ + function _getAnswerByTagName(xmlDoc: XMLDocument, tagName: string) { + const vals = xmlDoc.getElementsByTagName(tagName); + const val = vals.length ? vals[0].innerHTML : null; + if (!val) return ''; + return val; +} + +/** + * _lazyLoadConfig load enketo survey config. If already loaded, return the cached config + * @returns {Promise} enketo survey config + */ +function _lazyLoadConfig() { + if (_config !== undefined) { + return Promise.resolve(_config); + } + return getConfig().then((newConfig) => { + logInfo("Resolved UI_CONFIG_READY promise in enketoHelper, filling in templates"); + _config = newConfig.survey_info.surveys; + return _config; + }) +} + +/** + * filterByNameAndVersion filter the survey answers by survey name and their version. + * The version for filtering is specified in enketo survey `compatibleWith` config. + * The stored survey answer version must be greater than or equal to `compatibleWith` to be included. + * @param {string} name survey name (defined in enketo survey config) + * @param {EnketoAnswer[]} answers survey answers + * (usually retrieved by calling UnifiedDataLoader.getUnifiedMessagesForInterval('manual/survey_response', tq)) method. + * @return {Promise} filtered survey answers + */ + export function filterByNameAndVersion(name: string, answers: EnketoAnswer[]) { + return _lazyLoadConfig().then(config => { + console.log("filtering by name and version,", name, config, answers); + answers.filter(answer => + answer.data.name === name && + answer.data.version >= config[name].compatibleWith + )} + ); +} + +/** + * resolve answer label for the survey + * @param {string} name survey name + * @param {XMLDocument} xmlDoc survey answer object + * @returns {Promise} label string Promise + */ +function resolveLabel(name: string, xmlDoc: XMLDocument) { + // Some studies may want a custom label function for their survey. + // Those can be added in LABEL_FUNCTIONS with the survey name as the key. + // Otherwise, UseLabelTemplate will create a label using the template in the config + if (LABEL_FUNCTIONS[name]) + return LABEL_FUNCTIONS[name](xmlDoc); + return LABEL_FUNCTIONS.UseLabelTemplate(xmlDoc, name); +} + /** * @param xmlModel the blank XML model to be prefilled * @param prefillFields an object with keys that are the XML tag names and values that are the values to be prefilled @@ -21,7 +147,7 @@ export type SurveyOptions = { */ function getXmlWithPrefills(xmlModel: string, prefillFields: PrefillFields) { if (!prefillFields) return null; - const xmlParser = new window.DOMParser(); + const xmlParser = new DOMParser(); const xmlDoc = xmlParser.parseFromString(xmlModel, 'text/xml'); for (const [tagName, value] of Object.entries(prefillFields)) { @@ -46,6 +172,57 @@ export function getInstanceStr(xmlModel: string, opts: SurveyOptions): string|nu return null; } +/** + * resolve timestamps label from the survey response + * @param {XMLDocument} xmlDoc survey answer object + * @param {object} trip trip object + * @returns {object} object with `start_ts` and `end_ts` + * - null if no timestamps are resolved + * - undefined if the timestamps are invalid + */ + function resolveTimestamps(xmlDoc, timelineEntry) { + // check for Date and Time fields + const startDate = xmlDoc.getElementsByTagName('Start_date')?.[0]?.innerHTML; + let startTime = xmlDoc.getElementsByTagName('Start_time')?.[0]?.innerHTML; + const endDate = xmlDoc.getElementsByTagName('End_date')?.[0]?.innerHTML; + let endTime = xmlDoc.getElementsByTagName('End_time')?.[0]?.innerHTML; + + // if any of the fields are missing, return null + if (!startDate || !startTime || !endDate || !endTime) return null; + + const timezone = timelineEntry.start_local_dt?.timezone + || timelineEntry.enter_local_dt?.timezone + || timelineEntry.end_local_dt?.timezone + || timelineEntry.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}).valueOf(); + let additionEndTs = DateTime.fromISO(endDate + "T" + endTime, {zone: timezone}).valueOf(); + + if (additionStartTs > additionEndTs) { + return undefined; // if the start time is after the end time, this is an invalid response + } + + /* Enketo survey time inputs are only precise to the minute, while trips/places are precise to + the millisecond. To avoid precision issues, we will check if the start/end timestamps from + the survey response are within the same minute as the start/end or enter/exit timestamps. + If so, we will use the exact trip/place timestamps */ + const entryStartTs = timelineEntry.start_ts || timelineEntry.enter_ts; + const entryEndTs = timelineEntry.end_ts || timelineEntry.exit_ts; + if (additionStartTs - (additionStartTs % 60) == entryStartTs - (entryStartTs % 60)) + additionStartTs = entryStartTs; + if (additionEndTs - (additionEndTs % 60) == entryEndTs - (entryEndTs % 60)) + additionEndTs = entryEndTs; + + // return unix timestamps in seconds + return { + start_ts: additionStartTs, + end_ts: additionEndTs + }; +} + /** * @param surveyName the name of the survey (e.g. "TimeUseSurvey") * @param enketoForm the Form object from enketo-core that contains this survey @@ -54,13 +231,13 @@ export function getInstanceStr(xmlModel: string, opts: SurveyOptions): string|nu * @returns Promise of the saved result, or an Error if there was a problem */ export function saveResponse(surveyName: string, enketoForm: Form, appConfig, opts: SurveyOptions) { - const EnketoSurveyAnswer = getAngularService('EnketoSurveyAnswer'); + // const EnketoSurveyAnswer = getAngularService('EnketoSurveyAnswer'); const xmlParser = new window.DOMParser(); const xmlResponse = enketoForm.getDataStr(); const xmlDoc = xmlParser.parseFromString(xmlResponse, 'text/xml'); const xml2js = new XMLParser({ignoreAttributes: false, attributeNamePrefix: 'attr'}); const jsonDocResponse = xml2js.parse(xmlResponse); - return EnketoSurveyAnswer.resolveLabel(surveyName, xmlDoc).then(rsLabel => { + return resolveLabel(surveyName, xmlDoc).then(rsLabel => { const data: any = { label: rsLabel, name: surveyName, @@ -69,15 +246,14 @@ export function saveResponse(surveyName: string, enketoForm: Form, appConfig, op jsonDocResponse, }; if (opts.timelineEntry) { - let timestamps = EnketoSurveyAnswer.resolveTimestamps(xmlDoc, opts.timelineEntry); + let timestamps = resolveTimestamps(xmlDoc, opts.timelineEntry); if (timestamps === undefined) { // timestamps were resolved, but they are invalid return new Error(i18next.t('survey.enketo-timestamps-invalid')); //"Timestamps are invalid. Please ensure that the start time is before the end time."); } // if timestamps were not resolved from the survey, we will use the trip or place timestamps - timestamps ||= opts.timelineEntry; - data.start_ts = timestamps.start_ts || timestamps.enter_ts; - data.end_ts = timestamps.end_ts || timestamps.exit_ts; + data.start_ts = timestamps.start_ts || opts.timelineEntry.enter_ts; + data.end_ts = timestamps.end_ts || opts.timelineEntry.exit_ts; // UUID generated using this method https://stackoverflow.com/a/66332305 data.match_id = URL.createObjectURL(new Blob([])).slice(-36); } else { From 9840b5a2914fcfc11ae932b47cf4563bded3e96f Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 11 Oct 2023 15:22:08 -0600 Subject: [PATCH 003/112] exclude platforms When testing, I was getting an error from Jest about duplicate modules, one of which was in platforms. This change resolves that error --- jest.config.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jest.config.json b/jest.config.json index 71bc5f5ca..8d194ffd0 100644 --- a/jest.config.json +++ b/jest.config.json @@ -13,6 +13,9 @@ "transformIgnorePatterns": [ "/node_modules/(?!(@react-native|react-native|react-native-vector-icons))" ], + "modulePathIgnorePatterns": [ + "/platforms/" + ], "moduleNameMapper": { "^react-native$": "react-native-web" } From 1eaf8b8e592d42a980168574452b0b1af98b54a9 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 11 Oct 2023 15:58:34 -0600 Subject: [PATCH 004/112] add more tests additional test for filterByNameAndVersion fake answers have been constructed to be filtered --- www/__tests__/enketoHelper.test.ts | 52 ++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index 537accb79..bdb55e112 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -63,8 +63,54 @@ it('loads the previous response to a given survey', () => { * @return {Promise} filtered survey answers */ it('filters the survey answers by their name and version', () => { - const surveyName = "TimeUseSurvey"; - const answers = []; - expect(filterByNameAndVersion(surveyName, answers)).resolves.toBe([]); + //no answers -> no filtered answers + expect(filterByNameAndVersion("TimeUseSurvey", [])).resolves.toBe([]); + const answer = [ + { + data: { + label: "Activity", //display label (this value is use for displaying on the button) + ts: "100000000", //the timestamp at which the survey was filled out (in seconds) + fmt_time: "12:36", //the formatted timestamp at which the survey was filled out + name: "TimeUseSurvey", //survey name + version: "1", //survey version + xmlResponse: "", //survey answer XML string + jsonDocResponse: "this is my json object" //survey answer JSON object + }, + labels: {labelField: "goodbye"} //TODO learn more about answer type + } + ]; + + //one answer -> that answer + expect(filterByNameAndVersion("TimeUseSurvey", answer)).resolves.toBe(answer); + + const answers = [ + { + data: { + label: "Activity", //display label (this value is use for displaying on the button) + ts: "100000000", //the timestamp at which the survey was filled out (in seconds) + fmt_time: "12:36", //the formatted timestamp at which the survey was filled out + name: "TimeUseSurvey", //survey name + version: "1", //survey version + xmlResponse: "", //survey answer XML string + jsonDocResponse: "this is my json object" //survey answer JSON object + }, + labels: {labelField: "goodbye"} + }, + { + data: { + label: "Activity", //display label (this value is use for displaying on the button) + ts: "100000000", //the timestamp at which the survey was filled out (in seconds) + fmt_time: "12:36", //the formatted timestamp at which the survey was filled out + name: "OtherSurvey", //survey name + version: "1", //survey version + xmlResponse: "", //survey answer XML string + jsonDocResponse: "this is my json object" //survey answer JSON object + }, + labels: {labelField: "goodbye"} + } + ]; + + //several answers -> only the one that has a name match + expect(filterByNameAndVersion("TimeUseSurvey", answers)).resolves.toBe(answer); }); From 74b847156c420c095ad891b9f15e487e461ec3e8 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 11 Oct 2023 16:12:56 -0600 Subject: [PATCH 005/112] remove answer.js completely remove answer.js and all references to it, replace references with references to enketoHelper.ts --- www/__tests__/enketoHelper.test.ts | 4 - www/index.js | 1 - www/js/survey/enketo/answer.js | 193 ------------------ .../survey/enketo/enketo-add-note-button.js | 8 +- www/js/survey/enketo/enketo-trip-button.js | 8 +- www/js/survey/enketo/enketoHelper.ts | 6 +- 6 files changed, 10 insertions(+), 210 deletions(-) delete mode 100644 www/js/survey/enketo/answer.js diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index bdb55e112..f601f0f27 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -57,10 +57,6 @@ it('loads the previous response to a given survey', () => { * filterByNameAndVersion filter the survey answers by survey name and their version. * The version for filtering is specified in enketo survey `compatibleWith` config. * The stored survey answer version must be greater than or equal to `compatibleWith` to be included. - * @param {string} name survey name (defined in enketo survey config) - * @param {EnketoAnswer[]} answers survey answers - * (usually retrieved by calling UnifiedDataLoader.getUnifiedMessagesForInterval('manual/survey_response', tq)) method. - * @return {Promise} filtered survey answers */ it('filters the survey answers by their name and version', () => { //no answers -> no filtered answers diff --git a/www/index.js b/www/index.js index 89c3a5e26..30070245f 100644 --- a/www/index.js +++ b/www/index.js @@ -21,7 +21,6 @@ import './js/survey/multilabel/infinite_scroll_filters.js'; import './js/survey/multilabel/multi-label-ui.js'; import './js/diary.js'; import './js/diary/services.js'; -import './js/survey/enketo/answer.js'; import './js/survey/enketo/infinite_scroll_filters.js'; import './js/survey/enketo/enketo-trip-button.js'; import './js/survey/enketo/enketo-add-note-button.js'; diff --git a/www/js/survey/enketo/answer.js b/www/js/survey/enketo/answer.js deleted file mode 100644 index e6077c479..000000000 --- a/www/js/survey/enketo/answer.js +++ /dev/null @@ -1,193 +0,0 @@ -import angular from 'angular'; -import MessageFormat from 'messageformat'; -import { getConfig } from '../../config/dynamicConfig'; - -angular.module('emission.survey.enketo.answer', ['ionic']) -.factory('EnketoSurveyAnswer', function($http) { - /** - * @typedef EnketoAnswerData - * @type {object} - * @property {string} label - display label (this value is use for displaying on the button) - * @property {string} ts - the timestamp at which the survey was filled out (in seconds) - * @property {string} fmt_time - the formatted timestamp at which the survey was filled out - * @property {string} name - survey name - * @property {string} version - survey version - * @property {string} xmlResponse - survey answer XML string - * @property {string} jsonDocResponse - survey answer JSON object - */ - - /** - * @typedef EnketoAnswer - * @type {object} - * @property {EnketoAnswerData} data - answer data - * @property {{[labelField:string]: string}} [labels] - virtual labels (populated by populateLabels method) - */ - - /** - * @typedef EnketoSurveyConfig - * @type {{ - * [surveyName:string]: { - * formPath: string; - * labelFields: string[]; - * version: number; - * compatibleWith: number; - * } - * }} - */ - - const LABEL_FUNCTIONS = { - UseLabelTemplate: (xmlDoc, name) => { - - return _lazyLoadConfig().then(configSurveys => { - - const config = configSurveys[name]; // config for this survey - const lang = i18next.resolvedLanguage; - const labelTemplate = config.labelTemplate?.[lang]; - - if (!labelTemplate) return "Answered"; // no template given in config - if (!config.labelVars) return labelTemplate; // if no vars given, nothing to interpolate, - // so we return the unaltered template - - // gather vars that will be interpolated into the template according to the survey config - const labelVars = {} - for (let lblVar in config.labelVars) { - const fieldName = config.labelVars[lblVar].key; - let fieldStr = _getAnswerByTagName(xmlDoc, fieldName); - if (fieldStr == '') fieldStr = null; - if (config.labelVars[lblVar].type == 'length') { - const fieldMatches = fieldStr?.split(' '); - labelVars[lblVar] = fieldMatches?.length || 0; - } else { - throw new Error(`labelVar type ${config.labelVars[lblVar].type } is not supported!`) - } - } - - // use MessageFormat interpolate the label template with the label vars - const mf = new MessageFormat(lang); - const label = mf.compile(labelTemplate)(labelVars); - return label.replace(/^[ ,]+|[ ,]+$/g, ''); // trim leading and trailing spaces and commas - }) - } - }; - - /** @type {EnketoSurveyConfig} _config */ - let _config; - - /** - * _getAnswerByTagName lookup for the survey answer by tag name form the given XML document. - * @param {XMLDocument} xmlDoc survey answer object - * @param {string} tagName tag name - * @returns {string} answer string. If not found, return "\" - */ - function _getAnswerByTagName(xmlDoc, tagName) { - const vals = xmlDoc.getElementsByTagName(tagName); - const val = vals.length ? vals[0].innerHTML : null; - if (!val) return ''; - return val; - } - - /** - * _lazyLoadConfig load enketo survey config. If already loaded, return the cached config - * @returns {Promise} enketo survey config - */ - function _lazyLoadConfig() { - if (_config !== undefined) { - return Promise.resolve(_config); - } - return getConfig().then((newConfig) => { - Logger.log("Resolved UI_CONFIG_READY promise in answer.js, filling in templates"); - _config = newConfig.survey_info.surveys; - return _config; - }) - } - - /** - * filterByNameAndVersion filter the survey answers by survey name and their version. - * The version for filtering is specified in enketo survey `compatibleWith` config. - * The stored survey answer version must be greater than or equal to `compatibleWith` to be included. - * @param {string} name survey name (defined in enketo survey config) - * @param {EnketoAnswer[]} answers survey answers - * (usually retrieved by calling UnifiedDataLoader.getUnifiedMessagesForInterval('manual/survey_response', tq)) method. - * @return {Promise} filtered survey answers - */ - function filterByNameAndVersion(name, answers) { - return _lazyLoadConfig().then(config => - answers.filter(answer => - answer.data.name === name && - answer.data.version >= config[name].compatibleWith - ) - ); - } - - /** - * resolve answer label for the survey - * @param {string} name survey name - * @param {XMLDocument} xmlDoc survey answer object - * @returns {Promise} label string Promise - */ - function resolveLabel(name, xmlDoc) { - // Some studies may want a custom label function for their survey. - // Those can be added in LABEL_FUNCTIONS with the survey name as the key. - // Otherwise, UseLabelTemplate will create a label using the template in the config - if (LABEL_FUNCTIONS[name]) - return LABEL_FUNCTIONS[name](xmlDoc); - return LABEL_FUNCTIONS.UseLabelTemplate(xmlDoc, name); - } - - /** - * resolve timestamps label from the survey response - * @param {XMLDocument} xmlDoc survey answer object - * @param {object} trip trip object - * @returns {object} object with `start_ts` and `end_ts` - * - null if no timestamps are resolved - * - undefined if the timestamps are invalid - */ - function resolveTimestamps(xmlDoc, timelineEntry) { - // check for Date and Time fields - const startDate = xmlDoc.getElementsByTagName('Start_date')?.[0]?.innerHTML; - let startTime = xmlDoc.getElementsByTagName('Start_time')?.[0]?.innerHTML; - const endDate = xmlDoc.getElementsByTagName('End_date')?.[0]?.innerHTML; - let endTime = xmlDoc.getElementsByTagName('End_time')?.[0]?.innerHTML; - - // if any of the fields are missing, return null - if (!startDate || !startTime || !endDate || !endTime) return null; - - const timezone = timelineEntry.start_local_dt?.timezone - || timelineEntry.enter_local_dt?.timezone - || timelineEntry.end_local_dt?.timezone - || timelineEntry.exit_local_dt?.timezone; - // split by + or - to get time without offset - startTime = startTime.split(/\-|\+/)[0]; - endTime = endTime.split(/\-|\+/)[0]; - - let additionStartTs = moment.tz(startDate+'T'+startTime, timezone).unix(); - let additionEndTs = moment.tz(endDate+'T'+endTime, timezone).unix(); - - if (additionStartTs > additionEndTs) { - return undefined; // if the start time is after the end time, this is an invalid response - } - - /* Enketo survey time inputs are only precise to the minute, while trips/places are precise to - the millisecond. To avoid precision issues, we will check if the start/end timestamps from - the survey response are within the same minute as the start/end or enter/exit timestamps. - If so, we will use the exact trip/place timestamps */ - const entryStartTs = timelineEntry.start_ts || timelineEntry.enter_ts; - const entryEndTs = timelineEntry.end_ts || timelineEntry.exit_ts; - if (additionStartTs - (additionStartTs % 60) == entryStartTs - (entryStartTs % 60)) - additionStartTs = entryStartTs; - if (additionEndTs - (additionEndTs % 60) == entryEndTs - (entryEndTs % 60)) - additionEndTs = entryEndTs; - - // return unix timestamps in seconds - return { - start_ts: additionStartTs, - end_ts: additionEndTs - }; - } - - return { - filterByNameAndVersion, - resolveLabel, - resolveTimestamps, - }; -}); diff --git a/www/js/survey/enketo/enketo-add-note-button.js b/www/js/survey/enketo/enketo-add-note-button.js index 49f7747f6..6dc6be7e5 100644 --- a/www/js/survey/enketo/enketo-add-note-button.js +++ b/www/js/survey/enketo/enketo-add-note-button.js @@ -3,12 +3,12 @@ */ import angular from 'angular'; +import { filterByNameAndVersion } from './enketoHelper' angular.module('emission.survey.enketo.add-note-button', ['emission.services', - 'emission.survey.enketo.answer', 'emission.survey.inputmatcher']) -.factory("EnketoNotesButtonService", function(InputMatcher, EnketoSurveyAnswer, Logger, $timeout) { +.factory("EnketoNotesButtonService", function(InputMatcher, Logger, $timeout) { var enbs = {}; console.log("Creating EnketoNotesButtonService"); enbs.SINGLE_KEY="NOTES"; @@ -33,9 +33,9 @@ angular.module('emission.survey.enketo.add-note-button', * Embed 'inputType' to the timelineEntry. */ enbs.extractResult = function(results) { - const resultsPromises = [EnketoSurveyAnswer.filterByNameAndVersion(enbs.timelineEntrySurveyName, results)]; + const resultsPromises = [filterByNameAndVersion(enbs.timelineEntrySurveyName, results)]; if (enbs.timelineEntrySurveyName != enbs.placeSurveyName) { - resultsPromises.push(EnketoSurveyAnswer.filterByNameAndVersion(enbs.placeSurveyName, results)); + resultsPromises.push(filterByNameAndVersion(enbs.placeSurveyName, results)); } return Promise.all(resultsPromises); }; diff --git a/www/js/survey/enketo/enketo-trip-button.js b/www/js/survey/enketo/enketo-trip-button.js index 6e710435f..623137450 100644 --- a/www/js/survey/enketo/enketo-trip-button.js +++ b/www/js/survey/enketo/enketo-trip-button.js @@ -12,11 +12,11 @@ */ import angular from 'angular'; +import { filterByNameAndVersion } from "./enketoHelper"; angular.module('emission.survey.enketo.trip.button', - ['emission.survey.enketo.answer', - 'emission.survey.inputmatcher']) -.factory("EnketoTripButtonService", function(InputMatcher, EnketoSurveyAnswer, Logger, $timeout) { + ['emission.survey.inputmatcher']) +.factory("EnketoTripButtonService", function(InputMatcher, Logger, $timeout) { var etbs = {}; console.log("Creating EnketoTripButtonService"); etbs.key = "manual/trip_user_input"; @@ -26,7 +26,7 @@ angular.module('emission.survey.enketo.trip.button', /** * Embed 'inputType' to the trip. */ - etbs.extractResult = (results) => EnketoSurveyAnswer.filterByNameAndVersion('TripConfirmSurvey', results); + etbs.extractResult = (results) => filterByNameAndVersion('TripConfirmSurvey', results); etbs.processManualInputs = function(manualResults, resultMap) { if (manualResults.length > 1) { diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 84c057658..83085ebd7 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -116,12 +116,11 @@ function _lazyLoadConfig() { * @return {Promise} filtered survey answers */ export function filterByNameAndVersion(name: string, answers: EnketoAnswer[]) { - return _lazyLoadConfig().then(config => { - console.log("filtering by name and version,", name, config, answers); + return _lazyLoadConfig().then(config => answers.filter(answer => answer.data.name === name && answer.data.version >= config[name].compatibleWith - )} + ) ); } @@ -231,7 +230,6 @@ export function getInstanceStr(xmlModel: string, opts: SurveyOptions): string|nu * @returns Promise of the saved result, or an Error if there was a problem */ export function saveResponse(surveyName: string, enketoForm: Form, appConfig, opts: SurveyOptions) { - // const EnketoSurveyAnswer = getAngularService('EnketoSurveyAnswer'); const xmlParser = new window.DOMParser(); const xmlResponse = enketoForm.getDataStr(); const xmlDoc = xmlParser.parseFromString(xmlResponse, 'text/xml'); From d259799020bd604f852fc19476af6470e965e41c Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 12 Oct 2023 10:25:44 -0600 Subject: [PATCH 006/112] adding tests for resolveTimestamps resolveTimestamps is a helper function to saveResponse, but still contains a fair amount of its own logic. Testing the edge cases for this function ensures that it will behave as expected within the larger context --- www/__tests__/enketoHelper.test.ts | 21 ++++++++++++++++++++- www/js/survey/enketo/enketoHelper.ts | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index f601f0f27..6ba78d170 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -1,4 +1,4 @@ -import { getInstanceStr, filterByNameAndVersion } from '../js/survey/enketo/enketoHelper'; +import { getInstanceStr, filterByNameAndVersion, resolveTimestamps } from '../js/survey/enketo/enketoHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; mockBEMUserCache(); @@ -29,6 +29,25 @@ it('gets the model response, if avaliable, or returns null', ()=> { expect(getInstanceStr(xmlModel, {})).toBe(null); }); +//resolve timestamps +it('resolves the timestamps', () => { + const xmlParser = new window.DOMParser(); + const timelineEntry = { end_local_dt: {timezone: "America/Los_Angeles"}, start_ts: 1469492672.928242, end_ts: 1469493031}; + + //missing data returns null + const missingData = ' 2016-08-28 2016-07-25 17:30:31.000-06:00 '; + const missDataDoc = xmlParser.parseFromString(missingData, 'text/html'); + expect(resolveTimestamps(missDataDoc, timelineEntry)).toBeNull(); + //bad time returns undefined + const badTimes = ' 2016-08-28 2016-07-25 17:32:32.928-06:00 17:30:31.000-06:00 '; + const badTimeDoc = xmlParser.parseFromString(badTimes, 'text/xml'); + expect(resolveTimestamps(badTimeDoc, timelineEntry)).toBeUndefined(); + //good info returns unix start and end timestamps -- TODO : address precise vs less precise? + const timeSurvey = ' 2016-07-25 2016-07-25 17:24:32.928-06:00 17:30:31.000-06:00 '; + const xmlDoc = xmlParser.parseFromString(timeSurvey, 'text/xml'); + expect(resolveTimestamps(xmlDoc, timelineEntry)).toMatchObject({start_ts: 1469492672928, end_ts: 1469493031000}); +}); + /** * @param surveyName the name of the survey (e.g. "TimeUseSurvey") * @param enketoForm the Form object from enketo-core that contains this survey diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 83085ebd7..99045c222 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -179,7 +179,7 @@ export function getInstanceStr(xmlModel: string, opts: SurveyOptions): string|nu * - null if no timestamps are resolved * - undefined if the timestamps are invalid */ - function resolveTimestamps(xmlDoc, timelineEntry) { + export function resolveTimestamps(xmlDoc, timelineEntry) { // check for Date and Time fields const startDate = xmlDoc.getElementsByTagName('Start_date')?.[0]?.innerHTML; let startTime = xmlDoc.getElementsByTagName('Start_time')?.[0]?.innerHTML; From 11a3df66eb4d9d15216800754af3a4c1d2dbdb95 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 12 Oct 2023 12:32:51 -0600 Subject: [PATCH 007/112] testing for _lazyLoadConfig adding a basic test for _loadLazyConfig, in order to ensure that my mock setup for that works, before moving into testing functions that depend on it --- www/__mocks__/cordovaMocks.ts | 27 +++++++++++++++++++++++ www/__tests__/enketoHelper.test.ts | 33 +++++++++++++++++++--------- www/js/survey/enketo/enketoHelper.ts | 4 ++-- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 44c21677c..31e3e7bf4 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -85,6 +85,33 @@ export const mockBEMUserCache = () => { rs(messages.filter(m => m.key == key).map(m => m.value)); }, 100) ); + }, + getDocument: (key: string, withMetadata?: boolean) => { + // this was mocked specifically for enketoHelper's use, could be expanded if needed + const fakeSurveyConfig = { + 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: " erea, plural, =0 {} other {# Employment/Education, } }{ da, plural, =0 {} other {# Domestic activities, }", + es: " erea, plural, =0 {} other {# Empleo/Educación, } }{ da, plural, =0 {} other {# Actividades domesticas, }"}, + labelVars: {da: {key: "Domestic_activities", type: "length"}, + erea: {key: "Employment_related_a_Education_activities", type:"length"}}, + version: 9} + } + } + } + + if(key == "config/app_ui_config"){ + return new Promise((rs, rj) => + setTimeout(() => { + rs(fakeSurveyConfig); + }, 100) + ); + } + else { + return null; + } } } window['cordova'] ||= {}; diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index 6ba78d170..54a147904 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -1,14 +1,27 @@ -import { getInstanceStr, filterByNameAndVersion, resolveTimestamps } from '../js/survey/enketo/enketoHelper'; +import { getInstanceStr, filterByNameAndVersion, resolveTimestamps, resolveLabel, _lazyLoadConfig} from '../js/survey/enketo/enketoHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; +import { mockLogger } from '../__mocks__/globalMocks'; mockBEMUserCache(); +mockLogger(); + +it('gets the survey config', async () => { + //this is aimed at testing my mock of the config + //mocked getDocument for the case of getting the config + let config = await _lazyLoadConfig(); + let mockSurveys = { + 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: " erea, plural, =0 {} other {# Employment/Education, } }{ da, plural, =0 {} other {# Domestic activities, }", + es: " erea, plural, =0 {} other {# Empleo/Educación, } }{ da, plural, =0 {} other {# Actividades domesticas, }"}, + labelVars: {da: {key: "Domestic_activities", type: "length"}, + erea: {key: "Employment_related_a_Education_activities", type:"length"}}, + version: 9} + } + // console.log(config); + expect(config).toMatchObject(mockSurveys); +}) - -/** - * @param xmlModel the blank XML model response for the survey - * @param opts object with options like 'prefilledSurveyResponse' or 'prefillFields' - * @returns XML string of an existing or prefilled model response, or null if no response is available - */ it('gets the model response, if avaliable, or returns null', ()=> { const xmlModel = '\n \n \n \n \n \n \n ;'; const filled = '\n \n car\n \n \n \n \n ;'; @@ -79,7 +92,7 @@ it('loads the previous response to a given survey', () => { */ it('filters the survey answers by their name and version', () => { //no answers -> no filtered answers - expect(filterByNameAndVersion("TimeUseSurvey", [])).resolves.toBe([]); + expect(filterByNameAndVersion("TimeUseSurvey", [])).resolves.toStrictEqual([]); const answer = [ { @@ -97,7 +110,7 @@ it('filters the survey answers by their name and version', () => { ]; //one answer -> that answer - expect(filterByNameAndVersion("TimeUseSurvey", answer)).resolves.toBe(answer); + expect(filterByNameAndVersion("TimeUseSurvey", answer)).resolves.toStrictEqual(answer); const answers = [ { @@ -127,5 +140,5 @@ it('filters the survey answers by their name and version', () => { ]; //several answers -> only the one that has a name match - expect(filterByNameAndVersion("TimeUseSurvey", answers)).resolves.toBe(answer); + expect(filterByNameAndVersion("TimeUseSurvey", answers)).resolves.toStrictEqual(answer); }); diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 99045c222..c757ac72b 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -95,7 +95,7 @@ const LABEL_FUNCTIONS = { * _lazyLoadConfig load enketo survey config. If already loaded, return the cached config * @returns {Promise} enketo survey config */ -function _lazyLoadConfig() { +export function _lazyLoadConfig() { if (_config !== undefined) { return Promise.resolve(_config); } @@ -179,7 +179,7 @@ export function getInstanceStr(xmlModel: string, opts: SurveyOptions): string|nu * - null if no timestamps are resolved * - undefined if the timestamps are invalid */ - export function resolveTimestamps(xmlDoc, timelineEntry) { +export function resolveTimestamps(xmlDoc, timelineEntry) { // check for Date and Time fields const startDate = xmlDoc.getElementsByTagName('Start_date')?.[0]?.innerHTML; let startTime = xmlDoc.getElementsByTagName('Start_time')?.[0]?.innerHTML; From bc42845ecfb91e16355cce92a8bad3b3435ab0e7 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 12 Oct 2023 16:27:51 -0600 Subject: [PATCH 008/112] attempting to add more tests currently struggling with i18next and MessageFormat, as I can't get either of those mocked and working --- www/__mocks__/i18nextMocks.ts | 8 +++++++ www/__mocks__/messageFormatMocks.ts | 32 +++++++++++++++++++++++++ www/__tests__/enketoHelper.test.ts | 36 +++++++++++++++++++++++++--- www/js/survey/enketo/enketoHelper.ts | 2 +- 4 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 www/__mocks__/i18nextMocks.ts create mode 100644 www/__mocks__/messageFormatMocks.ts diff --git a/www/__mocks__/i18nextMocks.ts b/www/__mocks__/i18nextMocks.ts new file mode 100644 index 000000000..dc0d3f2b4 --- /dev/null +++ b/www/__mocks__/i18nextMocks.ts @@ -0,0 +1,8 @@ +const i18next = jest.createMockFromModule('i18next'); + +let resolvedLanugage; + +function _setUpLanguage(language) { + console.log("setting resolved language to ", language, " for testing"); + resolvedLanugage = language; +} diff --git a/www/__mocks__/messageFormatMocks.ts b/www/__mocks__/messageFormatMocks.ts new file mode 100644 index 000000000..f32c07ed4 --- /dev/null +++ b/www/__mocks__/messageFormatMocks.ts @@ -0,0 +1,32 @@ +//call signature MessageFormat.compile(templage)(vars); +//in - template an vars -- {... pca: 0, ...} +//out - 1 Personal Care, + +export default class MessageFormat{ + + constructor( locale: string ) { } + + compile(message: string) { + return (vars: {}) => { + let label = ""; + const brokenList = message.split("}{"); + console.log(brokenList); + + for (let key in vars) { + brokenList.forEach((item) => { + let brokenItem = item.split(","); + if(brokenItem[0] == key) { + let getLabel = brokenItem[2].split("#"); + console.log(getLabel); + label = vars[key] + " " + getLabel[1]; + return label; + } + }) + } + + } + } +} + +exports.MessageFormat = MessageFormat; + \ No newline at end of file diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index 54a147904..96475bd58 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -1,9 +1,15 @@ -import { getInstanceStr, filterByNameAndVersion, resolveTimestamps, resolveLabel, _lazyLoadConfig} from '../js/survey/enketo/enketoHelper'; +import { getInstanceStr, filterByNameAndVersion, resolveTimestamps, resolveLabel, _lazyLoadConfig, loadPreviousResponseForSurvey} from '../js/survey/enketo/enketoHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; +import i18next from "i18next"; + mockBEMUserCache(); mockLogger(); +// jest.mock('../__mocks__/messageFormatMocks'); +// jest.mock("i18next"); + +// global.i18next = { resolvedLanguage : "en" } it('gets the survey config', async () => { //this is aimed at testing my mock of the config @@ -18,7 +24,6 @@ it('gets the survey config', async () => { erea: {key: "Employment_related_a_Education_activities", type:"length"}}, version: 9} } - // console.log(config); expect(config).toMatchObject(mockSurveys); }) @@ -61,6 +66,30 @@ it('resolves the timestamps', () => { expect(resolveTimestamps(xmlDoc, timelineEntry)).toMatchObject({start_ts: 1469492672928, end_ts: 1469493031000}); }); +//resolve label +// it('resolves the label', async () => { +// i18next.init({ +// fallbackLng: 'en', +// debug: true +// }, (err, t) => { +// if (err) return console.log('something went wrong loading', err); +// t('key'); // -> same as i18next.t +// }); + +// console.log("language in tests", i18next.resolvedLanguage); +// const xmlParser = new window.DOMParser(); +// //have a custom survey label function TODO: we currently don't have custome label functions, but should test when we do + +// //no custom function, fallback to UseLabelTemplate +// const xmlString = ' option_1/Domestic_activities> option_2 '; +// const xmlDoc = xmlParser.parseFromString(xmlString, 'text/html'); + +// //if no template, returns "Answered" +// expect(await resolveLabel("TimeUseSurvey", xmlDoc)).toBe(""); +// //if no labelVars, returns template +// //else interpolates +// }); + /** * @param surveyName the name of the survey (e.g. "TimeUseSurvey") * @param enketoForm the Form object from enketo-core that contains this survey @@ -82,7 +111,8 @@ it('gets the saved result or throws an error', () => { */ // export function loadPreviousResponseForSurvey(dataKey: string) { it('loads the previous response to a given survey', () => { - + //not really sure if I can test this yet given that it relies on an angular service... + loadPreviousResponseForSurvey("manual/demographic_survey"); }); /** diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index c757ac72b..f72921582 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -130,7 +130,7 @@ export function _lazyLoadConfig() { * @param {XMLDocument} xmlDoc survey answer object * @returns {Promise} label string Promise */ -function resolveLabel(name: string, xmlDoc: XMLDocument) { +export function resolveLabel(name: string, xmlDoc: XMLDocument) { // Some studies may want a custom label function for their survey. // Those can be added in LABEL_FUNCTIONS with the survey name as the key. // Otherwise, UseLabelTemplate will create a label using the template in the config From 608d97ddaa9447076259905374a933e61d34146b Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 13 Oct 2023 09:29:04 -0600 Subject: [PATCH 009/112] update messageformat plugin the message format plugin moved! https://github.com/messageformat/messageformat/tree/main/packages/core --- package.cordovabuild.json | 2 +- package.serve.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.cordovabuild.json b/package.cordovabuild.json index b5d69872f..306362726 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -102,6 +102,7 @@ }, "dependencies": { "@havesource/cordova-plugin-push": "git+https://github.com/havesource/cordova-plugin-push.git#4.0.0-dev.0", + "@messageformat/core": "^3.2.0", "@react-navigation/native": "^6.1.7", "@react-navigation/stack": "^6.3.17", "@shopify/flash-list": "^1.3.1", @@ -151,7 +152,6 @@ "klaw-sync": "^6.0.0", "leaflet": "^1.9.4", "luxon": "^3.3.0", - "messageformat": "^2.3.0", "moment": "^2.29.4", "moment-timezone": "^0.5.43", "ng-i18next": "^1.0.7", diff --git a/package.serve.json b/package.serve.json index 57470bc2d..c5ad26404 100644 --- a/package.serve.json +++ b/package.serve.json @@ -49,6 +49,7 @@ "webpack-cli": "^5.0.1" }, "dependencies": { + "@messageformat/core": "^3.2.0", "@react-navigation/native": "^6.1.7", "@react-navigation/stack": "^6.3.17", "@shopify/flash-list": "^1.3.1", @@ -78,7 +79,6 @@ "klaw-sync": "^6.0.0", "leaflet": "^1.9.4", "luxon": "^3.3.0", - "messageformat": "^2.3.0", "moment": "^2.29.4", "moment-timezone": "^0.5.43", "ng-i18next": "^1.0.7", From 8b901f6294c387bf69b08f6ec3eb3ec3cde5b494 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 13 Oct 2023 10:12:46 -0600 Subject: [PATCH 010/112] start to configure i18next for tests setting __DEV__ to false in globals, so that it can be used throught the testing suit calling the i18n setup in the tests should work once we incorporate the React testing changes --- jest.config.json | 3 +++ www/__tests__/enketoHelper.test.ts | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/jest.config.json b/jest.config.json index 8d194ffd0..21ba92e12 100644 --- a/jest.config.json +++ b/jest.config.json @@ -18,5 +18,8 @@ ], "moduleNameMapper": { "^react-native$": "react-native-web" + }, + "globals" : { + "__DEV__": true } } diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index 96475bd58..367cfd8bc 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -2,7 +2,8 @@ import { getInstanceStr, filterByNameAndVersion, resolveTimestamps, resolveLabel import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; -import i18next from "i18next"; +// import initializedI18next from '../js/i18nextInit'; +// window['i18next'] = initializedI18next; mockBEMUserCache(); mockLogger(); From cea96dd3b3ea9030a98f6b17358be6a83586ddff Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 13 Oct 2023 10:13:14 -0600 Subject: [PATCH 011/112] update the message format import --- www/js/survey/enketo/enketoHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index f72921582..fa7d300e3 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -2,7 +2,7 @@ import { getAngularService } from "../../angular-react-helper"; import { Form } from 'enketo-core'; import { XMLParser } from 'fast-xml-parser'; import i18next from 'i18next'; -import MessageFormat from 'messageformat'; +import MessageFormat from '@messageformat/core'; import { logDebug, logInfo } from "../../plugin/logger"; import { getConfig } from '../../config/dynamicConfig'; import { DateTime } from "luxon"; From b7f6d68d5385f87ce0066aeabfc5fec831afa78c Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 13 Oct 2023 10:16:31 -0600 Subject: [PATCH 012/112] updates to testing --- www/__tests__/enketoHelper.test.ts | 40 ++++++++++++------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index 367cfd8bc..ab3ef963e 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -68,28 +68,20 @@ it('resolves the timestamps', () => { }); //resolve label -// it('resolves the label', async () => { -// i18next.init({ -// fallbackLng: 'en', -// debug: true -// }, (err, t) => { -// if (err) return console.log('something went wrong loading', err); -// t('key'); // -> same as i18next.t -// }); - -// console.log("language in tests", i18next.resolvedLanguage); -// const xmlParser = new window.DOMParser(); -// //have a custom survey label function TODO: we currently don't have custome label functions, but should test when we do - -// //no custom function, fallback to UseLabelTemplate -// const xmlString = ' option_1/Domestic_activities> option_2 '; -// const xmlDoc = xmlParser.parseFromString(xmlString, 'text/html'); +it('resolves the label', async () => { + const xmlParser = new window.DOMParser(); -// //if no template, returns "Answered" -// expect(await resolveLabel("TimeUseSurvey", xmlDoc)).toBe(""); -// //if no labelVars, returns template -// //else interpolates -// }); + //have a custom survey label function TODO: we currently don't have custome label functions, but should test when we do + + //no custom function, fallback to UseLabelTemplate + const xmlString = ' option_1/Domestic_activities> option_2 '; + const xmlDoc = xmlParser.parseFromString(xmlString, 'text/html'); + + //if no template, returns "Answered" + expect(await resolveLabel("TimeUseSurvey", xmlDoc)).toBe(""); + //if no labelVars, returns template + //else interpolates +}); /** * @param surveyName the name of the survey (e.g. "TimeUseSurvey") @@ -136,7 +128,7 @@ it('filters the survey answers by their name and version', () => { xmlResponse: "", //survey answer XML string jsonDocResponse: "this is my json object" //survey answer JSON object }, - labels: {labelField: "goodbye"} //TODO learn more about answer type + labels: [{labelField: "goodbye"}] //TODO learn more about answer type } ]; @@ -154,7 +146,7 @@ it('filters the survey answers by their name and version', () => { xmlResponse: "", //survey answer XML string jsonDocResponse: "this is my json object" //survey answer JSON object }, - labels: {labelField: "goodbye"} + labels: [{labelField: "goodbye"}] }, { data: { @@ -166,7 +158,7 @@ it('filters the survey answers by their name and version', () => { xmlResponse: "", //survey answer XML string jsonDocResponse: "this is my json object" //survey answer JSON object }, - labels: {labelField: "goodbye"} + labels: [{labelField: "goodbye"}] } ]; From 75a0371ae8de99b5cb80bcbe797c7371b3d4255f Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 13 Oct 2023 10:48:46 -0600 Subject: [PATCH 013/112] updates to tests remove old i18n code, update types, comment out broken tests --- www/__tests__/enketoHelper.test.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index ab3ef963e..5603a010e 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -7,10 +7,6 @@ import { mockLogger } from '../__mocks__/globalMocks'; mockBEMUserCache(); mockLogger(); -// jest.mock('../__mocks__/messageFormatMocks'); -// jest.mock("i18next"); - -// global.i18next = { resolvedLanguage : "en" } it('gets the survey config', async () => { //this is aimed at testing my mock of the config @@ -78,7 +74,7 @@ it('resolves the label', async () => { const xmlDoc = xmlParser.parseFromString(xmlString, 'text/html'); //if no template, returns "Answered" - expect(await resolveLabel("TimeUseSurvey", xmlDoc)).toBe(""); + // expect(await resolveLabel("TimeUseSurvey", xmlDoc)).toBe(""); //if no labelVars, returns template //else interpolates }); @@ -105,7 +101,7 @@ it('gets the saved result or throws an error', () => { // export function loadPreviousResponseForSurvey(dataKey: string) { it('loads the previous response to a given survey', () => { //not really sure if I can test this yet given that it relies on an angular service... - loadPreviousResponseForSurvey("manual/demographic_survey"); + // loadPreviousResponseForSurvey("manual/demographic_survey"); }); /** @@ -128,7 +124,7 @@ it('filters the survey answers by their name and version', () => { xmlResponse: "", //survey answer XML string jsonDocResponse: "this is my json object" //survey answer JSON object }, - labels: [{labelField: "goodbye"}] //TODO learn more about answer type + metadata: {} } ]; @@ -146,7 +142,7 @@ it('filters the survey answers by their name and version', () => { xmlResponse: "", //survey answer XML string jsonDocResponse: "this is my json object" //survey answer JSON object }, - labels: [{labelField: "goodbye"}] + metadata: {} }, { data: { @@ -158,7 +154,7 @@ it('filters the survey answers by their name and version', () => { xmlResponse: "", //survey answer XML string jsonDocResponse: "this is my json object" //survey answer JSON object }, - labels: [{labelField: "goodbye"}] + metadata: {} } ]; From 09f21bc92cd1c9bad45986f465a0071a00f7c021 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 13 Oct 2023 10:49:14 -0600 Subject: [PATCH 014/112] update types from log statements, these answers have data and metadata, no labels --- www/js/survey/enketo/enketoHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index fa7d300e3..af578b2d9 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -29,7 +29,7 @@ type EnketoAnswerData = { type EnketoAnswer = { data: EnketoAnswerData; //answer data - labels: [{[labelField:string]: string}]; //virtual labels (populated by populateLabels method) + metadata: any; } type EnketoSurveyConfig = { From e7fe7a8005c039240c6a4bab32141ecee4a612d5 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 13 Oct 2023 11:03:30 -0600 Subject: [PATCH 015/112] don't mock i18n, use the real thing with the changes from #1049, we are now able to test using i18n, no need to mock! --- www/__mocks__/i18nextMocks.ts | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 www/__mocks__/i18nextMocks.ts diff --git a/www/__mocks__/i18nextMocks.ts b/www/__mocks__/i18nextMocks.ts deleted file mode 100644 index dc0d3f2b4..000000000 --- a/www/__mocks__/i18nextMocks.ts +++ /dev/null @@ -1,8 +0,0 @@ -const i18next = jest.createMockFromModule('i18next'); - -let resolvedLanugage; - -function _setUpLanguage(language) { - console.log("setting resolved language to ", language, " for testing"); - resolvedLanugage = language; -} From 70f98cd87adce8ad9408b76ec2d24c40a4c6d60d Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 13 Oct 2023 11:38:17 -0600 Subject: [PATCH 016/112] set up baseline testing for resolveLabels introduced i18next for the tests updated config in mock, and test of loading config to be accurate to what is expected (missing some '{' ) adjust formatting of function indentation --- www/__mocks__/cordovaMocks.ts | 4 +-- www/__tests__/enketoHelper.test.ts | 38 +++++++++++------------ www/js/survey/enketo/enketoHelper.ts | 45 ++++++++++++++-------------- 3 files changed, 44 insertions(+), 43 deletions(-) diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 31e3e7bf4..7590d0422 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -93,8 +93,8 @@ export const mockBEMUserCache = () => { 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: " erea, plural, =0 {} other {# Employment/Education, } }{ da, plural, =0 {} other {# Domestic activities, }", - es: " erea, plural, =0 {} other {# Empleo/Educación, } }{ da, plural, =0 {} other {# Actividades domesticas, }"}, + 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, }}"}, labelVars: {da: {key: "Domestic_activities", type: "length"}, erea: {key: "Employment_related_a_Education_activities", type:"length"}}, version: 9} diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index 5603a010e..7a13303a8 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -2,8 +2,8 @@ import { getInstanceStr, filterByNameAndVersion, resolveTimestamps, resolveLabel import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; -// import initializedI18next from '../js/i18nextInit'; -// window['i18next'] = initializedI18next; +import initializedI18next from '../js/i18nextInit'; +window['i18next'] = initializedI18next; mockBEMUserCache(); mockLogger(); @@ -13,14 +13,14 @@ it('gets the survey config', async () => { //mocked getDocument for the case of getting the config let config = await _lazyLoadConfig(); let mockSurveys = { - 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: " erea, plural, =0 {} other {# Employment/Education, } }{ da, plural, =0 {} other {# Domestic activities, }", - es: " erea, plural, =0 {} other {# Empleo/Educación, } }{ da, plural, =0 {} other {# Actividades domesticas, }"}, - labelVars: {da: {key: "Domestic_activities", type: "length"}, - erea: {key: "Employment_related_a_Education_activities", type:"length"}}, - version: 9} - } + 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: "{ 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, }}"}, + labelVars: {da: {key: "Domestic_activities", type: "length"}, + erea: {key: "Employment_related_a_Education_activities", type:"length"}}, + version: 9} + } expect(config).toMatchObject(mockSurveys); }) @@ -66,17 +66,17 @@ it('resolves the timestamps', () => { //resolve label it('resolves the label', async () => { const xmlParser = new window.DOMParser(); - - //have a custom survey label function TODO: we currently don't have custome label functions, but should test when we do - - //no custom function, fallback to UseLabelTemplate - const xmlString = ' option_1/Domestic_activities> option_2 '; + const xmlString = ' option_1 '; const xmlDoc = xmlParser.parseFromString(xmlString, 'text/html'); + const xmlString2 = ' option_1 option_3 '; + const xmlDoc2 = xmlParser.parseFromString(xmlString2, 'text/xml'); - //if no template, returns "Answered" - // expect(await resolveLabel("TimeUseSurvey", xmlDoc)).toBe(""); - //if no labelVars, returns template - //else interpolates + //if no template, returns "Answered" TODO: find a way to engineer this case + //if no labelVars, returns template TODO: find a way to engineer this case + //have a custom survey label function TODO: we currently don't have custome label functions, but should test when we do + //no custom function, fallback to UseLabelTemplate (standard case) + expect(await resolveLabel("TimeUseSurvey", xmlDoc)).toBe("3 Domestic"); + expect(await resolveLabel("TimeUseSurvey", xmlDoc2)).toBe("3 Employment/Education, 3 Domestic"); }); /** diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index af578b2d9..133789a48 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -50,31 +50,32 @@ const LABEL_FUNCTIONS = { let configSurveys = await _lazyLoadConfig(); const config = configSurveys[name]; // config for this survey - const lang = i18next.resolvedLanguage; - const labelTemplate = config.labelTemplate?.[lang]; + const lang = i18next.resolvedLanguage; + const labelTemplate = config.labelTemplate?.[lang]; - if (!labelTemplate) return "Answered"; // no template given in config - if (!config.labelVars) return labelTemplate; // if no vars given, nothing to interpolate, - // so we return the unaltered template + if (!labelTemplate) return "Answered"; // no template given in config + if (!config.labelVars) return labelTemplate; // if no vars given, nothing to interpolate, + // so we return the unaltered template - // gather vars that will be interpolated into the template according to the survey config - const labelVars = {} - for (let lblVar in config.labelVars) { - const fieldName = config.labelVars[lblVar].key; - let fieldStr = _getAnswerByTagName(xmlDoc, fieldName); - if (fieldStr == '') fieldStr = null; - if (config.labelVars[lblVar].type == 'length') { - const fieldMatches = fieldStr?.split(' '); - labelVars[lblVar] = fieldMatches?.length || 0; - } else { - throw new Error(`labelVar type ${config.labelVars[lblVar].type } is not supported!`) - } - } + // gather vars that will be interpolated into the template according to the survey config + const labelVars = {} + for (let lblVar in config.labelVars) { + const fieldName = config.labelVars[lblVar].key; + let fieldStr = _getAnswerByTagName(xmlDoc, fieldName); + if (fieldStr == '') fieldStr = null; + if (config.labelVars[lblVar].type == 'length') { + const fieldMatches = fieldStr?.split(' '); + labelVars[lblVar] = fieldMatches?.length || 0; + } else { + throw new Error(`labelVar type ${config.labelVars[lblVar].type } is not supported!`) + } + } - // use MessageFormat interpolate the label template with the label vars - const mf = new MessageFormat(lang); - const label = mf.compile(labelTemplate)(labelVars); - return label.replace(/^[ ,]+|[ ,]+$/g, ''); // trim leading and trailing spaces and commas + // use MessageFormat interpolate the label template with the label vars + const mf = new MessageFormat(lang); + console.log(labelTemplate); + const label = mf.compile(labelTemplate)(labelVars); + return label.replace(/^[ ,]+|[ ,]+$/g, ''); // trim leading and trailing spaces and commas } } From 719403a231c50aeca6b694d03faf59bf16e77618 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 13 Oct 2023 14:39:52 -0600 Subject: [PATCH 017/112] rework getInstanceStr tests now that I understand how this function works, I got the xml (filled and unfilled) directly from console.log statements. The tests are now accurate, and cover each of the cases. --- www/__tests__/enketoHelper.test.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index 7a13303a8..55c0aceef 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -25,21 +25,17 @@ it('gets the survey config', async () => { }) it('gets the model response, if avaliable, or returns null', ()=> { - const xmlModel = '\n \n \n \n \n \n \n ;'; - const filled = '\n \n car\n \n \n \n \n ;'; + const xmlModel = ''; + const filled = '2016-07-2517:24:32.928-06:002016-07-2517:30:31.000-06:00'; const opts = {"prefilledSurveyResponse": filled}; - const opts2 = {"prefillFields": {"travel_mode" : "car"}}; + const opts2 = {"prefillFields": {"Start_date":"2016-07-25", "Start_time": "17:24:32.928-06:00", "End_date": "2016-07-25", "End_time": "17:30:31.000-06:00"}}; //if no xmlModel, returns null expect(getInstanceStr(null, opts)).toBe(null); - //if there is a prefilled survey, return it expect(getInstanceStr(xmlModel, opts)).toBe(filled); - //if there is a model and fields, return prefilled - // expect(getInstanceStr(xmlModel, opts2)).toBe(filled); - //TODO - figure out how to use the helper function with JEST -- getElementsByTagName is empty? should it be? - + expect(getInstanceStr(xmlModel, opts2)).toBe(filled); //if none of those things, also return null expect(getInstanceStr(xmlModel, {})).toBe(null); }); From 252e42f45a64a66322ba3363ca7df7c10ce418cf Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 13 Oct 2023 16:25:06 -0600 Subject: [PATCH 018/112] tests for saveResponse testing for saving the response, both when it works and when the timestamps are invalid, resulting in an error --- www/__tests__/enketoHelper.test.ts | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index 55c0aceef..576b77e18 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -1,4 +1,4 @@ -import { getInstanceStr, filterByNameAndVersion, resolveTimestamps, resolveLabel, _lazyLoadConfig, loadPreviousResponseForSurvey} from '../js/survey/enketo/enketoHelper'; +import { getInstanceStr, filterByNameAndVersion, resolveTimestamps, resolveLabel, _lazyLoadConfig, loadPreviousResponseForSurvey, saveResponse} from '../js/survey/enketo/enketoHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; @@ -8,6 +8,9 @@ window['i18next'] = initializedI18next; mockBEMUserCache(); mockLogger(); +global.URL = require('url').URL; +global.Blob = require('node:buffer').Blob; + it('gets the survey config', async () => { //this is aimed at testing my mock of the config //mocked getDocument for the case of getting the config @@ -84,7 +87,27 @@ it('resolves the label', async () => { */ // export function saveResponse(surveyName: string, enketoForm: Form, appConfig, opts: SurveyOptions) { it('gets the saved result or throws an error', () => { - + const surveyName = "TimeUseSurvey"; + const form = { getDataStr: () => { return '2023-10-13T15:05:48.890-06:002023-10-13T15:05:48.892-06:002016-07-2517:24:32.928-06:002016-07-2517:30:31.000-06:00personal_care_activitiesdoing_sportuuid:dc16c287-08b2-4435-95aa-e4d7838b4225'}}; + const badForm = { getDataStr: () => { return '2023-10-13T15:05:48.890-06:002023-10-13T15:05:48.892-06:002016-08-2517:24:32.928-06:002016-07-2517:30:31.000-06:00personal_care_activitiesdoing_sportuuid:dc16c287-08b2-4435-95aa-e4d7838b4225'}}; + const 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: "{ 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, }}"}, + labelVars: {da: {key: "Domestic_activities", type: "length"}, + erea: {key: "Employment_related_a_Education_activities", type:"length"}}, + version: 9} + } + } + }; + const opts = { timelineEntry: { end_local_dt: {timezone: "America/Los_Angeles"}, start_ts: 1469492672.928242, end_ts: 1469493031}}; + + console.log(config); + expect(saveResponse(surveyName, form, config, opts)).resolves.toMatchObject({label: "1 Personal Care", name: "TimeUseSurvey"}); + expect(saveResponse(surveyName, badForm, config, opts)).resolves.toMatchObject({message: "The times you entered are invalid. Please ensure that the start time is before the end time."}); }); /* From 9dc93844a9554d391434e7f39471ab5e4714195a Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 13 Oct 2023 16:25:19 -0600 Subject: [PATCH 019/112] take out old console.log --- www/js/survey/enketo/enketoHelper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 133789a48..3fa02f60c 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -73,7 +73,6 @@ const LABEL_FUNCTIONS = { // use MessageFormat interpolate the label template with the label vars const mf = new MessageFormat(lang); - console.log(labelTemplate); const label = mf.compile(labelTemplate)(labelVars); return label.replace(/^[ ,]+|[ ,]+$/g, ''); // trim leading and trailing spaces and commas } From 4d24d3e2a4a97492759c1f8c74da96c31fc3954d Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 13 Oct 2023 16:51:03 -0600 Subject: [PATCH 020/112] update types based on looking at these variables in breakpoints, these typings are more accurate --- www/js/survey/enketo/enketoHelper.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 3fa02f60c..147ffede8 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -35,7 +35,8 @@ type EnketoAnswer = { type EnketoSurveyConfig = { [surveyName:string]: { formPath: string - labelFields: string[]; + labelTemplate: {[lang: string] : string}; + labelVars: {[activity: string]: {[key: string]: string, type:string}}, version: number; compatibleWith: number; } From 1977258a45cab6738fbfd33ac05effa9a08602e4 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 16 Oct 2023 11:34:03 -0600 Subject: [PATCH 021/112] carry through async nature of the label functions --- www/js/survey/enketo/enketoHelper.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 147ffede8..773e3b7cb 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -131,13 +131,13 @@ export function _lazyLoadConfig() { * @param {XMLDocument} xmlDoc survey answer object * @returns {Promise} label string Promise */ -export function resolveLabel(name: string, xmlDoc: XMLDocument) { +export async function resolveLabel(name: string, xmlDoc: XMLDocument) { // Some studies may want a custom label function for their survey. // Those can be added in LABEL_FUNCTIONS with the survey name as the key. // Otherwise, UseLabelTemplate will create a label using the template in the config if (LABEL_FUNCTIONS[name]) - return LABEL_FUNCTIONS[name](xmlDoc); - return LABEL_FUNCTIONS.UseLabelTemplate(xmlDoc, name); + return await LABEL_FUNCTIONS[name](xmlDoc); + return await LABEL_FUNCTIONS.UseLabelTemplate(xmlDoc, name); } /** From a269a55ab8b94c3644a1e2788e71efe9801b4056 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 16 Oct 2023 11:34:47 -0600 Subject: [PATCH 022/112] correct precision of enketo dates the mismatch of precision and expected precision here is what was causing the added time entries to fail --- www/js/survey/enketo/enketoHelper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 773e3b7cb..0aab833b6 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -198,8 +198,8 @@ export function resolveTimestamps(xmlDoc, timelineEntry) { startTime = startTime.split(/\-|\+/)[0]; endTime = endTime.split(/\-|\+/)[0]; - let additionStartTs = DateTime.fromISO(startDate + "T" + startTime, {zone: timezone}).valueOf(); - let additionEndTs = DateTime.fromISO(endDate + "T" + endTime, {zone: timezone}).valueOf(); + let additionStartTs = DateTime.fromISO(startDate + "T" + startTime, {zone: timezone}).toSeconds(); + let additionEndTs = DateTime.fromISO(endDate + "T" + endTime, {zone: timezone}).toSeconds(); if (additionStartTs > additionEndTs) { return undefined; // if the start time is after the end time, this is an invalid response From e12ee3d326046ec4d356158d7f2579e2252f6c47 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 16 Oct 2023 15:24:38 -0600 Subject: [PATCH 023/112] resolve vscode errors there were errors because I was accessing with window.cordova instead of window['cordova'] --- www/js/control/ControlSyncHelper.tsx | 40 ++++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/www/js/control/ControlSyncHelper.tsx b/www/js/control/ControlSyncHelper.tsx index edc0e7470..7802caaeb 100644 --- a/www/js/control/ControlSyncHelper.tsx +++ b/www/js/control/ControlSyncHelper.tsx @@ -15,7 +15,7 @@ import { updateUser } from "../commHelper"; * BEGIN: Simple read/write wrappers */ export function forcePluginSync() { - return window.cordova.plugins.BEMServerSync.forceSync(); + return window['cordova'].plugins.BEMServerSync.forceSync(); }; const formatConfigForDisplay = (configToFormat) => { @@ -27,11 +27,11 @@ const formatConfigForDisplay = (configToFormat) => { } const setConfig = function(config) { - return window.cordova.plugins.BEMServerSync.setConfig(config); + return window['cordova'].plugins.BEMServerSync.setConfig(config); }; const getConfig = function() { - return window.cordova.plugins.BEMServerSync.getConfig(); + return window['cordova'].plugins.BEMServerSync.getConfig(); }; export async function getHelperSyncSettings() { @@ -40,10 +40,10 @@ export async function getHelperSyncSettings() { } const getEndTransitionKey = function() { - if(window.cordova.platformId == 'android') { + if(window['cordova'].platformId == 'android') { return "local.transition.stopped_moving"; } - else if(window.cordova.platformId == 'ios') { + else if(window['cordova'].platformId == 'ios') { return "T_TRIP_ENDED"; } } @@ -62,7 +62,7 @@ export const ForceSyncRow = ({getState}) => { async function forceSync() { try { - let addedEvent = addStatEvent(statKeys.BUTTON_FORCE_SYNC); + let addedEvent = await addStatEvent(statKeys.BUTTON_FORCE_SYNC); console.log("Added "+statKeys.BUTTON_FORCE_SYNC+" event"); let sync = await forcePluginSync(); @@ -72,7 +72,7 @@ export const ForceSyncRow = ({getState}) => { * See https://github.com/e-mission/e-mission-phone/issues/279 for details */ var sensorKey = "statemachine/transition"; - let sensorDataList = await window.cordova.plugins.BEMUserCache.getAllMessages(sensorKey, true); + let sensorDataList = await window['cordova'].plugins.BEMUserCache.getAllMessages(sensorKey, true); // If everything has been pushed, we should // have no more trip end transitions left @@ -98,28 +98,28 @@ export const ForceSyncRow = ({getState}) => { }; const getStartTransitionKey = function() { - if(window.cordova.platformId == 'android') { + if(window['cordova'].platformId == 'android') { return "local.transition.exited_geofence"; } - else if(window.cordova.platformId == 'ios') { + else if(window['cordova'].platformId == 'ios') { return "T_EXITED_GEOFENCE"; } } const getEndTransitionKey = function() { - if(window.cordova.platformId == 'android') { + if(window['cordova'].platformId == 'android') { return "local.transition.stopped_moving"; } - else if(window.cordova.platformId == 'ios') { + else if(window['cordova'].platformId == 'ios') { return "T_TRIP_ENDED"; } } const getOngoingTransitionState = function() { - if(window.cordova.platformId == 'android') { + if(window['cordova'].platformId == 'android') { return "local.state.ongoing_trip"; } - else if(window.cordova.platformId == 'ios') { + else if(window['cordova'].platformId == 'ios') { return "STATE_ONGOING_TRIP"; } } @@ -127,12 +127,12 @@ export const ForceSyncRow = ({getState}) => { async function getTransition(transKey) { var entry_data = {}; const curr_state = await getState(); - entry_data.curr_state = curr_state; + entry_data['curr_state'] = curr_state; if (transKey == getEndTransitionKey()) { - entry_data.curr_state = getOngoingTransitionState(); + entry_data['curr_state'] = getOngoingTransitionState(); } - entry_data.transition = transKey; - entry_data.ts = moment().unix(); + entry_data['transition'] = transKey; + entry_data['ts'] = moment().unix(); return entry_data; } @@ -141,9 +141,9 @@ export const ForceSyncRow = ({getState}) => { * result for start so that we ensure ordering */ var sensorKey = "statemachine/transition"; let entry_data = await getTransition(getStartTransitionKey()); - let messagePut = await window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); + let messagePut = await window['cordova'].plugins.BEMUserCache.putMessage(sensorKey, entry_data); entry_data = await getTransition(getEndTransitionKey()); - messagePut = await window.cordova.plugins.BEMUserCache.putMessage(sensorKey, entry_data); + messagePut = await window['cordova'].plugins.BEMUserCache.putMessage(sensorKey, entry_data); forceSync(); }; @@ -246,7 +246,7 @@ const ControlSyncHelper = ({ editVis, setEditVis }) => { * configure the UI */ let toggle; - if(window.cordova.platformId == 'ios'){ + if(window['cordova'].platformId == 'ios'){ toggle = Use Remote Push From ff30d3240e4d5d2b01891789628df1d5e4953cda Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 16 Oct 2023 15:39:05 -0600 Subject: [PATCH 024/112] update test after the change from miliseconds to seconds, the expected output here changed https://github.com/e-mission/e-mission-phone/pull/1063#issuecomment-1764950924 --- www/__tests__/enketoHelper.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index 576b77e18..2c0abd225 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -59,7 +59,7 @@ it('resolves the timestamps', () => { //good info returns unix start and end timestamps -- TODO : address precise vs less precise? const timeSurvey = ' 2016-07-25 2016-07-25 17:24:32.928-06:00 17:30:31.000-06:00 '; const xmlDoc = xmlParser.parseFromString(timeSurvey, 'text/xml'); - expect(resolveTimestamps(xmlDoc, timelineEntry)).toMatchObject({start_ts: 1469492672928, end_ts: 1469493031000}); + expect(resolveTimestamps(xmlDoc, timelineEntry)).toMatchObject({start_ts: 1469492672.928242, end_ts: 1469493031}); }); //resolve label From 6ef2725b21a5bd613752904b93af346fd543702a Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 17 Oct 2023 15:00:21 -0600 Subject: [PATCH 025/112] move the declaration of _config https://github.com/e-mission/e-mission-phone/pull/1063#discussion_r1362757509 --- www/js/survey/enketo/enketoHelper.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 0aab833b6..f9a6ddf7a 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -42,10 +42,6 @@ type EnketoSurveyConfig = { } } -/** @type {EnketoSurveyConfig} _config */ -//TODO find a more appropriate way to store this -let _config: EnketoSurveyConfig; - const LABEL_FUNCTIONS = { UseLabelTemplate : async (xmlDoc: XMLDocument, name: string) => { let configSurveys = await _lazyLoadConfig(); @@ -92,6 +88,9 @@ const LABEL_FUNCTIONS = { return val; } +/** @type {EnketoSurveyConfig} _config */ +let _config: EnketoSurveyConfig; + /** * _lazyLoadConfig load enketo survey config. If already loaded, return the cached config * @returns {Promise} enketo survey config From b48027958eb25a1e2f79f75dec53abd3f93a2ab8 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Tue, 17 Oct 2023 15:13:41 -0600 Subject: [PATCH 026/112] move the fake config, add on Moving the fake config into it's own file so that we can easily add onto it. I kept the survey portion that I set up for the enketoHelper tests, and added the rest of the sections found in a typical config. https://github.com/e-mission/e-mission-phone/pull/1063#discussion_r1362749725 --- www/__mocks__/cordovaMocks.ts | 15 +----- www/__mocks__/fakeConfig.json | 94 +++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 www/__mocks__/fakeConfig.json diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index bae65ad88..db22627f5 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -1,4 +1,5 @@ import packageJsonBuild from '../../package.cordovabuild.json'; +import fakeConfig from "./fakeConfig.json"; export const mockCordova = () => { window['cordova'] ||= {}; @@ -90,19 +91,7 @@ export const mockBEMUserCache = () => { }, getDocument: (key: string, withMetadata?: boolean) => { // this was mocked specifically for enketoHelper's use, could be expanded if needed - const fakeSurveyConfig = { - 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: "{ 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, }}"}, - labelVars: {da: {key: "Domestic_activities", type: "length"}, - erea: {key: "Employment_related_a_Education_activities", type:"length"}}, - version: 9} - } - } - } + const fakeSurveyConfig = fakeConfig; if(key == "config/app_ui_config"){ return new Promise((rs, rj) => diff --git a/www/__mocks__/fakeConfig.json b/www/__mocks__/fakeConfig.json new file mode 100644 index 000000000..40471718d --- /dev/null +++ b/www/__mocks__/fakeConfig.json @@ -0,0 +1,94 @@ +{ + "version": 1, + "ts": 1655143472, + "server": { + "connectUrl": "https://openpath-test.nrel.gov/api/", + "aggregate_call_auth": "user_only" + }, + "intro": { + "program_or_study": "study", + "start_month": "10", + "start_year": "2023", + "program_admin_contact": "K. Shankari", + "deployment_partner_name": "NREL", + "translated_text": { + "en": { + "deployment_partner_name": "NREL", + "deployment_name": "Testing environment for Jest testing", + "summary_line_1": "", + "summary_line_2": "", + "summary_line_3": "", + "short_textual_description": "", + "why_we_collect": "", + "research_questions": [ + "", + "" + ] + }, + "es": { + "deployment_partner_name": "NREL", + "deployment_name": "Ambiente prueba para las pruebas de Jest", + "summary_line_1": "", + "summary_line_2": "", + "summary_line_3": "", + "short_textual_description": "", + "why_we_collect": "", + "research_questions": [ + "", + "" + ] + } + } + }, + "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": "{ 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, }}" + }, + "labelVars": { + "da": { + "key": "Domestic_activities", + "type": "length" + }, + "erea": { + "key": "Employment_related_a_Education_activities", + "type": "length" + } + }, + "version": 9 + } + }, + "trip-labels": "ENKETO" + }, + "display_config": { + "use_imperial": false + }, + "profile_controls": { + "support_upload": true, + "trip_end_notification": false + }, + "admin_dashboard": { + "overview_users": true, + "overview_active_users": true, + "overview_trips": true, + "overview_signup_trends": true, + "overview_trips_trend": true, + "data_uuids": true, + "data_trips": true, + "data_trips_columns_exclude": [], + "additional_trip_columns": [], + "data_uuids_columns_exclude": [], + "token_generate": true, + "token_prefix": "nrelop", + "map_heatmap": true, + "map_bubble": true, + "map_trip_lines": true, + "push_send": true, + "options_uuids": true, + "options_emails": true + } +} \ No newline at end of file From f2976914e0dea5cb72766ec3154b7b8e792b0542 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan Date: Tue, 7 Nov 2023 13:16:35 -0700 Subject: [PATCH 027/112] changes related to emailService --- www/index.js | 2 +- www/js/control/LogPage.tsx | 2 +- www/js/control/SensedPage.tsx | 3 +- www/js/control/emailService.js | 114 --------------------------------- www/js/ngApp.js | 1 - 5 files changed, 4 insertions(+), 118 deletions(-) delete mode 100644 www/js/control/emailService.js diff --git a/www/index.js b/www/index.js index 78d29cf7a..d6a0a6e7a 100644 --- a/www/index.js +++ b/www/index.js @@ -21,7 +21,7 @@ import './js/diary/services.js'; import './js/survey/enketo/answer.js'; import './js/survey/enketo/enketo-trip-button.js'; import './js/survey/enketo/enketo-add-note-button.js'; -import './js/control/emailService.js'; +// import './js/control/emailService.js'; import './js/metrics-factory.js'; import './js/metrics-mappings.js'; import './js/plugin/logger.ts'; diff --git a/www/js/control/LogPage.tsx b/www/js/control/LogPage.tsx index ad369fbff..3fcce72ac 100644 --- a/www/js/control/LogPage.tsx +++ b/www/js/control/LogPage.tsx @@ -12,7 +12,7 @@ type loadStats = { currentStart: number; gotMaxIndex: boolean; reachedEnd: boole const LogPage = ({ pageVis, setPageVis }) => { const { t } = useTranslation(); const { colors } = useTheme(); - const EmailHelper = getAngularService('EmailHelper'); + //const EmailHelper = getAngularService('EmailHelper'); const [loadStats, setLoadStats] = useState(); const [entries, setEntries] = useState([]); diff --git a/www/js/control/SensedPage.tsx b/www/js/control/SensedPage.tsx index 82fa60581..4d51b5308 100644 --- a/www/js/control/SensedPage.tsx +++ b/www/js/control/SensedPage.tsx @@ -5,11 +5,12 @@ import { getAngularService } from '../angular-react-helper'; import { useTranslation } from 'react-i18next'; import { FlashList } from '@shopify/flash-list'; import moment from 'moment'; +import EmailHelper from './emailService'; const SensedPage = ({ pageVis, setPageVis }) => { const { t } = useTranslation(); const { colors } = useTheme(); - const EmailHelper = getAngularService('EmailHelper'); + //const EmailHelper = getAngularService('EmailHelper'); /* Let's keep a reference to the database for convenience */ const [DB, setDB] = useState(); diff --git a/www/js/control/emailService.js b/www/js/control/emailService.js deleted file mode 100644 index 8eeaf39bb..000000000 --- a/www/js/control/emailService.js +++ /dev/null @@ -1,114 +0,0 @@ -'use strict'; - -import angular from 'angular'; - -angular - .module('emission.services.email', ['emission.plugin.logger']) - - .service('EmailHelper', function ($window, $http, Logger) { - const getEmailConfig = function () { - return new Promise(function (resolve, reject) { - window.Logger.log(window.Logger.LEVEL_INFO, 'About to get email config'); - var address = []; - $http - .get('json/emailConfig.json') - .then(function (emailConfig) { - window.Logger.log( - window.Logger.LEVEL_DEBUG, - 'emailConfigString = ' + JSON.stringify(emailConfig.data), - ); - address.push(emailConfig.data.address); - resolve(address); - }) - .catch(function (err) { - $http - .get('json/emailConfig.json.sample') - .then(function (emailConfig) { - window.Logger.log( - window.Logger.LEVEL_DEBUG, - 'default emailConfigString = ' + JSON.stringify(emailConfig.data), - ); - address.push(emailConfig.data.address); - resolve(address); - }) - .catch(function (err) { - window.Logger.log( - window.Logger.LEVEL_ERROR, - 'Error while reading default email config' + err, - ); - reject(err); - }); - }); - }); - }; - - const hasAccount = function () { - return new Promise(function (resolve, reject) { - $window.cordova.plugins.email.hasAccount(function (hasAct) { - resolve(hasAct); - }); - }); - }; - - this.sendEmail = function (database) { - Promise.all([getEmailConfig(), hasAccount()]).then(function ([address, hasAct]) { - var parentDir = 'unknown'; - - // Check this only for ios, since for android, the check always fails unless - // the user grants the "GET_ACCOUNTS" dynamic permission - // without the permission, we only see the e-mission account which is not valid - // - // https://developer.android.com/reference/android/accounts/AccountManager#getAccounts() - // - // Caller targeting API level below Build.VERSION_CODES.O that - // have not been granted the Manifest.permission.GET_ACCOUNTS - // permission, will only see those accounts managed by - // AbstractAccountAuthenticators whose signature matches the - // client. - // and on android, if the account is not configured, the gmail app will be launched anyway - // on iOS, nothing will happen. So we perform the check only on iOS so that we can - // generate a reasonably relevant error message - - if (ionic.Platform.isIOS() && !hasAct) { - alert(i18next.t('email-service.email-account-not-configured')); - return; - } - - if (ionic.Platform.isAndroid()) { - parentDir = 'app://databases'; - } - if (ionic.Platform.isIOS()) { - alert(i18next.t('email-service.email-account-mail-app')); - parentDir = cordova.file.dataDirectory + '../LocalDatabase'; - } - - if (parentDir == 'unknown') { - alert('parentDir unexpectedly = ' + parentDir + '!'); - } - - window.Logger.log(window.Logger.LEVEL_INFO, 'Going to email ' + database); - parentDir = parentDir + '/' + database; - /* - window.Logger.log(window.Logger.LEVEL_INFO, - "Going to export logs to "+parentDir); - */ - alert(i18next.t('email-service.going-to-email', { parentDir: parentDir })); - var email = { - to: address, - attachments: [parentDir], - subject: i18next.t('email-service.email-log.subject-logs'), - body: i18next.t('email-service.email-log.body-please-fill-in-what-is-wrong'), - }; - - $window.cordova.plugins.email.open(email, function () { - Logger.log( - 'email app closed while sending, ' + - JSON.stringify(email) + - ' not sure if we should do anything', - ); - // alert(i18next.t('email-service.no-email-address-configured') + err); - return; - }); - }); - }; - }); diff --git a/www/js/ngApp.js b/www/js/ngApp.js index 228c2a989..84b9972c4 100644 --- a/www/js/ngApp.js +++ b/www/js/ngApp.js @@ -40,7 +40,6 @@ angular 'emission.services', 'emission.plugin.logger', 'emission.splash.referral', - 'emission.services.email', 'emission.main', 'pascalprecht.translate', 'LocalStorageModule', From 159d665295f67ad3f0d59df5bc94967e72c83fe8 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 8 Nov 2023 13:29:37 -0500 Subject: [PATCH 028/112] support client-side transformation for enketo XMLs With this change, the `formPath` specifying the URL of an Enketo survey can be either JSON or XML. If it's JSON, we'll be able parse and use it directly. If it cannot be parsed as JSON, we'll perform XML -> JSON transformation with enketo-transformer/web. --- package.cordovabuild.json | 1 + package.serve.json | 1 + webpack.config.js | 7 ++++++- www/js/survey/enketo/EnketoModal.tsx | 16 +++++----------- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/package.cordovabuild.json b/package.cordovabuild.json index c61fa72c5..2e78f7363 100644 --- a/package.cordovabuild.json +++ b/package.cordovabuild.json @@ -142,6 +142,7 @@ "cordova-plugin-x-socialsharing": "6.0.4", "core-js": "^2.5.7", "enketo-core": "^6.1.7", + "enketo-transformer": "^4.0.0", "fast-xml-parser": "^4.2.2", "fs-extra": "^9.0.1", "i18next": "^22.5.0", diff --git a/package.serve.json b/package.serve.json index 59a803085..c66c32b1e 100644 --- a/package.serve.json +++ b/package.serve.json @@ -73,6 +73,7 @@ "chartjs-plugin-annotation": "^3.0.1", "core-js": "^2.5.7", "enketo-core": "^6.1.7", + "enketo-transformer": "^4.0.0", "fast-xml-parser": "^4.2.2", "fs-extra": "^9.0.1", "i18next": "^22.5.0", diff --git a/webpack.config.js b/webpack.config.js index 1e504ac5f..3e7e6d368 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -79,7 +79,12 @@ module.exports = { /* Enketo expects its per-app configuration to be available as 'enketo-config', so we have to alias it here. https://github.com/enketo/enketo-core#global-configuration */ - 'enketo/config': path.resolve(__dirname, 'www/js/config/enketo-config') + 'enketo/config': path.resolve(__dirname, 'www/js/config/enketo-config'), + /* enketo-transformer has 'libxslt' as an optional peer dependency. + We don't need it since we are only doing client-side transformations via + enketo-transformer/web (https://github.com/enketo/enketo-transformer#web). + So, we can tell webpack it's ok to ignore libxslt by aliasing it to false. */ + 'libxslt': false, }, extensions: ['.web.js', '.jsx', '.tsx', '.ts', '.js'], }, diff --git a/www/js/survey/enketo/EnketoModal.tsx b/www/js/survey/enketo/EnketoModal.tsx index de1f505f3..a0dc667c5 100644 --- a/www/js/survey/enketo/EnketoModal.tsx +++ b/www/js/survey/enketo/EnketoModal.tsx @@ -6,8 +6,8 @@ import useAppConfig from '../../useAppConfig'; import { useTranslation } from 'react-i18next'; import { SurveyOptions, getInstanceStr, saveResponse } from './enketoHelper'; import { fetchUrlCached } from '../../commHelper'; -import { displayError, displayErrorMsg } from '../../plugin/logger'; -// import { transform } from 'enketo-transformer/web'; +import { displayError, displayErrorMsg, logDebug } from '../../plugin/logger'; +import { transform } from 'enketo-transformer/web'; type Props = Omit & { surveyName: string; @@ -26,15 +26,9 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest }: Props) => { const responseText = await fetchUrlCached(url); try { return JSON.parse(responseText); - } catch ({ name, message }) { - // not JSON, so it must be XML - return Promise.reject( - 'downloaded survey was not JSON; enketo-transformer is not available yet', - ); - /* uncomment once enketo-transformer is available */ - // if `response` is not JSON, it is an XML string and needs transformation to JSON - // const xmlText = await res.text(); - // return await transform({xform: xmlText}); + } catch (e) { + logDebug(`${e.name}: Survey was not in JSON format. Attempting to transform XML -> JSON...`); + return await transform({ xform: responseText }); } } From 2b72850ce16de27531185bb8e3f0aa39a613ce39 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 8 Nov 2023 14:41:32 -0500 Subject: [PATCH 029/112] rename + move fetchSurvey to enketoHelper.ts --- www/js/survey/enketo/EnketoModal.tsx | 16 ++-------------- www/js/survey/enketo/enketoHelper.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/www/js/survey/enketo/EnketoModal.tsx b/www/js/survey/enketo/EnketoModal.tsx index a0dc667c5..9267b9808 100644 --- a/www/js/survey/enketo/EnketoModal.tsx +++ b/www/js/survey/enketo/EnketoModal.tsx @@ -4,10 +4,8 @@ import { StyleSheet, Modal, ScrollView, SafeAreaView, Pressable } from 'react-na import { ModalProps } from 'react-native-paper'; import useAppConfig from '../../useAppConfig'; import { useTranslation } from 'react-i18next'; -import { SurveyOptions, getInstanceStr, saveResponse } from './enketoHelper'; -import { fetchUrlCached } from '../../commHelper'; +import { SurveyOptions, fetchSurvey, getInstanceStr, saveResponse } from './enketoHelper'; import { displayError, displayErrorMsg, logDebug } from '../../plugin/logger'; -import { transform } from 'enketo-transformer/web'; type Props = Omit & { surveyName: string; @@ -22,16 +20,6 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest }: Props) => { const enketoForm = useRef
(null); const appConfig = useAppConfig(); - async function fetchSurveyJson(url) { - const responseText = await fetchUrlCached(url); - try { - return JSON.parse(responseText); - } catch (e) { - logDebug(`${e.name}: Survey was not in JSON format. Attempting to transform XML -> JSON...`); - return await transform({ xform: responseText }); - } - } - async function validateAndSave() { const valid = await enketoForm.current.validate(); if (!valid) return false; @@ -56,7 +44,7 @@ const EnketoModal = ({ surveyName, onResponseSaved, opts, ...rest }: Props) => { const formPath = appConfig.survey_info?.surveys?.[surveyName]?.formPath; if (!formPath) return console.error('No form path found for survey', surveyName); - fetchSurveyJson(formPath).then(({ form, model }) => { + fetchSurvey(formPath).then(({ form, model }) => { surveyJson.current = { form, model }; headerEl?.current.insertAdjacentHTML('afterend', form); // inject form into DOM const formEl = document.querySelector('form.or'); diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 9b96c8463..e4a1fcf45 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -1,11 +1,13 @@ import { getAngularService } from '../../angular-react-helper'; import { Form } from 'enketo-core'; +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 { getConfig } from '../../config/dynamicConfig'; import { DateTime } from 'luxon'; +import { fetchUrlCached } from '../../commHelper'; export type PrefillFields = { [key: string]: string }; @@ -287,3 +289,13 @@ export function loadPreviousResponseForSurvey(dataKey: string) { _getMostRecent(answers), ); } + +export async function fetchSurvey(url: string) { + const responseText = await fetchUrlCached(url); + try { + return JSON.parse(responseText); + } catch (e) { + logDebug(`${e.name}: Survey was not in JSON format. Attempting to transform XML -> JSON...`); + return await transform({ xform: responseText }); + } +} From de49924a7d42febde2109e999eb4d38ebbef7e25 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 8 Nov 2023 14:24:44 -0700 Subject: [PATCH 030/112] convert metrics services angular -> ts migration for metrics-mappings and metrics-factory The datasets have their own files, there is a customhelper, a methelper, and a footprint helper Tested in the emulator and dashboard still behaves as expected --- www/index.js | 2 - www/js/control/ProfileSettings.jsx | 16 +- www/js/metrics-factory.js | 271 ---------------- www/js/metrics-mappings.js | 425 ------------------------- www/js/metrics/CarbonDatasets.ts | 123 +++++++ www/js/metrics/CarbonFootprintCard.tsx | 33 +- www/js/metrics/CarbonTextCard.tsx | 29 +- www/js/metrics/CustomMetricsHelper.ts | 148 +++++++++ www/js/metrics/METDataset.ts | 128 ++++++++ www/js/metrics/footprintHelper.ts | 161 ++++++++++ www/js/metrics/metHelper.ts | 109 +++++++ 11 files changed, 704 insertions(+), 741 deletions(-) delete mode 100644 www/js/metrics-factory.js delete mode 100644 www/js/metrics-mappings.js create mode 100644 www/js/metrics/CarbonDatasets.ts create mode 100644 www/js/metrics/CustomMetricsHelper.ts create mode 100644 www/js/metrics/METDataset.ts create mode 100644 www/js/metrics/footprintHelper.ts create mode 100644 www/js/metrics/metHelper.ts diff --git a/www/index.js b/www/index.js index 78d29cf7a..06802d14f 100644 --- a/www/index.js +++ b/www/index.js @@ -22,6 +22,4 @@ import './js/survey/enketo/answer.js'; import './js/survey/enketo/enketo-trip-button.js'; import './js/survey/enketo/enketo-add-note-button.js'; import './js/control/emailService.js'; -import './js/metrics-factory.js'; -import './js/metrics-mappings.js'; import './js/plugin/logger.ts'; diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index b081e642a..b8943a81c 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -26,6 +26,11 @@ import ControlCollectionHelper, { helperToggleLowAccuracy, forceTransition, } from './ControlCollectionHelper'; +import { + getCarbonDatasetOptions, + getCurrentCarbonDatasetCode, + saveCurrentCarbonDatasetLocale, +} from '../metrics/customMetricsHelper'; import { resetDataAndRefresh } from '../config/dynamicConfig'; import { AppContext } from '../App'; import { shareQR } from '../components/QrCode'; @@ -43,7 +48,6 @@ const ProfileSettings = () => { const { setPermissionsPopupVis } = useContext(AppContext); //angular services needed - const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); const EmailHelper = getAngularService('EmailHelper'); const NotificationScheduler = getAngularService('NotificationScheduler'); const ControlHelper = getAngularService('ControlHelper'); @@ -84,8 +88,8 @@ const ProfileSettings = () => { const appVersion = useRef(); let carbonDatasetString = - t('general-settings.carbon-dataset') + ': ' + CarbonDatasetHelper.getCurrentCarbonDatasetCode(); - const carbonOptions = CarbonDatasetHelper.getCarbonDatasetOptions(); + t('general-settings.carbon-dataset') + ': ' + getCurrentCarbonDatasetCode(); + const carbonOptions = getCarbonDatasetOptions(); const stateActions = [ { text: 'Initialize', transition: 'INITIALIZE' }, { text: 'Start trip', transition: 'EXITED_GEOFENCE' }, @@ -361,12 +365,10 @@ const ProfileSettings = () => { const onSelectCarbon = function (carbonObject) { console.log('changeCarbonDataset(): chose locale ' + carbonObject.value); - CarbonDatasetHelper.saveCurrentCarbonDatasetLocale(carbonObject.value); //there's some sort of error here + saveCurrentCarbonDatasetLocale(carbonObject.value); //there's some sort of error here //Unhandled Promise Rejection: While logging, error -[NSNull UTF8String]: unrecognized selector sent to instance 0x7fff8a625fb0 carbonDatasetString = - i18next.t('general-settings.carbon-dataset') + - ': ' + - CarbonDatasetHelper.getCurrentCarbonDatasetCode(); + i18next.t('general-settings.carbon-dataset') + ': ' + getCurrentCarbonDatasetCode(); }; //conditional creation of setting sections diff --git a/www/js/metrics-factory.js b/www/js/metrics-factory.js deleted file mode 100644 index 28ef5ae9e..000000000 --- a/www/js/metrics-factory.js +++ /dev/null @@ -1,271 +0,0 @@ -'use strict'; - -import angular from 'angular'; -import { getBaseModeByValue } from './diary/diaryHelper'; -import { labelOptions } from './survey/multilabel/confirmHelper'; -import { storageGet, storageRemove, storageSet } from './plugin/storage'; - -angular - .module('emission.main.metrics.factory', ['emission.main.metrics.mappings']) - - .factory('FootprintHelper', function (CarbonDatasetHelper, CustomDatasetHelper) { - var fh = {}; - var highestFootprint = 0; - - var mtokm = function (v) { - return v / 1000; - }; - fh.useCustom = false; - - fh.setUseCustomFootprint = function () { - fh.useCustom = true; - }; - - fh.getFootprint = function () { - if (this.useCustom == true) { - return CustomDatasetHelper.getCustomFootprint(); - } else { - return CarbonDatasetHelper.getCurrentCarbonDatasetFootprint(); - } - }; - - fh.readableFormat = function (v) { - return v > 999 ? Math.round(v / 1000) + 'k kg CO₂' : Math.round(v) + ' kg CO₂'; - }; - fh.getFootprintForMetrics = function (userMetrics, defaultIfMissing = 0) { - var footprint = fh.getFootprint(); - var result = 0; - for (var i in userMetrics) { - var mode = userMetrics[i].key; - if (mode == 'ON_FOOT') { - mode = 'WALKING'; - } - - if (mode in footprint) { - result += footprint[mode] * mtokm(userMetrics[i].values); - } else if (mode == 'IN_VEHICLE') { - result += - ((footprint['CAR'] + - footprint['BUS'] + - footprint['LIGHT_RAIL'] + - footprint['TRAIN'] + - footprint['TRAM'] + - footprint['SUBWAY']) / - 6) * - mtokm(userMetrics[i].values); - } else { - console.warn( - 'WARNING FootprintHelper.getFootprintFromMetrics() was requested for an unknown mode: ' + - mode + - ' metrics JSON: ' + - JSON.stringify(userMetrics), - ); - result += defaultIfMissing * mtokm(userMetrics[i].values); - } - } - return result; - }; - fh.getLowestFootprintForDistance = function (distance) { - var footprint = fh.getFootprint(); - var lowestFootprint = Number.MAX_SAFE_INTEGER; - for (var mode in footprint) { - if (mode == 'WALKING' || mode == 'BICYCLING') { - // these modes aren't considered when determining the lowest carbon footprint - } else { - lowestFootprint = Math.min(lowestFootprint, footprint[mode]); - } - } - return lowestFootprint * mtokm(distance); - }; - - fh.getHighestFootprint = function () { - if (!highestFootprint) { - var footprint = fh.getFootprint(); - let footprintList = []; - for (var mode in footprint) { - footprintList.push(footprint[mode]); - } - highestFootprint = Math.max(...footprintList); - } - return highestFootprint; - }; - - fh.getHighestFootprintForDistance = function (distance) { - return fh.getHighestFootprint() * mtokm(distance); - }; - - var getLowestMotorizedNonAirFootprint = function (footprint, rlmCO2) { - var lowestFootprint = Number.MAX_SAFE_INTEGER; - for (var mode in footprint) { - if (mode == 'AIR_OR_HSR' || mode == 'air') { - console.log('Air mode, ignoring'); - } else { - if (footprint[mode] == 0 || footprint[mode] <= rlmCO2) { - console.log( - 'Non motorized mode or footprint <= range_limited_motorized', - mode, - footprint[mode], - rlmCO2, - ); - } else { - lowestFootprint = Math.min(lowestFootprint, footprint[mode]); - } - } - } - return lowestFootprint; - }; - - fh.getOptimalDistanceRanges = function () { - const FIVE_KM = 5 * 1000; - const SIX_HUNDRED_KM = 600 * 1000; - if (!fh.useCustom) { - const defaultFootprint = CarbonDatasetHelper.getCurrentCarbonDatasetFootprint(); - const lowestMotorizedNonAir = getLowestMotorizedNonAirFootprint(defaultFootprint); - const airFootprint = defaultFootprint['AIR_OR_HSR']; - return [ - { low: 0, high: FIVE_KM, optimal: 0 }, - { low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir }, - { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, - ]; - } else { - // custom footprint, let's get the custom values - const customFootprint = CustomDatasetHelper.getCustomFootprint(); - let airFootprint = customFootprint['air']; - if (!airFootprint) { - // 2341 BTU/PMT from - // https://tedb.ornl.gov/wp-content/uploads/2021/02/TEDB_Ed_39.pdf#page=68 - // 159.25 lb per million BTU from EIA - // https://www.eia.gov/environment/emissions/co2_vol_mass.php - // (2341 * (159.25/1000000))/(1.6*2.2) = 0.09975, rounded up a bit - console.log('No entry for air in ', customFootprint, ' using default'); - airFootprint = 0.1; - } - const rlm = CustomDatasetHelper.range_limited_motorized; - if (!rlm) { - return [ - { low: 0, high: FIVE_KM, optimal: 0 }, - { low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir }, - { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, - ]; - } else { - console.log('Found range_limited_motorized mode', rlm); - const lowestMotorizedNonAir = getLowestMotorizedNonAirFootprint( - customFootprint, - rlm.kgCo2PerKm, - ); - return [ - { low: 0, high: FIVE_KM, optimal: 0 }, - { low: FIVE_KM, high: rlm.range_limit_km * 1000, optimal: rlm.kgCo2PerKm }, - { - low: rlm.range_limit_km * 1000, - high: SIX_HUNDRED_KM, - optimal: lowestMotorizedNonAir, - }, - { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, - ]; - } - } - }; - - return fh; - }) - - .factory('CalorieCal', function (METDatasetHelper, CustomDatasetHelper) { - var cc = {}; - var highestMET = 0; - var USER_DATA_KEY = 'user-data'; - cc.useCustom = false; - - cc.setUseCustomFootprint = function () { - cc.useCustom = true; - }; - - cc.getMETs = function () { - if (this.useCustom == true) { - return CustomDatasetHelper.getCustomMETs(); - } else { - return METDatasetHelper.getStandardMETs(); - } - }; - - cc.set = function (info) { - return storageSet(USER_DATA_KEY, info); - }; - cc.get = function () { - return storageGet(USER_DATA_KEY); - }; - cc.delete = function () { - return storageRemove(USER_DATA_KEY); - }; - Number.prototype.between = function (min, max) { - return this >= min && this <= max; - }; - cc.getHighestMET = function () { - if (!highestMET) { - var met = cc.getMETs(); - let metList = []; - for (var mode in met) { - var rangeList = met[mode]; - for (var range in rangeList) { - metList.push(rangeList[range].mets); - } - } - highestMET = Math.max(...metList); - } - return highestMET; - }; - cc.getMet = function (mode, speed, defaultIfMissing) { - if (mode == 'ON_FOOT') { - console.log("CalorieCal.getMet() converted 'ON_FOOT' to 'WALKING'"); - mode = 'WALKING'; - } - let currentMETs = cc.getMETs(); - if (!currentMETs[mode]) { - console.warn('CalorieCal.getMet() Illegal mode: ' + mode); - return defaultIfMissing; //So the calorie sum does not break with wrong return type - } - for (var i in currentMETs[mode]) { - if (mpstomph(speed).between(currentMETs[mode][i].range[0], currentMETs[mode][i].range[1])) { - return currentMETs[mode][i].mets; - } else if (mpstomph(speed) < 0) { - console.log('CalorieCal.getMet() Negative speed: ' + mpstomph(speed)); - return 0; - } - } - }; - var mpstomph = function (mps) { - return 2.23694 * mps; - }; - var lbtokg = function (lb) { - return lb * 0.453592; - }; - var fttocm = function (ft) { - return ft * 30.48; - }; - cc.getCorrectedMet = function (met, gender, age, height, heightUnit, weight, weightUnit) { - var height = heightUnit == 0 ? fttocm(height) : height; - var weight = weightUnit == 0 ? lbtokg(weight) : weight; - if (gender == 1) { - //male - var met = - (met * 3.5) / - (((66.473 + 5.0033 * height + 13.7516 * weight - 6.755 * age) / 1440 / 5 / weight) * - 1000); - return met; - } else if (gender == 0) { - //female - var met = - (met * 3.5) / - (((655.0955 + 1.8496 * height + 9.5634 * weight - 4.6756 * age) / 1440 / 5 / weight) * - 1000); - return met; - } - }; - cc.getuserCalories = function (durationInMin, met) { - return 65 * durationInMin * met; - }; - cc.getCalories = function (weightInKg, durationInMin, met) { - return weightInKg * durationInMin * met; - }; - return cc; - }); diff --git a/www/js/metrics-mappings.js b/www/js/metrics-mappings.js deleted file mode 100644 index 38836a3a1..000000000 --- a/www/js/metrics-mappings.js +++ /dev/null @@ -1,425 +0,0 @@ -import angular from 'angular'; -import { getLabelOptions } from './survey/multilabel/confirmHelper'; -import { getConfig } from './config/dynamicConfig'; -import { storageGet, storageSet } from './plugin/storage'; - -angular - .module('emission.main.metrics.mappings', ['emission.plugin.logger']) - - .service('CarbonDatasetHelper', function () { - var CARBON_DATASET_KEY = 'carbon_dataset_locale'; - - // Values are in Kg/PKm (kilograms per passenger-kilometer) - // Sources for EU values: - // - Tremod: 2017, CO2, CH4 and N2O in CO2-equivalent - // - HBEFA: 2020, CO2 (per country) - // German data uses Tremod. Other EU countries (and Switzerland) use HBEFA for car and bus, - // and Tremod for train and air (because HBEFA doesn't provide these). - // EU data is an average of the Tremod/HBEFA data for the countries listed; - // for this average the HBEFA data was used also in the German set (for car and bus). - var carbonDatasets = { - US: { - regionName: 'United States', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 267 / 1609, - BUS: 278 / 1609, - LIGHT_RAIL: 120 / 1609, - SUBWAY: 74 / 1609, - TRAM: 90 / 1609, - TRAIN: 92 / 1609, - AIR_OR_HSR: 217 / 1609, - }, - }, - EU: { - // Plain average of values for the countries below (using HBEFA for car and bus, Tremod for others) - regionName: 'European Union', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.14515, - BUS: 0.04751, - LIGHT_RAIL: 0.064, - SUBWAY: 0.064, - TRAM: 0.064, - TRAIN: 0.048, - AIR_OR_HSR: 0.201, - }, - }, - DE: { - regionName: 'Germany', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.139, // Tremod (passenger car) - BUS: 0.0535, // Tremod (average city/coach) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - FR: { - regionName: 'France', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13125, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04838, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - AT: { - regionName: 'Austria', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.14351, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04625, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - SE: { - regionName: 'Sweden', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13458, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04557, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - NO: { - regionName: 'Norway', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13265, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04185, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - CH: { - regionName: 'Switzerland', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.17638, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04866, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - }; - - var defaultCarbonDatasetCode = 'US'; - var currentCarbonDatasetCode = defaultCarbonDatasetCode; - - // we need to call the method from within a promise in initialize() - // and using this.setCurrentCarbonDatasetLocale doesn't seem to work - var setCurrentCarbonDatasetLocale = function (localeCode) { - for (var code in carbonDatasets) { - if (code == localeCode) { - currentCarbonDatasetCode = localeCode; - break; - } - } - }; - - this.loadCarbonDatasetLocale = function () { - return storageGet(CARBON_DATASET_KEY).then(function (localeCode) { - Logger.log( - 'CarbonDatasetHelper.loadCarbonDatasetLocale() obtained value from storage [' + - localeCode + - ']', - ); - if (!localeCode) { - localeCode = defaultCarbonDatasetCode; - Logger.log( - 'CarbonDatasetHelper.loadCarbonDatasetLocale() no value in storage, using [' + - localeCode + - '] instead', - ); - } - setCurrentCarbonDatasetLocale(localeCode); - }); - }; - - this.saveCurrentCarbonDatasetLocale = function (localeCode) { - setCurrentCarbonDatasetLocale(localeCode); - storageSet(CARBON_DATASET_KEY, currentCarbonDatasetCode); - Logger.log( - 'CarbonDatasetHelper.saveCurrentCarbonDatasetLocale() saved value [' + - currentCarbonDatasetCode + - '] to storage', - ); - }; - - this.getCarbonDatasetOptions = function () { - var options = []; - for (var code in carbonDatasets) { - options.push({ - text: code, //carbonDatasets[code].regionName, - value: code, - }); - } - return options; - }; - - this.getCurrentCarbonDatasetCode = function () { - return currentCarbonDatasetCode; - }; - - this.getCurrentCarbonDatasetFootprint = function () { - return carbonDatasets[currentCarbonDatasetCode].footprintData; - }; - }) - .service('METDatasetHelper', function () { - var standardMETs = { - WALKING: { - VERY_SLOW: { - range: [0, 2.0], - mets: 2.0, - }, - SLOW: { - range: [2.0, 2.5], - mets: 2.8, - }, - MODERATE_0: { - range: [2.5, 2.8], - mets: 3.0, - }, - MODERATE_1: { - range: [2.8, 3.2], - mets: 3.5, - }, - FAST: { - range: [3.2, 3.5], - mets: 4.3, - }, - VERY_FAST_0: { - range: [3.5, 4.0], - mets: 5.0, - }, - 'VERY_FAST_!': { - range: [4.0, 4.5], - mets: 6.0, - }, - VERY_VERY_FAST: { - range: [4.5, 5], - mets: 7.0, - }, - SUPER_FAST: { - range: [5, 6], - mets: 8.3, - }, - RUNNING: { - range: [6, Number.MAX_VALUE], - mets: 9.8, - }, - }, - BICYCLING: { - VERY_VERY_SLOW: { - range: [0, 5.5], - mets: 3.5, - }, - VERY_SLOW: { - range: [5.5, 10], - mets: 5.8, - }, - SLOW: { - range: [10, 12], - mets: 6.8, - }, - MODERATE: { - range: [12, 14], - mets: 8.0, - }, - FAST: { - range: [14, 16], - mets: 10.0, - }, - VERT_FAST: { - range: [16, 19], - mets: 12.0, - }, - RACING: { - range: [20, Number.MAX_VALUE], - mets: 15.8, - }, - }, - UNKNOWN: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - IN_VEHICLE: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - CAR: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - BUS: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - LIGHT_RAIL: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - TRAIN: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - TRAM: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - SUBWAY: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - AIR_OR_HSR: { - ALL: { - range: [0, Number.MAX_VALUE], - mets: 0, - }, - }, - }; - this.getStandardMETs = function () { - return standardMETs; - }; - }) - .factory('CustomDatasetHelper', function (METDatasetHelper, Logger, $ionicPlatform) { - var cdh = {}; - - cdh.getCustomMETs = function () { - console.log('Getting custom METs', cdh.customMETs); - return cdh.customMETs; - }; - - cdh.getCustomFootprint = function () { - console.log('Getting custom footprint', cdh.customPerKmFootprint); - return cdh.customPerKmFootprint; - }; - - cdh.populateCustomMETs = function () { - let standardMETs = METDatasetHelper.getStandardMETs(); - let modeOptions = cdh.inputParams['MODE']; - let modeMETEntries = modeOptions.map((opt) => { - if (opt.met_equivalent) { - let currMET = standardMETs[opt.met_equivalent]; - return [opt.value, currMET]; - } else { - if (opt.met) { - let currMET = opt.met; - // if the user specifies a custom MET, they can't specify - // Number.MAX_VALUE since it is not valid JSON - // we assume that they specify -1 instead, and we will - // map -1 to Number.MAX_VALUE here by iterating over all the ranges - for (const rangeName in currMET) { - // console.log("Handling range ", rangeName); - currMET[rangeName].range = currMET[rangeName].range.map((i) => - i == -1 ? Number.MAX_VALUE : i, - ); - } - return [opt.value, currMET]; - } else { - console.warn( - 'Did not find either met_equivalent or met for ' + opt.value + ' ignoring entry', - ); - return undefined; - } - } - }); - cdh.customMETs = Object.fromEntries(modeMETEntries.filter((e) => angular.isDefined(e))); - console.log('After populating, custom METs = ', cdh.customMETs); - }; - - cdh.populateCustomFootprints = function () { - let modeOptions = cdh.inputParams['MODE']; - let modeCO2PerKm = modeOptions - .map((opt) => { - if (opt.range_limit_km) { - if (cdh.range_limited_motorized) { - Logger.displayError('Found two range limited motorized options', { - first: cdh.range_limited_motorized, - second: opt, - }); - } - cdh.range_limited_motorized = opt; - console.log('Found range limited motorized mode', cdh.range_limited_motorized); - } - if (angular.isDefined(opt.kgCo2PerKm)) { - return [opt.value, opt.kgCo2PerKm]; - } else { - return undefined; - } - }) - .filter((modeCO2) => angular.isDefined(modeCO2)); - cdh.customPerKmFootprint = Object.fromEntries(modeCO2PerKm); - console.log('After populating, custom perKmFootprint', cdh.customPerKmFootprint); - }; - - cdh.init = function (newConfig) { - try { - getLabelOptions(newConfig).then((inputParams) => { - console.log('Input params = ', inputParams); - cdh.inputParams = inputParams; - cdh.populateCustomMETs(); - cdh.populateCustomFootprints(); - }); - } catch (e) { - setTimeout(() => { - Logger.displayError( - 'Error in metrics-mappings while initializing custom dataset helper', - e, - ); - }, 1000); - } - }; - - $ionicPlatform.ready().then(function () { - getConfig().then((newConfig) => cdh.init(newConfig)); - }); - - return cdh; - }); diff --git a/www/js/metrics/CarbonDatasets.ts b/www/js/metrics/CarbonDatasets.ts new file mode 100644 index 000000000..b4815eead --- /dev/null +++ b/www/js/metrics/CarbonDatasets.ts @@ -0,0 +1,123 @@ +// Values are in Kg/PKm (kilograms per passenger-kilometer) +// Sources for EU values: +// - Tremod: 2017, CO2, CH4 and N2O in CO2-equivalent +// - HBEFA: 2020, CO2 (per country) +// German data uses Tremod. Other EU countries (and Switzerland) use HBEFA for car and bus, +// and Tremod for train and air (because HBEFA doesn't provide these). +// EU data is an average of the Tremod/HBEFA data for the countries listed; +// for this average the HBEFA data was used also in the German set (for car and bus). +export const carbonDatasets = { + US: { + regionName: 'United States', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 267 / 1609, + BUS: 278 / 1609, + LIGHT_RAIL: 120 / 1609, + SUBWAY: 74 / 1609, + TRAM: 90 / 1609, + TRAIN: 92 / 1609, + AIR_OR_HSR: 217 / 1609, + }, + }, + EU: { + // Plain average of values for the countries below (using HBEFA for car and bus, Tremod for others) + regionName: 'European Union', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.14515, + BUS: 0.04751, + LIGHT_RAIL: 0.064, + SUBWAY: 0.064, + TRAM: 0.064, + TRAIN: 0.048, + AIR_OR_HSR: 0.201, + }, + }, + DE: { + regionName: 'Germany', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.139, // Tremod (passenger car) + BUS: 0.0535, // Tremod (average city/coach) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201, // Tremod (DE airplane) + }, + }, + FR: { + regionName: 'France', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.13125, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04838, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201, // Tremod (DE airplane) + }, + }, + AT: { + regionName: 'Austria', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.14351, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04625, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201, // Tremod (DE airplane) + }, + }, + SE: { + regionName: 'Sweden', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.13458, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04557, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201, // Tremod (DE airplane) + }, + }, + NO: { + regionName: 'Norway', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.13265, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04185, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201, // Tremod (DE airplane) + }, + }, + CH: { + regionName: 'Switzerland', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 0.17638, // HBEFA (passenger car, considering 1 passenger) + BUS: 0.04866, // HBEFA (average short/long distance, considering 16/25 passengers) + LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) + SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) + TRAM: 0.064, // Tremod (DE tram, urban rail and subway) + TRAIN: 0.048, // Tremod (DE average short/long distance) + AIR_OR_HSR: 0.201, // Tremod (DE airplane) + }, + }, +}; diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 7c9bf3891..f2ac1cc76 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -3,6 +3,12 @@ import { View } from 'react-native'; import { Card, Text, useTheme } from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; +import { + getFootprintForMetrics, + setUseCustomFootprint, + getHighestFootprint, + getHighestFootprintForDistance, +} from './footprintHelper'; import { formatDateRangeOfDays, parseDataFromMetrics, @@ -13,13 +19,11 @@ import { } from './metricsHelper'; import { useTranslation } from 'react-i18next'; import BarChart from '../components/BarChart'; -import { getAngularService } from '../angular-react-helper'; import ChangeIndicator from './ChangeIndicator'; import color from 'color'; type Props = { userMetrics: MetricsData; aggMetrics: MetricsData }; const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { - const FootprintHelper = getAngularService('FootprintHelper'); const { colors } = useTheme(); const { t } = useTranslation(); @@ -51,18 +55,15 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //set custon dataset, if the labels are custom if (isCustomLabels(userThisWeekModeMap)) { - FootprintHelper.setUseCustomFootprint(); + setUseCustomFootprint(); } //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) let userPrevWeek; if (userLastWeekSummaryMap[0]) { userPrevWeek = { - low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics( - userLastWeekSummaryMap, - FootprintHelper.getHighestFootprint(), - ), + low: getFootprintForMetrics(userLastWeekSummaryMap, 0), + high: getFootprintForMetrics(userLastWeekSummaryMap, getHighestFootprint()), }; graphRecords.push({ label: t('main-metrics.unlabeled'), @@ -78,11 +79,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //calculate low-high and format range for past week (7 days ago -> yesterday) let userPastWeek = { - low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics( - userThisWeekSummaryMap, - FootprintHelper.getHighestFootprint(), - ), + low: getFootprintForMetrics(userThisWeekSummaryMap, 0), + high: getFootprintForMetrics(userThisWeekSummaryMap, getHighestFootprint()), }; graphRecords.push({ label: t('main-metrics.unlabeled'), @@ -100,7 +98,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { } //calculate worst-case carbon footprint - let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); + let worstCarbon = getHighestFootprintForDistance(worstDistance); graphRecords.push({ label: t('main-metrics.labeled'), x: worstCarbon, @@ -138,11 +136,8 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { let groupRecords = []; let aggCarbon = { - low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), - high: FootprintHelper.getFootprintForMetrics( - aggCarbonData, - FootprintHelper.getHighestFootprint(), - ), + low: getFootprintForMetrics(aggCarbonData, 0), + high: getFootprintForMetrics(aggCarbonData, getHighestFootprint()), }; console.log('testing group past week', aggCarbon); groupRecords.push({ diff --git a/www/js/metrics/CarbonTextCard.tsx b/www/js/metrics/CarbonTextCard.tsx index 9f1b4490f..bf40c4a61 100644 --- a/www/js/metrics/CarbonTextCard.tsx +++ b/www/js/metrics/CarbonTextCard.tsx @@ -4,6 +4,11 @@ import { Card, Text, useTheme } from 'react-native-paper'; import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; import { useTranslation } from 'react-i18next'; +import { + getFootprintForMetrics, + getHighestFootprint, + getHighestFootprintForDistance, +} from './footprintHelper'; import { formatDateRangeOfDays, parseDataFromMetrics, @@ -17,7 +22,6 @@ type Props = { userMetrics: MetricsData; aggMetrics: MetricsData }; const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { const { colors } = useTheme(); const { t } = useTranslation(); - const FootprintHelper = getAngularService('FootprintHelper'); const userText = useMemo(() => { if (userMetrics?.distance?.length > 0) { @@ -46,11 +50,8 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) if (userLastWeekSummaryMap[0]) { let userPrevWeek = { - low: FootprintHelper.getFootprintForMetrics(userLastWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics( - userLastWeekSummaryMap, - FootprintHelper.getHighestFootprint(), - ), + low: getFootprintForMetrics(userLastWeekSummaryMap, 0), + high: getFootprintForMetrics(userLastWeekSummaryMap, getHighestFootprint()), }; const label = `${t('main-metrics.prev-week')} (${formatDateRangeOfDays(lastWeekDistance)})`; if (userPrevWeek.low == userPrevWeek.high) @@ -64,11 +65,8 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { //calculate low-high and format range for past week (7 days ago -> yesterday) let userPastWeek = { - low: FootprintHelper.getFootprintForMetrics(userThisWeekSummaryMap, 0), - high: FootprintHelper.getFootprintForMetrics( - userThisWeekSummaryMap, - FootprintHelper.getHighestFootprint(), - ), + low: getFootprintForMetrics(userThisWeekSummaryMap, 0), + high: getFootprintForMetrics(userThisWeekSummaryMap, getHighestFootprint()), }; const label = `${t('main-metrics.past-week')} (${formatDateRangeOfDays(thisWeekDistance)})`; if (userPastWeek.low == userPastWeek.high) @@ -80,7 +78,7 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { }); //calculate worst-case carbon footprint - let worstCarbon = FootprintHelper.getHighestFootprintForDistance(worstDistance); + let worstCarbon = getHighestFootprintForDistance(worstDistance); textList.push({ label: t('main-metrics.worst-case'), value: Math.round(worstCarbon) }); return textList; @@ -113,11 +111,8 @@ const CarbonTextCard = ({ userMetrics, aggMetrics }: Props) => { let groupText = []; let aggCarbon = { - low: FootprintHelper.getFootprintForMetrics(aggCarbonData, 0), - high: FootprintHelper.getFootprintForMetrics( - aggCarbonData, - FootprintHelper.getHighestFootprint(), - ), + low: getFootprintForMetrics(aggCarbonData, 0), + high: getFootprintForMetrics(aggCarbonData, getHighestFootprint()), }; console.log('testing group past week', aggCarbon); const label = t('main-metrics.average'); diff --git a/www/js/metrics/CustomMetricsHelper.ts b/www/js/metrics/CustomMetricsHelper.ts new file mode 100644 index 000000000..062f6a2ca --- /dev/null +++ b/www/js/metrics/CustomMetricsHelper.ts @@ -0,0 +1,148 @@ +import angular from 'angular'; +import { getLabelOptions } from '../survey/multilabel/confirmHelper'; +import { getConfig } from '../config/dynamicConfig'; +import { storageGet, storageSet } from '../plugin/storage'; +import { displayError, logDebug } from '../plugin/logger'; +import { standardMETs } from './metDataset'; +import { carbonDatasets } from './carbonDatasets'; + +const CARBON_DATASET_KEY = 'carbon_dataset_locale'; +const defaultCarbonDatasetCode = 'US'; + +let _customMETs; +let _customPerKmFootprint; +let _range_limited_motorized; +let _inputParams; +let _currentCarbonDatasetCode = defaultCarbonDatasetCode; + +// we need to call the method from within a promise in initialize() +// and using this.setCurrentCarbonDatasetLocale doesn't seem to work +const setCurrentCarbonDatasetLocale = function (localeCode) { + for (var code in carbonDatasets) { + if (code == localeCode) { + _currentCarbonDatasetCode = localeCode; + break; + } + } +}; + +const loadCarbonDatasetLocale = function () { + return storageGet(CARBON_DATASET_KEY).then(function (localeCode) { + logDebug('loadCarbonDatasetLocale() obtained value from storage [' + localeCode + ']'); + if (!localeCode) { + localeCode = defaultCarbonDatasetCode; + logDebug('loadCarbonDatasetLocale() no value in storage, using [' + localeCode + '] instead'); + } + setCurrentCarbonDatasetLocale(localeCode); + }); +}; + +export const saveCurrentCarbonDatasetLocale = function (localeCode) { + setCurrentCarbonDatasetLocale(localeCode); + storageSet(CARBON_DATASET_KEY, _currentCarbonDatasetCode); + logDebug( + 'saveCurrentCarbonDatasetLocale() saved value [' + _currentCarbonDatasetCode + '] to storage', + ); +}; + +export const getCarbonDatasetOptions = function () { + var options = []; + for (var code in carbonDatasets) { + options.push({ + text: code, //carbonDatasets[code].regionName, + value: code, + }); + } + return options; +}; + +export const getCurrentCarbonDatasetCode = function () { + return _currentCarbonDatasetCode; +}; + +export const getCurrentCarbonDatasetFootprint = function () { + return carbonDatasets[_currentCarbonDatasetCode].footprintData; +}; + +export const getCustomMETs = function () { + console.log('Getting custom METs', _customMETs); + return _customMETs; +}; + +export const getCustomFootprint = function () { + console.log('Getting custom footprint', _customPerKmFootprint); + return _customPerKmFootprint; +}; + +const populateCustomMETs = function () { + let modeOptions = _inputParams['MODE']; + let modeMETEntries = modeOptions.map((opt) => { + if (opt.met_equivalent) { + let currMET = standardMETs[opt.met_equivalent]; + return [opt.value, currMET]; + } else { + if (opt.met) { + let currMET = opt.met; + // if the user specifies a custom MET, they can't specify + // Number.MAX_VALUE since it is not valid JSON + // we assume that they specify -1 instead, and we will + // map -1 to Number.MAX_VALUE here by iterating over all the ranges + for (const rangeName in currMET) { + // console.log("Handling range ", rangeName); + currMET[rangeName].range = currMET[rangeName].range.map((i) => + i == -1 ? Number.MAX_VALUE : i, + ); + } + return [opt.value, currMET]; + } else { + console.warn( + 'Did not find either met_equivalent or met for ' + opt.value + ' ignoring entry', + ); + return undefined; + } + } + }); + _customMETs = Object.fromEntries(modeMETEntries.filter((e) => angular.isDefined(e))); + console.log('After populating, custom METs = ', _customMETs); +}; + +const populateCustomFootprints = function () { + let modeOptions = _inputParams['MODE']; + let modeCO2PerKm = modeOptions + .map((opt) => { + if (opt.range_limit_km) { + if (_range_limited_motorized) { + displayError( + { first: _range_limited_motorized, second: opt }, + 'Found two range limited motorized options', + ); + } + _range_limited_motorized = opt; + console.log('Found range limited motorized mode', _range_limited_motorized); + } + if (angular.isDefined(opt.kgCo2PerKm)) { + return [opt.value, opt.kgCo2PerKm]; + } else { + return undefined; + } + }) + .filter((modeCO2) => angular.isDefined(modeCO2)); + _customPerKmFootprint = Object.fromEntries(modeCO2PerKm); + console.log('After populating, custom perKmFootprint', _customPerKmFootprint); +}; + +const initCustomDatasetHelper = async function (newConfig) { + newConfig = await getConfig(); + try { + getLabelOptions(newConfig).then((inputParams) => { + console.log('Input params = ', inputParams); + _inputParams = inputParams; + populateCustomMETs(); + populateCustomFootprints(); + }); + } catch (e) { + setTimeout(() => { + displayError(e, 'Error while initializing custom dataset helper'); + }, 1000); + } +}; diff --git a/www/js/metrics/METDataset.ts b/www/js/metrics/METDataset.ts new file mode 100644 index 000000000..901c17ae6 --- /dev/null +++ b/www/js/metrics/METDataset.ts @@ -0,0 +1,128 @@ +export const standardMETs = { + WALKING: { + VERY_SLOW: { + range: [0, 2.0], + mets: 2.0, + }, + SLOW: { + range: [2.0, 2.5], + mets: 2.8, + }, + MODERATE_0: { + range: [2.5, 2.8], + mets: 3.0, + }, + MODERATE_1: { + range: [2.8, 3.2], + mets: 3.5, + }, + FAST: { + range: [3.2, 3.5], + mets: 4.3, + }, + VERY_FAST_0: { + range: [3.5, 4.0], + mets: 5.0, + }, + 'VERY_FAST_!': { + range: [4.0, 4.5], + mets: 6.0, + }, + VERY_VERY_FAST: { + range: [4.5, 5], + mets: 7.0, + }, + SUPER_FAST: { + range: [5, 6], + mets: 8.3, + }, + RUNNING: { + range: [6, Number.MAX_VALUE], + mets: 9.8, + }, + }, + BICYCLING: { + VERY_VERY_SLOW: { + range: [0, 5.5], + mets: 3.5, + }, + VERY_SLOW: { + range: [5.5, 10], + mets: 5.8, + }, + SLOW: { + range: [10, 12], + mets: 6.8, + }, + MODERATE: { + range: [12, 14], + mets: 8.0, + }, + FAST: { + range: [14, 16], + mets: 10.0, + }, + VERT_FAST: { + range: [16, 19], + mets: 12.0, + }, + RACING: { + range: [20, Number.MAX_VALUE], + mets: 15.8, + }, + }, + UNKNOWN: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + IN_VEHICLE: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + CAR: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + BUS: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + LIGHT_RAIL: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + TRAIN: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + TRAM: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + SUBWAY: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, + AIR_OR_HSR: { + ALL: { + range: [0, Number.MAX_VALUE], + mets: 0, + }, + }, +}; diff --git a/www/js/metrics/footprintHelper.ts b/www/js/metrics/footprintHelper.ts new file mode 100644 index 000000000..763e067a2 --- /dev/null +++ b/www/js/metrics/footprintHelper.ts @@ -0,0 +1,161 @@ +import { getCustomFootprint } from './CustomMetricsHelper'; +import { getCurrentCarbonDatasetFootprint } from './CustomMetricsHelper'; + +var highestFootprint = 0; + +var mtokm = function (v) { + return v / 1000; +}; +let useCustom = false; + +export const setUseCustomFootprint = function () { + useCustom = true; +}; + +const getFootprint = function () { + if (useCustom == true) { + return getCustomFootprint(); + } else { + return getCurrentCarbonDatasetFootprint(); + } +}; + +const readableFormat = function (v) { + return v > 999 ? Math.round(v / 1000) + 'k kg CO₂' : Math.round(v) + ' kg CO₂'; +}; + +export const getFootprintForMetrics = function (userMetrics, defaultIfMissing = 0) { + var footprint = getFootprint(); + var result = 0; + for (var i in userMetrics) { + var mode = userMetrics[i].key; + if (mode == 'ON_FOOT') { + mode = 'WALKING'; + } + + if (mode in footprint) { + result += footprint[mode] * mtokm(userMetrics[i].values); + } else if (mode == 'IN_VEHICLE') { + result += + ((footprint['CAR'] + + footprint['BUS'] + + footprint['LIGHT_RAIL'] + + footprint['TRAIN'] + + footprint['TRAM'] + + footprint['SUBWAY']) / + 6) * + mtokm(userMetrics[i].values); + } else { + console.warn( + 'WARNING getFootprintFromMetrics() was requested for an unknown mode: ' + + mode + + ' metrics JSON: ' + + JSON.stringify(userMetrics), + ); + result += defaultIfMissing * mtokm(userMetrics[i].values); + } + } + return result; +}; + +const getLowestFootprintForDistance = function (distance) { + var footprint = getFootprint(); + var lowestFootprint = Number.MAX_SAFE_INTEGER; + for (var mode in footprint) { + if (mode == 'WALKING' || mode == 'BICYCLING') { + // these modes aren't considered when determining the lowest carbon footprint + } else { + lowestFootprint = Math.min(lowestFootprint, footprint[mode]); + } + } + return lowestFootprint * mtokm(distance); +}; + +export const getHighestFootprint = function () { + if (!highestFootprint) { + var footprint = getFootprint(); + let footprintList = []; + for (var mode in footprint) { + footprintList.push(footprint[mode]); + } + highestFootprint = Math.max(...footprintList); + } + return highestFootprint; +}; + +export const getHighestFootprintForDistance = function (distance) { + return getHighestFootprint() * mtokm(distance); +}; + +const getLowestMotorizedNonAirFootprint = function (footprint, rlmCO2) { + var lowestFootprint = Number.MAX_SAFE_INTEGER; + for (var mode in footprint) { + if (mode == 'AIR_OR_HSR' || mode == 'air') { + console.log('Air mode, ignoring'); + } else { + if (footprint[mode] == 0 || footprint[mode] <= rlmCO2) { + console.log( + 'Non motorized mode or footprint <= range_limited_motorized', + mode, + footprint[mode], + rlmCO2, + ); + } else { + lowestFootprint = Math.min(lowestFootprint, footprint[mode]); + } + } + } + return lowestFootprint; +}; + +const getOptimalDistanceRanges = function () { + const FIVE_KM = 5 * 1000; + const SIX_HUNDRED_KM = 600 * 1000; + if (!useCustom) { + const defaultFootprint = getCurrentCarbonDatasetFootprint(); + const lowestMotorizedNonAir = getLowestMotorizedNonAirFootprint(defaultFootprint); + const airFootprint = defaultFootprint['AIR_OR_HSR']; + return [ + { low: 0, high: FIVE_KM, optimal: 0 }, + { low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir }, + { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, + ]; + } else { + // custom footprint, let's get the custom values + const customFootprint = getCustomFootprint(); + let airFootprint = customFootprint['air']; + if (!airFootprint) { + // 2341 BTU/PMT from + // https://tedb.ornl.gov/wp-content/uploads/2021/02/TEDB_Ed_39.pdf#page=68 + // 159.25 lb per million BTU from EIA + // https://www.eia.gov/environment/emissions/co2_vol_mass.php + // (2341 * (159.25/1000000))/(1.6*2.2) = 0.09975, rounded up a bit + console.log('No entry for air in ', customFootprint, ' using default'); + airFootprint = 0.1; + } + const rlm = CustomDatasetHelper.range_limited_motorized; + if (!rlm) { + return [ + { low: 0, high: FIVE_KM, optimal: 0 }, + { low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir }, + { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, + ]; + } else { + console.log('Found range_limited_motorized mode', rlm); + const lowestMotorizedNonAir = getLowestMotorizedNonAirFootprint( + customFootprint, + rlm.kgCo2PerKm, + ); + return [ + { low: 0, high: FIVE_KM, optimal: 0 }, + { low: FIVE_KM, high: rlm.range_limit_km * 1000, optimal: rlm.kgCo2PerKm }, + { + low: rlm.range_limit_km * 1000, + high: SIX_HUNDRED_KM, + optimal: lowestMotorizedNonAir, + }, + { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, + ]; + } + } +}; diff --git a/www/js/metrics/metHelper.ts b/www/js/metrics/metHelper.ts new file mode 100644 index 000000000..8f8b4df85 --- /dev/null +++ b/www/js/metrics/metHelper.ts @@ -0,0 +1,109 @@ +import { getCustomMETs } from './customMetricsHelper'; +import { standardMETs } from './metDataset'; +import { storageGet, storageSet, storageRemove } from '../plugin/storage'; + +var highestMET = 0; +var USER_DATA_KEY = 'user-data'; +let useCustom = false; + +const setUseCustomFootprint = function () { + useCustom = true; +}; + +const getMETs = function () { + if (useCustom == true) { + return getCustomMETs(); + } else { + return standardMETs; + } +}; + +const set = function (info) { + return storageSet(USER_DATA_KEY, info); +}; + +const get = function () { + return storageGet(USER_DATA_KEY); +}; + +const remove = function () { + return storageRemove(USER_DATA_KEY); +}; + +const between = function (num, min, max) { + return num >= min && num <= max; +}; + +const getHighestMET = function () { + if (!highestMET) { + var met = getMETs(); + let metList = []; + for (var mode in met) { + var rangeList = met[mode]; + for (var range in rangeList) { + metList.push(rangeList[range].mets); + } + } + highestMET = Math.max(...metList); + } + return highestMET; +}; + +const getMet = function (mode, speed, defaultIfMissing) { + if (mode == 'ON_FOOT') { + console.log("getMet() converted 'ON_FOOT' to 'WALKING'"); + mode = 'WALKING'; + } + let currentMETs = getMETs(); + if (!currentMETs[mode]) { + console.warn('getMet() Illegal mode: ' + mode); + return defaultIfMissing; //So the calorie sum does not break with wrong return type + } + for (var i in currentMETs[mode]) { + if (between(mpstomph(speed), currentMETs[mode][i].range[0], currentMETs[mode][i].range[1])) { + return currentMETs[mode][i].mets; + } else if (mpstomph(speed) < 0) { + console.log('getMet() Negative speed: ' + mpstomph(speed)); + return 0; + } + } +}; + +const mpstomph = function (mps) { + return 2.23694 * mps; +}; + +const lbtokg = function (lb) { + return lb * 0.453592; +}; + +const fttocm = function (ft) { + return ft * 30.48; +}; + +const getCorrectedMet = function (met, gender, age, height, heightUnit, weight, weightUnit) { + var height = heightUnit == 0 ? fttocm(height) : height; + var weight = weightUnit == 0 ? lbtokg(weight) : weight; + let calcMet; + if (gender == 1) { + //male + calcMet = + (met * 3.5) / + (((66.473 + 5.0033 * height + 13.7516 * weight - 6.755 * age) / 1440 / 5 / weight) * 1000); + return met; + } else if (gender == 0) { + //female + let met = + (calcMet * 3.5) / + (((655.0955 + 1.8496 * height + 9.5634 * weight - 4.6756 * age) / 1440 / 5 / weight) * 1000); + return calcMet; + } +}; + +const getuserCalories = function (durationInMin, met) { + return 65 * durationInMin * met; +}; + +const getCalories = function (weightInKg, durationInMin, met) { + return weightInKg * durationInMin * met; +}; From 70f7238b232006c3e1ae5a40d4d30a195691ae23 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 8 Nov 2023 15:55:29 -0700 Subject: [PATCH 031/112] display message instead of error I was using the wrong function when I first converted --- www/js/metrics/CustomMetricsHelper.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/js/metrics/CustomMetricsHelper.ts b/www/js/metrics/CustomMetricsHelper.ts index 062f6a2ca..7d87f0344 100644 --- a/www/js/metrics/CustomMetricsHelper.ts +++ b/www/js/metrics/CustomMetricsHelper.ts @@ -2,7 +2,7 @@ import angular from 'angular'; import { getLabelOptions } from '../survey/multilabel/confirmHelper'; import { getConfig } from '../config/dynamicConfig'; import { storageGet, storageSet } from '../plugin/storage'; -import { displayError, logDebug } from '../plugin/logger'; +import { displayError, displayErrorMsg, logDebug } from '../plugin/logger'; import { standardMETs } from './metDataset'; import { carbonDatasets } from './carbonDatasets'; @@ -112,8 +112,8 @@ const populateCustomFootprints = function () { .map((opt) => { if (opt.range_limit_km) { if (_range_limited_motorized) { - displayError( - { first: _range_limited_motorized, second: opt }, + displayErrorMsg( + JSON.stringify({ first: _range_limited_motorized, second: opt }), 'Found two range limited motorized options', ); } From 5108352e77ad04bdb0f563c2a8f74a63cb611096 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 8 Nov 2023 15:56:12 -0700 Subject: [PATCH 032/112] reduce naming confusion there is a similar function for the footprint, lets make sure they have different names --- www/js/metrics/metHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/metrics/metHelper.ts b/www/js/metrics/metHelper.ts index 8f8b4df85..6c052b0be 100644 --- a/www/js/metrics/metHelper.ts +++ b/www/js/metrics/metHelper.ts @@ -6,7 +6,7 @@ var highestMET = 0; var USER_DATA_KEY = 'user-data'; let useCustom = false; -const setUseCustomFootprint = function () { +const setUseCustomMET = function () { useCustom = true; }; From 9118133e23ae2298779d23315020c388c8be7d84 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 8 Nov 2023 19:13:30 -0700 Subject: [PATCH 033/112] write footprinthelper tests first pass of tests over the footprintHelper getFootprintForMetrics is one of the main functions, used to calculate displayed values for the dashboard screen --- www/__mocks__/cordovaMocks.ts | 22 ++- www/__mocks__/fakeConfig.json | 88 +++++++++ www/__mocks__/fakeLabels.json | 211 ++++++++++++++++++++++ www/__tests__/footprintHelper.test.ts | 64 +++++++ www/js/metrics/CustomMetricsHelper.ts | 2 +- www/js/metrics/footprintHelper.ts | 4 +- www/js/survey/multilabel/confirmHelper.ts | 3 +- 7 files changed, 385 insertions(+), 9 deletions(-) create mode 100644 www/__mocks__/fakeConfig.json create mode 100644 www/__mocks__/fakeLabels.json create mode 100644 www/__tests__/footprintHelper.test.ts diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 62aa9be1a..ad69b52fe 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -1,4 +1,5 @@ import packageJsonBuild from '../../package.cordovabuild.json'; +import fakeConfig from './fakeConfig.json'; export const mockCordova = () => { window['cordova'] ||= {}; @@ -99,11 +100,22 @@ export const mockBEMUserCache = () => { ); }, getDocument: (key: string, withMetadata?: boolean) => { - return new Promise((rs, rj) => - setTimeout(() => { - rs(_storage[key]); - }, 100), - ); + // this was mocked specifically for enketoHelper's use, could be expanded if needed + const fakeSurveyConfig = fakeConfig; + + if (key == 'config/app_ui_config') { + return new Promise((rs, rj) => + setTimeout(() => { + rs(fakeSurveyConfig); + }, 100), + ); + } else { + return new Promise((rs, rj) => + setTimeout(() => { + rs(_storage[key]); + }, 100), + ); + } }, isEmptyDoc: (doc) => { if (doc == undefined) { diff --git a/www/__mocks__/fakeConfig.json b/www/__mocks__/fakeConfig.json new file mode 100644 index 000000000..dabec6cd9 --- /dev/null +++ b/www/__mocks__/fakeConfig.json @@ -0,0 +1,88 @@ +{ + "version": 1, + "ts": 1655143472, + "server": { + "connectUrl": "https://openpath-test.nrel.gov/api/", + "aggregate_call_auth": "user_only" + }, + "intro": { + "program_or_study": "study", + "start_month": "10", + "start_year": "2023", + "program_admin_contact": "K. Shankari", + "deployment_partner_name": "NREL", + "translated_text": { + "en": { + "deployment_partner_name": "NREL", + "deployment_name": "Testing environment for Jest testing", + "summary_line_1": "", + "summary_line_2": "", + "summary_line_3": "", + "short_textual_description": "", + "why_we_collect": "", + "research_questions": ["", ""] + }, + "es": { + "deployment_partner_name": "NREL", + "deployment_name": "Ambiente prueba para las pruebas de Jest", + "summary_line_1": "", + "summary_line_2": "", + "summary_line_3": "", + "short_textual_description": "", + "why_we_collect": "", + "research_questions": ["", ""] + } + } + }, + "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": "{ 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, }}" + }, + "labelVars": { + "da": { + "key": "Domestic_activities", + "type": "length" + }, + "erea": { + "key": "Employment_related_a_Education_activities", + "type": "length" + } + }, + "version": 9 + } + }, + "trip-labels": "ENKETO" + }, + "display_config": { + "use_imperial": false + }, + "profile_controls": { + "support_upload": true, + "trip_end_notification": false + }, + "admin_dashboard": { + "overview_users": true, + "overview_active_users": true, + "overview_trips": true, + "overview_signup_trends": true, + "overview_trips_trend": true, + "data_uuids": true, + "data_trips": true, + "data_trips_columns_exclude": [], + "additional_trip_columns": [], + "data_uuids_columns_exclude": [], + "token_generate": true, + "token_prefix": "nrelop", + "map_heatmap": true, + "map_bubble": true, + "map_trip_lines": true, + "push_send": true, + "options_uuids": true, + "options_emails": true + } +} diff --git a/www/__mocks__/fakeLabels.json b/www/__mocks__/fakeLabels.json new file mode 100644 index 000000000..2b1f3e824 --- /dev/null +++ b/www/__mocks__/fakeLabels.json @@ -0,0 +1,211 @@ +{ + "MODE": [ + { + "value": "walk", + "baseMode": "WALKING", + "met_equivalent": "WALKING", + "kgCo2PerKm": 0 + }, + { + "value": "e-bike", + "baseMode": "E_BIKE", + "met": { + "ALL": { + "range": [ + 0, + -1 + ], + "mets": 4.9 + } + }, + "kgCo2PerKm": 0.00728 + }, + { + "value": "bike", + "baseMode": "BICYCLING", + "met_equivalent": "BICYCLING", + "kgCo2PerKm": 0 + }, + { + "value": "bikeshare", + "baseMode": "BICYCLING", + "met_equivalent": "BICYCLING", + "kgCo2PerKm": 0 + }, + { + "value": "scootershare", + "baseMode": "E_SCOOTER", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.00894 + }, + { + "value": "drove_alone", + "baseMode": "CAR", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.22031 + }, + { + "value": "shared_ride", + "baseMode": "CAR", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.11015 + }, + { + "value": "hybrid_drove_alone", + "baseMode": "CAR", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.127 + }, + { + "value": "hybrid_shared_ride", + "baseMode": "CAR", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.0635 + }, + { + "value": "e_car_drove_alone", + "baseMode": "E_CAR", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.08216 + }, + { + "value": "e_car_shared_ride", + "baseMode": "E_CAR", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.04108 + }, + { + "value": "taxi", + "baseMode": "TAXI", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.30741 + }, + { + "value": "bus", + "baseMode": "BUS", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.20727 + }, + { + "value": "train", + "baseMode": "TRAIN", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.12256 + }, + { + "value": "free_shuttle", + "baseMode": "BUS", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.20727 + }, + { + "value": "air", + "baseMode": "AIR", + "met_equivalent": "IN_VEHICLE", + "kgCo2PerKm": 0.09975 + }, + { + "value": "not_a_trip", + "baseMode": "UNKNOWN", + "met_equivalent": "UNKNOWN", + "kgCo2PerKm": 0 + }, + { + "value": "other", + "baseMode": "OTHER", + "met_equivalent": "UNKNOWN", + "kgCo2PerKm": 0 + } + ], + "PURPOSE": [ + { + "value": "home" + }, + { + "value": "work" + }, + { + "value": "at_work" + }, + { + "value": "school" + }, + { + "value": "transit_transfer" + }, + { + "value": "shopping" + }, + { + "value": "meal" + }, + { + "value": "pick_drop_person" + }, + { + "value": "pick_drop_item" + }, + { + "value": "personal_med" + }, + { + "value": "access_recreation" + }, + { + "value": "exercise" + }, + { + "value": "entertainment" + }, + { + "value": "religious" + }, + { + "value": "other" + } + ], + "REPLACED_MODE": [ + { + "value": "no_travel" + }, + { + "value": "walk" + }, + { + "value": "bike" + }, + { + "value": "bikeshare" + }, + { + "value": "scootershare" + }, + { + "value": "drove_alone" + }, + { + "value": "shared_ride" + }, + { + "value": "e_car_drove_alone" + }, + { + "value": "e_car_shared_ride" + }, + { + "value": "taxi" + }, + { + "value": "bus" + }, + { + "value": "train" + }, + { + "value": "free_shuttle" + }, + { + "value": "other" + } + ] +} diff --git a/www/__tests__/footprintHelper.test.ts b/www/__tests__/footprintHelper.test.ts new file mode 100644 index 000000000..336e46c1d --- /dev/null +++ b/www/__tests__/footprintHelper.test.ts @@ -0,0 +1,64 @@ +import { initCustomDatasetHelper } from '../js/metrics/CustomMetricsHelper'; +import { + getFootprintForMetrics, + mtokm, + setUseCustomFootprint, +} from '../js/metrics/footprintHelper'; +import { getConfig } from '../js/config/dynamicConfig'; +import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; +import { mockLogger } from '../__mocks__/globalMocks'; +import fakeLabels from '../__mocks__/fakeLabels.json'; + +mockBEMUserCache(); +mockLogger(); + +global.fetch = (url: string) => + new Promise((rs, rj) => { + setTimeout(() => + rs({ + text: () => + new Promise((rs, rj) => { + let myJSON = JSON.stringify(fakeLabels); + setTimeout(() => rs(myJSON), 100); + }), + }), + ); + }) as any; + +const metrics = [ + { key: 'WALKING', values: 3000 }, + { key: 'BICYCLING', values: 6500 }, + { key: 'CAR', values: 50000 }, + { key: 'LIGHT_RAIL', values: 30000 }, + { key: 'Unicycle', values: 5000 }, +]; + +it('gets footprint for metrics (not custom, fallback 0', () => { + expect(getFootprintForMetrics(metrics, 0)).toBe(10.534493474207583); +}); + +it('gets footprint for metrics (not custom, fallback 0.1', () => { + expect(getFootprintForMetrics(metrics, 0.1)).toBe(10.534493474207583 + 0.5); +}); + +const custom_metrics = [ + { key: 'walk', values: 3000 }, + { key: 'bike', values: 6500 }, + { key: 'drove_alone', values: 10000 }, + { key: 'scootershare', values: 25000 }, + { key: 'unicycle', values: 5000 }, +]; + +it('gets footprint for metrics (custom, fallback 0', async () => { + initCustomDatasetHelper(getConfig()); + setUseCustomFootprint(); + await new Promise((r) => setTimeout(r, 500)); + expect(getFootprintForMetrics(custom_metrics, 0)).toBe(2.4266); +}); + +it('gets footprint for metrics (custom, fallback 0.1', async () => { + initCustomDatasetHelper(getConfig()); + setUseCustomFootprint(); + await new Promise((r) => setTimeout(r, 500)); + expect(getFootprintForMetrics(custom_metrics, 0.1)).toBe(2.4266 + 0.5); +}); diff --git a/www/js/metrics/CustomMetricsHelper.ts b/www/js/metrics/CustomMetricsHelper.ts index 7d87f0344..3d7eaf507 100644 --- a/www/js/metrics/CustomMetricsHelper.ts +++ b/www/js/metrics/CustomMetricsHelper.ts @@ -131,7 +131,7 @@ const populateCustomFootprints = function () { console.log('After populating, custom perKmFootprint', _customPerKmFootprint); }; -const initCustomDatasetHelper = async function (newConfig) { +export const initCustomDatasetHelper = async function (newConfig) { newConfig = await getConfig(); try { getLabelOptions(newConfig).then((inputParams) => { diff --git a/www/js/metrics/footprintHelper.ts b/www/js/metrics/footprintHelper.ts index 763e067a2..4725aa1c7 100644 --- a/www/js/metrics/footprintHelper.ts +++ b/www/js/metrics/footprintHelper.ts @@ -2,11 +2,11 @@ import { getCustomFootprint } from './CustomMetricsHelper'; import { getCurrentCarbonDatasetFootprint } from './CustomMetricsHelper'; var highestFootprint = 0; +let useCustom = false; -var mtokm = function (v) { +const mtokm = function (v) { return v / 1000; }; -let useCustom = false; export const setUseCustomFootprint = function () { useCustom = true; diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index a8972709b..6bcd85a50 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -35,7 +35,7 @@ export let inputDetails: InputDetails<'MODE' | 'PURPOSE' | 'REPLACED_MODE'>; export async function getLabelOptions(appConfigParam?) { if (appConfigParam) appConfig = appConfigParam; if (labelOptions) return labelOptions; - + console.log('in get label options', appConfig); if (appConfig.label_options) { const labelOptionsJson = await fetchUrlCached(appConfig.label_options); logDebug( @@ -48,6 +48,7 @@ export async function getLabelOptions(appConfigParam?) { 'No label_options found in config, using default label options at ' + defaultLabelOptionsURL, ); const defaultLabelOptionsJson = await fetchUrlCached(defaultLabelOptionsURL); + console.log('label options', defaultLabelOptionsJson); labelOptions = JSON.parse(defaultLabelOptionsJson) as LabelOptions; } /* fill in the translations to the 'text' fields of the labelOptions, From 2cdd212c778320fb76cfb151545389b2012c8e09 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 8 Nov 2023 19:15:29 -0700 Subject: [PATCH 034/112] fix formatting --- www/__mocks__/fakeLabels.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/www/__mocks__/fakeLabels.json b/www/__mocks__/fakeLabels.json index 2b1f3e824..676dc97b6 100644 --- a/www/__mocks__/fakeLabels.json +++ b/www/__mocks__/fakeLabels.json @@ -11,10 +11,7 @@ "baseMode": "E_BIKE", "met": { "ALL": { - "range": [ - 0, - -1 - ], + "range": [0, -1], "mets": 4.9 } }, From 1a734f766a887a13006f6c1d67281a46d60fd441 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 8 Nov 2023 19:15:44 -0700 Subject: [PATCH 035/112] remove unused import --- www/__tests__/footprintHelper.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/www/__tests__/footprintHelper.test.ts b/www/__tests__/footprintHelper.test.ts index 336e46c1d..07c0a1264 100644 --- a/www/__tests__/footprintHelper.test.ts +++ b/www/__tests__/footprintHelper.test.ts @@ -1,9 +1,5 @@ import { initCustomDatasetHelper } from '../js/metrics/CustomMetricsHelper'; -import { - getFootprintForMetrics, - mtokm, - setUseCustomFootprint, -} from '../js/metrics/footprintHelper'; +import { getFootprintForMetrics, setUseCustomFootprint } from '../js/metrics/footprintHelper'; import { getConfig } from '../js/config/dynamicConfig'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; From 1d7bf5b2bc42c6e55556bdcfd8aa00f84586ef7a Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 9 Nov 2023 09:13:35 -0700 Subject: [PATCH 036/112] remove country-specific since we've changed the way fallback labels are handled, everything should be custom now. This means that we're not using the carbon values from the set country, but rather from the labels --- www/__tests__/footprintHelper.test.ts | 4 +- www/js/metrics/CarbonDatasets.ts | 123 ------------------------ www/js/metrics/CustomMetricsHelper.ts | 74 +++----------- www/js/metrics/carbonDatasetFallback.ts | 14 +++ www/js/metrics/footprintHelper.ts | 12 +-- 5 files changed, 36 insertions(+), 191 deletions(-) delete mode 100644 www/js/metrics/CarbonDatasets.ts create mode 100644 www/js/metrics/carbonDatasetFallback.ts diff --git a/www/__tests__/footprintHelper.test.ts b/www/__tests__/footprintHelper.test.ts index 07c0a1264..5267bc858 100644 --- a/www/__tests__/footprintHelper.test.ts +++ b/www/__tests__/footprintHelper.test.ts @@ -29,11 +29,11 @@ const metrics = [ { key: 'Unicycle', values: 5000 }, ]; -it('gets footprint for metrics (not custom, fallback 0', () => { +it('gets footprint for metrics (not custom, fallback 0)', () => { expect(getFootprintForMetrics(metrics, 0)).toBe(10.534493474207583); }); -it('gets footprint for metrics (not custom, fallback 0.1', () => { +it('gets footprint for metrics (not custom, fallback 0.1)', () => { expect(getFootprintForMetrics(metrics, 0.1)).toBe(10.534493474207583 + 0.5); }); diff --git a/www/js/metrics/CarbonDatasets.ts b/www/js/metrics/CarbonDatasets.ts deleted file mode 100644 index b4815eead..000000000 --- a/www/js/metrics/CarbonDatasets.ts +++ /dev/null @@ -1,123 +0,0 @@ -// Values are in Kg/PKm (kilograms per passenger-kilometer) -// Sources for EU values: -// - Tremod: 2017, CO2, CH4 and N2O in CO2-equivalent -// - HBEFA: 2020, CO2 (per country) -// German data uses Tremod. Other EU countries (and Switzerland) use HBEFA for car and bus, -// and Tremod for train and air (because HBEFA doesn't provide these). -// EU data is an average of the Tremod/HBEFA data for the countries listed; -// for this average the HBEFA data was used also in the German set (for car and bus). -export const carbonDatasets = { - US: { - regionName: 'United States', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 267 / 1609, - BUS: 278 / 1609, - LIGHT_RAIL: 120 / 1609, - SUBWAY: 74 / 1609, - TRAM: 90 / 1609, - TRAIN: 92 / 1609, - AIR_OR_HSR: 217 / 1609, - }, - }, - EU: { - // Plain average of values for the countries below (using HBEFA for car and bus, Tremod for others) - regionName: 'European Union', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.14515, - BUS: 0.04751, - LIGHT_RAIL: 0.064, - SUBWAY: 0.064, - TRAM: 0.064, - TRAIN: 0.048, - AIR_OR_HSR: 0.201, - }, - }, - DE: { - regionName: 'Germany', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.139, // Tremod (passenger car) - BUS: 0.0535, // Tremod (average city/coach) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - FR: { - regionName: 'France', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13125, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04838, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - AT: { - regionName: 'Austria', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.14351, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04625, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - SE: { - regionName: 'Sweden', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13458, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04557, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - NO: { - regionName: 'Norway', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.13265, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04185, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, - CH: { - regionName: 'Switzerland', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 0.17638, // HBEFA (passenger car, considering 1 passenger) - BUS: 0.04866, // HBEFA (average short/long distance, considering 16/25 passengers) - LIGHT_RAIL: 0.064, // Tremod (DE tram, urban rail and subway) - SUBWAY: 0.064, // Tremod (DE tram, urban rail and subway) - TRAM: 0.064, // Tremod (DE tram, urban rail and subway) - TRAIN: 0.048, // Tremod (DE average short/long distance) - AIR_OR_HSR: 0.201, // Tremod (DE airplane) - }, - }, -}; diff --git a/www/js/metrics/CustomMetricsHelper.ts b/www/js/metrics/CustomMetricsHelper.ts index 3d7eaf507..11493cb95 100644 --- a/www/js/metrics/CustomMetricsHelper.ts +++ b/www/js/metrics/CustomMetricsHelper.ts @@ -1,79 +1,35 @@ import angular from 'angular'; import { getLabelOptions } from '../survey/multilabel/confirmHelper'; import { getConfig } from '../config/dynamicConfig'; -import { storageGet, storageSet } from '../plugin/storage'; import { displayError, displayErrorMsg, logDebug } from '../plugin/logger'; import { standardMETs } from './metDataset'; -import { carbonDatasets } from './carbonDatasets'; - -const CARBON_DATASET_KEY = 'carbon_dataset_locale'; -const defaultCarbonDatasetCode = 'US'; +import { fallbackCarbon } from './carbonDatasetFallback'; let _customMETs; let _customPerKmFootprint; let _range_limited_motorized; let _inputParams; -let _currentCarbonDatasetCode = defaultCarbonDatasetCode; - -// we need to call the method from within a promise in initialize() -// and using this.setCurrentCarbonDatasetLocale doesn't seem to work -const setCurrentCarbonDatasetLocale = function (localeCode) { - for (var code in carbonDatasets) { - if (code == localeCode) { - _currentCarbonDatasetCode = localeCode; - break; - } - } -}; - -const loadCarbonDatasetLocale = function () { - return storageGet(CARBON_DATASET_KEY).then(function (localeCode) { - logDebug('loadCarbonDatasetLocale() obtained value from storage [' + localeCode + ']'); - if (!localeCode) { - localeCode = defaultCarbonDatasetCode; - logDebug('loadCarbonDatasetLocale() no value in storage, using [' + localeCode + '] instead'); - } - setCurrentCarbonDatasetLocale(localeCode); - }); -}; - -export const saveCurrentCarbonDatasetLocale = function (localeCode) { - setCurrentCarbonDatasetLocale(localeCode); - storageSet(CARBON_DATASET_KEY, _currentCarbonDatasetCode); - logDebug( - 'saveCurrentCarbonDatasetLocale() saved value [' + _currentCarbonDatasetCode + '] to storage', - ); -}; - -export const getCarbonDatasetOptions = function () { - var options = []; - for (var code in carbonDatasets) { - options.push({ - text: code, //carbonDatasets[code].regionName, - value: code, - }); - } - return options; -}; - -export const getCurrentCarbonDatasetCode = function () { - return _currentCarbonDatasetCode; -}; - -export const getCurrentCarbonDatasetFootprint = function () { - return carbonDatasets[_currentCarbonDatasetCode].footprintData; -}; export const getCustomMETs = function () { - console.log('Getting custom METs', _customMETs); + logDebug('Getting custom METs' + JSON.stringify(_customMETs)); return _customMETs; }; export const getCustomFootprint = function () { - console.log('Getting custom footprint', _customPerKmFootprint); + logDebug('Getting custom footprint' + JSON.stringify(_customPerKmFootprint)); return _customPerKmFootprint; }; +export const getRangeLimitedMotorixe = function () { + logDebug('Getting range limited motorized' + JSON.stringify(_range_limited_motorized)); + return _range_limited_motorized; +}; + +export const getFallbackFootprint = function () { + console.log('getting fallback carbon'); + return fallbackCarbon.footprintData; +}; + const populateCustomMETs = function () { let modeOptions = _inputParams['MODE']; let modeMETEntries = modeOptions.map((opt) => { @@ -103,7 +59,7 @@ const populateCustomMETs = function () { } }); _customMETs = Object.fromEntries(modeMETEntries.filter((e) => angular.isDefined(e))); - console.log('After populating, custom METs = ', _customMETs); + logDebug('After populating, custom METs = ' + JSON.stringify(_customMETs)); }; const populateCustomFootprints = function () { @@ -128,7 +84,7 @@ const populateCustomFootprints = function () { }) .filter((modeCO2) => angular.isDefined(modeCO2)); _customPerKmFootprint = Object.fromEntries(modeCO2PerKm); - console.log('After populating, custom perKmFootprint', _customPerKmFootprint); + logDebug('After populating, custom perKmFootprint' + JSON.stringify(_customPerKmFootprint)); }; export const initCustomDatasetHelper = async function (newConfig) { diff --git a/www/js/metrics/carbonDatasetFallback.ts b/www/js/metrics/carbonDatasetFallback.ts new file mode 100644 index 000000000..4561d078a --- /dev/null +++ b/www/js/metrics/carbonDatasetFallback.ts @@ -0,0 +1,14 @@ +export const fallbackCarbon = { + regionName: 'United States', + footprintData: { + WALKING: 0, + BICYCLING: 0, + CAR: 267 / 1609, + BUS: 278 / 1609, + LIGHT_RAIL: 120 / 1609, + SUBWAY: 74 / 1609, + TRAM: 90 / 1609, + TRAIN: 92 / 1609, + AIR_OR_HSR: 217 / 1609, + }, +}; diff --git a/www/js/metrics/footprintHelper.ts b/www/js/metrics/footprintHelper.ts index 4725aa1c7..2162e87bf 100644 --- a/www/js/metrics/footprintHelper.ts +++ b/www/js/metrics/footprintHelper.ts @@ -1,5 +1,5 @@ -import { getCustomFootprint } from './CustomMetricsHelper'; -import { getCurrentCarbonDatasetFootprint } from './CustomMetricsHelper'; +import { displayErrorMsg } from '../plugin/logger'; +import { getCustomFootprint, getFallbackFootprint } from './CustomMetricsHelper'; var highestFootprint = 0; let useCustom = false; @@ -16,14 +16,12 @@ const getFootprint = function () { if (useCustom == true) { return getCustomFootprint(); } else { - return getCurrentCarbonDatasetFootprint(); + //TODO: check through configs and ensure they all have custom lables + displayErrorMsg('Error in Footprint Calculatons', 'issue with data or default labels'); + return getFallbackFootprint(); } }; -const readableFormat = function (v) { - return v > 999 ? Math.round(v / 1000) + 'k kg CO₂' : Math.round(v) + ' kg CO₂'; -}; - export const getFootprintForMetrics = function (userMetrics, defaultIfMissing = 0) { var footprint = getFootprint(); var result = 0; From 1eec588f8e3c231830d0c10402f66049972523d8 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 9 Nov 2023 09:39:49 -0700 Subject: [PATCH 037/112] remove stale references after we take out the angular modules, we need to take out all the references to them as well --- www/js/main.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/www/js/main.js b/www/js/main.js index 2b351e2c4..2c789891a 100644 --- a/www/js/main.js +++ b/www/js/main.js @@ -7,8 +7,6 @@ angular 'emission.main.diary', 'emission.i18n.utils', 'emission.splash.notifscheduler', - 'emission.main.metrics.factory', - 'emission.main.metrics.mappings', 'emission.services', ]) From 6102d62e43ef646f150ed995ad05145de570b96b Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 9 Nov 2023 09:43:19 -0700 Subject: [PATCH 038/112] remove dataset setting since we no longer rely on a set country for carbon calculations, but rather custom or default label configurations, we can take out the option to set it from the profile --- www/js/control/ProfileSettings.jsx | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index b8943a81c..894a47bee 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -26,11 +26,6 @@ import ControlCollectionHelper, { helperToggleLowAccuracy, forceTransition, } from './ControlCollectionHelper'; -import { - getCarbonDatasetOptions, - getCurrentCarbonDatasetCode, - saveCurrentCarbonDatasetLocale, -} from '../metrics/customMetricsHelper'; import { resetDataAndRefresh } from '../config/dynamicConfig'; import { AppContext } from '../App'; import { shareQR } from '../components/QrCode'; @@ -59,7 +54,6 @@ const ProfileSettings = () => { //states and variables used to control/create the settings const [opCodeVis, setOpCodeVis] = useState(false); const [nukeSetVis, setNukeVis] = useState(false); - const [carbonDataVis, setCarbonDataVis] = useState(false); const [forceStateVis, setForceStateVis] = useState(false); const [logoutVis, setLogoutVis] = useState(false); const [invalidateSuccessVis, setInvalidateSuccessVis] = useState(false); @@ -87,9 +81,6 @@ const ProfileSettings = () => { const [uploadReason, setUploadReason] = useState(''); const appVersion = useRef(); - let carbonDatasetString = - t('general-settings.carbon-dataset') + ': ' + getCurrentCarbonDatasetCode(); - const carbonOptions = getCarbonDatasetOptions(); const stateActions = [ { text: 'Initialize', transition: 'INITIALIZE' }, { text: 'Start trip', transition: 'EXITED_GEOFENCE' }, @@ -363,14 +354,6 @@ const ProfileSettings = () => { forceTransition(stateObject.transition); }; - const onSelectCarbon = function (carbonObject) { - console.log('changeCarbonDataset(): chose locale ' + carbonObject.value); - saveCurrentCarbonDatasetLocale(carbonObject.value); //there's some sort of error here - //Unhandled Promise Rejection: While logging, error -[NSNull UTF8String]: unrecognized selector sent to instance 0x7fff8a625fb0 - carbonDatasetString = - i18next.t('general-settings.carbon-dataset') + ': ' + getCurrentCarbonDatasetCode(); - }; - //conditional creation of setting sections let logUploadSection; @@ -441,10 +424,6 @@ const ProfileSettings = () => { textKey="control.medium-accuracy" action={toggleLowAccuracy} switchValue={collectSettings.lowAccuracy}> - setCarbonDataVis(true)}> { - {/* menu for "set carbon dataset - only somewhat working" */} - clearNotifications()}> - {/* force state sheet */} Date: Thu, 9 Nov 2023 09:51:06 -0700 Subject: [PATCH 039/112] increase message readability --- www/js/metrics/CustomMetricsHelper.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/js/metrics/CustomMetricsHelper.ts b/www/js/metrics/CustomMetricsHelper.ts index 11493cb95..088ec8831 100644 --- a/www/js/metrics/CustomMetricsHelper.ts +++ b/www/js/metrics/CustomMetricsHelper.ts @@ -11,17 +11,17 @@ let _range_limited_motorized; let _inputParams; export const getCustomMETs = function () { - logDebug('Getting custom METs' + JSON.stringify(_customMETs)); + logDebug('Getting custom METs ' + JSON.stringify(_customMETs)); return _customMETs; }; export const getCustomFootprint = function () { - logDebug('Getting custom footprint' + JSON.stringify(_customPerKmFootprint)); + logDebug('Getting custom footprint ' + JSON.stringify(_customPerKmFootprint)); return _customPerKmFootprint; }; export const getRangeLimitedMotorixe = function () { - logDebug('Getting range limited motorized' + JSON.stringify(_range_limited_motorized)); + logDebug('Getting range limited motorized ' + JSON.stringify(_range_limited_motorized)); return _range_limited_motorized; }; From ec4abff2a35ea478c78d0750fe9b262ed8f3458a Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 9 Nov 2023 09:52:03 -0700 Subject: [PATCH 040/112] ensure initialization of footprints we need to initialize the footprints before we can call them, accomplish this by calling the initialization function from the metrics tab once the app config is loaded --- www/js/metrics/MetricsTab.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index d23cdd454..6385e2fd3 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -17,6 +17,8 @@ import DailyActiveMinutesCard from './DailyActiveMinutesCard'; import CarbonTextCard from './CarbonTextCard'; import ActiveMinutesTableCard from './ActiveMinutesTableCard'; import { getAggregateData, getMetrics } from '../commHelper'; +import useAppConfig from '../useAppConfig'; +import { initCustomDatasetHelper } from './CustomMetricsHelper'; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; @@ -41,6 +43,7 @@ function getLastTwoWeeksDtRange() { const MetricsTab = () => { const { t } = useTranslation(); + const appConfig = useAppConfig(); const { getFormattedSpeed, speedSuffix, getFormattedDistance, distanceSuffix } = useImperialConfig(); @@ -53,6 +56,12 @@ const MetricsTab = () => { loadMetricsForPopulation('aggregate', dateRange); }, [dateRange]); + //initialize once config is populated + useEffect(() => { + if (!appConfig) return; + initCustomDatasetHelper(appConfig); + }, [appConfig]); + async function loadMetricsForPopulation(population: 'user' | 'aggregate', dateRange: DateTime[]) { const serverResponse = await fetchMetricsFromServer(population, dateRange); console.debug('Got metrics = ', serverResponse); From 7820646de8433ab19c2eadbce5389095ac0a559a Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 9 Nov 2023 10:13:09 -0700 Subject: [PATCH 041/112] remove old MET code we no longer store user's height/weight/etc because we are not calculating calories burned, for that reason the code related to storing user's data or calculating calories is not needed we will keep MET handling so we can bin activity into high/med/low intensity later not currently using the "highestMET" calculation code, so commenting that out for now --- www/js/metrics/metHelper.ts | 87 ++++++++----------------------------- 1 file changed, 19 insertions(+), 68 deletions(-) diff --git a/www/js/metrics/metHelper.ts b/www/js/metrics/metHelper.ts index 6c052b0be..b696c5790 100644 --- a/www/js/metrics/metHelper.ts +++ b/www/js/metrics/metHelper.ts @@ -1,12 +1,9 @@ import { getCustomMETs } from './customMetricsHelper'; import { standardMETs } from './metDataset'; -import { storageGet, storageSet, storageRemove } from '../plugin/storage'; -var highestMET = 0; -var USER_DATA_KEY = 'user-data'; let useCustom = false; -const setUseCustomMET = function () { +export const setUseCustomMET = function () { useCustom = true; }; @@ -18,38 +15,15 @@ const getMETs = function () { } }; -const set = function (info) { - return storageSet(USER_DATA_KEY, info); -}; - -const get = function () { - return storageGet(USER_DATA_KEY); -}; - -const remove = function () { - return storageRemove(USER_DATA_KEY); -}; - const between = function (num, min, max) { return num >= min && num <= max; }; -const getHighestMET = function () { - if (!highestMET) { - var met = getMETs(); - let metList = []; - for (var mode in met) { - var rangeList = met[mode]; - for (var range in rangeList) { - metList.push(rangeList[range].mets); - } - } - highestMET = Math.max(...metList); - } - return highestMET; +const mpstomph = function (mps) { + return 2.23694 * mps; }; -const getMet = function (mode, speed, defaultIfMissing) { +export const getMet = function (mode, speed, defaultIfMissing) { if (mode == 'ON_FOOT') { console.log("getMet() converted 'ON_FOOT' to 'WALKING'"); mode = 'WALKING'; @@ -69,41 +43,18 @@ const getMet = function (mode, speed, defaultIfMissing) { } }; -const mpstomph = function (mps) { - return 2.23694 * mps; -}; - -const lbtokg = function (lb) { - return lb * 0.453592; -}; - -const fttocm = function (ft) { - return ft * 30.48; -}; - -const getCorrectedMet = function (met, gender, age, height, heightUnit, weight, weightUnit) { - var height = heightUnit == 0 ? fttocm(height) : height; - var weight = weightUnit == 0 ? lbtokg(weight) : weight; - let calcMet; - if (gender == 1) { - //male - calcMet = - (met * 3.5) / - (((66.473 + 5.0033 * height + 13.7516 * weight - 6.755 * age) / 1440 / 5 / weight) * 1000); - return met; - } else if (gender == 0) { - //female - let met = - (calcMet * 3.5) / - (((655.0955 + 1.8496 * height + 9.5634 * weight - 4.6756 * age) / 1440 / 5 / weight) * 1000); - return calcMet; - } -}; - -const getuserCalories = function (durationInMin, met) { - return 65 * durationInMin * met; -}; - -const getCalories = function (weightInKg, durationInMin, met) { - return weightInKg * durationInMin * met; -}; +// var highestMET = 0; +// const getHighestMET = function () { +// if (!highestMET) { +// var met = getMETs(); +// let metList = []; +// for (var mode in met) { +// var rangeList = met[mode]; +// for (var range in rangeList) { +// metList.push(rangeList[range].mets); +// } +// } +// highestMET = Math.max(...metList); +// } +// return highestMET; +// }; From 7ebcc94fd68ce97dceaa31a96f815fbc1cf61a5a Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 9 Nov 2023 10:51:35 -0700 Subject: [PATCH 042/112] add tests for highest footprint needed to include methods for clearing out the local variables, so added parameter to be able to set useCuston to false, and to clearHighest --- www/__tests__/footprintHelper.test.ts | 43 +++++++++++++++++++++++--- www/js/metrics/CarbonFootprintCard.tsx | 2 +- www/js/metrics/footprintHelper.ts | 8 +++-- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/www/__tests__/footprintHelper.test.ts b/www/__tests__/footprintHelper.test.ts index 5267bc858..fa0b4574d 100644 --- a/www/__tests__/footprintHelper.test.ts +++ b/www/__tests__/footprintHelper.test.ts @@ -1,5 +1,11 @@ import { initCustomDatasetHelper } from '../js/metrics/CustomMetricsHelper'; -import { getFootprintForMetrics, setUseCustomFootprint } from '../js/metrics/footprintHelper'; +import { + clearHighestFootprint, + getFootprintForMetrics, + getHighestFootprint, + getHighestFootprintForDistance, + setUseCustomFootprint, +} from '../js/metrics/footprintHelper'; import { getConfig } from '../js/config/dynamicConfig'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; @@ -21,6 +27,11 @@ global.fetch = (url: string) => ); }) as any; +beforeEach(() => { + setUseCustomFootprint(false); + clearHighestFootprint(); +}); + const metrics = [ { key: 'WALKING', values: 3000 }, { key: 'BICYCLING', values: 6500 }, @@ -37,6 +48,14 @@ it('gets footprint for metrics (not custom, fallback 0.1)', () => { expect(getFootprintForMetrics(metrics, 0.1)).toBe(10.534493474207583 + 0.5); }); +it('gets the highest footprint from the dataset, not custom', () => { + expect(getHighestFootprint()).toBe(278 / 1609); +}); + +it('gets the highest footprint for distance, not custom', () => { + expect(getHighestFootprintForDistance(12345)).toBe((278 / 1609) * (12345 / 1000)); +}); + const custom_metrics = [ { key: 'walk', values: 3000 }, { key: 'bike', values: 6500 }, @@ -45,16 +64,30 @@ const custom_metrics = [ { key: 'unicycle', values: 5000 }, ]; -it('gets footprint for metrics (custom, fallback 0', async () => { +it('gets footprint for metrics (custom, fallback 0)', async () => { initCustomDatasetHelper(getConfig()); - setUseCustomFootprint(); + setUseCustomFootprint(true); await new Promise((r) => setTimeout(r, 500)); expect(getFootprintForMetrics(custom_metrics, 0)).toBe(2.4266); }); -it('gets footprint for metrics (custom, fallback 0.1', async () => { +it('gets footprint for metrics (custom, fallback 0.1)', async () => { initCustomDatasetHelper(getConfig()); - setUseCustomFootprint(); + setUseCustomFootprint(true); await new Promise((r) => setTimeout(r, 500)); expect(getFootprintForMetrics(custom_metrics, 0.1)).toBe(2.4266 + 0.5); }); + +it('gets the highest footprint from the dataset, custom', async () => { + initCustomDatasetHelper(getConfig()); + setUseCustomFootprint(true); + await new Promise((r) => setTimeout(r, 500)); + expect(getHighestFootprint()).toBe(0.30741); +}); + +it('gets the highest footprint for distance, custom', async () => { + initCustomDatasetHelper(getConfig()); + setUseCustomFootprint(true); + await new Promise((r) => setTimeout(r, 500)); + expect(getHighestFootprintForDistance(12345)).toBe(0.30741 * (12345 / 1000)); +}); diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index f2ac1cc76..30c265bfc 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -55,7 +55,7 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //set custon dataset, if the labels are custom if (isCustomLabels(userThisWeekModeMap)) { - setUseCustomFootprint(); + setUseCustomFootprint(true); } //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) diff --git a/www/js/metrics/footprintHelper.ts b/www/js/metrics/footprintHelper.ts index 2162e87bf..1c950e690 100644 --- a/www/js/metrics/footprintHelper.ts +++ b/www/js/metrics/footprintHelper.ts @@ -7,9 +7,13 @@ let useCustom = false; const mtokm = function (v) { return v / 1000; }; +export const setUseCustomFootprint = function (val: boolean) { + useCustom = val; +}; -export const setUseCustomFootprint = function () { - useCustom = true; +export const clearHighestFootprint = function () { + //need to clear for testing + highestFootprint = undefined; }; const getFootprint = function () { From 3943053782924e449e8befc9d0d8080812c5c7de Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 9 Nov 2023 12:13:55 -0700 Subject: [PATCH 043/112] write tests for metHelper --- www/__tests__/metHelper.test.ts | 45 +++++++++++++++++++++++++++++++++ www/js/metrics/metHelper.ts | 4 +-- 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 www/__tests__/metHelper.test.ts diff --git a/www/__tests__/metHelper.test.ts b/www/__tests__/metHelper.test.ts new file mode 100644 index 000000000..ee4fbd70d --- /dev/null +++ b/www/__tests__/metHelper.test.ts @@ -0,0 +1,45 @@ +import { getMet, setUseCustomMET } from '../js/metrics/metHelper'; +import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; +import { mockLogger } from '../__mocks__/globalMocks'; +import fakeLabels from '../__mocks__/fakeLabels.json'; +import { getConfig } from '../js/config/dynamicConfig'; +import { initCustomDatasetHelper } from '../js/metrics/CustomMetricsHelper'; + +mockBEMUserCache(); +mockLogger(); + +global.fetch = (url: string) => + new Promise((rs, rj) => { + setTimeout(() => + rs({ + text: () => + new Promise((rs, rj) => { + let myJSON = JSON.stringify(fakeLabels); + setTimeout(() => rs(myJSON), 100); + }), + }), + ); + }) as any; + +beforeEach(() => { + setUseCustomMET(false); +}); + +it('gets met for mode and speed', () => { + expect(getMet('WALKING', 1.47523, 0)).toBe(4.3); + expect(getMet('BICYCLING', 4.5, 0)).toBe(6.8); + expect(getMet('UNICYCLE', 100, 0)).toBe(0); + expect(getMet('CAR', 25, 1)).toBe(0); +}); + +it('gets custom met for mode and speed', async () => { + initCustomDatasetHelper(getConfig()); + setUseCustomMET(true); + await new Promise((r) => setTimeout(r, 500)); + expect(getMet('walk', 1.47523, 0)).toBe(4.3); + expect(getMet('bike', 4.5, 0)).toBe(6.8); + expect(getMet('unicycle', 100, 0)).toBe(0); + expect(getMet('drove_alone', 25, 1)).toBe(0); + expect(getMet('e-bike', 6, 1)).toBe(4.9); + expect(getMet('e-bike', 12, 1)).toBe(4.9); +}); diff --git a/www/js/metrics/metHelper.ts b/www/js/metrics/metHelper.ts index b696c5790..dc1f7d296 100644 --- a/www/js/metrics/metHelper.ts +++ b/www/js/metrics/metHelper.ts @@ -3,8 +3,8 @@ import { standardMETs } from './metDataset'; let useCustom = false; -export const setUseCustomMET = function () { - useCustom = true; +export const setUseCustomMET = function (val: boolean) { + useCustom = val; }; const getMETs = function () { From 7436502a0d56d790e58b72e5e4152cb41fe67a44 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 9 Nov 2023 14:45:12 -0700 Subject: [PATCH 044/112] custom dataset helper tests --- www/__tests__/customMetricsHelper.test.ts | 68 +++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 www/__tests__/customMetricsHelper.test.ts diff --git a/www/__tests__/customMetricsHelper.test.ts b/www/__tests__/customMetricsHelper.test.ts new file mode 100644 index 000000000..ec4b9d709 --- /dev/null +++ b/www/__tests__/customMetricsHelper.test.ts @@ -0,0 +1,68 @@ +import { getConfig } from '../js/config/dynamicConfig'; +import { + getCustomFootprint, + getCustomMETs, + getFallbackFootprint, + initCustomDatasetHelper, +} from '../js/metrics/CustomMetricsHelper'; +import { setUseCustomMET } from '../js/metrics/metHelper'; +import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; +import { mockLogger } from '../__mocks__/globalMocks'; +import fakeLabels from '../__mocks__/fakeLabels.json'; +import { setUseCustomFootprint } from '../js/metrics/footprintHelper'; +import { number } from 'prop-types'; + +mockBEMUserCache(); +mockLogger(); + +global.fetch = (url: string) => + new Promise((rs, rj) => { + setTimeout(() => + rs({ + text: () => + new Promise((rs, rj) => { + let myJSON = JSON.stringify(fakeLabels); + setTimeout(() => rs(myJSON), 100); + }), + }), + ); + }) as any; + +it('gets the fallback carbon', async () => { + expect(getFallbackFootprint()).toEqual( + expect.objectContaining({ + WALKING: 0, + BICYCLING: 0, + CAR: 267 / 1609, + TRAIN: 92 / 1609, + }), + ); +}); + +it('gets the custom mets', async () => { + initCustomDatasetHelper(getConfig()); + setUseCustomMET(true); + await new Promise((r) => setTimeout(r, 800)); + expect(getCustomMETs()).toMatchObject({ + walk: {}, + bike: {}, + bikeshare: {}, + 'e-bike': {}, + scootershare: {}, + drove_alone: {}, + }); +}); + +it('gets the custom footprint', async () => { + initCustomDatasetHelper(getConfig()); + setUseCustomFootprint(true); + await new Promise((r) => setTimeout(r, 800)); + expect(getCustomFootprint()).toMatchObject({ + walk: {}, + bike: {}, + bikeshare: {}, + 'e-bike': {}, + scootershare: {}, + drove_alone: {}, + }); +}); From 974ac3ad124038dc46a11edc3b3e73bdc8284816 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 9 Nov 2023 15:30:27 -0700 Subject: [PATCH 045/112] remove carbon fallback This code should never be used at this point -- labels will default to those in the sample file, which means that they are "walk, bike, ect" -- custom labels rather than the default "WALKING, BICYCLING, etc" Since the labels are always some version of custom (even the fallback) those values will always be used over the country-specific fallback values --- www/__tests__/customMetricsHelper.test.ts | 13 ------------ www/__tests__/footprintHelper.test.ts | 24 ----------------------- www/js/metrics/CustomMetricsHelper.ts | 8 +------- www/js/metrics/carbonDatasetFallback.ts | 14 ------------- www/js/metrics/footprintHelper.ts | 4 ++-- 5 files changed, 3 insertions(+), 60 deletions(-) delete mode 100644 www/js/metrics/carbonDatasetFallback.ts diff --git a/www/__tests__/customMetricsHelper.test.ts b/www/__tests__/customMetricsHelper.test.ts index ec4b9d709..0f221e12c 100644 --- a/www/__tests__/customMetricsHelper.test.ts +++ b/www/__tests__/customMetricsHelper.test.ts @@ -2,7 +2,6 @@ import { getConfig } from '../js/config/dynamicConfig'; import { getCustomFootprint, getCustomMETs, - getFallbackFootprint, initCustomDatasetHelper, } from '../js/metrics/CustomMetricsHelper'; import { setUseCustomMET } from '../js/metrics/metHelper'; @@ -10,7 +9,6 @@ import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; import fakeLabels from '../__mocks__/fakeLabels.json'; import { setUseCustomFootprint } from '../js/metrics/footprintHelper'; -import { number } from 'prop-types'; mockBEMUserCache(); mockLogger(); @@ -28,17 +26,6 @@ global.fetch = (url: string) => ); }) as any; -it('gets the fallback carbon', async () => { - expect(getFallbackFootprint()).toEqual( - expect.objectContaining({ - WALKING: 0, - BICYCLING: 0, - CAR: 267 / 1609, - TRAIN: 92 / 1609, - }), - ); -}); - it('gets the custom mets', async () => { initCustomDatasetHelper(getConfig()); setUseCustomMET(true); diff --git a/www/__tests__/footprintHelper.test.ts b/www/__tests__/footprintHelper.test.ts index fa0b4574d..1de4fd701 100644 --- a/www/__tests__/footprintHelper.test.ts +++ b/www/__tests__/footprintHelper.test.ts @@ -32,30 +32,6 @@ beforeEach(() => { clearHighestFootprint(); }); -const metrics = [ - { key: 'WALKING', values: 3000 }, - { key: 'BICYCLING', values: 6500 }, - { key: 'CAR', values: 50000 }, - { key: 'LIGHT_RAIL', values: 30000 }, - { key: 'Unicycle', values: 5000 }, -]; - -it('gets footprint for metrics (not custom, fallback 0)', () => { - expect(getFootprintForMetrics(metrics, 0)).toBe(10.534493474207583); -}); - -it('gets footprint for metrics (not custom, fallback 0.1)', () => { - expect(getFootprintForMetrics(metrics, 0.1)).toBe(10.534493474207583 + 0.5); -}); - -it('gets the highest footprint from the dataset, not custom', () => { - expect(getHighestFootprint()).toBe(278 / 1609); -}); - -it('gets the highest footprint for distance, not custom', () => { - expect(getHighestFootprintForDistance(12345)).toBe((278 / 1609) * (12345 / 1000)); -}); - const custom_metrics = [ { key: 'walk', values: 3000 }, { key: 'bike', values: 6500 }, diff --git a/www/js/metrics/CustomMetricsHelper.ts b/www/js/metrics/CustomMetricsHelper.ts index 088ec8831..0c25b7d41 100644 --- a/www/js/metrics/CustomMetricsHelper.ts +++ b/www/js/metrics/CustomMetricsHelper.ts @@ -3,7 +3,6 @@ import { getLabelOptions } from '../survey/multilabel/confirmHelper'; import { getConfig } from '../config/dynamicConfig'; import { displayError, displayErrorMsg, logDebug } from '../plugin/logger'; import { standardMETs } from './metDataset'; -import { fallbackCarbon } from './carbonDatasetFallback'; let _customMETs; let _customPerKmFootprint; @@ -20,16 +19,11 @@ export const getCustomFootprint = function () { return _customPerKmFootprint; }; -export const getRangeLimitedMotorixe = function () { +export const getRangeLimitedMotorized = function () { logDebug('Getting range limited motorized ' + JSON.stringify(_range_limited_motorized)); return _range_limited_motorized; }; -export const getFallbackFootprint = function () { - console.log('getting fallback carbon'); - return fallbackCarbon.footprintData; -}; - const populateCustomMETs = function () { let modeOptions = _inputParams['MODE']; let modeMETEntries = modeOptions.map((opt) => { diff --git a/www/js/metrics/carbonDatasetFallback.ts b/www/js/metrics/carbonDatasetFallback.ts deleted file mode 100644 index 4561d078a..000000000 --- a/www/js/metrics/carbonDatasetFallback.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const fallbackCarbon = { - regionName: 'United States', - footprintData: { - WALKING: 0, - BICYCLING: 0, - CAR: 267 / 1609, - BUS: 278 / 1609, - LIGHT_RAIL: 120 / 1609, - SUBWAY: 74 / 1609, - TRAM: 90 / 1609, - TRAIN: 92 / 1609, - AIR_OR_HSR: 217 / 1609, - }, -}; diff --git a/www/js/metrics/footprintHelper.ts b/www/js/metrics/footprintHelper.ts index 1c950e690..aef7249b3 100644 --- a/www/js/metrics/footprintHelper.ts +++ b/www/js/metrics/footprintHelper.ts @@ -1,5 +1,5 @@ import { displayErrorMsg } from '../plugin/logger'; -import { getCustomFootprint, getFallbackFootprint } from './CustomMetricsHelper'; +import { getCustomFootprint } from './CustomMetricsHelper'; var highestFootprint = 0; let useCustom = false; @@ -22,7 +22,7 @@ const getFootprint = function () { } else { //TODO: check through configs and ensure they all have custom lables displayErrorMsg('Error in Footprint Calculatons', 'issue with data or default labels'); - return getFallbackFootprint(); + return; } }; From 8c28737a73f3e078f0a606b78f79114bf06aff9a Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 9 Nov 2023 16:47:38 -0700 Subject: [PATCH 046/112] remove unused code --- www/js/metrics/footprintHelper.ts | 86 ------------------------------- www/js/metrics/metHelper.ts | 16 ------ 2 files changed, 102 deletions(-) diff --git a/www/js/metrics/footprintHelper.ts b/www/js/metrics/footprintHelper.ts index aef7249b3..840647e75 100644 --- a/www/js/metrics/footprintHelper.ts +++ b/www/js/metrics/footprintHelper.ts @@ -60,19 +60,6 @@ export const getFootprintForMetrics = function (userMetrics, defaultIfMissing = return result; }; -const getLowestFootprintForDistance = function (distance) { - var footprint = getFootprint(); - var lowestFootprint = Number.MAX_SAFE_INTEGER; - for (var mode in footprint) { - if (mode == 'WALKING' || mode == 'BICYCLING') { - // these modes aren't considered when determining the lowest carbon footprint - } else { - lowestFootprint = Math.min(lowestFootprint, footprint[mode]); - } - } - return lowestFootprint * mtokm(distance); -}; - export const getHighestFootprint = function () { if (!highestFootprint) { var footprint = getFootprint(); @@ -88,76 +75,3 @@ export const getHighestFootprint = function () { export const getHighestFootprintForDistance = function (distance) { return getHighestFootprint() * mtokm(distance); }; - -const getLowestMotorizedNonAirFootprint = function (footprint, rlmCO2) { - var lowestFootprint = Number.MAX_SAFE_INTEGER; - for (var mode in footprint) { - if (mode == 'AIR_OR_HSR' || mode == 'air') { - console.log('Air mode, ignoring'); - } else { - if (footprint[mode] == 0 || footprint[mode] <= rlmCO2) { - console.log( - 'Non motorized mode or footprint <= range_limited_motorized', - mode, - footprint[mode], - rlmCO2, - ); - } else { - lowestFootprint = Math.min(lowestFootprint, footprint[mode]); - } - } - } - return lowestFootprint; -}; - -const getOptimalDistanceRanges = function () { - const FIVE_KM = 5 * 1000; - const SIX_HUNDRED_KM = 600 * 1000; - if (!useCustom) { - const defaultFootprint = getCurrentCarbonDatasetFootprint(); - const lowestMotorizedNonAir = getLowestMotorizedNonAirFootprint(defaultFootprint); - const airFootprint = defaultFootprint['AIR_OR_HSR']; - return [ - { low: 0, high: FIVE_KM, optimal: 0 }, - { low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir }, - { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, - ]; - } else { - // custom footprint, let's get the custom values - const customFootprint = getCustomFootprint(); - let airFootprint = customFootprint['air']; - if (!airFootprint) { - // 2341 BTU/PMT from - // https://tedb.ornl.gov/wp-content/uploads/2021/02/TEDB_Ed_39.pdf#page=68 - // 159.25 lb per million BTU from EIA - // https://www.eia.gov/environment/emissions/co2_vol_mass.php - // (2341 * (159.25/1000000))/(1.6*2.2) = 0.09975, rounded up a bit - console.log('No entry for air in ', customFootprint, ' using default'); - airFootprint = 0.1; - } - const rlm = CustomDatasetHelper.range_limited_motorized; - if (!rlm) { - return [ - { low: 0, high: FIVE_KM, optimal: 0 }, - { low: FIVE_KM, high: SIX_HUNDRED_KM, optimal: lowestMotorizedNonAir }, - { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, - ]; - } else { - console.log('Found range_limited_motorized mode', rlm); - const lowestMotorizedNonAir = getLowestMotorizedNonAirFootprint( - customFootprint, - rlm.kgCo2PerKm, - ); - return [ - { low: 0, high: FIVE_KM, optimal: 0 }, - { low: FIVE_KM, high: rlm.range_limit_km * 1000, optimal: rlm.kgCo2PerKm }, - { - low: rlm.range_limit_km * 1000, - high: SIX_HUNDRED_KM, - optimal: lowestMotorizedNonAir, - }, - { low: SIX_HUNDRED_KM, high: Number.MAX_VALUE, optimal: airFootprint }, - ]; - } - } -}; diff --git a/www/js/metrics/metHelper.ts b/www/js/metrics/metHelper.ts index dc1f7d296..e1d1ce28b 100644 --- a/www/js/metrics/metHelper.ts +++ b/www/js/metrics/metHelper.ts @@ -42,19 +42,3 @@ export const getMet = function (mode, speed, defaultIfMissing) { } } }; - -// var highestMET = 0; -// const getHighestMET = function () { -// if (!highestMET) { -// var met = getMETs(); -// let metList = []; -// for (var mode in met) { -// var rangeList = met[mode]; -// for (var range in rangeList) { -// metList.push(rangeList[range].mets); -// } -// } -// highestMET = Math.max(...metList); -// } -// return highestMET; -// }; From deb7104362459ad108ea9022a2ec16421384e4f3 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Thu, 9 Nov 2023 18:09:20 -0700 Subject: [PATCH 047/112] add docstrings --- www/js/metrics/CustomMetricsHelper.ts | 27 +++++++++++++---- www/js/metrics/footprintHelper.ts | 43 ++++++++++++++++++++++++--- www/js/metrics/metHelper.ts | 27 +++++++++++++++++ 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/www/js/metrics/CustomMetricsHelper.ts b/www/js/metrics/CustomMetricsHelper.ts index 0c25b7d41..eab094062 100644 --- a/www/js/metrics/CustomMetricsHelper.ts +++ b/www/js/metrics/CustomMetricsHelper.ts @@ -4,26 +4,34 @@ import { getConfig } from '../config/dynamicConfig'; import { displayError, displayErrorMsg, logDebug } from '../plugin/logger'; import { standardMETs } from './metDataset'; +//variables to store values locally let _customMETs; let _customPerKmFootprint; let _range_limited_motorized; let _inputParams; +/** + * @function gets custom mets, must be initialized + * @returns the custom mets stored locally + */ export const getCustomMETs = function () { logDebug('Getting custom METs ' + JSON.stringify(_customMETs)); return _customMETs; }; +/** + * @function gets the custom footprint, must be initialized + * @returns custom footprint + */ export const getCustomFootprint = function () { logDebug('Getting custom footprint ' + JSON.stringify(_customPerKmFootprint)); return _customPerKmFootprint; }; -export const getRangeLimitedMotorized = function () { - logDebug('Getting range limited motorized ' + JSON.stringify(_range_limited_motorized)); - return _range_limited_motorized; -}; - +/** + * @function stores custom mets in local var + * needs _inputParams, label options stored after gotten from config + */ const populateCustomMETs = function () { let modeOptions = _inputParams['MODE']; let modeMETEntries = modeOptions.map((opt) => { @@ -56,6 +64,10 @@ const populateCustomMETs = function () { logDebug('After populating, custom METs = ' + JSON.stringify(_customMETs)); }; +/** + * @function stores custom footprint in local var + * needs _inputParams which is stored after gotten from config + */ const populateCustomFootprints = function () { let modeOptions = _inputParams['MODE']; let modeCO2PerKm = modeOptions @@ -81,6 +93,11 @@ const populateCustomFootprints = function () { logDebug('After populating, custom perKmFootprint' + JSON.stringify(_customPerKmFootprint)); }; +/** + * @function initializes the datasets based on configured label options + * calls popuplateCustomMETs and populateCustomFootprint + * @param newConfig the app config file + */ export const initCustomDatasetHelper = async function (newConfig) { newConfig = await getConfig(); try { diff --git a/www/js/metrics/footprintHelper.ts b/www/js/metrics/footprintHelper.ts index 840647e75..a7b7f74ce 100644 --- a/www/js/metrics/footprintHelper.ts +++ b/www/js/metrics/footprintHelper.ts @@ -1,31 +1,57 @@ import { displayErrorMsg } from '../plugin/logger'; import { getCustomFootprint } from './CustomMetricsHelper'; -var highestFootprint = 0; +//variables for the highest footprint in the set and if using custom +let highestFootprint = 0; let useCustom = false; +/** + * @function converts meters to kilometers + * @param {number} v value in meters to be converted + * @returns {number} converted value in km + */ const mtokm = function (v) { return v / 1000; }; + +/** + * @function sets the value of useCustom + * @param {boolean} val if using custom footprint + */ export const setUseCustomFootprint = function (val: boolean) { useCustom = val; }; +/** + * @function clears the stored highest footprint + */ export const clearHighestFootprint = function () { //need to clear for testing highestFootprint = undefined; }; +/** + * @function gets the footprint + * currently will only be custom, as all labels are "custom" + * fallback is json/label-options.json.sample, with MET and kgCO2 defined + * @returns the footprint or undefined + */ const getFootprint = function () { if (useCustom == true) { return getCustomFootprint(); } else { - //TODO: check through configs and ensure they all have custom lables - displayErrorMsg('Error in Footprint Calculatons', 'issue with data or default labels'); - return; + displayErrorMsg('failed to use custom labels', 'Error in Footprint Calculatons'); + return undefined; } }; +/** + * @function calculates footprint for given metrics + * @param {Array} userMetrics string mode + number distance in meters pairs + * ex: const custom_metrics = [ { key: 'walk', values: 3000 }, { key: 'bike', values: 6500 }, ]; + * @param {number} defaultIfMissing optional, carbon intensity if mode not in footprint + * @returns {number} the sum of carbon emissions for userMetrics given + */ export const getFootprintForMetrics = function (userMetrics, defaultIfMissing = 0) { var footprint = getFootprint(); var result = 0; @@ -60,6 +86,10 @@ export const getFootprintForMetrics = function (userMetrics, defaultIfMissing = return result; }; +/** + * @function gets highest co2 intensity in the footprint + * @returns {number} the highest co2 intensity in the footprint + */ export const getHighestFootprint = function () { if (!highestFootprint) { var footprint = getFootprint(); @@ -72,6 +102,11 @@ export const getHighestFootprint = function () { return highestFootprint; }; +/** + * @function gets highest theoretical footprint for given distance + * @param {number} distance in meters to calculate max footprint + * @returns max footprint for given distance + */ export const getHighestFootprintForDistance = function (distance) { return getHighestFootprint() * mtokm(distance); }; diff --git a/www/js/metrics/metHelper.ts b/www/js/metrics/metHelper.ts index e1d1ce28b..c5ea7554e 100644 --- a/www/js/metrics/metHelper.ts +++ b/www/js/metrics/metHelper.ts @@ -3,10 +3,18 @@ import { standardMETs } from './metDataset'; let useCustom = false; +/** + * @function sets boolean to use custom mets + * @param {boolean} val + */ export const setUseCustomMET = function (val: boolean) { useCustom = val; }; +/** + * @function gets the METs object + * @returns {object} mets either custom or standard + */ const getMETs = function () { if (useCustom == true) { return getCustomMETs(); @@ -15,14 +23,33 @@ const getMETs = function () { } }; +/** + * @function checks number agains bounds + * @param num the number to check + * @param min lower bound + * @param max upper bound + * @returns {boolean} if number is within given bounds + */ const between = function (num, min, max) { return num >= min && num <= max; }; +/** + * @function converts meters per second to miles per hour + * @param mps meters per second speed + * @returns speed in miles per hour + */ const mpstomph = function (mps) { return 2.23694 * mps; }; +/** + * @function gets met for a given mode and speed + * @param {string} mode of travel + * @param {number} speed of travel in meters per second + * @param {number} defaultIfMissing default MET if mode not in METs + * @returns + */ export const getMet = function (mode, speed, defaultIfMissing) { if (mode == 'ON_FOOT') { console.log("getMet() converted 'ON_FOOT' to 'WALKING'"); From 9340297c6d94d4edaf7cba89aac9a8ef0a9ed23b Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 10 Nov 2023 09:15:55 -0700 Subject: [PATCH 048/112] working to debug dashboard --- www/js/metrics/CustomMetricsHelper.ts | 2 +- www/js/metrics/footprintHelper.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/www/js/metrics/CustomMetricsHelper.ts b/www/js/metrics/CustomMetricsHelper.ts index eab094062..5a70933f8 100644 --- a/www/js/metrics/CustomMetricsHelper.ts +++ b/www/js/metrics/CustomMetricsHelper.ts @@ -99,8 +99,8 @@ const populateCustomFootprints = function () { * @param newConfig the app config file */ export const initCustomDatasetHelper = async function (newConfig) { - newConfig = await getConfig(); try { + logDebug('initializing custom datasets with config' + newConfig); getLabelOptions(newConfig).then((inputParams) => { console.log('Input params = ', inputParams); _inputParams = inputParams; diff --git a/www/js/metrics/footprintHelper.ts b/www/js/metrics/footprintHelper.ts index a7b7f74ce..fd4ef8122 100644 --- a/www/js/metrics/footprintHelper.ts +++ b/www/js/metrics/footprintHelper.ts @@ -1,4 +1,4 @@ -import { displayErrorMsg } from '../plugin/logger'; +import { displayErrorMsg, logDebug } from '../plugin/logger'; import { getCustomFootprint } from './CustomMetricsHelper'; //variables for the highest footprint in the set and if using custom @@ -54,6 +54,7 @@ const getFootprint = function () { */ export const getFootprintForMetrics = function (userMetrics, defaultIfMissing = 0) { var footprint = getFootprint(); + logDebug('getting footprint for ' + userMetrics + ' with ' + footprint); var result = 0; for (var i in userMetrics) { var mode = userMetrics[i].key; From acc80223cb1115ab60235b8a22d1ecef0800fe85 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 10 Nov 2023 09:41:23 -0700 Subject: [PATCH 049/112] lift dataset initialization dataset was not initialized in time from metricsTab, lifting the call up to App fixed this issue --- www/js/App.tsx | 2 ++ www/js/metrics/MetricsTab.tsx | 10 ---------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/www/js/App.tsx b/www/js/App.tsx index a955b032d..77bf42463 100644 --- a/www/js/App.tsx +++ b/www/js/App.tsx @@ -19,6 +19,7 @@ import { initPushNotify } from './splash/pushNotifySettings'; import { initStoreDeviceSettings } from './splash/storeDeviceSettings'; import { initRemoteNotifyHandler } from './splash/remoteNotifyHandler'; import { withErrorBoundary } from './plugin/ErrorBoundary'; +import { initCustomDatasetHelper } from './metrics/CustomMetricsHelper'; const defaultRoutes = (t) => [ { @@ -77,6 +78,7 @@ const App = () => { initPushNotify(); initStoreDeviceSettings(); initRemoteNotifyHandler(); + initCustomDatasetHelper(appConfig); }, [appConfig]); const appContextValue = { diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 67670bf0c..03c6737a9 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState, useMemo } from 'react'; -import { getAngularService } from '../angular-react-helper'; import { View, ScrollView, useWindowDimensions } from 'react-native'; import { Appbar } from 'react-native-paper'; import NavBarButton from '../components/NavBarButton'; @@ -17,8 +16,6 @@ import DailyActiveMinutesCard from './DailyActiveMinutesCard'; import CarbonTextCard from './CarbonTextCard'; import ActiveMinutesTableCard from './ActiveMinutesTableCard'; import { getAggregateData, getMetrics } from '../commHelper'; -import useAppConfig from '../useAppConfig'; -import { initCustomDatasetHelper } from './CustomMetricsHelper'; import { displayError, logDebug } from '../plugin/logger'; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; @@ -44,7 +41,6 @@ function getLastTwoWeeksDtRange() { const MetricsTab = () => { const { t } = useTranslation(); - const appConfig = useAppConfig(); const { getFormattedSpeed, speedSuffix, getFormattedDistance, distanceSuffix } = useImperialConfig(); @@ -57,12 +53,6 @@ const MetricsTab = () => { loadMetricsForPopulation('aggregate', dateRange); }, [dateRange]); - //initialize once config is populated - useEffect(() => { - if (!appConfig) return; - initCustomDatasetHelper(appConfig); - }, [appConfig]); - async function loadMetricsForPopulation(population: 'user' | 'aggregate', dateRange: DateTime[]) { try { logDebug(`MetricsTab: fetching metrics for population ${population}' From e230420fa5c816a4eb7da359946312a93fc82a3c Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 10 Nov 2023 10:41:11 -0700 Subject: [PATCH 050/112] prettify merge conflicted file --- www/js/survey/enketo/enketo-add-note-button.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/www/js/survey/enketo/enketo-add-note-button.js b/www/js/survey/enketo/enketo-add-note-button.js index 2d8730f6f..8dc2e26e4 100644 --- a/www/js/survey/enketo/enketo-add-note-button.js +++ b/www/js/survey/enketo/enketo-add-note-button.js @@ -7,8 +7,8 @@ import { filterByNameAndVersion } from './enketoHelper'; import { getAdditionsForTimelineEntry, getUniqueEntries } from '../inputMatcher'; angular - .module('emission.survey.enketo.add-note-button', ['emission.services',]) - .factory('EnketoNotesButtonService', function ( Logger, $timeout) { + .module('emission.survey.enketo.add-note-button', ['emission.services']) + .factory('EnketoNotesButtonService', function (Logger, $timeout) { var enbs = {}; console.log('Creating EnketoNotesButtonService'); enbs.SINGLE_KEY = 'NOTES'; @@ -33,13 +33,9 @@ angular * Embed 'inputType' to the timelineEntry. */ enbs.extractResult = function (results) { - const resultsPromises = [ - filterByNameAndVersion(enbs.timelineEntrySurveyName, results), - ]; + const resultsPromises = [filterByNameAndVersion(enbs.timelineEntrySurveyName, results)]; if (enbs.timelineEntrySurveyName != enbs.placeSurveyName) { - resultsPromises.push( - filterByNameAndVersion(enbs.placeSurveyName, results), - ); + resultsPromises.push(filterByNameAndVersion(enbs.placeSurveyName, results)); } return Promise.all(resultsPromises); }; From da4947d7c303f6dfeef865410f901ae524e0aa75 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 10 Nov 2023 10:57:47 -0700 Subject: [PATCH 051/112] add exception to transform the enketoHelper tests were failing because one of the files in the added directory was not transpiled ,so it was picking up as bad syntax --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 743ab5a00..ef3503294 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,7 +12,7 @@ module.exports = { "^.+\\.(ts|tsx|js|jsx)$": "babel-jest" }, transformIgnorePatterns: [ - "node_modules/(?!((jest-)?react-native(-.*)?|@react-native(-community)?)/)" + "node_modules/(?!((enketo-transformer/dist/enketo-transformer/web)|(jest-)?react-native(-.*)?|@react-native(-community)?)/)", ], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], moduleDirectories: ["node_modules", "src"], From 245982159acb2ee4f6410d027b8acfe2a73ff9c2 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 10 Nov 2023 14:19:56 -0700 Subject: [PATCH 052/112] lingering merge conflict this file does not exist in this branch! --- www/js/diary.js | 1 - 1 file changed, 1 deletion(-) diff --git a/www/js/diary.js b/www/js/diary.js index c580ad8f2..7e64e555f 100644 --- a/www/js/diary.js +++ b/www/js/diary.js @@ -5,7 +5,6 @@ angular .module('emission.main.diary', [ 'emission.main.diary.services', 'emission.plugin.logger', - 'emission.survey.enketo.answer', ]) .config(function ($stateProvider) { From 0890e23f6d82e8fd878fef9d74633889f5650fc0 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 10 Nov 2023 14:25:41 -0700 Subject: [PATCH 053/112] re-run prettier after merging --- www/js/diary.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/www/js/diary.js b/www/js/diary.js index 7e64e555f..08909886d 100644 --- a/www/js/diary.js +++ b/www/js/diary.js @@ -2,10 +2,7 @@ import angular from 'angular'; import LabelTab from './diary/LabelTab'; angular - .module('emission.main.diary', [ - 'emission.main.diary.services', - 'emission.plugin.logger', - ]) + .module('emission.main.diary', ['emission.main.diary.services', 'emission.plugin.logger']) .config(function ($stateProvider) { $stateProvider.state('root.main.inf_scroll', { From fa22ee4b7621430ef5b4399aafd3578fa9ce0733 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Fri, 10 Nov 2023 17:45:19 -0500 Subject: [PATCH 054/112] remove use of `rootScope` in getAggregateData This function used the Angular `rootScope` to get 2 of the server conn setting: connectUrl and aggregateAuth. These are both derived from dynamic config file in the 'server' section. So we can useAppConfig() in MetricsTab and pass 'appConfig.server' in as a parameter 'serverConnConfig'. Also, - cleaned up some syntax of getAggregateData to make it more readable - added type definitions for the server conn config One thing to note is that I don't think any active study or program uses "no_auth". But it still looks like it is an option so I am including it. --- www/js/commHelper.ts | 29 +++++++++++------------------ www/js/metrics/MetricsTab.tsx | 16 ++++++++++++---- www/js/types/appConfigTypes.ts | 12 ++++++++++++ 3 files changed, 35 insertions(+), 22 deletions(-) create mode 100644 www/js/types/appConfigTypes.ts diff --git a/www/js/commHelper.ts b/www/js/commHelper.ts index 5f144888b..f69f42331 100644 --- a/www/js/commHelper.ts +++ b/www/js/commHelper.ts @@ -1,5 +1,6 @@ import { DateTime } from 'luxon'; import { logDebug } from './plugin/logger'; +import { ServerConnConfig } from './types/appConfigTypes'; /** * @param url URL endpoint for the request @@ -129,20 +130,17 @@ export function getMetrics(timeType: 'timestamp' | 'local_date', metricsQuery) { }); } -export function getAggregateData(path: string, data: any) { +export function getAggregateData(path: string, query, serverConnConfig: ServerConnConfig) { return new Promise((rs, rj) => { - const fullUrl = `${window['$rootScope'].connectUrl}/${path}`; - data['aggregate'] = true; + const fullUrl = `${serverConnConfig.connectUrl}/${path}`; + query['aggregate'] = true; - if (window['$rootScope'].aggregateAuth === 'no_auth') { - logDebug( - `getting aggregate data without user authentication from ${fullUrl} with arguments ${JSON.stringify( - data, - )}`, - ); + if (serverConnConfig.aggregate_call_auth == 'no_auth') { + logDebug(`getting aggregate data without user authentication from ${fullUrl} + with arguments ${JSON.stringify(query)}`); const options = { method: 'post', - data: data, + data: query, responseType: 'json', }; window['cordova'].plugin.http.sendRequest( @@ -156,14 +154,9 @@ export function getAggregateData(path: string, data: any) { }, ); } else { - logDebug( - `getting aggregate data with user authentication from ${fullUrl} with arguments ${JSON.stringify( - data, - )}`, - ); - const msgFiller = (message) => { - return Object.assign(message, data); - }; + logDebug(`getting aggregate data with user authentication from ${fullUrl} + with arguments ${JSON.stringify(query)}`); + const msgFiller = (message) => Object.assign(message, query); window['cordova'].plugins.BEMServerComm.pushGetJSON(`/${path}`, msgFiller, rs, rj); } }).catch((error) => { diff --git a/www/js/metrics/MetricsTab.tsx b/www/js/metrics/MetricsTab.tsx index 03c6737a9..7a1636b61 100644 --- a/www/js/metrics/MetricsTab.tsx +++ b/www/js/metrics/MetricsTab.tsx @@ -17,10 +17,16 @@ import CarbonTextCard from './CarbonTextCard'; import ActiveMinutesTableCard from './ActiveMinutesTableCard'; import { getAggregateData, getMetrics } from '../commHelper'; import { displayError, logDebug } from '../plugin/logger'; +import useAppConfig from '../useAppConfig'; +import { ServerConnConfig } from '../types/appConfigTypes'; export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const; -async function fetchMetricsFromServer(type: 'user' | 'aggregate', dateRange: DateTime[]) { +async function fetchMetricsFromServer( + type: 'user' | 'aggregate', + dateRange: DateTime[], + serverConnConfig: ServerConnConfig, +) { const query = { freq: 'D', start_time: dateRange[0].toSeconds(), @@ -29,7 +35,7 @@ async function fetchMetricsFromServer(type: 'user' | 'aggregate', dateRange: Dat is_return_aggregate: type == 'aggregate', }; if (type == 'user') return getMetrics('timestamp', query); - return getAggregateData('result/metrics/timestamp', query); + return getAggregateData('result/metrics/timestamp', query, serverConnConfig); } function getLastTwoWeeksDtRange() { @@ -40,6 +46,7 @@ function getLastTwoWeeksDtRange() { } const MetricsTab = () => { + const appConfig = useAppConfig(); const { t } = useTranslation(); const { getFormattedSpeed, speedSuffix, getFormattedDistance, distanceSuffix } = useImperialConfig(); @@ -49,15 +56,16 @@ const MetricsTab = () => { const [userMetrics, setUserMetrics] = useState(null); useEffect(() => { + if (!appConfig?.server) return; loadMetricsForPopulation('user', dateRange); loadMetricsForPopulation('aggregate', dateRange); - }, [dateRange]); + }, [dateRange, appConfig?.server]); async function loadMetricsForPopulation(population: 'user' | 'aggregate', dateRange: DateTime[]) { try { logDebug(`MetricsTab: fetching metrics for population ${population}' in date range ${JSON.stringify(dateRange)}`); - const serverResponse = await fetchMetricsFromServer(population, dateRange); + const serverResponse = await fetchMetricsFromServer(population, dateRange, appConfig.server); logDebug('MetricsTab: received metrics: ' + JSON.stringify(serverResponse)); const metrics = {}; const dataKey = population == 'user' ? 'user_metrics' : 'aggregate_metrics'; diff --git a/www/js/types/appConfigTypes.ts b/www/js/types/appConfigTypes.ts new file mode 100644 index 000000000..3d62f51cf --- /dev/null +++ b/www/js/types/appConfigTypes.ts @@ -0,0 +1,12 @@ +// WIP: type definitions for the 'dynamic config' spec +// examples of configs: https://github.com/e-mission/nrel-openpath-deploy-configs/tree/main/configs + +export type AppConfig = { + server: ServerConnConfig; + [k: string]: any; // TODO fill in all the other fields +}; + +export type ServerConnConfig = { + connectUrl: `https://${string}`; + aggregate_call_auth: 'no_auth' | 'user_only' | 'never'; +}; From 7df44e9c3f6892c572635f386ce32c7e87246703 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Sun, 12 Nov 2023 22:19:04 -0800 Subject: [PATCH 055/112] cleanup enketo after merge - Restore the use of 'filterByNameAndVersion' for trip confim surveys (not currently used for anything else). If a survey response is no longer compatible according to the appConfig, it should be filtered out here. - Make the terminology unambiguous such that "answer" means how the user answered a particular question within the survey, and "response" means the overall survey response - Clean up / clarify comments - Remove unused import statements --- www/__tests__/enketoHelper.test.ts | 32 ++++++++-------- www/js/diary/timelineHelper.ts | 7 ++-- www/js/survey/enketo/EnketoModal.tsx | 2 +- www/js/survey/enketo/enketoHelper.ts | 55 ++++++++++++++-------------- 4 files changed, 47 insertions(+), 49 deletions(-) diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index a2e5514b1..113e7f995 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -186,15 +186,15 @@ it('loads the previous response to a given survey', () => { }); /** - * filterByNameAndVersion filter the survey answers by survey name and their version. + * filterByNameAndVersion filter the survey responses by survey name and their version. * The version for filtering is specified in enketo survey `compatibleWith` config. - * The stored survey answer version must be greater than or equal to `compatibleWith` to be included. + * The stored survey response version must be greater than or equal to `compatibleWith` to be included. */ -it('filters the survey answers by their name and version', () => { - //no answers -> no filtered answers +it('filters the survey responses by their name and version', () => { + //no response -> no filtered responses expect(filterByNameAndVersion('TimeUseSurvey', [])).resolves.toStrictEqual([]); - const answer = [ + const response = [ { data: { label: 'Activity', //display label (this value is use for displaying on the button) @@ -202,17 +202,17 @@ it('filters the survey answers by their name and version', () => { fmt_time: '12:36', //the formatted timestamp at which the survey was filled out name: 'TimeUseSurvey', //survey name version: '1', //survey version - xmlResponse: '', //survey answer XML string - jsonDocResponse: 'this is my json object', //survey answer JSON object + xmlResponse: '', //survey response XML string + jsonDocResponse: 'this is my json object', //survey response JSON object }, metadata: {}, }, ]; - //one answer -> that answer - expect(filterByNameAndVersion('TimeUseSurvey', answer)).resolves.toStrictEqual(answer); + //one response -> that response + expect(filterByNameAndVersion('TimeUseSurvey', response)).resolves.toStrictEqual(response); - const answers = [ + const responses = [ { data: { label: 'Activity', //display label (this value is use for displaying on the button) @@ -220,8 +220,8 @@ it('filters the survey answers by their name and version', () => { fmt_time: '12:36', //the formatted timestamp at which the survey was filled out name: 'TimeUseSurvey', //survey name version: '1', //survey version - xmlResponse: '', //survey answer XML string - jsonDocResponse: 'this is my json object', //survey answer JSON object + xmlResponse: '', //survey response XML string + jsonDocResponse: 'this is my json object', //survey response JSON object }, metadata: {}, }, @@ -232,13 +232,13 @@ it('filters the survey answers by their name and version', () => { fmt_time: '12:36', //the formatted timestamp at which the survey was filled out name: 'OtherSurvey', //survey name version: '1', //survey version - xmlResponse: '', //survey answer XML string - jsonDocResponse: 'this is my json object', //survey answer JSON object + xmlResponse: '', //survey response XML string + jsonDocResponse: 'this is my json object', //survey response JSON object }, metadata: {}, }, ]; - //several answers -> only the one that has a name match - expect(filterByNameAndVersion('TimeUseSurvey', answers)).resolves.toStrictEqual(answer); + //several responses -> only the one that has a name match + expect(filterByNameAndVersion('TimeUseSurvey', responses)).resolves.toStrictEqual(response); }); diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 174974a9d..19c885cc1 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -1,11 +1,10 @@ import moment from 'moment'; -import { displayError, logDebug } from '../plugin/logger'; +import { logDebug } from '../plugin/logger'; import { getBaseModeByKey, getBaseModeByValue } from './diaryHelper'; import { getUnifiedDataForInterval } from '../services/unifiedDataLoader'; -import i18next from 'i18next'; import { UserInputEntry } from '../types/diaryTypes'; import { getLabelInputDetails, getLabelInputs } from '../survey/multilabel/confirmHelper'; -import { getNotDeletedCandidates, getUniqueEntries } from '../survey/inputMatcher'; +import { filterByNameAndVersion } from '../survey/enketo/enketoHelper'; const cachedGeojsons = new Map(); /** @@ -92,7 +91,7 @@ function updateUnprocessedInputs(labelsPromises, notesPromises, appConfig) { // fill in the unprocessedLabels object with the labels we just read labelResults.forEach((r, i) => { if (appConfig.survey_info?.['trip-labels'] == 'ENKETO') { - unprocessedLabels['SURVEY'] = r; + unprocessedLabels['SURVEY'] = filterByNameAndVersion('TripConfirmSurvey', r); } else { unprocessedLabels[getLabelInputs()[i]] = r; } diff --git a/www/js/survey/enketo/EnketoModal.tsx b/www/js/survey/enketo/EnketoModal.tsx index 9267b9808..1d169ee9b 100644 --- a/www/js/survey/enketo/EnketoModal.tsx +++ b/www/js/survey/enketo/EnketoModal.tsx @@ -5,7 +5,7 @@ import { ModalProps } from 'react-native-paper'; import useAppConfig from '../../useAppConfig'; import { useTranslation } from 'react-i18next'; import { SurveyOptions, fetchSurvey, getInstanceStr, saveResponse } from './enketoHelper'; -import { displayError, displayErrorMsg, logDebug } from '../../plugin/logger'; +import { displayError, displayErrorMsg } from '../../plugin/logger'; type Props = Omit & { surveyName: string; diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 9defa77f5..379120373 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -1,4 +1,3 @@ -import { getAngularService } from '../../angular-react-helper'; import { Form } from 'enketo-core'; import { transform } from 'enketo-transformer/web'; import { XMLParser } from 'fast-xml-parser'; @@ -7,7 +6,7 @@ import MessageFormat from '@messageformat/core'; import { logDebug, logInfo } from '../../plugin/logger'; import { getConfig } from '../../config/dynamicConfig'; import { DateTime } from 'luxon'; -import { fetchUrlCached } from '../../commHelper'; +import { fetchUrlCached } from '../../services/commHelper'; import { getUnifiedDataForInterval } from '../../services/unifiedDataLoader'; export type PrefillFields = { [key: string]: string }; @@ -20,18 +19,18 @@ export type SurveyOptions = { dataKey?: string; }; -type EnketoAnswerData = { +type EnketoResponseData = { label: string; //display label (this value is use for displaying on the button) ts: string; //the timestamp at which the survey was filled out (in seconds) fmt_time: string; //the formatted timestamp at which the survey was filled out name: string; //survey name version: string; //survey version - xmlResponse: string; //survey answer XML string - jsonDocResponse: string; //survey answer JSON object + xmlResponse: string; //survey response as XML string + jsonDocResponse: string; //survey response as JSON object }; -type EnketoAnswer = { - data: EnketoAnswerData; //answer data +type EnketoResponse = { + data: EnketoResponseData; //survey response data metadata: any; }; @@ -79,9 +78,10 @@ const LABEL_FUNCTIONS = { }; /** - * _getAnswerByTagName lookup for the survey answer by tag name form the given XML document. - * @param {XMLDocument} xmlDoc survey answer object - * @param {string} tagName tag name + * _getAnswerByTagName look up how a question was answered, given the survey response + * and the tag name of the question + * @param {XMLDocument} xmlDoc survey response as XML object + * @param {string} tagName tag name of the question * @returns {string} answer string. If not found, return "\" */ function _getAnswerByTagName(xmlDoc: XMLDocument, tagName: string) { @@ -110,26 +110,24 @@ export function _lazyLoadConfig() { } /** - * filterByNameAndVersion filter the survey answers by survey name and their version. + * filterByNameAndVersion filter the survey responses by survey name and their version. * The version for filtering is specified in enketo survey `compatibleWith` config. - * The stored survey answer version must be greater than or equal to `compatibleWith` to be included. + * The survey version of the response must be greater than or equal to `compatibleWith` to be included. * @param {string} name survey name (defined in enketo survey config) - * @param {EnketoAnswer[]} answers survey answers - * (usually retrieved by calling UnifiedDataLoader.getUnifiedMessagesForInterval('manual/survey_response', tq)) method. - * @return {Promise} filtered survey answers + * @param {EnketoResponse[]} responses An array of previously recorded responses to Enketo surveys + * (presumably having been retrieved from unifiedDataLoader) + * @return {Promise} filtered survey responses */ -export function filterByNameAndVersion(name: string, answers: EnketoAnswer[]) { +export function filterByNameAndVersion(name: string, responses: EnketoResponse[]) { return _lazyLoadConfig().then((config) => - answers.filter( - (answer) => answer.data.name === name && answer.data.version >= config[name].compatibleWith, - ), + responses.filter((r) => r.data.name === name && r.data.version >= config[name].compatibleWith), ); } /** - * resolve answer label for the survey + * resolve a label for the survey response * @param {string} name survey name - * @param {XMLDocument} xmlDoc survey answer object + * @param {XMLDocument} xmlDoc survey response as XML object * @returns {Promise} label string Promise */ export async function resolveLabel(name: string, xmlDoc: XMLDocument) { @@ -172,7 +170,7 @@ export function getInstanceStr(xmlModel: string, opts: SurveyOptions): string | /** * resolve timestamps label from the survey response - * @param {XMLDocument} xmlDoc survey answer object + * @param {XMLDocument} xmlDoc survey response as XML object * @param {object} trip trip object * @returns {object} object with `start_ts` and `end_ts` * - null if no timestamps are resolved @@ -269,10 +267,11 @@ export function saveResponse(surveyName: string, enketoForm: Form, appConfig, op .then((data) => data); } -const _getMostRecent = (answers) => { - answers.sort((a, b) => a.metadata.write_ts < b.metadata.write_ts); - console.log('first answer is ', answers[0], ' last answer is ', answers[answers.length - 1]); - return answers[0]; +const _getMostRecent = (responses) => { + responses.sort((a, b) => a.metadata.write_ts < b.metadata.write_ts); + logDebug(`_getMostRecent: first response is ${responses[0]}; + last response is ${responses.slice(-1)[0]}`); + return responses[0]; }; /* @@ -286,8 +285,8 @@ export function loadPreviousResponseForSurvey(dataKey: string) { const tq = window['cordova'].plugins.BEMUserCache.getAllTimeQuery(); logDebug('loadPreviousResponseForSurvey: dataKey = ' + dataKey + '; tq = ' + tq); const getMethod = window['cordova'].plugins.BEMUserCache.getSensorDataForInterval; - return getUnifiedDataForInterval(dataKey, tq, getMethod).then((answers) => - _getMostRecent(answers), + return getUnifiedDataForInterval(dataKey, tq, getMethod).then((responses) => + _getMostRecent(responses), ); } From c39b3684008de94882886c218a1a5e9852119462 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan Date: Mon, 13 Nov 2023 05:14:06 -0700 Subject: [PATCH 056/112] changes made to emailService and other files as needed --- www/js/control/LogPage.tsx | 3 +- www/js/control/ProfileSettings.jsx | 6 +- www/js/control/SensedPage.tsx | 4 +- www/js/control/emailService.ts | 105 +++++++++++++++++++++++++++++ www/json/emailConfig.json.sample | 3 - 5 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 www/js/control/emailService.ts delete mode 100644 www/json/emailConfig.json.sample diff --git a/www/js/control/LogPage.tsx b/www/js/control/LogPage.tsx index 3fcce72ac..fba7a72d5 100644 --- a/www/js/control/LogPage.tsx +++ b/www/js/control/LogPage.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'; import { FlashList } from '@shopify/flash-list'; import moment from 'moment'; import AlertBar from './AlertBar'; +import { sendEmail } from './emailService'; type loadStats = { currentStart: number; gotMaxIndex: boolean; reachedEnd: boolean }; @@ -96,7 +97,7 @@ const LogPage = ({ pageVis, setPageVis }) => { }; const emailLog = function () { - EmailHelper.sendEmail('loggerDB'); + sendEmail('loggerDB'); }; const separator = () => ; diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index b081e642a..9296b74c5 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -13,7 +13,7 @@ import useAppConfig from '../useAppConfig'; import AlertBar from './AlertBar'; import DataDatePicker from './DataDatePicker'; import PrivacyPolicyModal from './PrivacyPolicyModal'; - +import { sendEmail } from './emailService'; import { uploadFile } from './uploadService'; import ActionMenu from '../components/ActionMenu'; import SensedPage from './SensedPage'; @@ -44,7 +44,7 @@ const ProfileSettings = () => { //angular services needed const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); - const EmailHelper = getAngularService('EmailHelper'); + //const EmailHelper = getAngularService('EmailHelper'); const NotificationScheduler = getAngularService('NotificationScheduler'); const ControlHelper = getAngularService('ControlHelper'); @@ -256,7 +256,7 @@ const ProfileSettings = () => { const emailLog = function () { // Passing true, we want to send logs - EmailHelper.sendEmail('loggerDB'); + sendEmail('loggerDB'); }; async function updatePrefReminderTime(storeNewVal = true, newTime) { diff --git a/www/js/control/SensedPage.tsx b/www/js/control/SensedPage.tsx index 4d51b5308..a55188469 100644 --- a/www/js/control/SensedPage.tsx +++ b/www/js/control/SensedPage.tsx @@ -5,7 +5,7 @@ import { getAngularService } from '../angular-react-helper'; import { useTranslation } from 'react-i18next'; import { FlashList } from '@shopify/flash-list'; import moment from 'moment'; -import EmailHelper from './emailService'; +import { sendEmail } from './emailService'; const SensedPage = ({ pageVis, setPageVis }) => { const { t } = useTranslation(); @@ -17,7 +17,7 @@ const SensedPage = ({ pageVis, setPageVis }) => { const [entries, setEntries] = useState([]); const emailCache = function () { - EmailHelper.sendEmail('userCacheDB'); + sendEmail('userCacheDB'); }; async function updateEntries() { diff --git a/www/js/control/emailService.ts b/www/js/control/emailService.ts new file mode 100644 index 000000000..72ddfaf82 --- /dev/null +++ b/www/js/control/emailService.ts @@ -0,0 +1,105 @@ +import React, { useEffect, useState } from 'react'; +import i18next from "i18next"; +import { logInfo, logDebug, displayError } from "../plugin/logger"; +//import 'cordova-plugin-email-composer'; + + +// Separated functions here + +/* +function getEmailConfig() { + return new Promise(async (resolve, reject) => { + try { + logInfo("About to get email config"); + let url = "json/emailConfig.json"; + let response = await fetch(url); + let emailConfigData = await response.json(); + logDebug("emailConfigString = " + JSON.stringify(emailConfigData.address)); + resolve(emailConfigData.address); + } catch (err) { + try { + let url = "json/emailConfig.json.sample"; + let response = await fetch(url); + let emailConfigData = await response.json(); + logDebug("default emailConfigString = " + JSON.stringify(emailConfigData.address)); + resolve(emailConfigData.address); + } catch (err) { + displayError(err, "Error while reading default email config"); + reject(err); + } + } + }); +} +*/ + +async function hasAccount(): Promise { + return new Promise((resolve, reject) => { + window['cordova'].plugins['email'].hasAccount(hasAct => { + resolve(hasAct); + }); + }); +} + +export async function sendEmail(database: string) { + let parentDir = "unknown"; + + if (window['ionic'].Platform.isIOS() && !(await hasAccount())) { //check in iOS for configuration of email thingy + alert(i18next.t('email-service.email-account-not-configured')); + return; + } + + if (window['ionic'].Platform.isAndroid()) { + parentDir = "app://databases"; + } + + if (window['ionic'].Platform.isIOS()) { + alert(i18next.t('email-service.email-account-mail-app')); + console.log(window['cordova'].file.dataDirectory); + parentDir = window['cordova'].file.dataDirectory + "../LocalDatabase"; + } + + if (parentDir === 'unknown') { + alert('parentDir unexpectedly = ' + parentDir + '!'); + } + + logInfo('Going to email ' + database); + parentDir = parentDir + '/' + database; + + alert(i18next.t('email-service.going-to-email', { parentDir: parentDir })); + + let emailConfig = `nseptank@nrel.gov`; //remember to change it to Shankari's + + let emailData = { + to: emailConfig, + attachments: [parentDir], + subject: i18next.t('email-service.email-log.subject-logs'), + body: i18next.t('email-service.email-log.body-please-fill-in-what-is-wrong') + }; + + window['cordova'].plugins['email'].open(emailData, () => { + logInfo('Email app closed while sending, ' + JSON.stringify(emailData) + ' not sure if we should do anything'); + }); +} + +/* +function EmailHelper() { + const [emailConfig, setEmailConfig]; + + useEffect(() => { + }, [emailConfig]); + + + +// My export component here + return ( +
+ +
+ ); + +} + +export default EmailHelper; //maybe this is a good option qmark - I think so? +*/ \ No newline at end of file diff --git a/www/json/emailConfig.json.sample b/www/json/emailConfig.json.sample deleted file mode 100644 index b1e28e63b..000000000 --- a/www/json/emailConfig.json.sample +++ /dev/null @@ -1,3 +0,0 @@ -{ - "address": "shankari@eecs.berkeley.edu" -} \ No newline at end of file From 473893f6e05641a2078b86915dbdb84cc85a15da Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Mon, 13 Nov 2023 09:32:13 -0700 Subject: [PATCH 057/112] Update emailService.ts - Changed email address to Shankari's --- www/js/control/emailService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/js/control/emailService.ts b/www/js/control/emailService.ts index 72ddfaf82..e8dec1a9c 100644 --- a/www/js/control/emailService.ts +++ b/www/js/control/emailService.ts @@ -43,7 +43,7 @@ async function hasAccount(): Promise { export async function sendEmail(database: string) { let parentDir = "unknown"; - if (window['ionic'].Platform.isIOS() && !(await hasAccount())) { //check in iOS for configuration of email thingy + if (window['ionic'].Platform.isIOS() && !(await hasAccount())) { alert(i18next.t('email-service.email-account-not-configured')); return; } @@ -67,7 +67,7 @@ export async function sendEmail(database: string) { alert(i18next.t('email-service.going-to-email', { parentDir: parentDir })); - let emailConfig = `nseptank@nrel.gov`; //remember to change it to Shankari's + let emailConfig = `k.shankari@nrel.gov`; let emailData = { to: emailConfig, @@ -102,4 +102,4 @@ function EmailHelper() { } export default EmailHelper; //maybe this is a good option qmark - I think so? -*/ \ No newline at end of file +*/ From ab65825a380d1c892b0c378739d94d79fca75ef8 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Mon, 13 Nov 2023 09:39:51 -0700 Subject: [PATCH 058/112] Update emailService.ts Removed comments --- www/js/control/emailService.ts | 53 ---------------------------------- 1 file changed, 53 deletions(-) diff --git a/www/js/control/emailService.ts b/www/js/control/emailService.ts index e8dec1a9c..ea7f976b2 100644 --- a/www/js/control/emailService.ts +++ b/www/js/control/emailService.ts @@ -1,36 +1,6 @@ import React, { useEffect, useState } from 'react'; import i18next from "i18next"; import { logInfo, logDebug, displayError } from "../plugin/logger"; -//import 'cordova-plugin-email-composer'; - - -// Separated functions here - -/* -function getEmailConfig() { - return new Promise(async (resolve, reject) => { - try { - logInfo("About to get email config"); - let url = "json/emailConfig.json"; - let response = await fetch(url); - let emailConfigData = await response.json(); - logDebug("emailConfigString = " + JSON.stringify(emailConfigData.address)); - resolve(emailConfigData.address); - } catch (err) { - try { - let url = "json/emailConfig.json.sample"; - let response = await fetch(url); - let emailConfigData = await response.json(); - logDebug("default emailConfigString = " + JSON.stringify(emailConfigData.address)); - resolve(emailConfigData.address); - } catch (err) { - displayError(err, "Error while reading default email config"); - reject(err); - } - } - }); -} -*/ async function hasAccount(): Promise { return new Promise((resolve, reject) => { @@ -80,26 +50,3 @@ export async function sendEmail(database: string) { logInfo('Email app closed while sending, ' + JSON.stringify(emailData) + ' not sure if we should do anything'); }); } - -/* -function EmailHelper() { - const [emailConfig, setEmailConfig]; - - useEffect(() => { - }, [emailConfig]); - - - -// My export component here - return ( -
- -
- ); - -} - -export default EmailHelper; //maybe this is a good option qmark - I think so? -*/ From 695196e3377a0000c5df5a806de99e82fbe7611a Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Mon, 13 Nov 2023 09:40:36 -0700 Subject: [PATCH 059/112] Update SensedPage.tsx Removed comments --- www/js/control/SensedPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/www/js/control/SensedPage.tsx b/www/js/control/SensedPage.tsx index a55188469..db1d43535 100644 --- a/www/js/control/SensedPage.tsx +++ b/www/js/control/SensedPage.tsx @@ -10,7 +10,6 @@ import { sendEmail } from './emailService'; const SensedPage = ({ pageVis, setPageVis }) => { const { t } = useTranslation(); const { colors } = useTheme(); - //const EmailHelper = getAngularService('EmailHelper'); /* Let's keep a reference to the database for convenience */ const [DB, setDB] = useState(); From 35dbdcf426f4c803cc371ee9bc71d8947f5fed64 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Mon, 13 Nov 2023 09:41:13 -0700 Subject: [PATCH 060/112] Update LogPage.tsx Removed comments --- www/js/control/LogPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/www/js/control/LogPage.tsx b/www/js/control/LogPage.tsx index fba7a72d5..6d603f19e 100644 --- a/www/js/control/LogPage.tsx +++ b/www/js/control/LogPage.tsx @@ -13,7 +13,6 @@ type loadStats = { currentStart: number; gotMaxIndex: boolean; reachedEnd: boole const LogPage = ({ pageVis, setPageVis }) => { const { t } = useTranslation(); const { colors } = useTheme(); - //const EmailHelper = getAngularService('EmailHelper'); const [loadStats, setLoadStats] = useState(); const [entries, setEntries] = useState([]); From d04d87aa4781175f131381fcc2859534bfc5b30e Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan <69108657+niccolopaganini@users.noreply.github.com> Date: Mon, 13 Nov 2023 09:41:41 -0700 Subject: [PATCH 061/112] Update ProfileSettings.jsx Removed comments --- www/js/control/ProfileSettings.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 9296b74c5..f239b8b29 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -44,7 +44,6 @@ const ProfileSettings = () => { //angular services needed const CarbonDatasetHelper = getAngularService('CarbonDatasetHelper'); - //const EmailHelper = getAngularService('EmailHelper'); const NotificationScheduler = getAngularService('NotificationScheduler'); const ControlHelper = getAngularService('ControlHelper'); From 7d66e57a10d868d17d461d37f80fbeb176fdc291 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan Date: Mon, 13 Nov 2023 10:45:44 -0700 Subject: [PATCH 062/112] ran prettier --- www/js/control/emailService.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/www/js/control/emailService.ts b/www/js/control/emailService.ts index ea7f976b2..0efca1518 100644 --- a/www/js/control/emailService.ts +++ b/www/js/control/emailService.ts @@ -1,31 +1,31 @@ import React, { useEffect, useState } from 'react'; -import i18next from "i18next"; -import { logInfo, logDebug, displayError } from "../plugin/logger"; +import i18next from 'i18next'; +import { logInfo, logDebug, displayError } from '../plugin/logger'; async function hasAccount(): Promise { return new Promise((resolve, reject) => { - window['cordova'].plugins['email'].hasAccount(hasAct => { + window['cordova'].plugins['email'].hasAccount((hasAct) => { resolve(hasAct); }); }); } export async function sendEmail(database: string) { - let parentDir = "unknown"; + let parentDir = 'unknown'; - if (window['ionic'].Platform.isIOS() && !(await hasAccount())) { + if (window['ionic'].Platform.isIOS() && !(await hasAccount())) { alert(i18next.t('email-service.email-account-not-configured')); return; } if (window['ionic'].Platform.isAndroid()) { - parentDir = "app://databases"; + parentDir = 'app://databases'; } if (window['ionic'].Platform.isIOS()) { alert(i18next.t('email-service.email-account-mail-app')); console.log(window['cordova'].file.dataDirectory); - parentDir = window['cordova'].file.dataDirectory + "../LocalDatabase"; + parentDir = window['cordova'].file.dataDirectory + '../LocalDatabase'; } if (parentDir === 'unknown') { @@ -43,10 +43,14 @@ export async function sendEmail(database: string) { to: emailConfig, attachments: [parentDir], subject: i18next.t('email-service.email-log.subject-logs'), - body: i18next.t('email-service.email-log.body-please-fill-in-what-is-wrong') + body: i18next.t('email-service.email-log.body-please-fill-in-what-is-wrong'), }; window['cordova'].plugins['email'].open(emailData, () => { - logInfo('Email app closed while sending, ' + JSON.stringify(emailData) + ' not sure if we should do anything'); + logInfo( + 'Email app closed while sending, ' + + JSON.stringify(emailData) + + ' not sure if we should do anything', + ); }); } From 4499e24097e42bac9d096a690437cca7f90256da Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 13 Nov 2023 10:58:18 -0700 Subject: [PATCH 063/112] test remaining function now that Unified Data Loader has been merged, we can test this function, may benefit from more end-to-end testing later, as it relies on the usercache plugin heavily --- www/__mocks__/cordovaMocks.ts | 10 ++++++++++ www/__tests__/enketoHelper.test.ts | 4 +--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 0684fa06d..a74df6b18 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -129,6 +129,16 @@ export const mockBEMUserCache = () => { return false; } }, + getAllTimeQuery: () => { + return {key: "write_ts", startTs: 0, endTs: Date.now()/1000}; + }, + getSensorDataForInterval: (key, tq, withMetadata) => { + return new Promise((rs, rj) => + setTimeout(() => { + rs({metadata: {write_ts: "1699897723"}, data: "completed", time: "01/01/2001"}); + }, 100), + ); + }, }; window['cordova'] ||= {}; window['cordova'].plugins ||= {}; diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index 113e7f995..ae43b7bb4 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -179,10 +179,8 @@ it('gets the saved result or throws an error', () => { * Loading it on demand seems like the way to go. If we choose to experiment * with incremental updates, we may want to revisit this. */ -// export function loadPreviousResponseForSurvey(dataKey: string) { it('loads the previous response to a given survey', () => { - //not really sure if I can test this yet given that it relies on an angular service... - // loadPreviousResponseForSurvey("manual/demographic_survey"); + expect(loadPreviousResponseForSurvey("manual/demographic_survey")).resolves.toMatchObject({data: "completed", time: "01/01/2001"}); }); /** From 4a000fc4e82be569afe27e87066f5aaac172b4a2 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 13 Nov 2023 11:01:54 -0700 Subject: [PATCH 064/112] re-run prettier --- www/__mocks__/cordovaMocks.ts | 10 +++++----- www/__tests__/enketoHelper.test.ts | 5 ++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index a74df6b18..f08293a85 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -130,14 +130,14 @@ export const mockBEMUserCache = () => { } }, getAllTimeQuery: () => { - return {key: "write_ts", startTs: 0, endTs: Date.now()/1000}; + return { key: 'write_ts', startTs: 0, endTs: Date.now() / 1000 }; }, getSensorDataForInterval: (key, tq, withMetadata) => { return new Promise((rs, rj) => - setTimeout(() => { - rs({metadata: {write_ts: "1699897723"}, data: "completed", time: "01/01/2001"}); - }, 100), - ); + setTimeout(() => { + rs({ metadata: { write_ts: '1699897723' }, data: 'completed', time: '01/01/2001' }); + }, 100), + ); }, }; window['cordova'] ||= {}; diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index ae43b7bb4..a8f49b29c 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -180,7 +180,10 @@ it('gets the saved result or throws an error', () => { * 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({data: "completed", time: "01/01/2001"}); + expect(loadPreviousResponseForSurvey('manual/demographic_survey')).resolves.toMatchObject({ + data: 'completed', + time: '01/01/2001', + }); }); /** From ebaa1e6e5cd19b76ef7b6f23c5509f99b0c845f2 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Mon, 13 Nov 2023 15:41:41 -0700 Subject: [PATCH 065/112] fix issue with filter The root of the issue I was having is that filterByNameAndVersion returns a promise, but it was being assigned to the unprocessedLabels var before the promise was fulfilled, adding the fiter.then() flow resolved this --- www/js/diary/timelineHelper.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/www/js/diary/timelineHelper.ts b/www/js/diary/timelineHelper.ts index 19c885cc1..1a2c87462 100644 --- a/www/js/diary/timelineHelper.ts +++ b/www/js/diary/timelineHelper.ts @@ -91,7 +91,9 @@ function updateUnprocessedInputs(labelsPromises, notesPromises, appConfig) { // fill in the unprocessedLabels object with the labels we just read labelResults.forEach((r, i) => { if (appConfig.survey_info?.['trip-labels'] == 'ENKETO') { - unprocessedLabels['SURVEY'] = filterByNameAndVersion('TripConfirmSurvey', r); + filterByNameAndVersion('TripConfirmSurvey', r).then((filtered) => { + unprocessedLabels['SURVEY'] = filtered; + }); } else { unprocessedLabels[getLabelInputs()[i]] = r; } From 941907aa21159e408c537e78d422efb8642053b5 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan Date: Wed, 15 Nov 2023 11:34:18 -0700 Subject: [PATCH 066/112] Fixed buttons --- www/css/style.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/www/css/style.css b/www/css/style.css index a2ac29368..9838ddd63 100644 --- a/www/css/style.css +++ b/www/css/style.css @@ -6,8 +6,10 @@ .question.non-select { display: inline-block; } - .question input[name*='_date'], - .question input[name*='_time'] { + .question input[name$="Start_date"], + .question input[name$="Start_time"], + .question input[name$="End_date"], + .question input[name$="End_time"] { width: calc(40vw - 10px); margin-right: 5px; display: flex; From 6cf3a802bcdaa4e786193854fc0562f63ac4f318 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 15 Nov 2023 13:42:31 -0700 Subject: [PATCH 067/112] patch for WSOD on dashboard the reason I was seeing WSOD with some of my data was that I had spans of weeks where one week I only biked and the other I only walked, so each dataset (walk and bike) only had one datapoint each. I was able to fix this by acesssing the 0th entry in the ith dataset instead of the ith entry in the 0th dataset, since the latter does not always exist in the active minutes chart --- www/js/components/Chart.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx index 4ebf49c24..5374946f5 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -174,8 +174,8 @@ const Chart = ({ callback: (value, i) => { logDebug(`Vertical axis callback: i = ${i}; chartDatasets = ${JSON.stringify(chartDatasets)}; - chartDatasets[0].data = ${JSON.stringify(chartDatasets[0].data)}`); - const label = chartDatasets[0].data[i].x; + chartDatasets[i].data = ${JSON.stringify(chartDatasets[i].data)}`); + const label = chartDatasets[i].data[0].x; if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); return label; From 2a3fb1360ace53764f6afd7bb0f93087ffa8904b Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan Date: Wed, 15 Nov 2023 14:09:42 -0700 Subject: [PATCH 068/112] Run through prettier --- www/css/style.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/www/css/style.css b/www/css/style.css index 9838ddd63..2bc7c6fee 100644 --- a/www/css/style.css +++ b/www/css/style.css @@ -6,10 +6,10 @@ .question.non-select { display: inline-block; } - .question input[name$="Start_date"], - .question input[name$="Start_time"], - .question input[name$="End_date"], - .question input[name$="End_time"] { + .question input[name$='Start_date'], + .question input[name$='Start_time'], + .question input[name$='End_date'], + .question input[name$='End_time'] { width: calc(40vw - 10px); margin-right: 5px; display: flex; From 1693409d3a19d6ccbefb55ade32e06c1d6a7659e Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Wed, 15 Nov 2023 14:11:28 -0700 Subject: [PATCH 069/112] tweak to account for same mode both weeks as an add on to my previous patch, we also need to account for if the same mode is both weeks (like the span in July where I just walked both weeks) This if statement accounts for both data cases. --- www/js/components/Chart.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx index 5374946f5..8d9154713 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -174,8 +174,14 @@ const Chart = ({ callback: (value, i) => { logDebug(`Vertical axis callback: i = ${i}; chartDatasets = ${JSON.stringify(chartDatasets)}; - chartDatasets[i].data = ${JSON.stringify(chartDatasets[i].data)}`); - const label = chartDatasets[i].data[0].x; + chartDatasets[0].data = ${JSON.stringify(chartDatasets[0].data)}`); + //account for different data possiblities - one mode per weeek, one mode both weeks, mixed weeks + let label; + if (chartDatasets[0].data[i]) { + label = chartDatasets[0].data[i].x; + } else { + label = chartDatasets[i].data[0].x; + } if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); return label; From 1f495e21b298918b7310b9d750183821187f39e3 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 13:56:31 -0700 Subject: [PATCH 070/112] clean up bugfix fixed typo, made single line, and added to horizontal chart as well https://github.com/e-mission/e-mission-phone/pull/1098#discussion_r1397765721 --- www/js/components/Chart.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx index 8d9154713..372d7e6c8 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -138,7 +138,8 @@ const Chart = ({ logDebug(`Horizontal axis callback: i = ${i}; chartDatasets = ${JSON.stringify(chartDatasets)}; chartDatasets[0].data = ${JSON.stringify(chartDatasets[0].data)}`); - const label = chartDatasets[0].data[i].y; + //account for different data possiblities + const label = chartDatasets[0].data[i]?.y || chartDatasets[i].data[0]?.y if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); return label; @@ -175,13 +176,8 @@ const Chart = ({ logDebug(`Vertical axis callback: i = ${i}; chartDatasets = ${JSON.stringify(chartDatasets)}; chartDatasets[0].data = ${JSON.stringify(chartDatasets[0].data)}`); - //account for different data possiblities - one mode per weeek, one mode both weeks, mixed weeks - let label; - if (chartDatasets[0].data[i]) { - label = chartDatasets[0].data[i].x; - } else { - label = chartDatasets[i].data[0].x; - } + //account for different data possiblities - one mode per week, one mode both weeks, mixed weeks + const label = chartDatasets[0].data[i]?.x || chartDatasets[i].data[0]?.x if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); return label; From ef3bd59e2d1b6ad76371ade69855485e5c2c9b51 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 13:59:42 -0700 Subject: [PATCH 071/112] renamed file CustomMetricsHelper -> customMetricsHelper --- www/__tests__/customMetricsHelper.test.ts | 2 +- www/__tests__/footprintHelper.test.ts | 2 +- www/__tests__/metHelper.test.ts | 2 +- www/js/App.tsx | 2 +- www/js/metrics/footprintHelper.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/www/__tests__/customMetricsHelper.test.ts b/www/__tests__/customMetricsHelper.test.ts index 0f221e12c..0a45f739a 100644 --- a/www/__tests__/customMetricsHelper.test.ts +++ b/www/__tests__/customMetricsHelper.test.ts @@ -3,7 +3,7 @@ import { getCustomFootprint, getCustomMETs, initCustomDatasetHelper, -} from '../js/metrics/CustomMetricsHelper'; +} from '../js/metrics/customMetricsHelper'; import { setUseCustomMET } from '../js/metrics/metHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; diff --git a/www/__tests__/footprintHelper.test.ts b/www/__tests__/footprintHelper.test.ts index 1de4fd701..5360b7a39 100644 --- a/www/__tests__/footprintHelper.test.ts +++ b/www/__tests__/footprintHelper.test.ts @@ -1,4 +1,4 @@ -import { initCustomDatasetHelper } from '../js/metrics/CustomMetricsHelper'; +import { initCustomDatasetHelper } from '../js/metrics/customMetricsHelper'; import { clearHighestFootprint, getFootprintForMetrics, diff --git a/www/__tests__/metHelper.test.ts b/www/__tests__/metHelper.test.ts index ee4fbd70d..ea36ec87f 100644 --- a/www/__tests__/metHelper.test.ts +++ b/www/__tests__/metHelper.test.ts @@ -3,7 +3,7 @@ import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; import fakeLabels from '../__mocks__/fakeLabels.json'; import { getConfig } from '../js/config/dynamicConfig'; -import { initCustomDatasetHelper } from '../js/metrics/CustomMetricsHelper'; +import { initCustomDatasetHelper } from '../js/metrics/customMetricsHelper'; mockBEMUserCache(); mockLogger(); diff --git a/www/js/App.tsx b/www/js/App.tsx index 77bf42463..2eece7f55 100644 --- a/www/js/App.tsx +++ b/www/js/App.tsx @@ -19,7 +19,7 @@ import { initPushNotify } from './splash/pushNotifySettings'; import { initStoreDeviceSettings } from './splash/storeDeviceSettings'; import { initRemoteNotifyHandler } from './splash/remoteNotifyHandler'; import { withErrorBoundary } from './plugin/ErrorBoundary'; -import { initCustomDatasetHelper } from './metrics/CustomMetricsHelper'; +import { initCustomDatasetHelper } from './metrics/customMetricsHelper'; const defaultRoutes = (t) => [ { diff --git a/www/js/metrics/footprintHelper.ts b/www/js/metrics/footprintHelper.ts index fd4ef8122..e5a615c4a 100644 --- a/www/js/metrics/footprintHelper.ts +++ b/www/js/metrics/footprintHelper.ts @@ -1,5 +1,5 @@ import { displayErrorMsg, logDebug } from '../plugin/logger'; -import { getCustomFootprint } from './CustomMetricsHelper'; +import { getCustomFootprint } from './customMetricsHelper'; //variables for the highest footprint in the set and if using custom let highestFootprint = 0; From df8ec72287d95872aedaa05fdb80b4de60a465d4 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 14:07:34 -0700 Subject: [PATCH 072/112] input Params -> labelOptions --- www/js/metrics/CustomMetricsHelper.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/www/js/metrics/CustomMetricsHelper.ts b/www/js/metrics/CustomMetricsHelper.ts index 5a70933f8..3b479f8a2 100644 --- a/www/js/metrics/CustomMetricsHelper.ts +++ b/www/js/metrics/CustomMetricsHelper.ts @@ -1,6 +1,5 @@ import angular from 'angular'; import { getLabelOptions } from '../survey/multilabel/confirmHelper'; -import { getConfig } from '../config/dynamicConfig'; import { displayError, displayErrorMsg, logDebug } from '../plugin/logger'; import { standardMETs } from './metDataset'; @@ -8,7 +7,7 @@ import { standardMETs } from './metDataset'; let _customMETs; let _customPerKmFootprint; let _range_limited_motorized; -let _inputParams; +let _labelOptions; /** * @function gets custom mets, must be initialized @@ -30,10 +29,10 @@ export const getCustomFootprint = function () { /** * @function stores custom mets in local var - * needs _inputParams, label options stored after gotten from config + * needs _labelOptions, stored after gotten from config */ const populateCustomMETs = function () { - let modeOptions = _inputParams['MODE']; + let modeOptions = _labelOptions['MODE']; let modeMETEntries = modeOptions.map((opt) => { if (opt.met_equivalent) { let currMET = standardMETs[opt.met_equivalent]; @@ -69,7 +68,7 @@ const populateCustomMETs = function () { * needs _inputParams which is stored after gotten from config */ const populateCustomFootprints = function () { - let modeOptions = _inputParams['MODE']; + let modeOptions = _labelOptions['MODE']; let modeCO2PerKm = modeOptions .map((opt) => { if (opt.range_limit_km) { @@ -103,7 +102,7 @@ export const initCustomDatasetHelper = async function (newConfig) { logDebug('initializing custom datasets with config' + newConfig); getLabelOptions(newConfig).then((inputParams) => { console.log('Input params = ', inputParams); - _inputParams = inputParams; + _labelOptions = inputParams; populateCustomMETs(); populateCustomFootprints(); }); From c1607887fafa37c576369bdecb1e3d36de52193a Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 14:16:09 -0700 Subject: [PATCH 073/112] footprint will always be custom removing the "is custom" variable for the footprint, and all references checking that footprint gotten is defined before returning, if not defined will throw an error --- www/__tests__/customMetricsHelper.test.ts | 2 -- www/__tests__/footprintHelper.test.ts | 6 ------ www/js/metrics/CarbonFootprintCard.tsx | 6 ------ www/js/metrics/footprintHelper.ts | 14 +++----------- 4 files changed, 3 insertions(+), 25 deletions(-) diff --git a/www/__tests__/customMetricsHelper.test.ts b/www/__tests__/customMetricsHelper.test.ts index 0a45f739a..251c1d977 100644 --- a/www/__tests__/customMetricsHelper.test.ts +++ b/www/__tests__/customMetricsHelper.test.ts @@ -8,7 +8,6 @@ import { setUseCustomMET } from '../js/metrics/metHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; import fakeLabels from '../__mocks__/fakeLabels.json'; -import { setUseCustomFootprint } from '../js/metrics/footprintHelper'; mockBEMUserCache(); mockLogger(); @@ -42,7 +41,6 @@ it('gets the custom mets', async () => { it('gets the custom footprint', async () => { initCustomDatasetHelper(getConfig()); - setUseCustomFootprint(true); await new Promise((r) => setTimeout(r, 800)); expect(getCustomFootprint()).toMatchObject({ walk: {}, diff --git a/www/__tests__/footprintHelper.test.ts b/www/__tests__/footprintHelper.test.ts index 5360b7a39..3f6883bbe 100644 --- a/www/__tests__/footprintHelper.test.ts +++ b/www/__tests__/footprintHelper.test.ts @@ -4,7 +4,6 @@ import { getFootprintForMetrics, getHighestFootprint, getHighestFootprintForDistance, - setUseCustomFootprint, } from '../js/metrics/footprintHelper'; import { getConfig } from '../js/config/dynamicConfig'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; @@ -28,7 +27,6 @@ global.fetch = (url: string) => }) as any; beforeEach(() => { - setUseCustomFootprint(false); clearHighestFootprint(); }); @@ -42,28 +40,24 @@ const custom_metrics = [ it('gets footprint for metrics (custom, fallback 0)', async () => { initCustomDatasetHelper(getConfig()); - setUseCustomFootprint(true); await new Promise((r) => setTimeout(r, 500)); expect(getFootprintForMetrics(custom_metrics, 0)).toBe(2.4266); }); it('gets footprint for metrics (custom, fallback 0.1)', async () => { initCustomDatasetHelper(getConfig()); - setUseCustomFootprint(true); await new Promise((r) => setTimeout(r, 500)); expect(getFootprintForMetrics(custom_metrics, 0.1)).toBe(2.4266 + 0.5); }); it('gets the highest footprint from the dataset, custom', async () => { initCustomDatasetHelper(getConfig()); - setUseCustomFootprint(true); await new Promise((r) => setTimeout(r, 500)); expect(getHighestFootprint()).toBe(0.30741); }); it('gets the highest footprint for distance, custom', async () => { initCustomDatasetHelper(getConfig()); - setUseCustomFootprint(true); await new Promise((r) => setTimeout(r, 500)); expect(getHighestFootprintForDistance(12345)).toBe(0.30741 * (12345 / 1000)); }); diff --git a/www/js/metrics/CarbonFootprintCard.tsx b/www/js/metrics/CarbonFootprintCard.tsx index 30c265bfc..835f20a22 100644 --- a/www/js/metrics/CarbonFootprintCard.tsx +++ b/www/js/metrics/CarbonFootprintCard.tsx @@ -5,7 +5,6 @@ import { MetricsData } from './metricsTypes'; import { cardStyles } from './MetricsTab'; import { getFootprintForMetrics, - setUseCustomFootprint, getHighestFootprint, getHighestFootprintForDistance, } from './footprintHelper'; @@ -53,11 +52,6 @@ const CarbonFootprintCard = ({ userMetrics, aggMetrics }: Props) => { //setting up data to be displayed let graphRecords = []; - //set custon dataset, if the labels are custom - if (isCustomLabels(userThisWeekModeMap)) { - setUseCustomFootprint(true); - } - //calculate low-high and format range for prev week, if exists (14 days ago -> 8 days ago) let userPrevWeek; if (userLastWeekSummaryMap[0]) { diff --git a/www/js/metrics/footprintHelper.ts b/www/js/metrics/footprintHelper.ts index e5a615c4a..8b3a5de9e 100644 --- a/www/js/metrics/footprintHelper.ts +++ b/www/js/metrics/footprintHelper.ts @@ -3,7 +3,6 @@ import { getCustomFootprint } from './customMetricsHelper'; //variables for the highest footprint in the set and if using custom let highestFootprint = 0; -let useCustom = false; /** * @function converts meters to kilometers @@ -14,14 +13,6 @@ const mtokm = function (v) { return v / 1000; }; -/** - * @function sets the value of useCustom - * @param {boolean} val if using custom footprint - */ -export const setUseCustomFootprint = function (val: boolean) { - useCustom = val; -}; - /** * @function clears the stored highest footprint */ @@ -37,8 +28,9 @@ export const clearHighestFootprint = function () { * @returns the footprint or undefined */ const getFootprint = function () { - if (useCustom == true) { - return getCustomFootprint(); + let footprint = getCustomFootprint(); + if (footprint) { + return footprint; } else { displayErrorMsg('failed to use custom labels', 'Error in Footprint Calculatons'); return undefined; From 4c1a4a6a3d9a00d0c90e0f554fb29807cb8bdd28 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 14:19:02 -0700 Subject: [PATCH 074/112] rename the metDatasets file even though its an acronym, it is less confusing to have the name be lowercase since it is not a component --- www/js/metrics/{METDataset.ts => metDatasets.ts} | 0 www/js/metrics/metHelper.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename www/js/metrics/{METDataset.ts => metDatasets.ts} (100%) diff --git a/www/js/metrics/METDataset.ts b/www/js/metrics/metDatasets.ts similarity index 100% rename from www/js/metrics/METDataset.ts rename to www/js/metrics/metDatasets.ts diff --git a/www/js/metrics/metHelper.ts b/www/js/metrics/metHelper.ts index c5ea7554e..d3522e152 100644 --- a/www/js/metrics/metHelper.ts +++ b/www/js/metrics/metHelper.ts @@ -1,5 +1,5 @@ import { getCustomMETs } from './customMetricsHelper'; -import { standardMETs } from './metDataset'; +import { standardMETs } from './metDatasets'; let useCustom = false; From b655f4a17ea4d2630ade33af0375ce5672082896 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 14:23:53 -0700 Subject: [PATCH 075/112] name updates, just one dataset --- www/js/metrics/{metDatasets.ts => metDataset.ts} | 0 www/js/metrics/metHelper.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename www/js/metrics/{metDatasets.ts => metDataset.ts} (100%) diff --git a/www/js/metrics/metDatasets.ts b/www/js/metrics/metDataset.ts similarity index 100% rename from www/js/metrics/metDatasets.ts rename to www/js/metrics/metDataset.ts diff --git a/www/js/metrics/metHelper.ts b/www/js/metrics/metHelper.ts index d3522e152..c5ea7554e 100644 --- a/www/js/metrics/metHelper.ts +++ b/www/js/metrics/metHelper.ts @@ -1,5 +1,5 @@ import { getCustomMETs } from './customMetricsHelper'; -import { standardMETs } from './metDatasets'; +import { standardMETs } from './metDataset'; let useCustom = false; From dec322fc7262408d7814059a0b77cbd6d8ccc6e9 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 14:24:06 -0700 Subject: [PATCH 076/112] prettier in Chart.tsx --- www/js/components/Chart.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/www/js/components/Chart.tsx b/www/js/components/Chart.tsx index 372d7e6c8..257eb3cf6 100644 --- a/www/js/components/Chart.tsx +++ b/www/js/components/Chart.tsx @@ -139,7 +139,8 @@ const Chart = ({ chartDatasets = ${JSON.stringify(chartDatasets)}; chartDatasets[0].data = ${JSON.stringify(chartDatasets[0].data)}`); //account for different data possiblities - const label = chartDatasets[0].data[i]?.y || chartDatasets[i].data[0]?.y + const label = + chartDatasets[0].data[i]?.y || chartDatasets[i].data[0]?.y; if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); return label; @@ -177,7 +178,8 @@ const Chart = ({ chartDatasets = ${JSON.stringify(chartDatasets)}; chartDatasets[0].data = ${JSON.stringify(chartDatasets[0].data)}`); //account for different data possiblities - one mode per week, one mode both weeks, mixed weeks - const label = chartDatasets[0].data[i]?.x || chartDatasets[i].data[0]?.x + const label = + chartDatasets[0].data[i]?.x || chartDatasets[i].data[0]?.x; if (typeof label == 'string' && label.includes('\n')) return label.split('\n'); return label; From a29957459dcb34224c0124998f0818350d2ef72c Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 14:40:33 -0700 Subject: [PATCH 077/112] remove debugging log statements --- www/js/survey/multilabel/confirmHelper.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/www/js/survey/multilabel/confirmHelper.ts b/www/js/survey/multilabel/confirmHelper.ts index 7fffa54cb..f032f2f5a 100644 --- a/www/js/survey/multilabel/confirmHelper.ts +++ b/www/js/survey/multilabel/confirmHelper.ts @@ -37,7 +37,6 @@ export let inputDetails: InputDetails; export async function getLabelOptions(appConfigParam?) { if (appConfigParam) appConfig = appConfigParam; if (labelOptions) return labelOptions; - console.log('in get label options', appConfig); if (appConfig.label_options) { const labelOptionsJson = await fetchUrlCached(appConfig.label_options); logDebug( @@ -50,7 +49,6 @@ export async function getLabelOptions(appConfigParam?) { 'No label_options found in config, using default label options at ' + defaultLabelOptionsURL, ); const defaultLabelOptionsJson = await fetchUrlCached(defaultLabelOptionsURL); - console.log('label options', defaultLabelOptionsJson); labelOptions = JSON.parse(defaultLabelOptionsJson) as LabelOptions; } /* fill in the translations to the 'text' fields of the labelOptions, From 6272fb0d11d6e9e1558798f3cc2a7e865b6c17ff Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 14:44:13 -0700 Subject: [PATCH 078/112] logDebug statements logDebug/logWarn instead of the console equivalents --- www/js/metrics/metHelper.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/www/js/metrics/metHelper.ts b/www/js/metrics/metHelper.ts index c5ea7554e..377352a9b 100644 --- a/www/js/metrics/metHelper.ts +++ b/www/js/metrics/metHelper.ts @@ -1,3 +1,4 @@ +import { logDebug, logWarn } from '../plugin/logger'; import { getCustomMETs } from './customMetricsHelper'; import { standardMETs } from './metDataset'; @@ -52,19 +53,19 @@ const mpstomph = function (mps) { */ export const getMet = function (mode, speed, defaultIfMissing) { if (mode == 'ON_FOOT') { - console.log("getMet() converted 'ON_FOOT' to 'WALKING'"); + logDebug("getMet() converted 'ON_FOOT' to 'WALKING'"); mode = 'WALKING'; } let currentMETs = getMETs(); if (!currentMETs[mode]) { - console.warn('getMet() Illegal mode: ' + mode); + logWarn('getMet() Illegal mode: ' + mode); return defaultIfMissing; //So the calorie sum does not break with wrong return type } for (var i in currentMETs[mode]) { if (between(mpstomph(speed), currentMETs[mode][i].range[0], currentMETs[mode][i].range[1])) { return currentMETs[mode][i].mets; } else if (mpstomph(speed) < 0) { - console.log('getMet() Negative speed: ' + mpstomph(speed)); + logWarn('getMet() Negative speed: ' + mpstomph(speed)); return 0; } } From 6233af5b96a356d0cc50d00dff491bb67c8e781a Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 14:50:00 -0700 Subject: [PATCH 079/112] logWarn instead of console.warn logWarn in footprintHelper --- www/js/metrics/footprintHelper.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/www/js/metrics/footprintHelper.ts b/www/js/metrics/footprintHelper.ts index 8b3a5de9e..b42f5364a 100644 --- a/www/js/metrics/footprintHelper.ts +++ b/www/js/metrics/footprintHelper.ts @@ -1,4 +1,4 @@ -import { displayErrorMsg, logDebug } from '../plugin/logger'; +import { displayErrorMsg, logDebug, logWarn } from '../plugin/logger'; import { getCustomFootprint } from './customMetricsHelper'; //variables for the highest footprint in the set and if using custom @@ -67,11 +67,10 @@ export const getFootprintForMetrics = function (userMetrics, defaultIfMissing = 6) * mtokm(userMetrics[i].values); } else { - console.warn( - 'WARNING getFootprintFromMetrics() was requested for an unknown mode: ' + - mode + - ' metrics JSON: ' + - JSON.stringify(userMetrics), + logWarn( + `WARNING getFootprintFromMetrics() was requested for an unknown mode: ${mode} metrics JSON: ${JSON.stringify( + userMetrics, + )}`, ); result += defaultIfMissing * mtokm(userMetrics[i].values); } From 5918b6be7ac3ab8497706b1e2e1135fb74efb542 Mon Sep 17 00:00:00 2001 From: Abby Wheelis <54848919+Abby-Wheelis@users.noreply.github.com> Date: Fri, 17 Nov 2023 14:55:47 -0700 Subject: [PATCH 080/112] Rename CustomMetricsHelper.ts to customMetricsHelper.ts --- www/js/metrics/{CustomMetricsHelper.ts => customMetricsHelper.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename www/js/metrics/{CustomMetricsHelper.ts => customMetricsHelper.ts} (100%) diff --git a/www/js/metrics/CustomMetricsHelper.ts b/www/js/metrics/customMetricsHelper.ts similarity index 100% rename from www/js/metrics/CustomMetricsHelper.ts rename to www/js/metrics/customMetricsHelper.ts From 125faffa88d166f6123599c0774a803cf1b85a13 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 15:06:57 -0700 Subject: [PATCH 081/112] cleaning up customMetrics making sure the log statements here are thoughtful, renaming the input params to label options in the initialization code --- www/js/metrics/customMetricsHelper.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/www/js/metrics/customMetricsHelper.ts b/www/js/metrics/customMetricsHelper.ts index 3b479f8a2..3a2f551ca 100644 --- a/www/js/metrics/customMetricsHelper.ts +++ b/www/js/metrics/customMetricsHelper.ts @@ -1,6 +1,6 @@ import angular from 'angular'; import { getLabelOptions } from '../survey/multilabel/confirmHelper'; -import { displayError, displayErrorMsg, logDebug } from '../plugin/logger'; +import { displayError, displayErrorMsg, logDebug, logWarn } from '../plugin/logger'; import { standardMETs } from './metDataset'; //variables to store values locally @@ -52,9 +52,7 @@ const populateCustomMETs = function () { } return [opt.value, currMET]; } else { - console.warn( - 'Did not find either met_equivalent or met for ' + opt.value + ' ignoring entry', - ); + logWarn(`Did not find either met_equivalent or met for ${opt.value} ignoring entry`); return undefined; } } @@ -79,7 +77,7 @@ const populateCustomFootprints = function () { ); } _range_limited_motorized = opt; - console.log('Found range limited motorized mode', _range_limited_motorized); + logDebug(`Found range limited motorized mode - ${_range_limited_motorized}`); } if (angular.isDefined(opt.kgCo2PerKm)) { return [opt.value, opt.kgCo2PerKm]; @@ -100,9 +98,9 @@ const populateCustomFootprints = function () { export const initCustomDatasetHelper = async function (newConfig) { try { logDebug('initializing custom datasets with config' + newConfig); - getLabelOptions(newConfig).then((inputParams) => { - console.log('Input params = ', inputParams); - _labelOptions = inputParams; + getLabelOptions(newConfig).then((labelOptions) => { + console.log('In custom metrics, label options: ', labelOptions); + _labelOptions = labelOptions; populateCustomMETs(); populateCustomFootprints(); }); From e626f6e36c5b5cdadfb16968f03204cf024b2066 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 15:42:02 -0700 Subject: [PATCH 082/112] clarify "in vehicle" calculations The divisor being on it's own line was confusing, so now we are adding the vehicle modes and then dividing this should make the fact that it's an average more clear, and is one line shorter! --- www/js/metrics/footprintHelper.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/www/js/metrics/footprintHelper.ts b/www/js/metrics/footprintHelper.ts index b42f5364a..6e4bd46d9 100644 --- a/www/js/metrics/footprintHelper.ts +++ b/www/js/metrics/footprintHelper.ts @@ -57,15 +57,14 @@ export const getFootprintForMetrics = function (userMetrics, defaultIfMissing = if (mode in footprint) { result += footprint[mode] * mtokm(userMetrics[i].values); } else if (mode == 'IN_VEHICLE') { - result += - ((footprint['CAR'] + - footprint['BUS'] + - footprint['LIGHT_RAIL'] + - footprint['TRAIN'] + - footprint['TRAM'] + - footprint['SUBWAY']) / - 6) * - mtokm(userMetrics[i].values); + const sum = + footprint['CAR'] + + footprint['BUS'] + + footprint['LIGHT_RAIL'] + + footprint['TRAIN'] + + footprint['TRAM'] + + footprint['SUBWAY']; + result += (sum / 6) * mtokm(userMetrics[i].values); } else { logWarn( `WARNING getFootprintFromMetrics() was requested for an unknown mode: ${mode} metrics JSON: ${JSON.stringify( From b040691e4db8270c1a2ad7a45104fefa0a4630ab Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 15:54:27 -0700 Subject: [PATCH 083/112] all mets are custom similar to the footprint, we will always use the custom METs, since the labels are custom (either deployer specified or our sample set) The custom mets are set up in the initialization of the custom dataset helper if the custom mets return undefined, will fall back to the standard mets --- www/__tests__/customMetricsHelper.test.ts | 2 -- www/__tests__/metHelper.test.ts | 7 +------ www/js/metrics/metHelper.ts | 15 +++------------ 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/www/__tests__/customMetricsHelper.test.ts b/www/__tests__/customMetricsHelper.test.ts index 251c1d977..173fbc97f 100644 --- a/www/__tests__/customMetricsHelper.test.ts +++ b/www/__tests__/customMetricsHelper.test.ts @@ -4,7 +4,6 @@ import { getCustomMETs, initCustomDatasetHelper, } from '../js/metrics/customMetricsHelper'; -import { setUseCustomMET } from '../js/metrics/metHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; import fakeLabels from '../__mocks__/fakeLabels.json'; @@ -27,7 +26,6 @@ global.fetch = (url: string) => it('gets the custom mets', async () => { initCustomDatasetHelper(getConfig()); - setUseCustomMET(true); await new Promise((r) => setTimeout(r, 800)); expect(getCustomMETs()).toMatchObject({ walk: {}, diff --git a/www/__tests__/metHelper.test.ts b/www/__tests__/metHelper.test.ts index ea36ec87f..a06034a61 100644 --- a/www/__tests__/metHelper.test.ts +++ b/www/__tests__/metHelper.test.ts @@ -1,4 +1,4 @@ -import { getMet, setUseCustomMET } from '../js/metrics/metHelper'; +import { getMet } from '../js/metrics/metHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; import fakeLabels from '../__mocks__/fakeLabels.json'; @@ -21,10 +21,6 @@ global.fetch = (url: string) => ); }) as any; -beforeEach(() => { - setUseCustomMET(false); -}); - it('gets met for mode and speed', () => { expect(getMet('WALKING', 1.47523, 0)).toBe(4.3); expect(getMet('BICYCLING', 4.5, 0)).toBe(6.8); @@ -34,7 +30,6 @@ it('gets met for mode and speed', () => { it('gets custom met for mode and speed', async () => { initCustomDatasetHelper(getConfig()); - setUseCustomMET(true); await new Promise((r) => setTimeout(r, 500)); expect(getMet('walk', 1.47523, 0)).toBe(4.3); expect(getMet('bike', 4.5, 0)).toBe(6.8); diff --git a/www/js/metrics/metHelper.ts b/www/js/metrics/metHelper.ts index 377352a9b..28765d193 100644 --- a/www/js/metrics/metHelper.ts +++ b/www/js/metrics/metHelper.ts @@ -2,23 +2,14 @@ import { logDebug, logWarn } from '../plugin/logger'; import { getCustomMETs } from './customMetricsHelper'; import { standardMETs } from './metDataset'; -let useCustom = false; - -/** - * @function sets boolean to use custom mets - * @param {boolean} val - */ -export const setUseCustomMET = function (val: boolean) { - useCustom = val; -}; - /** * @function gets the METs object * @returns {object} mets either custom or standard */ const getMETs = function () { - if (useCustom == true) { - return getCustomMETs(); + let custom_mets = getCustomMETs(); + if (custom_mets) { + return custom_mets; } else { return standardMETs; } From 8f7c6748ed161e248e247cc04514dacc46908317 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 16:45:29 -0700 Subject: [PATCH 084/112] add a test case we also want to test that the timestamps follow the "minute" accuracy convention we implemented, so that if the survey and timelineEntry match within the minute, we use the timelineEntry timestamps, else use the timestamps from the survey https://github.com/e-mission/e-mission-phone/pull/1063#discussion_r1397710929 --- www/__tests__/enketoHelper.test.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index a8f49b29c..f4cfeb8ee 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -86,14 +86,22 @@ it('resolves the timestamps', () => { ' 2016-08-28 2016-07-25 17:32:32.928-06:00 17:30:31.000-06:00 '; const badTimeDoc = xmlParser.parseFromString(badTimes, 'text/xml'); expect(resolveTimestamps(badTimeDoc, timelineEntry)).toBeUndefined(); - //good info returns unix start and end timestamps -- TODO : address precise vs less precise? - const timeSurvey = + //if within a minute, timelineEntry timestamps + const timeEntry = ' 2016-07-25 2016-07-25 17:24:32.928-06:00 17:30:31.000-06:00 '; - const xmlDoc = xmlParser.parseFromString(timeSurvey, 'text/xml'); - expect(resolveTimestamps(xmlDoc, timelineEntry)).toMatchObject({ + const xmlDoc1 = xmlParser.parseFromString(timeEntry, 'text/xml'); + expect(resolveTimestamps(xmlDoc1, timelineEntry)).toMatchObject({ start_ts: 1469492672.928242, end_ts: 1469493031, }); + // else survey timestamps + const timeSurvey = + ' 2016-07-25 2016-07-25 17:22:33.928-06:00 17:33:33.000-06:00 '; + const xmlDoc2 = xmlParser.parseFromString(timeSurvey, 'text/xml'); + expect(resolveTimestamps(xmlDoc2, timelineEntry)).toMatchObject({ + start_ts: 1469492553.928, + end_ts: 1469493213, + }); }); //resolve label From 7ec575e41701725723609c9eb40654ef8d05385b Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 17:02:39 -0700 Subject: [PATCH 085/112] note the issue this form is invalide because of the start and end times mismatching --- www/__tests__/enketoHelper.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index f4cfeb8ee..e06dc94a6 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -136,6 +136,7 @@ it('gets the saved result or throws an error', () => { return '2023-10-13T15:05:48.890-06:002023-10-13T15:05:48.892-06:002016-07-2517:24:32.928-06:002016-07-2517:30:31.000-06:00personal_care_activitiesdoing_sportuuid:dc16c287-08b2-4435-95aa-e4d7838b4225'; }, }; + //the start time listed is after the end time listed const badForm = { getDataStr: () => { return '2023-10-13T15:05:48.890-06:002023-10-13T15:05:48.892-06:002016-08-2517:24:32.928-06:002016-07-2517:30:31.000-06:00personal_care_activitiesdoing_sportuuid:dc16c287-08b2-4435-95aa-e4d7838b4225'; From 8b3ab76327d93011e2a31cfa6409bcb23f710e1f Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 17:07:22 -0700 Subject: [PATCH 086/112] check for key before resolving with this information --- www/__mocks__/cordovaMocks.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index f08293a85..4911b3ebe 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -133,11 +133,15 @@ export const mockBEMUserCache = () => { return { key: 'write_ts', startTs: 0, endTs: Date.now() / 1000 }; }, getSensorDataForInterval: (key, tq, withMetadata) => { - return new Promise((rs, rj) => - setTimeout(() => { - rs({ metadata: { write_ts: '1699897723' }, data: 'completed', time: '01/01/2001' }); - }, 100), - ); + if (key == `manual/demographic_survey`) { + return new Promise((rs, rj) => + setTimeout(() => { + rs({ metadata: { write_ts: '1699897723' }, data: 'completed', time: '01/01/2001' }); + }, 100), + ); + } else { + return undefined; + } }, }; window['cordova'] ||= {}; From d123b4f23f90537671e13e59ff672f7156e655c2 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 17:43:34 -0700 Subject: [PATCH 087/112] remove _lazyLoadConfig, move types the function was not all that necessary, so I removed it Also moved my survey config type into the appropriate place https://github.com/e-mission/e-mission-phone/pull/1063#discussion_r1397747847 --- www/__tests__/enketoHelper.test.ts | 6 ++--- www/js/survey/enketo/enketoHelper.ts | 36 +++++++--------------------- www/js/types/appConfigTypes.ts | 22 +++++++++++++++++ 3 files changed, 33 insertions(+), 31 deletions(-) create mode 100644 www/js/types/appConfigTypes.ts diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index e06dc94a6..0f2982318 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -3,12 +3,12 @@ import { filterByNameAndVersion, resolveTimestamps, resolveLabel, - _lazyLoadConfig, loadPreviousResponseForSurvey, saveResponse, } from '../js/survey/enketo/enketoHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; +import { getConfig } from '../../www/js/config/dynamicConfig'; import initializedI18next from '../js/i18nextInit'; window['i18next'] = initializedI18next; @@ -22,7 +22,7 @@ global.Blob = require('node:buffer').Blob; it('gets the survey config', async () => { //this is aimed at testing my mock of the config //mocked getDocument for the case of getting the config - let config = await _lazyLoadConfig(); + let config = await getConfig(); let mockSurveys = { TimeUseSurvey: { compatibleWith: 1, @@ -39,7 +39,7 @@ it('gets the survey config', async () => { version: 9, }, }; - expect(config).toMatchObject(mockSurveys); + expect(config.survey_info.surveys).toMatchObject(mockSurveys); }); it('gets the model response, if avaliable, or returns null', () => { diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 379120373..cb5e0bac0 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -8,6 +8,7 @@ import { getConfig } from '../../config/dynamicConfig'; import { DateTime } from 'luxon'; import { fetchUrlCached } from '../../services/commHelper'; import { getUnifiedDataForInterval } from '../../services/unifiedDataLoader'; +import { EnketoSurveyConfig } from '../../types/appConfigTypes'; export type PrefillFields = { [key: string]: string }; @@ -34,19 +35,10 @@ type EnketoResponse = { metadata: any; }; -type EnketoSurveyConfig = { - [surveyName: string]: { - formPath: string; - labelTemplate: { [lang: string]: string }; - labelVars: { [activity: string]: { [key: string]: string; type: string } }; - version: number; - compatibleWith: number; - }; -}; - const LABEL_FUNCTIONS = { UseLabelTemplate: async (xmlDoc: XMLDocument, name: string) => { - let configSurveys = await _lazyLoadConfig(); + let appConfig = await getConfig(); + const configSurveys = appConfig.survey_info.surveys; const config = configSurveys[name]; // config for this survey const lang = i18next.resolvedLanguage; @@ -94,21 +86,6 @@ function _getAnswerByTagName(xmlDoc: XMLDocument, tagName: string) { /** @type {EnketoSurveyConfig} _config */ let _config: EnketoSurveyConfig; -/** - * _lazyLoadConfig load enketo survey config. If already loaded, return the cached config - * @returns {Promise} enketo survey config - */ -export function _lazyLoadConfig() { - if (_config !== undefined) { - return Promise.resolve(_config); - } - return getConfig().then((newConfig) => { - logInfo('Resolved UI_CONFIG_READY promise in enketoHelper, filling in templates'); - _config = newConfig.survey_info.surveys; - return _config; - }); -} - /** * filterByNameAndVersion filter the survey responses by survey name and their version. * The version for filtering is specified in enketo survey `compatibleWith` config. @@ -119,8 +96,11 @@ export function _lazyLoadConfig() { * @return {Promise} filtered survey responses */ export function filterByNameAndVersion(name: string, responses: EnketoResponse[]) { - return _lazyLoadConfig().then((config) => - responses.filter((r) => r.data.name === name && r.data.version >= config[name].compatibleWith), + return getConfig().then((config) => + responses.filter( + (r) => + r.data.name === name && r.data.version >= config.survey_info.surveys[name].compatibleWith, + ), ); } diff --git a/www/js/types/appConfigTypes.ts b/www/js/types/appConfigTypes.ts new file mode 100644 index 000000000..f55b27bc0 --- /dev/null +++ b/www/js/types/appConfigTypes.ts @@ -0,0 +1,22 @@ +// WIP: type definitions for the 'dynamic config' spec +// examples of configs: https://github.com/e-mission/nrel-openpath-deploy-configs/tree/main/configs + +export type AppConfig = { + server: ServerConnConfig; + [k: string]: any; // TODO fill in all the other fields +}; + +export type ServerConnConfig = { + connectUrl: `https://${string}`; + aggregate_call_auth: 'no_auth' | 'user_only' | 'never'; +}; + +export type EnketoSurveyConfig = { + [surveyName: string]: { + formPath: string; + labelTemplate: { [lang: string]: string }; + labelVars: { [activity: string]: { [key: string]: string; type: string } }; + version: number; + compatibleWith: number; + }; +}; From 100eefb76dd7bd118be1f78b8dd003a11c924ac2 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 17:54:37 -0700 Subject: [PATCH 088/112] test the version adding a response that should get filtered out because the version is too low https://github.com/e-mission/e-mission-phone/pull/1063#discussion_r1397717837 --- www/__tests__/enketoHelper.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index 0f2982318..e81e1e15b 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -247,6 +247,18 @@ it('filters the survey responses by their name and version', () => { }, metadata: {}, }, + { + data: { + label: 'Activity', //display label (this value is use for displaying on the button) + ts: '100000000', //the timestamp at which the survey was filled out (in seconds) + fmt_time: '12:39', //the formatted timestamp at which the survey was filled out + name: 'TimeUseSurvey', //survey name + version: '0.5', //survey version + xmlResponse: '', //survey response XML string + jsonDocResponse: 'this is my json object', //survey response JSON object + }, + metadata: {}, + }, ]; //several responses -> only the one that has a name match From 22decbfd0b58776450737ba536dc9bf42e375aa5 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 18:15:34 -0700 Subject: [PATCH 089/112] make config a parameter to mockBEMUserCache --- www/__mocks__/cordovaMocks.ts | 8 +++----- www/__tests__/clientStats.test.ts | 3 ++- www/__tests__/enketoHelper.test.ts | 3 ++- www/__tests__/pushNotifySettings.test.ts | 3 ++- www/__tests__/remoteNotifyHandler.test.ts | 3 ++- www/__tests__/startprefs.test.ts | 3 ++- www/__tests__/storage.test.ts | 3 ++- www/__tests__/storeDeviceSettings.test.ts | 3 ++- 8 files changed, 17 insertions(+), 12 deletions(-) diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 4911b3ebe..e8b680965 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -35,7 +35,7 @@ export const mockFile = () => { //for consent document const _storage = {}; -export const mockBEMUserCache = () => { +export const mockBEMUserCache = (config) => { const _cache = {}; const messages = []; const mockBEMUserCache = { @@ -101,13 +101,11 @@ export const mockBEMUserCache = () => { ); }, getDocument: (key: string, withMetadata?: boolean) => { - // this was mocked specifically for enketoHelper's use, could be expanded if needed - const fakeSurveyConfig = fakeConfig; - + //returns the config provided as a paramenter to this mock! if (key == 'config/app_ui_config') { return new Promise((rs, rj) => setTimeout(() => { - rs(fakeSurveyConfig); + rs(config); }, 100), ); } else { diff --git a/www/__tests__/clientStats.test.ts b/www/__tests__/clientStats.test.ts index a3a953582..a508550e5 100644 --- a/www/__tests__/clientStats.test.ts +++ b/www/__tests__/clientStats.test.ts @@ -6,12 +6,13 @@ import { getAppVersion, statKeys, } from '../js/plugin/clientStats'; +import fakeConfig from '../__mocks__/fakeConfig.json'; mockDevice(); // this mocks cordova-plugin-app-version, generating a "Mock App", version "1.2.3" mockGetAppVersion(); // clientStats.ts uses BEMUserCache to store the stats, so we need to mock that too -mockBEMUserCache(); +mockBEMUserCache(fakeConfig); const db = window['cordova']?.plugins?.BEMUserCache; it('gets the app version', async () => { diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index e81e1e15b..3b5a95f3d 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -9,11 +9,12 @@ import { import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; import { getConfig } from '../../www/js/config/dynamicConfig'; +import fakeConfig from '../__mocks__/fakeConfig.json'; import initializedI18next from '../js/i18nextInit'; window['i18next'] = initializedI18next; -mockBEMUserCache(); +mockBEMUserCache(fakeConfig); mockLogger(); global.URL = require('url').URL; diff --git a/www/__tests__/pushNotifySettings.test.ts b/www/__tests__/pushNotifySettings.test.ts index d452aa819..9e6e25bb5 100644 --- a/www/__tests__/pushNotifySettings.test.ts +++ b/www/__tests__/pushNotifySettings.test.ts @@ -11,11 +11,12 @@ import { mockPushNotification, getCalled, } from '../__mocks__/pushNotificationMocks'; +import fakeConfig from '../__mocks__/fakeConfig.json'; mockCordova(); mockLogger(); mockPushNotification(); -mockBEMUserCache(); +mockBEMUserCache(fakeConfig); mockBEMDataCollection(); global.fetch = (url: string) => diff --git a/www/__tests__/remoteNotifyHandler.test.ts b/www/__tests__/remoteNotifyHandler.test.ts index 320877c6b..6fe0a73fe 100644 --- a/www/__tests__/remoteNotifyHandler.test.ts +++ b/www/__tests__/remoteNotifyHandler.test.ts @@ -9,10 +9,11 @@ import { mockInAppBrowser, } from '../__mocks__/cordovaMocks'; import { clearAlerts, getAlerts, mockAlert, mockLogger } from '../__mocks__/globalMocks'; +import fakeConfig from '../__mocks__/fakeConfig.json'; mockLogger(); mockDevice(); -mockBEMUserCache(); +mockBEMUserCache(fakeConfig); mockGetAppVersion(); mockInAppBrowser(); mockAlert(); diff --git a/www/__tests__/startprefs.test.ts b/www/__tests__/startprefs.test.ts index 75ed707dc..17b44a4be 100644 --- a/www/__tests__/startprefs.test.ts +++ b/www/__tests__/startprefs.test.ts @@ -7,8 +7,9 @@ import { import { mockBEMUserCache, mockBEMDataCollection } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; +import fakeConfig from '../__mocks__/fakeConfig.json'; -mockBEMUserCache(); +mockBEMUserCache(fakeConfig); mockBEMDataCollection(); mockLogger(); diff --git a/www/__tests__/storage.test.ts b/www/__tests__/storage.test.ts index ca6d71dec..bbfa9c410 100644 --- a/www/__tests__/storage.test.ts +++ b/www/__tests__/storage.test.ts @@ -1,11 +1,12 @@ import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; import { storageClear, storageGet, storageRemove, storageSet } from '../js/plugin/storage'; +import fakeConfig from '../__mocks__/fakeConfig.json'; // mocks used - storage.ts uses BEMUserCache and logging. // localStorage is already mocked for us by Jest :) mockLogger(); -mockBEMUserCache(); +mockBEMUserCache(fakeConfig); it('stores a value and retrieves it back', async () => { await storageSet('test1', 'test value 1'); diff --git a/www/__tests__/storeDeviceSettings.test.ts b/www/__tests__/storeDeviceSettings.test.ts index 4bccbc0af..41c36eb16 100644 --- a/www/__tests__/storeDeviceSettings.test.ts +++ b/www/__tests__/storeDeviceSettings.test.ts @@ -13,8 +13,9 @@ import { import { mockLogger } from '../__mocks__/globalMocks'; import { EVENTS, publish } from '../js/customEventHandler'; import { markIntroDone } from '../js/onboarding/onboardingHelper'; +import fakeConfig from '../__mocks__/fakeConfig.json'; -mockBEMUserCache(); +mockBEMUserCache(fakeConfig); mockDevice(); mockCordova(); mockLogger(); From 80aa5ea18081849819236d65dc5d07ea583aa8e8 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Fri, 17 Nov 2023 18:42:33 -0700 Subject: [PATCH 090/112] more test cases!! Because of the new parameter, I was able to add more test cases to resolve Labels I needed to also clear the locally stored config out of dynamicConfig.ts --- www/__tests__/enketoHelper.test.ts | 81 ++++++++++++++++++++++++++++-- www/js/config/dynamicConfig.ts | 5 ++ 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/www/__tests__/enketoHelper.test.ts b/www/__tests__/enketoHelper.test.ts index 3b5a95f3d..c4fda7dc4 100644 --- a/www/__tests__/enketoHelper.test.ts +++ b/www/__tests__/enketoHelper.test.ts @@ -8,7 +8,7 @@ import { } from '../js/survey/enketo/enketoHelper'; import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; -import { getConfig } from '../../www/js/config/dynamicConfig'; +import { getConfig, resetStoredConfig } from '../../www/js/config/dynamicConfig'; import fakeConfig from '../__mocks__/fakeConfig.json'; import initializedI18next from '../js/i18nextInit'; @@ -20,6 +20,10 @@ mockLogger(); global.URL = require('url').URL; global.Blob = require('node:buffer').Blob; +beforeEach(() => { + resetStoredConfig(); +}); + it('gets the survey config', async () => { //this is aimed at testing my mock of the config //mocked getDocument for the case of getting the config @@ -106,7 +110,7 @@ it('resolves the timestamps', () => { }); //resolve label -it('resolves the label', async () => { +it('resolves the label, normal case', async () => { const xmlParser = new window.DOMParser(); const xmlString = ' option_1 '; const xmlDoc = xmlParser.parseFromString(xmlString, 'text/html'); @@ -114,14 +118,83 @@ it('resolves the label', async () => { ' option_1 option_3 '; const xmlDoc2 = xmlParser.parseFromString(xmlString2, 'text/xml'); - //if no template, returns "Answered" TODO: find a way to engineer this case - //if no labelVars, returns template TODO: find a way to engineer this case //have a custom survey label function TODO: we currently don't have custome label functions, but should test when we do //no custom function, fallback to UseLabelTemplate (standard case) + mockBEMUserCache(fakeConfig); expect(await resolveLabel('TimeUseSurvey', xmlDoc)).toBe('3 Domestic'); expect(await resolveLabel('TimeUseSurvey', xmlDoc2)).toBe('3 Employment/Education, 3 Domestic'); }); +it('resolves the label, if no template, returns "Answered"', async () => { + const xmlParser = new window.DOMParser(); + const xmlString = ' option_1 '; + const xmlDoc = xmlParser.parseFromString(xmlString, 'text/html'); + const xmlString2 = + ' option_1 option_3 '; + const xmlDoc2 = xmlParser.parseFromString(xmlString2, 'text/xml'); + + const noTemplate = { + 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', + labelVars: { + da: { + key: 'Domestic_activities', + type: 'length', + }, + erea: { + key: 'Employment_related_a_Education_activities', + type: 'length', + }, + }, + version: 9, + }, + }, + 'trip-labels': 'ENKETO', + }, + }; + mockBEMUserCache(noTemplate); + expect(await resolveLabel('TimeUseSurvey', xmlDoc)).toBe('Answered'); + expect(await resolveLabel('TimeUseSurvey', xmlDoc2)).toBe('Answered'); +}); + +it('resolves the label, if no labelVars, returns template', async () => { + const xmlParser = new window.DOMParser(); + const xmlString = ' option_1 '; + const xmlDoc = xmlParser.parseFromString(xmlString, 'text/html'); + const xmlString2 = + ' option_1 option_3 '; + const xmlDoc2 = xmlParser.parseFromString(xmlString2, 'text/xml'); + + const noLabels = { + 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: '{ 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, }}', + }, + version: 9, + }, + }, + 'trip-labels': 'ENKETO', + }, + }; + mockBEMUserCache(noLabels); + expect(await resolveLabel('TimeUseSurvey', xmlDoc)).toBe( + '{ erea, plural, =0 {} other {# Employment/Education, } }{ da, plural, =0 {} other {# Domestic, } }', + ); + expect(await resolveLabel('TimeUseSurvey', xmlDoc2)).toBe( + '{ erea, plural, =0 {} other {# Employment/Education, } }{ da, plural, =0 {} other {# Domestic, } }', + ); +}); + /** * @param surveyName the name of the survey (e.g. "TimeUseSurvey") * @param enketoForm the Form object from enketo-core that contains this survey diff --git a/www/js/config/dynamicConfig.ts b/www/js/config/dynamicConfig.ts index eb709c16c..801e24b07 100644 --- a/www/js/config/dynamicConfig.ts +++ b/www/js/config/dynamicConfig.ts @@ -10,6 +10,11 @@ export let storedConfig = null; export let configChanged = false; export const setConfigChanged = (b) => (configChanged = b); +//used test multiple configs, not used outside of test +export const resetStoredConfig = function () { + storedConfig = null; +}; + const _getStudyName = function (connectUrl) { const orig_host = new URL(connectUrl).hostname; const first_domain = orig_host.split('.')[0]; From 0ef1db81e574d44f9fe09858a9c24ffc6175904a Mon Sep 17 00:00:00 2001 From: Abby Wheelis <54848919+Abby-Wheelis@users.noreply.github.com> Date: Sun, 19 Nov 2023 08:33:47 -0700 Subject: [PATCH 091/112] add survey_info to the AppConfig Type from @JGreenlee's suggestion in review Co-authored-by: Jack Greenlee --- www/js/types/appConfigTypes.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/www/js/types/appConfigTypes.ts b/www/js/types/appConfigTypes.ts index f55b27bc0..07e8ccb5f 100644 --- a/www/js/types/appConfigTypes.ts +++ b/www/js/types/appConfigTypes.ts @@ -3,6 +3,10 @@ export type AppConfig = { server: ServerConnConfig; + survey_info: { + 'trip-labels': 'MULTILABEL' | 'ENKETO'; + surveys: EnketoSurveyConfig; + } [k: string]: any; // TODO fill in all the other fields }; From 10b954669a277210ac129892aa1ac61682aa8fa4 Mon Sep 17 00:00:00 2001 From: Abby Wheelis <54848919+Abby-Wheelis@users.noreply.github.com> Date: Sun, 19 Nov 2023 08:36:24 -0700 Subject: [PATCH 092/112] make config optional param not all (in fact many) of the tests don't need this config at all, so the parameter should be optional to clean things up Co-authored-by: Jack Greenlee --- www/__mocks__/cordovaMocks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index e8b680965..08293e73f 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -35,7 +35,7 @@ export const mockFile = () => { //for consent document const _storage = {}; -export const mockBEMUserCache = (config) => { +export const mockBEMUserCache = (config?) => { const _cache = {}; const messages = []; const mockBEMUserCache = { From fd0103427627dab2a38d7cbb46ddaaabc8df313c Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Sun, 19 Nov 2023 08:43:44 -0700 Subject: [PATCH 093/112] fallback to fakeConfig follow-on to making the config an optional parameter to mockBEMUserCache Instead of passing it in EVERY TIME, it is now a fallback, and the config only needs to be specified and re-specified in the enketoHelper tests added fallback and removed specification from tests that didn't need it --- www/__mocks__/cordovaMocks.ts | 2 +- www/__tests__/clientStats.test.ts | 3 +-- www/__tests__/pushNotifySettings.test.ts | 3 +-- www/__tests__/remoteNotifyHandler.test.ts | 3 +-- www/__tests__/startprefs.test.ts | 3 +-- www/__tests__/storage.test.ts | 3 +-- www/__tests__/storeDeviceSettings.test.ts | 3 +-- 7 files changed, 7 insertions(+), 13 deletions(-) diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 08293e73f..1d3934ea4 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -105,7 +105,7 @@ export const mockBEMUserCache = (config?) => { if (key == 'config/app_ui_config') { return new Promise((rs, rj) => setTimeout(() => { - rs(config); + rs(config || fakeConfig); }, 100), ); } else { diff --git a/www/__tests__/clientStats.test.ts b/www/__tests__/clientStats.test.ts index a508550e5..a3a953582 100644 --- a/www/__tests__/clientStats.test.ts +++ b/www/__tests__/clientStats.test.ts @@ -6,13 +6,12 @@ import { getAppVersion, statKeys, } from '../js/plugin/clientStats'; -import fakeConfig from '../__mocks__/fakeConfig.json'; mockDevice(); // this mocks cordova-plugin-app-version, generating a "Mock App", version "1.2.3" mockGetAppVersion(); // clientStats.ts uses BEMUserCache to store the stats, so we need to mock that too -mockBEMUserCache(fakeConfig); +mockBEMUserCache(); const db = window['cordova']?.plugins?.BEMUserCache; it('gets the app version', async () => { diff --git a/www/__tests__/pushNotifySettings.test.ts b/www/__tests__/pushNotifySettings.test.ts index 9e6e25bb5..d452aa819 100644 --- a/www/__tests__/pushNotifySettings.test.ts +++ b/www/__tests__/pushNotifySettings.test.ts @@ -11,12 +11,11 @@ import { mockPushNotification, getCalled, } from '../__mocks__/pushNotificationMocks'; -import fakeConfig from '../__mocks__/fakeConfig.json'; mockCordova(); mockLogger(); mockPushNotification(); -mockBEMUserCache(fakeConfig); +mockBEMUserCache(); mockBEMDataCollection(); global.fetch = (url: string) => diff --git a/www/__tests__/remoteNotifyHandler.test.ts b/www/__tests__/remoteNotifyHandler.test.ts index 6fe0a73fe..320877c6b 100644 --- a/www/__tests__/remoteNotifyHandler.test.ts +++ b/www/__tests__/remoteNotifyHandler.test.ts @@ -9,11 +9,10 @@ import { mockInAppBrowser, } from '../__mocks__/cordovaMocks'; import { clearAlerts, getAlerts, mockAlert, mockLogger } from '../__mocks__/globalMocks'; -import fakeConfig from '../__mocks__/fakeConfig.json'; mockLogger(); mockDevice(); -mockBEMUserCache(fakeConfig); +mockBEMUserCache(); mockGetAppVersion(); mockInAppBrowser(); mockAlert(); diff --git a/www/__tests__/startprefs.test.ts b/www/__tests__/startprefs.test.ts index 17b44a4be..75ed707dc 100644 --- a/www/__tests__/startprefs.test.ts +++ b/www/__tests__/startprefs.test.ts @@ -7,9 +7,8 @@ import { import { mockBEMUserCache, mockBEMDataCollection } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; -import fakeConfig from '../__mocks__/fakeConfig.json'; -mockBEMUserCache(fakeConfig); +mockBEMUserCache(); mockBEMDataCollection(); mockLogger(); diff --git a/www/__tests__/storage.test.ts b/www/__tests__/storage.test.ts index bbfa9c410..ca6d71dec 100644 --- a/www/__tests__/storage.test.ts +++ b/www/__tests__/storage.test.ts @@ -1,12 +1,11 @@ import { mockBEMUserCache } from '../__mocks__/cordovaMocks'; import { mockLogger } from '../__mocks__/globalMocks'; import { storageClear, storageGet, storageRemove, storageSet } from '../js/plugin/storage'; -import fakeConfig from '../__mocks__/fakeConfig.json'; // mocks used - storage.ts uses BEMUserCache and logging. // localStorage is already mocked for us by Jest :) mockLogger(); -mockBEMUserCache(fakeConfig); +mockBEMUserCache(); it('stores a value and retrieves it back', async () => { await storageSet('test1', 'test value 1'); diff --git a/www/__tests__/storeDeviceSettings.test.ts b/www/__tests__/storeDeviceSettings.test.ts index 41c36eb16..4bccbc0af 100644 --- a/www/__tests__/storeDeviceSettings.test.ts +++ b/www/__tests__/storeDeviceSettings.test.ts @@ -13,9 +13,8 @@ import { import { mockLogger } from '../__mocks__/globalMocks'; import { EVENTS, publish } from '../js/customEventHandler'; import { markIntroDone } from '../js/onboarding/onboardingHelper'; -import fakeConfig from '../__mocks__/fakeConfig.json'; -mockBEMUserCache(fakeConfig); +mockBEMUserCache(); mockDevice(); mockCordova(); mockLogger(); From 83a773ec79096cd90fce36e18d5bb1a8f5e8610d Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Sun, 19 Nov 2023 08:45:25 -0700 Subject: [PATCH 094/112] prettier types --- www/js/types/appConfigTypes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/types/appConfigTypes.ts b/www/js/types/appConfigTypes.ts index 07e8ccb5f..aa2e3f312 100644 --- a/www/js/types/appConfigTypes.ts +++ b/www/js/types/appConfigTypes.ts @@ -6,7 +6,7 @@ export type AppConfig = { survey_info: { 'trip-labels': 'MULTILABEL' | 'ENKETO'; surveys: EnketoSurveyConfig; - } + }; [k: string]: any; // TODO fill in all the other fields }; From 51f6ece32cf73c110ce9b17477224ea2c52f95b6 Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Sun, 19 Nov 2023 09:22:39 -0700 Subject: [PATCH 095/112] await instead of timeout in testing the initCustomDatasets function, I was calling it and then setting a timeout to wait for it to run, now I await getting the config, then await initialization double checked the Jest tests and running in the emulator - still going smoothly! --- www/__tests__/customMetricsHelper.test.ts | 8 ++++---- www/__tests__/footprintHelper.test.ts | 16 ++++++++-------- www/__tests__/metHelper.test.ts | 4 ++-- www/js/metrics/customMetricsHelper.ts | 11 +++++------ 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/www/__tests__/customMetricsHelper.test.ts b/www/__tests__/customMetricsHelper.test.ts index 173fbc97f..572fc0c27 100644 --- a/www/__tests__/customMetricsHelper.test.ts +++ b/www/__tests__/customMetricsHelper.test.ts @@ -25,8 +25,8 @@ global.fetch = (url: string) => }) as any; it('gets the custom mets', async () => { - initCustomDatasetHelper(getConfig()); - await new Promise((r) => setTimeout(r, 800)); + const appConfig = await getConfig(); + await initCustomDatasetHelper(appConfig); expect(getCustomMETs()).toMatchObject({ walk: {}, bike: {}, @@ -38,8 +38,8 @@ it('gets the custom mets', async () => { }); it('gets the custom footprint', async () => { - initCustomDatasetHelper(getConfig()); - await new Promise((r) => setTimeout(r, 800)); + const appConfig = await getConfig(); + await initCustomDatasetHelper(appConfig); expect(getCustomFootprint()).toMatchObject({ walk: {}, bike: {}, diff --git a/www/__tests__/footprintHelper.test.ts b/www/__tests__/footprintHelper.test.ts index 3f6883bbe..842442153 100644 --- a/www/__tests__/footprintHelper.test.ts +++ b/www/__tests__/footprintHelper.test.ts @@ -39,25 +39,25 @@ const custom_metrics = [ ]; it('gets footprint for metrics (custom, fallback 0)', async () => { - initCustomDatasetHelper(getConfig()); - await new Promise((r) => setTimeout(r, 500)); + const appConfig = await getConfig(); + await initCustomDatasetHelper(appConfig); expect(getFootprintForMetrics(custom_metrics, 0)).toBe(2.4266); }); it('gets footprint for metrics (custom, fallback 0.1)', async () => { - initCustomDatasetHelper(getConfig()); - await new Promise((r) => setTimeout(r, 500)); + const appConfig = await getConfig(); + await initCustomDatasetHelper(appConfig); expect(getFootprintForMetrics(custom_metrics, 0.1)).toBe(2.4266 + 0.5); }); it('gets the highest footprint from the dataset, custom', async () => { - initCustomDatasetHelper(getConfig()); - await new Promise((r) => setTimeout(r, 500)); + const appConfig = await getConfig(); + await initCustomDatasetHelper(appConfig); expect(getHighestFootprint()).toBe(0.30741); }); it('gets the highest footprint for distance, custom', async () => { - initCustomDatasetHelper(getConfig()); - await new Promise((r) => setTimeout(r, 500)); + const appConfig = await getConfig(); + await initCustomDatasetHelper(appConfig); expect(getHighestFootprintForDistance(12345)).toBe(0.30741 * (12345 / 1000)); }); diff --git a/www/__tests__/metHelper.test.ts b/www/__tests__/metHelper.test.ts index a06034a61..bc477daa0 100644 --- a/www/__tests__/metHelper.test.ts +++ b/www/__tests__/metHelper.test.ts @@ -29,8 +29,8 @@ it('gets met for mode and speed', () => { }); it('gets custom met for mode and speed', async () => { - initCustomDatasetHelper(getConfig()); - await new Promise((r) => setTimeout(r, 500)); + const appConfig = await getConfig(); + await initCustomDatasetHelper(appConfig); expect(getMet('walk', 1.47523, 0)).toBe(4.3); expect(getMet('bike', 4.5, 0)).toBe(6.8); expect(getMet('unicycle', 100, 0)).toBe(0); diff --git a/www/js/metrics/customMetricsHelper.ts b/www/js/metrics/customMetricsHelper.ts index 3a2f551ca..ac345e39c 100644 --- a/www/js/metrics/customMetricsHelper.ts +++ b/www/js/metrics/customMetricsHelper.ts @@ -98,12 +98,11 @@ const populateCustomFootprints = function () { export const initCustomDatasetHelper = async function (newConfig) { try { logDebug('initializing custom datasets with config' + newConfig); - getLabelOptions(newConfig).then((labelOptions) => { - console.log('In custom metrics, label options: ', labelOptions); - _labelOptions = labelOptions; - populateCustomMETs(); - populateCustomFootprints(); - }); + const labelOptions = await getLabelOptions(newConfig); + console.log('In custom metrics, label options: ', labelOptions); + _labelOptions = labelOptions; + populateCustomMETs(); + populateCustomFootprints(); } catch (e) { setTimeout(() => { displayError(e, 'Error while initializing custom dataset helper'); From 3dee26f69f6a54f5265d938ac0897e713bb385cc Mon Sep 17 00:00:00 2001 From: Abby Wheelis Date: Sun, 19 Nov 2023 09:29:11 -0700 Subject: [PATCH 096/112] Use expect.any(Type) for stricter testing https://github.com/e-mission/e-mission-phone/pull/1098#discussion_r1398327203 --- www/__tests__/customMetricsHelper.test.ts | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/www/__tests__/customMetricsHelper.test.ts b/www/__tests__/customMetricsHelper.test.ts index 572fc0c27..0ae025bff 100644 --- a/www/__tests__/customMetricsHelper.test.ts +++ b/www/__tests__/customMetricsHelper.test.ts @@ -28,12 +28,12 @@ it('gets the custom mets', async () => { const appConfig = await getConfig(); await initCustomDatasetHelper(appConfig); expect(getCustomMETs()).toMatchObject({ - walk: {}, - bike: {}, - bikeshare: {}, - 'e-bike': {}, - scootershare: {}, - drove_alone: {}, + walk: expect.any(Object), + bike: expect.any(Object), + bikeshare: expect.any(Object), + 'e-bike': expect.any(Object), + scootershare: expect.any(Object), + drove_alone: expect.any(Object), }); }); @@ -41,11 +41,11 @@ it('gets the custom footprint', async () => { const appConfig = await getConfig(); await initCustomDatasetHelper(appConfig); expect(getCustomFootprint()).toMatchObject({ - walk: {}, - bike: {}, - bikeshare: {}, - 'e-bike': {}, - scootershare: {}, - drove_alone: {}, + walk: expect.any(Number), + bike: expect.any(Number), + bikeshare: expect.any(Number), + 'e-bike': expect.any(Number), + scootershare: expect.any(Number), + drove_alone: expect.any(Number), }); }); From becc4af0d724e676bedde73f3fb145fcdc211485 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 20 Nov 2023 10:41:35 -0500 Subject: [PATCH 097/112] enketoHelper: expand typings for resolveTimestamps -add types for the parameters of resolveTimestamps -update doc of this function -cast 'timelineEntry' to trip or place where needed -add a few fields that were missing from type defs of ConfirmedPlace and EnketoSurveyConfig --- www/js/survey/enketo/enketoHelper.ts | 28 ++++++++++++++++++---------- www/js/types/appConfigTypes.ts | 1 + www/js/types/diaryTypes.ts | 12 ++++++++++-- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index cb5e0bac0..933ca3aed 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -8,7 +8,8 @@ import { getConfig } from '../../config/dynamicConfig'; import { DateTime } from 'luxon'; import { fetchUrlCached } from '../../services/commHelper'; import { getUnifiedDataForInterval } from '../../services/unifiedDataLoader'; -import { EnketoSurveyConfig } from '../../types/appConfigTypes'; +import { AppConfig, EnketoSurveyConfig } from '../../types/appConfigTypes'; +import { CompositeTrip, ConfirmedPlace, TimelineEntry } from '../../types/diaryTypes'; export type PrefillFields = { [key: string]: string }; @@ -151,12 +152,12 @@ export function getInstanceStr(xmlModel: string, opts: SurveyOptions): string | /** * resolve timestamps label from the survey response * @param {XMLDocument} xmlDoc survey response as XML object - * @param {object} trip trip object + * @param {object} timelineEntry trip or place object * @returns {object} object with `start_ts` and `end_ts` * - null if no timestamps are resolved * - undefined if the timestamps are invalid */ -export function resolveTimestamps(xmlDoc, timelineEntry) { +export function resolveTimestamps(xmlDoc: XMLDocument, timelineEntry: TimelineEntry) { // check for Date and Time fields const startDate = xmlDoc.getElementsByTagName('Start_date')?.[0]?.innerHTML; let startTime = xmlDoc.getElementsByTagName('Start_time')?.[0]?.innerHTML; @@ -167,10 +168,10 @@ export function resolveTimestamps(xmlDoc, timelineEntry) { if (!startDate || !startTime || !endDate || !endTime) return null; const timezone = - timelineEntry.start_local_dt?.timezone || - timelineEntry.enter_local_dt?.timezone || - timelineEntry.end_local_dt?.timezone || - timelineEntry.exit_local_dt?.timezone; + (timelineEntry as CompositeTrip).start_local_dt?.timezone || + (timelineEntry as ConfirmedPlace).enter_local_dt?.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]; @@ -188,8 +189,10 @@ export function resolveTimestamps(xmlDoc, timelineEntry) { the millisecond. To avoid precision issues, we will check if the start/end timestamps from the survey response are within the same minute as the start/end or enter/exit timestamps. If so, we will use the exact trip/place timestamps */ - const entryStartTs = timelineEntry.start_ts || timelineEntry.enter_ts; - const entryEndTs = timelineEntry.end_ts || timelineEntry.exit_ts; + const entryStartTs = + (timelineEntry as CompositeTrip).start_ts || (timelineEntry as ConfirmedPlace).enter_ts; + const entryEndTs = + (timelineEntry as CompositeTrip).end_ts || (timelineEntry as ConfirmedPlace).exit_ts; if (additionStartTs - (additionStartTs % 60) == entryStartTs - (entryStartTs % 60)) additionStartTs = entryStartTs; if (additionEndTs - (additionEndTs % 60) == entryEndTs - (entryEndTs % 60)) @@ -209,7 +212,12 @@ export function resolveTimestamps(xmlDoc, timelineEntry) { * @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) { +export function saveResponse( + surveyName: string, + enketoForm: Form, + appConfig: AppConfig, + opts: SurveyOptions, +) { const xmlParser = new window.DOMParser(); const xmlResponse = enketoForm.getDataStr(); const xmlDoc = xmlParser.parseFromString(xmlResponse, 'text/xml'); diff --git a/www/js/types/appConfigTypes.ts b/www/js/types/appConfigTypes.ts index aa2e3f312..1a2e50722 100644 --- a/www/js/types/appConfigTypes.ts +++ b/www/js/types/appConfigTypes.ts @@ -22,5 +22,6 @@ export type EnketoSurveyConfig = { labelVars: { [activity: string]: { [key: string]: string; type: string } }; version: number; compatibleWith: number; + dataKey?: string; }; }; diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 743d75b15..7cce67923 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -5,14 +5,17 @@ import { BaseModeKey, MotionTypeKey } from '../diary/diaryHelper'; type ObjectId = { $oid: string }; -type ConfirmedPlace = { +export type ConfirmedPlace = { _id: ObjectId; additions: UserInputEntry[]; cleaned_place: ObjectId; ending_trip: ObjectId; - enter_fmt_time: string; // ISO string 2023-10-31T12:00:00.000-04:00 + enter_fmt_time: string; // ISO string e.g. 2023-10-31T12:00:00.000-04:00 enter_local_dt: LocalDt; enter_ts: number; // Unix timestamp + exit_fmt_time: string; // ISO string e.g. 2023-10-31T12:00:00.000-04:00 + exit_local_dt: LocalDt; + exit_ts: number; // Unix timestamp key: string; location: { type: string; coordinates: number[] }; origin_key: string; @@ -76,6 +79,11 @@ export type CompositeTrip = { so a 'timeline entry' is either a trip or a place. */ export type TimelineEntry = ConfirmedPlace | CompositeTrip; +/* Type guard to disambiguate timeline entries as either trips or places + If it has a 'start_ts' and 'end_ts', it's a trip. Else, it's a place. */ +export const isTrip = (entry: TimelineEntry): entry is CompositeTrip => + entry.hasOwnProperty('start_ts') && entry.hasOwnProperty('end_ts'); + /* These properties aren't received from the server, but are derived from the above properties. They are used in the UI to display trip/place details and are computed by the useDerivedProperties hook. */ export type DerivedProperties = { From 9a5753bf2e95e74494c248672b8aa2068aebc5d4 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Mon, 20 Nov 2023 11:01:42 -0500 Subject: [PATCH 098/112] code style cleanup There is no right or wrong here, but it's a bit cleaner and more consistent with the rest of the codebase if we generally follow this: i) for short one-liners, use const and declare an arrow function ii) for longer functions, use the traditional 'function' declaration also replaced some uses of 'var' with 'const' or 'let' added AppConfig type to initCustomDatasetHelper and replaced a console statement with logDebug --- www/js/metrics/customMetricsHelper.ts | 23 +++++++++-------- www/js/metrics/footprintHelper.ts | 36 ++++++++++++--------------- www/js/metrics/metHelper.ts | 18 ++++++-------- 3 files changed, 35 insertions(+), 42 deletions(-) diff --git a/www/js/metrics/customMetricsHelper.ts b/www/js/metrics/customMetricsHelper.ts index ac345e39c..317113327 100644 --- a/www/js/metrics/customMetricsHelper.ts +++ b/www/js/metrics/customMetricsHelper.ts @@ -2,6 +2,7 @@ import angular from 'angular'; import { getLabelOptions } from '../survey/multilabel/confirmHelper'; import { displayError, displayErrorMsg, logDebug, logWarn } from '../plugin/logger'; import { standardMETs } from './metDataset'; +import { AppConfig } from '../types/appConfigTypes'; //variables to store values locally let _customMETs; @@ -13,25 +14,25 @@ let _labelOptions; * @function gets custom mets, must be initialized * @returns the custom mets stored locally */ -export const getCustomMETs = function () { +export function getCustomMETs() { logDebug('Getting custom METs ' + JSON.stringify(_customMETs)); return _customMETs; -}; +} /** * @function gets the custom footprint, must be initialized * @returns custom footprint */ -export const getCustomFootprint = function () { +export function getCustomFootprint() { logDebug('Getting custom footprint ' + JSON.stringify(_customPerKmFootprint)); return _customPerKmFootprint; -}; +} /** * @function stores custom mets in local var * needs _labelOptions, stored after gotten from config */ -const populateCustomMETs = function () { +function populateCustomMETs() { let modeOptions = _labelOptions['MODE']; let modeMETEntries = modeOptions.map((opt) => { if (opt.met_equivalent) { @@ -59,13 +60,13 @@ const populateCustomMETs = function () { }); _customMETs = Object.fromEntries(modeMETEntries.filter((e) => angular.isDefined(e))); logDebug('After populating, custom METs = ' + JSON.stringify(_customMETs)); -}; +} /** * @function stores custom footprint in local var * needs _inputParams which is stored after gotten from config */ -const populateCustomFootprints = function () { +function populateCustomFootprints() { let modeOptions = _labelOptions['MODE']; let modeCO2PerKm = modeOptions .map((opt) => { @@ -88,18 +89,18 @@ const populateCustomFootprints = function () { .filter((modeCO2) => angular.isDefined(modeCO2)); _customPerKmFootprint = Object.fromEntries(modeCO2PerKm); logDebug('After populating, custom perKmFootprint' + JSON.stringify(_customPerKmFootprint)); -}; +} /** * @function initializes the datasets based on configured label options * calls popuplateCustomMETs and populateCustomFootprint * @param newConfig the app config file */ -export const initCustomDatasetHelper = async function (newConfig) { +export async function initCustomDatasetHelper(newConfig: AppConfig) { try { logDebug('initializing custom datasets with config' + newConfig); const labelOptions = await getLabelOptions(newConfig); - console.log('In custom metrics, label options: ', labelOptions); + logDebug('In custom metrics, label options = ' + JSON.stringify(labelOptions)); _labelOptions = labelOptions; populateCustomMETs(); populateCustomFootprints(); @@ -108,4 +109,4 @@ export const initCustomDatasetHelper = async function (newConfig) { displayError(e, 'Error while initializing custom dataset helper'); }, 1000); } -}; +} diff --git a/www/js/metrics/footprintHelper.ts b/www/js/metrics/footprintHelper.ts index 6e4bd46d9..24677feaf 100644 --- a/www/js/metrics/footprintHelper.ts +++ b/www/js/metrics/footprintHelper.ts @@ -9,17 +9,15 @@ let highestFootprint = 0; * @param {number} v value in meters to be converted * @returns {number} converted value in km */ -const mtokm = function (v) { - return v / 1000; -}; +const mtokm = (v) => v / 1000; /** * @function clears the stored highest footprint */ -export const clearHighestFootprint = function () { +export function clearHighestFootprint() { //need to clear for testing highestFootprint = undefined; -}; +} /** * @function gets the footprint @@ -27,7 +25,7 @@ export const clearHighestFootprint = function () { * fallback is json/label-options.json.sample, with MET and kgCO2 defined * @returns the footprint or undefined */ -const getFootprint = function () { +function getFootprint() { let footprint = getCustomFootprint(); if (footprint) { return footprint; @@ -35,7 +33,7 @@ const getFootprint = function () { displayErrorMsg('failed to use custom labels', 'Error in Footprint Calculatons'); return undefined; } -}; +} /** * @function calculates footprint for given metrics @@ -44,12 +42,12 @@ const getFootprint = function () { * @param {number} defaultIfMissing optional, carbon intensity if mode not in footprint * @returns {number} the sum of carbon emissions for userMetrics given */ -export const getFootprintForMetrics = function (userMetrics, defaultIfMissing = 0) { - var footprint = getFootprint(); +export function getFootprintForMetrics(userMetrics, defaultIfMissing = 0) { + const footprint = getFootprint(); logDebug('getting footprint for ' + userMetrics + ' with ' + footprint); - var result = 0; - for (var i in userMetrics) { - var mode = userMetrics[i].key; + let result = 0; + for (let i in userMetrics) { + let mode = userMetrics[i].key; if (mode == 'ON_FOOT') { mode = 'WALKING'; } @@ -75,29 +73,27 @@ export const getFootprintForMetrics = function (userMetrics, defaultIfMissing = } } return result; -}; +} /** * @function gets highest co2 intensity in the footprint * @returns {number} the highest co2 intensity in the footprint */ -export const getHighestFootprint = function () { +export function getHighestFootprint() { if (!highestFootprint) { - var footprint = getFootprint(); + const footprint = getFootprint(); let footprintList = []; - for (var mode in footprint) { + for (let mode in footprint) { footprintList.push(footprint[mode]); } highestFootprint = Math.max(...footprintList); } return highestFootprint; -}; +} /** * @function gets highest theoretical footprint for given distance * @param {number} distance in meters to calculate max footprint * @returns max footprint for given distance */ -export const getHighestFootprintForDistance = function (distance) { - return getHighestFootprint() * mtokm(distance); -}; +export const getHighestFootprintForDistance = (distance) => getHighestFootprint() * mtokm(distance); diff --git a/www/js/metrics/metHelper.ts b/www/js/metrics/metHelper.ts index 28765d193..25bcc2e7e 100644 --- a/www/js/metrics/metHelper.ts +++ b/www/js/metrics/metHelper.ts @@ -6,14 +6,14 @@ import { standardMETs } from './metDataset'; * @function gets the METs object * @returns {object} mets either custom or standard */ -const getMETs = function () { +function getMETs() { let custom_mets = getCustomMETs(); if (custom_mets) { return custom_mets; } else { return standardMETs; } -}; +} /** * @function checks number agains bounds @@ -22,18 +22,14 @@ const getMETs = function () { * @param max upper bound * @returns {boolean} if number is within given bounds */ -const between = function (num, min, max) { - return num >= min && num <= max; -}; +const between = (num, min, max) => num >= min && num <= max; /** * @function converts meters per second to miles per hour * @param mps meters per second speed * @returns speed in miles per hour */ -const mpstomph = function (mps) { - return 2.23694 * mps; -}; +const mpstomph = (mps) => 2.23694 * mps; /** * @function gets met for a given mode and speed @@ -42,7 +38,7 @@ const mpstomph = function (mps) { * @param {number} defaultIfMissing default MET if mode not in METs * @returns */ -export const getMet = function (mode, speed, defaultIfMissing) { +export function getMet(mode, speed, defaultIfMissing) { if (mode == 'ON_FOOT') { logDebug("getMet() converted 'ON_FOOT' to 'WALKING'"); mode = 'WALKING'; @@ -52,7 +48,7 @@ export const getMet = function (mode, speed, defaultIfMissing) { logWarn('getMet() Illegal mode: ' + mode); return defaultIfMissing; //So the calorie sum does not break with wrong return type } - for (var i in currentMETs[mode]) { + for (let i in currentMETs[mode]) { if (between(mpstomph(speed), currentMETs[mode][i].range[0], currentMETs[mode][i].range[1])) { return currentMETs[mode][i].mets; } else if (mpstomph(speed) < 0) { @@ -60,4 +56,4 @@ export const getMet = function (mode, speed, defaultIfMissing) { return 0; } } -}; +} From 7480b926d6586658defa7b1bf5d2f014a643b5c4 Mon Sep 17 00:00:00 2001 From: Nitish Ramakrishnan Date: Mon, 20 Nov 2023 10:30:21 -0800 Subject: [PATCH 099/112] Removed unnecessary imports --- www/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/www/index.js b/www/index.js index cabf5ef04..865cc9a32 100644 --- a/www/index.js +++ b/www/index.js @@ -14,9 +14,8 @@ import './js/main.js'; import './js/diary.js'; import './js/diary/services.js'; import './js/survey/enketo/answer.js'; -import './js/survey/enketo/enketo-trip-button.js'; -import './js/survey/enketo/enketo-add-note-button.js'; -import './js/control/emailService.js'; +//import './js/survey/enketo/enketo-trip-button.js'; +//import './js/survey/enketo/enketo-add-note-button.js'; import './js/metrics-factory.js'; import './js/metrics-mappings.js'; import './js/plugin/logger.ts'; From b81493c138647e08d013f461091cc10349a400a8 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 21 Nov 2023 11:24:03 -0500 Subject: [PATCH 100/112] Clean up this file - remove unused imports - use logDebug, logInfo, logWarn, format nicely - replace ionic.Platform.isIOS / isAndroid with cordova.platformId --- www/index.js | 2 -- www/js/control/emailService.ts | 18 +++++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/www/index.js b/www/index.js index 865cc9a32..0ae3896a2 100644 --- a/www/index.js +++ b/www/index.js @@ -14,8 +14,6 @@ import './js/main.js'; import './js/diary.js'; import './js/diary/services.js'; import './js/survey/enketo/answer.js'; -//import './js/survey/enketo/enketo-trip-button.js'; -//import './js/survey/enketo/enketo-add-note-button.js'; import './js/metrics-factory.js'; import './js/metrics-mappings.js'; import './js/plugin/logger.ts'; diff --git a/www/js/control/emailService.ts b/www/js/control/emailService.ts index 0efca1518..601413546 100644 --- a/www/js/control/emailService.ts +++ b/www/js/control/emailService.ts @@ -1,6 +1,5 @@ -import React, { useEffect, useState } from 'react'; import i18next from 'i18next'; -import { logInfo, logDebug, displayError } from '../plugin/logger'; +import { logDebug, logInfo, logWarn } from '../plugin/logger'; async function hasAccount(): Promise { return new Promise((resolve, reject) => { @@ -13,18 +12,18 @@ async function hasAccount(): Promise { export async function sendEmail(database: string) { let parentDir = 'unknown'; - if (window['ionic'].Platform.isIOS() && !(await hasAccount())) { + if (window['cordova'].platformId == 'ios' && !(await hasAccount())) { alert(i18next.t('email-service.email-account-not-configured')); return; } - if (window['ionic'].Platform.isAndroid()) { + if (window['cordova'].platformId) == 'android') { parentDir = 'app://databases'; } - if (window['ionic'].Platform.isIOS()) { + if (window['cordova'].platformId) == 'ios') { alert(i18next.t('email-service.email-account-mail-app')); - console.log(window['cordova'].file.dataDirectory); + logDebug(window['cordova'].file.dataDirectory); parentDir = window['cordova'].file.dataDirectory + '../LocalDatabase'; } @@ -47,10 +46,7 @@ export async function sendEmail(database: string) { }; window['cordova'].plugins['email'].open(emailData, () => { - logInfo( - 'Email app closed while sending, ' + - JSON.stringify(emailData) + - ' not sure if we should do anything', - ); + logWarn(`Email app closed while sending, + emailData = ${JSON.stringify(emailData)}`); }); } From 810c31911ba6c0477c8abdf7da80b75e597cee9e Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 21 Nov 2023 11:27:50 -0500 Subject: [PATCH 101/112] fix bad parentheses --- www/js/control/emailService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/control/emailService.ts b/www/js/control/emailService.ts index 601413546..3a6e8a5c5 100644 --- a/www/js/control/emailService.ts +++ b/www/js/control/emailService.ts @@ -17,11 +17,11 @@ export async function sendEmail(database: string) { return; } - if (window['cordova'].platformId) == 'android') { + if (window['cordova'].platformId == 'android') { parentDir = 'app://databases'; } - if (window['cordova'].platformId) == 'ios') { + if (window['cordova'].platformId == 'ios') { alert(i18next.t('email-service.email-account-mail-app')); logDebug(window['cordova'].file.dataDirectory); parentDir = window['cordova'].file.dataDirectory + '../LocalDatabase'; From 828ee1279fa2ad866e3753d4894c8f95888eac65 Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Tue, 21 Nov 2023 11:55:54 -0500 Subject: [PATCH 102/112] enketoHelper: fix error if timestamps are null if the survey doesn't use start and end times, timestamps will be null. This is fine and we can use optional chaining to prevent throwing an error here --- www/js/survey/enketo/enketoHelper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/js/survey/enketo/enketoHelper.ts b/www/js/survey/enketo/enketoHelper.ts index 9b96c8463..d3ad13c98 100644 --- a/www/js/survey/enketo/enketoHelper.ts +++ b/www/js/survey/enketo/enketoHelper.ts @@ -250,8 +250,8 @@ export function saveResponse(surveyName: string, enketoForm: Form, appConfig, op return new Error(i18next.t('survey.enketo-timestamps-invalid')); //"Timestamps are invalid. Please ensure that the start time is before the end time."); } // if timestamps were not resolved from the survey, we will use the trip or place timestamps - data.start_ts = timestamps.start_ts || opts.timelineEntry.enter_ts; - data.end_ts = timestamps.end_ts || opts.timelineEntry.exit_ts; + data.start_ts = timestamps?.start_ts || opts.timelineEntry.enter_ts; + data.end_ts = timestamps?.end_ts || opts.timelineEntry.exit_ts; // UUID generated using this method https://stackoverflow.com/a/66332305 data.match_id = URL.createObjectURL(new Blob([])).slice(-36); } else { From 659b53ed3c94ff1338450b4e81e5b733f60232f3 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Mon, 27 Nov 2023 11:00:14 -0800 Subject: [PATCH 103/112] add codecov report workflow --- .github/workflows/codecov.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/codecov.yml diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 000000000..92836cbdb --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,14 @@ +name: codecov +on: + pull_request: + +jobs: + run-codecov: + runs-on: ubuntu-latest + steps: + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + From 49d90fe5748e255ea1ad2e6b865a3630e9663c62 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 28 Nov 2023 11:25:36 -0800 Subject: [PATCH 104/112] Change token method --- .github/workflows/codecov.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 92836cbdb..c6126ae45 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -8,7 +8,8 @@ jobs: steps: - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + flags: unit + fail_ci_if_error: ${{ github.repository == 'e-mission/e-mission-phone' }} From 1a6a721ac7c93d3911bede9efddf33126d269a2a Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 28 Nov 2023 11:30:15 -0800 Subject: [PATCH 105/112] add file path --- .github/workflows/codecov.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index c6126ae45..b9592a9d9 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -9,6 +9,7 @@ jobs: - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 with: + files: ./coverage.json flags: unit fail_ci_if_error: ${{ github.repository == 'e-mission/e-mission-phone' }} From 4e29f79baf766ac21b2d7a489c3e24139997e550 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 28 Nov 2023 11:52:04 -0800 Subject: [PATCH 106/112] Collect jest coverage and add it in gitignore --- .gitignore | 1 + jest.config.js | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 6801f890d..bdf59df4f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ app-settings.json www/dist/ config.xml package.json +coverage/ diff --git a/jest.config.js b/jest.config.js index 21184bdd7..a67623088 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,4 +16,5 @@ module.exports = { ], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], moduleDirectories: ["node_modules", "src"], + collectCoverage: true, }; From 4f6ec43bb3a47037275a75273da2f3ac00ca62fc Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 28 Nov 2023 11:52:24 -0800 Subject: [PATCH 107/112] Check if coverage file is in the target directory --- .github/workflows/codecov.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index b9592a9d9..4076a045a 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -6,11 +6,16 @@ jobs: run-codecov: runs-on: ubuntu-latest steps: - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 - with: - files: ./coverage.json - flags: unit - fail_ci_if_error: ${{ github.repository == 'e-mission/e-mission-phone' }} + - name : Check coverage file directory + - run: | + cd coverage + ls -la ./ + + # - name: Upload coverage reports to Codecov + # uses: codecov/codecov-action@v3 + # with: + # files: coverage/coverage.json + # flags: unit + # fail_ci_if_error: ${{ github.repository == 'e-mission/e-mission-phone' }} From f497f4de85cc7f428fef3e2f59bcc016c24f9c72 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 28 Nov 2023 11:59:51 -0800 Subject: [PATCH 108/112] Execute codecov report after jest test --- .github/workflows/codecov.yml | 21 --------------------- .github/workflows/serve-install.yml | 11 +++++++++++ 2 files changed, 11 insertions(+), 21 deletions(-) delete mode 100644 .github/workflows/codecov.yml diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml deleted file mode 100644 index 4076a045a..000000000 --- a/.github/workflows/codecov.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: codecov -on: - pull_request: - -jobs: - run-codecov: - runs-on: ubuntu-latest - steps: - - name : Check coverage file directory - - run: | - cd coverage - ls -la ./ - - # - name: Upload coverage reports to Codecov - # uses: codecov/codecov-action@v3 - # with: - # files: coverage/coverage.json - # flags: unit - # fail_ci_if_error: ${{ github.repository == 'e-mission/e-mission-phone' }} - - diff --git a/.github/workflows/serve-install.yml b/.github/workflows/serve-install.yml index c78ce1f86..6eed87594 100644 --- a/.github/workflows/serve-install.yml +++ b/.github/workflows/serve-install.yml @@ -64,6 +64,17 @@ jobs: run: | npx jest + - name : Check coverage file directory + run: | + ls -la ./ + + # - name: Upload coverage reports to Codecov + # uses: codecov/codecov-action@v3 + # with: + # files: coverage/coverage-final.json + # flags: unit + # fail_ci_if_error: ${{ github.repository == 'e-mission/e-mission-phone' }} + # TODO: figure out how to check that a server started correctly # - name: Try starting it # run: npx run serve From 84d4e3899bf2da7e8dd282fb63af632153a9877b Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 28 Nov 2023 12:12:46 -0800 Subject: [PATCH 109/112] Upload coverage reports to Codecov --- .github/workflows/serve-install.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/serve-install.yml b/.github/workflows/serve-install.yml index 6eed87594..f7902b2ea 100644 --- a/.github/workflows/serve-install.yml +++ b/.github/workflows/serve-install.yml @@ -64,16 +64,12 @@ jobs: run: | npx jest - - name : Check coverage file directory - run: | - ls -la ./ - - # - name: Upload coverage reports to Codecov - # uses: codecov/codecov-action@v3 - # with: - # files: coverage/coverage-final.json - # flags: unit - # fail_ci_if_error: ${{ github.repository == 'e-mission/e-mission-phone' }} + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage/coverage-final.json + flags: unit + fail_ci_if_error: ${{ github.repository == 'e-mission/e-mission-phone' }} # TODO: figure out how to check that a server started correctly # - name: Try starting it From d2a335c794ed967c95b60792684ddc54f9df435e Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 28 Nov 2023 12:41:22 -0800 Subject: [PATCH 110/112] Add codecov config file --- codecov.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..125b560ef --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +comment: + layout: " diff, flags, files" + behavior: default + require_changes: false + require_base: false + require_head: true + hide_project_coverage: false + \ No newline at end of file From 7a94e22a0aa041075d90e2be24a0b1fb00d935b5 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 28 Nov 2023 13:05:18 -0800 Subject: [PATCH 111/112] Add token in workflow --- .github/workflows/serve-install.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/serve-install.yml b/.github/workflows/serve-install.yml index f7902b2ea..71f950189 100644 --- a/.github/workflows/serve-install.yml +++ b/.github/workflows/serve-install.yml @@ -67,6 +67,7 @@ jobs: - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 with: + token: ${{secrets.CODECOV_TOKEN}} files: ./coverage/coverage-final.json flags: unit fail_ci_if_error: ${{ github.repository == 'e-mission/e-mission-phone' }} From 7121ec6d8d9db4ad1fd4c2e177a1a111e6bead83 Mon Sep 17 00:00:00 2001 From: Jijeong Lee Date: Tue, 28 Nov 2023 13:19:26 -0800 Subject: [PATCH 112/112] rollback to previous commit --- .github/workflows/serve-install.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/serve-install.yml b/.github/workflows/serve-install.yml index 71f950189..f7902b2ea 100644 --- a/.github/workflows/serve-install.yml +++ b/.github/workflows/serve-install.yml @@ -67,7 +67,6 @@ jobs: - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 with: - token: ${{secrets.CODECOV_TOKEN}} files: ./coverage/coverage-final.json flags: unit fail_ci_if_error: ${{ github.repository == 'e-mission/e-mission-phone' }}