Skip to content
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-1341: Repeating tasks #5844

Merged
merged 31 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1b1fb78
WIP rrule
alexd-bes Aug 14, 2024
1371391
Working rrule utils
alexd-bes Aug 14, 2024
f76f87c
Add generic rrule handler
alexd-bes Aug 14, 2024
d7b90de
frequency enum
alexd-bes Aug 14, 2024
d4b00bc
types
alexd-bes Aug 14, 2024
3f08a8b
handle repeat schedule in task changes
alexd-bes Aug 14, 2024
b4ac8ea
Create and edit repeating tasks
alexd-bes Aug 14, 2024
45f3506
Handle system comments
alexd-bes Aug 14, 2024
f3d7f02
Creating and editing with comments
alexd-bes Aug 15, 2024
408f7fa
WIP
alexd-bes Aug 15, 2024
681d156
Update filtering
alexd-bes Aug 15, 2024
d7fdc37
disable sort by repeat frequency
alexd-bes Aug 15, 2024
35e2661
Add next occurrence handler
alexd-bes Aug 15, 2024
755c120
Apply due date to completed tasks
alexd-bes Aug 15, 2024
0caedc9
Merge branch 'epic-tasks' into rn-1341-rrule-tasks
alexd-bes Aug 15, 2024
351c4fd
Handle bugs
alexd-bes Aug 15, 2024
cb4e8b7
Update tests
alexd-bes Aug 15, 2024
73db3c5
Merge branch 'epic-tasks' into rn-1341-rrule-tasks
alexd-bes Aug 15, 2024
3745b58
Merge branch 'epic-tasks' into rn-1341-rrule-tasks
alexd-bes Aug 15, 2024
a6f809f
Update yarn.lock
alexd-bes Aug 15, 2024
f8c5f1c
Merge branch 'epic-tasks' into rn-1341-rrule-tasks
alexd-bes Aug 15, 2024
5a7afeb
Mark due date of tasks
alexd-bes Aug 15, 2024
d90b81b
Merge branch 'epic-tasks' into rn-1341-rrule-tasks
alexd-bes Aug 16, 2024
5b2ad88
Comments and tidy ups
alexd-bes Aug 16, 2024
b4a1e0b
Use timestamp for due date
alexd-bes Aug 16, 2024
199356b
Fix tests
alexd-bes Aug 16, 2024
72b76e5
Merge branch 'epic-tasks' into rn-1341-rrule-tasks
alexd-bes Aug 22, 2024
680440d
add comments
alexd-bes Aug 22, 2024
651aa7e
Move getRepeatScheduleOptions
alexd-bes Aug 22, 2024
d40e292
Remove duplicate identifier
alexd-bes Aug 22, 2024
989872c
Allow removing of repeat schedule
alexd-bes Aug 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -450,50 +450,8 @@ export const constructForSingle = (models, recordType) => {
entity_id: [constructRecordExistsWithId(models.entity)],
survey_id: [constructRecordExistsWithId(models.survey)],
assignee_id: [constructIsEmptyOr(constructRecordExistsWithId(models.user))],
due_date: [
(value, { repeat_schedule: repeatSchedule }) => {
if (repeatSchedule) {
if (value) {
throw new Error('Recurring tasks must not have a due date');
}
return true;
}
if (!value) throw new Error('Due date is required for non-recurring tasks');
return true;
},
],
repeat_schedule: [
(value, { due_date: dueDate }) => {
// If the task has a due date, the repeat schedule is empty
if (dueDate) {
if (value) {
throw new Error('Non-recurring tasks must not have a repeat schedule');
}
return true;
}

if (!value) {
throw new Error('Repeat schedule is required for recurring tasks');
}
return true;
},
],
status: [
(value, { repeat_schedule: repeatSchedule }) => {
// If the task is recurring, the status is empty
if (repeatSchedule) {
if (value) {
throw new Error('Recurring tasks cannot have a status');
}
return true;
}

if (!value) {
throw new Error('Status is required for non-recurring tasks');
}
return true;
},
],
due_date: [hasContent],
status: [hasContent],
};

