diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index 9f4346c414..b01d0eed99 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -23,5 +23,6 @@ "checklistItemNotFound": "No checklist item was wound with given id.", "itemIdRequired": "\"itemId\" must be a valid UUID", "positionRequired": "\"position\" is required and must be a number.", - "cantMoveCompletedTodo": "Can't move a completed todo." + "cantMoveCompletedTodo": "Can't move a completed todo.", + "directionUpDown": "\"direction\" is required and must be 'up' or 'down'" } diff --git a/test/api/v3/integration/user/auth/POST-register_local.test.js b/test/api/v3/integration/user/auth/POST-register_local.test.js index 4ad2127cd4..0e11e7b924 100644 --- a/test/api/v3/integration/user/auth/POST-register_local.test.js +++ b/test/api/v3/integration/user/auth/POST-register_local.test.js @@ -194,10 +194,10 @@ describe('POST /user/auth/local/register', () => { password, confirmPassword: password, }).then((user) => { - expect(user.todos).to.not.be.empty; - expect(user.dailys).to.be.empty; - expect(user.habits).to.be.empty; - expect(user.rewards).to.be.empty; + expect(user.tasksOrder.todos).to.not.be.empty; + expect(user.tasksOrder.dailys).to.be.empty; + expect(user.tasksOrder.habits).to.be.empty; + expect(user.tasksOrder.rewards).to.be.empty; }); }); @@ -245,10 +245,10 @@ describe('POST /user/auth/local/register', () => { password, confirmPassword: password, }).then((user) => { - expect(user.todos).to.not.be.empty; - expect(user.dailys).to.be.empty; - expect(user.habits).to.not.be.empty; - expect(user.rewards).to.not.be.empty; + expect(user.tasksOrder.todos).to.not.be.empty; + expect(user.tasksOrder.dailys).to.be.empty; + expect(user.tasksOrder.habits).to.not.be.empty; + expect(user.tasksOrder.rewards).to.not.be.empty; }); }); diff --git a/website/src/controllers/api-v3/tasks.js b/website/src/controllers/api-v3/tasks.js index f1aeb639e2..8ba7fc21a0 100644 --- a/website/src/controllers/api-v3/tasks.js +++ b/website/src/controllers/api-v3/tasks.js @@ -169,9 +169,45 @@ api.updateTask = { }, }; -// completed todos cannot be moved, they'll be returned ordered by date of completion /** - * @api {put} /tasks/move/:taskId/to/:position Move a task to a new position + * @api {put} /tasks/score/:taskId/:direction Score a task + * @apiVersion 3.0.0 + * @apiName ScoreTask + * @apiGroup Task + * + * @apiParam {UUID} taskId The task _id + * @apiParam {string="up","down"} direction The direction for scoring the task + * + * @apiSuccess {object} empty An empty object + */ +api.scoreTask = { + method: 'POST', + url: 'tasks/score/:taskId/:direction', + middlewares: [authWithHeaders()], + handler (req, res, next) { + req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID(); + req.checkParams('direction', res.t('directionUpDown')).notEmpty().isIn(['up', 'down']); + + let user = res.locals.user; + + Tasks.Task.findOne({ + _id: req.params.taskId, + userId: user._id, + }).exec() + .then((task) => { + if (!task) throw new NotFound(res.t('taskNotFound')); + + let delta = user.ops.score({params:{id:task.id, direction:direction}, language: req.language}); + }) + .then(() => res.respond(200, {})) // TODO what to return + .catch(next); + }, +}; + +// completed todos cannot be moved, they'll be returned ordered by date of completion +// TODO check that it works when a tag is selected or todos are split between dated and due +/** + * @api {post} /tasks/move/:taskId/to/:position Move a task to a new position * @apiVersion 3.0.0 * @apiName MoveTask * @apiGroup Task @@ -179,7 +215,7 @@ api.updateTask = { * @apiParam {UUID} taskId The task _id * @apiParam {Number} position Where to move the task (-1 means push to bottom) * - * @apiSuccess {object} emoty An empty object + * @apiSuccess {object} empty An empty object */ api.moveTask = { method: 'POST', diff --git a/website/src/models/task.js b/website/src/models/task.js index 9228ad4788..d697bac622 100644 --- a/website/src/models/task.js +++ b/website/src/models/task.js @@ -19,7 +19,7 @@ export let TaskSchema = new Schema({ notes: {type: String, default: ''}, tags: {type: Schema.Types.Mixed, default: {}}, // TODO dictionary? { "4ddf03d9-54bd-41a3-b011-ca1f1d2e9371" : true }, validate value: {type: Number, default: 0}, // redness - priority: {type: Number, default: 1}, + priority: {type: Number, default: 1, required: true}, attribute: {type: String, default: 'str', enum: ['str', 'con', 'int', 'per']}, userId: {type: String, ref: 'User'}, // When null it belongs to a challenge @@ -71,7 +71,7 @@ export let HabitSchema = new Schema(_.defaults({ up: {type: Boolean, default: true}, down: {type: Boolean, default: true}, }, habitDailySchema()), subDiscriminatorOptions); -export let Habit = Task.discriminator('habit', HabitSchema); +export let habit = Task.discriminator('habit', HabitSchema); export let DailySchema = new Schema(_.defaults({ frequency: {type: String, default: 'weekly', enum: ['daily', 'weekly']}, @@ -93,7 +93,7 @@ export let DailySchema = new Schema(_.defaults({ }, streak: {type: Number, default: 0}, }, habitDailySchema(), dailyTodoSchema()), subDiscriminatorOptions); -export let Daily = Task.discriminator('daily', DailySchema); +export let daily = Task.discriminator('daily', DailySchema); export let TodoSchema = new Schema(_.defaults({ dateCompleted: Date, @@ -101,7 +101,7 @@ export let TodoSchema = new Schema(_.defaults({ // TODO change field name date: String, // due date for todos }, dailyTodoSchema()), subDiscriminatorOptions); -export let Todo = Task.discriminator('todo', TodoSchema); +export let todo = Task.discriminator('todo', TodoSchema); export let RewardSchema = new Schema({}, subDiscriminatorOptions); -export let Reward = Task.discriminator('reward', RewardSchema); +export let reward = Task.discriminator('reward', RewardSchema); diff --git a/website/src/models/user.js b/website/src/models/user.js index 7e14b29c86..bdcc479ea9 100644 --- a/website/src/models/user.js +++ b/website/src/models/user.js @@ -477,8 +477,6 @@ schema.plugin(baseModel, { noSet: ['_id', 'apikey', 'auth.blocked', 'auth.timestamps', 'lastCron', 'auth.local.hashed_password', 'auth.local.salt', 'tasksOrder'], private: ['auth.local.hashed_password', 'auth.local.salt'], toJSONTransform: function toJSON (doc) { - doc.id = doc._id; // TODO remove? - // FIXME? Is this a reference to `doc.filters` or just disabled code? Remove? doc.filters = {}; doc._tmp = this._tmp; // be sure to send down drop notifs @@ -492,7 +490,7 @@ schema.post('init', function postInitUser (doc) { }); function _populateDefaultTasks (user, taskTypes) { - let tagsI = taskTypes.indexOf('tags'); + let tagsI = taskTypes.indexOf('tag'); if (tagsI !== -1) { user.tags = _.map(shared.content.userDefaults.tags, (tag) => { @@ -508,16 +506,20 @@ function _populateDefaultTasks (user, taskTypes) { let tasksToCreate = []; - taskTypes = tagsI !== -1 ? _.clone(taskTypes).slice(tagsI, 1) : taskTypes; + if (tagsI !== -1) { + taskTypes = _.clone(taskTypes); + taskTypes.splice(tagsI, 1); + }; + _.each(taskTypes, (taskType) => { - let tasksOfType = _.map(shared.content.userDefaults[taskType], (taskDefaults) => { - let newTask = new Tasks[taskType](taskDefaults); + let tasksOfType = _.map(shared.content.userDefaults[`${taskType}s`], (taskDefaults) => { + let newTask = new (Tasks[taskType])(taskDefaults); newTask.userId = user._id; - newTask.text = newTask.text(user.preferences.language); - if (newTask.notes) newTask.notes = newTask.notes(user.preferences.language); - if (newTask.checklist) { - newTask.checklist = _.map(newTask.checklist, (checklistItem) => { + newTask.text = taskDefaults.text(user.preferences.language); + if (newTask.notes) newTask.notes = taskDefaults.notes(user.preferences.language); + if (taskDefaults.checklist) { + newTask.checklist = _.map(taskDefaults.checklist, (checklistItem) => { checklistItem.text = checklistItem.text(user.preferences.language); return checklistItem; }); @@ -542,13 +544,13 @@ function _populateDefaultsForNewUser (user) { let iterableFlags = user.flags.toObject(); if (user.registeredThrough === 'habitica-web') { - taskTypes = ['habits', 'dailys', 'todos', 'rewards', 'tags']; + taskTypes = ['habit', 'daily', 'todo', 'reward', 'tag']; _.each(iterableFlags.tutorial.common, (val, section) => { user.flags.tutorial.common[section] = true; }); } else { - taskTypes = ['todos', 'tags']; + taskTypes = ['todo', 'tag']; user.flags.showTour = false; _.each(iterableFlags.tour, (val, section) => {