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..1baa24042e 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(6); + }); + 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..4f90dedd49 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(6); }); 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..433b263179 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(6); }); 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..0cf1845c69 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(6); + }); + it('should add history', () => { cron({user, tasksByType, daysMissed, analytics}); expect(tasksByType.dailys[0].history).to.be.lengthOf(1); diff --git a/test/common/shouldDo.test.js b/test/common/shouldDo.test.js index 0c048adc15..144a2663be 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; @@ -24,6 +24,7 @@ describe('shouldDo', () => { }, startDate: new Date(), }; + options = {}; }); it('returns false if task type is not a daily', () => { @@ -38,6 +39,165 @@ describe('shouldDo', () => { expect(shouldDo(day, dailyTask, options)).to.equal(false); }); + context('Timezone variations', () => { + context('User timezone is UTC', () => { + beforeEach(() => { + options.timezoneOffset = 0; + }); + + it('returns true if Start Date is before today', () => { + dailyTask.startDate = moment().subtract(1, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + it('returns true if Start Date is today', () => { + 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)', () => { + beforeEach(() => { + options.timezoneOffset = 600; + }); + + it('returns true if Start Date is before today', () => { + dailyTask.startDate = moment().subtract(1, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + it('returns true if Start Date is today', () => { + dailyTask.startDate = moment().startOf('day').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + it('returns true if the user\'s current time is after start date and Custom Day Start', () => { + options.dayStart = 4; + 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().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); + }); + }); + + context('User timezone is between UTC and GMT+14 (-840~0)', () => { + beforeEach(() => { + options.timezoneOffset = -420; + }); + + it('returns true if Start Date is before today', () => { + dailyTask.startDate = moment().subtract(1, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + it('returns true if Start Date is today', () => { + dailyTask.startDate = moment().startOf('day').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + it('returns true if the user\'s current time is after Custom Day Start', () => { + options.dayStart = 4; + 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().zone(options.timezoneOffset).startOf('day').add(2, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); + }); + + context('Custom Day Start variations', () => { + beforeEach(() => { + // Daily is due every 2 days, and start today + dailyTask.frequency = 'daily'; + dailyTask.everyX = 2; + dailyTask.startDate = new Date(); + }); + + context('Custom Day Start is midnight (Default dayStart=0)', () => { + beforeEach(() => { + options.dayStart = 0; + }); + + context('Current Date is yesterday', () => { + it('should not be due yesterday', () => { + day = moment(day).subtract(1, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); + + context('Current Date is today', () => { + it('returns false if current time is before midnight', () => { + day = moment(day).startOf('day').subtract(1, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + + it('returns true if current time is after midnight', () => { + day = moment(day).startOf('day').add(1, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + }); + + context('Current Date is tomorrow', () => { + it('should not be due tomorrow', () => { + day = moment(day).add(1, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); + }); + + context('Custom Day Start is 0 <= n < 24', () => { + beforeEach(() => { + options.dayStart = 7; + }); + + context('Current Date is yesterday', () => { + it('should not be due yesterday', () => { + day = moment(day).subtract(1, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); + + context('Current Date is today', () => { + 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 tomorrow', () => { + 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 Days', () => { beforeEach(() => { dailyTask.frequency = 'daily'; @@ -61,12 +221,65 @@ describe('shouldDo', () => { dailyTask.everyX = 7; expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); - day = moment(day).add(7, 'days'); - expect(shouldDo(day, dailyTask, options)).to.equal(true); + context('On multiples of x', () => { + it('returns true when Custom Day Start is midnight', () => { + dailyTask.startDate = moment().subtract(7, 'days').toDate(); + dailyTask.everyX = 7; - day = moment(day).add(7, 'days'); - expect(shouldDo(day, dailyTask, options)).to.equal(true); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + + day = moment(day).add(7, 'days'); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + + day = moment(day).add(7, 'days'); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + it('returns true when current time is after Custom Day Start', () => { + dailyTask.startDate = moment().subtract(5, 'days').toDate(); + dailyTask.everyX = 5; + + options.dayStart = 3; + day = moment(day).startOf('day').add(8, 'hours').toDate(); + + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + + it('returns false when current time is before Custom Day Start', () => { + dailyTask.startDate = moment().subtract(5, 'days').toDate(); + dailyTask.everyX = 5; + + options.dayStart = 14; + day = moment(day).startOf('day').add(7, 'hours').toDate(); + + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); + + context('If number of X days is zero', () => { + beforeEach(() => { + dailyTask.everyX = 0; + }); + + it('returns false on the Start Date', () => { + dailyTask.startDate = moment().subtract(4, 'days').toDate(); + day = moment().subtract(4, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + + it('returns false on the day before Start Date', () => { + dailyTask.startDate = moment().subtract(4, 'days').toDate(); + day = moment().subtract(5, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + + it('returns false on the day after Start Date', () => { + dailyTask.startDate = moment().subtract(4, 'days').toDate(); + day = moment().subtract(3, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); }); }); @@ -134,217 +347,597 @@ describe('shouldDo', () => { it('returns true if Daily on matching days of the week', () => { expect(shouldDo(day, dailyTask, options)).to.equal(true); }); + + context('Day of the week matches', () => { + const weekdayMap = { + 1: 'm', + 2: 't', + 3: 'w', + 4: 'th', + 5: 'f', + 6: 's', + 7: 'su', + }; + + beforeEach(() => { + // Set repeat day to current weekday + const currentWeekday = moment().isoWeekday(); + dailyTask.startDate = moment().startOf('day').toDate(); + dailyTask.repeat = { + su: false, + s: false, + f: false, + th: false, + w: false, + t: false, + m: false, + }; + dailyTask.repeat[weekdayMap[currentWeekday]] = true; + }); + + context('Custom Day Start is midnight (Default dayStart=0)', () => { + beforeEach(() => { + options.dayStart = 0; + }); + + context('Current Date is one day before the matching day', () => { + it('should return false', () => { + 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 time is before midnight', () => { + day = moment(day).startOf('day').subtract(1, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + + it('returns true if current time is after midnight', () => { + day = moment(day).startOf('day').add(1, 'hours').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(true); + }); + }); + + context('Current Date is one day after the matching day', () => { + it('should not be due tomorrow', () => { + day = moment(day).add(1, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); + }); + + context('Custom Day Start is 0 <= n < 24', () => { + beforeEach(() => { + options.dayStart = 7; + }); + + 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('No days of the week is selected', () => { + beforeEach(() => { + dailyTask.repeat = { + su: false, + s: false, + f: false, + th: false, + w: false, + t: false, + m: false, + }; + }); + + it('returns false for a day before the Start Date', () => { + day = moment().subtract(1, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + + it('returns false for the Start Date', () => { + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + + it('returns false for a day after the Start Date', () => { + day = moment().add(1, 'days').toDate(); + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + + it('returns false for today', () => { + expect(shouldDo(day, dailyTask, options)).to.equal(false); + }); + }); }); - // 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.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 tomorrow = day.add(2, 'weeks').day(day.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); + }); + + 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; + 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', () => { + 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', () => { + 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); + }); + + it('activates Daily on start date', () => { + 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('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', () => { + 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); + }); + + 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', () => { + 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); + }); + + 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/client-old/js/services/taskServices.js b/website/client-old/js/services/taskServices.js index 195258b3f2..0581df566e 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,6 +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._edit, $scope.user); $scope.repeatSuffix = generateRepeatSuffix($scope.task); if ($scope.task._edit.repeatsOn == 'dayOfMonth') { @@ -352,7 +354,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; @@ -381,9 +387,24 @@ 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, user) { + var options = angular.copy(user); + options.nextDue = true; + var nextDueDates = Shared.shouldDo(new Date, task, options); + if (!nextDueDates) return ''; + + var dateFormat = 'MM-DD-YYYY'; + if (user.preferences.dateFormat) dateFormat = user.preferences.dateFormat.toUpperCase(); + + var nextDue = nextDueDates.map(function (date) { + return date.format(dateFormat); + }); + + 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 633dc06033..1e3eee45dc 100644 --- a/website/common/locales/en/tasks.json +++ b/website/common/locales/en/tasks.json @@ -167,5 +167,7 @@ "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 %> ", + "nextDue": "Next Due Dates" } diff --git a/website/common/script/cron.js b/website/common/script/cron.js index 2ce36c0e9b..bd6532278d 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,14 @@ 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 (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,7 +86,7 @@ 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'); } /* @@ -93,32 +98,94 @@ 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 taskStartDate = moment(dailyTask.startDate).zone(o.timezoneOffset); - taskStartDate = moment(taskStartDate).startOf('day'); - if (taskStartDate > startOfDayWithCDSTime.startOf('day')) { + let startDate = moment(dailyTask.startDate).zone(o.timezoneOffset).startOf('day'); + + if (startDate > startOfDayWithCDSTime.startOf('day') && !options.nextDue) { return false; // Daily starts in the future } - if (dailyTask.frequency === 'daily') { // "Every X Days" - if (!dailyTask.everyX) { - return false; // error condition - } - 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 + let daysOfTheWeek = []; - return dailyTask.repeat[DAY_MAPPING[dayOfWeekNum]]; - } else { - return false; // error condition - unexpected frequency string + if (dailyTask.repeat) { + for (let [repeatDay, active] of Object.entries(dailyTask.repeat)) { + if (active) daysOfTheWeek.push(parseInt(DAY_MAPPING_STRING_TO_NUMBER[repeatDay], 10)); + } } -} \ 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(); + + if (options.nextDue) return schedule.fromDate(startOfDayWithCDSTime).next(6); + + return schedule.matches(startOfDayWithCDSTime); + } else if (dailyTask.frequency === 'weekly') { + let schedule = moment(startDate).recur(); + + let differenceInWeeks = moment(startOfDayWithCDSTime).week() - moment(startDate).week(); + let matchEveryX = differenceInWeeks % dailyTask.everyX === 0; + + if (daysOfTheWeek.length === 0) return false; + schedule = schedule.every(daysOfTheWeek).daysOfWeek(); + + if (options.nextDue) { + 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; + return matchX; + }); + return filterDates; + } + + return schedule.matches(startOfDayWithCDSTime) && matchEveryX; + } else if (dailyTask.frequency === 'monthly') { + let schedule = moment(startDate).recur(); + + let differenceInMonths = moment(startOfDayWithCDSTime).month() - moment(startDate).month(); + 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(); + } + + 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') { + let schedule = moment(startDate).recur(); + + schedule = schedule.every(dailyTask.everyX).years(); + + 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); + } + + return false; +} diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index ef61c1b8f3..c48ce8d47c 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -17,6 +17,7 @@ import { createTasks, getTasks, moveTask, + setNextDue, } from '../../libs/taskManager'; import common from '../../../common'; import Bluebird from 'bluebird'; @@ -456,9 +457,7 @@ api.updateTask = { task.group.approval.required = true; } - if (sanitizedObj.type === 'daily') { - task.isDue = common.shouldDo(Date.now(), sanitizedObj, user.preferences); - } + setNextDue(task, user); let savedTask = await task.save(); @@ -589,6 +588,8 @@ api.scoreTask = { } } + setNextDue(task, user); + if (user._ABtests && user._ABtests.guildReminder && user._ABtests.counter !== -1) { user._ABtests.counter++; if (user._ABtests.counter > 1) { @@ -601,10 +602,6 @@ api.scoreTask = { user.markModified('_ABtests'); } - if (task.type === 'daily') { - task.isDue = common.shouldDo(Date.now(), task, user.preferences); - } - let results = await Bluebird.all([ user.save(), task.save(), 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..8c4ecb1c79 100644 --- a/website/server/libs/taskManager.js +++ b/website/server/libs/taskManager.js @@ -22,6 +22,19 @@ async function _validateTaskAlias (tasks, res) { }); } +export function setNextDue (task, user) { + if (task.type !== 'daily') return; + + let optionsForShouldDo = 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. @@ -64,7 +77,7 @@ export async function createTasks (req, res, options = {}) { newTask.userId = user._id; } - if (newTask.type === 'daily') newTask.isDue = shared.shouldDo(Date.now(), newTask, user.preferences); + 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 182c174ce8..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; @@ -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); 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') diff --git a/website/views/shared/tasks/edit/repeatables.jade b/website/views/shared/tasks/edit/repeatables.jade index bb34d2fb2a..5bb8e72911 100644 --- a/website/views/shared/tasks/edit/repeatables.jade +++ b/website/views/shared/tasks/edit/repeatables.jade @@ -8,27 +8,31 @@ 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 .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 + .form-group + legend.option-title=env.t('summary') + div {{summary}} + + .form-group(ng-if='nextDue') + legend.option-title=env.t('nextDue') + div {{nextDue}}