Repeatables (#8444)

* Added initial should do weekly tests

* Added support back in for days of the week and every x day

* Added better week day mapper

* Added initial monthly

* Added every x months

* Added yearlies

* Fixed every nth weekdy of month

* Fixed tests to check every x week on weekday

* Began combining x month with nth weekday

* Added every x month combined with date and weekday

* Fixed lint issues

* Saved moment-recurr to package.json

* Added new repeat fields

* Added UI for repeatables

* Ensured only dalies are affected by summary

* Added local strings

* Updated npm shrinkwrap

* Shared day map constant

* Updated shrinkwrap

* Added ui back

* Updated copy of test cases

* Added new translation strings

* Updated shrinkwrap

* Fixed broken test

* Made should do tests static for better consitency

* Fixed issue with no repeat

* Fixed line endings

* Added frequency enum values

* Fixed spacing
This commit is contained in:
Keith Holliday
2017-02-27 15:41:21 -07:00
committed by GitHub
parent 93befcebcc
commit ef02e59590
12 changed files with 636 additions and 3407 deletions

3478
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -78,6 +78,7 @@
"merge-stream": "^1.0.0", "merge-stream": "^1.0.0",
"method-override": "^2.3.5", "method-override": "^2.3.5",
"moment": "^2.13.0", "moment": "^2.13.0",
"moment-recur": "^1.0.5",
"mongoose": "^4.7.1", "mongoose": "^4.7.1",
"mongoose-id-autoinc": "~2013.7.14-4", "mongoose-id-autoinc": "~2013.7.14-4",
"morgan": "^1.7.0", "morgan": "^1.7.0",

View File

@@ -496,6 +496,8 @@ describe('POST /tasks/user', () => {
frequency: 'daily', frequency: 'daily',
everyX: 5, everyX: 5,
startDate: now, startDate: now,
daysOfMonth: [15],
weeksOfMonth: [3],
}); });
expect(task.userId).to.equal(user._id); expect(task.userId).to.equal(user._id);
@@ -504,6 +506,8 @@ describe('POST /tasks/user', () => {
expect(task.type).to.eql('daily'); expect(task.type).to.eql('daily');
expect(task.frequency).to.eql('daily'); expect(task.frequency).to.eql('daily');
expect(task.everyX).to.eql(5); expect(task.everyX).to.eql(5);
expect(task.daysOfMonth).to.eql([15]);
expect(task.weeksOfMonth).to.eql([3]);
expect(new Date(task.startDate)).to.eql(now); expect(new Date(task.startDate)).to.eql(now);
}); });

View File

