mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-15 13:47:33 +01:00
Move from deprecated moment#zone to moment#utcOffset (#12207)
* Issue 10209 - Remove read usages of zone * Issue 10209 - Add coverage on daysSince and startOfDay cron utility functions * Issue 10209 - Add unit test for daysUserHasMissed method * Issue 10209 - Remove usages of deprecated `moment.js#zone` method. * Issue 10209 - Add helper function to centralise logic Also simplify timezoneOffsetToUtc function in site.vue * Issue 10209 - Also add getUtcOffset as method on user Co-authored-by: Matteo Pagliazzi <matteopagliazzi@gmail.com>
This commit is contained in:
9
package-lock.json
generated
9
package-lock.json
generated
@@ -3008,6 +3008,15 @@
|
||||
"check-error": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"chai-moment": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chai-moment/-/chai-moment-0.1.0.tgz",
|
||||
"integrity": "sha1-SpFoDPo6dc/aGEULK6tltIyQJAE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"moment": "^2.10.6"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"axios": "^0.19.2",
|
||||
"chai": "^4.1.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-moment": "^0.1.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"expect.js": "^0.3.1",
|
||||
|
||||
@@ -42,13 +42,13 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('updates user.preferences.timezoneOffsetAtLastCron', () => {
|
||||
const timezoneOffsetFromUserPrefs = 1;
|
||||
const timezoneUtcOffsetFromUserPrefs = -1;
|
||||
|
||||
cron({
|
||||
user, tasksByType, daysMissed, analytics, timezoneOffsetFromUserPrefs,
|
||||
user, tasksByType, daysMissed, analytics, timezoneUtcOffsetFromUserPrefs,
|
||||
});
|
||||
|
||||
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(timezoneOffsetFromUserPrefs);
|
||||
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1);
|
||||
});
|
||||
|
||||
it('resets user.items.lastDrop.count', () => {
|
||||
@@ -240,7 +240,7 @@ describe('cron', () => {
|
||||
user1.purchased.plan.consecutive.gemCapExtra = 0;
|
||||
|
||||
it('does not increment consecutive benefits after the first month', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
// Add 1 month to simulate what happens a month after the subscription was created.
|
||||
@@ -256,7 +256,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits after the second month', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
// Add 1 month to simulate what happens a month after the subscription was created.
|
||||
@@ -272,7 +272,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits after the third month', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
// Add 1 month to simulate what happens a month after the subscription was created.
|
||||
@@ -288,7 +288,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits after the fourth month', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
// Add 1 month to simulate what happens a month after the subscription was created.
|
||||
@@ -304,7 +304,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(10, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -339,7 +339,7 @@ describe('cron', () => {
|
||||
user3.purchased.plan.consecutive.gemCapExtra = 5;
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -352,7 +352,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the middle of the period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -365,7 +365,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -378,7 +378,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the second paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -391,7 +391,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(5, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(5, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -404,7 +404,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the final month of the second period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(6, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -417,7 +417,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the third paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -430,7 +430,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(10, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -465,7 +465,7 @@ describe('cron', () => {
|
||||
user6.purchased.plan.consecutive.gemCapExtra = 10;
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -478,7 +478,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(6, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -491,7 +491,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the second paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -504,7 +504,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the third paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -517,7 +517,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(19, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(19, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -552,7 +552,7 @@ describe('cron', () => {
|
||||
user12.purchased.plan.consecutive.gemCapExtra = 20;
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -565,7 +565,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(12, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(12, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -578,7 +578,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the second paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -591,7 +591,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the third paid period has started', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(25, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(25, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -604,7 +604,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(37, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(37, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -641,7 +641,7 @@ describe('cron', () => {
|
||||
user3g.purchased.plan.consecutive.gemCapExtra = 5;
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the gift subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -654,7 +654,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the second month of the gift subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -667,7 +667,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the third month of the gift subscription', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -680,7 +680,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the month after the gift subscription has ended', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -717,7 +717,7 @@ describe('cron', () => {
|
||||
user6x.purchased.plan.consecutive.gemCapExtra = 15;
|
||||
|
||||
it('increments consecutive benefits in the first month since the fix for #4819 goes live', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -730,7 +730,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the second month after the fix goes live', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -743,7 +743,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the third month after the fix goes live', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
@@ -756,7 +756,7 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('increments consecutive benefits in the seventh month after the fix goes live', () => {
|
||||
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
cron({
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('preenHistory', () => {
|
||||
beforeEach(() => {
|
||||
// Replace system clocks so we can get predictable results
|
||||
clock = sinon.useFakeTimers({
|
||||
now: Number(moment('2013-10-20').zone(0).startOf('day').toDate()),
|
||||
now: Number(moment('2013-10-20').utcOffset(0).startOf('day').toDate()),
|
||||
toFake: ['Date'],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -761,7 +761,7 @@ describe('User Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('days missed', () => {
|
||||
describe('daysUserHasMissed', () => {
|
||||
// http://forbrains.co.uk/international_tools/earth_timezones
|
||||
let user;
|
||||
|
||||
@@ -769,24 +769,51 @@ describe('User Model', () => {
|
||||
user = new User();
|
||||
});
|
||||
|
||||
it('should not cron early when going back a timezone', () => {
|
||||
const yesterday = moment('2017-12-05T00:00:00.000-06:00'); // 11 pm on 4 Texas
|
||||
const timezoneOffset = moment().zone('-06:00').zone();
|
||||
user.lastCron = yesterday;
|
||||
user.preferences.timezoneOffset = timezoneOffset;
|
||||
it('correctly calculates days missed since lastCron', () => {
|
||||
const now = moment();
|
||||
user.lastCron = moment(now).subtract(5, 'days');
|
||||
|
||||
const today = moment('2017-12-06T00:00:00.000-06:00'); // 11 pm on 4 Texas
|
||||
const req = {};
|
||||
req.header = () => timezoneOffset + 60;
|
||||
const { daysMissed } = user.daysUserHasMissed(now);
|
||||
|
||||
const { daysMissed } = user.daysUserHasMissed(today, req);
|
||||
expect(daysMissed).to.eql(5);
|
||||
});
|
||||
|
||||
it('uses timezone from preferences to calculate days missed', () => {
|
||||
const now = moment('2017-07-08 01:00:00Z');
|
||||
user.lastCron = moment('2017-07-04 13:00:00Z');
|
||||
user.preferences.timezoneOffset = 120;
|
||||
|
||||
const { daysMissed } = user.daysUserHasMissed(now);
|
||||
|
||||
expect(daysMissed).to.eql(3);
|
||||
});
|
||||
|
||||
it('uses timezone at last cron to calculate days missed', () => {
|
||||
const now = moment('2017-09-08 13:00:00Z');
|
||||
user.lastCron = moment('2017-09-06 01:00:00+02:00');
|
||||
user.preferences.timezoneOffset = 0;
|
||||
user.preferences.timezoneOffsetAtLastCron = -120;
|
||||
|
||||
const { daysMissed } = user.daysUserHasMissed(now);
|
||||
|
||||
expect(daysMissed).to.eql(2);
|
||||
});
|
||||
|
||||
it('respects new timezone that drags time into same day', () => {
|
||||
user.lastCron = moment('2017-12-05T00:00:00.000-06:00');
|
||||
user.preferences.timezoneOffset = 360;
|
||||
const today = moment('2017-12-06T00:00:00.000-06:00');
|
||||
const requestWithMinus7Timezone = { header: () => 420 };
|
||||
|
||||
const { daysMissed } = user.daysUserHasMissed(today, requestWithMinus7Timezone);
|
||||
|
||||
expect(user.preferences.timezoneOffset).to.eql(420);
|
||||
expect(daysMissed).to.eql(0);
|
||||
});
|
||||
|
||||
it('should not cron early when going back a timezone with a custom day start', () => {
|
||||
const yesterday = moment('2017-12-05T02:00:00.000-08:00');
|
||||
const timezoneOffset = moment().zone('-08:00').zone();
|
||||
const timezoneOffset = 480;
|
||||
user.lastCron = yesterday;
|
||||
user.preferences.timezoneOffset = timezoneOffset;
|
||||
user.preferences.dayStart = 2;
|
||||
|
||||
@@ -153,12 +153,12 @@ describe('GET /tasks/user', () => {
|
||||
});
|
||||
|
||||
xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
|
||||
const timezone = 420;
|
||||
const timezoneOffset = 420;
|
||||
await user.update({
|
||||
'preferences.dayStart': 0,
|
||||
'preferences.timezoneOffset': timezone,
|
||||
'preferences.timezoneOffset': timezoneOffset,
|
||||
});
|
||||
const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day')
|
||||
const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day')
|
||||
.toISOString();
|
||||
await user.post('/tasks/user', [
|
||||
{
|
||||
@@ -180,12 +180,12 @@ describe('GET /tasks/user', () => {
|
||||
});
|
||||
|
||||
xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
|
||||
const timezone = 240;
|
||||
const timezoneOffset = 240;
|
||||
await user.update({
|
||||
'preferences.dayStart': 0,
|
||||
'preferences.timezoneOffset': timezone,
|
||||
'preferences.timezoneOffset': timezoneOffset,
|
||||
});
|
||||
const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day')
|
||||
const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day')
|
||||
.toISOString();
|
||||
await user.post('/tasks/user', [
|
||||
{
|
||||
@@ -207,12 +207,12 @@ describe('GET /tasks/user', () => {
|
||||
});
|
||||
|
||||
xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
|
||||
const timezone = 540;
|
||||
const timezoneOffset = 540;
|
||||
await user.update({
|
||||
'preferences.dayStart': 0,
|
||||
'preferences.timezoneOffset': timezone,
|
||||
'preferences.timezoneOffset': timezoneOffset,
|
||||
});
|
||||
const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day')
|
||||
const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day')
|
||||
.toISOString();
|
||||
await user.post('/tasks/user', [
|
||||
{
|
||||
|
||||
25
test/common/fns/getUtcOffset.test.js
Normal file
25
test/common/fns/getUtcOffset.test.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import getUtcOffset from '../../../website/common/script/fns/getUtcOffset';
|
||||
|
||||
describe('getUtcOffset', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = { preferences: {} };
|
||||
});
|
||||
|
||||
it('returns 0 when user.timezoneOffset is not set', () => {
|
||||
expect(getUtcOffset(user)).to.equal(0);
|
||||
});
|
||||
|
||||
it('returns 0 when user.timezoneOffset is zero', () => {
|
||||
user.preferences.timezoneOffset = 0;
|
||||
|
||||
expect(getUtcOffset(user)).to.equal(0);
|
||||
});
|
||||
|
||||
it('returns the opposite of user.timezoneOffset', () => {
|
||||
user.preferences.timezoneOffset = -10;
|
||||
|
||||
expect(getUtcOffset(user)).to.eql(10);
|
||||
});
|
||||
});
|
||||
184
test/common/libs/cron.test.js
Normal file
184
test/common/libs/cron.test.js
Normal file
@@ -0,0 +1,184 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import { startOfDay, daysSince } from '../../../website/common/script/cron';
|
||||
|
||||
function localMoment (timeString, utcOffset) {
|
||||
return moment(timeString).utcOffset(utcOffset, true);
|
||||
}
|
||||
|
||||
describe('cron utility functions', () => {
|
||||
describe('startOfDay', () => {
|
||||
it('is zero when no daystart configured', () => {
|
||||
const options = { now: moment('2020-02-02 09:30:00Z'), timezoneOffset: 0 };
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 00:00:00Z');
|
||||
});
|
||||
|
||||
it('is zero when negative daystart configured', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 09:30:00Z'),
|
||||
timezoneOffset: 0,
|
||||
daystart: -5,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 00:00:00Z');
|
||||
});
|
||||
|
||||
it('is zero when daystart over 24 is configured', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 09:30:00Z'),
|
||||
timezoneOffset: 0,
|
||||
daystart: 25,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 00:00:00Z');
|
||||
});
|
||||
|
||||
it('is equal to daystart o\'clock when daystart configured', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 09:30:00Z'),
|
||||
timezoneOffset: 0,
|
||||
dayStart: 5,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 05:00:00Z');
|
||||
});
|
||||
|
||||
it('is previous day daystart o\'clock when daystart is after current time', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 04:30:00Z'),
|
||||
timezoneOffset: 0,
|
||||
dayStart: 5,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-01 05:00:00Z');
|
||||
});
|
||||
|
||||
it('is daystart o\'clock when daystart is after current time due to timezone', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 04:30:00Z'),
|
||||
timezoneOffset: -120,
|
||||
dayStart: 5,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 05:00:00+02:00');
|
||||
});
|
||||
|
||||
it('returns in default timezone if no timezone defined', () => {
|
||||
const utcOffset = moment().utcOffset();
|
||||
const now = localMoment('2020-02-02 04:30:00', utcOffset).utc();
|
||||
|
||||
const result = startOfDay({ now });
|
||||
|
||||
expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset));
|
||||
});
|
||||
|
||||
it('returns in default timezone if timezone lower than -12:00', () => {
|
||||
const utcOffset = moment().utcOffset();
|
||||
const options = {
|
||||
now: localMoment('2020-02-02 17:30:00', utcOffset).utc(),
|
||||
timezoneOffset: 721,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset));
|
||||
});
|
||||
|
||||
it('returns in default timezone if timezone higher than +14:00', () => {
|
||||
const utcOffset = moment().utcOffset();
|
||||
const options = {
|
||||
now: localMoment('2020-02-02 07:32:25.376', utcOffset).utc(),
|
||||
timezoneOffset: -841,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset));
|
||||
});
|
||||
|
||||
it('returns in overridden timezone if override present', () => {
|
||||
const options = {
|
||||
now: moment('2020-02-02 13:30:27Z'),
|
||||
timezoneOffset: 0,
|
||||
timezoneUtcOffsetOverride: -240,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment('2020-02-02 00:00:00-04:00');
|
||||
});
|
||||
|
||||
it('returns start of yesterday if timezone difference carries it over datelines', () => {
|
||||
const offset = 300;
|
||||
const options = {
|
||||
now: moment('2020-02-02 04:30:00Z'),
|
||||
timezoneOffset: offset,
|
||||
};
|
||||
|
||||
const result = startOfDay(options);
|
||||
|
||||
expect(result).to.be.sameMoment(localMoment('2020-02-01', -offset));
|
||||
});
|
||||
});
|
||||
|
||||
describe('daysSince', () => {
|
||||
it('correctly calculates days between two dates', () => {
|
||||
const now = moment();
|
||||
const dayBeforeYesterday = moment(now).subtract({ days: 2 });
|
||||
|
||||
expect(daysSince(dayBeforeYesterday, { now })).to.equal(2);
|
||||
});
|
||||
|
||||
it('is one lower if current time is before dayStart', () => {
|
||||
const oneWeekAgoAtOnePm = moment().hour(13).subtract({ days: 7 });
|
||||
const thisMorningThreeAm = moment().hour(3);
|
||||
const options = {
|
||||
now: thisMorningThreeAm,
|
||||
dayStart: 6,
|
||||
};
|
||||
|
||||
const result = daysSince(oneWeekAgoAtOnePm, options);
|
||||
|
||||
expect(result).to.equal(6);
|
||||
});
|
||||
|
||||
it('is one higher if reference time is before dayStart and current time after dayStart', () => {
|
||||
const oneWeekAgoAtEightAm = moment().hour(8).subtract({ days: 7 });
|
||||
const todayAtFivePm = moment().hour(17);
|
||||
const options = {
|
||||
now: todayAtFivePm,
|
||||
dayStart: 11,
|
||||
};
|
||||
|
||||
const result = daysSince(oneWeekAgoAtEightAm, options);
|
||||
|
||||
expect(result).to.equal(8);
|
||||
});
|
||||
|
||||
// Variations in timezone configuration options are already covered by startOfDay tests.
|
||||
it('uses now in user timezone as configured in options', () => {
|
||||
const timezoneOffset = 120;
|
||||
const options = {
|
||||
now: moment('1989-11-09 02:53:00+01:00'),
|
||||
timezoneOffset,
|
||||
};
|
||||
|
||||
const result = daysSince(localMoment('1989-11-08', -timezoneOffset), options);
|
||||
|
||||
expect(result).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import taskDefaults from '../../../website/common/script/libs/taskDefaults';
|
||||
import getUtcOffset from '../../../website/common/script/fns/getUtcOffset';
|
||||
import { generateUser } from '../../helpers/common.helper';
|
||||
|
||||
describe('taskDefaults', () => {
|
||||
@@ -72,7 +73,7 @@ describe('taskDefaults', () => {
|
||||
|
||||
expect(task.startDate).to.eql(
|
||||
moment()
|
||||
.zone(user.preferences.timezoneOffset, 'hour')
|
||||
.utcOffset(getUtcOffset(user))
|
||||
.startOf('day')
|
||||
.subtract(1, 'day')
|
||||
.toDate(),
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'moment-recur';
|
||||
describe('shouldDo', () => {
|
||||
let day; let
|
||||
dailyTask;
|
||||
// Options is a mapping of user.preferences, therefor `timezoneOffset` still holds old zone
|
||||
// values instead of utcOffset values.
|
||||
let options = {};
|
||||
let nextDue = [];
|
||||
|
||||
@@ -80,17 +82,17 @@ describe('shouldDo', () => {
|
||||
|
||||
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')
|
||||
day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(6, 'hours')
|
||||
.toDate();
|
||||
dailyTask.startDate = moment().zone(options.timezoneOffset).startOf('day').toDate();
|
||||
dailyTask.startDate = moment().utcOffset(-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')
|
||||
day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(2, 'hours')
|
||||
.toDate();
|
||||
dailyTask.startDate = moment().zone(options.timezoneOffset).startOf('day').toDate();
|
||||
dailyTask.startDate = moment().utcOffset(-options.timezoneOffset).startOf('day').toDate();
|
||||
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||
});
|
||||
});
|
||||
@@ -112,14 +114,14 @@ describe('shouldDo', () => {
|
||||
|
||||
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')
|
||||
day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(6, 'hours')
|
||||
.toDate();
|
||||
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false if the user\'s current time is before Custom Day Start', () => {
|
||||
options.dayStart = 8;
|
||||
day = moment().zone(options.timezoneOffset).startOf('day').add(2, 'hours')
|
||||
day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(2, 'hours')
|
||||
.toDate();
|
||||
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||
});
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
//------------------------------
|
||||
global._ = require('lodash');
|
||||
global.chai = require('chai');
|
||||
chai.use(require('sinon-chai'));
|
||||
chai.use(require('chai-as-promised'));
|
||||
chai.use(require('chai-moment'));
|
||||
chai.use(require('sinon-chai'));
|
||||
|
||||
global.expect = chai.expect;
|
||||
global.sinon = require('sinon');
|
||||
|
||||
@@ -297,7 +297,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['isUserLoggedIn', 'browserTimezoneOffset', 'isUserLoaded', 'notificationsRemoved']),
|
||||
...mapState(['isUserLoggedIn', 'browserTimezoneUtcOffset', 'isUserLoaded', 'notificationsRemoved']),
|
||||
...mapState({ user: 'user.data' }),
|
||||
isStaticPage () {
|
||||
return this.$route.meta.requiresLogin === false;
|
||||
@@ -493,9 +493,10 @@ export default {
|
||||
this.hideLoadingScreen();
|
||||
|
||||
// Adjust the timezone offset
|
||||
if (this.user.preferences.timezoneOffset !== this.browserTimezoneOffset) {
|
||||
const browserTimezoneOffset = -this.browserTimezoneUtcOffset;
|
||||
if (this.user.preferences.timezoneOffset !== browserTimezoneOffset) {
|
||||
this.$store.dispatch('user:set', {
|
||||
'preferences.timezoneOffset': this.browserTimezoneOffset,
|
||||
'preferences.timezoneOffset': browserTimezoneOffset,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -559,6 +559,7 @@ import resetModal from './resetModal';
|
||||
import deleteModal from './deleteModal';
|
||||
import { SUPPORTED_SOCIAL_NETWORKS } from '@/../../common/script/constants';
|
||||
import changeClass from '@/../../common/script/ops/changeClass';
|
||||
import getUtcOffset from '@/../../common/script/fns/getUtcOffset';
|
||||
import notificationsMixin from '../../mixins/notifications';
|
||||
import sounds from '../../libs/sounds';
|
||||
import { buildAppleAuthUrl } from '../../libs/auth';
|
||||
@@ -616,17 +617,8 @@ export default {
|
||||
return ['off', ...this.content.audioThemes];
|
||||
},
|
||||
timezoneOffsetToUtc () {
|
||||
let offset = this.user.preferences.timezoneOffset;
|
||||
const sign = offset > 0 ? '-' : '+';
|
||||
|
||||
offset = Math.abs(offset) / 60;
|
||||
|
||||
const hour = Math.floor(offset);
|
||||
|
||||
const minutesInt = (offset - hour) * 60;
|
||||
const minutes = minutesInt < 10 ? `0${minutesInt}` : minutesInt;
|
||||
|
||||
return `UTC${sign}${hour}:${minutes}`;
|
||||
const offsetString = moment().utcOffset(getUtcOffset(this.user)).format('Z');
|
||||
return `UTC${offsetString}`;
|
||||
},
|
||||
dayStart () {
|
||||
return this.user.preferences.dayStart;
|
||||
|
||||
@@ -8,13 +8,14 @@ export function setUpAxios (AUTH_SETTINGS) { // eslint-disable-line import/prefe
|
||||
AUTH_SETTINGS = JSON.parse(AUTH_SETTINGS); // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
const browserTimezoneOffset = moment().zone();
|
||||
const browserTimezoneUtcOffset = moment().utcOffset();
|
||||
|
||||
if (AUTH_SETTINGS.auth && AUTH_SETTINGS.auth.apiId && AUTH_SETTINGS.auth.apiToken) {
|
||||
axios.defaults.headers.common['x-api-user'] = AUTH_SETTINGS.auth.apiId;
|
||||
axios.defaults.headers.common['x-api-key'] = AUTH_SETTINGS.auth.apiToken;
|
||||
|
||||
axios.defaults.headers.common['x-user-timezoneOffset'] = browserTimezoneOffset;
|
||||
// Communicate in "old" timezone variant for backwards compatibility
|
||||
axios.defaults.headers.common['x-user-timezoneOffset'] = -browserTimezoneUtcOffset;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ const IS_TEST = process.env.NODE_ENV === 'test'; // eslint-disable-line no-proce
|
||||
// before trying to load data
|
||||
let isUserLoggedIn = false;
|
||||
|
||||
// eg, 240 - this will be converted on server as -(offset/60)
|
||||
const browserTimezoneOffset = moment().zone();
|
||||
// eg, -240 - this will be converted on server as (offset/60)
|
||||
const browserTimezoneUtcOffset = moment().utcOffset();
|
||||
|
||||
axios.defaults.headers.common['x-client'] = 'habitica-web';
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function () {
|
||||
// store the timezone offset in case it's different than the one in
|
||||
// user.preferences.timezoneOffset and change it after the user is synced
|
||||
// in app.vue
|
||||
browserTimezoneOffset,
|
||||
browserTimezoneUtcOffset,
|
||||
tasks: asyncResourceFactory(), // user tasks
|
||||
// @TODO use asyncresource?
|
||||
completedTodosStatus: 'NOT_LOADED',
|
||||
|
||||
@@ -33,26 +33,29 @@ function sanitizeOptions (o) {
|
||||
const ref = Number(o.dayStart || 0);
|
||||
const dayStart = !Number.isNaN(ref) && ref >= 0 && ref <= 24 ? ref : 0;
|
||||
|
||||
let timezoneOffset;
|
||||
const timezoneOffsetDefault = Number(moment().zone());
|
||||
let timezoneUtcOffset;
|
||||
const timezoneUtcOffsetDefault = moment().utcOffset();
|
||||
|
||||
if (Number.isFinite(o.timezoneOffsetOverride)) {
|
||||
timezoneOffset = Number(o.timezoneOffsetOverride);
|
||||
if (Number.isFinite(o.timezoneUtcOffset)) {
|
||||
// Options were already sanitized
|
||||
timezoneUtcOffset = o.timezoneUtcOffset;
|
||||
} else if (Number.isFinite(o.timezoneUtcOffsetOverride)) {
|
||||
timezoneUtcOffset = o.timezoneUtcOffsetOverride;
|
||||
} else if (Number.isFinite(o.timezoneOffset)) {
|
||||
timezoneOffset = Number(o.timezoneOffset);
|
||||
timezoneUtcOffset = -o.timezoneOffset;
|
||||
} else {
|
||||
timezoneOffset = timezoneOffsetDefault;
|
||||
timezoneUtcOffset = timezoneUtcOffsetDefault;
|
||||
}
|
||||
if (timezoneOffset > 720 || timezoneOffset < -840) {
|
||||
// timezones range from -12 (offset +720) to +14 (offset -840)
|
||||
timezoneOffset = timezoneOffsetDefault;
|
||||
if (timezoneUtcOffset < -720 || timezoneUtcOffset > 840) {
|
||||
// timezones range from -12 (offset -720) to +14 (offset 840)
|
||||
timezoneUtcOffset = timezoneUtcOffsetDefault;
|
||||
}
|
||||
|
||||
const now = o.now ? moment(o.now).zone(timezoneOffset) : moment().zone(timezoneOffset);
|
||||
const now = moment(o.now).utcOffset(timezoneUtcOffset);
|
||||
// return a new object, we don't want to add "now" to user object
|
||||
return {
|
||||
dayStart,
|
||||
timezoneOffset,
|
||||
timezoneUtcOffset,
|
||||
now,
|
||||
};
|
||||
}
|
||||
@@ -81,7 +84,7 @@ export function startOfDay (options = {}) {
|
||||
const o = sanitizeOptions(options);
|
||||
const dayStart = moment(o.now).startOf('day').add({ hours: o.dayStart });
|
||||
|
||||
if (moment(o.now).hour() < o.dayStart) {
|
||||
if (o.now.hour() < o.dayStart) {
|
||||
dayStart.subtract({ days: 1 });
|
||||
}
|
||||
|
||||
@@ -119,7 +122,7 @@ export function shouldDo (day, dailyTask, options = {}) {
|
||||
// NB: The user's day start date has already been converted to the PREVIOUS
|
||||
// day's date if the time portion was before CDS.
|
||||
|
||||
const startDate = moment(dailyTask.startDate).zone(o.timezoneOffset).startOf('day');
|
||||
const startDate = moment(dailyTask.startDate).utcOffset(o.timezoneUtcOffset).startOf('day');
|
||||
|
||||
if (startDate > startOfDayWithCDSTime.startOf('day') && !options.nextDue) {
|
||||
return false; // Daily starts in the future
|
||||
|
||||
12
website/common/script/fns/getUtcOffset.js
Normal file
12
website/common/script/fns/getUtcOffset.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Converts from timezoneOffset (which corresponded to the value returned
|
||||
* from moment.js's deprecated `moment#zone` method) to timezoneUtcOffset.
|
||||
*
|
||||
* This is done with conversion instead of changing it in the database to
|
||||
* be backwards compatible with the database values and old clients.
|
||||
*
|
||||
* Not as a user method since it needs to work in the frontend as well.
|
||||
*/
|
||||
export default function getUtcOffset (user) {
|
||||
return -(user.preferences.timezoneOffset || 0);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import apiErrors from './errors/apiErrorMessages';
|
||||
import commonErrors from './errors/commonErrorMessages';
|
||||
import autoAllocate from './fns/autoAllocate';
|
||||
import crit from './fns/crit';
|
||||
import getUtcOffset from './fns/getUtcOffset';
|
||||
import handleTwoHanded from './fns/handleTwoHanded';
|
||||
import predictableRandom from './fns/predictableRandom';
|
||||
import randomDrop from './fns/randomDrop';
|
||||
@@ -151,6 +152,7 @@ api.fns = {
|
||||
resetGear,
|
||||
ultimateGear,
|
||||
updateStats,
|
||||
getUtcOffset,
|
||||
};
|
||||
|
||||
api.ops = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import defaults from 'lodash/defaults';
|
||||
import moment from 'moment';
|
||||
import getUtcOffset from '../fns/getUtcOffset';
|
||||
|
||||
// Even though Mongoose handles task defaults,
|
||||
// we want to make sure defaults are set on the client-side before
|
||||
@@ -66,7 +67,7 @@ export default function taskDefaults (task, user) {
|
||||
}
|
||||
|
||||
if (task.type === 'daily') {
|
||||
const now = moment().zone(user.preferences.timezoneOffset);
|
||||
const now = moment().utcOffset(getUtcOffset(user));
|
||||
const startOfDay = now.clone().startOf('day');
|
||||
const startOfDayWithCDSTime = startOfDay
|
||||
.clone()
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
import i18n from '../i18n';
|
||||
import updateStats from '../fns/updateStats';
|
||||
import crit from '../fns/crit';
|
||||
import getUtcOffset from '../fns/getUtcOffset';
|
||||
|
||||
import statsComputed from '../libs/statsComputed';
|
||||
import { checkOnboardingStatus } from '../libs/onboarding';
|
||||
|
||||
@@ -194,14 +196,14 @@ function _lastHistoryEntryWasToday (lastHistoryEntry, user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { timezoneOffset } = user.preferences;
|
||||
const timezoneUtcOffset = getUtcOffset(user);
|
||||
const { dayStart } = user.preferences;
|
||||
|
||||
// Adjust the last entry date according to the user's timezone and CDS
|
||||
const dateWithTimeZone = moment(lastHistoryEntry.date).zone(timezoneOffset);
|
||||
const dateWithTimeZone = moment(lastHistoryEntry.date).utcOffset(timezoneUtcOffset);
|
||||
if (dateWithTimeZone.hour() < dayStart) dateWithTimeZone.subtract(1, 'day');
|
||||
|
||||
return moment().zone(timezoneOffset).isSame(dateWithTimeZone, 'day');
|
||||
return moment().utcOffset(timezoneUtcOffset).isSame(dateWithTimeZone, 'day');
|
||||
}
|
||||
|
||||
function _updateLastHistoryEntry (lastHistoryEntry, task, direction, times) {
|
||||
|
||||
@@ -317,7 +317,7 @@ api.exportUserPrivateMessages = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
const { timezoneOffset } = user.preferences;
|
||||
const timezoneUtcOffset = user.getUtcOffset();
|
||||
const dateFormat = user.preferences.dateFormat.toUpperCase();
|
||||
const TO = res.t('to');
|
||||
const FROM = res.t('from');
|
||||
@@ -329,7 +329,7 @@ api.exportUserPrivateMessages = {
|
||||
inbox.forEach((message, index) => {
|
||||
const recipientLabel = message.sent ? TO : FROM;
|
||||
const messageUser = message.user;
|
||||
const timestamp = moment.utc(message.timestamp).zone(timezoneOffset).format(`${dateFormat} HH:mm:ss`);
|
||||
const timestamp = moment.utc(message.timestamp).utcOffset(timezoneUtcOffset).format(`${dateFormat} HH:mm:ss`);
|
||||
const text = md.render(message.text);
|
||||
const pageIndex = `(${index + 1}/${inbox.length})`;
|
||||
messages += `
|
||||
|
||||
@@ -172,7 +172,7 @@ function resetHabitCounters (user, tasksByType, now, daysMissed) {
|
||||
break;
|
||||
}
|
||||
const thatDay = moment(now)
|
||||
.zone(user.preferences.timezoneOffset + user.preferences.dayStart * 60)
|
||||
.utcOffset(user.getUtcOffset() - user.preferences.dayStart * 60)
|
||||
.subtract({ days: i });
|
||||
if (thatDay.day() === 1) {
|
||||
resetWeekly = true;
|
||||
@@ -281,14 +281,14 @@ function awardLoginIncentives (user) {
|
||||
// Perform various beginning-of-day reset actions.
|
||||
export function cron (options = {}) {
|
||||
const {
|
||||
user, tasksByType, analytics, now = new Date(), daysMissed, timezoneOffsetFromUserPrefs,
|
||||
user, tasksByType, analytics, now = new Date(), daysMissed, timezoneUtcOffsetFromUserPrefs,
|
||||
} = options;
|
||||
let _progress = { down: 0, up: 0, collectedItems: 0 };
|
||||
|
||||
// Record pre-cron values of HP and MP to show notifications later
|
||||
const beforeCronStats = _.pick(user.stats, ['hp', 'mp']);
|
||||
|
||||
user.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
|
||||
user.preferences.timezoneOffsetAtLastCron = -timezoneUtcOffsetFromUserPrefs;
|
||||
// User is only allowed a certain number of drops a day. This resets the count.
|
||||
if (user.items.lastDrop.count > 0) user.items.lastDrop.count = 0;
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
// Aggregate entries
|
||||
function _aggregate (history, aggregateBy, timezoneOffset, dayStart) {
|
||||
function _aggregate (history, aggregateBy, timezoneUtcOffset, dayStart) {
|
||||
return _.chain(history)
|
||||
.groupBy(entry => { // group entries by aggregateBy
|
||||
const entryDate = moment(entry.date).zone(timezoneOffset);
|
||||
const entryDate = moment(entry.date).utcOffset(timezoneUtcOffset);
|
||||
if (entryDate.hour() < dayStart) entryDate.subtract(1, 'day');
|
||||
return entryDate.format(aggregateBy);
|
||||
})
|
||||
@@ -35,16 +35,16 @@ Subscribers and challenges:
|
||||
- 1 value each month for the previous 12 months
|
||||
- 1 value each year for the previous years
|
||||
*/
|
||||
export function preenHistory (history, isSubscribed, timezoneOffset = 0, dayStart = 0) {
|
||||
export function preenHistory (history, isSubscribed, timezoneUtcOffset = 0, dayStart = 0) {
|
||||
// history = _.filter(history, historyEntry => Boolean(historyEntry)); // Filter missing entries
|
||||
const now = moment().zone(timezoneOffset);
|
||||
const now = moment().utcOffset(timezoneUtcOffset);
|
||||
// Date after which to begin compressing data
|
||||
const cutOff = now.subtract(isSubscribed ? 365 : 60, 'days').startOf('day');
|
||||
|
||||
// Keep uncompressed entries (modifies history and returns removed items)
|
||||
const newHistory = _.remove(history, entry => {
|
||||
if (!entry) return true; // sometimes entries are `null`
|
||||
const entryDate = moment(entry.date).zone(timezoneOffset);
|
||||
const entryDate = moment(entry.date).utcOffset(timezoneUtcOffset);
|
||||
if (entryDate.hour() < dayStart) entryDate.subtract(1, 'day');
|
||||
return entryDate.isSame(cutOff) || entryDate.isAfter(cutOff);
|
||||
});
|
||||
@@ -53,13 +53,13 @@ export function preenHistory (history, isSubscribed, timezoneOffset = 0, dayStar
|
||||
const monthsCutOff = cutOff.subtract(isSubscribed ? 12 : 10, 'months').startOf('day');
|
||||
const aggregateByMonth = _.remove(history, entry => {
|
||||
if (!entry) return true; // sometimes entries are `null`
|
||||
const entryDate = moment(entry.date).zone(timezoneOffset);
|
||||
const entryDate = moment(entry.date).utcOffset(timezoneUtcOffset);
|
||||
if (entryDate.hour() < dayStart) entryDate.subtract(1, 'day');
|
||||
return entryDate.isSame(monthsCutOff) || entryDate.isAfter(monthsCutOff);
|
||||
});
|
||||
// Aggregate remaining entries by month and year
|
||||
if (aggregateByMonth.length > 0) newHistory.unshift(..._aggregate(aggregateByMonth, 'YYYYMM', timezoneOffset, dayStart));
|
||||
if (history.length > 0) newHistory.unshift(..._aggregate(history, 'YYYY', timezoneOffset, dayStart));
|
||||
if (aggregateByMonth.length > 0) newHistory.unshift(..._aggregate(aggregateByMonth, 'YYYYMM', timezoneUtcOffset, dayStart));
|
||||
if (history.length > 0) newHistory.unshift(..._aggregate(history, 'YYYY', timezoneUtcOffset, dayStart));
|
||||
|
||||
return newHistory;
|
||||
}
|
||||
@@ -67,13 +67,13 @@ export function preenHistory (history, isSubscribed, timezoneOffset = 0, dayStar
|
||||
// Preen history for users and tasks.
|
||||
export function preenUserHistory (user, tasksByType) {
|
||||
const isSubscribed = user.isSubscribed();
|
||||
const { timezoneOffset } = user.preferences;
|
||||
const timezoneUtcOffset = user.getUtcOffset();
|
||||
const { dayStart } = user.preferences;
|
||||
const minHistoryLength = isSubscribed ? 365 : 60;
|
||||
|
||||
function _processTask (task) {
|
||||
if (task.history && task.history.length > minHistoryLength) {
|
||||
task.history = preenHistory(task.history, isSubscribed, timezoneOffset, dayStart);
|
||||
task.history = preenHistory(task.history, isSubscribed, timezoneUtcOffset, dayStart);
|
||||
task.markModified('history');
|
||||
}
|
||||
}
|
||||
@@ -82,12 +82,14 @@ export function preenUserHistory (user, tasksByType) {
|
||||
tasksByType.dailys.forEach(_processTask);
|
||||
|
||||
if (user.history.exp.length > minHistoryLength) {
|
||||
user.history.exp = preenHistory(user.history.exp, isSubscribed, timezoneOffset, dayStart);
|
||||
user.history.exp = preenHistory(user.history.exp, isSubscribed, timezoneUtcOffset, dayStart);
|
||||
user.markModified('history.exp');
|
||||
}
|
||||
|
||||
if (user.history.todos.length > minHistoryLength) {
|
||||
user.history.todos = preenHistory(user.history.todos, isSubscribed, timezoneOffset, dayStart);
|
||||
user.history.todos = preenHistory(
|
||||
user.history.todos, isSubscribed, timezoneUtcOffset, dayStart,
|
||||
);
|
||||
user.markModified('history.todos');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ async function cronAsync (req, res) {
|
||||
|
||||
user = await User.findOne({ _id: user._id }).exec();
|
||||
res.locals.user = user;
|
||||
const { daysMissed, timezoneOffsetFromUserPrefs } = user.daysUserHasMissed(now, req);
|
||||
const { daysMissed, timezoneUtcOffsetFromUserPrefs } = user.daysUserHasMissed(now, req);
|
||||
|
||||
await updateLastCron(user, now);
|
||||
|
||||
@@ -94,7 +94,7 @@ async function cronAsync (req, res) {
|
||||
now,
|
||||
daysMissed,
|
||||
analytics,
|
||||
timezoneOffsetFromUserPrefs,
|
||||
timezoneUtcOffsetFromUserPrefs,
|
||||
headers: req.headers,
|
||||
});
|
||||
|
||||
|
||||
@@ -347,35 +347,40 @@ schema.methods.cancelSubscription = async function cancelSubscription (options =
|
||||
return payments.cancelSubscription(options);
|
||||
};
|
||||
|
||||
schema.methods.getUtcOffset = function getUtcOffset () {
|
||||
return common.fns.getUtcOffset(this);
|
||||
};
|
||||
|
||||
schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
|
||||
// If the user's timezone has changed (due to travel or daylight savings),
|
||||
// cron can be triggered twice in one day, so we check for that and use
|
||||
// both timezones to work out if cron should run.
|
||||
// CDS = Custom Day Start time.
|
||||
let timezoneOffsetFromUserPrefs = this.preferences.timezoneOffset;
|
||||
const timezoneOffsetAtLastCron = Number.isFinite(this.preferences.timezoneOffsetAtLastCron)
|
||||
? this.preferences.timezoneOffsetAtLastCron
|
||||
: timezoneOffsetFromUserPrefs;
|
||||
let timezoneOffsetFromBrowser = typeof req.header === 'function' && Number(req.header('x-user-timezoneoffset'));
|
||||
timezoneOffsetFromBrowser = Number.isFinite(timezoneOffsetFromBrowser)
|
||||
? timezoneOffsetFromBrowser
|
||||
: timezoneOffsetFromUserPrefs;
|
||||
let timezoneUtcOffsetFromUserPrefs = this.getUtcOffset();
|
||||
const timezoneUtcOffsetAtLastCron = Number.isFinite(this.preferences.timezoneOffsetAtLastCron)
|
||||
? -this.preferences.timezoneOffsetAtLastCron
|
||||
: timezoneUtcOffsetFromUserPrefs;
|
||||
|
||||
let timezoneUtcOffsetFromBrowser = typeof req.header === 'function' && -Number(req.header('x-user-timezoneoffset'));
|
||||
timezoneUtcOffsetFromBrowser = Number.isFinite(timezoneUtcOffsetFromBrowser)
|
||||
? timezoneUtcOffsetFromBrowser
|
||||
: timezoneUtcOffsetFromUserPrefs;
|
||||
// NB: All timezone offsets can be 0, so can't use `... || ...` to apply non-zero defaults
|
||||
|
||||
if (timezoneOffsetFromBrowser !== timezoneOffsetFromUserPrefs) {
|
||||
if (timezoneUtcOffsetFromBrowser !== timezoneUtcOffsetFromUserPrefs) {
|
||||
// The user's browser has just told Habitica that the user's timezone has
|
||||
// changed so store and use the new zone.
|
||||
this.preferences.timezoneOffset = timezoneOffsetFromBrowser;
|
||||
timezoneOffsetFromUserPrefs = timezoneOffsetFromBrowser;
|
||||
this.preferences.timezoneOffset = -timezoneUtcOffsetFromBrowser;
|
||||
timezoneUtcOffsetFromUserPrefs = timezoneUtcOffsetFromBrowser;
|
||||
}
|
||||
|
||||
// How many days have we missed using the user's current timezone:
|
||||
let daysMissed = daysSince(this.lastCron, defaults({ now }, this.preferences));
|
||||
|
||||
if (timezoneOffsetAtLastCron !== timezoneOffsetFromUserPrefs) {
|
||||
if (timezoneUtcOffsetAtLastCron !== timezoneUtcOffsetFromUserPrefs) {
|
||||
// Give the user extra time based on the difference in timezones
|
||||
if (timezoneOffsetAtLastCron < timezoneOffsetFromUserPrefs) {
|
||||
const differenceBetweenTimezonesInMinutes = timezoneOffsetFromUserPrefs - timezoneOffsetAtLastCron; // eslint-disable-line max-len
|
||||
if (timezoneUtcOffsetAtLastCron > timezoneUtcOffsetFromUserPrefs) {
|
||||
const differenceBetweenTimezonesInMinutes = timezoneUtcOffsetAtLastCron - timezoneUtcOffsetFromUserPrefs; // eslint-disable-line max-len
|
||||
now = moment(now).subtract(differenceBetweenTimezonesInMinutes, 'minutes'); // eslint-disable-line no-param-reassign, max-len
|
||||
}
|
||||
|
||||
@@ -384,13 +389,13 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
|
||||
const daysMissedNewZone = daysMissed;
|
||||
const daysMissedOldZone = daysSince(this.lastCron, defaults({
|
||||
now,
|
||||
timezoneOffsetOverride: timezoneOffsetAtLastCron,
|
||||
timezoneUtcOffsetOverride: timezoneUtcOffsetAtLastCron,
|
||||
}, this.preferences));
|
||||
|
||||
if (timezoneOffsetAtLastCron < timezoneOffsetFromUserPrefs) {
|
||||
if (timezoneUtcOffsetAtLastCron > timezoneUtcOffsetFromUserPrefs) {
|
||||
// The timezone change was in the unsafe direction.
|
||||
// E.g., timezone changes from UTC+1 (offset -60) to UTC+0 (offset 0).
|
||||
// or timezone changes from UTC-4 (offset 240) to UTC-5 (offset 300).
|
||||
// E.g., timezone changes from UTC+1 (utcOffset 60) to UTC+0 (offset 0).
|
||||
// or timezone changes from UTC-4 (utcOffset -240) to UTC-5 (utcOffset -300).
|
||||
// Local time changed from, for example, 03:00 to 02:00.
|
||||
|
||||
if (daysMissedOldZone > 0 && daysMissedNewZone > 0) {
|
||||
@@ -419,20 +424,21 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
|
||||
// timezone interprets as being in today.
|
||||
|
||||
daysMissed = 0; // prevent cron running now
|
||||
const timezoneOffsetDiff = timezoneOffsetAtLastCron - timezoneOffsetFromUserPrefs;
|
||||
// e.g., for dangerous zone change: 240 - 300 = -60 or -660 - -600 = -60
|
||||
const timezoneOffsetDiff = timezoneUtcOffsetFromUserPrefs - timezoneUtcOffsetAtLastCron;
|
||||
// e.g., for dangerous zone change: -300 - -240 = -60 or 600 - 660= -60
|
||||
|
||||
this.lastCron = moment(this.lastCron).subtract(timezoneOffsetDiff, 'minutes');
|
||||
// NB: We don't change this.auth.timestamps.loggedin so that will still record
|
||||
// the time that the previous cron actually ran.
|
||||
// From now on we can ignore the old timezone:
|
||||
this.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
|
||||
// This is still timezoneOffset for backwards compatibility reasons.
|
||||
this.preferences.timezoneOffsetAtLastCron = -timezoneUtcOffsetAtLastCron;
|
||||
} else {
|
||||
// Both old and new timezones indicate that cron should
|
||||
// NOT run.
|
||||
daysMissed = 0; // prevent cron running now
|
||||
}
|
||||
} else if (timezoneOffsetAtLastCron > timezoneOffsetFromUserPrefs) {
|
||||
} else if (timezoneUtcOffsetAtLastCron < timezoneUtcOffsetFromUserPrefs) {
|
||||
daysMissed = daysMissedNewZone;
|
||||
// TODO: Either confirm that there is nothing that could possibly go wrong
|
||||
// here and remove the need for this else branch, or fix stuff.
|
||||
@@ -445,7 +451,7 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
return { daysMissed, timezoneOffsetFromUserPrefs };
|
||||
return { daysMissed, timezoneUtcOffsetFromUserPrefs };
|
||||
};
|
||||
|
||||
async function getUserGroupData (user) {
|
||||
|
||||
Reference in New Issue
Block a user