mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
rever new history preening
This commit is contained in:
2
common/dist/sprites/habitrpg-shared.css
vendored
2
common/dist/sprites/habitrpg-shared.css
vendored
File diff suppressed because one or more lines are too long
@@ -7,10 +7,9 @@ import {
|
||||
MAX_LEVEL,
|
||||
MAX_STAT_POINTS,
|
||||
} from './constants';
|
||||
import { preenHistory, preenUserHistory } from './preenUserHistory';
|
||||
import * as statHelpers from './statHelpers';
|
||||
|
||||
var $w, _, api, content, i18n, moment, sortOrder,
|
||||
var $w, _, api, content, i18n, preenHistory, moment, sortOrder,
|
||||
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
|
||||
|
||||
moment = require('moment');
|
||||
@@ -32,8 +31,6 @@ api.maxHealth = MAX_HEALTH;
|
||||
api.tnl = statHelpers.toNextLevel;
|
||||
api.diminishingReturns = statHelpers.diminishingReturns;
|
||||
|
||||
api.preenHistory = preenHistory;
|
||||
|
||||
$w = api.$w = function(s) {
|
||||
return s.split(' ');
|
||||
};
|
||||
@@ -82,6 +79,47 @@ api.planGemLimits = {
|
||||
convCap: 25
|
||||
};
|
||||
|
||||
/*
|
||||
Preen history for users with > 7 history entries
|
||||
This takes an infinite array of single day entries [day day day day day...], and turns it into a condensed array
|
||||
of averages, condensing more the further back in time we go. Eg, 7 entries each for last 7 days; 1 entry each week
|
||||
of this month; 1 entry for each month of this year; 1 entry per previous year: [day*7 week*4 month*12 year*infinite]
|
||||
*/
|
||||
|
||||
preenHistory = function(history) {
|
||||
var newHistory, preen, thisMonth;
|
||||
history = _.filter(history, function(h) {
|
||||
return !!h;
|
||||
});
|
||||
newHistory = [];
|
||||
preen = function(amount, groupBy) {
|
||||
var groups;
|
||||
groups = _.chain(history).groupBy(function(h) {
|
||||
return moment(h.date).format(groupBy);
|
||||
}).sortBy(function(h, k) {
|
||||
return k;
|
||||
}).value();
|
||||
groups = groups.slice(-amount);
|
||||
groups.pop();
|
||||
return _.each(groups, function(group) {
|
||||
newHistory.push({
|
||||
date: moment(group[0].date).toDate(),
|
||||
value: _.reduce(group, (function(m, obj) {
|
||||
return m + obj.value;
|
||||
}), 0) / group.length
|
||||
});
|
||||
return true;
|
||||
});
|
||||
};
|
||||
preen(50, "YYYY");
|
||||
preen(moment().format('MM'), "YYYYMM");
|
||||
thisMonth = moment().format('YYYYMM');
|
||||
newHistory = newHistory.concat(_.filter(history, function(h) {
|
||||
return moment(h.date).format('YYYYMM') === thisMonth;
|
||||
}));
|
||||
return newHistory;
|
||||
};
|
||||
|
||||
/*
|
||||
Preen 3-day past-completed To-Dos from Angular & mobile app
|
||||
*/
|
||||
@@ -2547,7 +2585,15 @@ api.wrap = function(user, main) {
|
||||
date: now,
|
||||
value: expTally
|
||||
});
|
||||
preenUserHistory(user);
|
||||
if (!((ref1 = user.purchased) != null ? (ref2 = ref1.plan) != null ? ref2.customerId : void 0 : void 0)) {
|
||||
user.fns.preenUserHistory();
|
||||
if (typeof user.markModified === "function") {
|
||||
user.markModified('history');
|
||||
}
|
||||
if (typeof user.markModified === "function") {
|
||||
user.markModified('dailys');
|
||||
}
|
||||
}
|
||||
user.stats.buffs = perfect ? ((base3 = user.achievements).perfect != null ? base3.perfect : base3.perfect = 0, user.achievements.perfect++, lvlDiv2 = Math.ceil(api.capByLevel(user.stats.lvl) / 2), {
|
||||
str: lvlDiv2,
|
||||
int: lvlDiv2,
|
||||
@@ -2592,6 +2638,28 @@ api.wrap = function(user, main) {
|
||||
}
|
||||
return _progress;
|
||||
},
|
||||
preenUserHistory: function(minHistLen) {
|
||||
if (minHistLen == null) {
|
||||
minHistLen = 7;
|
||||
}
|
||||
_.each(user.habits.concat(user.dailys), function(task) {
|
||||
var ref;
|
||||
if (((ref = task.history) != null ? ref.length : void 0) > minHistLen) {
|
||||
task.history = preenHistory(task.history);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
_.defaults(user.history, {
|
||||
todos: [],
|
||||
exp: []
|
||||
});
|
||||
if (user.history.exp.length > minHistLen) {
|
||||
user.history.exp = preenHistory(user.history.exp);
|
||||
}
|
||||
if (user.history.todos.length > minHistLen) {
|
||||
return user.history.todos = preenHistory(user.history.todos);
|
||||
}
|
||||
},
|
||||
ultimateGear: function() {
|
||||
var base, owned;
|
||||
owned = typeof window !== "undefined" && window !== null ? user.items.gear.owned : user.items.gear.owned.toObject();
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
// Aggregate entries
|
||||
function _aggregate (history, aggregateBy) {
|
||||
return _.chain(history)
|
||||
.groupBy(entry => { // group entries by aggregateBy
|
||||
return moment(entry.date).format(aggregateBy);
|
||||
})
|
||||
.sortBy((entry, key) => key) // sort by date
|
||||
.map(entries => {
|
||||
return {
|
||||
date: Number(entries[0].date),
|
||||
value: _.reduce(entries, (previousValue, entry) => {
|
||||
return previousValue + entry.value;
|
||||
}, 0) / entries.length,
|
||||
};
|
||||
})
|
||||
.value();
|
||||
}
|
||||
|
||||
/* Preen an array of history entries
|
||||
|
||||
Free users:
|
||||
- 1 value for each day of the past 60 days (no compression)
|
||||
- 1 value each month for the previous 10 months
|
||||
- 1 value each year for the previous years
|
||||
|
||||
Subscribers and challenges:
|
||||
- 1 value for each day of the past 365 days (no compression)
|
||||
- 1 value each month for the previous 12 months
|
||||
- 1 value each year for the previous years
|
||||
*/
|
||||
export function preenHistory (history, isSubscribed, timezoneOffset) {
|
||||
history = _.filter(history, historyEntry => Boolean(historyEntry)); // Filter missing entries
|
||||
let now = timezoneOffset ? moment().zone(timezoneOffset) : moment();
|
||||
// Date after which to begin compressing data
|
||||
let cutOff = now.subtract(isSubscribed ? 365 : 60, 'days').startOf('day');
|
||||
|
||||
// Keep uncompressed entries (modifies history)
|
||||
let newHistory = _.remove(history, entry => {
|
||||
let date = moment(entry.date);
|
||||
return date.isSame(cutOff) || date.isAfter(cutOff);
|
||||
});
|
||||
|
||||
// Date after which to begin compressing data by year
|
||||
let monthsCutOff = cutOff.subtract(isSubscribed ? 12 : 10, 'months').startOf('day');
|
||||
let aggregateByMonth = _.remove(history, entry => {
|
||||
let date = moment(entry.date);
|
||||
return date.isSame(monthsCutOff) || date.isAfter(monthsCutOff);
|
||||
});
|
||||
// Aggregate remaining entries by month and year
|
||||
if (aggregateByMonth.length > 0) newHistory.unshift(..._aggregate(aggregateByMonth, 'YYYYMM'));
|
||||
if (history.length > 0) newHistory.unshift(..._aggregate(history, 'YYYY'));
|
||||
|
||||
return newHistory;
|
||||
}
|
||||
|
||||
// Preen history for users and tasks. This code runs only on the server.
|
||||
export function preenUserHistory (user) {
|
||||
let isSubscribed = user.purchased && user.purchased.plan && user.purchased.plan.customerId;
|
||||
let minHistoryLength = isSubscribed ? 365 : 60;
|
||||
|
||||
function _processTask (task, index) {
|
||||
if (task.history && task.history.length > minHistoryLength) {
|
||||
task.history = preenHistory(task.history, isSubscribed, user.preferences.timezoneOffset);
|
||||
if (user.markModified) user.markModified(`${task.type}s.${index}.history`);
|
||||
}
|
||||
}
|
||||
|
||||
_.each(user.habits, _processTask);
|
||||
_.each(user.dailys, _processTask);
|
||||
|
||||
_.defaults(user.history, {
|
||||
todos: [],
|
||||
exp: [],
|
||||
});
|
||||
|
||||
if (user.history.exp.length > minHistoryLength) {
|
||||
user.history.exp = preenHistory(user.history.exp, isSubscribed, user.preferences.timezoneOffset);
|
||||
user.markModified('history.exp');
|
||||
}
|
||||
|
||||
if (user.history.todos.length > minHistoryLength) {
|
||||
user.history.todos = preenHistory(user.history.todos, isSubscribed, user.preferences.timezoneOffset);
|
||||
user.markModified('history.todos');
|
||||
}
|
||||
}
|
||||
@@ -990,6 +990,124 @@ describe('Cron', () => {
|
||||
expect(beforeTasks).to.eql(afterTasks);
|
||||
});
|
||||
|
||||
describe('preening', () => {
|
||||
beforeEach(function () {
|
||||
this.clock = sinon.useFakeTimers(Date.parse('2013-11-20'), 'Date');
|
||||
});
|
||||
afterEach(function () {
|
||||
return this.clock.restore();
|
||||
});
|
||||
|
||||
it('should preen user history', function () {
|
||||
let ref = beforeAfter({
|
||||
daysAgo: 1,
|
||||
});
|
||||
let after = ref.after;
|
||||
|
||||
let history = [
|
||||
{
|
||||
date: '09/01/2012',
|
||||
value: 0,
|
||||
}, {
|
||||
date: '10/01/2012',
|
||||
value: 0,
|
||||
}, {
|
||||
date: '11/01/2012',
|
||||
value: 2,
|
||||
}, {
|
||||
date: '12/01/2012',
|
||||
value: 2,
|
||||
}, {
|
||||
date: '01/01/2013',
|
||||
value: 1,
|
||||
}, {
|
||||
date: '01/15/2013',
|
||||
value: 3,
|
||||
}, {
|
||||
date: '02/01/2013',
|
||||
value: 2,
|
||||
}, {
|
||||
date: '02/15/2013',
|
||||
value: 4,
|
||||
}, {
|
||||
date: '03/01/2013',
|
||||
value: 3,
|
||||
}, {
|
||||
date: '03/15/2013',
|
||||
value: 5,
|
||||
}, {
|
||||
date: '04/01/2013',
|
||||
value: 4,
|
||||
}, {
|
||||
date: '04/15/2013',
|
||||
value: 6,
|
||||
}, {
|
||||
date: '05/01/2013',
|
||||
value: 5,
|
||||
}, {
|
||||
date: '05/15/2013',
|
||||
value: 7,
|
||||
}, {
|
||||
date: '06/01/2013',
|
||||
value: 6,
|
||||
}, {
|
||||
date: '06/15/2013',
|
||||
value: 8,
|
||||
}, {
|
||||
date: '07/01/2013',
|
||||
value: 7,
|
||||
}, {
|
||||
date: '07/15/2013',
|
||||
value: 9,
|
||||
}, {
|
||||
date: '08/01/2013',
|
||||
value: 8,
|
||||
}, {
|
||||
date: '08/15/2013',
|
||||
value: 10,
|
||||
}, {
|
||||
date: '09/01/2013',
|
||||
value: 9,
|
||||
}, {
|
||||
date: '09/15/2013',
|
||||
value: 11,
|
||||
}, {
|
||||
date: '010/01/2013',
|
||||
value: 10,
|
||||
}, {
|
||||
date: '010/15/2013',
|
||||
value: 12,
|
||||
}, {
|
||||
date: '011/01/2013',
|
||||
value: 12,
|
||||
}, {
|
||||
date: '011/02/2013',
|
||||
value: 13,
|
||||
}, {
|
||||
date: '011/03/2013',
|
||||
value: 14,
|
||||
}, {
|
||||
date: '011/04/2013',
|
||||
value: 15,
|
||||
},
|
||||
];
|
||||
|
||||
after.history = {
|
||||
exp: _.cloneDeep(history),
|
||||
todos: _.cloneDeep(history),
|
||||
};
|
||||
after.habits[0].history = _.cloneDeep(history);
|
||||
after.fns.cron();
|
||||
after.history.exp.pop();
|
||||
after.history.todos.pop();
|
||||
_.each([after.history.exp, after.history.todos, after.habits[0].history], function (arr) {
|
||||
expect(_.map(arr, (x) => {
|
||||
return x.value;
|
||||
})).to.eql([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Todos', () => {
|
||||
it('1 day missed', () => {
|
||||
let ref = beforeAfter({
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { preenHistory } from '../../common/script/preenUserHistory';
|
||||
import moment from 'moment';
|
||||
import sinon from 'sinon'; // eslint-disable-line no-shadow
|
||||
|
||||
function generateHistory (days) {
|
||||
let history = [];
|
||||
let now = Number(moment().toDate());
|
||||
|
||||
while (days > 0) {
|
||||
history.push({
|
||||
value: days,
|
||||
date: Number(moment(now).subtract(days, 'days').toDate()),
|
||||
});
|
||||
days--;
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
describe('preenHistory', () => {
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
// Replace system clocks so we can get predictable results
|
||||
clock = sinon.useFakeTimers(Number(moment('2013-10-20').zone(0).startOf('day').toDate()), 'Date');
|
||||
});
|
||||
afterEach(() => {
|
||||
return clock.restore();
|
||||
});
|
||||
|
||||
it('does not modify history if all entries are more recent than cutoff (free users)', () => {
|
||||
let h = generateHistory(60);
|
||||
expect(preenHistory(_.cloneDeep(h), false, 0)).to.eql(h);
|
||||
});
|
||||
|
||||
it('does not modify history if all entries are more recent than cutoff (subscribers)', () => {
|
||||
let h = generateHistory(365);
|
||||
expect(preenHistory(_.cloneDeep(h), true, 0)).to.eql(h);
|
||||
});
|
||||
|
||||
it('does aggregate data in monthly entries before cutoff (free users)', () => {
|
||||
let h = generateHistory(81); // Jumps to July
|
||||
let preened = preenHistory(_.cloneDeep(h), false, 0);
|
||||
expect(preened.length).to.eql(62); // Keeps 60 days + 2 entries per august and july
|
||||
});
|
||||
|
||||
it('does aggregate data in monthly entries before cutoff (subscribers)', () => {
|
||||
let h = generateHistory(396); // Jumps to September 2012
|
||||
let preened = preenHistory(_.cloneDeep(h), true, 0);
|
||||
expect(preened.length).to.eql(367); // Keeps 365 days + 2 entries per october and september
|
||||
});
|
||||
|
||||
it('does aggregate data in monthly and yearly entries before cutoff (free users)', () => {
|
||||
let h = generateHistory(731); // Jumps to October 21 2012
|
||||
let preened = preenHistory(_.cloneDeep(h), false, 0);
|
||||
expect(preened.length).to.eql(73); // Keeps 60 days + 11 montly entries and 2 yearly entry for 2011 and 2012
|
||||
});
|
||||
|
||||
it('does aggregate data in monthly and yearly entries before cutoff (subscribers)', () => {
|
||||
let h = generateHistory(1031); // Jumps to October 21 2012
|
||||
let preened = preenHistory(_.cloneDeep(h), true, 0);
|
||||
expect(preened.length).to.eql(380); // Keeps 365 days + 13 montly entries and 2 yearly entries for 2011 and 2010
|
||||
});
|
||||
|
||||
it('correctly aggregates values', () => {
|
||||
let h = generateHistory(63); // Compress last 3 days
|
||||
let preened = preenHistory(_.cloneDeep(h), false, 0);
|
||||
expect(preened[0].value).to.eql((61 + 62 + 63) / 3);
|
||||
});
|
||||
});
|
||||
@@ -136,27 +136,7 @@ api.score = function(req, res, next) {
|
||||
|
||||
t.value += delta;
|
||||
if (t.type == 'habit' || t.type == 'daily') {
|
||||
var tIndex = chal[`${t.type}s`].indexOf(t);
|
||||
|
||||
// Add only one history entry per day
|
||||
if (moment(t.history[t.history.length - 1].date).isSame(new Date, 'day')) {
|
||||
t.history[t.history.length - 1] = {
|
||||
value: t.value,
|
||||
date: +(new Date),
|
||||
};
|
||||
chal.markModified(`${t.type}s.${tIndex}.history`);
|
||||
} else {
|
||||
t.history.push({
|
||||
date: +(new Date),
|
||||
value: t.value
|
||||
});
|
||||
|
||||
// Only preen task history once a day when the task is scored first
|
||||
if (t.history.length > 365) {
|
||||
t.history = shared.preenHistory(t.history, true); // true means the challenge will retain as much entries as a subscribed user
|
||||
chal.markModified(`${t.type}s.${tIndex}.history`); // Setting habits/dailys as modified because we don't know the index of the task
|
||||
}
|
||||
}
|
||||
t.history.push({value: t.value, date: +new Date});
|
||||
}
|
||||
chal.save();
|
||||
clearMemory();
|
||||
|
||||
Reference in New Issue
Block a user