@@ -0,0 +1,310 @@
import { shouldDo, DAY_MAPPING } from '../../website/common/script/cron';
import moment from 'moment';
import 'moment-recur';
describe('shouldDo', () => {
let day, dailyTask;
let options = {};
beforeEach(() => {
day = new Date();
dailyTask = {
completed: 'false',
everyX: 1,
frequency: 'weekly',
type: 'daily',
repeat: {
su: true,
s: true,
f: true,
th: true,
w: true,
t: true,
m: true,
},
startDate: new Date(),
};
});
it('leaves Daily inactive before start date', () => {
dailyTask.startDate = moment().add(1, 'days').toDate();
expect(shouldDo(day, dailyTask, options)).to.equal(false);
});
context('Every X Days', () => {
it('leaves Daily inactive in between X Day intervals', () => {
dailyTask.startDate = moment().subtract(1, 'days').toDate();
dailyTask.frequency = 'daily';
dailyTask.everyX = 2;
expect(shouldDo(day, dailyTask, options)).to.equal(false);
});
it('activates Daily on multiples of X Days', () => {
dailyTask.startDate = moment().subtract(7, 'days').toDate();
dailyTask.frequency = 'daily';
dailyTask.everyX = 7;
expect(shouldDo(day, dailyTask, options)).to.equal(true);
});
});
context('Certain Days of the Week', () => {
it('leaves Daily inactive if day of the week does not match', () => {
dailyTask.repeat = {
su: false,
s: false,
f: false,
th: false,
w: false,
t: false,
m: false,
};
for (let weekday of [0, 1, 2, 3, 4, 5, 6]) {
day = moment().day(weekday).toDate();
expect(shouldDo(day, dailyTask, options)).to.equal(false);
}
});
it('leaves Daily inactive if day of the week does not match and active on the day it matches', () => {
dailyTask.repeat = {
su: false,
s: false,
f: false,
th: true,
w: false,
t: false,
m: false,
};
for (let weekday of [0, 1, 2, 3, 4, 5, 6]) {
day = moment().add(1, 'weeks').day(weekday).toDate();
if (weekday === 4) {
expect(shouldDo(day, dailyTask, options)).to.equal(true);
} else {
expect(shouldDo(day, dailyTask, options)).to.equal(false);
}
}
});
it('activates Daily on matching days of the week', () => {
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('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);
});
});
});

View File

@@ -281,12 +281,108 @@ angular.module('habitrpg')
} }
modalScope.cancelTaskEdit = cancelTaskEdit; modalScope.cancelTaskEdit = cancelTaskEdit;
$rootScope.openModal('task-edit', {scope: modalScope }) modalScope.task._edit.repeatsOn = 'dayOfMonth';
if (modalScope.task === 'daily' && modalScope.task._edit.weeksOfMonth.length > 0) {
modalScope.task._edit.repeatsOn = 'dayOfWeek';
}
$rootScope.openModal('task-edit', {
scope: modalScope,
controller: function ($scope) {
$scope.$watch('task._edit', function (newValue, oldValue) {
if ($scope.task.type !== 'daily') return;
$scope.summary = generateSummary($scope.task);
$scope.repeatSuffix = generateRepeatSuffix($scope.task);
if ($scope.task._edit.repeatsOn == 'dayOfMonth') {
var date = moment().date();
$scope.task._edit.weeksOfMonth = [];
$scope.task._edit.dayOfMonth = [date]; // @TODO This can handle multiple dates later
} else if ($scope.task._edit.repeatsOn == 'dayOfWeek') {
var week = Math.ceil(moment().date() / 7) - 1;
var dayOfWeek = moment().day();
var shortDay = numberToShortDay[dayOfWeek];
$scope.task._edit.dayOfMonth = [];
$scope.task._edit.weeksOfMonth = [week]; // @TODO: This can handle multiple weeks
for (var key in $scope.task._edit.repeat) {
$scope.task._edit.repeat[key] = false;
}
$scope.task._edit.repeat[shortDay] = true;
}
}, true);
},
})
.result.catch(function() { .result.catch(function() {
cancelTaskEdit(task); cancelTaskEdit(task);
}); });
} }
/*
* Summary
*/
var frequencyMap = {
'daily': 'days',
'weekly': 'weeks',
'monthly': 'months',
'yearly': 'years',
};
var shortDayToLongDayMap = {
'su': moment().day(0).format('dddd'),
's': moment().day(6).format('dddd'),
'f': moment().day(5).format('dddd'),
'th': moment().day(4).format('dddd'),
'w': moment().day(3).format('dddd'),
't': moment().day(2).format('dddd'),
'm': moment().day(1).format('dddd'),
};
var numberToShortDay = Shared.DAY_MAPPING;
function generateSummary(task) {
var frequencyPlural = frequencyMap[task._edit.frequency];
var repeatDays = '';
for (var key in task._edit.repeat) {
if (task._edit.repeat[key]) {
repeatDays += shortDayToLongDayMap[key] + ', ';
}
}
var summary = 'Repeats ' + task._edit.frequency + ' every ' + task._edit.everyX + ' ' + frequencyPlural;
if (task._edit.frequency === 'weekly') summary += ' on ' + repeatDays;
if (task._edit.frequency === 'monthly' && task._edit.repeatsOn == 'dayOfMonth') {
var date = moment().date();
summary += ' on the ' + date;
} else if (task._edit.frequency === 'monthly' && task._edit.repeatsOn == 'dayOfWeek') {
var week = Math.ceil(moment().date() / 7) - 1;
var dayOfWeek = moment().day();
var shortDay = numberToShortDay[dayOfWeek];
var longDay = shortDayToLongDayMap[shortDay];
summary += ' on the ' + (week + 1) + ' ' + longDay;
}
return summary;
}
function generateRepeatSuffix (task) {
if (task._edit.frequency === 'daily') {
return task._edit.everyX == 1 ? window.env.t('day') : window.env.t('days');
} else if (task._edit.frequency === 'weekly') {
return task._edit.everyX == 1 ? window.env.t('week') : window.env.t('weeks');
} else if (task._edit.frequency === 'monthly') {
return task._edit.everyX == 1 ? window.env.t('month') : window.env.t('months');
} else if (task._edit.frequency === 'yearly') {
return task._edit.everyX == 1 ? window.env.t('year') : window.env.t('years');
}
};
function cancelTaskEdit(task) { function cancelTaskEdit(task) {
task._edit = undefined; task._edit = undefined;
task._editing = false; task._editing = false;

View File

@@ -147,6 +147,20 @@
"taskApprovalHasBeenRequested": "Approval has been requested", "taskApprovalHasBeenRequested": "Approval has been requested",
"approvals": "Approvals", "approvals": "Approvals",
"approvalRequired": "Approval Required", "approvalRequired": "Approval Required",
"weekly": "Weekly",
"monthly": "Monthly",
"yearly": "Yearly",
"onDays": "On Days",
"summary": "Summary",
"repeatsOn": "Repeats On",
"dayOfWeek": "Day of the Week",
"dayOfMonth": "Day of the Month",
"month": "Month",
"months": "Months",
"week": "Week",
"weeks": "Weeks",
"year": "Year",
"years": "Years",
"confirmScoreNotes": "Confirm task scoring with notes", "confirmScoreNotes": "Confirm task scoring with notes",
"taskScoreNotesTooLong": "Task score notes must be less than 256 characters", "taskScoreNotesTooLong": "Task score notes must be less than 256 characters",
"groupTasksByChallenge": "Group tasks by challenge title" "groupTasksByChallenge": "Group tasks by challenge title"

View File

@@ -6,6 +6,7 @@
*/ */
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import 'moment-recur';
export const DAY_MAPPING = { export const DAY_MAPPING = {
0: 'su', 0: 'su',
@@ -17,6 +18,8 @@ export const DAY_MAPPING = {
6: 's', 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. 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. Specifically {dayStart} (custom day start) and {timezoneOffset}. This function sanitizes / defaults those values.
@@ -88,37 +91,56 @@ export function daysSince (yesterday, options = {}) {
Should the user do this task on this date, given the task's repeat options and user.preferences.dayStart? 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') { if (dailyTask.type !== 'daily') {
return false; 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. let daysOfTheWeek = [];
// 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 (dailyTask.repeat) {
if (taskStartDate > startOfDayWithCDSTime.startOf('day')) { for (let [repeatDay, active] of Object.entries(dailyTask.repeat)) {
return false; // Daily starts in the future if (active) daysOfTheWeek.push(parseInt(DAY_MAPPING_STRING_TO_NUMBER[repeatDay], 10));
} }
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
return dailyTask.repeat[DAY_MAPPING[dayOfWeekNum]]; if (dailyTask.frequency === 'daily') {
} else { if (!dailyTask.everyX) return false; // error condition
return false; // error condition - unexpected frequency string let schedule = moment(dailyTask.startDate).recur()
.every(dailyTask.everyX).days();
return schedule.matches(day);
} else if (dailyTask.frequency === 'weekly') {
let schedule = moment(dailyTask.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(dailyTask.startDate).recur();
let differenceInMonths = moment(day).month() + 1 - moment(dailyTask.startDate).month() + 1;
let matchEveryX = differenceInMonths % dailyTask.everyX === 0;
if (dailyTask.weeksOfMonth) {
schedule = schedule.every(daysOfTheWeek).daysOfWeek()
.every(dailyTask.weeksOfMonth).weeksOfMonthByDay();
} else if (dailyTask.daysOfMonth) {
schedule = schedule.every(dailyTask.daysOfMonth).daysOfMonth();
}
return schedule.matches(day) && matchEveryX;
} else if (dailyTask.frequency === 'yearly') {
let schedule = moment(dailyTask.startDate).recur();
schedule = schedule.every(dailyTask.everyX).years();
return schedule.matches(day);
}
return false;
} }

View File

@@ -13,9 +13,10 @@ import i18n from './i18n';
api.i18n = i18n; api.i18n = i18n;
// TODO under api.libs.cron? // TODO under api.libs.cron?
import { shouldDo, daysSince } from './cron'; import { shouldDo, daysSince, DAY_MAPPING } from './cron';
api.shouldDo = shouldDo; api.shouldDo = shouldDo;
api.daysSince = daysSince; api.daysSince = daysSince;
api.DAY_MAPPING = DAY_MAPPING;
import { import {
MAX_HEALTH, MAX_HEALTH,

View File

@@ -222,7 +222,7 @@ export let HabitSchema = new Schema(_.defaults({
export let habit = Task.discriminator('habit', HabitSchema); export let habit = Task.discriminator('habit', HabitSchema);
export let DailySchema = new Schema(_.defaults({ export let DailySchema = new Schema(_.defaults({
frequency: {type: String, default: 'weekly', enum: ['daily', 'weekly']}, frequency: {type: String, default: 'weekly', enum: ['daily', 'weekly', 'monthly', 'yearly']},
everyX: {type: Number, default: 1}, // e.g. once every X weeks everyX: {type: Number, default: 1}, // e.g. once every X weeks
startDate: { startDate: {
type: Date, type: Date,
@@ -240,6 +240,8 @@ export let DailySchema = new Schema(_.defaults({
su: {type: Boolean, default: true}, su: {type: Boolean, default: true},
}, },
streak: {type: Number, default: 0}, streak: {type: Number, default: 0},
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
}, habitDailySchema(), dailyTodoSchema()), subDiscriminatorOptions); }, habitDailySchema(), dailyTodoSchema()), subDiscriminatorOptions);
export let daily = Task.discriminator('daily', DailySchema); export let daily = Task.discriminator('daily', DailySchema);

View File

@@ -27,8 +27,36 @@ div(ng-if='(task.type !== "reward") || (!obj.auth && obj.purchased && obj.purcha
hr hr
fieldset.option-group.advanced-option(ng-show="task._edit._advanced") .form-group
legend.option-title=env.t('repeat')
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')
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')
=env.t('dayOfMonth')
label
input(type="radio", ng-model='task._edit.repeatsOn', value='dayOfWeek')
=env.t('dayOfWeek')
.form-group
legend.option-title=env.t('summary')
div {{summary}}
hr
fieldset.option-group.advanced-option(ng-show="task._edit._advanced")
legend.option-title legend.option-title
a.hint.priority-multiplier-help(href='http://habitica.wikia.com/wiki/Difficulty', target='_blank', popover-title=env.t('difficultyHelpTitle'), popover-trigger='mouseenter', popover=env.t('difficultyHelpContent'))=env.t('difficulty') a.hint.priority-multiplier-help(href='http://habitica.wikia.com/wiki/Difficulty', target='_blank', popover-title=env.t('difficultyHelpTitle'), popover-trigger='mouseenter', popover=env.t('difficultyHelpContent'))=env.t('difficulty')
ul.priority-multiplier ul.priority-multiplier

View File

@@ -1,21 +1,22 @@
.form-group //- .form-group
legend.option-title=env.t('repeat') //- legend.option-title=env.t('repeat')
select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)') //- select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
option(value='weekly')=env.t('repeatWeek') //- option(value='weekly')=env.t('repeatWeek')
option(value='daily')=env.t('repeatDays') //- option(value='daily')=env.t('repeatDays')
legend.option-title legend.option-title
span.hint(popover-trigger='mouseenter', popover-title=env.t('repeatHelpTitle'), span.hint(popover-trigger='mouseenter', popover-title=env.t('repeatHelpTitle'),
popover='{{env.t(task._edit.frequency + "RepeatHelpContent")}}')=env.t('repeatEvery') popover='{{env.t(task._edit.frequency + "RepeatHelpContent")}}')=env.t('repeatEvery')
// If frequency is daily // If frequency is daily
ng-form.form-group(name='everyX', ng-if='task._edit.frequency=="daily"') ng-form.form-group(name='everyX')
.input-group .input-group
input.form-control(type='number', ng-model='task._edit.everyX', min='0', ng-disabled='!canEdit(task)', required) input.form-control(type='number', ng-model='task._edit.everyX', min='0', ng-disabled='!canEdit(task)', required)
span.input-group-addon {{task._edit.everyX == 1 ? env.t('day') : env.t('days')}} span.input-group-addon {{repeatSuffix}}
// If frequency is weekly // If frequency is weekly
ng-form.form-group(ng-if='task._edit.frequency=="weekly"') .form-group(ng-if='task._edit.frequency=="weekly"')
legend.option-title=env.t('onDays')
ul.repeat-days ul.repeat-days
// note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding // note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding
mixin dayOfWeek(day, num) mixin dayOfWeek(day, num)