diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js new file mode 100644 index 0000000000..adb5bbff10 --- /dev/null +++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js @@ -0,0 +1,55 @@ +import { + createAndPopulateGroup, + translate as t, +} from '../../../../../helpers/api-integration/v3'; +import { find } from 'lodash'; + +describe('POST /tasks/:id/approve/:userId', () => { + let user, guild, member, task; + + function findAssignedTask (memberTask) { + return memberTask.group.id === guild._id; + } + + beforeEach(async () => { + let {group, members, groupLeader} = await createAndPopulateGroup({ + groupDetails: { + name: 'Test Guild', + type: 'guild', + }, + members: 1, + }); + + guild = group; + user = groupLeader; + member = members[0]; + + task = await user.post(`/tasks/group/${guild._id}`, { + text: 'test todo', + type: 'todo', + requiresApproval: true, + }); + + await user.post(`/tasks/${task._id}/assign/${member._id}`); + }); + + it('approves an assigned user', async () => { + let memberTasks = await member.get('/tasks/user'); + let syncedTask = find(memberTasks, findAssignedTask); + + await expect(user.post(`/tasks/${task._id}/approve/${member._id}`)) + .to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('taskRequiresApproval'), + }); + }); + + // it('prevents user from scoring a task that needs to be approved', async () => { + // await user.post(`/tasks/${task._id}/score/up`); + // let task = await user.get(`/tasks/${todo._id}`); + // + // expect(task.completed).to.equal(true); + // expect(task.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type + // }); +}); diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js new file mode 100644 index 0000000000..b0870aec04 --- /dev/null +++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js @@ -0,0 +1,56 @@ +import { + createAndPopulateGroup, + translate as t, +} from '../../../../../helpers/api-integration/v3'; +import { find } from 'lodash'; + +describe('POST /tasks/:id/score/:direction', () => { + let user, guild, member, task; + + function findAssignedTask (memberTask) { + return memberTask.group.id === guild._id; + } + + beforeEach(async () => { + let {group, members, groupLeader} = await createAndPopulateGroup({ + groupDetails: { + name: 'Test Guild', + type: 'guild', + }, + members: 1, + }); + + guild = group; + user = groupLeader; + member = members[0]; + + task = await user.post(`/tasks/group/${guild._id}`, { + text: 'test todo', + type: 'todo', + requiresApproval: true, + }); + + await user.post(`/tasks/${task._id}/assign/${member._id}`); + }); + + it('prevents user from scoring a task that needs to be approved', async () => { + let memberTasks = await member.get('/tasks/user'); + let syncedTask = find(memberTasks, findAssignedTask); + + await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) + .to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('taskRequiresApproval'), + }); + }); + + xit('allows a user to score an apporoved task', async () => { + await user.post(`/tasks/${task._id}/approve/${member._id}`); + await user.post(`/tasks/${task._id}/score/up`); + let updatedTask = await user.get(`/tasks/${task._id}`); + + expect(updatedTask.completed).to.equal(true); + expect(updatedTask.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type + }); +}); diff --git a/website/common/locales/en/tasks.json b/website/common/locales/en/tasks.json index 8ae133571f..e8686b7b1d 100644 --- a/website/common/locales/en/tasks.json +++ b/website/common/locales/en/tasks.json @@ -134,5 +134,6 @@ "strengthExample": "Relating to exercise and activity", "intelligenceExample": "Relating to academic or mentally challenging pursuits", "perceptionExample": "Relating to work or financial tasks", - "constitutionExample": "Relating to health, wellness, and social interaction" + "constitutionExample": "Relating to health, wellness, and social interaction", + "taskRequiresApproval": "This task must be approved before you can complete it." } diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 3eb9b8c134..a254fcc521 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -315,6 +315,10 @@ api.scoreTask = { if (!task) throw new NotFound(res.t('taskNotFound')); + if (task.requiresApproval && !task.approved) { + throw new NotAuthorized(res.t('taskRequiresApproval')); + } + let wasCompleted = task.completed; let [delta] = common.ops.scoreTask({task, user, direction}, req); diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index 92ac10a843..67476b811d 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -178,4 +178,54 @@ api.unassignTask = { }, }; +/** + * @api {post} /api/v3/tasks/:taskId/approve/:userId Approve a user's task + * @apiDescription Approves a user assigned to a group task + * @apiVersion 3.0.0 + * @apiName ApproveTask + * @apiGroup Task + * + * @apiParam {UUID} taskId The id of the task that is the original group task + * @apiParam {UUID} userId The id of the user that will be approved + * + * @apiSuccess task The approved task + */ +api.approveTask = { + method: 'POST', + url: '/tasks/:taskId/approve/:userId', + middlewares: [ensureDevelpmentMode, authWithHeaders()], + async handler (req, res) { + req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID(); + req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID(); + + let reqValidationErrors = req.validationErrors(); + if (reqValidationErrors) throw reqValidationErrors; + + let user = res.locals.user; + let assignedUserId = req.params.assignedUserId; + let assignedUser = await User.findById(assignedUserId); + + let taskId = req.params.taskId; + let task = await Tasks.Task.findByIdOrAlias(taskId, user._id); + + if (!task) { + throw new NotFound(res.t('taskNotFound')); + } + + if (!task.group.id) { + throw new NotAuthorized(res.t('onlyGroupTasksCanBeAssigned')); + } + + let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields}); + if (!group) throw new NotFound(res.t('groupNotFound')); + + if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); + + await group.unlinkTask(task, assignedUser); + + res.respond(200, task); + }, +}; + + module.exports = api; diff --git a/website/server/models/task.js b/website/server/models/task.js index efff2ff61c..cdba5b6f6c 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -71,6 +71,9 @@ export let TaskSchema = new Schema({ taskId: {type: String, ref: 'Task', validate: [validator.isUUID, 'Invalid uuid.']}, }, + requiresApproval: {type: Boolean, default: false}, + approved: {type: Boolean, default: false}, + reminders: [{ _id: false, id: {type: String, validate: [validator.isUUID, 'Invalid uuid.'], default: shared.uuid, required: true},