mirror of
https://github.com/sudoxnym/habitica-self-host.git
synced 2026-04-14 19:47:03 +00:00
setup for async cron
This commit is contained in:
parent
baf689a8d1
commit
13a7019413
4 changed files with 200 additions and 127 deletions
|
|
@ -1,5 +1,7 @@
|
|||
// Generated by CoffeeScript 1.3.3
|
||||
var MODIFIER, content, cron, expModifier, helpers, hpModifier, model, moment, score, setModel, setupNotifications, tally, updateStats, user;
|
||||
var MODIFIER, async, content, cron, expModifier, helpers, hpModifier, model, moment, score, setModel, setupNotifications, updateStats, user;
|
||||
|
||||
async = require('async');
|
||||
|
||||
moment = require('moment');
|
||||
|
||||
|
|
@ -122,17 +124,20 @@ updateStats = function(stats) {
|
|||
}
|
||||
};
|
||||
|
||||
score = function(taskId, direction, cron) {
|
||||
var adjustvalue, delta, exp, hp, lvl, modified, money, num, sign, task, taskObj, taskPath, type, userObj, value, _ref, _ref1, _ref2, _ref3;
|
||||
if (cron == null) {
|
||||
cron = false;
|
||||
score = function(taskId, direction, options) {
|
||||
var adjustvalue, delta, exp, hp, lvl, modified, money, num, sign, task, taskObj, taskPath, type, userObj, value, _ref, _ref1, _ref2;
|
||||
if (options == null) {
|
||||
options = {
|
||||
cron: false,
|
||||
times: 1
|
||||
};
|
||||
}
|
||||
taskPath = "_user.tasks." + taskId;
|
||||
_ref = [model.at(taskPath), model.get(taskPath)], task = _ref[0], taskObj = _ref[1];
|
||||
_ref1 = [taskObj.type, taskObj.value], type = _ref1[0], value = _ref1[1];
|
||||
type = taskObj.type, value = taskObj.value;
|
||||
userObj = user.get();
|
||||
if (!task) {
|
||||
_ref2 = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp], money = _ref2[0], hp = _ref2[1], exp = _ref2[2];
|
||||
_ref1 = userObj.stats, money = _ref1.money, hp = _ref1.hp, exp = _ref1.exp;
|
||||
if (direction === "up") {
|
||||
modified = expModifier(1);
|
||||
money += modified;
|
||||
|
|
@ -157,6 +162,7 @@ score = function(taskId, direction, cron) {
|
|||
if (adjustvalue) {
|
||||
value += delta;
|
||||
}
|
||||
value *= options.times;
|
||||
if (type === 'habit') {
|
||||
if (taskObj.value !== value) {
|
||||
task.push('history', {
|
||||
|
|
@ -166,7 +172,7 @@ score = function(taskId, direction, cron) {
|
|||
}
|
||||
}
|
||||
task.set('value', value);
|
||||
_ref3 = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp, userObj.stats.lvl], money = _ref3[0], hp = _ref3[1], exp = _ref3[2], lvl = _ref3[3];
|
||||
_ref2 = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp, userObj.stats.lvl], money = _ref2[0], hp = _ref2[1], exp = _ref2[2], lvl = _ref2[3];
|
||||
if (type === 'reward') {
|
||||
money -= task.get('value');
|
||||
num = parseFloat(task.get('value')).toFixed(2);
|
||||
|
|
@ -175,7 +181,7 @@ score = function(taskId, direction, cron) {
|
|||
money = 0;
|
||||
}
|
||||
}
|
||||
if ((delta > 0 || (type === 'daily' || type === 'todo')) && !cron) {
|
||||
if ((delta > 0 || (type === 'daily' || type === 'todo')) && !options.cron) {
|
||||
modified = expModifier(delta);
|
||||
exp += modified;
|
||||
money += modified;
|
||||
|
|
@ -192,78 +198,79 @@ score = function(taskId, direction, cron) {
|
|||
};
|
||||
|
||||
cron = function() {
|
||||
var daysPassed, lastCron, today;
|
||||
var daysPassed, lastCron, tallyTask, tasks, today, todoTally;
|
||||
today = moment().sod();
|
||||
user.setNull('lastCron', today.toDate());
|
||||
lastCron = moment(user.get('lastCron'));
|
||||
daysPassed = today.diff(lastCron, 'days');
|
||||
if (daysPassed > 0) {
|
||||
user.set('lastCron', today.toDate());
|
||||
return _.times(daysPassed, function(n) {
|
||||
var tallyFor;
|
||||
tallyFor = lastCron.add('d', n);
|
||||
return tally(tallyFor);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
tally = function(momentDate) {
|
||||
var expTally, lvl, todoTally;
|
||||
todoTally = 0;
|
||||
_.each(user.get('tasks'), function(taskObj, taskId, list) {
|
||||
var absVal, completed, dayMapping, dueToday, repeat, task, type, value, _ref;
|
||||
if (taskObj.id == null) {
|
||||
return;
|
||||
}
|
||||
_ref = [taskObj.type, taskObj.value, taskObj.completed, taskObj.repeat], type = _ref[0], value = _ref[1], completed = _ref[2], repeat = _ref[3];
|
||||
task = user.at("tasks." + taskId);
|
||||
if (type === 'todo' || type === 'daily') {
|
||||
if (!completed) {
|
||||
dayMapping = {
|
||||
0: 'su',
|
||||
1: 'm',
|
||||
2: 't',
|
||||
3: 'w',
|
||||
4: 'th',
|
||||
5: 'f',
|
||||
6: 's',
|
||||
7: 'su'
|
||||
};
|
||||
dueToday = repeat && repeat[dayMapping[momentDate.day()]] === true;
|
||||
if (dueToday || type === 'todo') {
|
||||
score(taskId, 'down', true);
|
||||
todoTally = 0;
|
||||
tallyTask = function(taskObj, next) {
|
||||
var absVal, completed, dayMapping, daysFailed, dueToday, id, repeat, task, type, value;
|
||||
id = taskObj.id, type = taskObj.type, completed = taskObj.completed, repeat = taskObj.repeat;
|
||||
if (id == null) {
|
||||
return;
|
||||
}
|
||||
task = user.at("tasks." + id);
|
||||
if (type === 'todo' || type === 'daily') {
|
||||
if (!completed) {
|
||||
daysFailed = daysPassed;
|
||||
if (type === 'daily' && repeat) {
|
||||
dayMapping = {
|
||||
0: 'su',
|
||||
1: 'm',
|
||||
2: 't',
|
||||
3: 'w',
|
||||
4: 'th',
|
||||
5: 'f',
|
||||
6: 's',
|
||||
7: 'su'
|
||||
};
|
||||
dueToday = repeat && repeat[dayMapping[momentDate.day()]] === true;
|
||||
}
|
||||
score(taskId, 'down', {
|
||||
cron: true,
|
||||
times: daysFailed
|
||||
});
|
||||
}
|
||||
value = task.get('value');
|
||||
if (type === 'daily') {
|
||||
task.push("history", {
|
||||
date: today.toDate(),
|
||||
value: value
|
||||
});
|
||||
} else {
|
||||
absVal = completed ? Math.abs(value) : value;
|
||||
todoTally += absVal;
|
||||
}
|
||||
if (type === 'daily') {
|
||||
task.pass({
|
||||
cron: true
|
||||
}).set('completed', false);
|
||||
}
|
||||
}
|
||||
if (type === 'daily') {
|
||||
task.push("history", {
|
||||
date: new Date(momentDate),
|
||||
value: value
|
||||
});
|
||||
} else {
|
||||
absVal = completed ? Math.abs(value) : value;
|
||||
todoTally += absVal;
|
||||
return next();
|
||||
};
|
||||
tasks = _.toArray(user.get('tasks'));
|
||||
return async.forEach(tasks, tallyTask, function(err) {
|
||||
var expTally, lvl;
|
||||
user.push('history.todos', {
|
||||
date: today.toDate(),
|
||||
value: todoTally
|
||||
});
|
||||
expTally = user.get('stats.exp');
|
||||
lvl = 0;
|
||||
while (lvl < (user.get('stats.lvl') - 1)) {
|
||||
lvl++;
|
||||
expTally += (lvl * 100) / 5;
|
||||
}
|
||||
if (type === 'daily') {
|
||||
return task.pass({
|
||||
cron: true
|
||||
}).set('completed', false);
|
||||
}
|
||||
}
|
||||
});
|
||||
user.push('history.todos', {
|
||||
date: new Date(momentDate),
|
||||
value: todoTally
|
||||
});
|
||||
expTally = user.get('stats.exp');
|
||||
lvl = 0;
|
||||
while (lvl < (user.get('stats.lvl') - 1)) {
|
||||
lvl++;
|
||||
expTally += (lvl * 100) / 5;
|
||||
user.push('history.exp', {
|
||||
date: today.toDate(),
|
||||
value: expTally
|
||||
});
|
||||
return user.set('lastCron', today.toDate());
|
||||
});
|
||||
}
|
||||
return user.push('history.exp', {
|
||||
date: new Date(),
|
||||
value: expTally
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@
|
|||
"guid": "*",
|
||||
"node.extend": "*",
|
||||
"moment": "*",
|
||||
"stripe": "*"
|
||||
"stripe": "*",
|
||||
"async": "*",
|
||||
"lodash": "*"
|
||||
},
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
async = require 'async'
|
||||
moment = require 'moment'
|
||||
content = require './content'
|
||||
helpers = require './helpers'
|
||||
|
|
@ -107,17 +108,17 @@ updateStats = (stats) ->
|
|||
# {taskId} task you want to score
|
||||
# {direction} 'up' or 'down'
|
||||
# {cron} is this function being called by cron? (this will usually be false)
|
||||
score = (taskId, direction, cron=false) ->
|
||||
score = (taskId, direction, options={cron:false, times:1}) ->
|
||||
taskPath = "_user.tasks.#{taskId}"
|
||||
[task, taskObj] = [model.at(taskPath), model.get(taskPath)]
|
||||
[type, value] = [taskObj.type, taskObj.value]
|
||||
{type, value} = taskObj
|
||||
userObj = user.get()
|
||||
|
||||
|
||||
# up / down was called by itself, probably as REST from 3rd party service
|
||||
#FIXME handle this
|
||||
if !task
|
||||
[money, hp, exp] = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp]
|
||||
{money, hp, exp} = userObj.stats
|
||||
if (direction == "up")
|
||||
modified = expModifier(1)
|
||||
money += modified
|
||||
|
|
@ -140,6 +141,9 @@ score = (taskId, direction, cron=false) ->
|
|||
if (type == 'habit') and (taskObj.up==false or taskObj.down==false)
|
||||
adjustvalue = false
|
||||
value += delta if adjustvalue
|
||||
|
||||
# If multiple days have passed, multiply times days missed
|
||||
value *= options.times
|
||||
|
||||
if type == 'habit'
|
||||
# Add habit value to habit-history (if different)
|
||||
|
|
@ -160,7 +164,7 @@ score = (taskId, direction, cron=false) ->
|
|||
|
||||
# Add points to exp & money if positive delta
|
||||
# Only take away mony if it was a mistake (aka, a checkbox)
|
||||
if (delta > 0 or (type in ['daily', 'todo'])) and !cron
|
||||
if (delta > 0 or (type in ['daily', 'todo'])) and !options.cron
|
||||
modified = expModifier(delta)
|
||||
exp += modified
|
||||
money += modified
|
||||
|
|
@ -172,50 +176,58 @@ score = (taskId, direction, cron=false) ->
|
|||
updateStats({hp: hp, exp: exp, money: money})
|
||||
|
||||
return delta
|
||||
|
||||
|
||||
# At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
|
||||
# For incomplete Dailys, deduct experience
|
||||
cron = ->
|
||||
today = moment().sod() # start of day
|
||||
user.setNull 'lastCron', today.toDate()
|
||||
lastCron = moment(user.get('lastCron'))
|
||||
daysPassed = today.diff(lastCron, 'days')
|
||||
if daysPassed > 0
|
||||
user.set('lastCron', today.toDate()) # reset cron
|
||||
_.times daysPassed, (n) ->
|
||||
tallyFor = lastCron.add('d',n)
|
||||
tally(tallyFor)
|
||||
# Tally function, which is called asyncronously below - but function is defined here.
|
||||
# We need access to some closure variables above
|
||||
todoTally = 0
|
||||
tallyTask = (taskObj, next) ->
|
||||
{id, type, completed, repeat} = taskObj
|
||||
return unless id? #this shouldn't be happening, some tasks seem to be corrupted
|
||||
task = user.at("tasks.#{id}")
|
||||
if type in ['todo', 'daily']
|
||||
# Deduct experience for missed Daily tasks,
|
||||
# but not for Todos (just increase todo's value)
|
||||
unless completed
|
||||
# for todos & typical dailies, these are equivalent
|
||||
daysFailed = daysPassed
|
||||
# however, for dailys which have repeat dates, need
|
||||
# to calculate how many they've missed according to their own schedule
|
||||
if type=='daily' && repeat
|
||||
dayMapping = {0:'su',1:'m',2:'t',3:'w',4:'th',5:'f',6:'s',7:'su'}
|
||||
dueToday = (repeat && repeat[dayMapping[momentDate.day()]]==true)
|
||||
score(taskId, 'down', {cron:true, times: daysFailed})
|
||||
|
||||
# At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
|
||||
# For incomplete Dailys, deduct experience
|
||||
tally = (momentDate) ->
|
||||
todoTally = 0
|
||||
_.each user.get('tasks'), (taskObj, taskId, list) ->
|
||||
#FIXME is it hiccuping here? taskId == "$_65255f4e-3728-4d50-bade-3b05633639af_2", & taskObj.id = undefined
|
||||
return unless taskObj.id? #this shouldn't be happening, some tasks seem to be corrupted
|
||||
[type, value, completed, repeat] = [taskObj.type, taskObj.value, taskObj.completed, taskObj.repeat]
|
||||
task = user.at("tasks.#{taskId}")
|
||||
if type in ['todo', 'daily']
|
||||
# Deduct experience for missed Daily tasks,
|
||||
# but not for Todos (just increase todo's value)
|
||||
unless completed
|
||||
dayMapping = {0:'su',1:'m',2:'t',3:'w',4:'th',5:'f',6:'s',7:'su'}
|
||||
dueToday = (repeat && repeat[dayMapping[momentDate.day()]]==true)
|
||||
if dueToday or type=='todo'
|
||||
score(taskId, 'down', true)
|
||||
if type == 'daily'
|
||||
task.push "history", { date: new Date(momentDate), value: value }
|
||||
else
|
||||
absVal = if (completed) then Math.abs(value) else value
|
||||
todoTally += absVal
|
||||
task.pass({cron:true}).set('completed', false) if type == 'daily'
|
||||
user.push 'history.todos', { date: new Date(momentDate), value: todoTally }
|
||||
|
||||
# tally experience
|
||||
expTally = user.get 'stats.exp'
|
||||
lvl = 0 #iterator
|
||||
while lvl < (user.get('stats.lvl')-1)
|
||||
lvl++
|
||||
expTally += (lvl*100)/5
|
||||
user.push 'history.exp', { date: new Date(), value: expTally }
|
||||
value = task.get('value') #get updated value
|
||||
if type == 'daily'
|
||||
task.push "history", { date: today.toDate(), value: value }
|
||||
else
|
||||
absVal = if (completed) then Math.abs(value) else value
|
||||
todoTally += absVal
|
||||
task.pass({cron:true}).set('completed', false) if type == 'daily'
|
||||
next()
|
||||
|
||||
# Tally each task
|
||||
tasks = _.toArray(user.get('tasks'))
|
||||
async.forEach tasks, tallyTask, (err) ->
|
||||
# Finished tallying, this is the 'completed' callback
|
||||
user.push 'history.todos', { date: today.toDate(), value: todoTally }
|
||||
# tally experience
|
||||
expTally = user.get 'stats.exp'
|
||||
lvl = 0 #iterator
|
||||
while lvl < (user.get('stats.lvl')-1)
|
||||
lvl++
|
||||
expTally += (lvl*100)/5
|
||||
user.push 'history.exp', { date: today.toDate(), value: expTally }
|
||||
user.set('lastCron', today.toDate()) # reset cron
|
||||
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
|
|
@ -5,14 +5,23 @@ derby = require 'derby'
|
|||
# Custom modules
|
||||
scoring = require '../src/app/scoring'
|
||||
schema = require '../src/app/schema'
|
||||
_ = require '../public/js/underscore-min'
|
||||
_ = require 'lodash'
|
||||
moment = require 'moment'
|
||||
|
||||
###### Helper Functions ######
|
||||
|
||||
modifictionLookup = (value, direction) ->
|
||||
#TODO implement a lookup table to test if user stats & task value has been modified properly
|
||||
|
||||
###### Specs ######
|
||||
|
||||
describe 'User', ->
|
||||
model = null
|
||||
|
||||
## Helper which clones the content at a path so tests can compare before/after values
|
||||
pathSnapshots = (paths) ->
|
||||
_.map paths, (path) -> _.clone(model.get(path))
|
||||
|
||||
beforeEach ->
|
||||
model = new Model
|
||||
model.set '_user', schema.newUserObject()
|
||||
|
|
@ -35,11 +44,18 @@ describe 'User', ->
|
|||
uuid = null
|
||||
taskPath = null
|
||||
|
||||
before ->
|
||||
# Reset tasks
|
||||
model.set '_user.tasks', {}
|
||||
model.set '_user.habitIds', []
|
||||
model.set '_user.dailyIds', []
|
||||
model.set '_user.todoIds', []
|
||||
model.set '_user.rewardIds', []
|
||||
|
||||
describe 'Habits', ->
|
||||
|
||||
beforeEach ->
|
||||
# create a test task
|
||||
user = model.get('_user')
|
||||
uuid = derby.uuid()
|
||||
taskPath = "_user.tasks.#{uuid}"
|
||||
model.refList "_habitList", "_user.tasks", "_user.habitIds"
|
||||
|
|
@ -106,27 +122,63 @@ describe 'User', ->
|
|||
|
||||
it 'should show "undo" notification if user unchecks completed daily'
|
||||
|
||||
describe 'Dailies', ->
|
||||
describe 'Dailies', ->
|
||||
|
||||
beforeEach ->
|
||||
# create a test task
|
||||
uuid = derby.uuid()
|
||||
taskPath = "_user.tasks.#{uuid}"
|
||||
model.refList "_dailyList", "_user.tasks", "_user.dailyIds"
|
||||
model.at('_dailyList').push {type: 'daily', text: 'Daily', value: 0, completed: false, id: uuid }
|
||||
|
||||
it 'created the daily', ->
|
||||
task = model.get(taskPath)
|
||||
expect(task.text).to.eql 'Daily'
|
||||
expect(task.value).to.eql 0
|
||||
|
||||
it 'does proper calculations when daily is complete'
|
||||
|
||||
it 'calculates user.stats & task.value properly on cron', ->
|
||||
[statsBefore, taskBefore] = pathSnapshots(['_user.stats', taskPath])
|
||||
# Set lastCron to yesterday
|
||||
today = moment()
|
||||
model.set '_user.lastCron', today.subtract('days',1).toDate()
|
||||
# Run run
|
||||
scoring.cron()
|
||||
[statsAfter, taskAfter] = pathSnapshots(['_user.stats', taskPath])
|
||||
|
||||
# Should have updated cron to today
|
||||
lastCron = moment(model.get('_user.lastCron'))
|
||||
expect(today.diff(lastCron, 'days')).to.eql 0
|
||||
|
||||
# Should have updated points properly
|
||||
expect(statsBefore.hp).to.be.lessThan statsAfter.hp
|
||||
expect(taskBefore.value).to.eql 0
|
||||
expect(taskAfter.value).to.eql -1
|
||||
|
||||
#TODO clicking repeat dates on newly-created item doesn't refresh until you refresh the page
|
||||
#TODO dates on dailies is having issues, possibility: date cusps? my saturday exempts were set to exempt at 8pm friday
|
||||
|
||||
#TODO refactor as user->habits, user->dailys, user->todos, user->rewards
|
||||
describe 'Todos', ->
|
||||
describe 'Cron', ->
|
||||
it 'should calculate user.stats & task.value properly on cron'
|
||||
it 'should calculate cron based on difference between start-of-days, and not run in the middle of the day'
|
||||
it 'should only run set operations once per task, even when daysPassed > 1'
|
||||
# pass in daysPassed to score, multiply modification values by daysPassed before running set
|
||||
it 'should only push a history point for lastCron, not each day in between'
|
||||
# stop passing in tallyFor, let moment().sod().toDate() be handled in scoring.score()
|
||||
it 'should defer saving user modifications until, save as aggregate values'
|
||||
# pass in commit parameter to scoring func, if true save right away, otherwise return aggregated array so can save in the end (so total hp loss, etc)
|
||||
|
||||
describe 'Rewards', ->
|
||||
|
||||
describe 'Lvl & Items', ->
|
||||
it 'modified damage based on lvl & armor'
|
||||
it 'always decreases hp with damage, regardless of stats/items'
|
||||
it 'always increases exp/gp with gain, regardless of stats/items'
|
||||
|
||||
describe 'Cron', ->
|
||||
it 'should calculate user.stats & task.value properly on cron'
|
||||
it 'should calculate cron based on difference between start-of-days, and not run in the middle of the day'
|
||||
it 'should only run set operations once per task, even when daysPassed > 1'
|
||||
# pass in daysPassed to score, multiply modification values by daysPassed before running set
|
||||
it 'should only push a history point for lastCron, not each day in between'
|
||||
# stop passing in tallyFor, let moment().sod().toDate() be handled in scoring.score()
|
||||
it 'should defer saving user modifications until, save as aggregate values'
|
||||
# pass in commit parameter to scoring func, if true save right away, otherwise return aggregated array so can save in the end (so total hp loss, etc)
|
||||
|
||||
#### Require.js stuff, might be necessary to place in casper.coffee
|
||||
it "doesn't setup dependent functions until their modules are loaded, require.js callback"
|
||||
# sortable, stripe, etc
|
||||
|
||||
#TODO refactor as user->habits, user->dailys, user->todos, user->rewards
|
||||
Loading…
Reference in a new issue