From 9d1c543c37303a1d44ad7a1b03512ff3c2f65add Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Mon, 9 Sep 2024 14:24:55 +1200 Subject: [PATCH 1/4] Working check --- .../importSurveys/importSurveyQuestions.js | 185 +++++++++++++----- 1 file changed, 140 insertions(+), 45 deletions(-) diff --git a/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js b/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js index 2226d31ef3..8fd27869b2 100644 --- a/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js +++ b/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js @@ -23,10 +23,21 @@ import { SURVEY_METADATA, } from './processSurveyMetadata'; import { caseAndSpaceInsensitiveEquals, convertCellToJson } from './utilities'; +import { RECORDS } from '@tupaia/database'; const QUESTION_TYPE_LIST = Object.values(ANSWER_TYPES); const VIS_CRITERIA_CONJUNCTION = '_conjunction'; +const objectsAreEqual = (a, b) => { + if (!!a === !!b) return true; + return Object.entries(a).every(([key, value]) => { + if (Array.isArray(value)) { + return value.every((v, i) => v === b[key][i]); + } + return value === b[key]; + }); +}; + const validateQuestionExistence = rows => { const isQuestionRow = ({ type }) => QUESTION_TYPE_LIST.includes(type); if (!rows || !rows.some(isQuestionRow)) { @@ -35,6 +46,111 @@ const validateQuestionExistence = rows => { return true; }; +/** + * + * @param {object} models + * @param {string} screenId + * @param {string} questionId + * @param {number} componentNumber + * @param {object} questionObject + * + * @returns {Promise} + * + * @description Checks if the screen component already exists, if it does, it updates the changed values, otherwise it creates a new one + */ +const updateOrCreateSurveyScreenComponent = async ( + models, + screenId, + questionId, + componentNumber, + questionObject, +) => { + const existingScreenComponent = await models.surveyScreenComponent.findOne({ + screen_id: screenId, + question_id: questionId, + component_number: componentNumber, + }); + const { + questionLabel = null, + detailLabel = null, + visibilityCriteria = null, + validationCriteria = null, + } = questionObject; + + const validationCriteriaObject = convertCellToJson( + validationCriteria, + processValidationCriteriaValue, + ); + // Create a new survey screen component to display this question + const visibilityCriteriaObject = convertCellToJson(visibilityCriteria, splitStringOnComma); + + const processedVisibilityCriteria = {}; + + await Promise.all( + Object.entries(visibilityCriteriaObject).map(async ([questionCode, answers]) => { + if (questionCode === VIS_CRITERIA_CONJUNCTION) { + // This is the special _conjunction key, extract the 'and' or the 'or' from answers, + // i.e. { conjunction: ['and'] } -> { conjunction: 'and' } + const [conjunctionType] = answers; + processedVisibilityCriteria[VIS_CRITERIA_CONJUNCTION] = conjunctionType; + } else if (questionCode === 'hidden') { + processedVisibilityCriteria.hidden = answers[0] === 'true'; + } else { + const { id: questionId } = await models.question.findOne({ + code: questionCode, + }); + processedVisibilityCriteria[questionId] = answers; + } + }), + ); + + // If the screen component already exists, update only the changed values, otherwise create a new one + if (existingScreenComponent) { + const changes = {}; + if ( + !objectsAreEqual( + JSON.parse(existingScreenComponent.visibility_criteria), + processedVisibilityCriteria, + ) + ) { + changes.visibility_criteria = JSON.stringify(processedVisibilityCriteria); + } + + if ( + !objectsAreEqual( + JSON.parse(existingScreenComponent.validation_criteria), + validationCriteriaObject, + ) + ) { + changes.validation_criteria = JSON.stringify(validationCriteriaObject); + } + + if (questionLabel !== existingScreenComponent.question_label) { + changes.question_label = questionLabel; + } + + if (detailLabel !== existingScreenComponent.detail_label) { + changes.detail_label = detailLabel; + } + + if (Object.keys(changes).length > 0) { + await models.surveyScreenComponent.update({ id: existingScreenComponent.id }, changes); + } + return existingScreenComponent; + } + const newSurveyScreenComponent = await models.surveyScreenComponent.create({ + screen_id: screenId, + question_id: questionId, + component_number: componentNumber, + visibility_criteria: JSON.stringify(processedVisibilityCriteria), + validation_criteria: JSON.stringify(validationCriteriaObject), + question_label: questionLabel, + detail_label: detailLabel, + }); + + return newSurveyScreenComponent; +}; + const updateOrCreateDataElementInGroup = async ( models, dataElementCode, @@ -100,11 +216,16 @@ export async function importSurveysQuestions({ models, file, survey, dataGroup, await dataGroup.deleteSurveyDateElement(); await dataGroup.upsertSurveyDateElement(); - // Delete all existing survey screens and components that were attached to this survey - await deleteScreensForSurvey(models, survey.id); const rows = xlsx.utils.sheet_to_json(sheet); validateQuestionExistence(rows); + const questions = await survey.questions(); + + // If the questions have changed order or had questions added/removed, delete all screens from the survey and re-create them + if (rows.map(({ code }) => code).join(',') !== questions.map(({ code }) => code).join(',')) { + await deleteScreensForSurvey(models, survey.id); + } + // Add all questions to the survey, creating screens, components and questions as required let currentScreen; let currentSurveyScreenComponent; @@ -172,15 +293,11 @@ export async function importSurveysQuestions({ models, file, survey, dataGroup, type, name, text, - questionLabel, detail, - detailLabel, options, optionLabels, optionColors, newScreen, - visibilityCriteria, - validationCriteria, optionSet, hook, } = questionObject; @@ -222,52 +339,30 @@ export async function importSurveysQuestions({ models, file, survey, dataGroup, if (!currentScreen || shouldStartNewScreen) { // Spreadsheet indicates this question starts a new screen // Create a new survey screen - currentScreen = await models.surveyScreen.create({ + const params = { survey_id: survey.id, screen_number: currentScreen ? currentScreen.screen_number + 1 : 1, // Next screen - }); + }; + const existingScreen = await models.surveyScreen.findOne(params); + if (existingScreen) { + currentScreen = existingScreen; + } else { + currentScreen = await models.surveyScreen.create(params); + } // Clear existing survey screen component currentSurveyScreenComponent = undefined; } - // Create a new survey screen component to display this question - const visibilityCriteriaObject = await convertCellToJson( - visibilityCriteria, - splitStringOnComma, - ); - const processedVisibilityCriteria = {}; - await Promise.all( - Object.entries(visibilityCriteriaObject).map(async ([questionCode, answers]) => { - if (questionCode === VIS_CRITERIA_CONJUNCTION) { - // This is the special _conjunction key, extract the 'and' or the 'or' from answers, - // i.e. { conjunction: ['and'] } -> { conjunction: 'and' } - const [conjunctionType] = answers; - processedVisibilityCriteria[VIS_CRITERIA_CONJUNCTION] = conjunctionType; - } else if (questionCode === 'hidden') { - processedVisibilityCriteria.hidden = answers[0] === 'true'; - } else { - const { id: questionId } = await models.question.findOne({ - code: questionCode, - }); - processedVisibilityCriteria[questionId] = answers; - } - }), + const componentNumber = currentSurveyScreenComponent + ? currentSurveyScreenComponent.component_number + 1 + : 1; + currentSurveyScreenComponent = await updateOrCreateSurveyScreenComponent( + models, + currentScreen.id, + question.id, + componentNumber, + questionObject, ); - - currentSurveyScreenComponent = await models.surveyScreenComponent.create({ - screen_id: currentScreen.id, - question_id: question.id, - component_number: currentSurveyScreenComponent - ? currentSurveyScreenComponent.component_number + 1 - : 1, - visibility_criteria: JSON.stringify(processedVisibilityCriteria), - validation_criteria: JSON.stringify( - convertCellToJson(validationCriteria, processValidationCriteriaValue), - ), - question_label: questionLabel, - detail_label: detailLabel, - }); - const componentId = currentSurveyScreenComponent.id; await configImporter.add(rowIndex, componentId, constructImportValidationError); } catch (e) { From 97c4dfb245199d395af79a891319ff13964070f7 Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Mon, 9 Sep 2024 14:28:42 +1200 Subject: [PATCH 2/4] Handle object equality with stringify --- .../import/importSurveys/importSurveyQuestions.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js b/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js index 8fd27869b2..62049d3d7b 100644 --- a/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js +++ b/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js @@ -30,12 +30,7 @@ const VIS_CRITERIA_CONJUNCTION = '_conjunction'; const objectsAreEqual = (a, b) => { if (!!a === !!b) return true; - return Object.entries(a).every(([key, value]) => { - if (Array.isArray(value)) { - return value.every((v, i) => v === b[key][i]); - } - return value === b[key]; - }); + return JSON.stringify(a) === JSON.stringify(b); }; const validateQuestionExistence = rows => { @@ -109,7 +104,9 @@ const updateOrCreateSurveyScreenComponent = async ( const changes = {}; if ( !objectsAreEqual( - JSON.parse(existingScreenComponent.visibility_criteria), + existingScreenComponent.visibility_criteria + ? JSON.parse(existingScreenComponent.visibility_criteria) + : {}, processedVisibilityCriteria, ) ) { @@ -118,7 +115,9 @@ const updateOrCreateSurveyScreenComponent = async ( if ( !objectsAreEqual( - JSON.parse(existingScreenComponent.validation_criteria), + existingScreenComponent.validation_criteria + ? JSON.parse(existingScreenComponent.validation_criteria) + : {}, validationCriteriaObject, ) ) { From fcd350e0df569ffd9392538f4a7abfffaf5fb56c Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Fri, 13 Sep 2024 10:40:47 +1200 Subject: [PATCH 3/4] Fix object equality check --- .../import/importSurveys/importSurveyQuestions.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js b/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js index 62049d3d7b..c204f91e8b 100644 --- a/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js +++ b/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js @@ -4,7 +4,7 @@ */ import xlsx from 'xlsx'; - +import { isEqual } from 'lodash'; import { DatabaseError, UploadError, @@ -23,14 +23,14 @@ import { SURVEY_METADATA, } from './processSurveyMetadata'; import { caseAndSpaceInsensitiveEquals, convertCellToJson } from './utilities'; -import { RECORDS } from '@tupaia/database'; const QUESTION_TYPE_LIST = Object.values(ANSWER_TYPES); const VIS_CRITERIA_CONJUNCTION = '_conjunction'; const objectsAreEqual = (a, b) => { - if (!!a === !!b) return true; - return JSON.stringify(a) === JSON.stringify(b); + // If one is falsy and the other is truthy, they are not equal + if (!!a !== !!b) return false; + return isEqual(a, b); }; const validateQuestionExistence = rows => { @@ -132,6 +132,8 @@ const updateOrCreateSurveyScreenComponent = async ( changes.detail_label = detailLabel; } + console.log('changes', changes); + if (Object.keys(changes).length > 0) { await models.surveyScreenComponent.update({ id: existingScreenComponent.id }, changes); } From e56d960c9cbd26f7200be03fba3a5362fefb20f1 Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Fri, 13 Sep 2024 10:41:19 +1200 Subject: [PATCH 4/4] Remove console log --- .../src/apiV2/import/importSurveys/importSurveyQuestions.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js b/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js index c204f91e8b..660a95fd14 100644 --- a/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js +++ b/packages/central-server/src/apiV2/import/importSurveys/importSurveyQuestions.js @@ -132,8 +132,6 @@ const updateOrCreateSurveyScreenComponent = async ( changes.detail_label = detailLabel; } - console.log('changes', changes); - if (Object.keys(changes).length > 0) { await models.surveyScreenComponent.update({ id: existingScreenComponent.id }, changes); }