From 8c68f450c6380b98218fe43df90c28ba3625c286 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Wed, 15 Mar 2017 14:13:15 -0600 Subject: [PATCH] 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