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:
Bart Enkelaar
2020-07-25 13:22:41 +02:00
committed by GitHub
parent c10b9b7993
commit 234258b41e
25 changed files with 413 additions and 141 deletions

9
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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({

View File

@@ -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'],
});
});

View File

@@ -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;

View File

@@ -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', [
{

View 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);
});
});

View 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);
});
});
});

View File

@@ -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(),

View File

@@ -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);
});

View File

@@ -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');

View File

@@ -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,
});
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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',

View File

@@ -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

View 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);
}

View File

@@ -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 = {

View File

@@ -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()

View File

@@ -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) {

View File

@@ -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 += `

View File

@@ -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;

View File

@@ -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');
}
}

View File

@@ -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,
});

View File

@@ -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) {