From 97bf9ee8e8be3a27dd63dcd148d7a71a5fef7622 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sat, 17 Sep 2016 16:31:04 -0500 Subject: [PATCH 01/13] Added inital group task approval --- ...POST-group_tasks_id_approve_userId.test.js | 55 ++++++++++++++++++ ...OST-group_tasks_id_score_direction.test.js | 56 +++++++++++++++++++ website/common/locales/en/tasks.json | 3 +- website/server/controllers/api-v3/tasks.js | 4 ++ .../server/controllers/api-v3/tasks/groups.js | 50 +++++++++++++++++ website/server/models/task.js | 3 + 6 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js create mode 100644 test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js 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}, From 9b515ebdd189a80003ea98ebdf7313457e0c53a9 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sat, 17 Sep 2016 16:47:42 -0500 Subject: [PATCH 02/13] Added task approve route --- ...POST-group_tasks_id_approve_userId.test.js | 42 +++++++++++-------- .../server/controllers/api-v3/tasks/groups.js | 17 ++++---- 2 files changed, 33 insertions(+), 26 deletions(-) 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 index adb5bbff10..2409b7c324 100644 --- 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 @@ -29,27 +29,35 @@ describe('POST /tasks/:id/approve/:userId', () => { 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); - + it('errors when user is not assigned', async () => { await expect(user.post(`/tasks/${task._id}/approve/${member._id}`)) - .to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskRequiresApproval'), + .to.eventually.be.rejected.and.to.eql({ + code: 404, + error: 'NotFound', + message: t('taskNotFound'), }); }); - // 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 - // }); + it('errors when user is not the group leader', async () => { + await user.post(`/tasks/${task._id}/assign/${member._id}`); + await expect(member.post(`/tasks/${task._id}/approve/${member._id}`)) + .to.eventually.be.rejected.and.to.eql({ + code: 401, + error: 'NotAuthorized', + message: t('onlyGroupLeaderCanEditTasks'), + }); + }); + + + it('approves an assigned user', async () => { + await user.post(`/tasks/${task._id}/assign/${member._id}`); + await user.post(`/tasks/${task._id}/approve/${member._id}`); + + let memberTasks = await member.get('/tasks/user'); + let syncedTask = find(memberTasks, findAssignedTask); + + expect(syncedTask.approved).to.be.true; + }); }); diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index 67476b811d..5f5c717f35 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -196,32 +196,31 @@ api.approveTask = { middlewares: [ensureDevelpmentMode, authWithHeaders()], async handler (req, res) { req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID(); - req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID(); + req.checkParams('userId', 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 assignedUserId = req.params.userId; let taskId = req.params.taskId; - let task = await Tasks.Task.findByIdOrAlias(taskId, user._id); + let task = await Tasks.Task.findOne({ + 'group.taskId': taskId, + 'userId': assignedUserId, + }); 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); + task.approved = true; + await task.save(); res.respond(200, task); }, From ad5045bc09ac6521569f914c2001471dc5c20065 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sat, 17 Sep 2016 16:50:15 -0500 Subject: [PATCH 03/13] Added git score approved task test --- .../groups/POST-group_tasks_id_score_direction.test.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 index b0870aec04..c8e05d3d28 100644 --- 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 @@ -45,10 +45,14 @@ describe('POST /tasks/:id/score/:direction', () => { }); }); - xit('allows a user to score an apporoved task', async () => { + it('allows a user to score an apporoved task', async () => { + let memberTasks = await member.get('/tasks/user'); + let syncedTask = find(memberTasks, findAssignedTask); + 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}`); + + await member.post(`/tasks/${syncedTask._id}/score/up`); + let updatedTask = await member.get(`/tasks/${syncedTask._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 From 393a9290e910f1288cb211ad8907c527a97353ac Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sun, 18 Sep 2016 12:37:07 -0500 Subject: [PATCH 04/13] Added approval test and fixed line endings --- website/server/controllers/api-v3/tasks/groups.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index 5f5c717f35..426ac0bbfc 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -207,7 +207,7 @@ api.approveTask = { let taskId = req.params.taskId; let task = await Tasks.Task.findOne({ 'group.taskId': taskId, - 'userId': assignedUserId, + userId: assignedUserId, }); if (!task) { From f2e5bc52e50598d2dcfa02e211e82bc9d55ffec6 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sat, 8 Oct 2016 06:29:23 -0500 Subject: [PATCH 05/13] Added requested approval fields and logic --- .../POST-group_tasks_id_score_direction.test.js | 14 ++++++++++++++ website/server/controllers/api-v3/tasks.js | 8 +++++++- website/server/models/task.js | 4 ++++ 3 files changed, 25 insertions(+), 1 deletion(-) 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 index c8e05d3d28..71eb8e8d40 100644 --- 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 @@ -37,6 +37,20 @@ describe('POST /tasks/:id/score/:direction', () => { let memberTasks = await member.get('/tasks/user'); let syncedTask = find(memberTasks, findAssignedTask); + let response = await member.post(`/tasks/${syncedTask._id}/score/up`); + let updatedTask = await member.get(`/tasks/${syncedTask._id}`); + + expect(response.message).to.equal(t('taskRequiresApproval')); + expect(updatedTask.approvalRequested).to.equal(true); + expect(updatedTask.approvalRequestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type + }); + + it('errors when approval has already been requested', async () => { + let memberTasks = await member.get('/tasks/user'); + let syncedTask = find(memberTasks, findAssignedTask); + + await member.post(`/tasks/${syncedTask._id}/score/up`); + await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) .to.eventually.be.rejected.and.eql({ code: 401, diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index a254fcc521..93537a3c65 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -316,7 +316,13 @@ api.scoreTask = { if (!task) throw new NotFound(res.t('taskNotFound')); if (task.requiresApproval && !task.approved) { - throw new NotAuthorized(res.t('taskRequiresApproval')); + if (task.approvalRequested) { + throw new NotAuthorized(res.t('taskRequiresApproval')); + } + task.approvalRequested = true; + task.approvalRequestedDate = new Date(); + await task.save(); + return res.respond(200, {message: res.t('taskRequiresApproval'), task}); } let wasCompleted = task.completed; diff --git a/website/server/models/task.js b/website/server/models/task.js index cdba5b6f6c..ad31454391 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -73,6 +73,10 @@ export let TaskSchema = new Schema({ requiresApproval: {type: Boolean, default: false}, approved: {type: Boolean, default: false}, + approvedDate: {type: Date}, + approvingUser: [{type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.']}], + approvalRequested: {type: Boolean, default: false}, + approvalRequestedDate: {type: Date}, reminders: [{ _id: false, From 2173f53883a1c11d3a19e5c0260a8fa4e671ec47 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sat, 8 Oct 2016 06:35:53 -0500 Subject: [PATCH 06/13] Added fields for more approver details --- .../tasks/groups/POST-group_tasks_id_approve_userId.test.js | 2 ++ website/server/controllers/api-v3/tasks/groups.js | 2 ++ website/server/models/task.js | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) 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 index 2409b7c324..bc7cdf5522 100644 --- 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 @@ -59,5 +59,7 @@ describe('POST /tasks/:id/approve/:userId', () => { let syncedTask = find(memberTasks, findAssignedTask); expect(syncedTask.approved).to.be.true; + expect(syncedTask.approvingUser).to.equal(user._id); + expect(syncedTask.approvedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type }); }); diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index 426ac0bbfc..7f160133a1 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -219,6 +219,8 @@ api.approveTask = { if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); + task.approvedDate = new Date(); + task.approvingUser = user._id; task.approved = true; await task.save(); diff --git a/website/server/models/task.js b/website/server/models/task.js index ad31454391..dfe9bc593f 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -74,7 +74,7 @@ export let TaskSchema = new Schema({ requiresApproval: {type: Boolean, default: false}, approved: {type: Boolean, default: false}, approvedDate: {type: Date}, - approvingUser: [{type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.']}], + approvingUser: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.']}, approvalRequested: {type: Boolean, default: false}, approvalRequestedDate: {type: Date}, From 016de411c9dba5ecb6ddb7b1b05513122393df19 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sat, 8 Oct 2016 07:35:38 -0500 Subject: [PATCH 07/13] Added notifications --- .../POST-group_tasks_id_approve_userId.test.js | 6 ++++++ .../POST-group_tasks_id_score_direction.test.js | 11 ++++++++++- website/common/locales/en/groups.json | 4 +++- website/common/locales/en/tasks.json | 3 ++- website/server/controllers/api-v3/tasks.js | 17 +++++++++++++++-- .../server/controllers/api-v3/tasks/groups.js | 7 ++++++- website/server/models/userNotification.js | 1 + 7 files changed, 43 insertions(+), 6 deletions(-) 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 index bc7cdf5522..a3a3e031f2 100644 --- 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 @@ -58,6 +58,12 @@ describe('POST /tasks/:id/approve/:userId', () => { let memberTasks = await member.get('/tasks/user'); let syncedTask = find(memberTasks, findAssignedTask); + await member.sync(); + + expect(member.notifications.length).to.equal(1); + expect(member.notifications[0].type).to.equal('GROUP'); + expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved')); + expect(syncedTask.approved).to.be.true; expect(syncedTask.approvingUser).to.equal(user._id); expect(syncedTask.approvedDate).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 index 71eb8e8d40..89a5407091 100644 --- 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 @@ -40,7 +40,16 @@ describe('POST /tasks/:id/score/:direction', () => { let response = await member.post(`/tasks/${syncedTask._id}/score/up`); let updatedTask = await member.get(`/tasks/${syncedTask._id}`); - expect(response.message).to.equal(t('taskRequiresApproval')); + await user.sync(); + + expect(user.notifications.length).to.equal(1); + expect(user.notifications[0].type).to.equal('GROUP'); + expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', { + user: member.auth.local.username, + taskName: updatedTask.text, + })); + + expect(response.message).to.equal(t('taskApprovalHasBeenRequested')); expect(updatedTask.approvalRequested).to.equal(true); expect(updatedTask.approvalRequestedDate).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/groups.json b/website/common/locales/en/groups.json index d750b4aec8..11ea380b05 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -215,5 +215,7 @@ "confirmRemoveTag": "Do you really want to remove \"<%= tag %>\"?", "assignTask": "Assign Task", "desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.

You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.

This box will close automatically when a decision is made.", - "claim": "Claim" + "claim": "Claim", + "yourTaskHasBeenApproved": "Your task has been approved", + "userHasRequestedTaskApproval": "<%= user %> has requested task approval for <%= taskName %>" } diff --git a/website/common/locales/en/tasks.json b/website/common/locales/en/tasks.json index e8686b7b1d..be0578c984 100644 --- a/website/common/locales/en/tasks.json +++ b/website/common/locales/en/tasks.json @@ -135,5 +135,6 @@ "intelligenceExample": "Relating to academic or mentally challenging pursuits", "perceptionExample": "Relating to work or financial tasks", "constitutionExample": "Relating to health, wellness, and social interaction", - "taskRequiresApproval": "This task must be approved before you can complete it." + "taskRequiresApproval": "This task must be approved before you can complete it. Approval has already been requested", + "taskApprovalHasBeenRequested": "Approval has been requested" } diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 93537a3c65..f14e2cba2c 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -7,6 +7,7 @@ import { removeFromArray } from '../../libs/collectionManipulators'; import * as Tasks from '../../models/task'; import { model as Challenge } from '../../models/challenge'; import { model as Group } from '../../models/group'; +import { model as User } from '../../models/user'; import { NotFound, NotAuthorized, @@ -319,10 +320,22 @@ api.scoreTask = { if (task.approvalRequested) { throw new NotAuthorized(res.t('taskRequiresApproval')); } + task.approvalRequested = true; task.approvalRequestedDate = new Date(); - await task.save(); - return res.respond(200, {message: res.t('taskRequiresApproval'), task}); + + let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields}); + let groupLeader = await User.findById(group.leader); //Use this method so we can get access to notifications + groupLeader.addNotification('GROUP', { + message: res.t('userHasRequestedTaskApproval', { + user: user.auth.local.username, + taskName: task.text, + }), + }); + + await Bluebird.all([groupLeader.save(), task.save()]); + + return res.respond(200, {message: res.t('taskApprovalHasBeenRequested'), task}); } let wasCompleted = task.completed; diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index 7f160133a1..f83d8c2a68 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -1,5 +1,6 @@ import { authWithHeaders } from '../../../middlewares/auth'; import ensureDevelpmentMode from '../../../middlewares/ensureDevelpmentMode'; +import Bluebird from 'bluebird'; import * as Tasks from '../../../models/task'; import { model as Group } from '../../../models/group'; import { model as User } from '../../../models/user'; @@ -203,6 +204,7 @@ api.approveTask = { let user = res.locals.user; let assignedUserId = req.params.userId; + let assignedUser = await User.findById(assignedUserId); let taskId = req.params.taskId; let task = await Tasks.Task.findOne({ @@ -222,7 +224,10 @@ api.approveTask = { task.approvedDate = new Date(); task.approvingUser = user._id; task.approved = true; - await task.save(); + + assignedUser.addNotification('GROUP', {message: res.t('yourTaskHasBeenApproved')}); + + await Bluebird.all([assignedUser.save(), task.save()]); res.respond(200, task); }, diff --git a/website/server/models/userNotification.js b/website/server/models/userNotification.js index 9d72831a13..116936cb51 100644 --- a/website/server/models/userNotification.js +++ b/website/server/models/userNotification.js @@ -12,6 +12,7 @@ const NOTIFICATION_TYPES = [ 'REBIRTH_ACHIEVEMENT', 'NEW_CONTRIBUTOR_LEVEL', 'CRON', + 'GROUP', ]; const Schema = mongoose.Schema; From 6cbddef627875bcd3f34009b2c1ddd7624969cc0 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sat, 8 Oct 2016 08:03:08 -0500 Subject: [PATCH 08/13] Added get approvals route --- .../groups/GET-approvals_group_id.test.js | 55 +++++++++++++++++++ ...POST-group_tasks_id_approve_userId.test.js | 1 - .../server/controllers/api-v3/tasks/groups.js | 38 +++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js diff --git a/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js b/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js new file mode 100644 index 0000000000..d8029bc68f --- /dev/null +++ b/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js @@ -0,0 +1,55 @@ +import { + createAndPopulateGroup, + translate as t, +} from '../../../../../helpers/api-integration/v3'; +import { v4 as generateUUID } from 'uuid'; +import { find } from 'lodash'; + +describe('GET /approvals/group/:groupId', () => { + let user, guild, member, task, syncedTask; + + 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}`); + + let memberTasks = await member.get('/tasks/user'); + syncedTask = find(memberTasks, findAssignedTask); + await member.post(`/tasks/${syncedTask._id}/score/up`); + }); + + it('errors when user is not the group leader', async () => { + await expect(member.get(`/approvals/group/${guild._id}`)) + .to.eventually.be.rejected.and.to.eql({ + code: 401, + error: 'NotAuthorized', + message: t('onlyGroupLeaderCanEditTasks'), + }); + }); + + it('gets a list of task that need approval', async () => { + let apporovals = await user.get(`/approvals/group/${guild._id}`); + + expect(apporovals[0]._id).to.equal(syncedTask._id); + }); +}); 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 index a3a3e031f2..daf2a478ed 100644 --- 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 @@ -50,7 +50,6 @@ describe('POST /tasks/:id/approve/:userId', () => { }); }); - it('approves an assigned user', async () => { await user.post(`/tasks/${task._id}/assign/${member._id}`); await user.post(`/tasks/${task._id}/approve/${member._id}`); diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index f83d8c2a68..78d978f77a 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -233,5 +233,43 @@ api.approveTask = { }, }; +/** + * @api {get} /api/v3/approvals/group/:groupId Get a group's approvals + * @apiVersion 3.0.0 + * @apiName GetGroupApprovals + * @apiGroup Task + * @apiIgnore + * + * @apiParam {UUID} groupId The id of the group from which to retrieve the approvals + * + * @apiSuccess {Array} data An array of tasks + */ +api.getGroupApprovals = { + method: 'GET', + url: '/approvals/group/:groupId', + middlewares: [ensureDevelpmentMode, authWithHeaders()], + async handler (req, res) { + req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID(); + + let validationErrors = req.validationErrors(); + if (validationErrors) throw validationErrors; + + let user = res.locals.user; + let groupId = req.params.groupId; + + let group = await Group.getGroup({user, groupId, fields: requiredGroupFields}); + if (!group) throw new NotFound(res.t('groupNotFound')); + + if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); + + let approvals = await Tasks.Task.find({ + 'group.id': groupId, + approved: false, + approvalRequested: true, + }).exec(); + + res.respond(200, approvals); + }, +}; module.exports = api; From d798ebadfe8e9c8151631bf7d05bfe5f5ea448a7 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sat, 8 Oct 2016 10:46:11 -0500 Subject: [PATCH 09/13] Fixed line endings --- .../v3/integration/tasks/groups/GET-approvals_group_id.test.js | 1 - website/server/controllers/api-v3/tasks.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js b/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js index d8029bc68f..c7d5bd7eb9 100644 --- a/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js +++ b/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js @@ -2,7 +2,6 @@ import { createAndPopulateGroup, translate as t, } from '../../../../../helpers/api-integration/v3'; -import { v4 as generateUUID } from 'uuid'; import { find } from 'lodash'; describe('GET /approvals/group/:groupId', () => { diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index f14e2cba2c..bf91c62103 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -325,7 +325,7 @@ api.scoreTask = { task.approvalRequestedDate = new Date(); let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields}); - let groupLeader = await User.findById(group.leader); //Use this method so we can get access to notifications + let groupLeader = await User.findById(group.leader); // Use this method so we can get access to notifications groupLeader.addNotification('GROUP', { message: res.t('userHasRequestedTaskApproval', { user: user.auth.local.username, From 3ec37220386e91029626368adb38a7912b44b133 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sat, 22 Oct 2016 13:07:26 -0500 Subject: [PATCH 10/13] Moved approval fields to group subdoc --- .../POST-group_tasks_id_approve_userId.test.js | 6 +++--- .../POST-group_tasks_id_score_direction.test.js | 4 ++-- website/server/controllers/api-v3/tasks.js | 8 ++++---- website/server/controllers/api-v3/tasks/groups.js | 10 +++++----- website/server/libs/taskManager.js | 4 ++++ website/server/models/group.js | 2 ++ website/server/models/task.js | 13 ++++++------- 7 files changed, 26 insertions(+), 21 deletions(-) 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 index daf2a478ed..247553f5f6 100644 --- 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 @@ -63,8 +63,8 @@ describe('POST /tasks/:id/approve/:userId', () => { expect(member.notifications[0].type).to.equal('GROUP'); expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved')); - expect(syncedTask.approved).to.be.true; - expect(syncedTask.approvingUser).to.equal(user._id); - expect(syncedTask.approvedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type + expect(syncedTask.group.approved).to.be.true; + expect(syncedTask.group.approvingUser).to.equal(user._id); + expect(syncedTask.group.approvedDate).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 index 89a5407091..82ee1df755 100644 --- 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 @@ -50,8 +50,8 @@ describe('POST /tasks/:id/score/:direction', () => { })); expect(response.message).to.equal(t('taskApprovalHasBeenRequested')); - expect(updatedTask.approvalRequested).to.equal(true); - expect(updatedTask.approvalRequestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type + expect(updatedTask.group.approvalRequested).to.equal(true); + expect(updatedTask.group.approvalRequestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type }); it('errors when approval has already been requested', async () => { diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index bf91c62103..2d102e8be0 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -316,13 +316,13 @@ api.scoreTask = { if (!task) throw new NotFound(res.t('taskNotFound')); - if (task.requiresApproval && !task.approved) { - if (task.approvalRequested) { + if (task.group.requiresApproval && !task.group.approved) { + if (task.group.approvalRequested) { throw new NotAuthorized(res.t('taskRequiresApproval')); } - task.approvalRequested = true; - task.approvalRequestedDate = new Date(); + task.group.approvalRequested = true; + task.group.approvalRequestedDate = new Date(); let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields}); let groupLeader = await User.findById(group.leader); // Use this method so we can get access to notifications diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index 78d978f77a..2e9bf77114 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -221,9 +221,9 @@ api.approveTask = { if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); - task.approvedDate = new Date(); - task.approvingUser = user._id; - task.approved = true; + task.group.approvedDate = new Date(); + task.group.approvingUser = user._id; + task.group.approved = true; assignedUser.addNotification('GROUP', {message: res.t('yourTaskHasBeenApproved')}); @@ -264,8 +264,8 @@ api.getGroupApprovals = { let approvals = await Tasks.Task.find({ 'group.id': groupId, - approved: false, - approvalRequested: true, + 'group.approved': false, + 'group.approvalRequested': true, }).exec(); res.respond(200, approvals); diff --git a/website/server/libs/taskManager.js b/website/server/libs/taskManager.js index 1ba5ef587c..e5edd43d2d 100644 --- a/website/server/libs/taskManager.js +++ b/website/server/libs/taskManager.js @@ -51,6 +51,10 @@ export async function createTasks (req, res, options = {}) { let taskType = taskData.type; let newTask = new Tasks[taskType](Tasks.Task.sanitize(taskData)); + if (taskData.requiresApproval) { + newTask.group.requiresApproval = true; + } + if (challenge) { newTask.challenge.id = challenge.id; } else if (group) { diff --git a/website/server/models/group.js b/website/server/models/group.js index da25eef480..57b1492275 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -938,6 +938,8 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, user) { if (orderList.indexOf(matchingTask._id) === -1 && (matchingTask.type !== 'todo' || !matchingTask.completed)) orderList.push(matchingTask._id); } + matchingTask.group.requiresApproval = taskToSync.group.requiresApproval; + if (!matchingTask.notes) matchingTask.notes = taskToSync.notes; // don't override the notes, but provide it if not provided if (matchingTask.tags.indexOf(group._id) === -1) matchingTask.tags.push(group._id); // add tag if missing diff --git a/website/server/models/task.js b/website/server/models/task.js index dfe9bc593f..4ff7c6eded 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -69,15 +69,14 @@ export let TaskSchema = new Schema({ broken: {type: String, enum: ['GROUP_DELETED', 'TASK_DELETED', 'UNSUBSCRIBED']}, assignedUsers: [{type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.']}], taskId: {type: String, ref: 'Task', validate: [validator.isUUID, 'Invalid uuid.']}, + requiresApproval: {type: Boolean, default: false}, + approved: {type: Boolean, default: false}, + approvedDate: {type: Date}, + approvingUser: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.']}, + approvalRequested: {type: Boolean, default: false}, + approvalRequestedDate: {type: Date}, }, - requiresApproval: {type: Boolean, default: false}, - approved: {type: Boolean, default: false}, - approvedDate: {type: Date}, - approvingUser: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.']}, - approvalRequested: {type: Boolean, default: false}, - approvalRequestedDate: {type: Date}, - reminders: [{ _id: false, id: {type: String, validate: [validator.isUUID, 'Invalid uuid.'], default: shared.uuid, required: true}, From 5b240a1950d13afc106a4d65c24cc16262f47438 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Tue, 25 Oct 2016 07:56:43 -0500 Subject: [PATCH 11/13] Updated notification name and other minor fixes --- .../tasks/groups/GET-approvals_group_id.test.js | 5 ++--- .../groups/POST-group_tasks_id_approve_userId.test.js | 2 +- .../groups/POST-group_tasks_id_score_direction.test.js | 2 +- website/server/controllers/api-v3/tasks.js | 4 ++-- website/server/controllers/api-v3/tasks/groups.js | 4 ++-- website/server/libs/taskManager.js | 8 ++++---- website/server/models/userNotification.js | 2 +- 7 files changed, 13 insertions(+), 14 deletions(-) diff --git a/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js b/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js index c7d5bd7eb9..cf5935e443 100644 --- a/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js +++ b/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js @@ -47,8 +47,7 @@ describe('GET /approvals/group/:groupId', () => { }); it('gets a list of task that need approval', async () => { - let apporovals = await user.get(`/approvals/group/${guild._id}`); - - expect(apporovals[0]._id).to.equal(syncedTask._id); + let approvals = await user.get(`/approvals/group/${guild._id}`); + expect(approvals[0]._id).to.equal(syncedTask._id); }); }); 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 index 247553f5f6..2f63df65d5 100644 --- 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 @@ -60,7 +60,7 @@ describe('POST /tasks/:id/approve/:userId', () => { await member.sync(); expect(member.notifications.length).to.equal(1); - expect(member.notifications[0].type).to.equal('GROUP'); + expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVAL'); expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved')); expect(syncedTask.group.approved).to.be.true; 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 index 82ee1df755..4a565f2bac 100644 --- 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 @@ -43,7 +43,7 @@ describe('POST /tasks/:id/score/:direction', () => { await user.sync(); expect(user.notifications.length).to.equal(1); - expect(user.notifications[0].type).to.equal('GROUP'); + expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL'); expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', { user: member.auth.local.username, taskName: updatedTask.text, diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 2d102e8be0..0a4d161b9c 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -326,9 +326,9 @@ api.scoreTask = { let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields}); let groupLeader = await User.findById(group.leader); // Use this method so we can get access to notifications - groupLeader.addNotification('GROUP', { + groupLeader.addNotification('GROUP_TASK_APPROVAL', { message: res.t('userHasRequestedTaskApproval', { - user: user.auth.local.username, + user: user.profile.name, taskName: task.text, }), }); diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index 2e9bf77114..7aad25ca59 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -225,7 +225,7 @@ api.approveTask = { task.group.approvingUser = user._id; task.group.approved = true; - assignedUser.addNotification('GROUP', {message: res.t('yourTaskHasBeenApproved')}); + assignedUser.addNotification('GROUP_TASK_APPROVAL', {message: res.t('yourTaskHasBeenApproved')}); await Bluebird.all([assignedUser.save(), task.save()]); @@ -266,7 +266,7 @@ api.getGroupApprovals = { 'group.id': groupId, 'group.approved': false, 'group.approvalRequested': true, - }).exec(); + }, 'userId group').exec(); res.respond(200, approvals); }, diff --git a/website/server/libs/taskManager.js b/website/server/libs/taskManager.js index e5edd43d2d..877541ad72 100644 --- a/website/server/libs/taskManager.js +++ b/website/server/libs/taskManager.js @@ -31,6 +31,7 @@ async function _validateTaskAlias (tasks, res) { * @param options.user The user that these tasks belong to * @param options.challenge The challenge that these tasks belong to * @param options.group The group that these tasks belong to + * @param options.requiresApproval A boolean stating if the task will require approval * @return The created tasks */ export async function createTasks (req, res, options = {}) { @@ -51,14 +52,13 @@ export async function createTasks (req, res, options = {}) { let taskType = taskData.type; let newTask = new Tasks[taskType](Tasks.Task.sanitize(taskData)); - if (taskData.requiresApproval) { - newTask.group.requiresApproval = true; - } - if (challenge) { newTask.challenge.id = challenge.id; } else if (group) { newTask.group.id = group._id; + if (taskData.requiresApproval) { + newTask.group.requiresApproval = true; + } } else { newTask.userId = user._id; } diff --git a/website/server/models/userNotification.js b/website/server/models/userNotification.js index 116936cb51..0b338b2e57 100644 --- a/website/server/models/userNotification.js +++ b/website/server/models/userNotification.js @@ -12,7 +12,7 @@ const NOTIFICATION_TYPES = [ 'REBIRTH_ACHIEVEMENT', 'NEW_CONTRIBUTOR_LEVEL', 'CRON', - 'GROUP', + 'GROUP_TASK_APPROVAL', ]; const Schema = mongoose.Schema; From 59e1de6771badc023992ab7dc52bc73a63551c2d Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Wed, 26 Oct 2016 16:01:43 -0500 Subject: [PATCH 12/13] Moved approval to subdoc --- .../POST-group_tasks_id_approve_userId.test.js | 6 +++--- .../POST-group_tasks_id_score_direction.test.js | 4 ++-- website/server/controllers/api-v3/tasks.js | 8 ++++---- website/server/controllers/api-v3/tasks/groups.js | 10 +++++----- website/server/libs/taskManager.js | 2 +- website/server/models/group.js | 2 +- website/server/models/task.js | 14 ++++++++------ 7 files changed, 24 insertions(+), 22 deletions(-) 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 index 2f63df65d5..0fa9807e07 100644 --- 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 @@ -63,8 +63,8 @@ describe('POST /tasks/:id/approve/:userId', () => { expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVAL'); expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved')); - expect(syncedTask.group.approved).to.be.true; - expect(syncedTask.group.approvingUser).to.equal(user._id); - expect(syncedTask.group.approvedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type + expect(syncedTask.group.approval.approved).to.be.true; + expect(syncedTask.group.approval.approvingUser).to.equal(user._id); + expect(syncedTask.group.approval.dateApproved).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 index 4a565f2bac..8a42ae9f75 100644 --- 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 @@ -50,8 +50,8 @@ describe('POST /tasks/:id/score/:direction', () => { })); expect(response.message).to.equal(t('taskApprovalHasBeenRequested')); - expect(updatedTask.group.approvalRequested).to.equal(true); - expect(updatedTask.group.approvalRequestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type + expect(updatedTask.group.approval.requested).to.equal(true); + expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type }); it('errors when approval has already been requested', async () => { diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 0a4d161b9c..4e4ea6acf9 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -316,13 +316,13 @@ api.scoreTask = { if (!task) throw new NotFound(res.t('taskNotFound')); - if (task.group.requiresApproval && !task.group.approved) { - if (task.group.approvalRequested) { + if (task.group.approval.required && !task.group.approval.approved) { + if (task.group.approval.requested) { throw new NotAuthorized(res.t('taskRequiresApproval')); } - task.group.approvalRequested = true; - task.group.approvalRequestedDate = new Date(); + task.group.approval.requested = true; + task.group.approval.requestedDate = new Date(); let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields}); let groupLeader = await User.findById(group.leader); // Use this method so we can get access to notifications diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index 7aad25ca59..db522e86b1 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -221,9 +221,9 @@ api.approveTask = { if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); - task.group.approvedDate = new Date(); - task.group.approvingUser = user._id; - task.group.approved = true; + task.group.approval.dateApproved = new Date(); + task.group.approval.approvingUser = user._id; + task.group.approval.approved = true; assignedUser.addNotification('GROUP_TASK_APPROVAL', {message: res.t('yourTaskHasBeenApproved')}); @@ -264,8 +264,8 @@ api.getGroupApprovals = { let approvals = await Tasks.Task.find({ 'group.id': groupId, - 'group.approved': false, - 'group.approvalRequested': true, + 'group.approval.approved': false, + 'group.approval.requested': true, }, 'userId group').exec(); res.respond(200, approvals); diff --git a/website/server/libs/taskManager.js b/website/server/libs/taskManager.js index 877541ad72..0fbd9f2bfc 100644 --- a/website/server/libs/taskManager.js +++ b/website/server/libs/taskManager.js @@ -57,7 +57,7 @@ export async function createTasks (req, res, options = {}) { } else if (group) { newTask.group.id = group._id; if (taskData.requiresApproval) { - newTask.group.requiresApproval = true; + newTask.group.approval.required = true; } } else { newTask.userId = user._id; diff --git a/website/server/models/group.js b/website/server/models/group.js index 57b1492275..c3a52ba5d9 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -938,7 +938,7 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, user) { if (orderList.indexOf(matchingTask._id) === -1 && (matchingTask.type !== 'todo' || !matchingTask.completed)) orderList.push(matchingTask._id); } - matchingTask.group.requiresApproval = taskToSync.group.requiresApproval; + matchingTask.group.approval.required = taskToSync.group.approval.required; if (!matchingTask.notes) matchingTask.notes = taskToSync.notes; // don't override the notes, but provide it if not provided if (matchingTask.tags.indexOf(group._id) === -1) matchingTask.tags.push(group._id); // add tag if missing diff --git a/website/server/models/task.js b/website/server/models/task.js index 4ff7c6eded..8de85da31e 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -69,12 +69,14 @@ export let TaskSchema = new Schema({ broken: {type: String, enum: ['GROUP_DELETED', 'TASK_DELETED', 'UNSUBSCRIBED']}, assignedUsers: [{type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.']}], taskId: {type: String, ref: 'Task', validate: [validator.isUUID, 'Invalid uuid.']}, - requiresApproval: {type: Boolean, default: false}, - approved: {type: Boolean, default: false}, - approvedDate: {type: Date}, - approvingUser: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.']}, - approvalRequested: {type: Boolean, default: false}, - approvalRequestedDate: {type: Date}, + approval: { + required: {type: Boolean, default: false}, + approved: {type: Boolean, default: false}, + dateApproved: {type: Date}, + approvingUser: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.']}, + requested: {type: Boolean, default: false}, + requestedDate: {type: Date}, + }, }, reminders: [{ From 6801dae75d20c88c4f7cc00335985b2224106ec8 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sun, 30 Oct 2016 03:23:01 -0500 Subject: [PATCH 13/13] Fixed history test --- .../integration/dataexport/GET-export_history.csv.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/api/v3/integration/dataexport/GET-export_history.csv.test.js b/test/api/v3/integration/dataexport/GET-export_history.csv.test.js index df2b9bb680..fafc55d7cb 100644 --- a/test/api/v3/integration/dataexport/GET-export_history.csv.test.js +++ b/test/api/v3/integration/dataexport/GET-export_history.csv.test.js @@ -43,9 +43,9 @@ describe('GET /export/history.csv', () => { expect(splitRes[0]).to.equal('Task Name,Task ID,Task Type,Date,Value'); expect(splitRes[1]).to.equal(`habit 1,${tasks[0]._id},habit,${moment(tasks[0].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[0].history[0].value}`); expect(splitRes[2]).to.equal(`habit 1,${tasks[0]._id},habit,${moment(tasks[0].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[0].history[1].value}`); - expect(splitRes[3]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[0].value}`); - expect(splitRes[4]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[1].value}`); - expect(splitRes[5]).to.equal(`daily 1,${tasks[1]._id},daily,${moment(tasks[1].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[1].history[0].value}`); + expect(splitRes[3]).to.equal(`daily 1,${tasks[1]._id},daily,${moment(tasks[1].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[1].history[0].value}`); + expect(splitRes[4]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[0].value}`); + expect(splitRes[5]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[1].value}`); expect(splitRes[6]).to.equal(''); }); });