-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(datatrakWeb): RN-1338: Mark tasks as completed when survey responses are submitted #5766
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice comments