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
This commit is contained in:
Keith Holliday 2017-02-27 14:56:34 -07:00 committed by GitHub
parent 68a042cdb9
commit 93befcebcc
12 changed files with 102 additions and 9 deletions

View file

@ -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', () => {

View file

@ -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}, {});

View file

@ -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) {

View file

@ -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,
});
};

View file

@ -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;

View file

@ -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"
}

View file

@ -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') {

View file

@ -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) {

View file

@ -469,6 +469,7 @@ let schema = new Schema({
},
tasks: {
groupByChallenge: {type: Boolean, default: false},
confirmScoreNotes: {type: Boolean, default: false},
},
improvementCategories: {
type: Array,

View file

@ -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')

View file

@ -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')

View file

@ -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')