mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 22:27:26 +01:00
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:
3478
npm-shrinkwrap.json
generated
3478
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
@@ -78,6 +78,7 @@
|
||||
"merge-stream": "^1.0.0",
|
||||
"method-override": "^2.3.5",
|
||||
"moment": "^2.13.0",
|
||||
"moment-recur": "^1.0.5",
|
||||
"mongoose": "^4.7.1",
|
||||
"mongoose-id-autoinc": "~2013.7.14-4",
|
||||
"morgan": "^1.7.0",
|
||||
|
||||
@@ -496,6 +496,8 @@ describe('POST /tasks/user', () => {
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: now,
|
||||
daysOfMonth: [15],
|
||||
weeksOfMonth: [3],
|
||||
});
|
||||
|
||||
expect(task.userId).to.equal(user._id);
|
||||
@@ -504,6 +506,8 @@ describe('POST /tasks/user', () => {
|
||||
expect(task.type).to.eql('daily');
|
||||
expect(task.frequency).to.eql('daily');
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
310
test/common/shouldDo.test.js
Normal file
310
test/common/shouldDo.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -281,12 +281,108 @@ angular.module('habitrpg')
|
||||
}
|
||||
modalScope.cancelTaskEdit = cancelTaskEdit;
|
||||
|
||||
$rootScope.openModal('task-edit', {scope: modalScope })
|
||||
.result.catch(function() {
|
||||
cancelTaskEdit(task);
|
||||
});
|
||||
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() {
|
||||
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) {
|
||||
task._edit = undefined;
|
||||
task._editing = false;
|
||||
|
||||
@@ -147,6 +147,20 @@
|
||||
"taskApprovalHasBeenRequested": "Approval has been requested",
|
||||
"approvals": "Approvals",
|
||||
"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",
|
||||
"taskScoreNotesTooLong": "Task score notes must be less than 256 characters",
|
||||
"groupTasksByChallenge": "Group tasks by challenge title"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import 'moment-recur';
|
||||
|
||||
export const DAY_MAPPING = {
|
||||
0: 'su',
|
||||
@@ -17,6 +18,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.
|
||||
@@ -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?
|
||||
*/
|
||||
|
||||
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);
|
||||
let daysOfTheWeek = [];
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
if (dailyTask.frequency === 'daily') {
|
||||
if (!dailyTask.everyX) return false; // error condition
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -13,9 +13,10 @@ import i18n from './i18n';
|
||||
api.i18n = i18n;
|
||||
|
||||
// TODO under api.libs.cron?
|
||||
import { shouldDo, daysSince } from './cron';
|
||||
import { shouldDo, daysSince, DAY_MAPPING } from './cron';
|
||||
api.shouldDo = shouldDo;
|
||||
api.daysSince = daysSince;
|
||||
api.DAY_MAPPING = DAY_MAPPING;
|
||||
|
||||
import {
|
||||
MAX_HEALTH,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash' ;
|
||||
import _ from 'lodash';
|
||||
import analytics from './analyticsService';
|
||||
import {
|
||||
getUserInfo,
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from './email';
|
||||
import moment from 'moment';
|
||||
import { sendNotification as sendPushNotification } from './pushNotifications';
|
||||
import shared from '../../common' ;
|
||||
import shared from '../../common';
|
||||
import {
|
||||
model as Group,
|
||||
basicFields as basicGroupFields,
|
||||
|
||||
@@ -222,7 +222,7 @@ export let HabitSchema = new Schema(_.defaults({
|
||||
export let habit = Task.discriminator('habit', HabitSchema);
|
||||
|
||||
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
|
||||
startDate: {
|
||||
type: Date,
|
||||
@@ -240,6 +240,8 @@ export let DailySchema = new Schema(_.defaults({
|
||||
su: {type: Boolean, default: true},
|
||||
},
|
||||
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);
|
||||
export let daily = Task.discriminator('daily', DailySchema);
|
||||
|
||||
|
||||
@@ -27,8 +27,36 @@ div(ng-if='(task.type !== "reward") || (!obj.auth && obj.purchased && obj.purcha
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
.form-group
|
||||
legend.option-title=env.t('repeat')
|
||||
select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
|
||||
option(value='weekly')=env.t('repeatWeek')
|
||||
option(value='daily')=env.t('repeatDays')
|
||||
//- .form-group
|
||||
//- legend.option-title=env.t('repeat')
|
||||
//- select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
|
||||
//- option(value='weekly')=env.t('repeatWeek')
|
||||
//- option(value='daily')=env.t('repeatDays')
|
||||
|
||||
legend.option-title
|
||||
span.hint(popover-trigger='mouseenter', popover-title=env.t('repeatHelpTitle'),
|
||||
popover='{{env.t(task._edit.frequency + "RepeatHelpContent")}}')=env.t('repeatEvery')
|
||||
|
||||
// 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.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
|
||||
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
|
||||
// note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding
|
||||
mixin dayOfWeek(day, num)
|
||||
|
||||
Reference in New Issue
Block a user