default:
Expand Down
3 changes: 2 additions & 1 deletion packages/central-server/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { startSyncWithMs1 } from './ms1';
import { startSyncWithKoBo } from './kobo';
import { startFeedScraper } from './social';
import { createApp } from './createApp';
import { TaskOverdueChecker } from './scheduledTasks';
import { TaskOverdueChecker, RepeatingTaskDueDateHandler } from './scheduledTasks';
import winston from './log';
import { configureEnv } from './configureEnv';

Expand Down Expand Up @@ -74,6 +74,7 @@ configureEnv();
* Scheduled tasks
*/
new TaskOverdueChecker(models).init();
new RepeatingTaskDueDateHandler(models).init();

/**
* Set up actual app with routes etc.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import winston from 'winston';
import { getNextOccurrence } from '@tupaia/utils';
import { ScheduledTask } from './ScheduledTask';

export class RepeatingTaskDueDateHandler extends ScheduledTask {
constructor(models) {
// run RepeatingTaskDueDateHandler every hour
super(models, 'RepeatingTaskDueDateHandler', '0 * * * *');
}

async run() {
const { task } = this.models;
// find all repeating tasks that have passed their current due date
const repeatingTasks = await task.find({
task_status: 'repeating',
due_date: { comparator: '<', comparisonValue: new Date().getTime() },
});

winston.info(`Found ${repeatingTasks.length} repeating task(s)`);

// update the due date for each repeating task to the next occurrence
for (const repeatingTask of repeatingTasks) {
const { repeat_schedule: repeatSchedule } = repeatingTask;

const nextDueDate = getNextOccurrence({
...repeatSchedule,
dtstart: new Date(repeatSchedule.dtstart), // convert string to date because rrule.js expects a Date object
});

repeatingTask.due_date = nextDueDate;
await repeatingTask.save();

winston.info(`Updated due date for task ${repeatingTask.id} to ${nextDueDate}`);
}
}
}
1 change: 1 addition & 0 deletions packages/central-server/src/scheduledTasks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
*/

export { TaskOverdueChecker } from './TaskOverdueChecker';
export { RepeatingTaskDueDateHandler } from './RepeatingTaskDueDateHandler';
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '@tupaia/database';
import { TestableApp, resetTestData } from '../../testUtilities';
import { BES_ADMIN_PERMISSION_GROUP } from '../../../permissions';
import { RRULE_FREQUENCIES } from '@tupaia/utils';

