From 137636cb4096ab9c983666448a4646dbb034a942 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Thu, 8 Jun 2017 14:03:30 -0700 Subject: [PATCH 01/15] Removed cron from every route --- website/server/controllers/api-v3/groups.js | 1 - website/server/libs/routes.js | 4 ---- 2 files changed, 5 deletions(-) diff --git a/website/server/controllers/api-v3/groups.js b/website/server/controllers/api-v3/groups.js index 35858e2f73..e06f44013d 100644 --- a/website/server/controllers/api-v3/groups.js +++ b/website/server/controllers/api-v3/groups.js @@ -343,7 +343,6 @@ api.getGroups = { api.getGroup = { method: 'GET', url: '/groups/:groupId', - runCron: false, // Do not run cron to avoid double cronning because it's called in parallel to GET /user when the site loads middlewares: [authWithHeaders()], async handler (req, res) { let user = res.locals.user; diff --git a/website/server/libs/routes.js b/website/server/libs/routes.js index 46b6478823..e341b9bca4 100644 --- a/website/server/libs/routes.js +++ b/website/server/libs/routes.js @@ -27,10 +27,6 @@ module.exports.readController = function readController (router, controller) { let middlewaresToAdd = [getUserLanguage]; if (authMiddlewareIndex !== -1) { // the user will be authenticated, getUserLanguage and cron after authentication - if (!(runCron === false)) { // eslint-disable-line no-extra-parens - middlewaresToAdd.push(cron); - } - if (authMiddlewareIndex === middlewares.length - 1) { middlewares.push(...middlewaresToAdd); } else { From 60de7c8f21c5db5fb072e54b4cdf028551de2b0b Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Thu, 8 Jun 2017 14:07:33 -0700 Subject: [PATCH 02/15] Added cron route --- website/server/controllers/api-v3/cron.js | 20 ++++++++++++++++++++ website/server/libs/routes.js | 3 +-- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 website/server/controllers/api-v3/cron.js diff --git a/website/server/controllers/api-v3/cron.js b/website/server/controllers/api-v3/cron.js new file mode 100644 index 0000000000..cf5e3b2e2d --- /dev/null +++ b/website/server/controllers/api-v3/cron.js @@ -0,0 +1,20 @@ +import { authWithHeaders } from '../../middlewares/auth'; +import cron from '../middlewares/cron'; + +let api = {}; + +/** + * @api {post} /api/v3/cron Runs cron + * @apiName Cron + * @apiGroup Cron + * + * @apiSuccess {Object} data An empty Object + */ +api.cron = { + method: 'POST', + url: '/debug/cron', + middlewares: [authWithHeaders(), cron], + async handler (req, res) { + res.respond(200, {}); + }, +}; diff --git a/website/server/libs/routes.js b/website/server/libs/routes.js index e341b9bca4..9e76af74c1 100644 --- a/website/server/libs/routes.js +++ b/website/server/libs/routes.js @@ -3,7 +3,6 @@ import _ from 'lodash'; import { getUserLanguage, } from '../middlewares/language'; -import cron from '../middlewares/cron'; // Wrapper function to handler `async` route handlers that return promises // It takes the async function, execute it and pass any error to next (args[2]) @@ -12,7 +11,7 @@ let noop = (req, res, next) => next(); module.exports.readController = function readController (router, controller) { _.each(controller, (action) => { - let {method, url, middlewares = [], handler, runCron} = action; + let {method, url, middlewares = [], handler} = action; // If an authentication middleware is used run getUserLanguage after it, otherwise before // for cron instead use it only if an authentication middleware is present From 5d0fe0aac351a4234ba544b73457dd831258042b Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Thu, 8 Jun 2017 15:37:36 -0700 Subject: [PATCH 03/15] Added yesterdailiy to model --- test/api/v3/integration/tasks/POST-tasks_user.test.js | 1 + test/api/v3/integration/tasks/PUT-tasks_id.test.js | 2 ++ website/common/script/libs/taskDefaults.js | 1 + website/server/controllers/api-v3/cron.js | 2 +- website/server/models/task.js | 3 ++- 5 files changed, 7 insertions(+), 2 deletions(-) diff --git a/test/api/v3/integration/tasks/POST-tasks_user.test.js b/test/api/v3/integration/tasks/POST-tasks_user.test.js index 4f90dedd49..1210d56323 100644 --- a/test/api/v3/integration/tasks/POST-tasks_user.test.js +++ b/test/api/v3/integration/tasks/POST-tasks_user.test.js @@ -133,6 +133,7 @@ describe('POST /tasks/user', () => { expect(task.completed).to.equal(false); expect(task.streak).not.to.equal('never'); expect(task.value).not.to.equal(324); + expect(task.yesterDaily).to.equal(true); }); it('ignores invalid fields', async () => { diff --git a/test/api/v3/integration/tasks/PUT-tasks_id.test.js b/test/api/v3/integration/tasks/PUT-tasks_id.test.js index 433b263179..b2b802f837 100644 --- a/test/api/v3/integration/tasks/PUT-tasks_id.test.js +++ b/test/api/v3/integration/tasks/PUT-tasks_id.test.js @@ -396,6 +396,7 @@ describe('PUT /tasks/:id', () => { notes: 'some new notes', frequency: 'daily', everyX: 5, + yesterDaily: false, startDate: moment().add(1, 'days').toDate(), }); @@ -405,6 +406,7 @@ describe('PUT /tasks/:id', () => { expect(savedDaily.everyX).to.eql(5); expect(savedDaily.isDue).to.be.false; expect(savedDaily.nextDue.length).to.eql(6); + expect(savedDaily.yesterDaily).to.be.false; }); it('can update checklists (replace it)', async () => { diff --git a/website/common/script/libs/taskDefaults.js b/website/common/script/libs/taskDefaults.js index 26c81e02ea..a1ce59bf38 100644 --- a/website/common/script/libs/taskDefaults.js +++ b/website/common/script/libs/taskDefaults.js @@ -25,6 +25,7 @@ module.exports = function taskDefaults (task = {}) { challenge: { shortName: 'None', }, + yesterDaily: true, reminders: [], attribute: 'str', createdAt: new Date(), // TODO these are going to be overwritten by the server... diff --git a/website/server/controllers/api-v3/cron.js b/website/server/controllers/api-v3/cron.js index cf5e3b2e2d..6d69ab3eb6 100644 --- a/website/server/controllers/api-v3/cron.js +++ b/website/server/controllers/api-v3/cron.js @@ -1,5 +1,5 @@ import { authWithHeaders } from '../../middlewares/auth'; -import cron from '../middlewares/cron'; +import cron from '../../middlewares/cron'; let api = {}; diff --git a/website/server/models/task.js b/website/server/models/task.js index ee09612d46..ac1d5aa18d 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -128,7 +128,7 @@ TaskSchema.statics.findByIdOrAlias = async function findByIdOrAlias (identifier, TaskSchema.statics.sanitizeUserChallengeTask = function sanitizeUserChallengeTask (taskObj) { let initialSanitization = this.sanitize(taskObj); - return _.pick(initialSanitization, ['streak', 'checklist', 'attribute', 'reminders', 'tags', 'notes', 'collapseChecklist', 'alias']); + return _.pick(initialSanitization, ['streak', 'checklist', 'attribute', 'reminders', 'tags', 'notes', 'collapseChecklist', 'alias', 'yesterDaily']); }; // Sanitize checklist objects (disallowing id) @@ -244,6 +244,7 @@ export let DailySchema = new Schema(_.defaults({ weeksOfMonth: {type: [Number], default: []}, // Weeks of the month that the daily should repeat on isDue: {type: Boolean}, nextDue: [{type: String}], + yesterDaily: {type: Boolean, default: true}, }, habitDailySchema(), dailyTodoSchema()), subDiscriminatorOptions); export let daily = Task.discriminator('daily', DailySchema); From 7422d020b16e7894453b34bbc6711e03aeee8047 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Thu, 8 Jun 2017 16:03:20 -0700 Subject: [PATCH 04/15] Ported over UI code --- .../js/controllers/notificationCtrl.js | 52 +++++++++++++++++++ .../client-old/js/controllers/tasksCtrl.js | 3 ++ .../js/directives/task-list.directive.js | 10 +++- website/common/locales/en/tasks.json | 7 ++- website/views/shared/modals/index.jade | 1 + .../views/shared/modals/yester-dailies.jade | 12 +++++ .../shared/tasks/edit/advanced_options.jade | 6 +++ website/views/shared/tasks/task.jade | 8 +-- 8 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 website/views/shared/modals/yester-dailies.jade diff --git a/website/client-old/js/controllers/notificationCtrl.js b/website/client-old/js/controllers/notificationCtrl.js index 092da25a71..21c3afe84e 100644 --- a/website/client-old/js/controllers/notificationCtrl.js +++ b/website/client-old/js/controllers/notificationCtrl.js @@ -3,6 +3,58 @@ habitrpg.controller('NotificationCtrl', ['$scope', '$rootScope', 'Shared', 'Content', 'User', 'Guide', 'Notification', 'Analytics', 'Achievement', 'Social', 'Tasks', function ($scope, $rootScope, Shared, Content, User, Guide, Notification, Analytics, Achievement, Social, Tasks) { + $scope.yesterDailiesModalOpen = false; + $rootScope.$watch('user.yesterDailies', function (after, before) { + if (!after || after.length === 0 || $scope.yesterDailiesModalOpen) return; + + var yesterDailies = []; + after.forEach(function (taskId) { + var dailyFound = _.find(User.user.dailys, function (task) { + return taskId === task._id; + }); + + if (dailyFound && dailyFound.group.approval && dailyFound.group.approval.requested) return; + if (dailyFound) yesterDailies.push(dailyFound); + }); + + if (yesterDailies.length === 0) return; + + var modalScope = $rootScope.$new(); + modalScope.obj = User.user; + modalScope.taskList = yesterDailies; + modalScope.list = { + showCompleted: false, + type: 'daily', + }; + + $scope.yesterDailiesModalOpen = true; + $rootScope.openModal('yesterDailies', { + scope: modalScope, + controller: ['$scope', 'Tasks', 'User', '$rootScope', function ($scope, Tasks, User, $rootScope) { + $rootScope.$on('task:scored', function (event, data) { + var task = data.task; + var indexOfTask = _.findIndex($scope.taskList, function (taskInList) { + return taskInList._id === task._id; + }); + if (!$scope.taskList[indexOfTask]) return; + $scope.taskList[indexOfTask].group.approval.requested = task.group.approval.requested; + if ($scope.taskList[indexOfTask].group.approval.requested) return; + $scope.taskList[indexOfTask].completed = task.completed; + }); + + $scope.ageDailies = function () { + Tasks.ageDailies() + .then(function () { + $scope.yesterDailiesModalOpen = false; + User.sync(); + }); + }; + }], + }) + .result.catch(function() { + $scope.yesterDailiesModalOpen = false; + }); + }); $rootScope.$watch('user.stats.hp', function (after, before) { if (after <= 0){ diff --git a/website/client-old/js/controllers/tasksCtrl.js b/website/client-old/js/controllers/tasksCtrl.js index c82dd27fab..40aa2fa0c5 100644 --- a/website/client-old/js/controllers/tasksCtrl.js +++ b/website/client-old/js/controllers/tasksCtrl.js @@ -36,6 +36,9 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N }, }); Analytics.updateUser(); + + if (task.group.approval.required) task.group.approval.requested = true; + $rootScope.$broadcast('task:scored', {task: task}); } $scope.score = function(task, direction) { diff --git a/website/client-old/js/directives/task-list.directive.js b/website/client-old/js/directives/task-list.directive.js index 8406b8de65..a10a046009 100644 --- a/website/client-old/js/directives/task-list.directive.js +++ b/website/client-old/js/directives/task-list.directive.js @@ -9,9 +9,10 @@ '$state', 'User', '$rootScope', + 'Tasks', ]; - function taskList($state, User, $rootScope) { + function taskList($state, User, $rootScope, Tasks) { return { restrict: 'EA', templateUrl: 'templates/task-list.html', @@ -24,6 +25,11 @@ // user: "=", // }, link: function($scope, element, attrs) { + $scope.checklistCompletion = Tasks.checklistCompletion; + + $scope.completeChecklistItem = function completeChecklistItem(task) { + User.updateTask(task, {body: task}); + }; // @TODO: The use of scope with tasks is incorrect. We need to fix all task ctrls to use directives/services // $scope.obj = {}; function setObj (obj, force) { @@ -95,4 +101,4 @@ } } } -}()); \ No newline at end of file +}()); diff --git a/website/common/locales/en/tasks.json b/website/common/locales/en/tasks.json index 1e3eee45dc..f1e0260df8 100644 --- a/website/common/locales/en/tasks.json +++ b/website/common/locales/en/tasks.json @@ -169,5 +169,10 @@ "yearlyRepeatHelpContent": "This task will be due every X years", "resets": "Resets", "summaryStart": "Repeats <%= frequency %> every <%= everyX %> <%= frequencyPlural %> ", - "nextDue": "Next Due Dates" + "nextDue": "Next Due Dates", + "yesterDailiesTitle": "Did you complete any of these?", + "yesterDailiesSubTitle": "(Check any you have)", + "yesterDailiesCallToAction": "That's All", + "yesterDailiesOptionTitle": "Prompt to check off if missed", + "yesterDailiesDescription": "Prompt to check off if missed" } diff --git a/website/views/shared/modals/index.jade b/website/views/shared/modals/index.jade index d3ae56c8ad..f947991f4b 100644 --- a/website/views/shared/modals/index.jade +++ b/website/views/shared/modals/index.jade @@ -28,6 +28,7 @@ include ./tasks-edit.jade include ./task-notes.jade include ./task-extra-notes.jade include ./testing.jade +include ./yester-dailies.jade //- Settings script(type='text/ng-template', id='modals/change-day-start.html') diff --git a/website/views/shared/modals/yester-dailies.jade b/website/views/shared/modals/yester-dailies.jade new file mode 100644 index 0000000000..f1604a3a93 --- /dev/null +++ b/website/views/shared/modals/yester-dailies.jade @@ -0,0 +1,12 @@ +script(type='text/ng-template', id='modals/yesterDailies.html') + .modal-header + h3.text-center=env.t('yesterDailiesTitle') + h5.text-center=env.t('yesterDailiesSubTitle') + .modal-body + .container-fluid + .row + .col-md-6.col-md-offset-3 + .task-column.dailys + task-list + .modal-footer + a.btn.btn-info.btn-lg.flex-column.btn-wrap(ng-click='ageDailies();$close()')=env.t('yesterDailiesCallToAction') diff --git a/website/views/shared/tasks/edit/advanced_options.jade b/website/views/shared/tasks/edit/advanced_options.jade index 16ae367614..94c71a6f39 100644 --- a/website/views/shared/tasks/edit/advanced_options.jade +++ b/website/views/shared/tasks/edit/advanced_options.jade @@ -10,6 +10,12 @@ div(ng-if='(task.type !== "reward") || task.userId || (!obj.auth && obj.purchase input.form-control(ng-model='task._edit.alias' type='text' placeholder=env.t('taskAliasPlaceholder')) include ./habits/frequency + + fieldset.option-group.advanced-option(ng-if='task.type === "daily" && task._edit._advanced') + .form-group + input(type='checkbox', ng-model='task._edit.yesterDaily') + legend.option-title + a.hint(href='http://habitica.wikia.com/wiki/Task_Alias', target='_blank', popover-trigger='mouseenter', popover="{{::env.t('yesterDailiesDescription')}}")=env.t('yesterDailiesOptionTitle') fieldset.option-group.advanced-option(ng-show="task._edit._advanced", ng-if="!obj.auth && obj.purchased && obj.purchased.active") group-tasks-actions(task='task', group='obj') diff --git a/website/views/shared/tasks/task.jade b/website/views/shared/tasks/task.jade index 7cd70f521f..3735ff62a0 100644 --- a/website/views/shared/tasks/task.jade +++ b/website/views/shared/tasks/task.jade @@ -1,13 +1,13 @@ script(id='templates/task.html', type="text/ng-template") li(id='task-{{::task._id}}', - ng-repeat='task in getTaskList(list, taskList, obj) | filterByTaskInfo: obj.filterQuery | conditionalOrderBy: list.view=="dated":"date"', + ng-repeat='task in getTaskList(list, taskList, obj) | filterByTaskInfo: obj.filterQuery | conditionalOrderBy: list.view=="dated":"date"', class='task {{getClasses(task, user, list, main)}}', ng-class='{"cast-target":spell && (list.type != "reward"), "locked-task":obj._locked === true}', ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', - ng-show='shouldShow(task, list, user.preferences)', + ng-show='!shouldShow || shouldShow(task, list, user.preferences)', popover-trigger='mouseenter', popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}', - data-popover-html="{{taskPopover(task) | markdown}}") - + data-popover-html="{{taskPopover(task) | markdown}}" + ) ng-form(name='taskForm') include ./meta_controls From 36fa3ab06f5c8455ef4bd734c33b2953c2fbd7b1 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Thu, 8 Jun 2017 17:10:05 -0700 Subject: [PATCH 05/15] Added cron check --- .../js/controllers/notificationCtrl.js | 39 ++++++++++--------- .../client-old/js/services/userServices.js | 10 +++++ website/server/controllers/api-v3/cron.js | 4 +- .../shared/tasks/edit/advanced_options.jade | 2 +- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/website/client-old/js/controllers/notificationCtrl.js b/website/client-old/js/controllers/notificationCtrl.js index 21c3afe84e..c955ba6937 100644 --- a/website/client-old/js/controllers/notificationCtrl.js +++ b/website/client-old/js/controllers/notificationCtrl.js @@ -3,18 +3,26 @@ habitrpg.controller('NotificationCtrl', ['$scope', '$rootScope', 'Shared', 'Content', 'User', 'Guide', 'Notification', 'Analytics', 'Achievement', 'Social', 'Tasks', function ($scope, $rootScope, Shared, Content, User, Guide, Notification, Analytics, Achievement, Social, Tasks) { - $scope.yesterDailiesModalOpen = false; - $rootScope.$watch('user.yesterDailies', function (after, before) { - if (!after || after.length === 0 || $scope.yesterDailiesModalOpen) return; + $rootScope.$watch('user', function (after, before) { + runYesterDailies(); + }); + + $rootScope.$on('userUpdated', function (after, before) { + runYesterDailies(); + }); + + function runYesterDailies() { + let userLastCron = moment(User.user.lastCron).local(); + let userDayStart = moment().startOf('day').add({ hours: User.user.preferences.dayStart }); + if (!userLastCron.isBefore(userDayStart)) return; + let dailys = User.user.dailys; + + if (!Boolean(dailys) || dailys.length === 0) return; var yesterDailies = []; - after.forEach(function (taskId) { - var dailyFound = _.find(User.user.dailys, function (task) { - return taskId === task._id; - }); - - if (dailyFound && dailyFound.group.approval && dailyFound.group.approval.requested) return; - if (dailyFound) yesterDailies.push(dailyFound); + dailys.forEach(function (task) { + if (task && task.group.approval && task.group.approval.requested) return; + if (task) yesterDailies.push(task); }); if (yesterDailies.length === 0) return; @@ -43,18 +51,11 @@ habitrpg.controller('NotificationCtrl', }); $scope.ageDailies = function () { - Tasks.ageDailies() - .then(function () { - $scope.yesterDailiesModalOpen = false; - User.sync(); - }); + User.runCron(); }; }], - }) - .result.catch(function() { - $scope.yesterDailiesModalOpen = false; }); - }); + } $rootScope.$watch('user.stats.hp', function (after, before) { if (after <= 0){ diff --git a/website/client-old/js/services/userServices.js b/website/client-old/js/services/userServices.js index dd42725b2b..d0ec3b89cf 100644 --- a/website/client-old/js/services/userServices.js +++ b/website/client-old/js/services/userServices.js @@ -416,6 +416,16 @@ angular.module('habitrpg') }); }, + runCron: function () { + $http({ + method: "POST", + url: 'api/v3/cron', + }) + .then(function (response) { + sync(); + }) + }, + setCustomDayStart: function (dayStart) { $http({ method: "POST", diff --git a/website/server/controllers/api-v3/cron.js b/website/server/controllers/api-v3/cron.js index 6d69ab3eb6..32ed99039a 100644 --- a/website/server/controllers/api-v3/cron.js +++ b/website/server/controllers/api-v3/cron.js @@ -12,9 +12,11 @@ let api = {}; */ api.cron = { method: 'POST', - url: '/debug/cron', + url: '/cron', middlewares: [authWithHeaders(), cron], async handler (req, res) { res.respond(200, {}); }, }; + +module.exports = api; diff --git a/website/views/shared/tasks/edit/advanced_options.jade b/website/views/shared/tasks/edit/advanced_options.jade index 94c71a6f39..2a87683713 100644 --- a/website/views/shared/tasks/edit/advanced_options.jade +++ b/website/views/shared/tasks/edit/advanced_options.jade @@ -13,7 +13,7 @@ div(ng-if='(task.type !== "reward") || task.userId || (!obj.auth && obj.purchase fieldset.option-group.advanced-option(ng-if='task.type === "daily" && task._edit._advanced') .form-group - input(type='checkbox', ng-model='task._edit.yesterDaily') + input(type='checkbox', ng-model='task._edit.yesterDaily', ng-disabled='$state.includes("options.social.challenges") || $state.includes("options.social.groups")') legend.option-title a.hint(href='http://habitica.wikia.com/wiki/Task_Alias', target='_blank', popover-trigger='mouseenter', popover="{{::env.t('yesterDailiesDescription')}}")=env.t('yesterDailiesOptionTitle') From 495bc9aa50dc7b2561333683abeb47460229ad73 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sun, 11 Jun 2017 22:55:35 -0600 Subject: [PATCH 06/15] Removed let from angular --- website/client-old/js/controllers/notificationCtrl.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/client-old/js/controllers/notificationCtrl.js b/website/client-old/js/controllers/notificationCtrl.js index c955ba6937..20b4457ca5 100644 --- a/website/client-old/js/controllers/notificationCtrl.js +++ b/website/client-old/js/controllers/notificationCtrl.js @@ -12,10 +12,10 @@ habitrpg.controller('NotificationCtrl', }); function runYesterDailies() { - let userLastCron = moment(User.user.lastCron).local(); - let userDayStart = moment().startOf('day').add({ hours: User.user.preferences.dayStart }); + var userLastCron = moment(User.user.lastCron).local(); + var userDayStart = moment().startOf('day').add({ hours: User.user.preferences.dayStart }); if (!userLastCron.isBefore(userDayStart)) return; - let dailys = User.user.dailys; + var dailys = User.user.dailys; if (!Boolean(dailys) || dailys.length === 0) return; From f738f550e754c09d3cba3cb22bd43ee62f32ee59 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Mon, 12 Jun 2017 07:01:14 -0600 Subject: [PATCH 07/15] Updated copy --- website/common/locales/en/tasks.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/common/locales/en/tasks.json b/website/common/locales/en/tasks.json index f1e0260df8..b8f0e2f648 100644 --- a/website/common/locales/en/tasks.json +++ b/website/common/locales/en/tasks.json @@ -170,9 +170,9 @@ "resets": "Resets", "summaryStart": "Repeats <%= frequency %> every <%= everyX %> <%= frequencyPlural %> ", "nextDue": "Next Due Dates", - "yesterDailiesTitle": "Did you complete any of these?", - "yesterDailiesSubTitle": "(Check any you have)", - "yesterDailiesCallToAction": "That's All", - "yesterDailiesOptionTitle": "Prompt to check off if missed", - "yesterDailiesDescription": "Prompt to check off if missed" + "yesterDailiesTitle": "You left these Dailies unchecked yesterday! Do you want to check off any of them now?", + "yesterDailiesSubTitle": "", + "yesterDailiesCallToAction": "Start My New Day!", + "yesterDailiesOptionTitle": "Confirm that this Daily wasn't done before applying damage", + "yesterDailiesDescription": "Hover: If this setting is applied, Habitica will ask you if you meant to leave the Daily undone before calculating and applying damage to your avatar. This can protect you against unintentional damage." } From a52bd668714edf451c350839e917084a98a176c4 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Thu, 15 Jun 2017 10:13:33 -0600 Subject: [PATCH 08/15] Fixed strings, yesterdaily filtering and cron check --- website/client-old/js/controllers/notificationCtrl.js | 9 +++++++-- website/common/locales/en/tasks.json | 3 +-- website/views/shared/modals/yester-dailies.jade | 1 - website/views/shared/tasks/edit/advanced_options.jade | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/website/client-old/js/controllers/notificationCtrl.js b/website/client-old/js/controllers/notificationCtrl.js index 20b4457ca5..c2e5706283 100644 --- a/website/client-old/js/controllers/notificationCtrl.js +++ b/website/client-old/js/controllers/notificationCtrl.js @@ -14,15 +14,20 @@ habitrpg.controller('NotificationCtrl', function runYesterDailies() { var userLastCron = moment(User.user.lastCron).local(); var userDayStart = moment().startOf('day').add({ hours: User.user.preferences.dayStart }); - if (!userLastCron.isBefore(userDayStart)) return; + + if (userLastCron.date() == userDayStart.date()) return; var dailys = User.user.dailys; if (!Boolean(dailys) || dailys.length === 0) return; + var yesterDay = moment().subtract('1', 'day').startOf('day').add({ hours: User.user.preferences.dayStart }); var yesterDailies = []; dailys.forEach(function (task) { if (task && task.group.approval && task.group.approval.requested) return; - if (task) yesterDailies.push(task); + + var shouldDo = Shared.shouldDo(yesterDay, task); + + if (task.yesterDaily && shouldDo) yesterDailies.push(task); }); if (yesterDailies.length === 0) return; diff --git a/website/common/locales/en/tasks.json b/website/common/locales/en/tasks.json index b8f0e2f648..c2bb16c928 100644 --- a/website/common/locales/en/tasks.json +++ b/website/common/locales/en/tasks.json @@ -171,8 +171,7 @@ "summaryStart": "Repeats <%= frequency %> every <%= everyX %> <%= frequencyPlural %> ", "nextDue": "Next Due Dates", "yesterDailiesTitle": "You left these Dailies unchecked yesterday! Do you want to check off any of them now?", - "yesterDailiesSubTitle": "", "yesterDailiesCallToAction": "Start My New Day!", "yesterDailiesOptionTitle": "Confirm that this Daily wasn't done before applying damage", - "yesterDailiesDescription": "Hover: If this setting is applied, Habitica will ask you if you meant to leave the Daily undone before calculating and applying damage to your avatar. This can protect you against unintentional damage." + "yesterDailiesDescription": "If this setting is applied, Habitica will ask you if you meant to leave the Daily undone before calculating and applying damage to your avatar. This can protect you against unintentional damage." } diff --git a/website/views/shared/modals/yester-dailies.jade b/website/views/shared/modals/yester-dailies.jade index f1604a3a93..461675a2af 100644 --- a/website/views/shared/modals/yester-dailies.jade +++ b/website/views/shared/modals/yester-dailies.jade @@ -1,7 +1,6 @@ script(type='text/ng-template', id='modals/yesterDailies.html') .modal-header h3.text-center=env.t('yesterDailiesTitle') - h5.text-center=env.t('yesterDailiesSubTitle') .modal-body .container-fluid .row diff --git a/website/views/shared/tasks/edit/advanced_options.jade b/website/views/shared/tasks/edit/advanced_options.jade index 2a87683713..39bff1884b 100644 --- a/website/views/shared/tasks/edit/advanced_options.jade +++ b/website/views/shared/tasks/edit/advanced_options.jade @@ -15,7 +15,7 @@ div(ng-if='(task.type !== "reward") || task.userId || (!obj.auth && obj.purchase .form-group input(type='checkbox', ng-model='task._edit.yesterDaily', ng-disabled='$state.includes("options.social.challenges") || $state.includes("options.social.groups")') legend.option-title - a.hint(href='http://habitica.wikia.com/wiki/Task_Alias', target='_blank', popover-trigger='mouseenter', popover="{{::env.t('yesterDailiesDescription')}}")=env.t('yesterDailiesOptionTitle') + span.hint(popover-trigger='mouseenter', popover="{{::env.t('yesterDailiesDescription')}}")=env.t('yesterDailiesOptionTitle') fieldset.option-group.advanced-option(ng-show="task._edit._advanced", ng-if="!obj.auth && obj.purchased && obj.purchased.active") group-tasks-actions(task='task', group='obj') From 7a8857010e746a53d8d5643ef55b0f3cff427384 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Fri, 16 Jun 2017 09:01:23 -0600 Subject: [PATCH 09/15] Resized tasks, filter completed, add cron for non yesterdailies --- .../js/controllers/notificationCtrl.js | 7 +++++-- .../components/guilds/newPartyModal.jade | 20 +++++++++++++++++++ .../views/shared/modals/yester-dailies.jade | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 website/client/components/guilds/newPartyModal.jade diff --git a/website/client-old/js/controllers/notificationCtrl.js b/website/client-old/js/controllers/notificationCtrl.js index c2e5706283..febd0c6e74 100644 --- a/website/client-old/js/controllers/notificationCtrl.js +++ b/website/client-old/js/controllers/notificationCtrl.js @@ -24,13 +24,16 @@ habitrpg.controller('NotificationCtrl', var yesterDailies = []; dailys.forEach(function (task) { if (task && task.group.approval && task.group.approval.requested) return; - + if (task.completed) return; var shouldDo = Shared.shouldDo(yesterDay, task); if (task.yesterDaily && shouldDo) yesterDailies.push(task); }); - if (yesterDailies.length === 0) return; + if (yesterDailies.length === 0) { + User.runCron(); + return; + }; var modalScope = $rootScope.$new(); modalScope.obj = User.user; diff --git a/website/client/components/guilds/newPartyModal.jade b/website/client/components/guilds/newPartyModal.jade new file mode 100644 index 0000000000..9d2eede077 --- /dev/null +++ b/website/client/components/guilds/newPartyModal.jade @@ -0,0 +1,20 @@ + diff --git a/website/views/shared/modals/yester-dailies.jade b/website/views/shared/modals/yester-dailies.jade index 461675a2af..312ca6b7c6 100644 --- a/website/views/shared/modals/yester-dailies.jade +++ b/website/views/shared/modals/yester-dailies.jade @@ -4,7 +4,7 @@ script(type='text/ng-template', id='modals/yesterDailies.html') .modal-body .container-fluid .row - .col-md-6.col-md-offset-3 + .col-md-8.col-md-offset-2 .task-column.dailys task-list .modal-footer From 136dcd27a9ff02b95fd21013d84937b2a3584f65 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Fri, 16 Jun 2017 12:28:42 -0600 Subject: [PATCH 10/15] Prevented modal close and showed correct due class in modal --- website/client-old/js/controllers/notificationCtrl.js | 2 ++ website/client-old/js/directives/task.directive.js | 2 +- website/common/script/libs/taskClasses.js | 7 +++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/website/client-old/js/controllers/notificationCtrl.js b/website/client-old/js/controllers/notificationCtrl.js index febd0c6e74..5a72fbaf10 100644 --- a/website/client-old/js/controllers/notificationCtrl.js +++ b/website/client-old/js/controllers/notificationCtrl.js @@ -42,10 +42,12 @@ habitrpg.controller('NotificationCtrl', showCompleted: false, type: 'daily', }; + modalScope.processingYesterdailies = true; $scope.yesterDailiesModalOpen = true; $rootScope.openModal('yesterDailies', { scope: modalScope, + backdrop: 'static', controller: ['$scope', 'Tasks', 'User', '$rootScope', function ($scope, Tasks, User, $rootScope) { $rootScope.$on('task:scored', function (event, data) { var task = data.task; diff --git a/website/client-old/js/directives/task.directive.js b/website/client-old/js/directives/task.directive.js index e1f8733c76..f7d527bdd7 100644 --- a/website/client-old/js/directives/task.directive.js +++ b/website/client-old/js/directives/task.directive.js @@ -17,7 +17,7 @@ scope: true, link: function($scope, element, attrs) { $scope.getClasses = function (task, user, list, main) { - return Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main); + return Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main, $scope.processingYesterdailies); } $scope.showNoteDetails = function (task) { diff --git a/website/common/script/libs/taskClasses.js b/website/common/script/libs/taskClasses.js index f1082a9f8d..83df0470e7 100644 --- a/website/common/script/libs/taskClasses.js +++ b/website/common/script/libs/taskClasses.js @@ -1,6 +1,7 @@ import { shouldDo, } from '../cron'; +import moment from 'moment'; /* Task classes given everything about the class @@ -8,7 +9,7 @@ Task classes given everything about the class // TODO move to the client -module.exports = function taskClasses (task, filters = [], dayStart = 0, lastCron = Number(new Date()), showCompleted = false, main = false) { +module.exports = function taskClasses (task, filters = [], dayStart = 0, lastCron = Number(new Date()), showCompleted = false, main = false, processingYesterdailies = false) { if (!task) { return ''; } @@ -34,7 +35,9 @@ module.exports = function taskClasses (task, filters = [], dayStart = 0, lastCro } if (type === 'todo' || type === 'daily') { - let notDue = !shouldDo(Number(new Date()), task, { dayStart }); + let dayShouldDo = moment(); + if (processingYesterdailies) dayShouldDo.subtract(1, 'days'); + let notDue = !shouldDo(Number(dayShouldDo), task, { dayStart }); let isNotDueDaily = type === 'daily' && notDue; if (completed || isNotDueDaily) { From ca8541e8c40863c075811b5beb25e1fd20bfb24b Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Wed, 21 Jun 2017 13:28:12 -0600 Subject: [PATCH 11/15] Added needsCron field --- .../js/controllers/notificationCtrl.js | 2 +- website/common/script/cron.js | 4 +- website/server/controllers/api-v3/user.js | 4 + website/server/middlewares/cron.js | 82 +--------------- website/server/models/user/methods.js | 95 ++++++++++++++++++- 5 files changed, 99 insertions(+), 88 deletions(-) diff --git a/website/client-old/js/controllers/notificationCtrl.js b/website/client-old/js/controllers/notificationCtrl.js index 5a72fbaf10..d4f868e117 100644 --- a/website/client-old/js/controllers/notificationCtrl.js +++ b/website/client-old/js/controllers/notificationCtrl.js @@ -15,7 +15,7 @@ habitrpg.controller('NotificationCtrl', var userLastCron = moment(User.user.lastCron).local(); var userDayStart = moment().startOf('day').add({ hours: User.user.preferences.dayStart }); - if (userLastCron.date() == userDayStart.date()) return; + if (!User.user.needsCron) return; var dailys = User.user.dailys; if (!Boolean(dailys) || dailys.length === 0) return; diff --git a/website/common/script/cron.js b/website/common/script/cron.js index 1205e15472..e9d1cd25e6 100644 --- a/website/common/script/cron.js +++ b/website/common/script/cron.js @@ -85,8 +85,10 @@ export function startOfDay (options = {}) { export function daysSince (yesterday, options = {}) { let o = sanitizeOptions(options); + let startOfNow = startOfDay(defaults({ now: o.now }, o)); + let startOfYesterday = startOfDay(defaults({ now: yesterday }, o)); - return startOfDay(defaults({ now: o.now }, o)).diff(startOfDay(defaults({ now: yesterday }, o)), 'days'); + return startOfNow.diff(startOfYesterday, 'days'); } /* diff --git a/website/server/controllers/api-v3/user.js b/website/server/controllers/api-v3/user.js index 45a0d6b269..272120f5df 100644 --- a/website/server/controllers/api-v3/user.js +++ b/website/server/controllers/api-v3/user.js @@ -78,6 +78,10 @@ api.getUser = { // Remove apiToken from response TODO make it private at the user level? returned in signup/login delete userToJSON.apiToken; + let {daysMissed} = user.daysUserHasMissed(new Date(), req); + userToJSON.needsCron = false; + if (daysMissed > 0) userToJSON.needsCron = true; + user.addComputedStatsToJSONObj(userToJSON.stats); return res.respond(200, userToJSON); }, diff --git a/website/server/middlewares/cron.js b/website/server/middlewares/cron.js index 18bed327e7..addf27c1df 100644 --- a/website/server/middlewares/cron.js +++ b/website/server/middlewares/cron.js @@ -1,6 +1,4 @@ -import _ from 'lodash'; import moment from 'moment'; -import common from '../../common'; import * as Tasks from '../models/task'; import Bluebird from 'bluebird'; import { model as Group } from '../models/group'; @@ -8,8 +6,6 @@ import { model as User } from '../models/user'; import { recoverCron, cron } from '../libs/cron'; import { v4 as uuid } from 'uuid'; -const daysSince = common.daysSince; - async function cronAsync (req, res) { let user = res.locals.user; if (!user) return null; // User might not be available when authentication is not mandatory @@ -18,83 +14,7 @@ async function cronAsync (req, res) { let now = new Date(); try { - // If the user's timezone has changed (due to travel or daylight savings), - // cron can be triggered twice in one day, so we check for that and use - // both timezones to work out if cron should run. - // CDS = Custom Day Start time. - let timezoneOffsetFromUserPrefs = user.preferences.timezoneOffset; - let timezoneOffsetAtLastCron = _.isFinite(user.preferences.timezoneOffsetAtLastCron) ? user.preferences.timezoneOffsetAtLastCron : timezoneOffsetFromUserPrefs; - let timezoneOffsetFromBrowser = Number(req.header('x-user-timezoneoffset')); - timezoneOffsetFromBrowser = _.isFinite(timezoneOffsetFromBrowser) ? timezoneOffsetFromBrowser : timezoneOffsetFromUserPrefs; - // NB: All timezone offsets can be 0, so can't use `... || ...` to apply non-zero defaults - - if (timezoneOffsetFromBrowser !== timezoneOffsetFromUserPrefs) { - // The user's browser has just told Habitica that the user's timezone has - // changed so store and use the new zone. - user.preferences.timezoneOffset = timezoneOffsetFromBrowser; - timezoneOffsetFromUserPrefs = timezoneOffsetFromBrowser; - } - - // How many days have we missed using the user's current timezone: - let daysMissed = daysSince(user.lastCron, _.defaults({now}, user.preferences)); - - if (timezoneOffsetAtLastCron !== timezoneOffsetFromUserPrefs) { - // Since cron last ran, the user's timezone has changed. - // How many days have we missed using the old timezone: - let daysMissedNewZone = daysMissed; - let daysMissedOldZone = daysSince(user.lastCron, _.defaults({ - now, - timezoneOffsetOverride: timezoneOffsetAtLastCron, - }, user.preferences)); - - if (timezoneOffsetAtLastCron < timezoneOffsetFromUserPrefs) { - // The timezone change was in the unsafe direction. - // E.g., timezone changes from UTC+1 (offset -60) to UTC+0 (offset 0). - // or timezone changes from UTC-4 (offset 240) to UTC-5 (offset 300). - // Local time changed from, for example, 03:00 to 02:00. - - if (daysMissedOldZone > 0 && daysMissedNewZone > 0) { - // Both old and new timezones indicate that we SHOULD run cron, so - // it is safe to do so immediately. - daysMissed = Math.min(daysMissedOldZone, daysMissedNewZone); - // use minimum value to be nice to user - } else if (daysMissedOldZone > 0) { - // The old timezone says that cron should run; the new timezone does not. - // This should be impossible for this direction of timezone change, but - // just in case I'm wrong... - // TODO - // console.log("zone has changed - old zone says run cron, NEW zone says no - stop cron now only -- SHOULD NOT HAVE GOT TO HERE", timezoneOffsetAtLastCron, timezoneOffsetFromUserPrefs, now); // used in production for confirming this never happens - } else if (daysMissedNewZone > 0) { - // The old timezone says that cron should NOT run -- i.e., cron has - // already run today, from the old timezone's point of view. - // The new timezone says that cron SHOULD run, but this is almost - // certainly incorrect. - // This happens when cron occurred at a time soon after the CDS. When - // you reinterpret that time in the new timezone, it looks like it - // was before the CDS, because local time has stepped backwards. - // To fix this, rewrite the cron time to a time that the new - // timezone interprets as being in today. - - daysMissed = 0; // prevent cron running now - let timezoneOffsetDiff = timezoneOffsetAtLastCron - timezoneOffsetFromUserPrefs; - // e.g., for dangerous zone change: 240 - 300 = -60 or -660 - -600 = -60 - - user.lastCron = moment(user.lastCron).subtract(timezoneOffsetDiff, 'minutes'); - // NB: We don't change user.auth.timestamps.loggedin so that will still record the time that the previous cron actually ran. - // From now on we can ignore the old timezone: - user.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs; - } else { - // Both old and new timezones indicate that cron should - // NOT run. - daysMissed = 0; // prevent cron running now - } - } else if (timezoneOffsetAtLastCron > timezoneOffsetFromUserPrefs) { - daysMissed = daysMissedNewZone; - // TODO: Either confirm that there is nothing that could possibly go wrong here and remove the need for this else branch, or fix stuff. - // There are probably situations where the Dailies do not reset early enough for a user who was expecting the zone change and wants to use all their Dailies immediately in the new zone; - // if so, we should provide an option for easy reset of Dailies (can't be automatic because there will be other situations where the user was not prepared). - } - } + let {daysMissed, timezoneOffsetFromUserPrefs} = user.daysUserHasMissed(now, req); if (daysMissed <= 0) { if (user.isModified()) await user.save(); diff --git a/website/server/models/user/methods.js b/website/server/models/user/methods.js index 34cd5608f0..35bbeeda16 100644 --- a/website/server/models/user/methods.js +++ b/website/server/models/user/methods.js @@ -13,6 +13,9 @@ import amazonPayments from '../../libs/amazonPayments'; import stripePayments from '../../libs/stripePayments'; import paypalPayments from '../../libs/paypalPayments'; +const daysSince = common.daysSince; + + schema.methods.isSubscribed = function isSubscribed () { let now = new Date(); let plan = this.purchased.plan; @@ -27,7 +30,7 @@ schema.methods.hasNotCancelled = function hasNotCancelled () { // Get an array of groups ids the user is member of schema.methods.getGroups = function getUserGroups () { - let userGroups = this.guilds.slice(0); // clone user.guilds so we don't modify the original + let userGroups = this.guilds.slice(0); // clone this.guilds so we don't modify the original if (this.party._id) userGroups.push(this.party._id); userGroups.push(TAVERN_ID); return userGroups; @@ -35,7 +38,7 @@ schema.methods.getGroups = function getUserGroups () { /** - * Sends a message to a user. Archives a copy in sender's inbox. + * Sends a message to a this. Archives a copy in sender's inbox. * * @param userToReceiveMessage The receiver * @param options @@ -63,7 +66,7 @@ schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, o * Creates a notification based on the input parameters and adds it to the local user notifications array. * This does not save the notification to the database or interact with the database in any way. * - * @param type The type of notification to add to the user. Possible values are defined in the UserNotificaiton Schema + * @param type The type of notification to add to the this. Possible values are defined in the UserNotificaiton Schema * @param data The data to add to the notification */ schema.methods.addNotification = function addUserNotification (type, data = {}) { @@ -80,7 +83,7 @@ schema.methods.addNotification = function addUserNotification (type, data = {}) * the user document(s) opened. * * @param query A Mongoose query defining the users to add the notification to. - * @param type The type of notification to add to the user. Possible values are defined in the UserNotificaiton Schema + * @param type The type of notification to add to the this. Possible values are defined in the UserNotificaiton Schema * @param data The data to add to the notification */ schema.statics.pushNotification = async function pushNotification (query, type, data = {}) { @@ -95,7 +98,7 @@ schema.statics.pushNotification = async function pushNotification (query, type, // Add stats.toNextLevel, stats.maxMP and stats.maxHealth // to a JSONified User stats object schema.methods.addComputedStatsToJSONObj = function addComputedStatsToUserJSONObj (statsObject) { - // NOTE: if an item is manually added to user.stats then + // NOTE: if an item is manually added to this.stats then // common/fns/predictableRandom must be tweaked so the new item is not considered. // Otherwise the client will have it while the server won't and the results will be different. statsObject.toNextLevel = common.tnl(this.stats.lvl); @@ -123,3 +126,85 @@ schema.methods.cancelSubscription = async function cancelSubscription () { return await payments.cancelSubscription({user: this}); }; + +schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) { + // If the user's timezone has changed (due to travel or daylight savings), + // cron can be triggered twice in one day, so we check for that and use + // both timezones to work out if cron should run. + // CDS = Custom Day Start time. + let timezoneOffsetFromUserPrefs = this.preferences.timezoneOffset; + let timezoneOffsetAtLastCron = isFinite(this.preferences.timezoneOffsetAtLastCron) ? this.preferences.timezoneOffsetAtLastCron : timezoneOffsetFromUserPrefs; + let timezoneOffsetFromBrowser = typeof req.header === 'function' && Number(req.header('x-user-timezoneoffset')); + timezoneOffsetFromBrowser = isFinite(timezoneOffsetFromBrowser) ? timezoneOffsetFromBrowser : timezoneOffsetFromUserPrefs; + // NB: All timezone offsets can be 0, so can't use `... || ...` to apply non-zero defaults + + if (timezoneOffsetFromBrowser !== timezoneOffsetFromUserPrefs) { + // The user's browser has just told Habitica that the user's timezone has + // changed so store and use the new zone. + this.preferences.timezoneOffset = timezoneOffsetFromBrowser; + timezoneOffsetFromUserPrefs = timezoneOffsetFromBrowser; + } + + // How many days have we missed using the user's current timezone: + let daysMissed = daysSince(this.lastCron, defaults({now}, this.preferences)); + + if (timezoneOffsetAtLastCron !== timezoneOffsetFromUserPrefs) { + // Since cron last ran, the user's timezone has changed. + // How many days have we missed using the old timezone: + let daysMissedNewZone = daysMissed; + let daysMissedOldZone = daysSince(this.lastCron, defaults({ + now, + timezoneOffsetOverride: timezoneOffsetAtLastCron, + }, this.preferences)); + + if (timezoneOffsetAtLastCron < timezoneOffsetFromUserPrefs) { + // The timezone change was in the unsafe direction. + // E.g., timezone changes from UTC+1 (offset -60) to UTC+0 (offset 0). + // or timezone changes from UTC-4 (offset 240) to UTC-5 (offset 300). + // Local time changed from, for example, 03:00 to 02:00. + + if (daysMissedOldZone > 0 && daysMissedNewZone > 0) { + // Both old and new timezones indicate that we SHOULD run cron, so + // it is safe to do so immediately. + daysMissed = Math.min(daysMissedOldZone, daysMissedNewZone); + // use minimum value to be nice to user + } else if (daysMissedOldZone > 0) { + // The old timezone says that cron should run; the new timezone does not. + // This should be impossible for this direction of timezone change, but + // just in case I'm wrong... + // TODO + // console.log("zone has changed - old zone says run cron, NEW zone says no - stop cron now only -- SHOULD NOT HAVE GOT TO HERE", timezoneOffsetAtLastCron, timezoneOffsetFromUserPrefs, now); // used in production for confirming this never happens + } else if (daysMissedNewZone > 0) { + // The old timezone says that cron should NOT run -- i.e., cron has + // already run today, from the old timezone's point of view. + // The new timezone says that cron SHOULD run, but this is almost + // certainly incorrect. + // This happens when cron occurred at a time soon after the CDS. When + // you reinterpret that time in the new timezone, it looks like it + // was before the CDS, because local time has stepped backwards. + // To fix this, rewrite the cron time to a time that the new + // timezone interprets as being in today. + + daysMissed = 0; // prevent cron running now + let timezoneOffsetDiff = timezoneOffsetAtLastCron - timezoneOffsetFromUserPrefs; + // e.g., for dangerous zone change: 240 - 300 = -60 or -660 - -600 = -60 + + this.lastCron = moment(this.lastCron).subtract(timezoneOffsetDiff, 'minutes'); + // NB: We don't change this.auth.timestamps.loggedin so that will still record the time that the previous cron actually ran. + // From now on we can ignore the old timezone: + this.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs; + } else { + // Both old and new timezones indicate that cron should + // NOT run. + daysMissed = 0; // prevent cron running now + } + } else if (timezoneOffsetAtLastCron > timezoneOffsetFromUserPrefs) { + daysMissed = daysMissedNewZone; + // TODO: Either confirm that there is nothing that could possibly go wrong here and remove the need for this else branch, or fix stuff. + // There are probably situations where the Dailies do not reset early enough for a user who was expecting the zone change and wants to use all their Dailies immediately in the new zone; + // if so, we should provide an option for easy reset of Dailies (can't be automatic because there will be other situations where the user was not prepared). + } + } + + return {daysMissed, timezoneOffsetFromUserPrefs}; +}; From 829998248495bd76c54995533d70f5b2fa2a25de Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Tue, 27 Jun 2017 22:01:01 -0600 Subject: [PATCH 12/15] Ensured damage was only done for dailies that were due yesterday --- test/api/v3/unit/libs/cron.test.js | 19 +++++++++++++++++++ website/server/libs/cron.js | 6 +++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/test/api/v3/unit/libs/cron.test.js b/test/api/v3/unit/libs/cron.test.js index 6382b85a3d..ef465443c6 100644 --- a/test/api/v3/unit/libs/cron.test.js +++ b/test/api/v3/unit/libs/cron.test.js @@ -476,6 +476,25 @@ describe('cron', () => { expect(progress.down).to.equal(-1); }); + + it('should do damage for only yesterday\'s dailies', () => { + daysMissed = 3; + tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1}); + + let daily = { + text: 'test daily', + type: 'daily', + }; + let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap + tasksByType.dailys.push(task); + tasksByType.dailys[1].startDate = moment(new Date()).subtract({days: 2}); + tasksByType.dailys[1].everyX = 2; + tasksByType.dailys[1].frequency = 'daily'; + + cron({user, tasksByType, daysMissed, analytics}); + + expect(user.stats.hp).to.equal(48); + }); }); describe('habits', () => { diff --git a/website/server/libs/cron.js b/website/server/libs/cron.js index a7a64fae3d..9bca95c8da 100644 --- a/website/server/libs/cron.js +++ b/website/server/libs/cron.js @@ -261,6 +261,10 @@ export function cron (options = {}) { let EvadeTask = 0; let scheduleMisses = daysMissed; + // Only check one day back + let dailiesDaysMissed = daysMissed; + if (dailiesDaysMissed > 1) dailiesDaysMissed = 1; + if (completed) { dailyChecked += 1; if (!atLeastOneDailyDue) { // only bother checking until the first thing is found @@ -271,7 +275,7 @@ export function cron (options = {}) { // dailys repeat, so need to calculate how many they've missed according to their own schedule scheduleMisses = 0; - for (let i = 0; i < daysMissed; i++) { + for (let i = 0; i < dailiesDaysMissed; i++) { let thatDay = moment(now).subtract({days: i + 1}); if (shouldDo(thatDay.toDate(), task, user.preferences)) { From acb4b790785a298e973d6525ff8269b77848b75e Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Tue, 27 Jun 2017 22:22:20 -0600 Subject: [PATCH 13/15] Added option for getting isDue field for specified date --- .../integration/tasks/GET-tasks_user.test.js | 23 +++++++++++++++++++ website/server/controllers/api-v3/tasks.js | 3 ++- website/server/libs/taskManager.js | 13 ++++++++--- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/test/api/v3/integration/tasks/GET-tasks_user.test.js b/test/api/v3/integration/tasks/GET-tasks_user.test.js index d1a24ebfc8..87af87d7a5 100644 --- a/test/api/v3/integration/tasks/GET-tasks_user.test.js +++ b/test/api/v3/integration/tasks/GET-tasks_user.test.js @@ -1,3 +1,4 @@ +import moment from 'moment'; import { generateUser, } from '../../../../helpers/api-integration/v3'; @@ -127,4 +128,26 @@ describe('GET /tasks/user', () => { let allCompletedTodos = await user.get('/tasks/user?type=_allCompletedTodos'); expect(allCompletedTodos.length).to.equal(numberOfTodos); }); + + it('returns dailies with isDue for the date specified', async () => { + let startDate = moment().subtract('1', 'days').toDate(); + let createdTasks = await user.post('/tasks/user', [ + { + text: 'test daily', + type: 'daily', + startDate, + frequency: 'daily', + everyX: 2, + }, + ]); + let dailys = await user.get('/tasks/user?type=dailys'); + + expect(dailys.length).to.be.at.least(1); + expect(dailys[0]._id).to.equal(createdTasks._id); + expect(dailys[0].isDue).to.be.false; + + let dailys2 = await user.get(`/tasks/user?type=dailys&dueDate=${startDate}`); + expect(dailys2[0]._id).to.equal(createdTasks._id); + expect(dailys2[0].isDue).to.be.true; + }); }); diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index c48ce8d47c..9105bb1c62 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -272,8 +272,9 @@ api.getUserTasks = { if (validationErrors) throw validationErrors; let user = res.locals.user; + let dueDate = req.query.dueDate; - let tasks = await getTasks(req, res, {user}); + let tasks = await getTasks(req, res, {user, dueDate}); return res.respond(200, tasks); }, }; diff --git a/website/server/libs/taskManager.js b/website/server/libs/taskManager.js index 8c4ecb1c79..3b8759965b 100644 --- a/website/server/libs/taskManager.js +++ b/website/server/libs/taskManager.js @@ -1,3 +1,4 @@ +import moment from 'moment'; import * as Tasks from '../models/task'; import { BadRequest, @@ -22,13 +23,16 @@ async function _validateTaskAlias (tasks, res) { }); } -export function setNextDue (task, user) { +export function setNextDue (task, user, dueDateOption) { if (task.type !== 'daily') return; + let dateTaskIsDue = Date.now(); + if (dueDateOption) dateTaskIsDue = moment(dueDateOption); + console.log(dueDateOption) let optionsForShouldDo = user.preferences.toObject(); - task.isDue = shared.shouldDo(Date.now(), task, optionsForShouldDo); + task.isDue = shared.shouldDo(dateTaskIsDue, task, optionsForShouldDo); optionsForShouldDo.nextDue = true; - let nextDue = shared.shouldDo(Date.now(), task, optionsForShouldDo); + let nextDue = shared.shouldDo(dateTaskIsDue, task, optionsForShouldDo); if (nextDue && nextDue.length > 0) { task.nextDue = nextDue.map((dueDate) => { return dueDate.toISOString(); @@ -120,6 +124,7 @@ export async function getTasks (req, res, options = {}) { user, challenge, group, + dueDate, } = options; let query = {userId: user._id}; @@ -185,6 +190,8 @@ export async function getTasks (req, res, options = {}) { } else { orderedTasks[i] = task; } + + if (dueDate) setNextDue(task, user, dueDate); }); // Remove empty values from the array and add any unordered task From 43b6f71044040dc3006ec7a7f425b6648a36dc56 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Wed, 28 Jun 2017 15:25:47 -0600 Subject: [PATCH 14/15] Fixed lint issue --- website/server/libs/taskManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/server/libs/taskManager.js b/website/server/libs/taskManager.js index 3b8759965b..e9bcc4f2d2 100644 --- a/website/server/libs/taskManager.js +++ b/website/server/libs/taskManager.js @@ -28,7 +28,7 @@ export function setNextDue (task, user, dueDateOption) { let dateTaskIsDue = Date.now(); if (dueDateOption) dateTaskIsDue = moment(dueDateOption); - console.log(dueDateOption) + let optionsForShouldDo = user.preferences.toObject(); task.isDue = shared.shouldDo(dateTaskIsDue, task, optionsForShouldDo); optionsForShouldDo.nextDue = true; From e4f5950ffcd8bd2b4fcfc24886bf55789bc9dc44 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Thu, 29 Jun 2017 08:11:02 -0600 Subject: [PATCH 15/15] Added required field --- website/server/models/task.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/server/models/task.js b/website/server/models/task.js index 27f052111b..e17809ad8a 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -245,7 +245,7 @@ export let DailySchema = new Schema(_.defaults({ weeksOfMonth: {type: [Number], default: []}, // Weeks of the month that the daily should repeat on isDue: {type: Boolean}, nextDue: [{type: String}], - yesterDaily: {type: Boolean, default: true}, + yesterDaily: {type: Boolean, default: true, required: true}, }, habitDailySchema(), dailyTodoSchema()), subDiscriminatorOptions); export let daily = Task.discriminator('daily', DailySchema);