-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* task creation handler * get answers for task questions * handle saving string values * Update TaskCreationHandler.test.js * Update surveyScreenComponent.ts * move change handler to central server * move helpers to central server * handle primary entity question * generate types * clean up * Update TaskCreationHandler.js * move back to database * fix fe tests * handle success message * handle should_create_task * fix tests * fix tests * remove test code * update types * refactor * Update getTaskQuestionField.ts * remove toast message * hide task questions on the server side * Update SurveyRoute.ts * remove survey_response_id * fix tests * update types * test multiple task questions * update types * Update schemas.ts
- Loading branch information
Showing
6 changed files
with
313 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
184 changes: 184 additions & 0 deletions
184
packages/database/src/__tests__/changeHandlers/TaskCreationHandler.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
/* | ||
* Tupaia | ||
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd | ||
*/ | ||
|
||
import { TaskCreationHandler } from '../../changeHandlers'; | ||
import { | ||
buildAndInsertSurvey, | ||
buildAndInsertSurveyResponses, | ||
getTestModels, | ||
upsertDummyRecord, | ||
} from '../../testUtilities'; | ||
import { generateId } from '../../utilities'; | ||
|
||
const userId = generateId(); | ||
const entityId = generateId(); | ||
const entityCode = 'TO'; | ||
const taskSurveyId = generateId(); | ||
const taskSurveyCode = 'TEST_TASK_SURVEY'; | ||
|
||
const buildEntity = async (models, data) => { | ||
return upsertDummyRecord(models.entity, { id: entityId, code: entityCode, ...data }); | ||
}; | ||
const buildTaskCreationSurvey = async (models, config) => { | ||
const survey = { | ||
id: generateId(), | ||
code: generateId(), | ||
questions: [ | ||
{ | ||
id: 'TEST_ID_00', | ||
code: 'TEST_CODE_00', | ||
type: 'PrimaryEntity', | ||
}, | ||
{ | ||
id: 'TEST_ID_01', | ||
code: 'TEST_CODE_01', | ||
type: 'Binary', | ||
}, | ||
{ | ||
id: 'TEST_ID_02', | ||
code: 'TEST_CODE_02', | ||
type: 'Date', | ||
}, | ||
{ | ||
id: 'TEST_ID_03', | ||
code: 'TEST_CODE_03', | ||
type: 'FreeText', | ||
}, | ||
{ | ||
id: 'TEST_ID_04', | ||
code: 'TEST_CODE_04', | ||
type: 'Task', | ||
surveyScreenComponent: { | ||
config, | ||
}, | ||
}, | ||
{ | ||
id: 'TEST_ID_05', | ||
code: 'TEST_CODE_05', | ||
type: 'Task', | ||
surveyScreenComponent: { | ||
config, | ||
}, | ||
}, | ||
], | ||
}; | ||
|
||
await Promise.all( | ||
survey.questions.map(q => { | ||
return upsertDummyRecord(models.question, q); | ||
}), | ||
); | ||
|
||
return buildAndInsertSurvey(models, survey); | ||
}; | ||
|
||
const buildSurveyResponse = async (models, surveyCode, answers) => { | ||
const surveyResponse = { | ||
date: '2024-07-20', | ||
entityCode, | ||
surveyCode, | ||
answers, | ||
}; | ||
|
||
const surveyResponses = await buildAndInsertSurveyResponses(models, [surveyResponse]); | ||
return surveyResponses[0]; | ||
}; | ||
|
||
const TEST_DATA = [ | ||
[ | ||
'Sets task values based on configured question values', | ||
{ | ||
config: { | ||
task: { | ||
surveyCode: taskSurveyCode, | ||
entityId: { questionId: 'TEST_ID_00' }, | ||
shouldCreateTask: { questionId: 'TEST_ID_01' }, | ||
dueDate: { questionId: 'TEST_ID_02' }, | ||
assignee: { questionId: 'TEST_ID_03' }, | ||
}, | ||
}, | ||
answers: { | ||
TEST_CODE_00: entityId, | ||
TEST_CODE_01: true, | ||
TEST_CODE_02: '2024/06/06 00:00:00+00', | ||
TEST_CODE_03: userId, | ||
}, | ||
}, | ||
{ | ||
survey_id: taskSurveyId, | ||
due_date: '2024-06-06 00:00:00', | ||
assignee_id: userId, | ||
entity_id: entityId, | ||
}, | ||
], | ||
[ | ||
'Handles optional and missing values', | ||
{ | ||
config: { | ||
task: { | ||
surveyCode: taskSurveyCode, | ||
entityId: { questionId: 'TEST_ID_00' }, | ||
}, | ||
}, | ||
answers: { | ||
TEST_CODE_00: entityId, | ||
}, | ||
}, | ||
{ entity_id: entityId, survey_id: taskSurveyId }, | ||
], | ||
]; | ||
|
||
describe('TaskCreationHandler', () => { | ||
const models = getTestModels(); | ||
const taskCreationHandler = new TaskCreationHandler(models); | ||
taskCreationHandler.setDebounceTime(50); // short debounce time so tests run more quickly | ||
|
||
beforeAll(async () => { | ||
await buildEntity(models); | ||
await buildAndInsertSurvey(models, { id: taskSurveyId, code: taskSurveyCode }); | ||
await upsertDummyRecord(models.user, { id: userId }); | ||
}); | ||
|
||
beforeEach(async () => { | ||
taskCreationHandler.listenForChanges(); | ||
}); | ||
|
||
afterEach(async () => { | ||
taskCreationHandler.stopListeningForChanges(); | ||
await models.surveyResponse.delete({ survey_id: taskSurveyId }); | ||
}); | ||
|
||
it.each(TEST_DATA)('%s', async (_name, { config, answers = {} }, result) => { | ||
const { survey } = await buildTaskCreationSurvey(models, config); | ||
await buildSurveyResponse(models, survey.code, answers); | ||
await models.database.waitForAllChangeHandlers(); | ||
const tasks = await models.task.find({ entity_id: entityId }, { sort: ['created_at DESC'] }); | ||
|
||
const { survey_id, entity_id, status, due_date, assignee_id, repeat_schedule } = tasks[0]; | ||
|
||
expect({ | ||
survey_id, | ||
entity_id, | ||
assignee_id, | ||
due_date, | ||
status, | ||
repeat_schedule, | ||
}).toMatchObject({ | ||
repeat_schedule: null, | ||
due_date: null, | ||
status: 'to_do', | ||
...result, | ||
}); | ||
}); | ||
|
||
it('Does not create a task if shouldCreateTask is false', async () => { | ||
const beforeTasks = await models.task.find({ survey_id: taskSurveyId }); | ||
const { survey } = await buildTaskCreationSurvey(models, { shouldCreateTask: 'TEST_01' }); | ||
await buildSurveyResponse(models, survey.code, { TEST_01: false }); | ||
await models.database.waitForAllChangeHandlers(); | ||
const afterTasks = await models.task.find({ survey_id: taskSurveyId }); | ||
expect(beforeTasks.length).toEqual(afterTasks.length); | ||
}); | ||
}); |
110 changes: 110 additions & 0 deletions
110
packages/database/src/changeHandlers/TaskCreationHandler.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
/* | ||
* Tupaia | ||
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd | ||
*/ | ||
import keyBy from 'lodash.keyby'; | ||
import { ChangeHandler } from './ChangeHandler'; | ||
|
||
const getAnswerWrapper = (config, answers) => { | ||
const answersByQuestionId = keyBy(answers, 'question_id'); | ||
|
||
return questionKey => { | ||
const questionId = config[questionKey]?.questionId; | ||
if (!questionId) { | ||
return null; | ||
} | ||
const answer = answersByQuestionId[questionId]; | ||
return answer?.text; | ||
}; | ||
}; | ||
|
||
const isPrimaryEntityQuestion = (config, questions) => { | ||
const primaryEntityQuestion = questions.find(question => question.type === 'PrimaryEntity'); | ||
const { questionId } = config['entityId']; | ||
return primaryEntityQuestion.id === questionId; | ||
}; | ||
|
||
const getSurveyId = async (models, config) => { | ||
const surveyCode = config.surveyCode; | ||
const survey = await models.survey.findOne({ code: surveyCode }); | ||
return survey.id; | ||
}; | ||
|
||
const getQuestions = (models, surveyId) => { | ||
return models.database.executeSql( | ||
` | ||
SELECT q.*, ssc.config::json as config | ||
FROM question q | ||
JOIN survey_screen_component ssc ON ssc.question_id = q.id | ||
JOIN survey_screen ss ON ss.id = ssc.screen_id | ||
WHERE ss.survey_id = ?; | ||
`, | ||
[surveyId], | ||
); | ||
}; | ||
|
||
export class TaskCreationHandler extends ChangeHandler { | ||
constructor(models) { | ||
super(models, 'task-creation-handler'); | ||
|
||
this.changeTranslators = { | ||
surveyResponse: change => this.getNewSurveyResponses(change), | ||
}; | ||
} | ||
|
||
/** | ||
* @private | ||
* Only get the new survey responses that are created, as we only want to create new tasks when a survey response is created, not when it is updated | ||
*/ | ||
getNewSurveyResponses(changeDetails) { | ||
const { type, new_record: newRecord, old_record: oldRecord } = changeDetails; | ||
|
||
// if the change is not a create, we don't need to do anything. This is because once a task is marked as complete, it will never be undone | ||
if (type !== 'update' || !!oldRecord) { | ||
return []; | ||
} | ||
return [newRecord]; | ||
} | ||
|
||
async handleChanges(models, changedResponses) { | ||
// if there are no changed responses, we don't need to do anything | ||
if (changedResponses.length === 0) return; | ||
|
||
for (const response of changedResponses) { | ||
const sr = await models.surveyResponse.findById(response.id); | ||
const questions = await getQuestions(models, response.survey_id); | ||
|
||
const taskQuestions = questions.filter(question => question.type === 'Task'); | ||
|
||
if (!taskQuestions) { | ||
continue; | ||
} | ||
|
||
const answers = await sr.getAnswers(); | ||
|
||
for (const taskQuestion of taskQuestions) { | ||
const config = taskQuestion.config.task; | ||
const getAnswer = getAnswerWrapper(config, answers); | ||
|
||
if (!config || getAnswer('shouldCreateTask') === false) { | ||
continue; | ||
} | ||
|
||
// PrimaryEntity question is a special case, where the entity_id is saved against the survey | ||
// response directly rather than the answers | ||
const entityId = isPrimaryEntityQuestion(config, questions) | ||
? response.entity_id | ||
: getAnswer('entityId'); | ||
const surveyId = await getSurveyId(models, config); | ||
|
||
await models.task.create({ | ||
survey_id: surveyId, | ||
entity_id: entityId, | ||
assignee_id: getAnswer('assignee'), | ||
due_date: getAnswer('dueDate'), | ||
status: 'to_do', | ||
}); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters