-
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.
feat(datatrakWeb): RN-1338: Mark tasks as completed when survey respo…
…nses are submitted (#5766) * Create migration for created_at column * Create change handler
- Loading branch information
Showing
7 changed files
with
243 additions
and
0 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
112 changes: 112 additions & 0 deletions
112
packages/database/src/__tests__/changeHandlers/TaskCompletionHandler.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,112 @@ | ||
/** | ||
* Tupaia | ||
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd | ||
*/ | ||
|
||
import { TaskCompletionHandler } from '../../changeHandlers'; | ||
import { | ||
buildAndInsertSurveys, | ||
findOrCreateDummyRecord, | ||
getTestModels, | ||
populateTestData, | ||
upsertDummyRecord, | ||
} from '../../testUtilities'; | ||
import { generateId } from '../../utilities'; | ||
|
||
const buildSurvey = () => { | ||
const code = 'Test_survey'; | ||
|
||
return { | ||
id: generateId(), | ||
code, | ||
questions: [{ code: `${code}1`, type: 'Number' }], | ||
}; | ||
}; | ||
|
||
const userId = generateId(); | ||
|
||
const SURVEY = buildSurvey(); | ||
|
||
describe('TaskCompletionHandler', () => { | ||
const models = getTestModels(); | ||
const taskCompletionHandler = new TaskCompletionHandler(models); | ||
taskCompletionHandler.setDebounceTime(50); // short debounce time so tests run more quickly | ||
|
||
const createResponses = async data => { | ||
const { surveyResponses } = await populateTestData(models, { | ||
surveyResponse: data.map(({ date, ...otherFields }) => { | ||
// append time if required | ||
const datetime = date ?? `${date}T12:00:00`.slice(0, 'YYYY-MM-DDThh:mm:ss'.length); | ||
|
||
return { | ||
start_time: datetime, | ||
end_time: datetime, | ||
data_time: datetime, | ||
user_id: userId, | ||
survey_id: SURVEY.id, | ||
...otherFields, | ||
}; | ||
}), | ||
}); | ||
|
||
return surveyResponses.map(sr => sr.id); | ||
}; | ||
|
||
const assertTaskStatus = async (taskId, expectedStatus) => { | ||
await models.database.waitForAllChangeHandlers(); | ||
const task = await models.task.findById(taskId); | ||
|
||
expect(task.status).toBe(expectedStatus); | ||
}; | ||
|
||
let tonga; | ||
let task; | ||
|
||
beforeAll(async () => { | ||
await buildAndInsertSurveys(models, [SURVEY]); | ||
tonga = await findOrCreateDummyRecord(models.entity, { code: 'TO' }); | ||
task = await findOrCreateDummyRecord(models.task, { | ||
entity_id: tonga.id, | ||
survey_id: SURVEY.id, | ||
created_at: '2024-07-08', | ||
status: 'to_do', | ||
due_date: '2024-07-25', | ||
}); | ||
await upsertDummyRecord(models.user, { id: userId }); | ||
}); | ||
|
||
beforeEach(async () => { | ||
taskCompletionHandler.listenForChanges(); | ||
}); | ||
|
||
afterEach(async () => { | ||
taskCompletionHandler.stopListeningForChanges(); | ||
await models.surveyResponse.delete({ survey_id: SURVEY.id }); | ||
await models.task.update({ id: task.id }, { status: 'to_do' }); | ||
}); | ||
|
||
describe('creating a survey response', () => { | ||
it('created response marks associated tasks as completed if created_time < data_time', async () => { | ||
await createResponses([{ entity_id: tonga.id, date: '2024-07-20' }]); | ||
await assertTaskStatus(task.id, 'completed'); | ||
}); | ||
|
||
it('created response marks associated tasks as completed if created_time === data_time', async () => { | ||
await createResponses([{ entity_id: tonga.id, date: '2024-07-08' }]); | ||
await assertTaskStatus(task.id, 'completed'); | ||
}); | ||
|
||
it('created response does not mark associated tasks as completed if created_time > data_time', async () => { | ||
await createResponses([{ entity_id: tonga.id, date: '2021-07-08' }]); | ||
await assertTaskStatus(task.id, 'to_do'); | ||
}); | ||
}); | ||
|
||
describe('updating a survey response', () => { | ||
it('updating a survey response does not mark a task as completed', async () => { | ||
await createResponses([{ entity_id: tonga.id, date: '2021-07-20' }]); | ||
await models.surveyResponse.update({ entity_id: tonga.id }, { data_time: '2024-07-25' }); | ||
await assertTaskStatus(task.id, 'to_do'); | ||
}); | ||
}); | ||
}); |
79 changes: 79 additions & 0 deletions
79
packages/database/src/changeHandlers/TaskCompletionHandler.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,79 @@ | ||
/** | ||
* Tupaia | ||
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd | ||
*/ | ||
|
||
import { getUniqueEntries } from '@tupaia/utils'; | ||
import { ChangeHandler } from './ChangeHandler'; | ||
import { QUERY_CONJUNCTIONS } from '../TupaiaDatabase'; | ||
|
||
export class TaskCompletionHandler extends ChangeHandler { | ||
constructor(models) { | ||
super(models, 'task-completion-handler'); | ||
|
||
this.changeTranslators = { | ||
surveyResponse: change => this.getNewSurveyResponses(change), | ||
}; | ||
} | ||
|
||
/** | ||
* @private | ||
* Only get the new survey responses that are created, as we only want to mark tasks as completed 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]; | ||
} | ||
|
||
/** | ||
* @private Fetches all tasks that have the same survey_id and entity_id as the survey responses, and have a created_at date that is less than or equal to the data_time of the survey response | ||
*/ | ||
async fetchTaskIdsToUpdate(surveyResponses) { | ||
const surveyIdAndEntityIdPairs = getUniqueEntries( | ||
surveyResponses.map(surveyResponse => ({ | ||
surveyId: surveyResponse.survey_id, | ||
entityId: surveyResponse.entity_id, | ||
dataTime: surveyResponse.data_time, | ||
})), | ||
); | ||
|
||
const tasks = await this.models.task.find({ | ||
// only fetch tasks that have a status of 'to_do' | ||
status: 'to_do', | ||
[QUERY_CONJUNCTIONS.RAW]: { | ||
sql: `${surveyIdAndEntityIdPairs | ||
.map(() => `(survey_id = ? AND entity_id = ? AND created_at <= ?)`) | ||
.join(' OR ')}`, | ||
parameters: surveyIdAndEntityIdPairs.flatMap(({ surveyId, entityId, dataTime }) => [ | ||
surveyId, | ||
entityId, | ||
dataTime, | ||
]), | ||
}, | ||
}); | ||
|
||
return tasks.map(task => task.id); | ||
} | ||
|
||
async handleChanges(transactingModels, changedResponses) { | ||
// if there are no changed responses, we don't need to do anything | ||
if (changedResponses.length === 0) return; | ||
const taskIdsToUpdate = await this.fetchTaskIdsToUpdate(changedResponses); | ||
|
||
// if there are no tasks to update, we don't need to do anything | ||
if (taskIdsToUpdate.length === 0) return; | ||
|
||
// update the tasks to be completed | ||
await transactingModels.task.update( | ||
{ | ||
id: taskIdsToUpdate, | ||
}, | ||
{ status: 'completed' }, | ||
); | ||
} | ||
} |
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
30 changes: 30 additions & 0 deletions
30
packages/database/src/migrations/20240707213310-AddCreatedAtToTasks-modifies-schema.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,30 @@ | ||
'use strict'; | ||
|
||
var dbm; | ||
var type; | ||
var seed; | ||
|
||
/** | ||
* We receive the dbmigrate dependency from dbmigrate initially. | ||
* This enables us to not have to rely on NODE_PATH. | ||
*/ | ||
exports.setup = function (options, seedLink) { | ||
dbm = options.dbmigrate; | ||
type = dbm.dataType; | ||
seed = seedLink; | ||
}; | ||
|
||
exports.up = function (db) { | ||
return db.runSql(` | ||
ALTER TABLE task | ||
ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT now(); | ||
`); | ||
}; | ||
|
||
exports.down = function (db) { | ||
return db.runSql('ALTER TABLE task DROP COLUMN created_at;'); | ||
}; | ||
|
||
exports._meta = { | ||
version: 1, | ||
}; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.