diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index b01d0eed99..0b810c76d1 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -20,9 +20,12 @@ "invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\".", "cantDeleteChallengeTasks": "A task belonging to a challenge can't be deleted.", "checklistOnlyDailyTodo": "Checklists are supported only on dailies and todos", - "checklistItemNotFound": "No checklist item was wound with given id.", - "itemIdRequired": "\"itemId\" must be a valid UUID", + "checklistItemNotFound": "No checklist item was found with given id.", + "itemIdRequired": "\"itemId\" must be a valid UUID.", + "tagNotFound": "No tag item was found with given id.", + "tagIdRequired": "\"tagId\" must be a valid UUID corresponding to a tag belonging to the user.", "positionRequired": "\"position\" is required and must be a number.", "cantMoveCompletedTodo": "Can't move a completed todo.", - "directionUpDown": "\"direction\" is required and must be 'up' or 'down'" + "directionUpDown": "\"direction\" is required and must be 'up' or 'down'", + "alreadyTagged": "The task is already tagged with give tag." } diff --git a/test/api/v3/integration/tasks/POST-create_task.test.js b/test/api/v3/integration/tasks/POST-create_task.test.js index 90234d06a3..b76d6ceb4e 100644 --- a/test/api/v3/integration/tasks/POST-create_task.test.js +++ b/test/api/v3/integration/tasks/POST-create_task.test.js @@ -49,4 +49,19 @@ describe('POST /tasks', () => { }); }); }); + + context('correctly creates new tasks', () => { + it('habit', () => { + return api.post('/tasks', { + text: 'test habit', + type: 'habit', + up: false, + down: true, + history: 'i cannot be set', + notes: 1976, + }).then((task) => { + expect(task.userId).to.equal(user._id); + }); + }); + }); }); diff --git a/website/src/controllers/api-v3/tasks.js b/website/src/controllers/api-v3/tasks.js index 1431e3c0a2..065e8a9f7b 100644 --- a/website/src/controllers/api-v3/tasks.js +++ b/website/src/controllers/api-v3/tasks.js @@ -34,7 +34,7 @@ api.createTask = { let newTask = new Tasks[taskType](Tasks.Task.sanitizeCreate(req.body)); newTask.userId = user._id; - user.tasksOrder[taskType + 's'].unshift(newTask._id); + user.tasksOrder[`${taskType}s`].unshift(newTask._id); Q.all([ newTask.save(), @@ -155,6 +155,7 @@ api.updateTask = { req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID(); // TODO check that req.body isn't empty + // TODO make sure tags are updated correctly (they aren't set as modified!) maybe use specific routes let validationErrors = req.validationErrors(); if (validationErrors) return next(validationErrors); @@ -172,7 +173,7 @@ api.updateTask = { task.checklist = req.body.checklist; } // TODO merge goes deep into objects, it's ok? - // TODO also check that array fields are updated correctly without marking modified + // TODO also check that array and mixed fields are updated correctly without marking modified _.merge(task, Tasks.Task.sanitizeUpdate(req.body)); return task.save(); }) @@ -273,7 +274,7 @@ api.moveTask = { }; /** - * @api {post} /tasks/:taskId/checklist/addItem Add an item to a checklist, creating the checklist if it doesn't exist + * @api {post} /tasks/:taskId/checklist Add an item to a checklist, creating the checklist if it doesn't exist * @apiVersion 3.0.0 * @apiName AddChecklistItem * @apiGroup Task @@ -284,7 +285,7 @@ api.moveTask = { */ api.addChecklistItem = { method: 'POST', - url: '/tasks/:taskId/checklist/addItem', + url: '/tasks/:taskId/checklist', middlewares: [authWithHeaders()], handler (req, res, next) { let user = res.locals.user; @@ -441,6 +442,92 @@ api.removeChecklistItem = { }, }; +/** + * @api {post} /tasks/:taskId/tags/:tagId Add a tag to a task + * @apiVersion 3.0.0 + * @apiName AddTagToTask + * @apiGroup Task + * + * @apiParam {UUID} taskId The task _id + * @apiParam {UUID} tagId The tag id + * + * @apiSuccess {object} task The updated task + */ +api.addTagToTask = { + method: 'POST', + url: '/tasks/:taskId/tags', + middlewares: [authWithHeaders()], + handler (req, res, next) { + let user = res.locals.user; + + req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID(); + let userTags = user.tags.map(tag => tag._id); + req.checkParams('tagId', res.t('tagIdRequired')).notEmpty().isUUID().isIn(userTags); + + let validationErrors = req.validationErrors(); + if (validationErrors) return next(validationErrors); + + Tasks.Task.findOne({ + _id: req.params.taskId, + userId: user._id, + }).exec() + .then((task) => { + if (!task) throw new NotFound(res.t('taskNotFound')); + let tagId = req.params.tagId; + + let alreadyTagged = task.tags.indexOf(tagId) === -1; + if (alreadyTagged) throw new BadRequest(res.t('alreadyTagged')); + + task.tags.push(tagId); + return task.save(); + }) + .then((savedTask) => res.respond(200, savedTask)) // TODO what to return + .catch(next); + }, +}; + +/** + * @api {delete} /tasks/:taskId/tags/:tagId Remove a tag + * @apiVersion 3.0.0 + * @apiName RemoveTagFromTask + * @apiGroup Task + * + * @apiParam {UUID} taskId The task _id + * @apiParam {UUID} tagId The tag id + * + * @apiSuccess {object} empty An empty object + */ +api.removeTagFromTask = { + method: 'DELETE', + url: '/tasks/:taskId/tags/:tagId', + middlewares: [authWithHeaders()], + handler (req, res, next) { + let user = res.locals.user; + + req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID(); + req.checkParams('tagId', res.t('tagIdRequired')).notEmpty().isUUID(); + + let validationErrors = req.validationErrors(); + if (validationErrors) return next(validationErrors); + + Tasks.Task.findOne({ + _id: req.params.taskId, + userId: user._id, + }).exec() + .then((task) => { + if (!task) throw new NotFound(res.t('taskNotFound')); + + let tagI = _.findIndex(task.tags, {_id: req.params.tagId}); + if (tagI === -1) throw new NotFound(res.t('tagNotFound')); + + task.tags.splice(tagI, 1); + return task.save(); + }) + .then(() => res.respond(200, {})) // TODO what to return + .catch(next); + }, +}; + // Remove a task from user.tasksOrder function _removeTaskTasksOrder (user, taskId) { // Loop through all lists and when the task is found, remove it and return diff --git a/website/src/models/task.js b/website/src/models/task.js index c7c368c059..ad9411282f 100644 --- a/website/src/models/task.js +++ b/website/src/models/task.js @@ -17,9 +17,12 @@ export let TaskSchema = new Schema({ type: {type: String, enum: tasksTypes, required: true, default: tasksTypes[0]}, text: {type: String, required: true}, notes: {type: String, default: ''}, - tags: {type: Schema.Types.Mixed, default: {}}, // TODO dictionary? { "4ddf03d9-54bd-41a3-b011-ca1f1d2e9371" : true }, validate + tags: [{ + type: String, + validate: [validator.isUUID, 'Invalid uuid.'], + }], value: {type: Number, default: 0}, // redness or cost for rewards - priority: {type: Number, default: 1, required: true}, + priority: {type: Number, default: 1, required: true}, // TODO enum? attribute: {type: String, default: 'str', enum: ['str', 'con', 'int', 'per']}, userId: {type: String, ref: 'User'}, // When null it belongs to a challenge