From 93befcebcc7508d7106996b6c11b117708dbd7fe Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Mon, 27 Feb 2017 14:56:34 -0700 Subject: [PATCH] Tasks score notes (#8507) * Added setting and modal for score notes * Added persistent score notes * Fixed linting issues and documented new field * Added max length to task score notes * Added check for score notes existence * Combined tasks perferences --- .../POST-tasks_id_score_direction.test.js | 24 +++++++++++++ test/common/ops/scoreTask.test.js | 9 +++++ .../client-old/js/controllers/tasksCtrl.js | 36 ++++++++++++++++--- .../client-old/js/services/taskServices.js | 5 ++- .../client-old/js/services/userServices.js | 5 +-- website/common/locales/en/tasks.json | 2 ++ website/common/script/ops/scoreTask.js | 8 +++-- website/server/controllers/api-v3/tasks.js | 7 ++++ website/server/models/user/schema.js | 1 + website/views/options/settings/settings.jade | 3 ++ website/views/shared/modals/index.jade | 1 + website/views/shared/modals/task-notes.jade | 10 ++++++ 12 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 website/views/shared/modals/task-notes.jade diff --git a/test/api/v3/integration/tasks/POST-tasks_id_score_direction.test.js b/test/api/v3/integration/tasks/POST-tasks_id_score_direction.test.js index fdabc86752..20b03b302c 100644 --- a/test/api/v3/integration/tasks/POST-tasks_id_score_direction.test.js +++ b/test/api/v3/integration/tasks/POST-tasks_id_score_direction.test.js @@ -315,6 +315,30 @@ describe('POST /tasks/:id/score/:direction', () => { expect(updatedUser.stats.gp).to.be.greaterThan(user.stats.gp); }); + + it('adds score notes to task', async () => { + let scoreNotesString = 'test-notes'; + + await user.post(`/tasks/${habit._id}/score/up`, { + scoreNotes: scoreNotesString, + }); + let updatedTask = await user.get(`/tasks/${habit._id}`); + + expect(updatedTask.history[0].scoreNotes).to.eql(scoreNotesString); + }); + + it('errors when score notes are too large', async () => { + let scoreNotesString = new Array(258).join('a'); + + await expect(user.post(`/tasks/${habit._id}/score/up`, { + scoreNotes: scoreNotesString, + })) + .to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('taskScoreNotesTooLong'), + }); + }); }); context('reward', () => { diff --git a/test/common/ops/scoreTask.test.js b/test/common/ops/scoreTask.test.js index e5466105bf..624a664a5b 100644 --- a/test/common/ops/scoreTask.test.js +++ b/test/common/ops/scoreTask.test.js @@ -212,6 +212,15 @@ describe('shared.ops.scoreTask', () => { expect(ref.afterUser.stats.gp).to.be.greaterThan(ref.beforeUser.stats.gp); }); + it('adds score notes', () => { + let scoreNotesString = 'scoreNotes'; + habit.scoreNotes = scoreNotesString; + options = { user: ref.afterUser, task: habit, direction: 'up', times: 5, cron: false }; + scoreTask(options); + + expect(habit.history[0].scoreNotes).to.eql(scoreNotesString); + }); + it('down', () => { scoreTask({user: ref.afterUser, task: habit, direction: 'down', times: 5, cron: false}, {}); diff --git a/website/client-old/js/controllers/tasksCtrl.js b/website/client-old/js/controllers/tasksCtrl.js index baa43d9605..2936a8d476 100644 --- a/website/client-old/js/controllers/tasksCtrl.js +++ b/website/client-old/js/controllers/tasksCtrl.js @@ -1,7 +1,7 @@ "use strict"; -habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','Notification', '$http', 'ApiUrl', '$timeout', 'Content', 'Shared', 'Guide', 'Tasks', 'Analytics', - function($scope, $rootScope, $location, User, Notification, $http, ApiUrl, $timeout, Content, Shared, Guide, Tasks, Analytics) { +habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','Notification', '$http', 'ApiUrl', '$timeout', 'Content', 'Shared', 'Guide', 'Tasks', 'Analytics', '$modal', + function($scope, $rootScope, $location, User, Notification, $http, ApiUrl, $timeout, Content, Shared, Guide, Tasks, Analytics, $modal) { $scope.obj = User.user; // used for task-lists $scope.user = User.user; @@ -11,7 +11,7 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N return Shared.count.remainingGearInSet(gear, 'armoire'); }; - $scope.score = function(task, direction) { + function scoreTask (task, direction) { switch (task.type) { case 'reward': playRewardSound(task); @@ -26,8 +26,36 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N if (direction === 'down') $rootScope.playSound('Minus_Habit'); else if (direction === 'up') $rootScope.playSound('Plus_Habit'); } - User.score({params:{task: task, direction:direction}}); + User.score({ + params: { + task: task, + direction:direction + }, + body: { + scoreNotes: task.scoreNotes, + }, + }); Analytics.updateUser(); + } + + $scope.score = function(task, direction) { + if (!User.user.preferences.tasks.confirmScoreNotes) return scoreTask(task, direction); + + $modal.open({ + templateUrl: 'modals/task-notes.html', + controller: function ($scope, task) { + $scope.task = task; + }, + resolve: { + task: function() { + return task; + } + } + }).result.then(function(result) { + task = result; + if (!task) return; + scoreTask(task, direction); + }); }; function addUserTasks(listDef, tasks) { diff --git a/website/client-old/js/services/taskServices.js b/website/client-old/js/services/taskServices.js index 66982ab74e..d1ef3cf7d8 100644 --- a/website/client-old/js/services/taskServices.js +++ b/website/client-old/js/services/taskServices.js @@ -144,10 +144,13 @@ angular.module('habitrpg') }); }; - function scoreTask (taskId, direction) { + function scoreTask (taskId, direction, body) { + if (!body) body = {}; + return $http({ method: 'POST', url: '/api/v3/tasks/' + taskId + '/score/' + direction, + data: body, }); }; diff --git a/website/client-old/js/services/userServices.js b/website/client-old/js/services/userServices.js index 807bfd0a8e..c8c351240c 100644 --- a/website/client-old/js/services/userServices.js +++ b/website/client-old/js/services/userServices.js @@ -205,7 +205,7 @@ angular.module('habitrpg') }, allocate: function (data) { - callOpsFunctionAndRequest('allocate', 'allocate', "POST",'', data); + callOpsFunctionAndRequest('allocate', 'allocate', "POST", '', data); }, allocateNow: function () { @@ -243,7 +243,8 @@ angular.module('habitrpg') return; } - Tasks.scoreTask(data.params.task._id, data.params.direction).then(function (res) { + Tasks.scoreTask(data.params.task._id, data.params.direction, data.body) + .then(function (res) { var tmp = res.data.data._tmp || {}; // used to notify drops, critical hits and other bonuses var crit = tmp.crit; var drop = tmp.drop; diff --git a/website/common/locales/en/tasks.json b/website/common/locales/en/tasks.json index 38beee1023..d76f1271fb 100644 --- a/website/common/locales/en/tasks.json +++ b/website/common/locales/en/tasks.json @@ -147,5 +147,7 @@ "taskApprovalHasBeenRequested": "Approval has been requested", "approvals": "Approvals", "approvalRequired": "Approval Required", + "confirmScoreNotes": "Confirm task scoring with notes", + "taskScoreNotesTooLong": "Task score notes must be less than 256 characters", "groupTasksByChallenge": "Group tasks by challenge title" } diff --git a/website/common/script/ops/scoreTask.js b/website/common/script/ops/scoreTask.js index 8e3a04958f..295d98d32d 100644 --- a/website/common/script/ops/scoreTask.js +++ b/website/common/script/ops/scoreTask.js @@ -210,11 +210,15 @@ module.exports = function scoreTask (options = {}, req = {}) { _gainMP(user, _.max([0.25, 0.0025 * user._statsComputed.maxMP]) * (direction === 'down' ? -1 : 1)); task.history = task.history || []; + // Add history entry, even more than 1 per day - task.history.push({ + let historyEntry = { date: Number(new Date()), value: task.value, - }); + }; + if (task.scoreNotes) historyEntry.scoreNotes = task.scoreNotes; + + task.history.push(historyEntry); _updateCounter(task, direction, times); } else if (task.type === 'daily') { diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 9761906f12..362f1eab38 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -23,6 +23,8 @@ import Bluebird from 'bluebird'; import _ from 'lodash'; import logger from '../../libs/logger'; +const MAX_SCORE_NOTES_LENGTH = 256; + /** * @apiDefine TaskNotFound * @apiError (404) {NotFound} TaskNotFound The specified task could not be found. @@ -482,6 +484,7 @@ api.updateTask = { * * @apiParam {String} taskId The task _id or alias * @apiParam {String="up","down"} direction The direction for scoring the task + * @apiParam {String} scoreNotes Notes explaining the scoring * * @apiExample {json} Example call: * curl -X "POST" https://habitica.com/api/v3/tasks/test-api-params/score/up @@ -509,11 +512,15 @@ api.scoreTask = { if (validationErrors) throw validationErrors; let user = res.locals.user; + let scoreNotes = req.body.scoreNotes; + if (scoreNotes && scoreNotes.length > MAX_SCORE_NOTES_LENGTH) throw new NotAuthorized(res.t('taskScoreNotesTooLong')); let {taskId} = req.params; let task = await Tasks.Task.findByIdOrAlias(taskId, user._id, {userId: user._id}); let direction = req.params.direction; + if (scoreNotes) task.scoreNotes = scoreNotes; + if (!task) throw new NotFound(res.t('taskNotFound')); if (task.group.approval.required && !task.group.approval.approved) { diff --git a/website/server/models/user/schema.js b/website/server/models/user/schema.js index 6e5721881d..3cb7c9ecd4 100644 --- a/website/server/models/user/schema.js +++ b/website/server/models/user/schema.js @@ -469,6 +469,7 @@ let schema = new Schema({ }, tasks: { groupByChallenge: {type: Boolean, default: false}, + confirmScoreNotes: {type: Boolean, default: false}, }, improvementCategories: { type: Array, diff --git a/website/views/options/settings/settings.jade b/website/views/options/settings/settings.jade index 6b32fcacd4..c3ce5cf61f 100644 --- a/website/views/options/settings/settings.jade +++ b/website/views/options/settings/settings.jade @@ -64,6 +64,9 @@ script(type='text/ng-template', id='partials/options.settings.settings.html') .checkbox label=env.t('suppressStreakModal') input(type='checkbox', ng-model='user.preferences.suppressModals.streak', ng-change='set({"preferences.suppressModals.streak": user.preferences.suppressModals.streak?true: false})') + .checkbox + label=env.t('confirmScoreNotes') + input(type='checkbox', ng-model='user.preferences.tasks.confirmScoreNotes', ng-change='set({"preferences.tasks.confirmScoreNotes": user.preferences.tasks.confirmScoreNotes ? true: false})') .checkbox label=env.t('groupTasksByChallenge') diff --git a/website/views/shared/modals/index.jade b/website/views/shared/modals/index.jade index f8a8330c45..83d434351e 100644 --- a/website/views/shared/modals/index.jade +++ b/website/views/shared/modals/index.jade @@ -25,6 +25,7 @@ include ./login-incentives.jade include ./login-incentives-reward-unlocked.jade include ./generic.jade include ./tasks-edit.jade +include ./task-notes.jade //- Settings script(type='text/ng-template', id='modals/change-day-start.html') diff --git a/website/views/shared/modals/task-notes.jade b/website/views/shared/modals/task-notes.jade new file mode 100644 index 0000000000..2914795287 --- /dev/null +++ b/website/views/shared/modals/task-notes.jade @@ -0,0 +1,10 @@ +script(id='modals/task-notes.html', type='text/ng-template') + .modal-header + h4 Task Notes + + .modal-body + textarea.form-control(ng-model="task.scoreNotes", row="10") + + .modal-footer + .btn.btn-default(ng-click='$close()')=env.t('cancel') + .btn.btn-primary(ng-click="$close(task)")=env.t('save')