describe('Permissions checker for CreateTaskComment', async () => {
const BES_ADMIN_POLICY = {
Expand Down Expand Up @@ -101,7 +102,9 @@ describe('Permissions checker for CreateTaskComment', async () => {
entity_id: facilities[1].id,
assignee_id: assignee.id,
due_date: null,
repeat_schedule: '{}',
repeat_schedule: {
freq: RRULE_FREQUENCIES.DAILY,
},
status: null,
},
];
Expand Down
12 changes: 8 additions & 4 deletions packages/central-server/src/tests/apiV2/tasks/EditTask.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
findOrCreateDummyRecord,
generateId,
} from '@tupaia/database';
import { RRULE_FREQUENCIES } from '@tupaia/utils';
import { TestableApp, resetTestData } from '../../testUtilities';
import { BES_ADMIN_PERMISSION_GROUP } from '../../../permissions';

Expand Down Expand Up @@ -100,6 +101,7 @@ describe('Permissions checker for EditTask', async () => {
survey_id: surveys[0].survey.id,
entity_id: facilities[0].id,
due_date: dueDate,
repeat_schedule: null,
status: 'to_do',
},
{
Expand All @@ -108,6 +110,7 @@ describe('Permissions checker for EditTask', async () => {
entity_id: facilities[1].id,
assignee_id: assignee.id,
due_date: dueDate,
repeat_schedule: null,
status: 'to_do',
},
];
Expand Down Expand Up @@ -212,20 +215,21 @@ describe('Permissions checker for EditTask', async () => {

describe('System generated comments', () => {
it('Adds a comment when the due date changes on a task', async () => {
const newDate = new Date('2025-11-30').getTime();
await app.grantAccess({
DL: ['Donor'],
TO: ['Donor'],
});
await app.put(`tasks/${tasks[1].id}`, {
body: {
due_date: new Date('2021-11-30').getTime(),
due_date: newDate,
},
});

const comment = await models.taskComment.findOne({
task_id: tasks[1].id,
type: models.taskComment.types.System,
message: 'Changed due date from 31 December 21 to 30 November 21',
message: 'Changed due date from 31 December 21 to 30 November 25',
});
expect(comment).not.to.be.null;
});
Expand All @@ -240,7 +244,7 @@ describe('Permissions checker for EditTask', async () => {
// this is currently null when setting a task to repeat
due_date: null,
repeat_schedule: {
frequency: 'daily',
freq: RRULE_FREQUENCIES.DAILY,
},
},
});
Expand Down Expand Up @@ -269,7 +273,7 @@ describe('Permissions checker for EditTask', async () => {
await app.put(`tasks/${tasks[1].id}`, {
body: {
repeat_schedule: {
frequency: 'daily',
freq: RRULE_FREQUENCIES.DAILY,
},
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
findOrCreateDummyRecord,
generateId,
} from '@tupaia/database';
import { RRULE_FREQUENCIES } from '@tupaia/utils';
import { TestableApp, resetTestData } from '../../testUtilities';
import { BES_ADMIN_PERMISSION_GROUP } from '../../../permissions';

Expand Down Expand Up @@ -104,7 +105,9 @@ describe('Permissions checker for GETTasks', async () => {
entity_id: facilities[1].id,
assignee_id: assignee.id,
due_date: null,
repeat_schedule: '{}',
repeat_schedule: {
freq: RRULE_FREQUENCIES.DAILY,
},
status: null,
},
{
Expand All @@ -113,7 +116,9 @@ describe('Permissions checker for GETTasks', async () => {
entity_id: facilities[0].id,
assignee_id: assignee.id,
due_date: null,
repeat_schedule: '{}',
repeat_schedule: {
freq: RRULE_FREQUENCIES.DAILY,
},
status: null,
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,11 @@ describe('TaskCompletionHandler', () => {
entity_id: samoa.id,
survey_id: SURVEY.id,
created_at: '2024-07-08',
due_date: new Date('2024-07-25').getTime(),
status: null,
repeat_schedule: {
frequency: 'daily',
freq: 1,
dtstart: '2024-07-08',
},
});

Expand All @@ -155,6 +157,11 @@ describe('TaskCompletionHandler', () => {
survey_response_id: responses[0],
entity_id: samoa.id,
parent_task_id: repeatTask.id,
due_date: new Date('2024-07-25').getTime(),
repeat_schedule: {
freq: 1,
dtstart: '2024-07-08',
},
});
await assertTaskStatus(newTask.id, 'completed', responses[0]);
});
Expand All @@ -166,8 +173,10 @@ describe('TaskCompletionHandler', () => {
survey_id: SURVEY.id,
created_at: '2024-07-08',
status: 'to_do',
due_date: new Date('2024-07-08').getTime(),
repeat_schedule: {
frequency: 'daily',
freq: 1,
dtstart: '2024-07-08',
},
});

Expand All @@ -178,6 +187,11 @@ describe('TaskCompletionHandler', () => {
survey_response_id: responses[0],
entity_id: fiji.id,
parent_task_id: repeatTask.id,
due_date: new Date('2024-07-08').getTime(),
repeat_schedule: {
freq: 1,
dtstart: '2024-07-08',
},
});
await assertTaskStatus(newTask.id, 'completed', responses[0]);
});
Expand Down
65 changes: 35 additions & 30 deletions packages/database/src/modelClasses/Task.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { format } from 'date-fns';
import { RRULE_FREQUENCIES } from '@tupaia/utils';
import { DatabaseModel } from '../DatabaseModel';
import { DatabaseRecord } from '../DatabaseRecord';
import { RECORDS } from '../records';
Expand Down Expand Up @@ -47,17 +48,21 @@ const formatValue = async (field, value, models) => {
return assignee.full_name;
}
case 'repeat_schedule': {
if (!value || !value?.frequency) {
if (!value || value.freq === undefined || value.freq === null) {
return "Doesn't repeat";
}

return `${value.frequency.charAt(0).toUpperCase()}${value.frequency.slice(1)}`;
const frequency = Object.keys(RRULE_FREQUENCIES).find(
key => RRULE_FREQUENCIES[key] === value.freq,
);

if (!frequency) {
return "Doesn't repeat";
}

return `${frequency.charAt(0)}${frequency.slice(1).toLowerCase()}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not very obvious what is going on here. Could you add a comment or example of the output format to help?

}
case 'due_date': {
// TODO: Currently repeating tasks don't have a due date, so we need to handle null values. In RN-1341 we will add a due date to repeating tasks overnight, so this will need to be updated then
if (!value) {
return 'No due date';
}
// Format the date as 'd MMMM yy' (e.g. 1 January 21). This is so that there is no ambiguity between US and other date formats
return format(new Date(value), 'd MMMM yy');
}
Expand Down Expand Up @@ -126,6 +131,7 @@ export class TaskRecord extends DatabaseRecord {
entity_id: entityId,
repeat_schedule: repeatSchedule,
assignee_id: assigneeId,
due_date: dueDate,
id,
} = this;

Expand All @@ -144,35 +150,36 @@ export class TaskRecord extends DatabaseRecord {
repeat_schedule: repeatSchedule,
status: 'completed',
survey_response_id: surveyResponseId,
due_date: dueDate,
parent_task_id: id,
};

// Check for an existing task so that multiple tasks aren't created for the same survey response
const existingTask = await this.model.findOne(where);
if (!existingTask) {
const newTask = await this.model.create(where);
await newTask.addComment(
'Completed this task',
commentUserId,
this.otherModels.taskComment.types.System,
);
await this.addComment(
`Completed task ${newTask.id}`,
commentUserId,
this.otherModels.taskComment.types.System,
);
}
} else {
await this.model.updateById(id, {
status: 'completed',
survey_response_id: surveyResponseId,
});

if (existingTask) return;
const newTask = await this.model.create(where);
await newTask.addComment(
'Completed this task',
commentUserId,
this.otherModels.taskComment.types.System,
);
await this.addComment(
'Completed this task',
commentUserId,
this.otherModels.taskComment.types.System,
);
return;
}
await this.model.updateById(id, {
status: 'completed',
survey_response_id: surveyResponseId,
});
await this.addComment(
'Completed this task',
commentUserId,
this.otherModels.taskComment.types.System,
);
}

/**
Expand Down Expand Up @@ -242,12 +249,10 @@ export class TaskRecord extends DatabaseRecord {
const originalValue = this[field];
// If the field hasn't actually changed, don't add a comment
if (originalValue === newValue) continue;

// If the due date is changed and the task is repeating, don't add a comment, because this just means that the repeat schedule is being updated, not that the due date is being changed. This will likely change as part of RN-1341
// TODO: re-evaulate this when RN-1341 is implemented
if (field === 'due_date' && updatedFields.repeat_schedule) {
continue;
}
// Don't add a comment when repeat schedule is updated and the frequency is the same
if (field === 'repeat_schedule' && originalValue?.freq === newValue?.freq) continue;
// Don't add a comment when due date is updated for repeat schedule
if (field === 'due_date' && this.repeat_schedule) continue;

const friendlyFieldName = getFriendlyFieldName(field);
const formattedOriginalValue = await formatValue(field, originalValue, this.otherModels);
Expand Down
Loading