From 8c68f450c6380b98218fe43df90c28ba3625c286 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Wed, 15 Mar 2017 14:13:15 -0600 Subject: [PATCH 01/26] Enabled repeatables --- website/common/script/cron.js | 89 ++++++++++++------- .../views/shared/tasks/edit/repeatables.jade | 22 ++--- 2 files changed, 68 insertions(+), 43 deletions(-) diff --git a/website/common/script/cron.js b/website/common/script/cron.js index 2ce36c0e9b..6b9dc6a16c 100644 --- a/website/common/script/cron.js +++ b/website/common/script/cron.js @@ -4,8 +4,10 @@ Cron and time / day functions ------------------------------------------------------ */ -import _ from 'lodash'; // eslint-disable-line lodash/import-scope +import defaults from 'lodash/defaults'; +import invert from 'lodash/invert'; import moment from 'moment'; +import 'moment-recur'; export const DAY_MAPPING = { 0: 'su', @@ -17,6 +19,8 @@ export const DAY_MAPPING = { 6: 's', }; +export const DAY_MAPPING_STRING_TO_NUMBER = invert(DAY_MAPPING); + /* Each time we perform date maths (cron, task-due-days, etc), we need to consider user preferences. Specifically {dayStart} (custom day start) and {timezoneOffset}. This function sanitizes / defaults those values. @@ -25,13 +29,13 @@ export const DAY_MAPPING = { function sanitizeOptions (o) { let ref = Number(o.dayStart || 0); - let dayStart = !_.isNaN(ref) && ref >= 0 && ref <= 24 ? ref : 0; + let dayStart = !Number.isNaN(ref) && ref >= 0 && ref <= 24 ? ref : 0; let timezoneOffset; let timezoneOffsetDefault = Number(moment().zone()); - if (_.isFinite(o.timezoneOffsetOverride)) { + if (Number.isFinite(o.timezoneOffsetOverride)) { timezoneOffset = Number(o.timezoneOffsetOverride); - } else if (_.isFinite(o.timezoneOffset)) { + } else if (Number.isFinite(o.timezoneOffset)) { timezoneOffset = Number(o.timezoneOffset); } else { timezoneOffset = timezoneOffsetDefault; @@ -81,44 +85,65 @@ export function startOfDay (options = {}) { export function daysSince (yesterday, options = {}) { let o = sanitizeOptions(options); - return startOfDay(_.defaults({ now: o.now }, o)).diff(startOfDay(_.defaults({ now: yesterday }, o)), 'days'); + return startOfDay(defaults({ now: o.now }, o)).diff(startOfDay(defaults({ now: yesterday }, o)), 'days'); } /* Should the user do this task on this date, given the task's repeat options and user.preferences.dayStart? */ -export function shouldDo (day, dailyTask, options = {}) { +export function shouldDo (day, dailyTask) { if (dailyTask.type !== 'daily') { return false; } - let o = sanitizeOptions(options); - let startOfDayWithCDSTime = startOfDay(_.defaults({ now: day }, o)); - // The time portion of the Start Date is never visible to or modifiable by the user so we must ignore it. - // Therefore, we must also ignore the time portion of the user's day start (startOfDayWithCDSTime), otherwise the date comparison will be wrong for some times. - // NB: The user's day start date has already been converted to the PREVIOUS day's date if the time portion was before CDS. - let taskStartDate = moment(dailyTask.startDate).zone(o.timezoneOffset); + day = moment(day).startOf('day').toDate(); + let startDate = moment(dailyTask.startDate).startOf('day').toDate(); - taskStartDate = moment(taskStartDate).startOf('day'); - if (taskStartDate > startOfDayWithCDSTime.startOf('day')) { - return false; // Daily starts in the future - } - if (dailyTask.frequency === 'daily') { // "Every X Days" - if (!dailyTask.everyX) { - return false; // error condition + let daysOfTheWeek = []; + + if (dailyTask.repeat) { + for (let [repeatDay, active] of Object.entries(dailyTask.repeat)) { + if (active) daysOfTheWeek.push(parseInt(DAY_MAPPING_STRING_TO_NUMBER[repeatDay], 10)); } - let daysSinceTaskStart = startOfDayWithCDSTime.startOf('day').diff(taskStartDate, 'days'); - - return daysSinceTaskStart % dailyTask.everyX === 0; - } else if (dailyTask.frequency === 'weekly') { // "On Certain Days of the Week" - if (!dailyTask.repeat) { - return false; // error condition - } - let dayOfWeekNum = startOfDayWithCDSTime.day(); // e.g., 0 for Sunday - - return dailyTask.repeat[DAY_MAPPING[dayOfWeekNum]]; - } else { - return false; // error condition - unexpected frequency string } -} \ No newline at end of file + + if (dailyTask.frequency === 'daily') { + if (!dailyTask.everyX) return false; // error condition + let schedule = moment(startDate).recur() + .every(dailyTask.everyX).days(); + return schedule.matches(day); + } else if (dailyTask.frequency === 'weekly') { + let schedule = moment(startDate).recur(); + + if (dailyTask.everyX > 1) { + schedule = schedule.every(dailyTask.everyX).weeks(); + } + + schedule = schedule.every(daysOfTheWeek).daysOfWeek(); + + return schedule.matches(day); + } else if (dailyTask.frequency === 'monthly') { + let schedule = moment(startDate).recur(); + + let differenceInMonths = moment(day).month() + 1 - moment(startDate).month() + 1; + let matchEveryX = differenceInMonths % dailyTask.everyX === 0; + + if (dailyTask.weeksOfMonth && dailyTask.weeksOfMonth.length > 0) { + schedule = schedule.every(daysOfTheWeek).daysOfWeek() + .every(dailyTask.weeksOfMonth).weeksOfMonthByDay(); + } else if (dailyTask.daysOfMonth && dailyTask.daysOfMonth.length > 0) { + schedule = schedule.every(dailyTask.daysOfMonth).daysOfMonth(); + } + + return schedule.matches(day) && matchEveryX; + } else if (dailyTask.frequency === 'yearly') { + let schedule = moment(startDate).recur(); + + schedule = schedule.every(dailyTask.everyX).years(); + + return schedule.matches(day); + } + + return false; +} diff --git a/website/views/shared/tasks/edit/repeatables.jade b/website/views/shared/tasks/edit/repeatables.jade index bb34d2fb2a..54f82089ae 100644 --- a/website/views/shared/tasks/edit/repeatables.jade +++ b/website/views/shared/tasks/edit/repeatables.jade @@ -8,15 +8,15 @@ fieldset.option-group.advanced-option(ng-show="task.type === 'daily'") br - //- select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)') - //- option(value='daily')=env.t('daily') - //- option(value='weekly')=env.t('weekly') - //- option(value='monthly')=env.t('monthly') - //- option(value='yearly')=env.t('yearly') - select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)') - option(value='weekly')=env.t('repeatWeek') - option(value='daily')=env.t('repeatDays') + option(value='daily')=env.t('daily') + option(value='weekly')=env.t('weekly') + option(value='monthly')=env.t('monthly') + option(value='yearly')=env.t('yearly') + + //- select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)') + //- option(value='weekly')=env.t('repeatWeek') + //- option(value='daily')=env.t('repeatDays') include ./dailies/repeat_options @@ -29,6 +29,6 @@ fieldset.option-group.advanced-option(ng-show="task.type === 'daily'") input(type="radio", ng-model='task._edit.repeatsOn', value='dayOfWeek') =env.t('dayOfWeek') - //- .form-group - //- legend.option-title=env.t('summary') - //- div {{summary}} \ No newline at end of file + .form-group + legend.option-title=env.t('summary') + div {{summary}} \ No newline at end of file From 0fd85c0d60b1d19d5a9cdb7489640c293e463a96 Mon Sep 17 00:00:00 2001 From: TheHollidayInn Date: Mon, 27 Mar 2017 09:52:32 -0600 Subject: [PATCH 02/26] Added every x to weekly --- website/views/shared/tasks/edit/dailies/repeat_options.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/views/shared/tasks/edit/dailies/repeat_options.jade b/website/views/shared/tasks/edit/dailies/repeat_options.jade index 24a03728c8..ca6630e8df 100644 --- a/website/views/shared/tasks/edit/dailies/repeat_options.jade +++ b/website/views/shared/tasks/edit/dailies/repeat_options.jade @@ -1,4 +1,4 @@ -.form-group(ng-if='task._edit.frequency !== "weekly"') +.form-group legend.option-title span.hint(popover-trigger='mouseenter', popover-title=env.t('repeatHelpTitle'), popover='{{env.t(task._edit.frequency + "RepeatHelpContent")}}')=env.t('repeatEvery') From a0ee73e944f69561e2065e110b6465d962e0b593 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Mon, 8 May 2017 09:39:50 -0600 Subject: [PATCH 03/26] Updated new recur logic to work with tests --- test/common/shouldDo.test.js | 2 +- website/common/script/cron.js | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/test/common/shouldDo.test.js b/test/common/shouldDo.test.js index 5674f9c048..0c5d01766a 100644 --- a/test/common/shouldDo.test.js +++ b/test/common/shouldDo.test.js @@ -70,7 +70,7 @@ describe('shouldDo', () => { expect(shouldDo(day, dailyTask, options)).to.equal(true); }); - it('returns true if Start Date is today', () => { + it('returns true if Start Date is today', () => { dailyTask.startDate = moment().startOf('day').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); diff --git a/website/common/script/cron.js b/website/common/script/cron.js index e4ec27bfbe..b667632e74 100644 --- a/website/common/script/cron.js +++ b/website/common/script/cron.js @@ -33,7 +33,7 @@ function sanitizeOptions (o) { let timezoneOffset; let timezoneOffsetDefault = Number(moment().utcOffset()); - if (_.isFinite(o.timezoneOffsetOverride)) { + if (isFinite(o.timezoneOffsetOverride)) { timezoneOffset = Number(o.timezoneOffsetOverride); } else if (Number.isFinite(o.timezoneOffset)) { timezoneOffset = Number(o.timezoneOffset); @@ -97,11 +97,15 @@ export function shouldDo (day, dailyTask, options = {}) { return false; } let o = sanitizeOptions(options); - let startOfDayWithCDSTime = startOfDay(_.defaults({ now: day }, o)); + let startOfDayWithCDSTime = startOfDay(defaults({ now: day }, o)); // The time portion of the Start Date is never visible to or modifiable by the user so we must ignore it. // Therefore, we must also ignore the time portion of the user's day start (startOfDayWithCDSTime), otherwise the date comparison will be wrong for some times. // NB: The user's day start date has already been converted to the PREVIOUS day's date if the time portion was before CDS. - let startDate = moment(dailyTask.startDate).utcOffset(o.timezoneOffset); + let startDate = moment(dailyTask.startDate).utcOffset(o.timezoneOffset).startOf('day'); + + if (startDate > startOfDayWithCDSTime.startOf('day')) { + return false; // Daily starts in the future + } let daysOfTheWeek = []; @@ -115,7 +119,7 @@ export function shouldDo (day, dailyTask, options = {}) { if (!dailyTask.everyX) return false; // error condition let schedule = moment(startDate).recur() .every(dailyTask.everyX).days(); - return schedule.matches(day); + return schedule.matches(startOfDayWithCDSTime); } else if (dailyTask.frequency === 'weekly') { let schedule = moment(startDate).recur(); @@ -125,7 +129,7 @@ export function shouldDo (day, dailyTask, options = {}) { schedule = schedule.every(daysOfTheWeek).daysOfWeek(); - return schedule.matches(day); + return schedule.matches(startOfDayWithCDSTime); } else if (dailyTask.frequency === 'monthly') { let schedule = moment(startDate).recur(); From 2659a4117b5aab72161d9d7bccbf820624f1254c Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Mon, 8 May 2017 09:52:15 -0600 Subject: [PATCH 04/26] Added repeatable tests back --- test/common/shouldDo.test.js | 426 +++++++++++++++++------------------ 1 file changed, 213 insertions(+), 213 deletions(-) diff --git a/test/common/shouldDo.test.js b/test/common/shouldDo.test.js index 0c5d01766a..d11a959ca1 100644 --- a/test/common/shouldDo.test.js +++ b/test/common/shouldDo.test.js @@ -1,6 +1,6 @@ -import { shouldDo } from '../../website/common/script/cron'; +import { shouldDo, DAY_MAPPING } from '../../website/common/script/cron'; import moment from 'moment'; -// import 'moment-recur'; +import 'moment-recur'; describe('shouldDo', () => { let day, dailyTask; @@ -481,215 +481,215 @@ describe('shouldDo', () => { }); }); - // context('Every X Weeks', () => { - // it('leaves daily inactive if it has not been the specified number of weeks', () => { - // dailyTask.everyX = 3; - // let tomorrow = moment().add(1, 'day').toDate(); - // - // expect(shouldDo(tomorrow, dailyTask, options)).to.equal(false); - // }); - // - // it('leaves daily inactive if on every (x) week on weekday it is incorrect weekday', () => { - // dailyTask.repeat = { - // su: false, - // s: false, - // f: false, - // th: false, - // w: false, - // t: false, - // m: false, - // }; - // - // day = moment(); - // dailyTask.repeat[DAY_MAPPING[day.day()]] = true; - // dailyTask.everyX = 3; - // let threeWeeksFromTodayPlusOne = day.add(1, 'day').add(3, 'weeks').toDate(); - // - // expect(shouldDo(threeWeeksFromTodayPlusOne, dailyTask, options)).to.equal(false); - // }); - // - // it('activates Daily on matching week', () => { - // dailyTask.everyX = 3; - // let threeWeeksFromToday = moment().add(3, 'weeks').toDate(); - // - // expect(shouldDo(threeWeeksFromToday, dailyTask, options)).to.equal(true); - // }); - // - // it('activates Daily on every (x) week on weekday', () => { - // dailyTask.repeat = { - // su: false, - // s: false, - // f: false, - // th: false, - // w: false, - // t: false, - // m: false, - // }; - // - // day = moment(); - // dailyTask.repeat[DAY_MAPPING[day.day()]] = true; - // dailyTask.everyX = 3; - // let threeWeeksFromToday = day.add(6, 'weeks').day(day.day()).toDate(); - // - // expect(shouldDo(threeWeeksFromToday, dailyTask, options)).to.equal(true); - // }); - // }); - // - // context('Monthly - Every X Months on a specified date', () => { - // it('leaves daily inactive if not day of the month', () => { - // dailyTask.everyX = 1; - // dailyTask.frequency = 'monthly'; - // dailyTask.daysOfMonth = [15]; - // let tomorrow = moment().add(1, 'day').toDate();// @TODO: make sure this is not the 15 - // - // expect(shouldDo(tomorrow, dailyTask, options)).to.equal(false); - // }); - // - // it('activates Daily on matching day of month', () => { - // day = moment(); - // dailyTask.everyX = 1; - // dailyTask.frequency = 'monthly'; - // dailyTask.daysOfMonth = [day.date()]; - // day = day.add(1, 'months').date(day.date()).toDate(); - // - // expect(shouldDo(day, dailyTask, options)).to.equal(true); - // }); - // - // it('leaves daily inactive if not on date of the x month', () => { - // dailyTask.everyX = 2; - // dailyTask.frequency = 'monthly'; - // dailyTask.daysOfMonth = [15]; - // let tomorrow = moment().add(2, 'months').add(1, 'day').toDate(); - // - // expect(shouldDo(tomorrow, dailyTask, options)).to.equal(false); - // }); - // - // it('activates Daily if on date of the x month', () => { - // dailyTask.everyX = 2; - // dailyTask.frequency = 'monthly'; - // dailyTask.daysOfMonth = [15]; - // day = moment().add(2, 'months').date(15).toDate(); - // expect(shouldDo(day, dailyTask, options)).to.equal(true); - // }); - // }); - // - // context('Monthly - Certain days of the nth Week', () => { - // it('leaves daily inactive if not the correct week of the month on the day of the start date', () => { - // dailyTask.repeat = { - // su: false, - // s: false, - // f: false, - // th: false, - // w: false, - // t: false, - // m: false, - // }; - // - // let today = moment('01/27/2017'); - // let week = today.monthWeek(); - // let dayOfWeek = today.day(); - // dailyTask.startDate = today.toDate(); - // dailyTask.weeksOfMonth = [week]; - // dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true; - // dailyTask.everyX = 1; - // dailyTask.frequency = 'monthly'; - // day = moment('02/23/2017'); - // - // expect(shouldDo(day, dailyTask, options)).to.equal(false); - // }); - // - // it('activates Daily if correct week of the month on the day of the start date', () => { - // dailyTask.repeat = { - // su: false, - // s: false, - // f: false, - // th: false, - // w: false, - // t: false, - // m: false, - // }; - // - // let today = moment('01/27/2017'); - // let week = today.monthWeek(); - // let dayOfWeek = today.day(); - // dailyTask.startDate = today.toDate(); - // dailyTask.weeksOfMonth = [week]; - // dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true; - // dailyTask.everyX = 1; - // dailyTask.frequency = 'monthly'; - // day = moment('02/24/2017'); - // - // expect(shouldDo(day, dailyTask, options)).to.equal(true); - // }); - // - // it('leaves daily inactive if not day of the month with every x month on weekday', () => { - // dailyTask.repeat = { - // su: false, - // s: false, - // f: false, - // th: false, - // w: false, - // t: false, - // m: false, - // }; - // - // let today = moment('01/26/2017'); - // let week = today.monthWeek(); - // let dayOfWeek = today.day(); - // dailyTask.startDate = today.toDate(); - // dailyTask.weeksOfMonth = [week]; - // dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true; - // dailyTask.everyX = 2; - // dailyTask.frequency = 'monthly'; - // - // day = moment('03/24/2017'); - // - // expect(shouldDo(day, dailyTask, options)).to.equal(false); - // }); - // - // it('activates Daily if on nth weekday of the x month', () => { - // dailyTask.repeat = { - // su: false, - // s: false, - // f: false, - // th: false, - // w: false, - // t: false, - // m: false, - // }; - // - // let today = moment('01/27/2017'); - // let week = today.monthWeek(); - // let dayOfWeek = today.day(); - // dailyTask.startDate = today.toDate(); - // dailyTask.weeksOfMonth = [week]; - // dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true; - // dailyTask.everyX = 2; - // dailyTask.frequency = 'monthly'; - // - // day = moment('03/24/2017'); - // - // expect(shouldDo(day, dailyTask, options)).to.equal(true); - // }); - // }); - // - // context('Every X Years', () => { - // it('leaves daily inactive if not the correct year', () => { - // day = moment(); - // dailyTask.everyX = 2; - // dailyTask.frequency = 'yearly'; - // day = day.add(1, 'day').toDate(); - // - // expect(shouldDo(day, dailyTask, options)).to.equal(false); - // }); - // - // it('activates Daily on matching year', () => { - // day = moment(); - // dailyTask.everyX = 2; - // dailyTask.frequency = 'yearly'; - // day = day.add(2, 'years').toDate(); - // - // expect(shouldDo(day, dailyTask, options)).to.equal(true); - // }); - // }); + context('Every X Weeks', () => { + it('leaves daily inactive if it has not been the specified number of weeks', () => { + dailyTask.everyX = 3; + let tomorrow = moment().add(1, 'day').toDate(); + + expect(shouldDo(tomorrow, dailyTask, options)).to.equal(false); + }); + + it('leaves daily inactive if on every (x) week on weekday it is incorrect weekday', () => { + dailyTask.repeat = { + su: false, + s: false, + f: false, + th: false, + w: false, + t: false, + m: false, + }; + + day = moment(); + dailyTask.repeat[DAY_MAPPING[day.day()]] = true; + dailyTask.everyX = 3; + let threeWeeksFromTodayPlusOne = day.add(1, 'day').add(3, 'weeks').toDate(); + + expect(shouldDo(threeWeeksFromTodayPlusOne, dailyTask, options)).to.equal(false); + }); + + it('activates Daily on matching week', () => { + dailyTask.everyX = 3; + let threeWeeksFromToday = moment().add(3, 'weeks').toDate(); + + expect(shouldDo(threeWeeksFromToday, dailyTask, options)).to.equal(true); + }); + + it('activates Daily on every (x) week on weekday', () => { + dailyTask.repeat = { + su: false, + s: false, + f: false, + th: false, + w: false, + t: false, + m: false, + }; + + day = moment(); + dailyTask.repeat[DAY_MAPPING[day.day()]] = true; + dailyTask.everyX = 3; + let threeWeeksFromToday = day.add(6, 'weeks').day(day.day()).toDate(); + + expect(shouldDo(threeWeeksFromToday, dailyTask, options)).to.equal(true); + }); + }); + + context('Monthly - Every X Months on a specified date', () => { + it('leaves daily inactive if not day of the month', () => { + dailyTask.everyX = 1; + dailyTask.frequency = 'monthly'; + dailyTask.daysOfMonth = [15]; + let tomorrow = moment().add(1, 'day').toDate();// @TODO: make sure this is not the 15 + + expect(shouldDo(tomorrow, dailyTask, options)).to.equal(false); + }); + + it('activates Daily on matching day of month', () => { + day = moment(); + dailyTask.everyX = 1; + dailyTask.frequency = 'monthly'; + dailyTask.daysOfMonth = [day.date()]; + day = day.add(1, 'months').date(day.date()).toDate(); + + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + it('leaves daily inactive if not on date of the x month', () => { + dailyTask.everyX = 2; + dailyTask.frequency = 'monthly'; + dailyTask.daysOfMonth = [15]; + let tomorrow = moment().add(2, 'months').add(1, 'day').toDate(); + + expect(shouldDo(tomorrow, dailyTask, options)).to.equal(false); + }); + + it('activates Daily if on date of the x month', () => { + dailyTask.everyX = 2; + dailyTask.frequency = 'monthly'; + dailyTask.daysOfMonth = [15]; + day = moment().add(2, 'months').date(15).toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + }); + + context('Monthly - Certain days of the nth Week', () => { + it('leaves daily inactive if not the correct week of the month on the day of the start date', () => { + dailyTask.repeat = { + su: false, + s: false, + f: false, + th: false, + w: false, + t: false, + m: false, + }; + + let today = moment('2017-01-27'); + let week = today.monthWeek(); + let dayOfWeek = today.day(); + dailyTask.startDate = today.toDate(); + dailyTask.weeksOfMonth = [week]; + dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true; + dailyTask.everyX = 1; + dailyTask.frequency = 'monthly'; + day = moment('2017-02-23'); + + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + + it('activates Daily if correct week of the month on the day of the start date', () => { + dailyTask.repeat = { + su: false, + s: false, + f: false, + th: false, + w: false, + t: false, + m: false, + }; + + let today = moment('2017-01-27'); + let week = today.monthWeek(); + let dayOfWeek = today.day(); + dailyTask.startDate = today.toDate(); + dailyTask.weeksOfMonth = [week]; + dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true; + dailyTask.everyX = 1; + dailyTask.frequency = 'monthly'; + day = moment('2017-02-24'); + + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + it('leaves daily inactive if not day of the month with every x month on weekday', () => { + dailyTask.repeat = { + su: false, + s: false, + f: false, + th: false, + w: false, + t: false, + m: false, + }; + + let today = moment('2017-01-26'); + let week = today.monthWeek(); + let dayOfWeek = today.day(); + dailyTask.startDate = today.toDate(); + dailyTask.weeksOfMonth = [week]; + dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true; + dailyTask.everyX = 2; + dailyTask.frequency = 'monthly'; + + day = moment('2017-03-24'); + + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + + it('activates Daily if on nth weekday of the x month', () => { + dailyTask.repeat = { + su: false, + s: false, + f: false, + th: false, + w: false, + t: false, + m: false, + }; + + let today = moment('2017-01-27'); + let week = today.monthWeek(); + let dayOfWeek = today.day(); + dailyTask.startDate = today.toDate(); + dailyTask.weeksOfMonth = [week]; + dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true; + dailyTask.everyX = 2; + dailyTask.frequency = 'monthly'; + + day = moment('2017-03-24'); + + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + }); + + context('Every X Years', () => { + it('leaves daily inactive if not the correct year', () => { + day = moment(); + dailyTask.everyX = 2; + dailyTask.frequency = 'yearly'; + day = day.add(1, 'day').toDate(); + + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + + it('activates Daily on matching year', () => { + day = moment(); + dailyTask.everyX = 2; + dailyTask.frequency = 'yearly'; + day = day.add(2, 'years').toDate(); + + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + }); }); From 76222ac344077777ec102f7d9bec3142b4685313 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Mon, 8 May 2017 10:20:47 -0600 Subject: [PATCH 05/26] Added custom day start support --- test/common/shouldDo.test.js | 267 +++++++++++++++++++++++++++++++--- website/common/script/cron.js | 4 +- 2 files changed, 249 insertions(+), 22 deletions(-) diff --git a/test/common/shouldDo.test.js b/test/common/shouldDo.test.js index d11a959ca1..e640dce3d7 100644 --- a/test/common/shouldDo.test.js +++ b/test/common/shouldDo.test.js @@ -75,14 +75,14 @@ describe('shouldDo', () => { expect(shouldDo(day, dailyTask, options)).to.equal(true); }); - it('returns true if the user\'s current time is after start date and CDS', () => { + it('returns true if the user\'s current time is after start date and Custom Day Start', () => { options.dayStart = 4; day = moment().utcOffset(options.timezoneOffset).startOf('day').add(6, 'hours').toDate(); dailyTask.startDate = moment().utcOffset(options.timezoneOffset).subtract(1, 'day').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); - it('returns false if the user\'s current time is before CDS', () => { + it('returns false if the user\'s current time is before Custom Day Start', () => { options.dayStart = 8; day = moment().utcOffset(options.timezoneOffset).startOf('day').add(2, 'hours').toDate(); dailyTask.startDate = moment().utcOffset(options.timezoneOffset).startOf('day').toDate(); @@ -105,13 +105,13 @@ describe('shouldDo', () => { expect(shouldDo(day, dailyTask, options)).to.equal(true); }); - it('returns true if the user\'s current time is after CDS', () => { + it('returns true if the user\'s current time is after Custom Day Start', () => { options.dayStart = 4; day = moment().utcOffset(options.timezoneOffset).startOf('day').add(6, 'hours').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); - it('returns false if the user\'s current time is before CDS', () => { + it('returns false if the user\'s current time is before Custom Day Start', () => { options.dayStart = 8; day = moment().utcOffset(options.timezoneOffset).startOf('day').add(2, 'hours').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(false); @@ -119,7 +119,7 @@ describe('shouldDo', () => { }); }); - context('CDS variations', () => { + context('Custom Day Start variations', () => { beforeEach(() => { // Daily is due every 2 days, and start today dailyTask.frequency = 'daily'; @@ -127,7 +127,7 @@ describe('shouldDo', () => { dailyTask.startDate = new Date(); }); - context('CDS is midnight (Default dayStart=0)', () => { + context('Custom Day Start is midnight (Default dayStart=0)', () => { beforeEach(() => { options.dayStart = 0; }); @@ -159,7 +159,7 @@ describe('shouldDo', () => { }); }); - context('CDS is 0 <= n < 24', () => { + context('Custom Day Start is 0 <= n < 24', () => { beforeEach(() => { options.dayStart = 7; }); @@ -172,24 +172,24 @@ describe('shouldDo', () => { }); context('Current Date is today', () => { - it('returns false if current hour is before CDS', () => { + it('returns false if current hour is before Custom Day Start', () => { day = moment(day).startOf('day').add(1, 'hours').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(false); }); - it('returns true if current hour is after CDS', () => { + it('returns true if current hour is after Custom Day Start', () => { day = moment(day).startOf('day').add(9, 'hours').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); }); context('Current Date is tomorrow', () => { - it('returns true if current hour is before CDS', () => { + it('returns true if current hour is before Custom Day Start', () => { day = moment(day).endOf('day').add(1, 'hours').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); - it('returns false if current hour is after CDS', () => { + it('returns false if current hour is after Custom Day Start', () => { day = moment(day).endOf('day').add(9, 'hours').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(false); }); @@ -222,7 +222,7 @@ describe('shouldDo', () => { }); context('On multiples of x', () => { - it('returns true when CDS is midnight', () => { + it('returns true when Custom Day Start is midnight', () => { dailyTask.startDate = moment().subtract(7, 'days').toDate(); dailyTask.everyX = 7; @@ -235,7 +235,7 @@ describe('shouldDo', () => { expect(shouldDo(day, dailyTask, options)).to.equal(true); }); - it('returns true when current time is after CDS', () => { + it('returns true when current time is after Custom Day Start', () => { dailyTask.startDate = moment().subtract(5, 'days').toDate(); dailyTask.everyX = 5; @@ -245,7 +245,7 @@ describe('shouldDo', () => { expect(shouldDo(day, dailyTask, options)).to.equal(true); }); - it('returns false when current time is before CDS', () => { + it('returns false when current time is before Custom Day Start', () => { dailyTask.startDate = moment().subtract(5, 'days').toDate(); dailyTask.everyX = 5; @@ -378,7 +378,7 @@ describe('shouldDo', () => { dailyTask.repeat[weekdayMap[currentWeekday]] = true; }); - context('CDS is midnight (Default dayStart=0)', () => { + context('Custom Day Start is midnight (Default dayStart=0)', () => { beforeEach(() => { options.dayStart = 0; }); @@ -410,7 +410,7 @@ describe('shouldDo', () => { }); }); - context('CDS is 0 <= n < 24', () => { + context('Custom Day Start is 0 <= n < 24', () => { beforeEach(() => { options.dayStart = 7; }); @@ -423,24 +423,24 @@ describe('shouldDo', () => { }); context('Current Date is on the matching day', () => { - it('returns false if current hour is before CDS', () => { + it('returns false if current hour is before Custom Day Start', () => { day = moment(day).startOf('day').add(1, 'hours').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(false); }); - it('returns true if current hour is after CDS', () => { + it('returns true if current hour is after Custom Day Start', () => { day = moment(day).startOf('day').add(9, 'hours').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); }); context('Current Date is one day after the matching day', () => { - it('returns true if current hour is before CDS', () => { + it('returns true if current hour is before Custom Day Start', () => { day = moment(day).endOf('day').add(1, 'hours').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); - it('returns false if current hour is after CDS', () => { + it('returns false if current hour is after Custom Day Start', () => { day = moment(day).endOf('day').add(9, 'hours').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(false); }); @@ -533,6 +533,53 @@ describe('shouldDo', () => { expect(shouldDo(threeWeeksFromToday, dailyTask, options)).to.equal(true); }); + + it('activates Daily on start date', () => { + dailyTask.everyX = 3; + + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + context('Custom Day Start is 0 <= n < 24', () => { + let threeWeeksFromToday; + + beforeEach(() => { + options.dayStart = 7; + dailyTask.everyX = 3; + threeWeeksFromToday = moment().add(3, 'weeks').toDate(); + }); + + context('Current Date is one day before the matching day', () => { + it('should not be due', () => { + day = moment(threeWeeksFromToday).subtract(1, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); + + context('Current Date is on the matching day', () => { + it('returns false if current hour is before Custom Day Start', () => { + day = moment(threeWeeksFromToday).startOf('day').add(1, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + + it('returns true if current hour is after Custom Day Start', () => { + day = moment(threeWeeksFromToday).startOf('day').add(9, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + }); + + context('Current Date is one day after the matching day', () => { + it('returns true if current hour is before Custom Day Start', () => { + day = moment(threeWeeksFromToday).endOf('day').add(1, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + it('returns false if current hour is after Custom Day Start', () => { + day = moment(threeWeeksFromToday).endOf('day').add(9, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); + }); }); context('Monthly - Every X Months on a specified date', () => { @@ -571,6 +618,55 @@ describe('shouldDo', () => { day = moment().add(2, 'months').date(15).toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); + + it('activates Daily on start date', () => { + dailyTask.everyX = 2; + dailyTask.frequency = 'monthly'; + dailyTask.daysOfMonth = [15]; + day = moment().date(15).toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + context('Custom Day Start is 0 <= n < 24', () => { + beforeEach(() => { + options.dayStart = 7; + dailyTask.everyX = 2; + dailyTask.frequency = 'monthly'; + dailyTask.daysOfMonth = [15]; + day = moment().add(2, 'months').date(15).toDate(); + }); + + context('Current Date is one day before the matching day', () => { + it('should not be due', () => { + day = moment(day).subtract(1, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); + + context('Current Date is on the matching day', () => { + it('returns false if current hour is before Custom Day Start', () => { + day = moment(day).startOf('day').add(1, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + + it('returns true if current hour is after Custom Day Start', () => { + day = moment(day).startOf('day').add(9, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + }); + + context('Current Date is one day after the matching day', () => { + it('returns true if current hour is before Custom Day Start', () => { + day = moment(day).endOf('day').add(1, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + it('returns false if current hour is after Custom Day Start', () => { + day = moment(day).endOf('day').add(9, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); + }); }); context('Monthly - Certain days of the nth Week', () => { @@ -671,6 +767,87 @@ describe('shouldDo', () => { expect(shouldDo(day, dailyTask, options)).to.equal(true); }); + + it('activates Daily on start date', () => { + dailyTask.repeat = { + su: false, + s: false, + f: false, + th: false, + w: false, + t: false, + m: false, + }; + + let today = moment('2017-01-27'); + let week = today.monthWeek(); + let dayOfWeek = today.day(); + dailyTask.startDate = today.toDate(); + dailyTask.weeksOfMonth = [week]; + dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true; + dailyTask.everyX = 2; + dailyTask.frequency = 'monthly'; + + day = moment('2017-03-24'); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + context('Custom Day Start is 0 <= n < 24', () => { + beforeEach(() => { + options.dayStart = 7; + dailyTask.repeat = { + su: false, + s: false, + f: false, + th: false, + w: false, + t: false, + m: false, + }; + + let today = moment('2017-01-27'); + let week = today.monthWeek(); + let dayOfWeek = today.day(); + dailyTask.startDate = today.toDate(); + dailyTask.weeksOfMonth = [week]; + dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true; + dailyTask.everyX = 2; + dailyTask.frequency = 'monthly'; + + day = moment('2017-03-24'); + }); + + context('Current Date is one day before the matching day', () => { + it('should not be due', () => { + day = moment(day).subtract(1, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); + + context('Current Date is on the matching day', () => { + it('returns false if current hour is before Custom Day Start', () => { + day = moment(day).startOf('day').add(1, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + + it('returns true if current hour is after Custom Day Start', () => { + day = moment(day).startOf('day').add(9, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + }); + + context('Current Date is one day after the matching day', () => { + it('returns true if current hour is before Custom Day Start', () => { + day = moment(day).endOf('day').add(1, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + it('returns false if current hour is after Custom Day Start', () => { + day = moment(day).endOf('day').add(9, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); + }); }); context('Every X Years', () => { @@ -691,5 +868,55 @@ describe('shouldDo', () => { expect(shouldDo(day, dailyTask, options)).to.equal(true); }); + + it('activates Daily on start date', () => { + day = moment(); + dailyTask.everyX = 2; + dailyTask.frequency = 'yearly'; + day = day.add(2, 'years').toDate(); + + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + context('Custom Day Start is 0 <= n < 24', () => { + beforeEach(() => { + options.dayStart = 7; + day = moment(); + dailyTask.everyX = 2; + dailyTask.frequency = 'yearly'; + day = day.add(2, 'years').toDate(); + }); + + context('Current Date is one day before the matching day', () => { + it('should not be due', () => { + day = moment(day).subtract(1, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); + + context('Current Date is on the matching day', () => { + it('returns false if current hour is before Custom Day Start', () => { + day = moment(day).startOf('day').add(1, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + + it('returns true if current hour is after Custom Day Start', () => { + day = moment(day).startOf('day').add(9, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + }); + + context('Current Date is one day after the matching day', () => { + it('returns true if current hour is before Custom Day Start', () => { + day = moment(day).endOf('day').add(1, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + it('returns false if current hour is after Custom Day Start', () => { + day = moment(day).endOf('day').add(9, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); + }); }); }); diff --git a/website/common/script/cron.js b/website/common/script/cron.js index b667632e74..c07e666589 100644 --- a/website/common/script/cron.js +++ b/website/common/script/cron.js @@ -143,13 +143,13 @@ export function shouldDo (day, dailyTask, options = {}) { schedule = schedule.every(dailyTask.daysOfMonth).daysOfMonth(); } - return schedule.matches(day) && matchEveryX; + return schedule.matches(startOfDayWithCDSTime) && matchEveryX; } else if (dailyTask.frequency === 'yearly') { let schedule = moment(startDate).recur(); schedule = schedule.every(dailyTask.everyX).years(); - return schedule.matches(day); + return schedule.matches(startOfDayWithCDSTime); } return false; From 59fb32ea2e125bdbe2ed170371ffdeacf2fade48 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Wed, 10 May 2017 15:47:32 -0600 Subject: [PATCH 06/26] Moved back to zone function --- website/common/script/cron.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/common/script/cron.js b/website/common/script/cron.js index c07e666589..168a8cfe27 100644 --- a/website/common/script/cron.js +++ b/website/common/script/cron.js @@ -32,7 +32,7 @@ function sanitizeOptions (o) { let dayStart = !Number.isNaN(ref) && ref >= 0 && ref <= 24 ? ref : 0; let timezoneOffset; - let timezoneOffsetDefault = Number(moment().utcOffset()); + let timezoneOffsetDefault = Number(moment().zone()); if (isFinite(o.timezoneOffsetOverride)) { timezoneOffset = Number(o.timezoneOffsetOverride); } else if (Number.isFinite(o.timezoneOffset)) { @@ -45,7 +45,7 @@ function sanitizeOptions (o) { timezoneOffset = timezoneOffsetDefault; } - let now = o.now ? moment(o.now).utcOffset(timezoneOffset) : moment().utcOffset(timezoneOffset); + let now = o.now ? moment(o.now).zone(timezoneOffset) : moment().zone(timezoneOffset); // return a new object, we don't want to add "now" to user object return { @@ -101,7 +101,7 @@ export function shouldDo (day, dailyTask, options = {}) { // The time portion of the Start Date is never visible to or modifiable by the user so we must ignore it. // Therefore, we must also ignore the time portion of the user's day start (startOfDayWithCDSTime), otherwise the date comparison will be wrong for some times. // NB: The user's day start date has already been converted to the PREVIOUS day's date if the time portion was before CDS. - let startDate = moment(dailyTask.startDate).utcOffset(o.timezoneOffset).startOf('day'); + let startDate = moment(dailyTask.startDate).zone(o.timezoneOffset).startOf('day'); if (startDate > startOfDayWithCDSTime.startOf('day')) { return false; // Daily starts in the future From e7418472f67f12517c1132abbd0787f76c08adee Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Wed, 10 May 2017 22:06:31 -0600 Subject: [PATCH 07/26] Added zone back --- test/common/shouldDo.test.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/test/common/shouldDo.test.js b/test/common/shouldDo.test.js index 99c31ddf51..62571b2bde 100644 --- a/test/common/shouldDo.test.js +++ b/test/common/shouldDo.test.js @@ -24,6 +24,7 @@ describe('shouldDo', () => { }, startDate: new Date(), }; + options = {}; }); it('returns false if task type is not a daily', () => { @@ -39,10 +40,6 @@ describe('shouldDo', () => { }); context('Timezone variations', () => { - beforeEach(() => { - dailyTask.frequency = 'daily'; - }); - context('User timezone is UTC', () => { beforeEach(() => { options.timezoneOffset = 0; @@ -57,6 +54,11 @@ describe('shouldDo', () => { dailyTask.startDate = moment().toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); + + it('returns false if Start Date is after today', () => { + dailyTask.startDate = moment().add(1, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); }); context('User timezone is between UTC-12 and UTC (0~720)', () => { @@ -76,15 +78,15 @@ describe('shouldDo', () => { it('returns true if the user\'s current time is after start date and Custom Day Start', () => { options.dayStart = 4; - day = moment().utcOffset(options.timezoneOffset).startOf('day').add(6, 'hours').toDate(); - dailyTask.startDate = moment().utcOffset(options.timezoneOffset).subtract(1, 'day').toDate(); + day = moment().zone(options.timezoneOffset).startOf('day').add(6, 'hours').toDate(); + dailyTask.startDate = moment().zone(options.timezoneOffset).startOf('day').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); it('returns false if the user\'s current time is before Custom Day Start', () => { options.dayStart = 8; - day = moment().utcOffset(options.timezoneOffset).startOf('day').add(2, 'hours').toDate(); - dailyTask.startDate = moment().utcOffset(options.timezoneOffset).startOf('day').toDate(); + day = moment().zone(options.timezoneOffset).startOf('day').add(2, 'hours').toDate(); + dailyTask.startDate = moment().zone(options.timezoneOffset).startOf('day').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(false); }); }); @@ -106,13 +108,13 @@ describe('shouldDo', () => { it('returns true if the user\'s current time is after Custom Day Start', () => { options.dayStart = 4; - day = moment().utcOffset(options.timezoneOffset).startOf('day').add(6, 'hours').toDate(); + day = moment().zone(options.timezoneOffset).startOf('day').add(6, 'hours').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); it('returns false if the user\'s current time is before Custom Day Start', () => { options.dayStart = 8; - day = moment().utcOffset(options.timezoneOffset).startOf('day').add(2, 'hours').toDate(); + day = moment().zone(options.timezoneOffset).startOf('day').add(2, 'hours').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(false); }); }); @@ -278,9 +280,6 @@ describe('shouldDo', () => { day = moment().subtract(3, 'days').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(false); }); - - day = moment(day).add(7, 'days'); - expect(shouldDo(day, dailyTask, options)).to.equal(true); }); }); From 1292f9a3d5c60a41d73db52be4bc05effbb1085a Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Thu, 11 May 2017 13:11:16 -0600 Subject: [PATCH 08/26] Added nextDue field --- .../tasks/POST-tasks_id_score_direction.test.js | 7 +++++++ .../integration/tasks/POST-tasks_user.test.js | 1 + .../v3/integration/tasks/PUT-tasks_id.test.js | 1 + test/api/v3/unit/libs/cron.test.js | 8 ++++++++ website/common/script/cron.js | 11 ++++++++++- website/server/controllers/api-v3/tasks.js | 17 +++++++++++++++-- website/server/libs/cron.js | 11 ++++++++++- website/server/libs/taskManager.js | 11 ++++++++++- website/server/models/task.js | 1 + 9 files changed, 63 insertions(+), 5 deletions(-) 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 0918780bae..3f6965e4f6 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 @@ -215,6 +215,13 @@ describe('POST /tasks/:id/score/:direction', () => { expect(task.isDue).to.equal(true); }); + it('computes nextDue', async () => { + await user.post(`/tasks/${daily._id}/score/up`); + let task = await user.get(`/tasks/${daily._id}`); + + expect(task.nextDue.length).to.eql(3); + }); + it('scores up daily even if it is already completed'); // Yes? it('scores down daily even if it is already uncompleted'); // Yes? 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 c1385ea93e..cb11a9f672 100644 --- a/test/api/v3/integration/tasks/POST-tasks_user.test.js +++ b/test/api/v3/integration/tasks/POST-tasks_user.test.js @@ -510,6 +510,7 @@ describe('POST /tasks/user', () => { expect(task.weeksOfMonth).to.eql([3]); expect(new Date(task.startDate)).to.eql(now); expect(task.isDue).to.be.true; + expect(task.nextDue.length).to.eql(3); }); it('creates multiple dailys', 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 ce17b9c94b..82c7dcaa75 100644 --- a/test/api/v3/integration/tasks/PUT-tasks_id.test.js +++ b/test/api/v3/integration/tasks/PUT-tasks_id.test.js @@ -404,6 +404,7 @@ describe('PUT /tasks/:id', () => { expect(savedDaily.frequency).to.eql('daily'); expect(savedDaily.everyX).to.eql(5); expect(savedDaily.isDue).to.be.false; + expect(savedDaily.nextDue.length).to.eql(3); }); it('can update checklists (replace it)', async () => { diff --git a/test/api/v3/unit/libs/cron.test.js b/test/api/v3/unit/libs/cron.test.js index 93d164f1fd..18754398ae 100644 --- a/test/api/v3/unit/libs/cron.test.js +++ b/test/api/v3/unit/libs/cron.test.js @@ -366,6 +366,14 @@ describe('cron', () => { expect(tasksByType.dailys[0].isDue).to.be.false; }); + it('computes nextDue', () => { + tasksByType.dailys[0].frequency = 'daily'; + tasksByType.dailys[0].everyX = 5; + tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate(); + cron({user, tasksByType, daysMissed, analytics}); + expect(tasksByType.dailys[0].nextDue.length).to.eql(3); + }); + it('should add history', () => { cron({user, tasksByType, daysMissed, analytics}); expect(tasksByType.dailys[0].history).to.be.lengthOf(1); diff --git a/website/common/script/cron.js b/website/common/script/cron.js index 6e521d233c..ba632b8c81 100644 --- a/website/common/script/cron.js +++ b/website/common/script/cron.js @@ -105,7 +105,7 @@ export function shouldDo (day, dailyTask, options = {}) { let startDate = moment(dailyTask.startDate).zone(o.timezoneOffset).startOf('day'); - if (startDate > startOfDayWithCDSTime.startOf('day')) { + if (startDate > startOfDayWithCDSTime.startOf('day') && !options.nextDue) { return false; // Daily starts in the future } @@ -121,6 +121,9 @@ export function shouldDo (day, dailyTask, options = {}) { if (!dailyTask.everyX) return false; // error condition let schedule = moment(startDate).recur() .every(dailyTask.everyX).days(); + + if (options.nextDue) return schedule.next(3, 'L'); + return schedule.matches(startOfDayWithCDSTime); } else if (dailyTask.frequency === 'weekly') { let schedule = moment(startDate).recur(); @@ -131,6 +134,8 @@ export function shouldDo (day, dailyTask, options = {}) { schedule = schedule.every(daysOfTheWeek).daysOfWeek(); + if (options.nextDue) return schedule.next(3, 'L'); + return schedule.matches(startOfDayWithCDSTime); } else if (dailyTask.frequency === 'monthly') { let schedule = moment(startDate).recur(); @@ -145,12 +150,16 @@ export function shouldDo (day, dailyTask, options = {}) { schedule = schedule.every(dailyTask.daysOfMonth).daysOfMonth(); } + if (options.nextDue) return schedule.next(3, 'L'); + return schedule.matches(startOfDayWithCDSTime) && matchEveryX; } else if (dailyTask.frequency === 'yearly') { let schedule = moment(startDate).recur(); schedule = schedule.every(dailyTask.everyX).years(); + if (options.nextDue) return schedule.next(3, 'L'); + return schedule.matches(startOfDayWithCDSTime); } diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 401a432e24..f150f14b8d 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -21,6 +21,7 @@ import { import common from '../../../common'; import Bluebird from 'bluebird'; import _ from 'lodash'; +import cloneDeep from 'lodash/cloneDeep'; import logger from '../../libs/logger'; const MAX_SCORE_NOTES_LENGTH = 256; @@ -457,7 +458,13 @@ api.updateTask = { } if (sanitizedObj.type === 'daily') { - task.isDue = common.shouldDo(Date.now(), sanitizedObj, user.preferences); + let optionsForShouldDo = cloneDeep(user.preferences.toObject()); + task.isDue = common.shouldDo(Date.now(), sanitizedObj, optionsForShouldDo); + optionsForShouldDo.nextDue = true; + let nextDue = common.shouldDo(Date.now(), sanitizedObj, optionsForShouldDo); + if (nextDue && nextDue.length > 0) { + task.nextDue = nextDue; + } } let savedTask = await task.save(); @@ -590,7 +597,13 @@ api.scoreTask = { } if (task.type === 'daily') { - task.isDue = common.shouldDo(Date.now(), task, user.preferences); + let optionsForShouldDo = cloneDeep(user.preferences.toObject()); + task.isDue = common.shouldDo(Date.now(), task, optionsForShouldDo); + optionsForShouldDo.nextDue = true; + let nextDue = common.shouldDo(Date.now(), task, optionsForShouldDo); + if (nextDue && nextDue.length > 0) { + task.nextDue = nextDue; + } } let results = await Bluebird.all([ diff --git a/website/server/libs/cron.js b/website/server/libs/cron.js index 88a119c4f6..2cc319e824 100644 --- a/website/server/libs/cron.js +++ b/website/server/libs/cron.js @@ -4,6 +4,7 @@ import { model as User } from '../models/user'; import common from '../../common/'; import { preenUserHistory } from '../libs/preening'; import _ from 'lodash'; +import cloneDeep from 'lodash/cloneDeep'; import nconf from 'nconf'; const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true'; @@ -314,7 +315,15 @@ export function cron (options = {}) { value: task.value, }); task.completed = false; - task.isDue = common.shouldDo(Date.now(), task, user.preferences); + + let optionsForShouldDo = cloneDeep(user.preferences.toObject()); + task.isDue = common.shouldDo(now, task, optionsForShouldDo); + optionsForShouldDo.nextDue = true; + let nextDue = common.shouldDo(now, task, optionsForShouldDo); + + if (nextDue && nextDue.length > 0) { + task.nextDue = nextDue; + } if (completed || scheduleMisses > 0) { if (task.checklist) { diff --git a/website/server/libs/taskManager.js b/website/server/libs/taskManager.js index 5c2de7d5c4..8676b001b9 100644 --- a/website/server/libs/taskManager.js +++ b/website/server/libs/taskManager.js @@ -4,6 +4,7 @@ import { } from './errors'; import Bluebird from 'bluebird'; import _ from 'lodash'; +import cloneDeep from 'lodash/cloneDeep'; import shared from '../../common'; async function _validateTaskAlias (tasks, res) { @@ -64,7 +65,15 @@ export async function createTasks (req, res, options = {}) { newTask.userId = user._id; } - if (newTask.type === 'daily') newTask.isDue = shared.shouldDo(Date.now(), newTask, user.preferences); + if (newTask.type === 'daily') { + let optionsForShouldDo = cloneDeep(user.preferences.toObject()); + newTask.isDue = shared.shouldDo(Date.now(), newTask, optionsForShouldDo); + optionsForShouldDo.nextDue = true; + let nextDue = shared.shouldDo(Date.now(), newTask, optionsForShouldDo); + if (nextDue && nextDue.length > 0) { + newTask.nextDue = nextDue; + } + } // Validate that the task is valid and throw if it isn't // otherwise since we're saving user/challenge/group and task in parallel it could save the user/challenge/group with a tasksOrder that doens't match reality diff --git a/website/server/models/task.js b/website/server/models/task.js index 182c174ce8..c032293dc2 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -243,6 +243,7 @@ export let DailySchema = new Schema(_.defaults({ daysOfMonth: {type: [Number], default: []}, // Days of the month that the daily should repeat on weeksOfMonth: {type: [Number], default: []}, // Weeks of the month that the daily should repeat on isDue: {type: Boolean}, + nextDue: [{type: String}], }, habitDailySchema(), dailyTodoSchema()), subDiscriminatorOptions); export let daily = Task.discriminator('daily', DailySchema); From f192ca4c6f1709c1a8a6519df98cdd0c7ae6b7a7 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Fri, 12 May 2017 07:39:32 -0600 Subject: [PATCH 09/26] Abstracted set next due logic, set offset, and mapped to ISO --- website/common/script/cron.js | 8 ++++---- website/server/controllers/api-v3/tasks.js | 22 +++------------------ website/server/libs/taskManager.js | 23 +++++++++++++--------- website/server/models/task.js | 2 +- 4 files changed, 22 insertions(+), 33 deletions(-) diff --git a/website/common/script/cron.js b/website/common/script/cron.js index ba632b8c81..b3171976c7 100644 --- a/website/common/script/cron.js +++ b/website/common/script/cron.js @@ -122,7 +122,7 @@ export function shouldDo (day, dailyTask, options = {}) { let schedule = moment(startDate).recur() .every(dailyTask.everyX).days(); - if (options.nextDue) return schedule.next(3, 'L'); + if (options.nextDue) return schedule.fromDate(startOfDayWithCDSTime).next(3); return schedule.matches(startOfDayWithCDSTime); } else if (dailyTask.frequency === 'weekly') { @@ -134,7 +134,7 @@ export function shouldDo (day, dailyTask, options = {}) { schedule = schedule.every(daysOfTheWeek).daysOfWeek(); - if (options.nextDue) return schedule.next(3, 'L'); + if (options.nextDue) return schedule.fromDate(startOfDayWithCDSTime).next(3); return schedule.matches(startOfDayWithCDSTime); } else if (dailyTask.frequency === 'monthly') { @@ -150,7 +150,7 @@ export function shouldDo (day, dailyTask, options = {}) { schedule = schedule.every(dailyTask.daysOfMonth).daysOfMonth(); } - if (options.nextDue) return schedule.next(3, 'L'); + if (options.nextDue) return schedule.fromDate(startOfDayWithCDSTime).next(3); return schedule.matches(startOfDayWithCDSTime) && matchEveryX; } else if (dailyTask.frequency === 'yearly') { @@ -158,7 +158,7 @@ export function shouldDo (day, dailyTask, options = {}) { schedule = schedule.every(dailyTask.everyX).years(); - if (options.nextDue) return schedule.next(3, 'L'); + if (options.nextDue) return schedule.fromDate(startOfDayWithCDSTime).next(3); return schedule.matches(startOfDayWithCDSTime); } diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index f150f14b8d..c3d921b757 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -17,11 +17,11 @@ import { createTasks, getTasks, moveTask, + setNextDue, } from '../../libs/taskManager'; import common from '../../../common'; import Bluebird from 'bluebird'; import _ from 'lodash'; -import cloneDeep from 'lodash/cloneDeep'; import logger from '../../libs/logger'; const MAX_SCORE_NOTES_LENGTH = 256; @@ -457,15 +457,7 @@ api.updateTask = { task.group.approval.required = true; } - if (sanitizedObj.type === 'daily') { - let optionsForShouldDo = cloneDeep(user.preferences.toObject()); - task.isDue = common.shouldDo(Date.now(), sanitizedObj, optionsForShouldDo); - optionsForShouldDo.nextDue = true; - let nextDue = common.shouldDo(Date.now(), sanitizedObj, optionsForShouldDo); - if (nextDue && nextDue.length > 0) { - task.nextDue = nextDue; - } - } + setNextDue(task, user); let savedTask = await task.save(); @@ -596,15 +588,7 @@ api.scoreTask = { } } - if (task.type === 'daily') { - let optionsForShouldDo = cloneDeep(user.preferences.toObject()); - task.isDue = common.shouldDo(Date.now(), task, optionsForShouldDo); - optionsForShouldDo.nextDue = true; - let nextDue = common.shouldDo(Date.now(), task, optionsForShouldDo); - if (nextDue && nextDue.length > 0) { - task.nextDue = nextDue; - } - } + setNextDue(task, user); let results = await Bluebird.all([ user.save(), diff --git a/website/server/libs/taskManager.js b/website/server/libs/taskManager.js index 8676b001b9..47d2811f58 100644 --- a/website/server/libs/taskManager.js +++ b/website/server/libs/taskManager.js @@ -23,6 +23,19 @@ async function _validateTaskAlias (tasks, res) { }); } +export function setNextDue (task, user) { + if (task.type !== 'daily') return; + + let optionsForShouldDo = cloneDeep(user.preferences.toObject()); + task.isDue = shared.shouldDo(Date.now(), task, optionsForShouldDo); + optionsForShouldDo.nextDue = true; + let nextDue = shared.shouldDo(Date.now(), task, optionsForShouldDo); + if (nextDue && nextDue.length > 0) { + task.nextDue = nextDue.map((dueDate) => { + return dueDate.toISOString(); + }); + } +} /** * Creates tasks for a user, challenge or group. @@ -65,15 +78,7 @@ export async function createTasks (req, res, options = {}) { newTask.userId = user._id; } - if (newTask.type === 'daily') { - let optionsForShouldDo = cloneDeep(user.preferences.toObject()); - newTask.isDue = shared.shouldDo(Date.now(), newTask, optionsForShouldDo); - optionsForShouldDo.nextDue = true; - let nextDue = shared.shouldDo(Date.now(), newTask, optionsForShouldDo); - if (nextDue && nextDue.length > 0) { - newTask.nextDue = nextDue; - } - } + setNextDue(newTask, user); // Validate that the task is valid and throw if it isn't // otherwise since we're saving user/challenge/group and task in parallel it could save the user/challenge/group with a tasksOrder that doens't match reality diff --git a/website/server/models/task.js b/website/server/models/task.js index c032293dc2..ee09612d46 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -92,7 +92,7 @@ export let TaskSchema = new Schema({ }, discriminatorOptions)); TaskSchema.plugin(baseModel, { - noSet: ['challenge', 'userId', 'completed', 'history', 'dateCompleted', '_legacyId', 'group', 'isDue'], + noSet: ['challenge', 'userId', 'completed', 'history', 'dateCompleted', '_legacyId', 'group', 'isDue', 'nextDue'], sanitizeTransform (taskObj) { if (taskObj.type && taskObj.type !== 'reward') { // value should be settable directly only for rewards delete taskObj.value; From a82b60f14454633513b741dcf8aa3f00ab805c56 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Fri, 12 May 2017 09:21:29 -0600 Subject: [PATCH 10/26] Removed extra codes --- website/common/script/cron.js | 2 +- website/server/libs/taskManager.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/common/script/cron.js b/website/common/script/cron.js index b3171976c7..7195fc815c 100644 --- a/website/common/script/cron.js +++ b/website/common/script/cron.js @@ -140,7 +140,7 @@ export function shouldDo (day, dailyTask, options = {}) { } else if (dailyTask.frequency === 'monthly') { let schedule = moment(startDate).recur(); - let differenceInMonths = moment(day).month() + 1 - moment(startDate).month() + 1; + let differenceInMonths = moment(day).month() - moment(startDate).month(); let matchEveryX = differenceInMonths % dailyTask.everyX === 0; if (dailyTask.weeksOfMonth && dailyTask.weeksOfMonth.length > 0) { diff --git a/website/server/libs/taskManager.js b/website/server/libs/taskManager.js index 47d2811f58..3b2ea69531 100644 --- a/website/server/libs/taskManager.js +++ b/website/server/libs/taskManager.js @@ -26,7 +26,7 @@ async function _validateTaskAlias (tasks, res) { export function setNextDue (task, user) { if (task.type !== 'daily') return; - let optionsForShouldDo = cloneDeep(user.preferences.toObject()); + let optionsForShouldDo = user.preferences.toObject(); task.isDue = shared.shouldDo(Date.now(), task, optionsForShouldDo); optionsForShouldDo.nextDue = true; let nextDue = shared.shouldDo(Date.now(), task, optionsForShouldDo); From f8a99bd1270d3a4a5627d8e6a967a1049b57ce7f Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Fri, 12 May 2017 11:15:09 -0600 Subject: [PATCH 11/26] Removed clone deep --- website/server/libs/taskManager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/website/server/libs/taskManager.js b/website/server/libs/taskManager.js index 3b2ea69531..8c4ecb1c79 100644 --- a/website/server/libs/taskManager.js +++ b/website/server/libs/taskManager.js @@ -4,7 +4,6 @@ import { } from './errors'; import Bluebird from 'bluebird'; import _ from 'lodash'; -import cloneDeep from 'lodash/cloneDeep'; import shared from '../../common'; async function _validateTaskAlias (tasks, res) { From 388861b50317c579c017a63d61bc49717bef66a9 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Mon, 15 May 2017 09:15:14 -0600 Subject: [PATCH 12/26] Added summary local --- website/client-old/js/services/taskServices.js | 6 +++++- website/common/locales/en/tasks.json | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/website/client-old/js/services/taskServices.js b/website/client-old/js/services/taskServices.js index 195258b3f2..e28c3d8b2c 100644 --- a/website/client-old/js/services/taskServices.js +++ b/website/client-old/js/services/taskServices.js @@ -352,7 +352,11 @@ angular.module('habitrpg') } } - var summary = 'Repeats ' + task._edit.frequency + ' every ' + task._edit.everyX + ' ' + frequencyPlural; + var summary = window.env.t('summaryStart', { + frequency: task._edit.frequency, + everyX: task._edit.everyX, + frequencyPlural: frequencyPlural, + }); if (task._edit.frequency === 'weekly') summary += ' on ' + repeatDays; diff --git a/website/common/locales/en/tasks.json b/website/common/locales/en/tasks.json index 633dc06033..faddcd2c34 100644 --- a/website/common/locales/en/tasks.json +++ b/website/common/locales/en/tasks.json @@ -167,5 +167,6 @@ "taskNotes": "Task Notes", "monthlyRepeatHelpContent": "This task will be due every X months", "yearlyRepeatHelpContent": "This task will be due every X years", - "resets": "Resets" + "resets": "Resets", + "summaryStart": "Repeats <%= frequency %> every <%= everyX %> <%= frequencyPlural %> " } From 7f2719a75c226e3894402db3b666a5e50a63adfd Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Thu, 18 May 2017 10:11:29 -0600 Subject: [PATCH 13/26] Fixed every x weekly --- website/common/script/cron.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/website/common/script/cron.js b/website/common/script/cron.js index 7195fc815c..042f89006f 100644 --- a/website/common/script/cron.js +++ b/website/common/script/cron.js @@ -128,15 +128,14 @@ export function shouldDo (day, dailyTask, options = {}) { } else if (dailyTask.frequency === 'weekly') { let schedule = moment(startDate).recur(); - if (dailyTask.everyX > 1) { - schedule = schedule.every(dailyTask.everyX).weeks(); - } + let differenceInWeeks = moment(day).week() - moment(startDate).week(); + let matchEveryX = differenceInWeeks % dailyTask.everyX === 0; schedule = schedule.every(daysOfTheWeek).daysOfWeek(); if (options.nextDue) return schedule.fromDate(startOfDayWithCDSTime).next(3); - return schedule.matches(startOfDayWithCDSTime); + return schedule.matches(startOfDayWithCDSTime) && matchEveryX; } else if (dailyTask.frequency === 'monthly') { let schedule = moment(startDate).recur(); From 1d93943458149e64a97a36fc32420f892318527c Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Thu, 18 May 2017 10:26:57 -0600 Subject: [PATCH 14/26] Prevented edit of repeats on --- website/views/shared/tasks/edit/repeatables.jade | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/views/shared/tasks/edit/repeatables.jade b/website/views/shared/tasks/edit/repeatables.jade index 54f82089ae..d385aea332 100644 --- a/website/views/shared/tasks/edit/repeatables.jade +++ b/website/views/shared/tasks/edit/repeatables.jade @@ -23,12 +23,12 @@ fieldset.option-group.advanced-option(ng-show="task.type === 'daily'") .form-group(ng-show='task._edit.frequency === "monthly"') legend.option-title=env.t('repeatsOn') label - input(type="radio", ng-model='task._edit.repeatsOn', value='dayOfMonth') + input(type="radio", ng-model='task._edit.repeatsOn', value='dayOfMonth', ng-disabled='!canEdit(task)') =env.t('dayOfMonth') label - input(type="radio", ng-model='task._edit.repeatsOn', value='dayOfWeek') + input(type="radio", ng-model='task._edit.repeatsOn', value='dayOfWeek', ng-disabled='!canEdit(task)') =env.t('dayOfWeek') .form-group legend.option-title=env.t('summary') - div {{summary}} \ No newline at end of file + div {{summary}} From a5e0e171cca1f77ca27bf07d5b3d0bdf77e119ea Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Thu, 18 May 2017 10:42:11 -0600 Subject: [PATCH 15/26] Added next due date --- website/client-old/js/services/taskServices.js | 11 ++++++++++- website/common/locales/en/tasks.json | 3 ++- website/views/shared/tasks/edit/repeatables.jade | 4 ++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/website/client-old/js/services/taskServices.js b/website/client-old/js/services/taskServices.js index e28c3d8b2c..0b7cc9b952 100644 --- a/website/client-old/js/services/taskServices.js +++ b/website/client-old/js/services/taskServices.js @@ -294,6 +294,7 @@ angular.module('habitrpg') $scope.$watch('task._edit', function (newValue, oldValue) { if ($scope.task.type !== 'daily' || !task._edit) return; $scope.summary = generateSummary($scope.task); + $scope.nextDue = generateNextDue($scope.task); $scope.repeatSuffix = generateRepeatSuffix($scope.task); if ($scope.task._edit.repeatsOn == 'dayOfMonth') { @@ -385,9 +386,17 @@ angular.module('habitrpg') } else if (task._edit.frequency === 'yearly') { return task._edit.everyX == 1 ? window.env.t('year') : window.env.t('years'); } - }; + function generateNextDue (task) { + if (!task.nextDue) return; + let nextDue = task.nextDue.map((date) => { + let dateObject = new Date(date); + return [dateObject.getFullYear(), dateObject.getMonth() + 1, dateObject.getDate()].join('-'); + }) + return nextDue.join(', '); + } + function cancelTaskEdit(task) { task._edit = undefined; task._editing = false; diff --git a/website/common/locales/en/tasks.json b/website/common/locales/en/tasks.json index faddcd2c34..1e3eee45dc 100644 --- a/website/common/locales/en/tasks.json +++ b/website/common/locales/en/tasks.json @@ -168,5 +168,6 @@ "monthlyRepeatHelpContent": "This task will be due every X months", "yearlyRepeatHelpContent": "This task will be due every X years", "resets": "Resets", - "summaryStart": "Repeats <%= frequency %> every <%= everyX %> <%= frequencyPlural %> " + "summaryStart": "Repeats <%= frequency %> every <%= everyX %> <%= frequencyPlural %> ", + "nextDue": "Next Due Dates" } diff --git a/website/views/shared/tasks/edit/repeatables.jade b/website/views/shared/tasks/edit/repeatables.jade index d385aea332..5bb8e72911 100644 --- a/website/views/shared/tasks/edit/repeatables.jade +++ b/website/views/shared/tasks/edit/repeatables.jade @@ -32,3 +32,7 @@ fieldset.option-group.advanced-option(ng-show="task.type === 'daily'") .form-group legend.option-title=env.t('summary') div {{summary}} + + .form-group(ng-if='nextDue') + legend.option-title=env.t('nextDue') + div {{nextDue}} From 99a2013767eb57806d6bc261fdd71de24e9e6c85 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Thu, 18 May 2017 14:12:36 -0600 Subject: [PATCH 16/26] Fixed display of next due dates --- .../client-old/js/services/taskServices.js | 18 +++++++---- website/common/script/cron.js | 32 ++++++++++++++++--- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/website/client-old/js/services/taskServices.js b/website/client-old/js/services/taskServices.js index 0b7cc9b952..2c92acff33 100644 --- a/website/client-old/js/services/taskServices.js +++ b/website/client-old/js/services/taskServices.js @@ -263,6 +263,7 @@ angular.module('habitrpg') modalScope.task._tags = !user.preferences.tagsCollapsed; modalScope.task._advanced = !user.preferences.advancedCollapsed; modalScope.task._edit = angular.copy(task); + modalScope.user = user; if($rootScope.charts[task._id]) $rootScope.charts[task.id] = false; modalScope.taskStatus = taskStatus; @@ -294,7 +295,7 @@ angular.module('habitrpg') $scope.$watch('task._edit', function (newValue, oldValue) { if ($scope.task.type !== 'daily' || !task._edit) return; $scope.summary = generateSummary($scope.task); - $scope.nextDue = generateNextDue($scope.task); + $scope.nextDue = generateNextDue($scope.task._edit, $scope.user); $scope.repeatSuffix = generateRepeatSuffix($scope.task); if ($scope.task._edit.repeatsOn == 'dayOfMonth') { @@ -388,12 +389,15 @@ angular.module('habitrpg') } }; - function generateNextDue (task) { - if (!task.nextDue) return; - let nextDue = task.nextDue.map((date) => { - let dateObject = new Date(date); - return [dateObject.getFullYear(), dateObject.getMonth() + 1, dateObject.getDate()].join('-'); - }) + function generateNextDue (task, user) { + let options = angular.copy(user); + options.nextDue = true; + let nextDueDates = Shared.shouldDo(new Date, task, options); + + let nextDue = nextDueDates.map((date) => { + return date.format('MM-DD-YYYY'); + }); + return nextDue.join(', '); } diff --git a/website/common/script/cron.js b/website/common/script/cron.js index 042f89006f..513ac07ab4 100644 --- a/website/common/script/cron.js +++ b/website/common/script/cron.js @@ -122,7 +122,7 @@ export function shouldDo (day, dailyTask, options = {}) { let schedule = moment(startDate).recur() .every(dailyTask.everyX).days(); - if (options.nextDue) return schedule.fromDate(startOfDayWithCDSTime).next(3); + if (options.nextDue) return schedule.fromDate(startOfDayWithCDSTime).next(6); return schedule.matches(startOfDayWithCDSTime); } else if (dailyTask.frequency === 'weekly') { @@ -133,7 +133,15 @@ export function shouldDo (day, dailyTask, options = {}) { schedule = schedule.every(daysOfTheWeek).daysOfWeek(); - if (options.nextDue) return schedule.fromDate(startOfDayWithCDSTime).next(3); + if (options.nextDue) { + let dates = schedule.fromDate(startOfDayWithCDSTime).next(6); + let filterDates = dates.filter((momentDate) => { + let weekDiff = momentDate.week() - moment(startDate).week(); + let matchX = weekDiff % dailyTask.everyX === 0; + return matchX; + }); + return filterDates; + } return schedule.matches(startOfDayWithCDSTime) && matchEveryX; } else if (dailyTask.frequency === 'monthly') { @@ -149,7 +157,15 @@ export function shouldDo (day, dailyTask, options = {}) { schedule = schedule.every(dailyTask.daysOfMonth).daysOfMonth(); } - if (options.nextDue) return schedule.fromDate(startOfDayWithCDSTime).next(3); + if (options.nextDue) { + let dates = schedule.fromDate(startOfDayWithCDSTime).next(6); + let filterDates = dates.filter((momentDate) => { + let monthDiff = momentDate.month() - moment(startDate).month(); + let matchX = monthDiff % dailyTask.everyX === 0; + return matchX; + }); + return filterDates; + } return schedule.matches(startOfDayWithCDSTime) && matchEveryX; } else if (dailyTask.frequency === 'yearly') { @@ -157,7 +173,15 @@ export function shouldDo (day, dailyTask, options = {}) { schedule = schedule.every(dailyTask.everyX).years(); - if (options.nextDue) return schedule.fromDate(startOfDayWithCDSTime).next(3); + if (options.nextDue) { + let dates = schedule.fromDate(startOfDayWithCDSTime).next(6); + let filterDates = dates.filter((momentDate) => { + let monthDiff = momentDate.years() - moment(startDate).years(); + let matchX = monthDiff % dailyTask.everyX === 0; + return matchX; + }); + return filterDates; + } return schedule.matches(startOfDayWithCDSTime); } From 59bfe66c947fd3ac2e4884038ae6f733151843cc Mon Sep 17 00:00:00 2001 From: negue Date: Sat, 20 May 2017 19:59:45 +0200 Subject: [PATCH 17/26] Client/item content (#8738) * extract item popover-content as component # Conflicts: # website/client/components/inventory/item.vue * extract item-content as slot * scoped context to pass the item * itemContentClass instead of itemContent-slot --- website/client/assets/scss/item.scss | 2 +- .../client/components/inventory/equipment.vue | 13 +++++++++- .../inventory/equipmentAttributesPopover.vue | 25 +++++++++++++++++++ website/client/components/inventory/item.vue | 17 +++++++------ 4 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 website/client/components/inventory/equipmentAttributesPopover.vue diff --git a/website/client/assets/scss/item.scss b/website/client/assets/scss/item.scss index fed6a06ffe..39baee16e1 100644 --- a/website/client/assets/scss/item.scss +++ b/website/client/assets/scss/item.scss @@ -86,4 +86,4 @@ .item:hover > .badge { display: block; -} \ No newline at end of file +} diff --git a/website/client/components/inventory/equipment.vue b/website/client/components/inventory/equipment.vue index 7c055f612b..fa0785048b 100644 --- a/website/client/components/inventory/equipment.vue +++ b/website/client/components/inventory/equipment.vue @@ -30,7 +30,7 @@ b-dropdown-item(@click="groupBy = 'class'", :class="{'dropdown-item-active': groupBy === 'class'}") {{ $t('class') }} drawer( - :title="$t('equipment')", + :title="$t('equipment')", :errorMessage="(costume && !user.preferences.costume) ? $t('costumeDisabled') : null", ) div(slot="drawer-header") @@ -65,12 +65,17 @@ v-for="(label, group) in gearTypesToStrings", :key="group", :item="flatGear[activeItems[group]]", + :itemContentClass="'shop_' + flatGear[activeItems[group]].key", + :showPopover="flatGear[activeItems[group]] && flatGear[activeItems[group]].key.indexOf('_base_0') === -1", :label="$t(label)", :selected="true", :popoverPosition="'top'", :starVisible="!costume || user.preferences.costume", @click="equip", ) + template(slot="popoverContent", scope="ctx") + equipmentAttributesPopover(:item="ctx.item") + div( v-for="group in itemsGroups", v-if="viewOptions[group.key].selected", @@ -86,11 +91,15 @@ v-for="(item, index) in items[group.key]", v-if="viewOptions[group.key].open || index < itemsPerLine", :item="item", + :itemContentClass="'shop_' + item.key", + :showPopover="item && item.key.indexOf('_base_0') === -1", :key="item.key", :selected="activeItems[item.type] === item.key", :starVisible="!costume || user.preferences.costume", @click="equip", ) + template(slot="popoverContent", scope="ctx") + equipmentAttributesPopover(:item="ctx.item") div(v-if="items[group.key].length === 0") p(v-once) {{ $t('noGearItemsOfType', { type: $t(group.label) }) }} a.btn.btn-show-more( @@ -117,11 +126,13 @@ import bPopover from 'bootstrap-vue/lib/components/popover'; import toggleSwitch from 'client/components/ui/toggleSwitch'; import Item from 'client/components/inventory/item'; +import EquipmentAttributesPopover from 'client/components/inventory/equipmentAttributesPopover'; import Drawer from 'client/components/inventory/drawer'; export default { components: { Item, + EquipmentAttributesPopover, Drawer, bDropdown, bDropdownItem, diff --git a/website/client/components/inventory/equipmentAttributesPopover.vue b/website/client/components/inventory/equipmentAttributesPopover.vue new file mode 100644 index 0000000000..e44a5d168e --- /dev/null +++ b/website/client/components/inventory/equipmentAttributesPopover.vue @@ -0,0 +1,25 @@ + + + diff --git a/website/client/components/inventory/item.vue b/website/client/components/inventory/item.vue index 51e51a0b67..eccae17dc6 100644 --- a/website/client/components/inventory/item.vue +++ b/website/client/components/inventory/item.vue @@ -2,14 +2,10 @@ b-popover( :triggers="['hover']", :placement="popoverPosition", - v-if="item && item.key.indexOf('_base_0') === -1", + v-if="showPopover", ) span(slot="content") - h4.popover-content-title {{ item.text() }} - .popover-content-text {{ item.notes() }} - .popover-content-attr(v-for="attr in ATTRIBUTES") - span.popover-content-attr-key {{ `${$t(attr)}: ` }} - span.popover-content-attr-val {{ `+${item[attr]}` }} + slot(name="popoverContent", :item="item") .item-wrapper .item @@ -18,7 +14,7 @@ b-popover( @click="click", v-if="starVisible" ) ★ - span.item-content(:class="'shop_' + item.key") + span.item-content(:class="itemContentClass") span.item-label(v-if="label") {{ label }} div(v-else) .item-wrapper @@ -40,6 +36,9 @@ export default { item: { type: Object, }, + itemContentClass: { + type: String + }, selected: { type: Boolean, }, @@ -49,6 +48,10 @@ export default { label: { type: String, }, + showPopover: { + type: Boolean, + default: true, + }, popoverPosition: { type: String, default: 'bottom', From c08c0685f312b7970995093b6571e9b9b1510744 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sat, 20 May 2017 17:19:35 -0600 Subject: [PATCH 18/26] Fixed broken tests --- test/api/v3/unit/libs/cron.test.js | 2 +- test/common/shouldDo.test.js | 16 ++++++++++++++-- website/client-old/js/services/taskServices.js | 6 +++--- website/common/script/cron.js | 4 ++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/test/api/v3/unit/libs/cron.test.js b/test/api/v3/unit/libs/cron.test.js index 18754398ae..0cf1845c69 100644 --- a/test/api/v3/unit/libs/cron.test.js +++ b/test/api/v3/unit/libs/cron.test.js @@ -371,7 +371,7 @@ describe('cron', () => { tasksByType.dailys[0].everyX = 5; tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate(); cron({user, tasksByType, daysMissed, analytics}); - expect(tasksByType.dailys[0].nextDue.length).to.eql(3); + expect(tasksByType.dailys[0].nextDue.length).to.eql(6); }); it('should add history', () => { diff --git a/test/common/shouldDo.test.js b/test/common/shouldDo.test.js index 62571b2bde..b9972e831f 100644 --- a/test/common/shouldDo.test.js +++ b/test/common/shouldDo.test.js @@ -543,7 +543,19 @@ describe('shouldDo', () => { beforeEach(() => { options.dayStart = 7; dailyTask.everyX = 3; - threeWeeksFromToday = moment().add(3, 'weeks').toDate(); + dailyTask.repeat = { + su: false, + s: false, + f: false, + th: false, + w: false, + t: false, + m: false, + }; + + day = moment(); + dailyTask.repeat[DAY_MAPPING[day.day()]] = true; + threeWeeksFromToday = moment().add(3, 'weeks').day(day.day()).toDate(); }); context('Current Date is one day before the matching day', () => { @@ -620,7 +632,7 @@ describe('shouldDo', () => { dailyTask.everyX = 2; dailyTask.frequency = 'monthly'; dailyTask.daysOfMonth = [15]; - day = moment().date(15).toDate(); + day = moment().add(2, 'months').date(15).toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); diff --git a/website/client-old/js/services/taskServices.js b/website/client-old/js/services/taskServices.js index 2c92acff33..f13d68a41b 100644 --- a/website/client-old/js/services/taskServices.js +++ b/website/client-old/js/services/taskServices.js @@ -390,11 +390,11 @@ angular.module('habitrpg') }; function generateNextDue (task, user) { - let options = angular.copy(user); + var options = angular.copy(user); options.nextDue = true; - let nextDueDates = Shared.shouldDo(new Date, task, options); + var nextDueDates = Shared.shouldDo(new Date, task, options); - let nextDue = nextDueDates.map((date) => { + var nextDue = nextDueDates.map(function (date) { return date.format('MM-DD-YYYY'); }); diff --git a/website/common/script/cron.js b/website/common/script/cron.js index 513ac07ab4..6e24247b65 100644 --- a/website/common/script/cron.js +++ b/website/common/script/cron.js @@ -128,7 +128,7 @@ export function shouldDo (day, dailyTask, options = {}) { } else if (dailyTask.frequency === 'weekly') { let schedule = moment(startDate).recur(); - let differenceInWeeks = moment(day).week() - moment(startDate).week(); + let differenceInWeeks = moment(startOfDayWithCDSTime).week() - moment(startDate).week(); let matchEveryX = differenceInWeeks % dailyTask.everyX === 0; schedule = schedule.every(daysOfTheWeek).daysOfWeek(); @@ -147,7 +147,7 @@ export function shouldDo (day, dailyTask, options = {}) { } else if (dailyTask.frequency === 'monthly') { let schedule = moment(startDate).recur(); - let differenceInMonths = moment(day).month() - moment(startDate).month(); + let differenceInMonths = moment(startOfDayWithCDSTime).month() - moment(startDate).month(); let matchEveryX = differenceInMonths % dailyTask.everyX === 0; if (dailyTask.weeksOfMonth && dailyTask.weeksOfMonth.length > 0) { From f49d21d7b415e33bd759ce1ce7c6aaeef56889f7 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sat, 20 May 2017 18:38:53 -0600 Subject: [PATCH 19/26] added next due date as today for weekly --- website/common/script/cron.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/common/script/cron.js b/website/common/script/cron.js index 6e24247b65..124e0f0ce2 100644 --- a/website/common/script/cron.js +++ b/website/common/script/cron.js @@ -134,7 +134,7 @@ export function shouldDo (day, dailyTask, options = {}) { schedule = schedule.every(daysOfTheWeek).daysOfWeek(); if (options.nextDue) { - let dates = schedule.fromDate(startOfDayWithCDSTime).next(6); + let dates = schedule.fromDate(startOfDayWithCDSTime.subtract('1', 'days')).next(6); let filterDates = dates.filter((momentDate) => { let weekDiff = momentDate.week() - moment(startDate).week(); let matchX = weekDiff % dailyTask.everyX === 0; From 228b724d52e37a3c9eefe791d037b5dd2956dffa Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sun, 21 May 2017 14:43:25 +0200 Subject: [PATCH 20/26] fix missing trailing comma --- website/client/components/inventory/item.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/client/components/inventory/item.vue b/website/client/components/inventory/item.vue index eccae17dc6..1a8df044be 100644 --- a/website/client/components/inventory/item.vue +++ b/website/client/components/inventory/item.vue @@ -37,7 +37,7 @@ export default { type: Object, }, itemContentClass: { - type: String + type: String, }, selected: { type: Boolean, From f267456a30c8026c6cec234e34435cdc84e34734 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sun, 21 May 2017 15:38:33 +0200 Subject: [PATCH 21/26] client: fix drawer --- website/client/components/inventory/equipment.vue | 4 ++-- .../components/inventory/equipmentAttributesPopover.vue | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/website/client/components/inventory/equipment.vue b/website/client/components/inventory/equipment.vue index fa0785048b..f4476917ad 100644 --- a/website/client/components/inventory/equipment.vue +++ b/website/client/components/inventory/equipment.vue @@ -65,8 +65,8 @@ v-for="(label, group) in gearTypesToStrings", :key="group", :item="flatGear[activeItems[group]]", - :itemContentClass="'shop_' + flatGear[activeItems[group]].key", - :showPopover="flatGear[activeItems[group]] && flatGear[activeItems[group]].key.indexOf('_base_0') === -1", + :itemContentClass="flatGear[activeItems[group]] ? 'shop_' + flatGear[activeItems[group]].key : null", + :showPopover="!!flatGear[activeItems[group]] && flatGear[activeItems[group]].key.indexOf('_base_0') === -1", :label="$t(label)", :selected="true", :popoverPosition="'top'", diff --git a/website/client/components/inventory/equipmentAttributesPopover.vue b/website/client/components/inventory/equipmentAttributesPopover.vue index e44a5d168e..1beca9046c 100644 --- a/website/client/components/inventory/equipmentAttributesPopover.vue +++ b/website/client/components/inventory/equipmentAttributesPopover.vue @@ -1,10 +1,10 @@