Groups assign tasks (#7887)

* Added initial code for creating and reading group tasks

* Separated group task routes. Separated shared task functions

* Added taskOrder to group

* Minor style fixes

* Fixed lint issues

* Added unit tests for task manager

* Updated task helper functions

* Fixed history test

* Fixed group task query

* Removed extra var

* Updated with new file structure

* Updated noset values

* Removed unecessary undefineds, fixed comments, Added apiignore

* Separated group task routes. Separated shared task functions

* Added unit tests for task manager

* Added initial groups assign route and tests

* Added sync assigned task to user

* Added unassign route and unlink method

* Added remove and unlink group task

* Updated linking and unlinking. Add test for updating task info

* Added delete group task and tests

* Added sync on task update and tests

* Added multiple users assignment

* Updated unassign for multiple users

* Added test for delete task with multiple assigend users

* Added update task for multiple assigned users

* Fixed issue with get tasks

* Abstracted syncable attributes and add tests

* Fixed merge conflicts

* Fixed style issues, limited group query fields, and added await

* Fixed group fields needed. Removed api v2 code

* Fixed style issues

* Moved group field under group sub document. Updated tests. Fixed other broken tests

* Renamed linkedTaskId and fixed broken alias tests

* Added debug middleware to new routes

* Fixed debug middleware import

* Added additional user id check for original group tasks

* Updated challenge task check to look for challenge id

* Added checklist sync fix
This commit is contained in:
Keith Holliday
2016-09-03 03:54:55 -05:00
committed by Matteo Pagliazzi
parent 173b3f3f84
commit 836cee2531
17 changed files with 1488 additions and 162 deletions

View File

@@ -9,70 +9,17 @@ import {
NotAuthorized,
BadRequest,
} from '../../libs/errors';
import {
createTasks,
getTasks,
} from '../../libs/taskManager';
import common from '../../../../common';
import Bluebird from 'bluebird';
import _ from 'lodash';
import logger from '../../libs/logger';
let api = {};
async function _validateTaskAlias (tasks, res) {
let tasksWithAliases = tasks.filter(task => task.alias);
let aliases = tasksWithAliases.map(task => task.alias);
// Compares the short names in tasks against
// a Set, where values cannot repeat. If the
// lengths are different, some name was duplicated
if (aliases.length !== [...new Set(aliases)].length) {
throw new BadRequest(res.t('taskAliasAlreadyUsed'));
}
await Bluebird.map(tasksWithAliases, (task) => {
return task.validate();
});
}
// challenge must be passed only when a challenge task is being created
async function _createTasks (req, res, user, challenge) {
let toSave = Array.isArray(req.body) ? req.body : [req.body];
toSave = toSave.map(taskData => {
// Validate that task.type is valid
if (!taskData || Tasks.tasksTypes.indexOf(taskData.type) === -1) throw new BadRequest(res.t('invalidTaskType'));
let taskType = taskData.type;
let newTask = new Tasks[taskType](Tasks.Task.sanitize(taskData));
if (challenge) {
newTask.challenge.id = challenge.id;
} else {
newTask.userId = user._id;
}
// Validate that the task is valid and throw if it isn't
// otherwise since we're saving user/challenge and task in parallel it could save the user/challenge with a tasksOrder that doens't match reality
let validationErrors = newTask.validateSync();
if (validationErrors) throw validationErrors;
// Otherwise update the user/challenge
(challenge || user).tasksOrder[`${taskType}s`].unshift(newTask._id);
return newTask;
});
// tasks with aliases need to be validated asyncronously
await _validateTaskAlias(toSave, res);
toSave = toSave.map(task => task.save({ // If all tasks are valid (this is why it's not in the previous .map()), save everything, withough running validation again
validateBeforeSave: false,
}));
toSave.unshift((challenge || user).save());
let tasks = await Bluebird.all(toSave);
tasks.splice(0, 1); // Remove user or challenge
return tasks;
}
let requiredGroupFields = '_id leader tasksOrder name';
/**
* @api {post} /api/v3/tasks/user Create a new task belonging to the user
@@ -88,7 +35,8 @@ api.createUserTasks = {
url: '/tasks/user',
middlewares: [authWithHeaders()],
async handler (req, res) {
let tasks = await _createTasks(req, res, res.locals.user);
let user = res.locals.user;
let tasks = await createTasks(req, res, {user});
res.respond(201, tasks.length === 1 ? tasks[0] : tasks);
},
};
@@ -123,75 +71,15 @@ api.createChallengeTasks = {
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
let tasks = await _createTasks(req, res, user, challenge);
let tasks = await createTasks(req, res, {user, challenge});
res.respond(201, tasks.length === 1 ? tasks[0] : tasks);
// If adding tasks to a challenge -> sync users
if (challenge) challenge.addTasks(tasks);
return null;
},
};
// challenge must be passed only when a challenge task is being created
async function _getTasks (req, res, user, challenge) {
let query = challenge ? {'challenge.id': challenge.id, userId: {$exists: false}} : {userId: user._id};
let type = req.query.type;
if (type) {
if (type === 'todos') {
query.completed = false; // Exclude completed todos
query.type = 'todo';
} else if (type === 'completedTodos' || type === '_allCompletedTodos') { // _allCompletedTodos is currently in BETA and is likely to be removed in future
let limit = 30;
if (type === '_allCompletedTodos') {
limit = 0; // no limit
}
query = Tasks.Task.find({
userId: user._id,
type: 'todo',
completed: true,
}).limit(limit).sort({
dateCompleted: -1,
});
} else {
query.type = type.slice(0, -1); // removing the final "s"
}
} else {
query.$or = [ // Exclude completed todos
{type: 'todo', completed: false},
{type: {$in: ['habit', 'daily', 'reward']}},
];
}
let tasks = await Tasks.Task.find(query).exec();
// Order tasks based on tasksOrder
if (type && type !== 'completedTodos' && type !== '_allCompletedTodos') {
let order = (challenge || user).tasksOrder[type];
let orderedTasks = new Array(tasks.length);
let unorderedTasks = []; // what we want to add later
tasks.forEach((task, index) => {
let taskId = task._id;
let i = order[index] === taskId ? index : order.indexOf(taskId);
if (i === -1) {
unorderedTasks.push(task);
} else {
orderedTasks[i] = task;
}
});
// Remove empty values from the array and add any unordered task
orderedTasks = _.compact(orderedTasks).concat(unorderedTasks);
res.respond(200, orderedTasks);
} else {
res.respond(200, tasks);
}
}
/**
* @api {get} /api/v3/tasks/user Get a user's tasks
* @apiVersion 3.0.0
@@ -214,7 +102,10 @@ api.getUserTasks = {
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
return await _getTasks(req, res, res.locals.user);
let user = res.locals.user;
let tasks = await getTasks(req, res, {user});
return res.respond(200, tasks);
},
};
@@ -249,7 +140,8 @@ api.getChallengeTasks = {
let group = await Group.getGroup({user, groupId: challenge.group, fields: '_id type privacy', optionalMembership: true});
if (!group || !challenge.canView(user, group)) throw new NotFound(res.t('challengeNotFound'));
return await _getTasks(req, res, res.locals.user, challenge);
let tasks = await getTasks(req, res, {user, challenge});
return res.respond(200, tasks);
},
};
@@ -274,7 +166,7 @@ api.getTask = {
if (!task) {
throw new NotFound(res.t('taskNotFound'));
} else if (!task.userId) { // If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
let challenge = await Challenge.find({_id: task.challenge.id}).select('leader').exec();
if (!challenge || (user.challenges.indexOf(task.challenge.id) === -1 && challenge.leader !== user._id && !user.contributor.admin)) { // eslint-disable-line no-extra-parens
throw new NotFound(res.t('taskNotFound'));
@@ -312,10 +204,16 @@ api.updateTask = {
let taskId = req.params.taskId;
let task = await Tasks.Task.findByIdOrAlias(taskId, user._id);
let group;
if (!task) {
throw new NotFound(res.t('taskNotFound'));
} else if (!task.userId) { // If the task belongs to a challenge make sure the user has rights
} else if (task.group.id && !task.userId) {
// @TODO: Abstract this access snippet
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'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
@@ -341,10 +239,13 @@ api.updateTask = {
// see https://github.com/Automattic/mongoose/issues/2749
let savedTask = await task.save();
if (group && task.group.id && task.group.assignedUsers.length > 0) {
await group.updateTask(savedTask);
}
res.respond(200, savedTask);
if (challenge) challenge.updateTask(savedTask);
return null;
},
};
@@ -461,8 +362,6 @@ api.scoreTask = {
direction
});
*/
return null;
},
};
@@ -548,7 +447,7 @@ api.addChecklistItem = {
if (!task) {
throw new NotFound(res.t('taskNotFound'));
} else if (!task.userId) { // If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
@@ -563,8 +462,6 @@ api.addChecklistItem = {
res.respond(200, savedTask);
if (challenge) challenge.updateTask(savedTask);
return null;
},
};
@@ -638,7 +535,7 @@ api.updateChecklistItem = {
if (!task) {
throw new NotFound(res.t('taskNotFound'));
} else if (!task.userId) { // If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
@@ -655,8 +552,6 @@ api.updateChecklistItem = {
res.respond(200, savedTask);
if (challenge) challenge.updateTask(savedTask);
return null;
},
};
@@ -690,7 +585,7 @@ api.removeChecklistItem = {
if (!task) {
throw new NotFound(res.t('taskNotFound'));
} else if (!task.userId) { // If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
@@ -705,8 +600,6 @@ api.removeChecklistItem = {
let savedTask = await task.save();
res.respond(200, savedTask);
if (challenge) challenge.updateTask(savedTask);
return null;
},
};
@@ -952,7 +845,13 @@ api.deleteTask = {
if (!task) {
throw new NotFound(res.t('taskNotFound'));
} else if (!task.userId) { // If the task belongs to a challenge make sure the user has rights
} else if (task.group.id && !task.userId) {
// @TODO: Abstract this access snippet
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.removeTask(task);
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
@@ -971,8 +870,6 @@ api.deleteTask = {
res.respond(200, {});
if (challenge) challenge.removeTask(task);
return null;
},
};