From 897cd3f2bb77523fb262c401c0c60e88d0704553 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Wed, 20 Feb 2013 13:35:21 -0500 Subject: [PATCH 001/220] wip --- package.json | 3 +- src/server/api.coffee | 27 ++++++++++++ test/api.mocha.coffee | 92 ++++++++++++++++++++++++++++++++++++++++ test/user.mocha.coffee | 96 +++++++++++++++++++++--------------------- 4 files changed, 169 insertions(+), 49 deletions(-) create mode 100644 test/api.mocha.coffee diff --git a/package.json b/package.json index 965ee1113e..9ead9904f8 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "nconf": "*", "icalendar": "git://github.com/lefnire/node-icalendar#master", "nodetime": "*", - "resolve": "~0.2.3" + "resolve": "~0.2.3", + "request": "~2.14.0" }, "private": true, "subdomain": "habitrpg", diff --git a/src/server/api.coffee b/src/server/api.coffee index f394945f09..50208154d8 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -13,6 +13,33 @@ icalendar = require('icalendar') curl -X POST -H "Content-Type:application/json" -d '{"apiToken":"{TOKEN}"}' localhost:3000/v1/users/{UID}/tasks/productivity/up ### +router.get '/users/:uid/tasks', (req, res) -> + {uid, taskId, direction} = req.params + {apiToken, title, service, icon} = req.body + console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development' + + # Send error responses for improper API call + return res.send(500, 'request body "apiToken" required') unless apiToken + return res.send(500, ':uid required') unless uid + + model = req.getModel() + req._isServer = true + model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) -> + return res.send(500, err) if err + user = result.at(0) + userObj = user.get() + if _.isEmpty(userObj) + return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id") + + model.ref('_user', user) + + # Create task if doesn't exist + # TODO add service & icon to task + unless model.get("_user.tasks") + model.refList "_habitList", "_user.tasks", "_user.habitIds" + return res.json model.get '_habitList' + + router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> {uid, taskId, direction} = req.params {apiToken, title, service, icon} = req.body diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee new file mode 100644 index 0000000000..95cbe7472b --- /dev/null +++ b/test/api.mocha.coffee @@ -0,0 +1,92 @@ +assert = require 'assert' +{BrowserModel: Model} = require 'racer/test/util/model' +derby = require 'derby' +_ = require 'underscore' +moment = require 'moment' +request = require 'request' + +# Custom modules +scoring = require '../src/app/scoring' +character = require '../src/app/character' + +###### Helpers & Variables ###### + +model = null +uuid = null +taskPath = null +baseURL = 'http://localhost:3000' + +## Helper which clones the content at a path so tests can compare before/after values +# Otherwise, using model.get(path) will give the same object before as after +pathSnapshots = (paths) -> + if _.isString(paths) + return clone(model.get(paths)) + _.map paths, (path) -> clone(model.get(path)) +statsTask = -> pathSnapshots(['_user.stats', taskPath]) # quick snapshot of user.stats & task + +cleanUserObj = -> + userObj = character.newUserObject() + userObj.tasks = {} + userObj.habitIds = [] + userObj.dailyIds = [] + userObj.todoIds = [] + userObj.rewardIds = [] + return userObj +resetUser = -> model.set '_user', cleanUserObj() + +freshTask = (taskObj) -> + resetUser() + # create a test task + uuid = derby.uuid() + taskPath = "_user.tasks.#{uuid}" + {type} = taskObj + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + [taskObj.id, taskObj.value] = [uuid, 0] + model.at("_#{type}List").push taskObj + +### +Helper function to determine if stats updates are numerically correct based on scoring +@direction: 'up' or 'down' +@options: The user stats modifiers and times to run, defaults to {times:1, modifiers:{lvl:1, weapon:0, armor:0}} +### +modificationsLookup = (direction, options = {}) -> + merged = _.defaults options, {times:1, lvl:1, weapon:0, armor:0} + {times, lvl, armor, weapon} = merged + userObj = cleanUserObj() + value = 0 + _.times times, (n) -> + delta = scoring.taskDeltaFormula(value, direction) + value += delta + if direction=='up' + gain = scoring.expModifier(delta, options) + userObj.stats.exp += gain + userObj.stats.money += gain + else + loss = scoring.hpModifier(delta, options) + userObj.stats.hp += loss + return {user:userObj, value:value} + +###### Specs ###### + +describe 'API', -> + model = null + + before -> + model = new Model + model.set '_user', character.newUserObject() + scoring.setModel model + + it '/v1/:uid/tasks returns correct user defaults', (done) -> + user = model.get '_user' + + request "#{baseURL}/#{user.id}/tasks", (err, res, body) -> + assert.ok !err + tasks = [] + + ['habit','daily'].map (type) -> + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + tasks.concat model.get "_#{type}List" + + console.log 'hi', tasks + assert.ok _.isEqual tasks, body + done() diff --git a/test/user.mocha.coffee b/test/user.mocha.coffee index 33b1917014..3f4cc1ec5c 100644 --- a/test/user.mocha.coffee +++ b/test/user.mocha.coffee @@ -8,7 +8,7 @@ moment = require 'moment' # Custom modules scoring = require '../src/app/scoring' schema = require '../src/app/schema' - + ###### Helpers & Variables ###### model = null @@ -19,11 +19,11 @@ taskPath = null # Otherwise, using model.get(path) will give the same object before as after pathSnapshots = (paths) -> if _.isString(paths) - return clone(model.get(paths)) + return clone(model.get(paths)) _.map paths, (path) -> clone(model.get(path)) statsTask = -> pathSnapshots(['_user.stats', taskPath]) # quick snapshot of user.stats & task -cleanUserObj = -> +cleanUserObj = -> userObj = schema.newUserObject() userObj.tasks = {} userObj.habitIds = [] @@ -42,14 +42,14 @@ freshTask = (taskObj) -> model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" [taskObj.id, taskObj.value] = [uuid, 0] model.at("_#{type}List").push taskObj - + ### Helper function to determine if stats updates are numerically correct based on scoring @direction: 'up' or 'down' -@options: The user stats modifiers and times to run, defaults to {times:1, modifiers:{lvl:1, weapon:0, armor:0}} +@options: The user stats modifiers and times to run, defaults to {times:1, modifiers:{lvl:1, weapon:0, armor:0}} ### modificationsLookup = (direction, options = {}) -> - merged = _.defaults options, {times:1, lvl:1, weapon:0, armor:0} + merged = _.defaults options, {times:1, lvl:1, weapon:0, armor:0} {times, lvl, armor, weapon} = merged userObj = cleanUserObj() value = 0 @@ -64,12 +64,12 @@ modificationsLookup = (direction, options = {}) -> loss = scoring.hpModifier(delta, options) userObj.stats.hp += loss return {user:userObj, value:value} - -###### Specs ###### + +###### Specs ###### describe 'User', -> model = null - + before -> model = new Model model.set '_user', schema.newUserObject() @@ -85,23 +85,23 @@ describe 'User', -> expect(_.size(user.dailyIds)).to.eql 3 expect(_.size(user.todoIds)).to.eql 1 expect(_.size(user.rewardIds)).to.eql 2 - - ##### Habits ##### + + ##### Habits ##### describe 'Tasks', -> - - beforeEach -> + + beforeEach -> resetUser() - + describe 'Habits', -> - - beforeEach -> + + beforeEach -> freshTask {type: 'habit', text: 'Habit', up: true, down: true} - + it 'created the habit', -> task = model.get(taskPath) expect(task.text).to.eql 'Habit' expect(task.value).to.eql 0 - + it 'test a few scoring numbers (this will change if constants / formulae change)', -> {user} = modificationsLookup('down') expect(user.stats.hp).to.eql 49 @@ -113,10 +113,10 @@ describe 'User', -> expect(user.stats.money).to.eql 1 {user} = modificationsLookup('up', {times:5}) expect(user.stats.exp).to.be.within(4,5) - + it 'made proper modifications when down-scored', -> ## Trial 1 - + shouldBe = modificationsLookup('down') scoring.score(uuid,'down') [stats, task] = statsTask() @@ -130,14 +130,14 @@ describe 'User', -> [stats, task] = statsTask() expect(stats.hp).to.be.eql shouldBe.user.stats.hp expect(task.value).to.eql shouldBe.value - + it 'made proper modifications when up-scored', -> # Up-score the habit [statsBefore, taskBefore] = statsTask() scoring.score(uuid, 'up') [statsAfter, taskAfter] = statsTask() - - # User should have gained Exp, GP + + # User should have gained Exp, GP expect(statsAfter.exp).to.be.greaterThan statsBefore.exp expect(statsAfter.money).to.be.greaterThan statsBefore.money # HP should not change @@ -145,9 +145,9 @@ describe 'User', -> # Task should have lost value expect(taskBefore.value).to.eql 0 expect(taskAfter.value).to.be.greaterThan taskBefore.value - + ## Trial 2 - taskBefore = pathSnapshots(taskPath) + taskBefore = pathSnapshots(taskPath) scoring.score(uuid, 'up') taskAfter = pathSnapshots(taskPath) # Should have lost in value @@ -155,54 +155,54 @@ describe 'User', -> # And lost more than trial 1 diff = Math.abs(taskAfter.value) - Math.abs(taskBefore.value) expect(diff).to.be.lessThan 1 - - it 'makes history entry for habit' + + it 'makes history entry for habit' it 'makes proper modifications each time when clicking + / - in rapid succession' # saw an issue here once, so test that it wasn't a fluke - + it 'should not modify certain attributes given certain conditions' # non up+down habits # what else? - + it 'should show "undo" notification if user unchecks completed daily' - - - describe 'Lvl & Items', -> - - beforeEach -> + + + describe 'Lvl & Items', -> + + beforeEach -> freshTask {type: 'habit', text: 'Habit', up: true, down: true} - + it 'modified damage based on lvl & armor' it 'modified exp/gp based on lvl & weapon' it 'always decreases hp with damage, regardless of stats/items' it 'always increases exp/gp with gain, regardless of stats/items' - + describe 'Dailies', -> - + beforeEach -> freshTask {type: 'daily', text: 'Daily', completed: false} - + 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 dailys properly when they have repeat dates' - + runCron = (times, pass=1) -> # Set lastCron to days ago today = new moment() ago = new moment().subtract('days',times) model.set '_user.lastCron', ago.toDate() # Run run - scoring.cron() + scoring.cron() [stats, task] = statsTask() - + # Should have updated cron to today lastCron = moment(model.get('_user.lastCron')) expect(today.diff(lastCron, 'days')).to.eql 0 - + shouldBe = modificationsLookup('down', {times:times*pass}) # Should have updated points properly expect(Math.round(stats.hp)).to.be.eql Math.round(shouldBe.user.stats.hp) @@ -215,10 +215,10 @@ describe 'User', -> runCron(5) runCron(5, 2) - + #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 - + describe 'Todos', -> describe 'Cron', -> it 'calls cron asyncronously' @@ -230,11 +230,11 @@ describe 'User', -> # 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', -> - + #### 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 + # sortable, stripe, etc #TODO refactor as user->habits, user->dailys, user->todos, user->rewards \ No newline at end of file From f75e3662b9a358af4bb76eb59011bfbbd99b8335 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Wed, 20 Feb 2013 20:04:09 -0500 Subject: [PATCH 002/220] wi[ --- .gitignore | 3 +- package.json | 3 +- src/server/api.coffee | 78 +++++++----------------------------- src/server/deprecated.coffee | 72 +++++++++++++++++++++++++++++++++ src/server/index.coffee | 2 +- src/server/store.coffee | 14 +++---- test/api.mocha.coffee | 41 +++++++++++-------- 7 files changed, 124 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index f54c5f2a27..77291d9d0f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules #lib/ *.swp .idea* -config.json \ No newline at end of file +config.json +test/config.json \ No newline at end of file diff --git a/package.json b/package.json index 9ead9904f8..bc501098c9 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "icalendar": "git://github.com/lefnire/node-icalendar#master", "nodetime": "*", "resolve": "~0.2.3", - "request": "~2.14.0" + "request": "~2.14.0", + "querystring": "~0.1.0" }, "private": true, "subdomain": "habitrpg", diff --git a/src/server/api.coffee b/src/server/api.coffee index 50208154d8..b142ec09b3 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -5,6 +5,9 @@ scoring = require '../app/scoring' _ = require 'underscore' icalendar = require('icalendar') +NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request" +NO_USER_FOUND = err: "No user found." + # ---------- /v1 API ------------ # Every url added beneath router is prefaced by /v1 @@ -13,75 +16,24 @@ icalendar = require('icalendar') curl -X POST -H "Content-Type:application/json" -d '{"apiToken":"{TOKEN}"}' localhost:3000/v1/users/{UID}/tasks/productivity/up ### -router.get '/users/:uid/tasks', (req, res) -> - {uid, taskId, direction} = req.params - {apiToken, title, service, icon} = req.body - console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development' +router.get '/status', (req, res) -> + res.json + status: 'up' - # Send error responses for improper API call - return res.send(500, 'request body "apiToken" required') unless apiToken - return res.send(500, ':uid required') unless uid +router.get '/user', (req, res) -> + { uid, token } = req.query + return res.json 500, NO_TOKEN_OR_UID unless uid || token model = req.getModel() - req._isServer = true - model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) -> - return res.send(500, err) if err - user = result.at(0) - userObj = user.get() - if _.isEmpty(userObj) - return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id") + query = model.query('users').withIdAndToken(uid, token) - model.ref('_user', user) + model.fetch query, (err, user) -> + return res.json 500, err: err if err + return res.json 500, err: NO_USER_FOUND unless user - # Create task if doesn't exist - # TODO add service & icon to task - unless model.get("_user.tasks") - model.refList "_habitList", "_user.tasks", "_user.habitIds" - return res.json model.get '_habitList' + res.json user - -router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> - {uid, taskId, direction} = req.params - {apiToken, title, service, icon} = req.body - console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development' - - # Send error responses for improper API call - return res.send(500, 'request body "apiToken" required') unless apiToken - return res.send(500, ':uid required') unless uid - return res.send(500, ':taskId required') unless taskId - return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] - - model = req.getModel() - req._isServer = true - model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) -> - return res.send(500, err) if err - user = result.at(0) - userObj = user.get() - if _.isEmpty(userObj) - return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id") - - model.ref('_user', user) - - # Create task if doesn't exist - # TODO add service & icon to task - unless model.get("_user.tasks.#{taskId}") - model.refList "_habitList", "_user.tasks", "_user.habitIds" - model.at('_habitList').push - id: taskId - type: 'habit' - text: (title || taskId) - value: 0 - up: true - down: true - notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." - - scoring.setModel(model) - delta = scoring.score(taskId, direction) - result = model.get ('_user.stats') - result.delta = delta - res.send(result) - -router.get '/users/:uid/calendar.ics', (req, res) -> +router.get '/user/calendar.ics', (req, res) -> #return next() #disable for now {uid} = req.params {apiToken} = req.query diff --git a/src/server/deprecated.coffee b/src/server/deprecated.coffee index e8a5fb7340..44ae3d392c 100644 --- a/src/server/deprecated.coffee +++ b/src/server/deprecated.coffee @@ -1,6 +1,10 @@ express = require 'express' router = new express.Router() +scoring = require '../app/scoring' +_ = require 'underscore' +icalendar = require('icalendar') + # ---------- Deprecated Paths ------------ deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol' @@ -9,4 +13,72 @@ router.get '/:uid/up/:score?', (req, res) -> res.send(500, deprecatedMessage) router.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage) router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage) +router.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> + {uid, taskId, direction} = req.params + {apiToken, title, service, icon} = req.body + console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development' + + # Send error responses for improper API call + return res.send(500, 'request body "apiToken" required') unless apiToken + return res.send(500, ':uid required') unless uid + return res.send(500, ':taskId required') unless taskId + return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] + + model = req.getModel() + req._isServer = true + model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) -> + return res.send(500, err) if err + user = result.at(0) + userObj = user.get() + if _.isEmpty(userObj) + return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id") + + model.ref('_user', user) + + # Create task if doesn't exist + # TODO add service & icon to task + unless model.get("_user.tasks.#{taskId}") + model.refList "_habitList", "_user.tasks", "_user.habitIds" + model.at('_habitList').push + id: taskId + type: 'habit' + text: (title || taskId) + value: 0 + up: true + down: true + notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." + + scoring.setModel(model) + delta = scoring.score(taskId, direction) + result = model.get ('_user.stats') + result.delta = delta + res.send(result) + +router.get '/v1/users/:uid/calendar.ics', (req, res) -> + #return next() #disable for now + {uid} = req.params + {apiToken} = req.query + + model = req.getModel() + query = model.query('users').withIdAndToken(uid, apiToken) + query.fetch (err, result) -> + return res.send(500, err) if err + tasks = result.at(0).get('tasks') + # tasks = result[0].tasks + tasksWithDates = _.filter tasks, (task) -> !!task.date + return res.send(500, "No events found") if _.isEmpty(tasksWithDates) + + ical = new icalendar.iCalendar() + ical.addProperty('NAME', 'HabitRPG') + _.each tasksWithDates, (task) -> + event = new icalendar.VEvent(task.id); + event.setSummary(task.text); + d = new Date(task.date) + d.date_only = true + event.setDate d + ical.addComponent event + res.type('text/calendar') + formattedIcal = ical.toString().replace(/DTSTART\:/g, 'DTSTART;VALUE=DATE:') + res.send(200, formattedIcal) + module.exports = router diff --git a/src/server/index.coffee b/src/server/index.coffee index 8a1b29826b..21b9a69d4a 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -107,7 +107,7 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, -> .use(auth.middleware(strategies, options)) # Creates an express middleware from the app's routes .use(app.router()) - .use('/v1', require('./api').middleware) + .use('/api/v1', require('./api').middleware) .use(require('./static').middleware) .use(require('./deprecated').middleware) .use(expressApp.router) diff --git a/src/server/store.coffee b/src/server/store.coffee index 38476d72cc..1edada5125 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -53,14 +53,14 @@ userAccess = (store) -> Get user with API token ### REST = (store) -> - store.query.expose "users", "withIdAndToken", (id, apiToken) -> - @where("id").equals(id) - .where('apiToken').equals(apiToken) - .limit(1) + store.query.expose "users", "withIdAndToken", (uid, token) -> + @where('id').equals(uid) + .where('apiToken').equals(token) + .one - store.queryAccess "users", "withIdAndToken", (id, apiToken, accept, err) -> - return accept(true) unless @session?.userId # https://github.com/codeparty/racer/issues/37 - accept(true) # only user has id & token + store.queryAccess "users", "withIdAndToken", (id, token, accept, err) -> + return accept(true) if id && token + accept(false) # only user has id & token ### diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 95cbe7472b..ec3d5ca243 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -4,17 +4,22 @@ derby = require 'derby' _ = require 'underscore' moment = require 'moment' request = require 'request' +qs = require 'querystring' # Custom modules scoring = require '../src/app/scoring' character = require '../src/app/character' +config = require './config' ###### Helpers & Variables ###### model = null uuid = null taskPath = null -baseURL = 'http://localhost:3000' +baseURL = 'http://localhost:3000/api/v1' +UID_AND_TOKEN = + uid: config.uid + token: config.token ## Helper which clones the content at a path so tests can compare before/after values # Otherwise, using model.get(path) will give the same object before as after @@ -71,22 +76,26 @@ modificationsLookup = (direction, options = {}) -> describe 'API', -> model = null - before -> - model = new Model - model.set '_user', character.newUserObject() - scoring.setModel model + describe 'Without token or user id', -> - it '/v1/:uid/tasks returns correct user defaults', (done) -> - user = model.get '_user' + it '/api/v1/user', (done) -> + request.get { uri: "#{baseURL}/user" }, (err, res, body) -> + console.log "#{baseURL}/user", body + assert.ok !err + assert.equal res.statusCode, 500 + assert.ok body.err + done() - request "#{baseURL}/#{user.id}/tasks", (err, res, body) -> - assert.ok !err - tasks = [] + describe 'With token and user id', -> + before -> + model = new Model + model.set '_user', character.newUserObject() + scoring.setModel model - ['habit','daily'].map (type) -> - model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" - tasks.concat model.get "_#{type}List" + it '/api/v1/user', (done) -> + user = model.get '_user' - console.log 'hi', tasks - assert.ok _.isEqual tasks, body - done() + request.get { uri: "#{baseURL}/user?#{qs.stringify(UID_AND_TOKEN)}" }, (err, res, body) -> + assert.ok !err + assert.equal res.statusCode, 200 + done() From b92145f5a339146b9bbeb7aa3835420f07791040 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Wed, 20 Feb 2013 20:07:05 -0500 Subject: [PATCH 003/220] wip --- src/server/api.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index b142ec09b3..8d2d8e4875 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -29,7 +29,7 @@ router.get '/user', (req, res) -> model.fetch query, (err, user) -> return res.json 500, err: err if err - return res.json 500, err: NO_USER_FOUND unless user + return res.json 500, NO_USER_FOUND if !user || _.isEmpty(user) res.json user From ddff29ccf0d02899dce24b3afa75891e8820d3b2 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Thu, 21 Feb 2013 14:18:53 -0500 Subject: [PATCH 004/220] wip --- package.json | 4 +- server.js | 4 +- src/app/character.coffee | 80 ++++++++++++++++++------------------ src/server/api.coffee | 8 ++-- src/server/deprecated.coffee | 25 +++++++++++ src/server/index.coffee | 8 ++-- src/server/middleware.coffee | 3 +- src/server/store.coffee | 8 ++-- test/api.mocha.coffee | 67 ++++++++++++++++++++++-------- test/request/local.coffee | 25 +++++++++++ test/request/request.coffee | 74 +++++++++++++++++++++++++++++++++ 11 files changed, 234 insertions(+), 72 deletions(-) create mode 100644 test/request/local.coffee create mode 100644 test/request/request.coffee diff --git a/package.json b/package.json index bc501098c9..fb07f0753a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "nodetime": "*", "resolve": "~0.2.3", "request": "~2.14.0", - "querystring": "~0.1.0" + "querystring": "~0.1.0", + "nock": "~0.15.2", + "superagent": "~0.12.4" }, "private": true, "subdomain": "habitrpg", diff --git a/server.js b/server.js index 5709a5db48..50493a836c 100644 --- a/server.js +++ b/server.js @@ -67,7 +67,9 @@ process.on('uncaughtException', function (error) { }); require('coffee-script') // remove intermediate compilation requirement -require('./src/server').listen(process.env.PORT || 3000, process.env.IP || '0.0.0.0'); +module.exports = server = require('./src/server') + +server.listen(process.env.PORT || 3000, process.env.IP || '0.0.0.0'); // Note: removed "up" module, which is default for development (but interferes with and production + PaaS) // Restore to 5310bb0 if I want it back (see https://github.com/codeparty/derby/issues/165#issuecomment-10405693) diff --git a/src/app/character.coffee b/src/app/character.coffee index 901e3cb518..16d1c40fc5 100644 --- a/src/app/character.coffee +++ b/src/app/character.coffee @@ -137,7 +137,7 @@ module.exports.updateUser = (model) -> batch = new BatchUpdate(model) user = batch.user obj = batch.obj() - tasks = obj.tasks + tasks = obj?.tasks # Remove corrupted tasks _.each tasks, (task, key) -> @@ -172,49 +172,49 @@ module.exports.BatchUpdate = BatchUpdate = (model) -> updates = {} { - user: user + user: user - obj: -> - obj ?= model.get 'users.'+user.get('id') - return obj + obj: -> + obj ?= model.get 'users.'+user.get('id') + return obj - startTransaction: -> - # start a batch transaction - nothing between now and @commit() will be set immediately - transactionInProgress = true - model._dontPersist = true - @obj() + startTransaction: -> + # start a batch transaction - nothing between now and @commit() will be set immediately + transactionInProgress = true + model._dontPersist = true + @obj() - ### - Handles updating the user model. If this is an en-mass operation (eg, server cron), changes are queued - but not actually set to the model. It also modifies userObj in case you need to access properties manually later. - If transaction not in progress, it just runs standard model.set() - ### - set: (path, val) -> - updates[path] = val if transactionInProgress - user.set(path, val) + ### + Handles updating the user model. If this is an en-mass operation (eg, server cron), changes are queued + but not actually set to the model. It also modifies userObj in case you need to access properties manually later. + If transaction not in progress, it just runs standard model.set() + ### + set: (path, val) -> + updates[path] = val if transactionInProgress + user.set(path, val) - ### - Hack to get around dom bindings being lost if parent objects are replaced whole-sale - eg, user.set('stats', {hp:50, exp:10...}) will break dom bindings, but user.set('stats.hp',50) is ok - ### - setStats: (stats) -> - stats ?= obj.stats - that = @ - _.each Object.keys(stats), (key) -> that.set "stats.#{key}", stats[key] + ### + Hack to get around dom bindings being lost if parent objects are replaced whole-sale + eg, user.set('stats', {hp:50, exp:10...}) will break dom bindings, but user.set('stats.hp',50) is ok + ### + setStats: (stats) -> + stats ?= obj.stats + that = @ + _.each Object.keys(stats), (key) -> that.set "stats.#{key}", stats[key] -# queue: (path, val) -> -# # Special function for setting object properties by string dot-notation. See http://stackoverflow.com/a/6394168/362790 -# arr = path.split('.') -# arr.reduce (curr, next, index) -> -# if (arr.length - 1) == index -# curr[next] = val -# curr[next] -# , obj + # queue: (path, val) -> + # # Special function for setting object properties by string dot-notation. See http://stackoverflow.com/a/6394168/362790 + # arr = path.split('.') + # arr.reduce (curr, next, index) -> + # if (arr.length - 1) == index + # curr[next] = val + # curr[next] + # , obj - commit: -> - model._dontPersist = false - # some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js - user.set "update__", updates - transactionInProgress = false - updates = {} + commit: -> + model._dontPersist = false + # some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js + user.set "update__", updates + transactionInProgress = false + updates = {} } diff --git a/src/server/api.coffee b/src/server/api.coffee index 8d2d8e4875..37da942522 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -21,17 +21,19 @@ router.get '/status', (req, res) -> status: 'up' router.get '/user', (req, res) -> + console.log 'hi' { uid, token } = req.query return res.json 500, NO_TOKEN_OR_UID unless uid || token model = req.getModel() query = model.query('users').withIdAndToken(uid, token) - model.fetch query, (err, user) -> + query.fetch (err, user) -> + self = user.get() return res.json 500, err: err if err - return res.json 500, NO_USER_FOUND if !user || _.isEmpty(user) + return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) - res.json user + res.json self router.get '/user/calendar.ics', (req, res) -> #return next() #disable for now diff --git a/src/server/deprecated.coffee b/src/server/deprecated.coffee index 44ae3d392c..8e497764ef 100644 --- a/src/server/deprecated.coffee +++ b/src/server/deprecated.coffee @@ -13,6 +13,31 @@ router.get '/:uid/up/:score?', (req, res) -> res.send(500, deprecatedMessage) router.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage) router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage) +router.get '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> + {uid, taskId, direction} = req.params + {apiToken} = req.query + {title, service, icon} = req.body + console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development' + + # Send error responses for improper API call + return res.send(500, 'request body "apiToken" required') unless apiToken + return res.send(500, ':uid required') unless uid + return res.send(500, ':taskId required') unless taskId + return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] + + model = req.getModel() + req._isServer = true + model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) -> + return res.send(500, err) if err + user = result.at(0) + userObj = user.get() + if _.isEmpty(userObj) + return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id") + + model.ref('_user', user) + + res.send(user) + router.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> {uid, taskId, direction} = req.params {apiToken, title, service, icon} = req.body diff --git a/src/server/index.coffee b/src/server/index.coffee index 54e2be229f..ab304c3c72 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -17,9 +17,9 @@ middleware = require './middleware' racer.io.set('transports', ['xhr-polling']) racer.ioClient.set('reconnection limit', 300000) # max reconect timeout to 5 minutes racer.set('bundleTimeout', 40000) -#unless process.env.NODE_ENV == 'production' -# racer.use(racer.logPlugin) -# derby.use(derby.logPlugin) +unless process.env.NODE_ENV == 'production' + racer.use(racer.logPlugin) + derby.use(derby.logPlugin) ## SERVER CONFIGURATION ## @@ -28,7 +28,7 @@ server = http.createServer expressApp module.exports = server derby.use require('racer-db-mongo') -store = derby.createStore +module.exports.habitStore = store = derby.createStore db: {type: 'Mongo', uri: process.env.NODE_DB_URI, safe:true} listen: server diff --git a/src/server/middleware.coffee b/src/server/middleware.coffee index 44f53b90d0..d74eb1953e 100644 --- a/src/server/middleware.coffee +++ b/src/server/middleware.coffee @@ -1,7 +1,6 @@ module.exports.splash = (req, res, next) -> # This was an API call, not a page load - return next() if req.is('json') - + return next() if /^\/api/.test req.path if !req.session.userId? and !req.query?.play? res.redirect('/splash.html') else diff --git a/src/server/store.coffee b/src/server/store.coffee index 1edada5125..0a10b1b7e8 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -54,12 +54,12 @@ userAccess = (store) -> ### REST = (store) -> store.query.expose "users", "withIdAndToken", (uid, token) -> - @where('id').equals(uid) + @byId(uid) .where('apiToken').equals(token) .one - store.queryAccess "users", "withIdAndToken", (id, token, accept, err) -> - return accept(true) if id && token + store.queryAccess "users", "withIdAndToken", (uid, token, accept, err) -> + return accept(true) if uid && token accept(false) # only user has id & token @@ -90,4 +90,4 @@ partySystem = (store) -> store.writeAccess "*", "parties.*", -> accept = arguments[arguments.length-2] - accept(true) \ No newline at end of file + accept(true) diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index ec3d5ca243..7da76094b5 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -1,11 +1,16 @@ assert = require 'assert' {BrowserModel: Model} = require 'racer/test/util/model' derby = require 'derby' +racer = require 'racer' _ = require 'underscore' moment = require 'moment' -request = require 'request' +request = require 'superagent' qs = require 'querystring' +app = require '../server' + +store = app.habitStore + # Custom modules scoring = require '../src/app/scoring' character = require '../src/app/character' @@ -75,27 +80,55 @@ modificationsLookup = (direction, options = {}) -> describe 'API', -> model = null + user = null + params = null describe 'Without token or user id', -> it '/api/v1/user', (done) -> - request.get { uri: "#{baseURL}/user" }, (err, res, body) -> - console.log "#{baseURL}/user", body - assert.ok !err - assert.equal res.statusCode, 500 - assert.ok body.err - done() + request(app) + .get("/api/v1/user") + .set('Accept', 'application/json') + .on('error', (err) -> + console.log 'err', err + ) + .end (res) -> + assert.equal res.statusCode, 500 + assert.ok JSON.parse(res.text).err + done() describe 'With token and user id', -> - before -> - model = new Model - model.set '_user', character.newUserObject() - scoring.setModel model + before (done) -> + model = store.createModel() + store.flush() + + uid = model.id() + user = character.newUserObject() + user.apiToken = derby.uuid() + model.set "users.#{uid}", user + user = model.get("users.#{uid}") + + params = + uid: user.id + token: user.apiToken + + done() + + after (done) -> + store.flush done it '/api/v1/user', (done) -> - user = model.get '_user' - - request.get { uri: "#{baseURL}/user?#{qs.stringify(UID_AND_TOKEN)}" }, (err, res, body) -> - assert.ok !err - assert.equal res.statusCode, 200 - done() + console.log params + request(app) + .get("/api/v1/user") + .set('Accept', 'application/json') + .query(params) + .on('error', (err) -> + console.log 'err', err + ) + .end (res) -> + console.log 'test', res.body + assert.ok !res.body.err + assert.equal res.statusCode, 200 + assert.ok res.body + done() diff --git a/test/request/local.coffee b/test/request/local.coffee new file mode 100644 index 0000000000..1431d12cd5 --- /dev/null +++ b/test/request/local.coffee @@ -0,0 +1,25 @@ +Request = require './request' +qs = require 'querystring' + +makeUrl = (path, params) -> + if typeof params is "object" + params = qs.stringify(params) + else + params = '' + return "http://localhost:3000/#{path}?#{params}" + +# url, params (optional), callback +get = (obj, callback) -> + obj.params ||= {} + obj.path ||= '' + + requestUrl = makeUrl obj.path, obj.params + + request = new Request requestUrl + + request.done (res) -> + callback null, JSON.parse(res) + + request.fire() + +module.exports = { get } diff --git a/test/request/request.coffee b/test/request/request.coffee new file mode 100644 index 0000000000..5eadd9178b --- /dev/null +++ b/test/request/request.coffee @@ -0,0 +1,74 @@ +http = require 'http' + +# ### Request +# +# The Request class is just a wrapper +# around the `http.request` function, +# providing a cleaner and more reusable +# interface. +# +# `constructor(options)` +# +# `options` can be anything `http.request` takes. +# +# Callbacks can be queued up by calling `request.done` +# and passing it a callback function to be invoked when the +# http request has completed +# +# Example: +# +# callback = (json) -> +# doSomethingWithJSON( json ) +# +# request = new Request('http://api.phish.net/api.js?') +# request.done (response) -> +# callback JSON.parse(response)[0] +# +# request.fire(data) # data refers to the post data +# +class Request + constructor: (@options) -> + @callbacks = {} + + fire: (data) -> + request = http.request(@options, @_handler) + request.on 'error', (err) => + for callback in @callbacks['fail'] + callback(err) + + # Send data with request + if data + request.write data + + request.end() + + this + + done: (callback) -> + push.call(this, 'done', callback) + + this + + fail: (callback) -> + push.call(this, 'fail', callback) + + this + + ########### + # PRIVATE # + ########### + + _handler: (response) => + data = '' + response.on 'data', (chunk) -> + data += chunk + response.on 'end', => + for callback in @callbacks['done'] + callback(data) + + + push = (type, callback) -> + @callbacks[type] ?= [] + @callbacks[type].push callback + +module.exports = Request From 08d57e32ffbadd4ba1638e0c6f1e4ca89cfef511 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Thu, 21 Feb 2013 20:17:23 -0500 Subject: [PATCH 005/220] wip --- src/server/api.coffee | 10 +++++++--- src/server/store.coffee | 2 +- test/api.mocha.coffee | 27 +++++++++++---------------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 37da942522..808c25f32d 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -21,19 +21,23 @@ router.get '/status', (req, res) -> status: 'up' router.get '/user', (req, res) -> - console.log 'hi' - { uid, token } = req.query + console.log 'hi', { uid, token } = req.query return res.json 500, NO_TOKEN_OR_UID unless uid || token model = req.getModel() query = model.query('users').withIdAndToken(uid, token) query.fetch (err, user) -> + throw err if err + console.log user.at(0).get() self = user.get() + console.log self return res.json 500, err: err if err return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) - res.json self + return res.json self + res.json {err: 'Something went wrong'} + router.get '/user/calendar.ics', (req, res) -> #return next() #disable for now diff --git a/src/server/store.coffee b/src/server/store.coffee index 0a10b1b7e8..41ec5dd9ad 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -56,7 +56,7 @@ REST = (store) -> store.query.expose "users", "withIdAndToken", (uid, token) -> @byId(uid) .where('apiToken').equals(token) - .one + .limit(1) store.queryAccess "users", "withIdAndToken", (uid, token, accept, err) -> return accept(true) if uid && token diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 7da76094b5..4e73aeb3f6 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -7,9 +7,12 @@ moment = require 'moment' request = require 'superagent' qs = require 'querystring' -app = require '../server' +racer.use require 'racer-db-mongo' -store = app.habitStore +store = racer.createStore + db: + type: 'Mongo' + uri: process.env.NODE_DB_URI # Custom modules scoring = require '../src/app/scoring' @@ -86,12 +89,8 @@ describe 'API', -> describe 'Without token or user id', -> it '/api/v1/user', (done) -> - request(app) - .get("/api/v1/user") + request.get("#{baseURL}/user") .set('Accept', 'application/json') - .on('error', (err) -> - console.log 'err', err - ) .end (res) -> assert.equal res.statusCode, 500 assert.ok JSON.parse(res.text).err @@ -100,9 +99,9 @@ describe 'API', -> describe 'With token and user id', -> before (done) -> model = store.createModel() - store.flush() + #store.flush() - uid = model.id() + model.set '_userId', uid = model.id() user = character.newUserObject() user.apiToken = derby.uuid() model.set "users.#{uid}", user @@ -114,20 +113,16 @@ describe 'API', -> done() - after (done) -> - store.flush done - it '/api/v1/user', (done) -> - console.log params - request(app) - .get("/api/v1/user") + console.log "#{baseURL}/user?#{qs.stringify(params)}" + request.get("#{baseURL}/user") .set('Accept', 'application/json') .query(params) .on('error', (err) -> console.log 'err', err ) .end (res) -> - console.log 'test', res.body + console.log res.body assert.ok !res.body.err assert.equal res.statusCode, 200 assert.ok res.body From b8d43fabcc2e863dc8a2f81a16400b0ebce4773c Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Thu, 21 Feb 2013 20:38:12 -0500 Subject: [PATCH 006/220] wip --- src/server/api.coffee | 20 ++++++++++++++++---- src/server/store.coffee | 2 ++ test/api.mocha.coffee | 30 +++++++++++++++++++++++------- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 808c25f32d..c2a227d346 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -28,16 +28,28 @@ router.get '/user', (req, res) -> query = model.query('users').withIdAndToken(uid, token) query.fetch (err, user) -> - throw err if err - console.log user.at(0).get() - self = user.get() - console.log self return res.json 500, err: err if err + self = user.at(0).get() + console.log self return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) return res.json self res.json {err: 'Something went wrong'} +router.post '/task', (req, res) -> + { uid, token } = req.body + return res.json 500, NO_TOKEN_OR_UID unless uid || token + + model = req.getModel() + query = model.query('users').withIdAndToken(uid, token) + + query.fetch (err, user) -> + return res.json 500, err: err if err + self = user.at(0).get() + return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) + + return res.json self + res.json {err: 'Something went wrong'} router.get '/user/calendar.ics', (req, res) -> #return next() #disable for now diff --git a/src/server/store.coffee b/src/server/store.coffee index 41ec5dd9ad..0dc2af090e 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -62,6 +62,8 @@ REST = (store) -> return accept(true) if uid && token accept(false) # only user has id & token + #store.query.expose + ### Party permissions diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 4e73aeb3f6..7a0b769529 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -110,20 +110,36 @@ describe 'API', -> params = uid: user.id token: user.apiToken + setTimeout -> + done() + , 800 - done() - - it '/api/v1/user', (done) -> + ### + test '/api/v1/user', (done) -> console.log "#{baseURL}/user?#{qs.stringify(params)}" - request.get("#{baseURL}/user") + _.defer -> + request.get("#{baseURL}/user") + .set('Accept', 'application/json') + .query(params) + .on('error', (err) -> + console.log 'err', err + ) + .end (res) -> + assert.ok !res.body.err + assert.equal res.statusCode, 200 + assert.ok res.body.tasks + done() + ### + it '/api/v1/task', (done) -> + request.post("#{baseURL}/task") .set('Accept', 'application/json') - .query(params) + .send(params) .on('error', (err) -> console.log 'err', err ) .end (res) -> - console.log res.body + #console.log 'task', res.body assert.ok !res.body.err assert.equal res.statusCode, 200 - assert.ok res.body + assert.ok res.body.tasks done() From 9f044d2cf4f2003be5f8ce073236e51fbc7f70f4 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Fri, 22 Feb 2013 13:54:35 -0500 Subject: [PATCH 007/220] tests --- test/api.mocha.coffee | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 7a0b769529..14d17ec457 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -110,9 +110,7 @@ describe 'API', -> params = uid: user.id token: user.apiToken - setTimeout -> - done() - , 800 + done() ### test '/api/v1/user', (done) -> From 55afba34ae56be12815c72fac6e5c0edf4b24d25 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Fri, 22 Feb 2013 13:55:09 -0500 Subject: [PATCH 008/220] Nothing went wrong --- src/server/api.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index c2a227d346..95cd0ddf79 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -34,7 +34,6 @@ router.get '/user', (req, res) -> return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) return res.json self - res.json {err: 'Something went wrong'} router.post '/task', (req, res) -> { uid, token } = req.body @@ -49,7 +48,6 @@ router.post '/task', (req, res) -> return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) return res.json self - res.json {err: 'Something went wrong'} router.get '/user/calendar.ics', (req, res) -> #return next() #disable for now From e557bf89a74b23b3ade79c06b995c1a056dd9d31 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Fri, 22 Feb 2013 13:56:24 -0500 Subject: [PATCH 009/220] Status test --- test/api.mocha.coffee | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 14d17ec457..a346c06370 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -88,6 +88,14 @@ describe 'API', -> describe 'Without token or user id', -> + it '/api/v1/status', (done) -> + request.get("#{baseURL}/status") + .set('Accept', 'application/json') + .end (res) -> + assert.equal res.statusCode, 200 + assert.equal res.body.status, 'up' + done() + it '/api/v1/user', (done) -> request.get("#{baseURL}/user") .set('Accept', 'application/json') From 32323a39afbab009b5615d72236fc1dd1ac21aa8 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Fri, 22 Feb 2013 22:11:46 -0500 Subject: [PATCH 010/220] wip --- server.js | 6 ++---- src/server/api.coffee | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/server.js b/server.js index 50493a836c..7aae084865 100644 --- a/server.js +++ b/server.js @@ -1,10 +1,8 @@ // Load nconf and define default configuration values if config.json or ENV vars are not found var conf = require('nconf'); conf.argv().env().file({ file: __dirname + "/config.json" }).defaults({ - 'PORT': 3000, - 'IP': '0.0.0.0', - 'BASE_URL': 'http://localhost', - 'NODE_ENV': 'development' + 'PORT': process.env.PORT, + 'IP': process.env.IP }); // Override normal ENV values with nconf ENV values (ENV values are used the same way without nconf) diff --git a/src/server/api.coffee b/src/server/api.coffee index 95cd0ddf79..7bbc457611 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -3,7 +3,10 @@ router = new express.Router() scoring = require '../app/scoring' _ = require 'underscore' -icalendar = require('icalendar') +validator = require 'validator' +check = validator.check +snaitize = validator.sanitize +icalendar = require 'icalendar' NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request" NO_USER_FOUND = err: "No user found." @@ -35,8 +38,9 @@ router.get '/user', (req, res) -> return res.json self -router.post '/task', (req, res) -> +router.post '/user/tasks', (req, res) -> { uid, token } = req.body + task = { title, text, type, value, note } = req.body return res.json 500, NO_TOKEN_OR_UID unless uid || token model = req.getModel() @@ -46,6 +50,13 @@ router.post '/task', (req, res) -> return res.json 500, err: err if err self = user.at(0).get() return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) + return res.json 500, err: "type must be habit, todo, daily, reward" unless check(type).is /habit|todo|daily|reward/ + return res.json 500, err: "must have a title" unless check(title).notEmpty() + return res.json 500, err: "must have text" unless check(text).notEmpty() + value ||= 0 + + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + model.push "_#{type}List", task return res.json self From 4358455ef237d2a99699c159485fe7550905b882 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Fri, 22 Feb 2013 22:24:21 -0500 Subject: [PATCH 011/220] update --- src/server/api.coffee | 4 ++-- test/api.mocha.coffee | 29 ++++++++++++++--------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 7bbc457611..979e1ead6b 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -54,11 +54,11 @@ router.post '/user/tasks', (req, res) -> return res.json 500, err: "must have a title" unless check(title).notEmpty() return res.json 500, err: "must have text" unless check(text).notEmpty() value ||= 0 - + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" model.push "_#{type}List", task - return res.json self + return res.json task router.get '/user/calendar.ics', (req, res) -> #return next() #disable for now diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index a346c06370..3936315482 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -120,22 +120,21 @@ describe 'API', -> token: user.apiToken done() - ### - test '/api/v1/user', (done) -> + it '/api/v1/user', (done) -> console.log "#{baseURL}/user?#{qs.stringify(params)}" - _.defer -> - request.get("#{baseURL}/user") - .set('Accept', 'application/json') - .query(params) - .on('error', (err) -> - console.log 'err', err - ) - .end (res) -> - assert.ok !res.body.err - assert.equal res.statusCode, 200 - assert.ok res.body.tasks - done() - ### + request.get("#{baseURL}/user") + .set('Accept', 'application/json') + .query(params) + .on('error', (err) -> + console.log 'err', err + ) + .end (res) -> + assert.ok !res.body.err + assert.equal res.statusCode, 200 + assert.ok res.body + console.log res.body + done() + it '/api/v1/task', (done) -> request.post("#{baseURL}/task") .set('Accept', 'application/json') From 02569c2dfa663c47441ae8a1af41dd92a50e8c06 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Fri, 22 Feb 2013 22:30:28 -0500 Subject: [PATCH 012/220] Fix errors --- package.json | 3 ++- src/server/middleware.coffee | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index d2b409f62d..4f5e2c3e1d 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "querystring": "~0.1.0", "superagent": "~0.12.4", "resolve": "~0.2.3", - "browserify": "1.17.3" + "browserify": "1.17.3", + "validator": "~0.4.24" }, "private": true, "subdomain": "habitrpg", diff --git a/src/server/middleware.coffee b/src/server/middleware.coffee index 6fa8210a1d..e2183f2ea9 100644 --- a/src/server/middleware.coffee +++ b/src/server/middleware.coffee @@ -1,17 +1,17 @@ module.exports.splash = (req, res, next) -> # This was an API call, not a page load -return next() if /^\/(api)|(v1)/.test req.path + return next() if /^\/(api)|(v1)/.test req.path -unless req.query?.play? or req.getModel().get('_userId') - res.redirect('/splash.html') -else - next() + unless req.query?.play? or req.getModel().get('_userId') + res.redirect('/splash.html') + else + next() module.exports.view = (req, res, next) -> - model = req.getModel() - _view = model.get('_view') || {} - ## Set _mobileDevice to true or false so view can exclude portions from mobile device - _view.mobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header 'User-Agent') - _view.nodeEnv = process.env.NODE_ENV - model.set '_view', _view - next() + model = req.getModel() + _view = model.get('_view') || {} + ## Set _mobileDevice to true or false so view can exclude portions from mobile device + _view.mobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header 'User-Agent') + _view.nodeEnv = process.env.NODE_ENV + model.set '_view', _view + next() From 177c7e4f170e656bdf222433bad09c3987cb0b78 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Sat, 23 Feb 2013 03:20:16 -0500 Subject: [PATCH 013/220] Working again --- package.json | 5 ++--- server.js | 6 ++++-- src/server/api.coffee | 12 ++++++------ src/server/index.coffee | 3 ++- src/server/middleware.coffee | 3 --- test/api.mocha.coffee | 28 ++++++++++++++++------------ 6 files changed, 30 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 4f5e2c3e1d..1c8fa39a9d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "./server.js", "dependencies": { "derby": "git://github.com/lefnire/derby#habitrpg", - "racer": "git://github.com/lefnire/racer#habitrpg", + "racer": "git://github.com/switz/racer#master", "racer-db-mongo": "git://github.com/lefnire/racer-db-mongo#habitrpg", "derby-ui-boot": "git://github.com/codeparty/derby-ui-boot#master", "derby-auth": "git://github.com/lefnire/derby-auth#master", @@ -27,8 +27,7 @@ "querystring": "~0.1.0", "superagent": "~0.12.4", "resolve": "~0.2.3", - "browserify": "1.17.3", - "validator": "~0.4.24" + "browserify": "1.17.3" }, "private": true, "subdomain": "habitrpg", diff --git a/server.js b/server.js index c19fa948ec..98b139b185 100644 --- a/server.js +++ b/server.js @@ -1,8 +1,8 @@ // Load nconf and define default configuration values if config.json or ENV vars are not found var conf = require('nconf'); conf.argv().env().file({ file: __dirname + "/config.json" }).defaults({ - 'PORT': process.env.PORT, - 'IP': process.env.IP + 'PORT': 3000, + 'IP': '0.0.0.0' }); // Override normal ENV values with nconf ENV values (ENV values are used the same way without nconf) @@ -59,6 +59,8 @@ process.on('uncaughtException', function (error) { require('coffee-script') // remove intermediate compilation requirement module.exports = server = require('./src/server') +Error.stackTraceLimit = Infinity; + server.listen(process.env.PORT || 3000, process.env.IP || '0.0.0.0'); // Note: removed "up" module, which is default for development (but interferes with and production + PaaS) diff --git a/src/server/api.coffee b/src/server/api.coffee index 262712e193..8d9db75b52 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -3,7 +3,7 @@ router = new express.Router() scoring = require '../app/scoring' _ = require 'underscore' -validator = require 'validator' +validator = require 'derby-auth/node_modules/validator' check = validator.check snaitize = validator.sanitize icalendar = require 'icalendar' @@ -24,7 +24,7 @@ router.get '/status', (req, res) -> status: 'up' router.get '/user', (req, res) -> - console.log 'hi', { uid, token } = req.query + { uid, token } = req.query return res.json 500, NO_TOKEN_OR_UID unless uid || token model = req.getModel() @@ -33,12 +33,11 @@ router.get '/user', (req, res) -> query.fetch (err, user) -> return res.json 500, err: err if err self = user.at(0).get() - console.log self return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) return res.json self -router.post '/user/tasks', (req, res) -> +router.post '/user/task', (req, res) -> { uid, token } = req.body task = { title, text, type, value, note } = req.body return res.json 500, NO_TOKEN_OR_UID unless uid || token @@ -50,15 +49,16 @@ router.post '/user/tasks', (req, res) -> return res.json 500, err: err if err self = user.at(0).get() return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) - return res.json 500, err: "type must be habit, todo, daily, reward" unless check(type).is /habit|todo|daily|reward/ + return res.json 500, err: "type must be habit, todo, daily, reward" unless /habit|todo|daily|reward/.test type return res.json 500, err: "must have a title" unless check(title).notEmpty() return res.json 500, err: "must have text" unless check(text).notEmpty() value ||= 0 + #model.ref '_user', user model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" model.push "_#{type}List", task - return res.json task + return res.json 201, task router.get '/users/:uid/calendar.ics', (req, res) -> #return next() #disable for now diff --git a/src/server/index.coffee b/src/server/index.coffee index aaaabe8f00..c760e9628e 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -71,6 +71,8 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, -> ) # Adds req.getModel method .use(store.modelMiddleware()) + # API should be hit before all other routes + .use('/api/v1', require('./api').middleware) # Show splash page for newcomers .use(middleware.splash) .use(priv.middleware) @@ -78,7 +80,6 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, -> .use(auth.middleware(strategies, options)) # Creates an express middleware from the app's routes .use(app.router()) - .use('/api/v1', require('./api').middleware) .use(require('./static').middleware) .use(require('./deprecated').middleware) .use(expressApp.router) diff --git a/src/server/middleware.coffee b/src/server/middleware.coffee index e2183f2ea9..9ad133788d 100644 --- a/src/server/middleware.coffee +++ b/src/server/middleware.coffee @@ -1,7 +1,4 @@ module.exports.splash = (req, res, next) -> - # This was an API call, not a page load - return next() if /^\/(api)|(v1)/.test req.path - unless req.query?.play? or req.getModel().get('_userId') res.redirect('/splash.html') else diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 3936315482..33a7148068 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -113,13 +113,18 @@ describe 'API', -> user = character.newUserObject() user.apiToken = derby.uuid() model.set "users.#{uid}", user - user = model.get("users.#{uid}") + user = model.at("users.#{uid}") + currentUser = user.get() params = - uid: user.id - token: user.apiToken + uid: currentUser.id + token: currentUser.apiToken + title: 'Title' + text: 'Text' + type: 'habit' done() + ### it '/api/v1/user', (done) -> console.log "#{baseURL}/user?#{qs.stringify(params)}" request.get("#{baseURL}/user") @@ -133,18 +138,17 @@ describe 'API', -> assert.equal res.statusCode, 200 assert.ok res.body console.log res.body - done() + done() + ### - it '/api/v1/task', (done) -> - request.post("#{baseURL}/task") + it '/api/v1/user/task', (done) -> + request.post("#{baseURL}/user/task") .set('Accept', 'application/json') .send(params) - .on('error', (err) -> - console.log 'err', err - ) .end (res) -> - #console.log 'task', res.body + currentUser = user.get() assert.ok !res.body.err - assert.equal res.statusCode, 200 - assert.ok res.body.tasks + assert.equal res.statusCode, 201 + assert.ok res.body.id + assert.ok currentUser.tasks[res.body.id] done() From bddd0bd148e0900d958fafe2e7bfdb52094f38ee Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Sat, 23 Feb 2013 04:45:00 -0500 Subject: [PATCH 014/220] wip --- package.json | 3 ++- src/server/api.coffee | 2 +- src/server/store.coffee | 7 ++++++- test/api.mocha.coffee | 21 ++++++++++++--------- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 1c8fa39a9d..467223683a 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "querystring": "~0.1.0", "superagent": "~0.12.4", "resolve": "~0.2.3", - "browserify": "1.17.3" + "browserify": "1.17.3", + "expect.js": "~0.2.0" }, "private": true, "subdomain": "habitrpg", diff --git a/src/server/api.coffee b/src/server/api.coffee index 8d9db75b52..d1280731a5 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -54,7 +54,7 @@ router.post '/user/task', (req, res) -> return res.json 500, err: "must have text" unless check(text).notEmpty() value ||= 0 - #model.ref '_user', user + model.ref '_user', user.at(0) model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" model.push "_#{type}List", task diff --git a/src/server/store.coffee b/src/server/store.coffee index cebd70e5cd..4d71f36262 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -28,7 +28,12 @@ userAccess = (store) -> store.writeAccess "*", "users.*", -> # captures, value, accept, err -> accept = arguments[arguments.length-2] err = arguments[arguments.length - 1] -# return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@) + # return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@) + + # TEMPORARY token check to allow api + # Must fix before release + return accept(true) + return accept(false) if derbyAuth.bustedSession(@) captures = arguments[0].split('.') diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 33a7148068..f3499740df 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -1,4 +1,4 @@ -assert = require 'assert' +expect = require 'expect.js' {BrowserModel: Model} = require 'racer/test/util/model' derby = require 'derby' racer = require 'racer' @@ -92,16 +92,16 @@ describe 'API', -> request.get("#{baseURL}/status") .set('Accept', 'application/json') .end (res) -> - assert.equal res.statusCode, 200 - assert.equal res.body.status, 'up' + expect(res.statusCode).to.be 200 + expect(res.body.status).to.be 'up' done() it '/api/v1/user', (done) -> request.get("#{baseURL}/user") .set('Accept', 'application/json') .end (res) -> - assert.equal res.statusCode, 500 - assert.ok JSON.parse(res.text).err + expect(res.statusCode).to.be 500 + expect(res.body.err).to.be 'You must include a token and uid (user id) in your request' done() describe 'With token and user id', -> @@ -114,6 +114,7 @@ describe 'API', -> user.apiToken = derby.uuid() model.set "users.#{uid}", user user = model.at("users.#{uid}") + user.set 'tasks', {} currentUser = user.get() params = @@ -147,8 +148,10 @@ describe 'API', -> .send(params) .end (res) -> currentUser = user.get() - assert.ok !res.body.err - assert.equal res.statusCode, 201 - assert.ok res.body.id - assert.ok currentUser.tasks[res.body.id] + expect(currentUser).to.eql res.body + expect(res.body.err).to.be.empty() + expect(res.statusCode).to.be 201 + expect(res.body.id).not.to.be.empty() + # Ensure that user owns the newly created object + #expect(currentUser.tasks[res.body.id]).to.be.an('object') done() From faa94b3692560f2574e7b74d48828dce191e9dfc Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Sun, 24 Feb 2013 23:00:22 -0500 Subject: [PATCH 015/220] Almost perfect --- src/server/api.coffee | 69 +++++++++++++++++++++++++------------------ test/api.mocha.coffee | 58 +++++++++++++++++++++--------------- 2 files changed, 75 insertions(+), 52 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index d1280731a5..cdf5ce1bda 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -19,46 +19,57 @@ NO_USER_FOUND = err: "No user found." curl -X POST -H "Content-Type:application/json" -d '{"apiToken":"{TOKEN}"}' localhost:3000/v1/users/{UID}/tasks/productivity/up ### +auth = (req, res, next) -> + express.basicAuth((uid, token, callback) -> + return res.json 500, NO_TOKEN_OR_UID unless uid || token + + model = req.getModel() + query = model.query('users').withIdAndToken(uid, token) + + query.fetch (err, user) -> + return callback err if err + callback null, user.at(0) + )(req, res, next) + router.get '/status', (req, res) -> - res.json - status: 'up' + res.json status: 'up' -router.get '/user', (req, res) -> - { uid, token } = req.query - return res.json 500, NO_TOKEN_OR_UID unless uid || token +router.get '/user', auth, (req, res) -> + self = req.user.get() + return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) - model = req.getModel() - query = model.query('users').withIdAndToken(uid, token) + return res.json self - query.fetch (err, user) -> - return res.json 500, err: err if err - self = user.at(0).get() - return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) - - return res.json self - -router.post '/user/task', (req, res) -> - { uid, token } = req.body +router.post '/user/task', auth, (req, res) -> task = { title, text, type, value, note } = req.body - return res.json 500, NO_TOKEN_OR_UID unless uid || token + return res.json 500, err: "type must be habit, todo, daily, or reward" unless /habit|todo|daily|reward/.test type + return res.json 500, err: "must have a title" unless check(title).notEmpty() + return res.json 500, err: "must have text" unless check(text).notEmpty() + + self = req.user.get() + return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) + + value ||= 0 model = req.getModel() - query = model.query('users').withIdAndToken(uid, token) + model.ref '_user', req.user + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + model.at("_#{type}List").push task - query.fetch (err, user) -> - return res.json 500, err: err if err - self = user.at(0).get() - return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) - return res.json 500, err: "type must be habit, todo, daily, reward" unless /habit|todo|daily|reward/.test type - return res.json 500, err: "must have a title" unless check(title).notEmpty() - return res.json 500, err: "must have text" unless check(text).notEmpty() - value ||= 0 + return res.json 201, task - model.ref '_user', user.at(0) +router.get '/user/tasks', auth, (req, res) -> + self = req.user.get() + return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) + + model = req.getModel() + model.ref '_user', req.user + tasks = [] + for type in ['habit','todo','daily','reward'] model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" - model.push "_#{type}List", task + tasks = tasks.concat model.get("_#{type}List") - return res.json 201, task + return res.json 200, tasks router.get '/users/:uid/calendar.ics', (req, res) -> #return next() #disable for now diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index f3499740df..3b5d658636 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -82,10 +82,6 @@ modificationsLookup = (direction, options = {}) -> ###### Specs ###### describe 'API', -> - model = null - user = null - params = null - describe 'Without token or user id', -> it '/api/v1/status', (done) -> @@ -105,6 +101,11 @@ describe 'API', -> done() describe 'With token and user id', -> + params = null + currentUser = null + user = null + model = null + before (done) -> model = store.createModel() #store.flush() @@ -114,9 +115,7 @@ describe 'API', -> user.apiToken = derby.uuid() model.set "users.#{uid}", user user = model.at("users.#{uid}") - user.set 'tasks', {} currentUser = user.get() - params = uid: currentUser.id token: currentUser.apiToken @@ -125,33 +124,46 @@ describe 'API', -> type: 'habit' done() - ### - it '/api/v1/user', (done) -> - console.log "#{baseURL}/user?#{qs.stringify(params)}" + beforeEach -> + currentUser = user.get() + + it 'GET /api/v1/user', (done) -> request.get("#{baseURL}/user") .set('Accept', 'application/json') - .query(params) - .on('error', (err) -> - console.log 'err', err - ) + .auth(params.uid, params.token) .end (res) -> - assert.ok !res.body.err - assert.equal res.statusCode, 200 - assert.ok res.body - console.log res.body - done() - ### + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + expect(res.body.id).not.to.be.empty() + expect(res.body).to.eql(currentUser) + done() - it '/api/v1/user/task', (done) -> + it 'POST /api/v1/user/task', (done) -> request.post("#{baseURL}/user/task") .set('Accept', 'application/json') + .auth(params.uid, params.token) .send(params) .end (res) -> - currentUser = user.get() - expect(currentUser).to.eql res.body - expect(res.body.err).to.be.empty() + expect(res.body.err).to.be undefined expect(res.statusCode).to.be 201 expect(res.body.id).not.to.be.empty() # Ensure that user owns the newly created object #expect(currentUser.tasks[res.body.id]).to.be.an('object') done() + + it 'GET /api/v1/user/tasks', (done) -> + request.get("#{baseURL}/user/tasks") + .set('Accept', 'application/json') + .auth(params.uid, params.token) + .end (res) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + model.set '_user', user.get() + tasks = [] + for type in ['habit','todo','daily','reward'] + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + tasks = tasks.concat model.get("_#{type}List") + # Ensure that user owns the tasks + console.log _.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')) + expect(res.body.length).to.equal tasks.length + done() From bfd30cd4b72d2e6f64a5505cff5d94a49a0aadef Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Mon, 25 Feb 2013 00:24:11 -0500 Subject: [PATCH 016/220] wip --- src/server/api.coffee | 3 ++- test/api.mocha.coffee | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index cdf5ce1bda..f974d71bca 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -28,7 +28,8 @@ auth = (req, res, next) -> query.fetch (err, user) -> return callback err if err - callback null, user.at(0) + user = user.at(0) + callback null, user )(req, res, next) router.get '/status', (req, res) -> diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 3b5d658636..694a41578d 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -106,9 +106,9 @@ describe 'API', -> user = null model = null - before (done) -> - model = store.createModel() + before -> #store.flush() + model = store.createModel() model.set '_userId', uid = model.id() user = character.newUserObject() @@ -117,12 +117,9 @@ describe 'API', -> user = model.at("users.#{uid}") currentUser = user.get() params = - uid: currentUser.id - token: currentUser.apiToken title: 'Title' text: 'Text' type: 'habit' - done() beforeEach -> currentUser = user.get() @@ -130,7 +127,7 @@ describe 'API', -> it 'GET /api/v1/user', (done) -> request.get("#{baseURL}/user") .set('Accept', 'application/json') - .auth(params.uid, params.token) + .auth(currentUser.id, currentUser.apiToken) .end (res) -> expect(res.body.err).to.be undefined expect(res.statusCode).to.be 200 @@ -141,24 +138,28 @@ describe 'API', -> it 'POST /api/v1/user/task', (done) -> request.post("#{baseURL}/user/task") .set('Accept', 'application/json') - .auth(params.uid, params.token) + .auth(currentUser.id, currentUser.apiToken) .send(params) .end (res) -> expect(res.body.err).to.be undefined + console.log res.body expect(res.statusCode).to.be 201 expect(res.body.id).not.to.be.empty() # Ensure that user owns the newly created object - #expect(currentUser.tasks[res.body.id]).to.be.an('object') + console.log _.size(user.get().tasks) + expect(user.get().tasks[res.body.id]).to.be.an('object') done() it 'GET /api/v1/user/tasks', (done) -> request.get("#{baseURL}/user/tasks") .set('Accept', 'application/json') - .auth(params.uid, params.token) + .auth(currentUser.id, currentUser.apiToken) .end (res) -> expect(res.body.err).to.be undefined expect(res.statusCode).to.be 200 - model.set '_user', user.get() + currentUser = user.get() + console.log _.size(currentUser.tasks) + model.ref '_user', user tasks = [] for type in ['habit','todo','daily','reward'] model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" From 1be9fb97fec7f4323395bdca069320ac0d3f6189 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Mon, 25 Feb 2013 02:43:50 -0500 Subject: [PATCH 017/220] Use headers instead of auth --- src/server/api.coffee | 18 +++++++++--------- test/api.mocha.coffee | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index f974d71bca..d7bf239475 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -20,17 +20,17 @@ NO_USER_FOUND = err: "No user found." ### auth = (req, res, next) -> - express.basicAuth((uid, token, callback) -> - return res.json 500, NO_TOKEN_OR_UID unless uid || token + uid = req.headers['x-api-user'] + token = req.headers['x-api-key'] + return res.json 500, NO_TOKEN_OR_UID unless uid || token - model = req.getModel() - query = model.query('users').withIdAndToken(uid, token) + model = req.getModel() + query = model.query('users').withIdAndToken(uid, token) - query.fetch (err, user) -> - return callback err if err - user = user.at(0) - callback null, user - )(req, res, next) + query.fetch (err, user) -> + return res.json err: err if err + req.user = user.at(0) + next() router.get '/status', (req, res) -> res.json status: 'up' diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 694a41578d..077a7df450 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -7,6 +7,25 @@ moment = require 'moment' request = require 'superagent' qs = require 'querystring' +## monkey-patch expect.js for better diffs on mocha +## see: https://github.com/LearnBoost/expect.js/pull/34 + +origBe = expect.Assertion::be +expect.Assertion::be = expect.Assertion::equal = (obj) -> + @_expected = obj + origBe.call this, obj + +expect.Assertion::assert = (truth, msg, error) -> + msg = (if @flags.not then error else msg) + ok = (if @flags.not then not truth else truth) + unless ok + err = new Error(msg.call(this)) + if "_expected" of this + err.expected = @_expected + err.actual = @obj + throw err + @and = new expect.Assertion(@obj) + racer.use require 'racer-db-mongo' store = racer.createStore @@ -127,7 +146,8 @@ describe 'API', -> it 'GET /api/v1/user', (done) -> request.get("#{baseURL}/user") .set('Accept', 'application/json') - .auth(currentUser.id, currentUser.apiToken) + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) .end (res) -> expect(res.body.err).to.be undefined expect(res.statusCode).to.be 200 @@ -138,11 +158,11 @@ describe 'API', -> it 'POST /api/v1/user/task', (done) -> request.post("#{baseURL}/user/task") .set('Accept', 'application/json') - .auth(currentUser.id, currentUser.apiToken) + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) .send(params) .end (res) -> expect(res.body.err).to.be undefined - console.log res.body expect(res.statusCode).to.be 201 expect(res.body.id).not.to.be.empty() # Ensure that user owns the newly created object @@ -153,7 +173,8 @@ describe 'API', -> it 'GET /api/v1/user/tasks', (done) -> request.get("#{baseURL}/user/tasks") .set('Accept', 'application/json') - .auth(currentUser.id, currentUser.apiToken) + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) .end (res) -> expect(res.body.err).to.be undefined expect(res.statusCode).to.be 200 @@ -166,5 +187,8 @@ describe 'API', -> tasks = tasks.concat model.get("_#{type}List") # Ensure that user owns the tasks console.log _.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')) - expect(res.body.length).to.equal tasks.length - done() + #expect(res.body.length).to.equal tasks.length + setTimeout -> + console.log 'hi', _.size(user.get().tasks) + done() + , 1100 From 7ce767781662e4b5611e6f1ee33056f9c369a43e Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Mon, 25 Feb 2013 02:44:57 -0500 Subject: [PATCH 018/220] Cleanup --- test/api.mocha.coffee | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 077a7df450..42387f2217 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -187,8 +187,5 @@ describe 'API', -> tasks = tasks.concat model.get("_#{type}List") # Ensure that user owns the tasks console.log _.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')) - #expect(res.body.length).to.equal tasks.length - setTimeout -> - console.log 'hi', _.size(user.get().tasks) - done() - , 1100 + expect(res.body.length).to.equal tasks.length + done() From e4f42270726f5f147f7df9c4496cd28de035d8cb Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Mon, 25 Feb 2013 14:32:46 -0500 Subject: [PATCH 019/220] Cleanup --- src/server/api.coffee | 5 ++--- test/api.mocha.coffee | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index d7bf239475..e6aa77476d 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -11,12 +11,11 @@ icalendar = require 'icalendar' NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request" NO_USER_FOUND = err: "No user found." -# ---------- /v1 API ------------ -# Every url added beneath router is prefaced by /v1 +# ---------- /api/v1 API ------------ +# Every url added beneath router is prefaced by /api/v1 ### v1 API. Requires user-id and apiToken, task-id, direction. Test with: - curl -X POST -H "Content-Type:application/json" -d '{"apiToken":"{TOKEN}"}' localhost:3000/v1/users/{UID}/tasks/productivity/up ### auth = (req, res, next) -> diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 42387f2217..d9d6e95a67 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -186,6 +186,7 @@ describe 'API', -> model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" tasks = tasks.concat model.get("_#{type}List") # Ensure that user owns the tasks - console.log _.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')) expect(res.body.length).to.equal tasks.length + # Ensure that the two sets are equal + expect(_.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')).length).to.equal 0 done() From f9f57a6b91ffb736f9b2ba16d8ae5aba8c3f7287 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Tue, 26 Feb 2013 02:36:51 -0500 Subject: [PATCH 020/220] Cleanup and more checks --- server.js | 4 +- src/server/api.coffee | 26 +++++++++---- test/api.mocha.coffee | 26 ++++++++++++- test/request/local.coffee | 25 ------------- test/request/request.coffee | 74 ------------------------------------- 5 files changed, 46 insertions(+), 109 deletions(-) delete mode 100644 test/request/local.coffee delete mode 100644 test/request/request.coffee diff --git a/server.js b/server.js index 98b139b185..d7fc162cf7 100644 --- a/server.js +++ b/server.js @@ -19,7 +19,7 @@ process.env.SMTP_PASS = conf.get("SMTP_PASS"); process.env.SMTP_SERVICE = conf.get("SMTP_SERVICE"); process.env.STRIPE_API_KEY = conf.get("STRIPE_API_KEY"); process.env.STRIPE_PUB_KEY = conf.get("STRIPE_PUB_KEY"); - +/* process.on('uncaughtException', function (error) { function sendEmail(mailData) { @@ -55,7 +55,7 @@ process.on('uncaughtException', function (error) { }); console.log(error.stack); }); - +*/ require('coffee-script') // remove intermediate compilation requirement module.exports = server = require('./src/server') diff --git a/src/server/api.coffee b/src/server/api.coffee index e6aa77476d..88bf0cae77 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -15,7 +15,9 @@ NO_USER_FOUND = err: "No user found." # Every url added beneath router is prefaced by /api/v1 ### - v1 API. Requires user-id and apiToken, task-id, direction. Test with: + v1 API. Requires api-v1-user (user id) and api-v1-key (api key) headers, Test with: + $ cd node_modules/racer && npm install && cd ../.. + $ mocha test/api.mocha.coffee ### auth = (req, res, next) -> @@ -28,26 +30,36 @@ auth = (req, res, next) -> query.fetch (err, user) -> return res.json err: err if err - req.user = user.at(0) + user = user.at(0) + req.user = user + req.userObj = user.get() + return res.json 500, NO_USER_FOUND if !req.userObj || _.isEmpty(req.userObj) next() router.get '/status', (req, res) -> res.json status: 'up' router.get '/user', auth, (req, res) -> - self = req.user.get() - return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) + self = req.userObj + + delete self[val] for val in ['tasks', 'apiToken', 'flags', 'lastCron'] return res.json self +router.get '/task/:id', auth, (req, res) -> + task = req.user.get("tasks.#{req.params.id}") + console.log task + return res.json 500, err: "No task found." if !task || _.isEmpty(task) + + return res.json 200, task + router.post '/user/task', auth, (req, res) -> task = { title, text, type, value, note } = req.body return res.json 500, err: "type must be habit, todo, daily, or reward" unless /habit|todo|daily|reward/.test type return res.json 500, err: "must have a title" unless check(title).notEmpty() return res.json 500, err: "must have text" unless check(text).notEmpty() - self = req.user.get() - return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) + self = req.userObj value ||= 0 @@ -59,7 +71,7 @@ router.post '/user/task', auth, (req, res) -> return res.json 201, task router.get '/user/tasks', auth, (req, res) -> - self = req.user.get() + self = req.userObj return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) model = req.getModel() diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index d9d6e95a67..fc9a403e24 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -101,6 +101,7 @@ modificationsLookup = (direction, options = {}) -> ###### Specs ###### describe 'API', -> + describe 'Without token or user id', -> it '/api/v1/status', (done) -> @@ -124,6 +125,7 @@ describe 'API', -> currentUser = null user = null model = null + uid = null before -> #store.flush() @@ -141,6 +143,7 @@ describe 'API', -> type: 'habit' beforeEach -> + model = store.createModel() currentUser = user.get() it 'GET /api/v1/user', (done) -> @@ -152,9 +155,28 @@ describe 'API', -> expect(res.body.err).to.be undefined expect(res.statusCode).to.be 200 expect(res.body.id).not.to.be.empty() + model.set '_user', currentUser + ### + currentUser.tasks = [] + for type in ['habit','todo','daily','reward'] + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + currentUser.tasks = currentUser.tasks.concat model.get("_#{type}List") + ### expect(res.body).to.eql(currentUser) done() + it 'GET /api/v1/task/:id', (done) -> + tid = _.values(currentUser.tasks)[0].id + request.post("#{baseURL}/task/#{tid}") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .end (res) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + expect(res.body).to.eql currentUser.tasks[tid] + done() + it 'POST /api/v1/user/task', (done) -> request.post("#{baseURL}/user/task") .set('Accept', 'application/json') @@ -166,7 +188,7 @@ describe 'API', -> expect(res.statusCode).to.be 201 expect(res.body.id).not.to.be.empty() # Ensure that user owns the newly created object - console.log _.size(user.get().tasks) + console.log 'test', _.size(user.get().tasks) expect(user.get().tasks[res.body.id]).to.be.an('object') done() @@ -180,6 +202,8 @@ describe 'API', -> expect(res.statusCode).to.be 200 currentUser = user.get() console.log _.size(currentUser.tasks) + console.log uid + console.log 'hellomate', model.get("users.#{uid}") model.ref '_user', user tasks = [] for type in ['habit','todo','daily','reward'] diff --git a/test/request/local.coffee b/test/request/local.coffee deleted file mode 100644 index 1431d12cd5..0000000000 --- a/test/request/local.coffee +++ /dev/null @@ -1,25 +0,0 @@ -Request = require './request' -qs = require 'querystring' - -makeUrl = (path, params) -> - if typeof params is "object" - params = qs.stringify(params) - else - params = '' - return "http://localhost:3000/#{path}?#{params}" - -# url, params (optional), callback -get = (obj, callback) -> - obj.params ||= {} - obj.path ||= '' - - requestUrl = makeUrl obj.path, obj.params - - request = new Request requestUrl - - request.done (res) -> - callback null, JSON.parse(res) - - request.fire() - -module.exports = { get } diff --git a/test/request/request.coffee b/test/request/request.coffee deleted file mode 100644 index 5eadd9178b..0000000000 --- a/test/request/request.coffee +++ /dev/null @@ -1,74 +0,0 @@ -http = require 'http' - -# ### Request -# -# The Request class is just a wrapper -# around the `http.request` function, -# providing a cleaner and more reusable -# interface. -# -# `constructor(options)` -# -# `options` can be anything `http.request` takes. -# -# Callbacks can be queued up by calling `request.done` -# and passing it a callback function to be invoked when the -# http request has completed -# -# Example: -# -# callback = (json) -> -# doSomethingWithJSON( json ) -# -# request = new Request('http://api.phish.net/api.js?') -# request.done (response) -> -# callback JSON.parse(response)[0] -# -# request.fire(data) # data refers to the post data -# -class Request - constructor: (@options) -> - @callbacks = {} - - fire: (data) -> - request = http.request(@options, @_handler) - request.on 'error', (err) => - for callback in @callbacks['fail'] - callback(err) - - # Send data with request - if data - request.write data - - request.end() - - this - - done: (callback) -> - push.call(this, 'done', callback) - - this - - fail: (callback) -> - push.call(this, 'fail', callback) - - this - - ########### - # PRIVATE # - ########### - - _handler: (response) => - data = '' - response.on 'data', (chunk) -> - data += chunk - response.on 'end', => - for callback in @callbacks['done'] - callback(data) - - - push = (type, callback) -> - @callbacks[type] ?= [] - @callbacks[type].push callback - -module.exports = Request From 17a5040b3b7567df06b8738f9ce753fb03c6760e Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Tue, 26 Feb 2013 02:55:33 -0500 Subject: [PATCH 021/220] Server runs inline with tests --- test/api.mocha.coffee | 15 +++++++++++++-- test/mocha.opts | 3 ++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index fc9a403e24..f0c86f6825 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -6,6 +6,7 @@ _ = require 'underscore' moment = require 'moment' request = require 'superagent' qs = require 'querystring' +require 'coffee-script' ## monkey-patch expect.js for better diffs on mocha ## see: https://github.com/LearnBoost/expect.js/pull/34 @@ -101,6 +102,12 @@ modificationsLookup = (direction, options = {}) -> ###### Specs ###### describe 'API', -> + before (done) -> + server = require '../src/server' + server.listen '3000', '0.0.0.0' + server.on 'listening', (data) -> + # Crappy hack to let server start before tests run + setTimeout done, 2000 describe 'Without token or user id', -> @@ -143,7 +150,6 @@ describe 'API', -> type: 'habit' beforeEach -> - model = store.createModel() currentUser = user.get() it 'GET /api/v1/user', (done) -> @@ -198,12 +204,17 @@ describe 'API', -> .set('X-API-User', currentUser.id) .set('X-API-Key', currentUser.apiToken) .end (res) -> + store = racer.createStore + db: + type: 'Mongo' + uri: process.env.NODE_DB_URI + model = store.createModel() expect(res.body.err).to.be undefined expect(res.statusCode).to.be 200 currentUser = user.get() console.log _.size(currentUser.tasks) console.log uid - console.log 'hellomate', model.get("users.#{uid}") + console.log 'hellomate', user.get() model.ref '_user', user tasks = [] for type in ['habit','todo','daily','reward'] diff --git a/test/mocha.opts b/test/mocha.opts index d6ef947989..74e33e85ea 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,6 +1,7 @@ --colors --reporter spec ---timeout 1200 +--timeout 2500 +--ignore-leaks --growl --debug --compilers coffee:coffee-script From 52c54444530bfaa8b1dc38a72652093291788bef Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Tue, 26 Feb 2013 02:56:47 -0500 Subject: [PATCH 022/220] Remove second store --- test/api.mocha.coffee | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index f0c86f6825..2d965c48db 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -44,7 +44,7 @@ config = require './config' model = null uuid = null taskPath = null -baseURL = 'http://localhost:3000/api/v1' +baseURL = 'http://localhost:1337/api/v1' UID_AND_TOKEN = uid: config.uid token: config.token @@ -104,7 +104,7 @@ modificationsLookup = (direction, options = {}) -> describe 'API', -> before (done) -> server = require '../src/server' - server.listen '3000', '0.0.0.0' + server.listen '1337', '0.0.0.0' server.on 'listening', (data) -> # Crappy hack to let server start before tests run setTimeout done, 2000 @@ -204,11 +204,6 @@ describe 'API', -> .set('X-API-User', currentUser.id) .set('X-API-Key', currentUser.apiToken) .end (res) -> - store = racer.createStore - db: - type: 'Mongo' - uri: process.env.NODE_DB_URI - model = store.createModel() expect(res.body.err).to.be undefined expect(res.statusCode).to.be 200 currentUser = user.get() From 07648cd60b65181862e1251ba13aba6847c6391a Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Tue, 26 Feb 2013 03:48:10 -0500 Subject: [PATCH 023/220] all tests passing - woo --- src/server/api.coffee | 3 +- test/api.mocha.coffee | 90 +++++++++++++++++++++---------------------- 2 files changed, 44 insertions(+), 49 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 88bf0cae77..6ad138ffea 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -47,8 +47,7 @@ router.get '/user', auth, (req, res) -> return res.json self router.get '/task/:id', auth, (req, res) -> - task = req.user.get("tasks.#{req.params.id}") - console.log task + task = req.userObj.tasks[req.params.id] return res.json 500, err: "No task found." if !task || _.isEmpty(task) return res.json 200, task diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 2d965c48db..f9844d4de6 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -26,14 +26,14 @@ expect.Assertion::assert = (truth, msg, error) -> err.actual = @obj throw err @and = new expect.Assertion(@obj) - +### racer.use require 'racer-db-mongo' store = racer.createStore db: type: 'Mongo' uri: process.env.NODE_DB_URI - +### # Custom modules scoring = require '../src/app/scoring' character = require '../src/app/character' @@ -45,9 +45,6 @@ model = null uuid = null taskPath = null baseURL = 'http://localhost:1337/api/v1' -UID_AND_TOKEN = - uid: config.uid - token: config.token ## Helper which clones the content at a path so tests can compare before/after values # Otherwise, using model.get(path) will give the same object before as after @@ -102,10 +99,23 @@ modificationsLookup = (direction, options = {}) -> ###### Specs ###### describe 'API', -> + server = null + store = null + model = null + user = null + uid = null + before (done) -> server = require '../src/server' server.listen '1337', '0.0.0.0' server.on 'listening', (data) -> + store = server.habitStore + #store.flush() + model = store.createModel() + model.set '_userId', uid = model.id() + user = character.newUserObject() + user.apiToken = derby.uuid() + model.set "users.#{uid}", user # Crappy hack to let server start before tests run setTimeout done, 2000 @@ -130,18 +140,8 @@ describe 'API', -> describe 'With token and user id', -> params = null currentUser = null - user = null - model = null - uid = null before -> - #store.flush() - model = store.createModel() - - model.set '_userId', uid = model.id() - user = character.newUserObject() - user.apiToken = derby.uuid() - model.set "users.#{uid}", user user = model.at("users.#{uid}") currentUser = user.get() params = @@ -161,19 +161,15 @@ describe 'API', -> expect(res.body.err).to.be undefined expect(res.statusCode).to.be 200 expect(res.body.id).not.to.be.empty() - model.set '_user', currentUser - ### - currentUser.tasks = [] - for type in ['habit','todo','daily','reward'] - model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" - currentUser.tasks = currentUser.tasks.concat model.get("_#{type}List") - ### - expect(res.body).to.eql(currentUser) + self = _.clone(currentUser) + delete self[val] for val in ['tasks', 'apiToken', 'flags', 'lastCron'] + + expect(res.body).to.eql self done() it 'GET /api/v1/task/:id', (done) -> tid = _.values(currentUser.tasks)[0].id - request.post("#{baseURL}/task/#{tid}") + request.get("#{baseURL}/task/#{tid}") .set('Accept', 'application/json') .set('X-API-User', currentUser.id) .set('X-API-Key', currentUser.apiToken) @@ -190,13 +186,14 @@ describe 'API', -> .set('X-API-Key', currentUser.apiToken) .send(params) .end (res) -> - expect(res.body.err).to.be undefined - expect(res.statusCode).to.be 201 - expect(res.body.id).not.to.be.empty() - # Ensure that user owns the newly created object - console.log 'test', _.size(user.get().tasks) - expect(user.get().tasks[res.body.id]).to.be.an('object') - done() + query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken) + query.fetch (err, user) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 201 + expect(res.body.id).not.to.be.empty() + # Ensure that user owns the newly created object + expect(user.at(0).get().tasks[res.body.id]).to.be.an('object') + done() it 'GET /api/v1/user/tasks', (done) -> request.get("#{baseURL}/user/tasks") @@ -204,19 +201,18 @@ describe 'API', -> .set('X-API-User', currentUser.id) .set('X-API-Key', currentUser.apiToken) .end (res) -> - expect(res.body.err).to.be undefined - expect(res.statusCode).to.be 200 - currentUser = user.get() - console.log _.size(currentUser.tasks) - console.log uid - console.log 'hellomate', user.get() - model.ref '_user', user - tasks = [] - for type in ['habit','todo','daily','reward'] - model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" - tasks = tasks.concat model.get("_#{type}List") - # Ensure that user owns the tasks - expect(res.body.length).to.equal tasks.length - # Ensure that the two sets are equal - expect(_.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')).length).to.equal 0 - done() + query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken) + query.fetch (err, user) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + currentUser = user.at(0).get() + model.ref '_user', user.at(0) + tasks = [] + for type in ['habit','todo','daily','reward'] + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + tasks = tasks.concat model.get("_#{type}List") + # Ensure that user owns the tasks + expect(res.body.length).to.equal tasks.length + # Ensure that the two sets are equal + expect(_.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')).length).to.equal 0 + done() From c38f215db5ab2ce46020c5399ddc03f2f9092ed6 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Tue, 26 Feb 2013 16:44:26 -0500 Subject: [PATCH 024/220] Add type parameter to /user/tasks --- .gitignore | 1 - package.json | 3 +- server.js | 10 +---- src/server/api.coffee | 38 +++++++++------- test/api.mocha.coffee | 100 ++++++++++-------------------------------- test/mocha.opts | 2 +- 6 files changed, 50 insertions(+), 104 deletions(-) diff --git a/.gitignore b/.gitignore index 77291d9d0f..029b274b27 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,3 @@ node_modules *.swp .idea* config.json -test/config.json \ No newline at end of file diff --git a/package.json b/package.json index 08d33fc7ab..a5ab0f7bbe 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "./server.js", "dependencies": { "derby": "git://github.com/lefnire/derby#habitrpg", - "racer": "git://github.com/switz/racer#master", + "racer": "git://github.com/lefnire/racer#habitrpg", "racer-db-mongo": "git://github.com/lefnire/racer-db-mongo#habitrpg", "derby-ui-boot": "git://github.com/codeparty/derby-ui-boot#master", "derby-auth": "git://github.com/lefnire/derby-auth#master", @@ -24,7 +24,6 @@ "nconf": "*", "icalendar": "git://github.com/lefnire/node-icalendar#master", "nodetime": "*", - "querystring": "~0.1.0", "superagent": "~0.12.4", "resolve": "~0.2.3", "browserify": "1.17.3", diff --git a/server.js b/server.js index a95b925c12..f9d64274a7 100644 --- a/server.js +++ b/server.js @@ -20,8 +20,6 @@ process.env.SMTP_SERVICE = conf.get("SMTP_SERVICE"); process.env.STRIPE_API_KEY = conf.get("STRIPE_API_KEY"); process.env.STRIPE_PUB_KEY = conf.get("STRIPE_PUB_KEY"); -/* - var agent; if (process.env.NODE_ENV === 'development') { // Follow these instructions for profiling / debugging leaks @@ -68,13 +66,9 @@ process.on('uncaughtException', function (error) { }); console.log(error.stack); }); -*/ + require('coffee-script') // remove intermediate compilation requirement -module.exports = server = require('./src/server') - -Error.stackTraceLimit = Infinity; - -server.listen(process.env.PORT || 3000, process.env.IP || '0.0.0.0'); +require('./src/server').listen(process.env.PORT || 3000, process.env.IP || '0.0.0.0'); // Note: removed "up" module, which is default for development (but interferes with and production + PaaS) // Restore to 5310bb0 if I want it back (see https://github.com/codeparty/derby/issues/165#issuecomment-10405693) diff --git a/src/server/api.coffee b/src/server/api.coffee index 65c48a7d21..fba15fc09f 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -5,7 +5,7 @@ scoring = require '../app/scoring' _ = require 'underscore' validator = require 'derby-auth/node_modules/validator' check = validator.check -snaitize = validator.sanitize +sanitize = validator.sanitize icalendar = require 'icalendar' NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request" @@ -23,7 +23,7 @@ NO_USER_FOUND = err: "No user found." auth = (req, res, next) -> uid = req.headers['x-api-user'] token = req.headers['x-api-key'] - return res.json 500, NO_TOKEN_OR_UID unless uid || token + return res.json 401, NO_TOKEN_OR_UID unless uid || token model = req.getModel() query = model.query('users').withIdAndToken(uid, token) @@ -33,7 +33,7 @@ auth = (req, res, next) -> user = user.at(0) req.user = user req.userObj = user.get() - return res.json 500, NO_USER_FOUND if !req.userObj || _.isEmpty(req.userObj) + return res.json 401, NO_USER_FOUND if !req.userObj || _.isEmpty(req.userObj) next() router.get '/status', (req, res) -> @@ -48,27 +48,30 @@ router.get '/user', auth, (req, res) -> router.get '/task/:id', auth, (req, res) -> task = req.userObj.tasks[req.params.id] - return res.json 500, err: "No task found." if !task || _.isEmpty(task) + return res.json 400, err: "No task found." if !task || _.isEmpty(task) res.json 200, task router.put '/task/:id', auth, (req, res) -> task = req.userObj.tasks[req.params.id] - return res.json 500, err: "No task found." if !task || _.isEmpty(task) + return res.json 400, err: "No task found." if !task || _.isEmpty(task) - task.title = req.body.title if req.body.title - task.text = req.body.text if req.body.text - task.type = req.body.type if req.body.type + title = sanitize(req.body.title).xss() + text = sanitize(req.body.text).xss() + + task.title = title if title + task.text = text if text + #task.type = req.body.type if /^(habit|todo|daily|reward)$/.test req.body.type req.user.set "tasks.#{task.id}", task - res.send task + res.json 200, task router.post '/user/task', auth, (req, res) -> task = { title, text, type, value, note } = req.body - return res.json 500, err: "type must be habit, todo, daily, or reward" unless /habit|todo|daily|reward/.test type - return res.json 500, err: "must have a title" unless check(title).notEmpty() - return res.json 500, err: "must have text" unless check(text).notEmpty() + return res.json 400, err: "type must be habit, todo, daily, or reward" unless /^habit|todo|daily|reward$/.test type + return res.json 400, err: "must have a title" unless check(title).notEmpty() + return res.json 400, err: "must have text" unless check(text).notEmpty() self = req.userObj @@ -83,12 +86,15 @@ router.post '/user/task', auth, (req, res) -> router.get '/user/tasks', auth, (req, res) -> self = req.userObj - return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self) + return res.json 400, NO_USER_FOUND if !self || _.isEmpty(self) model = req.getModel() model.ref '_user', req.user tasks = [] - for type in ['habit','todo','daily','reward'] + types = ['habit','todo','daily','reward'] + if /^habit|todo|daily|reward$/.test req.query.type + types = [req.query.type] + for type in types model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" tasks = tasks.concat model.get("_#{type}List") @@ -102,11 +108,11 @@ router.get '/users/:uid/calendar.ics', (req, res) -> model = req.getModel() query = model.query('users').withIdAndToken(uid, apiToken) query.fetch (err, result) -> - return res.send(500, err) if err + return res.send(400, err) if err tasks = result.at(0).get('tasks') # tasks = result[0].tasks tasksWithDates = _.filter tasks, (task) -> !!task.date - return res.send(500, "No events found") if _.isEmpty(tasksWithDates) + return res.send(400, "No events found") if _.isEmpty(tasksWithDates) ical = new icalendar.iCalendar() ical.addProperty('NAME', 'HabitRPG') diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index e520688d24..1195eb1f2e 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -1,11 +1,6 @@ -expect = require 'expect.js' -{BrowserModel: Model} = require 'racer/test/util/model' -derby = require 'derby' -racer = require 'racer' _ = require 'underscore' -moment = require 'moment' request = require 'superagent' -qs = require 'querystring' +expect = require 'expect.js' require 'coffee-script' ## monkey-patch expect.js for better diffs on mocha @@ -16,24 +11,6 @@ expect.Assertion::be = expect.Assertion::equal = (obj) -> @_expected = obj origBe.call this, obj -expect.Assertion::assert = (truth, msg, error) -> - msg = (if @flags.not then error else msg) - ok = (if @flags.not then not truth else truth) - unless ok - err = new Error(msg.call(this)) - if "_expected" of this - err.expected = @_expected - err.actual = @obj - throw err - @and = new expect.Assertion(@obj) -### -racer.use require 'racer-db-mongo' - -store = racer.createStore - db: - type: 'Mongo' - uri: process.env.NODE_DB_URI -### # Custom modules scoring = require '../src/app/scoring' character = require '../src/app/character' @@ -46,56 +23,6 @@ uuid = null taskPath = null baseURL = 'http://localhost:1337/api/v1' -## Helper which clones the content at a path so tests can compare before/after values -# Otherwise, using model.get(path) will give the same object before as after -pathSnapshots = (paths) -> - if _.isString(paths) - return clone(model.get(paths)) - _.map paths, (path) -> clone(model.get(path)) -statsTask = -> pathSnapshots(['_user.stats', taskPath]) # quick snapshot of user.stats & task - -cleanUserObj = -> - userObj = character.newUserObject() - userObj.tasks = {} - userObj.habitIds = [] - userObj.dailyIds = [] - userObj.todoIds = [] - userObj.rewardIds = [] - return userObj -resetUser = -> model.set '_user', cleanUserObj() - -freshTask = (taskObj) -> - resetUser() - # create a test task - uuid = derby.uuid() - taskPath = "_user.tasks.#{uuid}" - {type} = taskObj - model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" - [taskObj.id, taskObj.value] = [uuid, 0] - model.at("_#{type}List").push taskObj - -### -Helper function to determine if stats updates are numerically correct based on scoring -@direction: 'up' or 'down' -@options: The user stats modifiers and times to run, defaults to {times:1, modifiers:{lvl:1, weapon:0, armor:0}} -### -modificationsLookup = (direction, options = {}) -> - merged = _.defaults options, {times:1, lvl:1, weapon:0, armor:0} - {times, lvl, armor, weapon} = merged - userObj = cleanUserObj() - value = 0 - _.times times, (n) -> - delta = scoring.taskDeltaFormula(value, direction) - value += delta - if direction=='up' - gain = scoring.expModifier(delta, options) - userObj.stats.exp += gain - userObj.stats.money += gain - else - loss = scoring.hpModifier(delta, options) - userObj.stats.hp += loss - return {user:userObj, value:value} - ###### Specs ###### describe 'API', -> @@ -114,7 +41,7 @@ describe 'API', -> model = store.createModel() model.set '_userId', uid = model.id() user = character.newUserObject() - user.apiToken = derby.uuid() + user.apiToken = model.id() model.set "users.#{uid}", user # Crappy hack to let server start before tests run setTimeout done, 2000 @@ -133,7 +60,7 @@ describe 'API', -> request.get("#{baseURL}/user") .set('Accept', 'application/json') .end (res) -> - expect(res.statusCode).to.be 500 + expect(res.statusCode).to.be 401 expect(res.body.err).to.be 'You must include a token and uid (user id) in your request' done() @@ -230,3 +157,24 @@ describe 'API', -> # Ensure that the two sets are equal expect(_.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')).length).to.equal 0 done() + + it 'GET /api/v1/user/tasks (todos)', (done) -> + request.get("#{baseURL}/user/tasks") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .query(type:'todo') + .end (res) -> + query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken) + query.fetch (err, user) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + currentUser = user.at(0).get() + model.ref '_user', user.at(0) + model.refList "_todoList", "_user.tasks", "_user.todoIds" + tasks = model.get("_todoList") + # Ensure that user owns the tasks + expect(res.body.length).to.equal tasks.length + # Ensure that the two sets are equal + expect(_.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')).length).to.equal 0 + done() diff --git a/test/mocha.opts b/test/mocha.opts index 74e33e85ea..72889931b5 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,6 +1,6 @@ --colors --reporter spec ---timeout 2500 +--timeout 2800 --ignore-leaks --growl --debug From 624d3360db2baa95025eb68231ed87f2c04601f2 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Tue, 26 Feb 2013 19:27:57 -0500 Subject: [PATCH 025/220] Cleanup pull request diff --- server.js | 5 +++-- src/server/index.coffee | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/server.js b/server.js index f9d64274a7..b9950075fe 100644 --- a/server.js +++ b/server.js @@ -1,8 +1,9 @@ // Load nconf and define default configuration values if config.json or ENV vars are not found var conf = require('nconf'); conf.argv().env().file({ file: __dirname + "/config.json" }).defaults({ - 'PORT': 3000, - 'IP': '0.0.0.0' + 'IP': '0.0.0.0', + 'BASE_URL': 'http://localhost', + 'NODE_ENV': 'development' }); // Override normal ENV values with nconf ENV values (ENV values are used the same way without nconf) diff --git a/src/server/index.coffee b/src/server/index.coffee index 01488aca35..673f54ae75 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -17,9 +17,9 @@ middleware = require './middleware' racer.io.set('transports', ['xhr-polling']) racer.ioClient.set('reconnection limit', 300000) # max reconect timeout to 5 minutes racer.set('bundleTimeout', 40000) -unless process.env.NODE_ENV == 'production' - racer.use(racer.logPlugin) - derby.use(derby.logPlugin) +#unless process.env.NODE_ENV == 'production' +# racer.use(racer.logPlugin) +# derby.use(derby.logPlugin) # Infinite stack trace Error.stackTraceLimit = Infinity if process.env.NODE_ENV is 'development' From 0aedef12e2675e592660c4883eeeb2250a6e7c37 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Tue, 26 Feb 2013 19:30:08 -0500 Subject: [PATCH 026/220] More cleanup --- server.js | 1 + src/app/character.coffee | 80 ++++++++++++++++++++-------------------- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/server.js b/server.js index b9950075fe..51db9480cf 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,7 @@ // Load nconf and define default configuration values if config.json or ENV vars are not found var conf = require('nconf'); conf.argv().env().file({ file: __dirname + "/config.json" }).defaults({ + 'PORT': 3000, 'IP': '0.0.0.0', 'BASE_URL': 'http://localhost', 'NODE_ENV': 'development' diff --git a/src/app/character.coffee b/src/app/character.coffee index 16d1c40fc5..901e3cb518 100644 --- a/src/app/character.coffee +++ b/src/app/character.coffee @@ -137,7 +137,7 @@ module.exports.updateUser = (model) -> batch = new BatchUpdate(model) user = batch.user obj = batch.obj() - tasks = obj?.tasks + tasks = obj.tasks # Remove corrupted tasks _.each tasks, (task, key) -> @@ -172,49 +172,49 @@ module.exports.BatchUpdate = BatchUpdate = (model) -> updates = {} { - user: user + user: user - obj: -> - obj ?= model.get 'users.'+user.get('id') - return obj + obj: -> + obj ?= model.get 'users.'+user.get('id') + return obj - startTransaction: -> - # start a batch transaction - nothing between now and @commit() will be set immediately - transactionInProgress = true - model._dontPersist = true - @obj() + startTransaction: -> + # start a batch transaction - nothing between now and @commit() will be set immediately + transactionInProgress = true + model._dontPersist = true + @obj() - ### - Handles updating the user model. If this is an en-mass operation (eg, server cron), changes are queued - but not actually set to the model. It also modifies userObj in case you need to access properties manually later. - If transaction not in progress, it just runs standard model.set() - ### - set: (path, val) -> - updates[path] = val if transactionInProgress - user.set(path, val) + ### + Handles updating the user model. If this is an en-mass operation (eg, server cron), changes are queued + but not actually set to the model. It also modifies userObj in case you need to access properties manually later. + If transaction not in progress, it just runs standard model.set() + ### + set: (path, val) -> + updates[path] = val if transactionInProgress + user.set(path, val) - ### - Hack to get around dom bindings being lost if parent objects are replaced whole-sale - eg, user.set('stats', {hp:50, exp:10...}) will break dom bindings, but user.set('stats.hp',50) is ok - ### - setStats: (stats) -> - stats ?= obj.stats - that = @ - _.each Object.keys(stats), (key) -> that.set "stats.#{key}", stats[key] + ### + Hack to get around dom bindings being lost if parent objects are replaced whole-sale + eg, user.set('stats', {hp:50, exp:10...}) will break dom bindings, but user.set('stats.hp',50) is ok + ### + setStats: (stats) -> + stats ?= obj.stats + that = @ + _.each Object.keys(stats), (key) -> that.set "stats.#{key}", stats[key] - # queue: (path, val) -> - # # Special function for setting object properties by string dot-notation. See http://stackoverflow.com/a/6394168/362790 - # arr = path.split('.') - # arr.reduce (curr, next, index) -> - # if (arr.length - 1) == index - # curr[next] = val - # curr[next] - # , obj +# queue: (path, val) -> +# # Special function for setting object properties by string dot-notation. See http://stackoverflow.com/a/6394168/362790 +# arr = path.split('.') +# arr.reduce (curr, next, index) -> +# if (arr.length - 1) == index +# curr[next] = val +# curr[next] +# , obj - commit: -> - model._dontPersist = false - # some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js - user.set "update__", updates - transactionInProgress = false - updates = {} + commit: -> + model._dontPersist = false + # some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js + user.set "update__", updates + transactionInProgress = false + updates = {} } From 8ba137d3ce5d2ac2e5f60d8286fbe53f5d456063 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Wed, 27 Feb 2013 16:58:57 -0500 Subject: [PATCH 027/220] Remove config and scoring --- package.json | 3 ++- test/api.mocha.coffee | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a5ab0f7bbe..83fce307e2 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "npm": "1.1.x" }, "scripts": { - "start": "server.js" + "start": "server.js", + "test": "mocha test/api.mocha.coffee" } } diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 2b1931fcce..03c4779f21 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -12,9 +12,7 @@ expect.Assertion::be = expect.Assertion::equal = (obj) -> origBe.call this, obj # Custom modules -scoring = require '../src/app/scoring' character = require '../src/app/character' -config = require './config' ###### Helpers & Variables ###### From ab52c59ee46885387e2375948f8199dd0205085f Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 27 Feb 2013 17:06:39 -0500 Subject: [PATCH 028/220] pin coffeescript --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 83fce307e2..f3f52343c6 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "stripe": "*", "async": "*", "lodash": "*", - "coffee-script": "*", + "coffee-script": "1.4.x", "underscore": "*", "mongoskin": "*", "nconf": "*", From 4b2b0281ee2323f19d8dadac8c9340d72f1445e3 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 27 Feb 2013 17:06:47 -0500 Subject: [PATCH 029/220] add nconf to test --- test/api.mocha.coffee | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 03c4779f21..ac5bedddc9 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -3,6 +3,29 @@ request = require 'superagent' expect = require 'expect.js' require 'coffee-script' +conf = require("nconf") +conf.argv().env().file(file: __dirname + "/config.json").defaults + PORT: 3000 + IP: "0.0.0.0" + BASE_URL: "http://localhost" + NODE_ENV: "development" + + +# Override normal ENV values with nconf ENV values (ENV values are used the same way without nconf) +process.env.IP = conf.get("IP") +process.env.PORT = conf.get("PORT") +process.env.BASE_URL = conf.get("BASE_URL") +process.env.FACEBOOK_KEY = conf.get("FACEBOOK_KEY") +process.env.FACEBOOK_SECRET = conf.get("FACEBOOK_SECRET") +process.env.NODE_DB_URI = conf.get("NODE_DB_URI") +process.env.NODE_ENV = conf.get("NODE_ENV") +process.env.SESSION_SECRET = conf.get("SESSION_SECRET") +process.env.SMTP_USER = conf.get("SMTP_USER") +process.env.SMTP_PASS = conf.get("SMTP_PASS") +process.env.SMTP_SERVICE = conf.get("SMTP_SERVICE") +process.env.STRIPE_API_KEY = conf.get("STRIPE_API_KEY") +process.env.STRIPE_PUB_KEY = conf.get("STRIPE_PUB_KEY") + ## monkey-patch expect.js for better diffs on mocha ## see: https://github.com/LearnBoost/expect.js/pull/34 From 9c6e061e8dd6eacdd194767cfd07b42a2d0d7272 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 27 Feb 2013 17:10:20 -0500 Subject: [PATCH 030/220] return all objects on user API call --- src/server/api.coffee | 2 +- test/api.mocha.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index c20e0d9582..c98384f47b 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -41,7 +41,7 @@ router.get '/status', (req, res) -> router.get '/user', auth, (req, res) -> self = req.userObj - delete self[val] for val in ['tasks', 'apiToken', 'flags', 'lastCron'] + delete self.apiToken res.json self diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index ac5bedddc9..969eb168f6 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -110,7 +110,7 @@ describe 'API', -> expect(res.statusCode).to.be 200 expect(res.body.id).not.to.be.empty() self = _.clone(currentUser) - delete self[val] for val in ['tasks', 'apiToken', 'flags', 'lastCron'] + delete self.apiToken expect(res.body).to.eql self done() From b1c770c0f9d655ba2774675c463806460aeb7069 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 28 Feb 2013 10:59:39 -0500 Subject: [PATCH 031/220] decrease "reset" button size --- views/app/settings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/app/settings.html b/views/app/settings.html index 753e3aaa6c..0bba816477 100644 --- a/views/app/settings.html +++ b/views/app/settings.html @@ -28,7 +28,7 @@

This is highly discouraged because you'll lose historical data, which is useful for graphing your progress over time. However, some people find it useful in the beginning after playing with the app for a while.

<@footer> - + From afe95975e1c31a4eb1b94c2a19b3c92c8575f745 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 28 Feb 2013 18:03:35 -0500 Subject: [PATCH 032/220] fix up deprecated to work again (order of middleware) --- src/server/deprecated.coffee | 35 +++++------------------------------ src/server/index.coffee | 2 +- 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/src/server/deprecated.coffee b/src/server/deprecated.coffee index 8e497764ef..d4e5d3b694 100644 --- a/src/server/deprecated.coffee +++ b/src/server/deprecated.coffee @@ -13,31 +13,6 @@ router.get '/:uid/up/:score?', (req, res) -> res.send(500, deprecatedMessage) router.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage) router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage) -router.get '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> - {uid, taskId, direction} = req.params - {apiToken} = req.query - {title, service, icon} = req.body - console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development' - - # Send error responses for improper API call - return res.send(500, 'request body "apiToken" required') unless apiToken - return res.send(500, ':uid required') unless uid - return res.send(500, ':taskId required') unless taskId - return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] - - model = req.getModel() - req._isServer = true - model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) -> - return res.send(500, err) if err - user = result.at(0) - userObj = user.get() - if _.isEmpty(userObj) - return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id") - - model.ref('_user', user) - - res.send(user) - router.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> {uid, taskId, direction} = req.params {apiToken, title, service, icon} = req.body @@ -50,15 +25,15 @@ router.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] model = req.getModel() - req._isServer = true model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) -> return res.send(500, err) if err - user = result.at(0) + user = result userObj = user.get() if _.isEmpty(userObj) return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id") model.ref('_user', user) + req._isServer = true # Create task if doesn't exist # TODO add service & icon to task @@ -73,8 +48,8 @@ router.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> down: true notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." - scoring.setModel(model) - delta = scoring.score(taskId, direction) + score = new scoring.Scoring(model) + delta = score.score(taskId, direction) result = model.get ('_user.stats') result.delta = delta res.send(result) @@ -88,7 +63,7 @@ router.get '/v1/users/:uid/calendar.ics', (req, res) -> query = model.query('users').withIdAndToken(uid, apiToken) query.fetch (err, result) -> return res.send(500, err) if err - tasks = result.at(0).get('tasks') + tasks = result.get('tasks') # tasks = result[0].tasks tasksWithDates = _.filter tasks, (task) -> !!task.date return res.send(500, "No events found") if _.isEmpty(tasksWithDates) diff --git a/src/server/index.coffee b/src/server/index.coffee index 673f54ae75..41b447f677 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -76,6 +76,7 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, -> .use(store.modelMiddleware()) # API should be hit before all other routes .use('/api/v1', require('./api').middleware) + .use(require('./deprecated').middleware) # Show splash page for newcomers .use(middleware.splash) .use(priv.middleware) @@ -84,7 +85,6 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, -> # Creates an express middleware from the app's routes .use(app.router()) .use(require('./static').middleware) - .use(require('./deprecated').middleware) .use(expressApp.router) .use(serverError(root)) From 2a49b640239a987faaa5e8958e9e11e612b9c4ab Mon Sep 17 00:00:00 2001 From: Philip How Date: Thu, 28 Feb 2013 23:09:12 +0000 Subject: [PATCH 033/220] Better notifications --- src/app/browser.coffee | 34 ++++++++++++++++++++++------------ styles/app/alerts.styl | 15 +++++++++++---- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/app/browser.coffee b/src/app/browser.coffee index 2830fefd48..5becd6bcc0 100644 --- a/src/app/browser.coffee +++ b/src/app/browser.coffee @@ -6,7 +6,7 @@ module.exports.restoreRefs = restoreRefs = (model) -> model.fn '_tnl', '_user.stats.lvl', (lvl) -> # see https://github.com/lefnire/habitrpg/issues/4 # also update in scoring.coffee. TODO create a function accessible in both locations - (lvl*100)/5 + 10*Math.pow(lvl,2)+(lvl*10)+80 #refLists _.each ['habit', 'daily', 'todo', 'reward'], (type) -> @@ -193,20 +193,30 @@ setupGrowlNotifications = (model) -> num = captures - args rounded = Math.abs(num.toFixed(1)) if num < 0 - statsNotification " -#{rounded} HP", 'hp' # lost hp from purchase + statsNotification " - #{rounded} HP", 'hp' # lost hp from purchase + else if num > 0 + statsNotification " + #{rounded} HP", 'hp' # gained hp from potion/level? + + user.on 'set', 'stats.exp', (captures, args) -> + num = captures - args + rounded = Math.abs(num.toFixed(1)) + if num < 0 + statsNotification " - #{rounded} XP", 'xp' + else if num > 0 + statsNotification " + #{rounded} XP", 'xp' user.on 'set', 'stats.gp', (captures, args) -> num = captures - args - rounded = Math.abs(num.toFixed(1)) - # made purchase - if num < 0 - # FIXME use 'warning' when unchecking an accidently completed daily/todo, and notify of exp too - statsNotification " -#{rounded} GP", 'gp' - # gained gp (and thereby exp) - else if num > 0 - num = Math.abs(num) - statsNotification " +#{rounded} XP", 'xp' - statsNotification " +#{rounded} GP", 'gp' + absolute = Math.abs(num) + gold = Math.floor(absolute) + silver = Math.floor((absolute-gold)*100) + sign = if num < 0 then '-' else '+' + if gold and silver > 0 + statsNotification "#{sign} #{gold} #{silver} ", 'gp' + else if gold > 0 + statsNotification "#{sign} #{gold} ", 'gp' + else if silver > 0 + statsNotification "#{sign} #{silver} ", 'gp' user.on 'set', 'stats.lvl', (captures, args) -> if captures > args diff --git a/styles/app/alerts.styl b/styles/app/alerts.styl index 8ecbd0e45e..38e411f480 100644 --- a/styles/app/alerts.styl +++ b/styles/app/alerts.styl @@ -34,9 +34,16 @@ borderDarken = 20% // alert icons -.icon-gp - background: url("img/sprites/shop_sprites.png") no-repeat - background-size: 400px - background-position: -386px 0 +.icon-gold + background: url("img/coin_single_gold.png") no-repeat + background-position: center center + background-size: 18px + width: 14px + height: 14px + +.icon-silver + background: url("img/coin_single_silver.png") no-repeat + background-position: center center + background-size: 18px width: 14px height: 14px \ No newline at end of file From 3017f5a592d96c6617a5196b001b9e4077f2ea01 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 28 Feb 2013 18:24:14 -0500 Subject: [PATCH 034/220] small test tweaks --- test/api.mocha.coffee | 19 +++---------------- test/casper/rest.casper.coffee | 2 +- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 969eb168f6..f1a7abec1e 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -4,27 +4,14 @@ expect = require 'expect.js' require 'coffee-script' conf = require("nconf") -conf.argv().env().file(file: __dirname + "/config.json").defaults - PORT: 3000 - IP: "0.0.0.0" - BASE_URL: "http://localhost" - NODE_ENV: "development" - +conf.argv().env().file({file: __dirname + '../config.json'}).defaults # Override normal ENV values with nconf ENV values (ENV values are used the same way without nconf) -process.env.IP = conf.get("IP") -process.env.PORT = conf.get("PORT") +#FIXME can't get nconf file above to load... process.env.BASE_URL = conf.get("BASE_URL") process.env.FACEBOOK_KEY = conf.get("FACEBOOK_KEY") process.env.FACEBOOK_SECRET = conf.get("FACEBOOK_SECRET") -process.env.NODE_DB_URI = conf.get("NODE_DB_URI") -process.env.NODE_ENV = conf.get("NODE_ENV") -process.env.SESSION_SECRET = conf.get("SESSION_SECRET") -process.env.SMTP_USER = conf.get("SMTP_USER") -process.env.SMTP_PASS = conf.get("SMTP_PASS") -process.env.SMTP_SERVICE = conf.get("SMTP_SERVICE") -process.env.STRIPE_API_KEY = conf.get("STRIPE_API_KEY") -process.env.STRIPE_PUB_KEY = conf.get("STRIPE_PUB_KEY") +process.env.NODE_DB_URI = 'mongodb://localhost/habirpg' ## monkey-patch expect.js for better diffs on mocha ## see: https://github.com/LearnBoost/expect.js/pull/34 diff --git a/test/casper/rest.casper.coffee b/test/casper/rest.casper.coffee index 18aaf3afde..767d93489e 100644 --- a/test/casper/rest.casper.coffee +++ b/test/casper/rest.casper.coffee @@ -12,8 +12,8 @@ casper.start "#{url}/?play=1", -> @fill 'form#derby-auth-register', username: user1.id email: "{user1.id}@gmail.com" - 'email-confirmation': "{user1.id}@gmail.com" password: 'habitrpg123' + 'password-confirmation': "habitrpg123" , true casper.thenOpen "#{url}/logout" casper.thenOpen "#{url}/?play=1", -> From 4597cba8139864809d0814bd4fbd334a5a1d883b Mon Sep 17 00:00:00 2001 From: Philip How Date: Thu, 28 Feb 2013 23:42:38 +0000 Subject: [PATCH 035/220] Changes to XP and GP gain --- src/app/scoring.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee index 4f1b78db70..ea8ab4b1fb 100644 --- a/src/app/scoring.coffee +++ b/src/app/scoring.coffee @@ -142,8 +142,8 @@ module.exports.Scoring = (model) -> addPoints = -> modified = expModifier(delta) - exp += modified - gp += modified + exp += modified*10 + gp += delta subtractPoints = -> modified = hpModifier(delta) From 6fe375114c4b5e49c49d1a0e91f22fabc10dfae2 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 28 Feb 2013 20:00:08 -0500 Subject: [PATCH 036/220] set req._isServer=true for API calls, use that as access check --- src/server/api.coffee | 1 + src/server/store.coffee | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index c98384f47b..09824e294c 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -33,6 +33,7 @@ auth = (req, res, next) -> req.user = user req.userObj = user.get() return res.json 401, NO_USER_FOUND if !req.userObj || _.isEmpty(req.userObj) + req._isServer = true next() router.get '/status', (req, res) -> diff --git a/src/server/store.coffee b/src/server/store.coffee index 659d890685..ac4ab491af 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -30,9 +30,7 @@ userAccess = (store) -> err = arguments[arguments.length - 1] # return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@) - # TEMPORARY token check to allow api - # Must fix before release - return accept(true) + return accept(true) if derbyAuth.isServer(@) return accept(false) if derbyAuth.bustedSession(@) @@ -45,8 +43,7 @@ userAccess = (store) -> return accept(true) # Same session (user.id = this.session.userId) - if (uid is @session.userId) or derbyAuth.isServer(@) - return accept(true) + return accept(true) if uid is @session.userId accept(false) From 26de70c17c34c16742d80562d82d2a53533ddd78 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 28 Feb 2013 20:42:52 -0500 Subject: [PATCH 037/220] temporary set session so we can save new user in tests --- test/api.mocha.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index f1a7abec1e..145542a525 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -50,7 +50,9 @@ describe 'API', -> model.set '_userId', uid = model.id() user = character.newUserObject() user.apiToken = model.id() + model.session = {userId:uid} model.set "users.#{uid}", user + delete model.session # Crappy hack to let server start before tests run setTimeout done, 2000 From c080ddc6832cad40e252497b421bacc4e268a5c5 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Fri, 1 Mar 2013 05:16:55 -0500 Subject: [PATCH 038/220] Separate out logic for tasks plus specs --- src/server/api.coffee | 55 ++++++++++++++++++++++++++----------------- test/api.mocha.coffee | 47 ++++++++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 24 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 09824e294c..3672799667 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -40,11 +40,11 @@ router.get '/status', (req, res) -> res.json status: 'up' router.get '/user', auth, (req, res) -> - self = req.userObj + user = req.userObj - delete self.apiToken + delete user.apiToken - res.json self + res.json user router.get '/task/:id', auth, (req, res) -> task = req.userObj.tasks[req.params.id] @@ -52,30 +52,41 @@ router.get '/task/:id', auth, (req, res) -> res.json 200, task -router.put '/task/:id', auth, (req, res) -> - task = req.userObj.tasks[req.params.id] - return res.json 400, err: "No task found." if !task || _.isEmpty(task) +validateTask = (req, res, next) -> + task = {} + # If we're updating, get the task from the user + if req.method is 'PUT' + task = req.userObj?.tasks[req.params.id] + return res.json 400, err: "No task found." if !task || _.isEmpty(task) - title = sanitize(req.body.title).xss() - text = sanitize(req.body.text).xss() + newTask = { type, text, notes, value, up, down, completed } = req.body - task.title = title if title - task.text = text if text - #task.type = req.body.type if /^(habit|todo|daily|reward)$/.test req.body.type + text = sanitize(text).xss() + notes = sanitize(notes).xss() + value = sanitize(value).toInt() - req.user.set "tasks.#{task.id}", task + switch type + when 'habit' + newTask.up = true unless typeof up is 'boolean' + newTask.down = true unless typeof down is 'boolean' + when 'daily', 'todo' + newTask.completed = false unless typeof completed is 'boolean' - res.json 200, task + _.extend task, newTask + req.task = task + next() -router.post '/user/task', auth, (req, res) -> - task = { title, text, type, value, note } = req.body - return res.json 400, err: "type must be habit, todo, daily, or reward" unless /^habit|todo|daily|reward$/.test type - return res.json 400, err: "must have a title" unless check(title).notEmpty() - return res.json 400, err: "must have text" unless check(text).notEmpty() +router.put '/task/:id', auth, validateTask, (req, res) -> + req.user.set "tasks.#{req.task.id}", req.task - self = req.userObj + res.json 200, req.task - value ||= 0 +router.post '/user/task', auth, validateTask, (req, res) -> + task = req.task + type = task.type + + unless /^habit|todo|daily|reward$/.test type + return res.json 400, err: 'type must be habit, todo, daily, or reward' model = req.getModel() model.ref '_user', req.user @@ -85,8 +96,8 @@ router.post '/user/task', auth, (req, res) -> res.json 201, task router.get '/user/tasks', auth, (req, res) -> - self = req.userObj - return res.json 400, NO_USER_FOUND if !self || _.isEmpty(self) + user = req.userObj + return res.json 400, NO_USER_FOUND if !user || _.isEmpty(user) model = req.getModel() model.ref '_user', req.user diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 145542a525..229603a815 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -132,18 +132,61 @@ describe 'API', -> expect(user.get().tasks[res.body.id]).to.be.an('object') done() + it 'POST /api/v1/user/task (without type)', (done) -> + request.post("#{baseURL}/user/task") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .send({}) + .end (res) -> + query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken) + query.fetch (err, user) -> + expect(res.body.err).to.be 'type must be habit, todo, daily, or reward' + expect(res.statusCode).to.be 400 + done() + + it 'POST /api/v1/user/task (only type)', (done) -> + request.post("#{baseURL}/user/task") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .send(type: 'habit') + .end (res) -> + query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken) + query.fetch (err, user) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 201 + expect(res.body.id).not.to.be.empty() + # Ensure that user owns the newly created object + expect(user.get().tasks[res.body.id]).to.be.an('object') + done() + it 'PUT /api/v1/task/:id', (done) -> tid = _.pluck(currentUser.tasks, 'id')[0] request.put("#{baseURL}/task/#{tid}") .set('Accept', 'application/json') .set('X-API-User', currentUser.id) .set('X-API-Key', currentUser.apiToken) - .send(title: 'a new title',text: 'hi') + .send(text: 'bye') + .end (res) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + currentUser.tasks[tid].text = 'bye' + expect(res.body).to.eql currentUser.tasks[tid] + done() + + it 'PUT /api/v1/task/:id (update notes)', (done) -> + tid = _.pluck(currentUser.tasks, 'id')[0] + request.put("#{baseURL}/task/#{tid}") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .send(text: 'hi',notes:'foobar matey') .end (res) -> expect(res.body.err).to.be undefined expect(res.statusCode).to.be 200 - currentUser.tasks[tid].title = 'a new title' currentUser.tasks[tid].text = 'hi' + currentUser.tasks[tid].notes = 'foobar matey' expect(res.body).to.eql currentUser.tasks[tid] done() From 4e6c741594a6347bd2b5425138154abdd4de0b13 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Fri, 1 Mar 2013 05:48:49 -0500 Subject: [PATCH 039/220] Shouldn't update type in task PUT --- src/server/api.coffee | 13 ++++++++----- test/api.mocha.coffee | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 3672799667..1234f0cebb 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -54,12 +54,18 @@ router.get '/task/:id', auth, (req, res) -> validateTask = (req, res, next) -> task = {} + newTask = { type, text, notes, value, up, down, completed } = req.body + # If we're updating, get the task from the user if req.method is 'PUT' task = req.userObj?.tasks[req.params.id] return res.json 400, err: "No task found." if !task || _.isEmpty(task) - - newTask = { type, text, notes, value, up, down, completed } = req.body + # Strip for now + type = undefined + delete newTask.type + else if req.method is 'POST' + unless /^habit|todo|daily|reward$/.test type + return res.json 400, err: 'type must be habit, todo, daily, or reward' text = sanitize(text).xss() notes = sanitize(notes).xss() @@ -85,9 +91,6 @@ router.post '/user/task', auth, validateTask, (req, res) -> task = req.task type = task.type - unless /^habit|todo|daily|reward$/.test type - return res.json 400, err: 'type must be habit, todo, daily, or reward' - model = req.getModel() model.ref '_user', req.user model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 229603a815..1d02e9e593 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -175,8 +175,23 @@ describe 'API', -> expect(res.body).to.eql currentUser.tasks[tid] done() + it 'PUT /api/v1/task/:id (shouldnt update type)', (done) -> + tid = _.pluck(currentUser.tasks, 'id')[1] + type = if currentUser.tasks[tid].type is 'habit' then 'daily' else 'habit' + request.put("#{baseURL}/task/#{tid}") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .send(type: type, text: 'fishman') + .end (res) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + currentUser.tasks[tid].text = 'fishman' + expect(res.body).to.eql currentUser.tasks[tid] + done() + it 'PUT /api/v1/task/:id (update notes)', (done) -> - tid = _.pluck(currentUser.tasks, 'id')[0] + tid = _.pluck(currentUser.tasks, 'id')[2] request.put("#{baseURL}/task/#{tid}") .set('Accept', 'application/json') .set('X-API-User', currentUser.id) From 33b529731cab908c5c8e6f589f6f553f0bfbb25c Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Fri, 1 Mar 2013 06:03:05 -0500 Subject: [PATCH 040/220] Remove extra query --- test/api.mocha.coffee | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 1d02e9e593..6dcbdaa2df 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -139,11 +139,9 @@ describe 'API', -> .set('X-API-Key', currentUser.apiToken) .send({}) .end (res) -> - query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken) - query.fetch (err, user) -> - expect(res.body.err).to.be 'type must be habit, todo, daily, or reward' - expect(res.statusCode).to.be 400 - done() + expect(res.body.err).to.be 'type must be habit, todo, daily, or reward' + expect(res.statusCode).to.be 400 + done() it 'POST /api/v1/user/task (only type)', (done) -> request.post("#{baseURL}/user/task") From 5ce5152757e12184e9d7c11d893db3601f2a119a Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Fri, 1 Mar 2013 06:11:32 -0500 Subject: [PATCH 041/220] More secure regex type check --- src/server/api.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 1234f0cebb..7ad2e2330d 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -64,7 +64,7 @@ validateTask = (req, res, next) -> type = undefined delete newTask.type else if req.method is 'POST' - unless /^habit|todo|daily|reward$/.test type + unless /^(habit|todo|daily|reward)$/.test type return res.json 400, err: 'type must be habit, todo, daily, or reward' text = sanitize(text).xss() @@ -106,7 +106,7 @@ router.get '/user/tasks', auth, (req, res) -> model.ref '_user', req.user tasks = [] types = ['habit','todo','daily','reward'] - if /^habit|todo|daily|reward$/.test req.query.type + if /^(habit|todo|daily|reward)$/.test req.query.type types = [req.query.type] for type in types model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" From 4377ff918a2ac0fa0ae2b32155872d8e571d380a Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Fri, 1 Mar 2013 15:11:35 -0500 Subject: [PATCH 042/220] Prepend /user to the /task/:id routes --- src/server/api.coffee | 10 +++++----- test/api.mocha.coffee | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 7ad2e2330d..287b354f5d 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -20,6 +20,9 @@ NO_USER_FOUND = err: "No user found." $ mocha test/api.mocha.coffee ### +router.get '/status', (req, res) -> + res.json status: 'up' + auth = (req, res, next) -> uid = req.headers['x-api-user'] token = req.headers['x-api-key'] @@ -36,9 +39,6 @@ auth = (req, res, next) -> req._isServer = true next() -router.get '/status', (req, res) -> - res.json status: 'up' - router.get '/user', auth, (req, res) -> user = req.userObj @@ -46,7 +46,7 @@ router.get '/user', auth, (req, res) -> res.json user -router.get '/task/:id', auth, (req, res) -> +router.get '/user/task/:id', auth, (req, res) -> task = req.userObj.tasks[req.params.id] return res.json 400, err: "No task found." if !task || _.isEmpty(task) @@ -82,7 +82,7 @@ validateTask = (req, res, next) -> req.task = task next() -router.put '/task/:id', auth, validateTask, (req, res) -> +router.put '/user/task/:id', auth, validateTask, (req, res) -> req.user.set "tasks.#{req.task.id}", req.task res.json 200, req.task diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 6dcbdaa2df..6d82d71516 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -104,9 +104,9 @@ describe 'API', -> expect(res.body).to.eql self done() - it 'GET /api/v1/task/:id', (done) -> + it 'GET /api/v1/user/task/:id', (done) -> tid = _.pluck(currentUser.tasks, 'id')[0] - request.get("#{baseURL}/task/#{tid}") + request.get("#{baseURL}/user/task/#{tid}") .set('Accept', 'application/json') .set('X-API-User', currentUser.id) .set('X-API-Key', currentUser.apiToken) @@ -159,9 +159,9 @@ describe 'API', -> expect(user.get().tasks[res.body.id]).to.be.an('object') done() - it 'PUT /api/v1/task/:id', (done) -> + it 'PUT /api/v1/user/task/:id', (done) -> tid = _.pluck(currentUser.tasks, 'id')[0] - request.put("#{baseURL}/task/#{tid}") + request.put("#{baseURL}/user/task/#{tid}") .set('Accept', 'application/json') .set('X-API-User', currentUser.id) .set('X-API-Key', currentUser.apiToken) @@ -173,10 +173,10 @@ describe 'API', -> expect(res.body).to.eql currentUser.tasks[tid] done() - it 'PUT /api/v1/task/:id (shouldnt update type)', (done) -> + it 'PUT /api/v1/user/task/:id (shouldnt update type)', (done) -> tid = _.pluck(currentUser.tasks, 'id')[1] type = if currentUser.tasks[tid].type is 'habit' then 'daily' else 'habit' - request.put("#{baseURL}/task/#{tid}") + request.put("#{baseURL}/user/task/#{tid}") .set('Accept', 'application/json') .set('X-API-User', currentUser.id) .set('X-API-Key', currentUser.apiToken) @@ -188,9 +188,9 @@ describe 'API', -> expect(res.body).to.eql currentUser.tasks[tid] done() - it 'PUT /api/v1/task/:id (update notes)', (done) -> + it 'PUT /api/v1/user/task/:id (update notes)', (done) -> tid = _.pluck(currentUser.tasks, 'id')[2] - request.put("#{baseURL}/task/#{tid}") + request.put("#{baseURL}/user/task/#{tid}") .set('Accept', 'application/json') .set('X-API-User', currentUser.id) .set('X-API-Key', currentUser.apiToken) From bc76aee85e736739551e412bc90360090cfab7ab Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Fri, 1 Mar 2013 15:13:53 -0500 Subject: [PATCH 043/220] Remove nodetime --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index f3f52343c6..a80458cca0 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "mongoskin": "*", "nconf": "*", "icalendar": "git://github.com/lefnire/node-icalendar#master", - "nodetime": "*", "superagent": "~0.12.4", "resolve": "~0.2.3", "browserify": "1.17.3", From 74e531bee52e666dbe3edb26320d390af4d8a58c Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 1 Mar 2013 16:22:24 -0500 Subject: [PATCH 044/220] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..1333ed77b7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +TODO From ba8cf7742af882f563e87c08056da79bdfabc880 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 1 Mar 2013 18:02:53 -0500 Subject: [PATCH 045/220] sortable cleanup --- src/app/browser.coffee | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/app/browser.coffee b/src/app/browser.coffee index 2830fefd48..7cfdb67d59 100644 --- a/src/app/browser.coffee +++ b/src/app/browser.coffee @@ -81,10 +81,7 @@ loadJavaScripts = (model) -> ### setupSortable = (model) -> unless (model.get('_view.mobileDevice') == true) #don't do sortable on mobile - # Make the lists draggable using jQuery UI - # Note, have to setup helper function here and call it for each type later - # due to variable binding of "type" - setupSortable = (type) -> + _.each ['habit', 'daily', 'todo', 'reward'], (type) -> $("ul.#{type}s").sortable dropOnEmpty: false cursor: "move" @@ -102,7 +99,6 @@ setupSortable = (model) -> # Also, note that refList index arguments can either be an index # or the item's id property model.at("_#{type}List").pass(ignore: domId).move {id}, to - _.each ['habit', 'daily', 'todo', 'reward'], (type) -> setupSortable(type) setupTooltips = (model) -> $('[rel=tooltip]').tooltip() From 7fc1de01e61631492fac7e5f360961a1cd9fa9bb Mon Sep 17 00:00:00 2001 From: Tobias Leugger Date: Sat, 2 Mar 2013 00:27:59 +0100 Subject: [PATCH 046/220] Small improvement of the error-alert styling --- views/app/alerts.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/app/alerts.html b/views/app/alerts.html index c76e61c010..bf10bd2bf0 100644 --- a/views/app/alerts.html +++ b/views/app/alerts.html @@ -1,6 +1,6 @@ {#if _flash.error} -
    +
      {#each _flash.error as :error}
    • {:error}
    • {/}
    {/} \ No newline at end of file From 08839278b38f0e29cc293c1f656aaae58a1993cc Mon Sep 17 00:00:00 2001 From: Philip How Date: Sat, 2 Mar 2013 18:23:43 +0000 Subject: [PATCH 047/220] Notification and Algorithim changes --- src/app/algos.coffee | 50 ++++++++++++++++++++++++++++ src/app/browser.coffee | 22 ++++++++----- src/app/character.coffee | 1 + src/app/helpers.coffee | 3 ++ src/app/items.coffee | 50 ++++++++++++++-------------- src/app/scoring.coffee | 70 +++++++++++----------------------------- views/app/header.html | 2 +- 7 files changed, 112 insertions(+), 86 deletions(-) create mode 100644 src/app/algos.coffee diff --git a/src/app/algos.coffee b/src/app/algos.coffee new file mode 100644 index 0000000000..69ab9b4e93 --- /dev/null +++ b/src/app/algos.coffee @@ -0,0 +1,50 @@ + +MODIFIER = .02 + +module.exports.tnl = (level) -> + return (Math.pow(level,2)*10)+(level*10)+80 + +### + Calculates Exp modificaiton based on level and weapon strength + {value} task.value for exp gain + {weaponStrength) weapon strength + {level} current user level +### +module.exports.expModifier = (value, weaponStrength, level) -> + levelModifier = (level-1) * MODIFIER + weaponModifier = weaponStrength / 100 + strength = 1 + weaponModifier + levelModifier + return value * strength + +### + Calculates HP modification based on level and armor defence + {value} task.value for hp loss + {armorDefense} defense from armor + {helmDefense} defense from helm + {level} current user level +### +module.exports.hpModifier = (value, armorDefense, helmDefense, shieldDefense, level) -> + levelModifier = (level-1) * MODIFIER + armorModifier = (armorDefense + helmDefense + shieldDefense) / 100 + defense = 1 - levelModifier + armorModifier + return value * defense + +### + Future use +### +module.exports.gpModifier = (value, modifier) -> + return value * modifier + +### + Calculates the next task.value based on direction + Uses a capped inverse log y=.9^x, y>= -5 + {currentValue} the current value of the task + {direction} up or down +### +module.exports.taskDeltaFormula = (currentValue, direction) -> + sign = if (direction is 'up') then 1 else -1 + delta = Math.pow(0.9,currentValue) * sign + if delta < -5 then delta = -5 + console.log("CurrentValue: " + currentValue + " delta: " + delta) + #delta = if (currentValue < 0) then (( -0.1 * currentValue + 1 ) * sign) else (( Math.pow(0.9,currentValue) ) * sign) + return delta \ No newline at end of file diff --git a/src/app/browser.coffee b/src/app/browser.coffee index 5becd6bcc0..63e6994d58 100644 --- a/src/app/browser.coffee +++ b/src/app/browser.coffee @@ -1,11 +1,13 @@ _ = require 'underscore' moment = require 'moment' +#algos = require './algos' module.exports.restoreRefs = restoreRefs = (model) -> # tnl function model.fn '_tnl', '_user.stats.lvl', (lvl) -> # see https://github.com/lefnire/habitrpg/issues/4 # also update in scoring.coffee. TODO create a function accessible in both locations + #TODO find a method of calling algos.tnl() 10*Math.pow(lvl,2)+(lvl*10)+80 #refLists @@ -85,7 +87,9 @@ setupSortable = (model) -> # Note, have to setup helper function here and call it for each type later # due to variable binding of "type" setupSortable = (type) -> - $("ul.#{type}s").sortable + for key, value of type + parsedType = value + $("ul.#{parsedType}s").sortable dropOnEmpty: false cursor: "move" items: "li" @@ -191,19 +195,21 @@ setupGrowlNotifications = (model) -> # Setup listeners which trigger notifications user.on 'set', 'stats.hp', (captures, args) -> num = captures - args + console.log("hp = " + num) rounded = Math.abs(num.toFixed(1)) if num < 0 statsNotification " - #{rounded} HP", 'hp' # lost hp from purchase else if num > 0 statsNotification " + #{rounded} HP", 'hp' # gained hp from potion/level? - user.on 'set', 'stats.exp', (captures, args) -> - num = captures - args - rounded = Math.abs(num.toFixed(1)) - if num < 0 - statsNotification " - #{rounded} XP", 'xp' - else if num > 0 - statsNotification " + #{rounded} XP", 'xp' + user.on 'set', 'stats.exp', (captures, args, isLocal, silent) -> + if not silent + num = captures - args + rounded = Math.abs(num.toFixed(1)) + if num < 0 + statsNotification " - #{rounded} XP", 'xp' + else if num > 0 + statsNotification " + #{rounded} XP", 'xp' user.on 'set', 'stats.gp', (captures, args) -> num = captures - args diff --git a/src/app/character.coffee b/src/app/character.coffee index 07b0996dcb..de1b280c2d 100644 --- a/src/app/character.coffee +++ b/src/app/character.coffee @@ -221,6 +221,7 @@ module.exports.BatchUpdate = BatchUpdate = (model) -> commit: -> model._dontPersist = false # some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js + # pass true if we have levelled to supress xp notification user.set "update__", updates transactionInProgress = false updates = {} diff --git a/src/app/helpers.coffee b/src/app/helpers.coffee index 38733f11d5..5c2cccaa15 100644 --- a/src/app/helpers.coffee +++ b/src/app/helpers.coffee @@ -14,6 +14,9 @@ module.exports.viewHelpers = (view) -> view.fn "round", (num) -> Math.round num + + view.fn "floor", (num) -> + Math.floor num view.fn "lt", (a, b) -> a < b diff --git a/src/app/items.coffee b/src/app/items.coffee index 911458e60c..a9ca688f52 100644 --- a/src/app/items.coffee +++ b/src/app/items.coffee @@ -2,37 +2,37 @@ _ = require 'underscore' items = module.exports.items = weapon: [ - {index: 0, text: "Training Sword", classes: "weapon_0", notes:'Training weapon.', modifier: 0.00, value:0} - {index: 1, text: "Sword", classes:'weapon_1', notes:'Increases experience gain by 3%.', modifier: 0.03, value:20} - {index: 2, text: "Axe", classes:'weapon_2', notes:'Increases experience gain by 6%.', modifier: 0.06, value:30} - {index: 3, text: "Morningstar", classes:'weapon_3', notes:'Increases experience gain by 9%.', modifier: 0.09, value:45} - {index: 4, text: "Blue Sword", classes:'weapon_4', notes:'Increases experience gain by 12%.', modifier: 0.12, value:65} - {index: 5, text: "Red Sword", classes:'weapon_5', notes:'Increases experience gain by 15%.', modifier: 0.15, value:90} - {index: 6, text: "Golden Sword", classes:'weapon_6', notes:'Increases experience gain by 18%.', modifier: 0.18, value:120} + {index: 0, text: "Training Sword", classes: "weapon_0", notes:'Training weapon.', strength: 0, value:0} + {index: 1, text: "Sword", classes:'weapon_1', notes:'Increases experience gain by 3%.', strength: 3, value:20} + {index: 2, text: "Axe", classes:'weapon_2', notes:'Increases experience gain by 6%.', strength: 6, value:30} + {index: 3, text: "Morningstar", classes:'weapon_3', notes:'Increases experience gain by 9%.', strength: 9, value:45} + {index: 4, text: "Blue Sword", classes:'weapon_4', notes:'Increases experience gain by 12%.', strength: 12, value:65} + {index: 5, text: "Red Sword", classes:'weapon_5', notes:'Increases experience gain by 15%.', strength: 15, value:90} + {index: 6, text: "Golden Sword", classes:'weapon_6', notes:'Increases experience gain by 18%.', strength: 18, value:120} ] armor: [ - {index: 0, text: "Cloth Armor", classes: 'armor_0', notes:'Training armor.', modifier: 0.00, value:0} - {index: 1, text: "Leather Armor", classes: 'armor_1', notes:'Decreases HP loss by 4%.', modifier: 0.04, value:30} - {index: 2, text: "Chain Mail", classes: 'armor_2', notes:'Decreases HP loss by 6%.', modifier: 0.06, value:45} - {index: 3, text: "Plate Mail", classes: 'armor_3', notes:'Decreases HP loss by 7%.', modifier: 0.07, value:65} - {index: 4, text: "Red Armor", classes: 'armor_4', notes:'Decreases HP loss by 8%.', modifier: 0.08, value:90} - {index: 5, text: "Golden Armor", classes: 'armor_5', notes:'Decreases HP loss by 10%.', modifier: 0.1, value:120} + {index: 0, text: "Cloth Armor", classes: 'armor_0', notes:'Training armor.', defense: 0, value:0} + {index: 1, text: "Leather Armor", classes: 'armor_1', notes:'Decreases HP loss by 4%.', defense: 4, value:30} + {index: 2, text: "Chain Mail", classes: 'armor_2', notes:'Decreases HP loss by 6%.', defense: 6, value:45} + {index: 3, text: "Plate Mail", classes: 'armor_3', notes:'Decreases HP loss by 7%.', defense: 7, value:65} + {index: 4, text: "Red Armor", classes: 'armor_4', notes:'Decreases HP loss by 8%.', defense: 8, value:90} + {index: 5, text: "Golden Armor", classes: 'armor_5', notes:'Decreases HP loss by 10%.', defense: 10, value:120} ] head: [ - {index: 0, text: "No Helm", classes: 'head_0', notes:'Training helm.', modifier: 0.00, value:0} - {index: 1, text: "Leather Helm", classes: 'head_1', notes:'Decreases HP loss by 2%.', modifier: 0.02, value:15} - {index: 2, text: "Chain Coif", classes: 'head_2', notes:'Decreases HP loss by 3%.', modifier: 0.03, value:25} - {index: 3, text: "Plate Helm", classes: 'head_3', notes:'Decreases HP loss by 4%.', modifier: 0.04, value:45} - {index: 4, text: "Red Helm", classes: 'head_4', notes:'Decreases HP loss by 5%.', modifier: 0.05, value:60} - {index: 5, text: "Golden Helm", classes: 'head_5', notes:'Decreases HP loss by 6%.', modifier: 0.06, value:80} + {index: 0, text: "No Helm", classes: 'head_0', notes:'Training helm.', defense: 0, value:0} + {index: 1, text: "Leather Helm", classes: 'head_1', notes:'Decreases HP loss by 2%.', defense: 2, value:15} + {index: 2, text: "Chain Coif", classes: 'head_2', notes:'Decreases HP loss by 3%.', defense: 3, value:25} + {index: 3, text: "Plate Helm", classes: 'head_3', notes:'Decreases HP loss by 4%.', defense: 4, value:45} + {index: 4, text: "Red Helm", classes: 'head_4', notes:'Decreases HP loss by 5%.', defense: 5, value:60} + {index: 5, text: "Golden Helm", classes: 'head_5', notes:'Decreases HP loss by 6%.', defense: 6, value:80} ] shield: [ - {index: 0, text: "No Shield", classes: 'shield_0', notes:'No Shield.', modifier: 0.00, value:0} - {index: 1, text: "Wooden Shield", classes: 'shield_1', notes:'Decreases HP loss by 3%', modifier: 0.03, value:20} - {index: 2, text: "Buckler", classes: 'shield_2', notes:'Decreases HP loss by 4%.', modifier: 0.04, value:35} - {index: 3, text: "Enforced Shield", classes: 'shield_3', notes:'Decreases HP loss by 5%.', modifier: 0.05, value:55} - {index: 4, text: "Red Shield", classes: 'shield_4', notes:'Decreases HP loss by 7%.', modifier: 0.07, value:70} - {index: 5, text: "Golden Shield", classes: 'shield_5', notes:'Decreases HP loss by 8%.', modifier: 0.08, value:90} + {index: 0, text: "No Shield", classes: 'shield_0', notes:'No Shield.', defense: 0, value:0} + {index: 1, text: "Wooden Shield", classes: 'shield_1', notes:'Decreases HP loss by 3%', defense: 3, value:20} + {index: 2, text: "Buckler", classes: 'shield_2', notes:'Decreases HP loss by 4%.', defense: 4, value:35} + {index: 3, text: "Enforced Shield", classes: 'shield_3', notes:'Decreases HP loss by 5%.', defense: 5, value:55} + {index: 4, text: "Red Shield", classes: 'shield_4', notes:'Decreases HP loss by 7%.', defense: 7, value:70} + {index: 5, text: "Golden Shield", classes: 'shield_5', notes:'Decreases HP loss by 8%.', defense: 8, value:90} ] potion: {type: 'potion', text: "Potion", notes: "Recover 15 HP", value: 25, classes: 'potion'} reroll: {type: 'reroll', text: "Re-Roll", classes: 'reroll', notes: "Resets your tasks. When you're struggling and everything's red, use for a clean slate.", value:0 } diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee index ea8ab4b1fb..9abe1fc3ff 100644 --- a/src/app/scoring.coffee +++ b/src/app/scoring.coffee @@ -5,53 +5,12 @@ helpers = require './helpers' browser = require './browser' character = require './character' items = require './items' +algos = require './algos' module.exports.Scoring = (model) -> - - MODIFIER = .02 # each new level, armor, weapon add 2% modifier (this mechanism will change) + MODIFIER = algos.MODIFIER # each new level, armor, weapon add 2% modifier (this mechanism will change) user = model.at '_user' - ### - Calculates Exp & GP modification based on weapon & lvl - {value} task.value for gain - {modifiers} may manually pass in stats as {weapon, exp}. This is used for testing - ### - expModifier = (value, modifiers = {}) -> - weapon = modifiers.weapon || user.get('items.weapon') - lvl = modifiers.lvl || user.get('stats.lvl') - dmg = items.items.weapon[weapon].modifier # each new weapon increases exp gain - dmg += (lvl-1) * MODIFIER # same for lvls - modified = value + (value * dmg) - return modified - - ### - Calculates HP-loss modification based on armor & lvl - {value} task.value which is hurting us - {modifiers} may manually pass in modifier as {armor, lvl}. This is used for testing - ### - hpModifier = (value, modifiers = {}) -> - armor = modifiers.armor || user.get('items.armor') - head = modifiers.head || user.get('items.head') - shield = modifiers.shield || user.get('items.shield') - lvl = modifiers.lvl || user.get('stats.lvl') - ac = items.items.armor[armor].modifier + items.items.head[head].modifier + items.items.shield[shield].modifier # each new armor decreases HP loss - ac += (lvl-1) * MODIFIER # same for lvls - modified = value - (value * ac) - return modified - - ### - Calculates the next task.value based on direction - For negative values, use a line: something like y=-.1x+1 - For positibe values, taper off with inverse log: y=.9^x - Would love to use inverse log for the whole thing, but after 13 fails it hits infinity. Revisit this formula later - {currentValue} the current value of the task, determines it's next value - {direction} 'up' or 'down' - ### - taskDeltaFormula = (currentValue, direction) -> - sign = if (direction == "up") then 1 else -1 - delta = if (currentValue < 0) then (( -0.1 * currentValue + 1 ) * sign) else (( Math.pow(0.9,currentValue) ) * sign) - return delta - ### Updates user stats with new stats. Handles death, leveling up, etc {stats} new stats @@ -75,12 +34,15 @@ module.exports.Scoring = (model) -> if newStats.exp? # level up & carry-over exp tnl = model.get '_tnl' + silent = false if newStats.exp >= tnl + silent = true newStats.exp -= tnl obj.stats.lvl++ obj.stats.hp = 50 obj.stats.exp = newStats.exp + user.pass(silent:true).set('stats.exp', obj.stats.exp) if silent # Set flags when they unlock features if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1) @@ -107,7 +69,6 @@ module.exports.Scoring = (model) -> # {times} # times to call score on this task (1 unless cron, usually) # {update} if we're running updates en-mass (eg, cron on server) pass in userObj score = (taskId, direction, times, batch, cron) -> - commit = false unless batch? commit = true @@ -136,17 +97,23 @@ module.exports.Scoring = (model) -> # Each iteration calculate the delta (nextDelta), which is then accumulated in delta # (aka, the total delta). This weirdness won't be necessary when calculating mathematically # rather than iteratively - nextDelta = taskDeltaFormula(value, direction) + nextDelta = algos.taskDeltaFormula(value, direction) value += nextDelta if adjustvalue delta += nextDelta addPoints = -> - modified = expModifier(delta) + level = user.get('stats.lvl') + weaponStrength = items.items.weapon[user.get('items.weapon')].strength + modified = algos.expModifier(delta,weaponStrength,level) exp += modified*10 gp += delta subtractPoints = -> - modified = hpModifier(delta) + level = user.get('stats.lvl') + armorDefense = items.items.armor[user.get('items.armor')].defense + helmDefense = items.items.head[user.get('items.head')].defense + shieldDefense = items.items.shield[user.get('items.shield')].defense + modified = algos.hpModifier(delta,armorDefense,helmDefense,shieldDefense,level) hp += modified switch type @@ -249,7 +216,7 @@ module.exports.Scoring = (model) -> lvl = 0 #iterator while lvl < (obj.stats.lvl-1) lvl++ - expTally += (lvl*100)/5 + expTally += algos.tnl(lvl) obj.history.exp.push { date: today, value: expTally } # Set the new user specs, and animate HP loss @@ -262,12 +229,11 @@ module.exports.Scoring = (model) -> return { - MODIFIER: MODIFIER score: score cron: cron # testing stuff - expModifier: expModifier - hpModifier: hpModifier - taskDeltaFormula: taskDeltaFormula + expModifier: algos.expModifier + hpModifier: algos.hpModifier + taskDeltaFormula: algos.taskDeltaFormula } diff --git a/views/app/header.html b/views/app/header.html index f3bc5f3c3a..2491090dae 100644 --- a/views/app/header.html +++ b/views/app/header.html @@ -29,7 +29,7 @@ {#if _user.history.exp}   {/} - {round(_user.stats.exp)} / {_tnl} + {floor(_user.stats.exp)} / {_tnl} From 9283b8adb4141ec4b106ecfff5018b9ce39d5320 Mon Sep 17 00:00:00 2001 From: Philip How Date: Sat, 2 Mar 2013 20:02:57 +0000 Subject: [PATCH 048/220] Add death notification and algorithm fixes --- src/app/algos.coffee | 14 +++-- src/app/browser.coffee | 11 ++-- src/app/debug.coffee | 9 +++ src/app/items.coffee | 2 +- src/app/scoring.coffee | 123 ++++++++++++++++++++++------------------- styles/app/alerts.styl | 14 +++++ 6 files changed, 104 insertions(+), 69 deletions(-) diff --git a/src/app/algos.coffee b/src/app/algos.coffee index 69ab9b4e93..205842d178 100644 --- a/src/app/algos.coffee +++ b/src/app/algos.coffee @@ -37,14 +37,18 @@ module.exports.gpModifier = (value, modifier) -> ### Calculates the next task.value based on direction - Uses a capped inverse log y=.9^x, y>= -5 + Uses a capped inverse log y=.95^x, y>= -5 {currentValue} the current value of the task {direction} up or down ### module.exports.taskDeltaFormula = (currentValue, direction) -> - sign = if (direction is 'up') then 1 else -1 - delta = Math.pow(0.9,currentValue) * sign - if delta < -5 then delta = -5 - console.log("CurrentValue: " + currentValue + " delta: " + delta) + if direction is 'up' + delta = Math.max(Math.pow(0.95,currentValue),0.25) + else + delta = -Math.min(Math.pow(0.95,currentValue),5) + #sign = if (direction is 'up') then 1 else -1 + #delta = Math.pow(0.95,currentValue) * sign + #if delta < -5 then delta = -5 + #console.log("CurrentValue: " + currentValue + " delta: " + delta) #delta = if (currentValue < 0) then (( -0.1 * currentValue + 1 ) * sign) else (( Math.pow(0.9,currentValue) ) * sign) return delta \ No newline at end of file diff --git a/src/app/browser.coffee b/src/app/browser.coffee index 63e6994d58..4f7b4168bf 100644 --- a/src/app/browser.coffee +++ b/src/app/browser.coffee @@ -184,7 +184,7 @@ setupGrowlNotifications = (model) -> return if user.get('stats.lvl') == 0 $.bootstrapGrowl html, ele: '#notification-area', - type: type # (null, 'info', 'error', 'success', 'gp', 'xp', 'hp', 'lvl') + type: type # (null, 'info', 'error', 'success', 'gp', 'xp', 'hp', 'lvl','death') top_offset: 20 align: 'right' # ('left', 'right', or 'center') width: 250 # (integer, or 'auto') @@ -195,7 +195,6 @@ setupGrowlNotifications = (model) -> # Setup listeners which trigger notifications user.on 'set', 'stats.hp', (captures, args) -> num = captures - args - console.log("hp = " + num) rounded = Math.abs(num.toFixed(1)) if num < 0 statsNotification " - #{rounded} HP", 'hp' # lost hp from purchase @@ -203,10 +202,9 @@ setupGrowlNotifications = (model) -> statsNotification " + #{rounded} HP", 'hp' # gained hp from potion/level? user.on 'set', 'stats.exp', (captures, args, isLocal, silent) -> - if not silent num = captures - args rounded = Math.abs(num.toFixed(1)) - if num < 0 + if num < 0 and not silent statsNotification " - #{rounded} XP", 'xp' else if num > 0 statsNotification " + #{rounded} XP", 'xp' @@ -226,4 +224,7 @@ setupGrowlNotifications = (model) -> user.on 'set', 'stats.lvl', (captures, args) -> if captures > args - statsNotification(' Level Up!', 'lvl') \ No newline at end of file + if captures is 1 and args is 0 + statsNotification ' You died!', 'death' + else + statsNotification ' Level Up!', 'lvl' \ No newline at end of file diff --git a/src/app/debug.coffee b/src/app/debug.coffee index 25cb833b8f..24195797bb 100644 --- a/src/app/debug.coffee +++ b/src/app/debug.coffee @@ -11,3 +11,12 @@ module.exports.app = (appExports, model) -> appExports.cheat = -> user.incr 'stats.exp', 20 user.incr 'stats.gp', 1000 + + appExports.reset = -> + user.set 'stats.exp', 0 + user.set 'stats.lvl', 0 + user.set 'stats.gp', 0 + user.set 'items.weapon', 0 + user.set 'items.armor', 0 + user.set 'items.head', 0 + user.set 'items.shield', 0 \ No newline at end of file diff --git a/src/app/items.coffee b/src/app/items.coffee index a9ca688f52..58799b1d1c 100644 --- a/src/app/items.coffee +++ b/src/app/items.coffee @@ -169,7 +169,7 @@ module.exports.app = (appExports, model) -> return unless captures == true html = """
    - + Congratulations, you have unlocked the Item Store! You can now buy weapons, armor, potions, etc. Read each item's comment for more information. [Close]
    diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee index 9abe1fc3ff..a25bc8d34e 100644 --- a/src/app/scoring.coffee +++ b/src/app/scoring.coffee @@ -11,59 +11,6 @@ module.exports.Scoring = (model) -> MODIFIER = algos.MODIFIER # each new level, armor, weapon add 2% modifier (this mechanism will change) user = model.at '_user' - ### - Updates user stats with new stats. Handles death, leveling up, etc - {stats} new stats - {update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately - ### - updateStats = (newStats, batch) -> - obj = batch.obj() - - # if user is dead, dont do anything - return if obj.stats.lvl == 0 - - if newStats.hp? - # Game Over - if newStats.hp <= 0 - obj.stats.lvl = 0 # signifies dead - obj.stats.hp = 0 - return - else - obj.stats.hp = newStats.hp - - if newStats.exp? - # level up & carry-over exp - tnl = model.get '_tnl' - silent = false - if newStats.exp >= tnl - silent = true - newStats.exp -= tnl - obj.stats.lvl++ - obj.stats.hp = 50 - - obj.stats.exp = newStats.exp - user.pass(silent:true).set('stats.exp', obj.stats.exp) if silent - - # Set flags when they unlock features - if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1) - batch.set 'flags.customizationsNotification', true - obj.flags.customizationsNotification = true - if !obj.flags.itemsEnabled and obj.stats.lvl >= 2 - # Set to object, then also send to browser right away to get model.on() subscription notification - batch.set 'flags.itemsEnabled', true - obj.flags.itemsEnabled = true - if !obj.flags.partyEnabled and obj.stats.lvl >= 3 - batch.set 'flags.partyEnabled', true - obj.flags.partyEnabled = true - if !obj.flags.petsEnabled and obj.stats.lvl >= 4 - batch.set 'flags.petsEnabled', true - obj.flags.petsEnabled = true - - if newStats.gp? - #FIXME what was I doing here? I can't remember, gp isn't defined - gp = 0.0 if (!gp? or gp<0) - obj.stats.gp = newStats.gp - # {taskId} task you want to score # {direction} 'up' or 'down' # {times} # times to call score on this task (1 unless cron, usually) @@ -119,7 +66,8 @@ module.exports.Scoring = (model) -> switch type when 'habit' # Don't adjust values for habits that don't have both + and - - adjustvalue = if (taskObj.up==false or taskObj.down==false) then false else true + #adjustvalue = if (taskObj.up==false or taskObj.down==false) then false else true + adjustvalue = true; calculateDelta(adjustvalue) # Add habit value to habit-history (if different) if (delta > 0) then addPoints() else subtractPoints() @@ -130,15 +78,20 @@ module.exports.Scoring = (model) -> batch.set "#{taskPath}.history", taskObj.history when 'daily' - calculateDelta() + #calculateDelta() if cron? # cron + calculateDelta() subtractPoints() else + calculateDelta(false) addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes when 'todo' - calculateDelta() - unless cron? # don't touch stats on cron + if cron? #cron + calculateDelta() + #don't touch stats on cron + else + calculateDelta(false) addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes when 'reward' @@ -161,10 +114,64 @@ module.exports.Scoring = (model) -> newStats = _.clone batch.obj().stats _.each Object.keys(origStats), (key) -> obj.stats[key] = origStats[key] batch.setStats(newStats) - # batch.setStats() + # batch.setStats() batch.commit() return delta + ### + Updates user stats with new stats. Handles death, leveling up, etc + {stats} new stats + {update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately + ### + updateStats = (newStats, batch) -> + obj = batch.obj() + + # if user is dead, dont do anything + return if obj.stats.lvl == 0 + + if newStats.hp? + # Game Over + if newStats.hp <= 0 + obj.stats.lvl = 0 # signifies dead + obj.stats.hp = 0 + return + else + obj.stats.hp = newStats.hp + + if newStats.exp? + # level up & carry-over exp + tnl = model.get '_tnl' + silent = false + if newStats.exp >= tnl + silent = true + user.set('stats.exp', newStats.exp) + newStats.exp -= tnl + obj.stats.lvl++ + obj.stats.hp = 50 + + obj.stats.exp = newStats.exp + user.pass(silent:true).set('stats.exp', obj.stats.exp) if silent + + # Set flags when they unlock features + if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1) + batch.set 'flags.customizationsNotification', true + obj.flags.customizationsNotification = true + if !obj.flags.itemsEnabled and obj.stats.lvl >= 2 + # Set to object, then also send to browser right away to get model.on() subscription notification + batch.set 'flags.itemsEnabled', true + obj.flags.itemsEnabled = true + if !obj.flags.partyEnabled and obj.stats.lvl >= 3 + batch.set 'flags.partyEnabled', true + obj.flags.partyEnabled = true + if !obj.flags.petsEnabled and obj.stats.lvl >= 4 + batch.set 'flags.petsEnabled', true + obj.flags.petsEnabled = true + + if newStats.gp? + #FIXME what was I doing here? I can't remember, gp isn't defined + gp = 0.0 if (!gp? or gp<0) + obj.stats.gp = newStats.gp + ### At end of day, add value to all incomplete Daily & Todo tasks (further incentive) For incomplete Dailys, deduct experience diff --git a/styles/app/alerts.styl b/styles/app/alerts.styl index 38e411f480..97f490cf52 100644 --- a/styles/app/alerts.styl +++ b/styles/app/alerts.styl @@ -8,6 +8,8 @@ lvlColor = #d9edf7 lvlText = #3a87ad xpColor = #DCC0FB xpText = #635673 +deathColor = #4E4E4E +deathText = #ABABAB borderDarken = 20% // alert styles @@ -32,6 +34,11 @@ borderDarken = 20% border-color: darken(xpColor,borderDarken) color: xpText +.alert-death + background-color: deathColor + border-color: deathColor + color: deathText + // alert icons .icon-gold @@ -46,4 +53,11 @@ borderDarken = 20% background-position: center center background-size: 18px width: 14px + height: 14px + +.icon-death + background: url("img/sprites/dead.png") no-repeat + background-position: center center + background-size: 14px + width: 14px height: 14px \ No newline at end of file From 81eb070ee65c02ec8903f0fbe806b0cbdd93436c Mon Sep 17 00:00:00 2001 From: Philip How Date: Sat, 2 Mar 2013 20:03:16 +0000 Subject: [PATCH 049/220] Reset button --- views/app/footer.html | 1 + 1 file changed, 1 insertion(+) diff --git a/views/app/footer.html b/views/app/footer.html index 60ccc1b9c9..35be956150 100644 --- a/views/app/footer.html +++ b/views/app/footer.html @@ -20,6 +20,7 @@
    +
    {/}
    From d20fccea752f1b8791d1ec2d24697630b49223d4 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 2 Mar 2013 23:38:31 -0500 Subject: [PATCH 050/220] pass model by reference to scoring functions --- src/app/index.coffee | 3 +- src/app/scoring.coffee | 434 ++++++++++++++++++----------------- src/app/tasks.coffee | 7 +- src/server/deprecated.coffee | 3 +- 4 files changed, 223 insertions(+), 224 deletions(-) diff --git a/src/app/index.coffee b/src/app/index.coffee index b25e52cb9d..73a33d3c9b 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -45,13 +45,12 @@ get '/', (page, model, params, next) -> ready (model) -> user = model.at('_user') - score = new scoring.Scoring(model) #set cron immediately lastCron = user.get('lastCron') user.set('lastCron', +new Date) if (!lastCron? or lastCron == 'new') - score.cron() + scoring.cron(model) character.app(exports, model) tasks.app(exports, model) diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee index a25bc8d34e..6818db1bac 100644 --- a/src/app/scoring.coffee +++ b/src/app/scoring.coffee @@ -7,240 +7,242 @@ character = require './character' items = require './items' algos = require './algos' -module.exports.Scoring = (model) -> - MODIFIER = algos.MODIFIER # each new level, armor, weapon add 2% modifier (this mechanism will change) +MODIFIER = algos.MODIFIER # each new level, armor, weapon add 2% modifier (this mechanism will change) + +# {taskId} task you want to score +# {direction} 'up' or 'down' +# {times} # times to call score on this task (1 unless cron, usually) +# {update} if we're running updates en-mass (eg, cron on server) pass in userObj +score = (model, taskId, direction, times, batch, cron) -> user = model.at '_user' - # {taskId} task you want to score - # {direction} 'up' or 'down' - # {times} # times to call score on this task (1 unless cron, usually) - # {update} if we're running updates en-mass (eg, cron on server) pass in userObj - score = (taskId, direction, times, batch, cron) -> - commit = false - unless batch? - commit = true - batch = new character.BatchUpdate(model) - batch.startTransaction() - obj = batch.obj() + commit = false + unless batch? + commit = true + batch = new character.BatchUpdate(model) + batch.startTransaction() + obj = batch.obj() - {gp, hp, exp, lvl} = obj.stats + {gp, hp, exp, lvl} = obj.stats - taskPath = "tasks.#{taskId}" - taskObj = obj.tasks[taskId] - {type, value} = taskObj + taskPath = "tasks.#{taskId}" + taskObj = obj.tasks[taskId] + {type, value} = taskObj - # If they're trying to purhcase a too-expensive reward, confirm they want to take a hit for it - if taskObj.value > obj.stats.gp and taskObj.type is 'reward' - r = confirm "Not enough GP to purchase this reward, buy anyway and lose HP? (Punishment for taking a reward you didn't earn)." - unless r - batch.commit() - return - - delta = 0 - times ?= 1 - calculateDelta = (adjustvalue=true) -> - # If multiple days have passed, multiply times days missed - _.times times, (n) -> - # Each iteration calculate the delta (nextDelta), which is then accumulated in delta - # (aka, the total delta). This weirdness won't be necessary when calculating mathematically - # rather than iteratively - nextDelta = algos.taskDeltaFormula(value, direction) - value += nextDelta if adjustvalue - delta += nextDelta - - addPoints = -> - level = user.get('stats.lvl') - weaponStrength = items.items.weapon[user.get('items.weapon')].strength - modified = algos.expModifier(delta,weaponStrength,level) - exp += modified*10 - gp += delta - - subtractPoints = -> - level = user.get('stats.lvl') - armorDefense = items.items.armor[user.get('items.armor')].defense - helmDefense = items.items.head[user.get('items.head')].defense - shieldDefense = items.items.shield[user.get('items.shield')].defense - modified = algos.hpModifier(delta,armorDefense,helmDefense,shieldDefense,level) - hp += modified - - switch type - when 'habit' - # Don't adjust values for habits that don't have both + and - - #adjustvalue = if (taskObj.up==false or taskObj.down==false) then false else true - adjustvalue = true; - calculateDelta(adjustvalue) - # Add habit value to habit-history (if different) - if (delta > 0) then addPoints() else subtractPoints() - taskObj.history ?= [] - if taskObj.value != value - historyEntry = { date: +new Date, value: value } - taskObj.history.push historyEntry - batch.set "#{taskPath}.history", taskObj.history - - when 'daily' - #calculateDelta() - if cron? # cron - calculateDelta() - subtractPoints() - else - calculateDelta(false) - addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes - - when 'todo' - if cron? #cron - calculateDelta() - #don't touch stats on cron - else - calculateDelta(false) - addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes - - when 'reward' - # Don't adjust values for rewards - calculateDelta(false) - # purchase item - gp -= Math.abs(taskObj.value) - num = parseFloat(taskObj.value).toFixed(2) - # if too expensive, reduce health & zero gp - if gp < 0 - hp += gp # hp - gp difference - gp = 0 - - taskObj.value = value - batch.set "#{taskPath}.value", taskObj.value - origStats = _.clone obj.stats - updateStats {hp: hp, exp: exp, gp: gp}, batch - if commit - # newStats / origStats is a glorious hack to trick Derby into seeing the change in model.on(*) - newStats = _.clone batch.obj().stats - _.each Object.keys(origStats), (key) -> obj.stats[key] = origStats[key] - batch.setStats(newStats) - # batch.setStats() + # If they're trying to purhcase a too-expensive reward, confirm they want to take a hit for it + if taskObj.value > obj.stats.gp and taskObj.type is 'reward' + r = confirm "Not enough GP to purchase this reward, buy anyway and lose HP? (Punishment for taking a reward you didn't earn)." + unless r batch.commit() - return delta + return - ### - Updates user stats with new stats. Handles death, leveling up, etc - {stats} new stats - {update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately - ### - updateStats = (newStats, batch) -> - obj = batch.obj() + delta = 0 + times ?= 1 + calculateDelta = (adjustvalue=true) -> + # If multiple days have passed, multiply times days missed + _.times times, (n) -> + # Each iteration calculate the delta (nextDelta), which is then accumulated in delta + # (aka, the total delta). This weirdness won't be necessary when calculating mathematically + # rather than iteratively + nextDelta = algos.taskDeltaFormula(value, direction) + value += nextDelta if adjustvalue + delta += nextDelta - # if user is dead, dont do anything - return if obj.stats.lvl == 0 + addPoints = -> + level = user.get('stats.lvl') + weaponStrength = items.items.weapon[user.get('items.weapon')].strength + modified = algos.expModifier(delta,weaponStrength,level) + exp += modified*10 + gp += delta - if newStats.hp? - # Game Over - if newStats.hp <= 0 - obj.stats.lvl = 0 # signifies dead - obj.stats.hp = 0 - return + subtractPoints = -> + level = user.get('stats.lvl') + armorDefense = items.items.armor[user.get('items.armor')].defense + helmDefense = items.items.head[user.get('items.head')].defense + shieldDefense = items.items.shield[user.get('items.shield')].defense + modified = algos.hpModifier(delta,armorDefense,helmDefense,shieldDefense,level) + hp += modified + + switch type + when 'habit' + # Don't adjust values for habits that don't have both + and - + #adjustvalue = if (taskObj.up==false or taskObj.down==false) then false else true + adjustvalue = true; + calculateDelta(adjustvalue) + # Add habit value to habit-history (if different) + if (delta > 0) then addPoints() else subtractPoints() + taskObj.history ?= [] + if taskObj.value != value + historyEntry = { date: +new Date, value: value } + taskObj.history.push historyEntry + batch.set "#{taskPath}.history", taskObj.history + + when 'daily' + #calculateDelta() + if cron? # cron + calculateDelta() + subtractPoints() else - obj.stats.hp = newStats.hp + calculateDelta(false) + addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes - if newStats.exp? - # level up & carry-over exp - tnl = model.get '_tnl' - silent = false - if newStats.exp >= tnl - silent = true - user.set('stats.exp', newStats.exp) - newStats.exp -= tnl - obj.stats.lvl++ - obj.stats.hp = 50 + when 'todo' + if cron? #cron + calculateDelta() + #don't touch stats on cron + else + calculateDelta(false) + addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes - obj.stats.exp = newStats.exp - user.pass(silent:true).set('stats.exp', obj.stats.exp) if silent + when 'reward' + # Don't adjust values for rewards + calculateDelta(false) + # purchase item + gp -= Math.abs(taskObj.value) + num = parseFloat(taskObj.value).toFixed(2) + # if too expensive, reduce health & zero gp + if gp < 0 + hp += gp # hp - gp difference + gp = 0 - # Set flags when they unlock features - if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1) - batch.set 'flags.customizationsNotification', true - obj.flags.customizationsNotification = true - if !obj.flags.itemsEnabled and obj.stats.lvl >= 2 - # Set to object, then also send to browser right away to get model.on() subscription notification - batch.set 'flags.itemsEnabled', true - obj.flags.itemsEnabled = true - if !obj.flags.partyEnabled and obj.stats.lvl >= 3 - batch.set 'flags.partyEnabled', true - obj.flags.partyEnabled = true - if !obj.flags.petsEnabled and obj.stats.lvl >= 4 - batch.set 'flags.petsEnabled', true - obj.flags.petsEnabled = true + taskObj.value = value + batch.set "#{taskPath}.value", taskObj.value + origStats = _.clone obj.stats + updateStats model, {hp: hp, exp: exp, gp: gp}, batch + if commit + # newStats / origStats is a glorious hack to trick Derby into seeing the change in model.on(*) + newStats = _.clone batch.obj().stats + _.each Object.keys(origStats), (key) -> obj.stats[key] = origStats[key] + batch.setStats(newStats) + # batch.setStats() + batch.commit() + return delta - if newStats.gp? - #FIXME what was I doing here? I can't remember, gp isn't defined - gp = 0.0 if (!gp? or gp<0) - obj.stats.gp = newStats.gp +### + Updates user stats with new stats. Handles death, leveling up, etc + {stats} new stats + {update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately +### +updateStats = (model, newStats, batch) -> + user = model.at '_user' + obj = batch.obj() - ### - At end of day, add value to all incomplete Daily & Todo tasks (further incentive) - For incomplete Dailys, deduct experience - ### - cron = () -> - today = +new Date - daysPassed = helpers.daysBetween(today, user.get('lastCron')) - if daysPassed > 0 - batch = new character.BatchUpdate(model) - batch.startTransaction() - batch.set 'lastCron', today - obj = batch.obj() - hpBefore = obj.stats.hp #we'll use this later so we can animate hp loss - # Tally each task - todoTally = 0 - _.each obj.tasks, (taskObj) -> - {id, type, completed, repeat} = taskObj - 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 - daysFailed = 0 - _.times daysPassed, (n) -> - thatDay = moment().subtract('days', n+1) - if repeat[helpers.dayMapping[thatDay.day()]]==true - daysFailed++ - score id, 'down', daysFailed, batch, true + # if user is dead, dont do anything + return if obj.stats.lvl == 0 - if type == 'daily' - taskObj.history ?= [] - taskObj.history.push { date: +new Date, value: value } - batch.set "tasks.#{taskObj.id}.history", taskObj.history - batch.set "tasks.#{taskObj.id}.completed", false - else - value = obj.tasks[taskObj.id].value #get updated value - absVal = if (completed) then Math.abs(value) else value - todoTally += absVal + if newStats.hp? + # Game Over + if newStats.hp <= 0 + obj.stats.lvl = 0 # signifies dead + obj.stats.hp = 0 + return + else + obj.stats.hp = newStats.hp - # Finished tallying - obj.history ?= {}; obj.history.todos ?= []; obj.history.exp ?= [] - obj.history.todos.push { date: today, value: todoTally } - # tally experience - expTally = obj.stats.exp - lvl = 0 #iterator - while lvl < (obj.stats.lvl-1) - lvl++ - expTally += algos.tnl(lvl) - obj.history.exp.push { date: today, value: expTally } + if newStats.exp? + # level up & carry-over exp + tnl = model.get '_tnl' + silent = false + if newStats.exp >= tnl + silent = true + user.set('stats.exp', newStats.exp) + newStats.exp -= tnl + obj.stats.lvl++ + obj.stats.hp = 50 - # Set the new user specs, and animate HP loss - [hpAfter, obj.stats.hp] = [obj.stats.hp, hpBefore] - batch.setStats() - batch.set('history', obj.history) - batch.commit() - browser.resetDom(model) - setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss + obj.stats.exp = newStats.exp + user.pass(silent:true).set('stats.exp', obj.stats.exp) if silent + + # Set flags when they unlock features + if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1) + batch.set 'flags.customizationsNotification', true + obj.flags.customizationsNotification = true + if !obj.flags.itemsEnabled and obj.stats.lvl >= 2 + # Set to object, then also send to browser right away to get model.on() subscription notification + batch.set 'flags.itemsEnabled', true + obj.flags.itemsEnabled = true + if !obj.flags.partyEnabled and obj.stats.lvl >= 3 + batch.set 'flags.partyEnabled', true + obj.flags.partyEnabled = true + if !obj.flags.petsEnabled and obj.stats.lvl >= 4 + batch.set 'flags.petsEnabled', true + obj.flags.petsEnabled = true + + if newStats.gp? + #FIXME what was I doing here? I can't remember, gp isn't defined + gp = 0.0 if (!gp? or gp<0) + obj.stats.gp = newStats.gp + +### + At end of day, add value to all incomplete Daily & Todo tasks (further incentive) + For incomplete Dailys, deduct experience +### +cron = (model) -> + user = model.at '_user' + today = +new Date + daysPassed = helpers.daysBetween(today, user.get('lastCron')) + if daysPassed > 0 + batch = new character.BatchUpdate(model) + batch.startTransaction() + batch.set 'lastCron', today + obj = batch.obj() + hpBefore = obj.stats.hp #we'll use this later so we can animate hp loss + # Tally each task + todoTally = 0 + _.each obj.tasks, (taskObj) -> + {id, type, completed, repeat} = taskObj + 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 + daysFailed = 0 + _.times daysPassed, (n) -> + thatDay = moment().subtract('days', n+1) + if repeat[helpers.dayMapping[thatDay.day()]]==true + daysFailed++ + score model, id, 'down', daysFailed, batch, true + + if type == 'daily' + taskObj.history ?= [] + taskObj.history.push { date: +new Date, value: value } + batch.set "tasks.#{taskObj.id}.history", taskObj.history + batch.set "tasks.#{taskObj.id}.completed", false + else + value = obj.tasks[taskObj.id].value #get updated value + absVal = if (completed) then Math.abs(value) else value + todoTally += absVal + + # Finished tallying + obj.history ?= {}; obj.history.todos ?= []; obj.history.exp ?= [] + obj.history.todos.push { date: today, value: todoTally } + # tally experience + expTally = obj.stats.exp + lvl = 0 #iterator + while lvl < (obj.stats.lvl-1) + lvl++ + expTally += algos.tnl(lvl) + obj.history.exp.push { date: today, value: expTally } + + # Set the new user specs, and animate HP loss + [hpAfter, obj.stats.hp] = [obj.stats.hp, hpBefore] + batch.setStats() + batch.set('history', obj.history) + batch.commit() + browser.resetDom(model) + setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss - return { - score: score - cron: cron +module.exports = { + score: score + cron: cron - # testing stuff - expModifier: algos.expModifier - hpModifier: algos.hpModifier - taskDeltaFormula: algos.taskDeltaFormula - } + # testing stuff + expModifier: algos.expModifier + hpModifier: algos.hpModifier + taskDeltaFormula: algos.taskDeltaFormula +} diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index 22995d868c..f84e8d2b2f 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -26,7 +26,6 @@ module.exports.view = (view) -> module.exports.app = (appExports, model) -> user = model.at('_user') - score = new scoring.Scoring(model) user.on 'set', 'tasks.*.completed', (i, completed, previous, isLocal, passed) -> return if passed? && passed.cron # Don't do this stuff on cron @@ -37,7 +36,7 @@ module.exports.app = (appExports, model) -> # Score the user based on todo task task = user.at("tasks.#{i}") - score.score(i, direction()) + scoring.score(model, i, direction()) appExports.addTask = (e, el, next) -> type = $(el).attr('data-task-type') @@ -82,7 +81,7 @@ module.exports.app = (appExports, model) -> return # Cancel. Don't delete, don't hurt user else task.set('type','habit') # hack to make sure it hits HP, instead of performing "undo checkbox" - score.score(id, direction:'down') + scoring.score(model, id, direction:'down') # prevent accidently deleting long-standing tasks else @@ -172,4 +171,4 @@ module.exports.app = (appExports, model) -> direction = 'up' if direction == 'true/' direction = 'down' if direction == 'false/' task = model.at $(el).parents('li')[0] - score.score(task.get('id'), direction) + scoring.score(model, task.get('id'), direction) diff --git a/src/server/deprecated.coffee b/src/server/deprecated.coffee index d4e5d3b694..c781433aca 100644 --- a/src/server/deprecated.coffee +++ b/src/server/deprecated.coffee @@ -48,8 +48,7 @@ router.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> down: true notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." - score = new scoring.Scoring(model) - delta = score.score(taskId, direction) + delta = scoring.score(model, taskId, direction) result = model.get ('_user.stats') result.delta = delta res.send(result) From e6e960ae5eb41abcf8c8e61dd094ab8de3397e03 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 2 Mar 2013 23:52:47 -0500 Subject: [PATCH 051/220] performance boost - only re-establish popover / tooltips on saving task --- src/app/browser.coffee | 5 ----- src/app/tasks.coffee | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/browser.coffee b/src/app/browser.coffee index 351573cf43..b7dc450972 100644 --- a/src/app/browser.coffee +++ b/src/app/browser.coffee @@ -105,11 +105,6 @@ setupSortable = (model) -> setupTooltips = (model) -> $('[rel=tooltip]').tooltip() $('[rel=popover]').popover() - # FIXME: this isn't very efficient, do model.on set for specific attrs for popover - model.on 'set', '*', -> - $('[rel=tooltip]').tooltip() - $('[rel=popover]').popover() - setupTour = (model) -> tourSteps = [ diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index f84e8d2b2f..50a7fa976b 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -172,3 +172,8 @@ module.exports.app = (appExports, model) -> direction = 'down' if direction == 'false/' task = model.at $(el).parents('li')[0] scoring.score(model, task.get('id'), direction) + + appExports.tasksSaveAndClose = -> + # When they update their notes, re-establish tooltip & popover + $('[rel=tooltip]').tooltip() + $('[rel=popover]').popover() From 0f0ca21f829513e66956eeb81ce11de5874c6cce Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 2 Mar 2013 23:57:10 -0500 Subject: [PATCH 052/220] fix default dailies repeat schema --- src/app/character.coffee | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/character.coffee b/src/app/character.coffee index de1b280c2d..155ea7961e 100644 --- a/src/app/character.coffee +++ b/src/app/character.coffee @@ -118,14 +118,18 @@ module.exports.newUserObject = -> newUser = lodash.cloneDeep userSchema newUser.apiToken = derby.uuid() + repeat = {m:true,t:true,w:true,th:true,f:true,s:true,su:true} defaultTasks = [ {type: 'habit', text: '1h Productive Work', notes: '-- Habits: Constantly Track --\nFor some habits, it only makes sense to *gain* points (like this one).', value: 0, up: true, down: false } {type: 'habit', text: 'Eat Junk Food', notes: 'For others, it only makes sense to *lose* points', value: 0, up: false, down: true} {type: 'habit', text: 'Take The Stairs', notes: 'For the rest, both + and - make sense (stairs = gain, elevator = lose)', value: 0, up: true, down: true} - {type: 'daily', text: '1h Personal Project', notes: '-- Dailies: Complete Once a Day --\nAt the end of each day, non-completed Dailies dock you points.', value: 0, completed: false } - {type: 'daily', text: 'Exercise', notes: "If you are doing well, they turn green and are less valuable (experience, gold) and less damaging (HP). This means you can ease up on them for a bit.", value: 3, completed: false } - {type: 'daily', text: '45m Reading', notes: 'But if you are doing poorly, they turn red. The worse you do, the more valuable (exp, gold) and more damaging (HP) these goals become. This encourages you to focus on your shortcomings, the reds.', value: -10, completed: false } + + {type: 'daily', text: '1h Personal Project', notes: '-- Dailies: Complete Once a Day --\nAt the end of each day, non-completed Dailies dock you points.', value: 0, completed: false, repeat: repeat } + {type: 'daily', text: 'Exercise', notes: "If you are doing well, they turn green and are less valuable (experience, gold) and less damaging (HP). This means you can ease up on them for a bit.", value: 3, completed: false, repeat: repeat } + {type: 'daily', text: '45m Reading', notes: 'But if you are doing poorly, they turn red. The worse you do, the more valuable (exp, gold) and more damaging (HP) these goals become. This encourages you to focus on your shortcomings, the reds.', value: -10, completed: false, repeat: repeat } + {type: 'todo', text: 'Call Mom', notes: "-- Todos: Complete Eventually --\nNon-completed Todos won't hurt you, but they will become more valuable over time. This will encourage you to wrap up stale Todos.", value: -3, completed: false } + {type: 'reward', text: '1 Episode of Game of Thrones', notes: '-- Rewards: Treat Yourself! --\nAs you complete goals, you earn gold to buy rewards. Buy them liberally - rewards are integral in forming good habits.', value: 20 } {type: 'reward', text: 'Cake', notes: 'But only buy if you have enough gold - you lose HP otherwise.', value: 10 } ] From 34f5f2dc54fc393fddd8efb0c1a6cdde5fa73d6b Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 3 Mar 2013 00:17:39 -0500 Subject: [PATCH 053/220] custom dayStart --- CHANGELOG.md | 3 ++- src/app/character.coffee | 2 +- src/app/helpers.coffee | 8 +++++--- src/app/scoring.coffee | 2 +- test/user.mocha.coffee | 21 +++++++++++++++++---- views/app/settings.html | 11 +++++++++++ 6 files changed, 37 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1333ed77b7..5f1508613a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ -TODO +3/3/2013 + * Add custom day start: https://trello.com/card/custom-day-start/50e5d3684fe3a7266b0036d6/15 \ No newline at end of file diff --git a/src/app/character.coffee b/src/app/character.coffee index 155ea7961e..d9c3208c22 100644 --- a/src/app/character.coffee +++ b/src/app/character.coffee @@ -99,7 +99,7 @@ userSchema = stats: { gp: 0, exp: 0, lvl: 1, hp: 50 } party: { current: null, invitation: null } items: { weapon: 0, armor: 0, head: 0, shield: 0 } - preferences: { gender: 'm', skin: 'white', hair: 'blond', armorSet: 'v1' } + preferences: { gender: 'm', skin: 'white', hair: 'blond', armorSet: 'v1', dayStart:0 } habitIds: [] dailyIds: [] todoIds: [] diff --git a/src/app/helpers.coffee b/src/app/helpers.coffee index 5c2cccaa15..ff0c82459d 100644 --- a/src/app/helpers.coffee +++ b/src/app/helpers.coffee @@ -1,8 +1,10 @@ moment = require('moment') -# Absolute diff between two dates, based on 12am for both days -module.exports.daysBetween = (a, b) -> - Math.abs(moment(a).startOf('day').diff(moment(b).startOf('day'), 'days')) +# Absolute diff between two dates +module.exports.daysBetween = (yesterday, now, dayStart) -> + #sanity-check reset-time (is it 24h time?) + dayStart = 0 unless (dayStart? and (dayStart = parseInt(dayStart)) and dayStart >= 0 and dayStart <= 24) + Math.abs moment(yesterday).startOf('day').add('h', dayStart).diff(moment(now), 'days') module.exports.dayMapping = dayMapping = {0:'su',1:'m',2:'t',3:'w',4:'th',5:'f',6:'s',7:'su'} diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee index 6818db1bac..f9f13a909c 100644 --- a/src/app/scoring.coffee +++ b/src/app/scoring.coffee @@ -180,7 +180,7 @@ updateStats = (model, newStats, batch) -> cron = (model) -> user = model.at '_user' today = +new Date - daysPassed = helpers.daysBetween(today, user.get('lastCron')) + daysPassed = helpers.daysBetween(user.get('lastCron'), today, user.get('preferences.dayStart')) if daysPassed > 0 batch = new character.BatchUpdate(model) batch.startTransaction() diff --git a/test/user.mocha.coffee b/test/user.mocha.coffee index 3f4cc1ec5c..33ab7ee431 100644 --- a/test/user.mocha.coffee +++ b/test/user.mocha.coffee @@ -1,13 +1,14 @@ {expect} = require 'derby/node_modules/racer/test/util' {BrowserModel: Model} = require 'derby/node_modules/racer/test/util/model' derby = require 'derby' -clone = require 'clone' +lodash = require 'lodash' _ = require 'underscore' moment = require 'moment' # Custom modules scoring = require '../src/app/scoring' -schema = require '../src/app/schema' +schema = require '../src/app/character' +helpers = require '../src/app/helpers' ###### Helpers & Variables ###### @@ -19,8 +20,8 @@ taskPath = null # Otherwise, using model.get(path) will give the same object before as after pathSnapshots = (paths) -> if _.isString(paths) - return clone(model.get(paths)) - _.map paths, (path) -> clone(model.get(path)) + return lodash.cloneDeep(model.get(paths)) + _.map paths, (path) -> lodash.cloneDeep(model.get(path)) statsTask = -> pathSnapshots(['_user.stats', taskPath]) # quick snapshot of user.stats & task cleanUserObj = -> @@ -67,6 +68,18 @@ modificationsLookup = (direction, options = {}) -> ###### Specs ###### +describe 'Cron', -> + it 'calculates day differences with dayStart properly', -> + dayStart = 4 + yesterday = moment().subtract('d', 1).add('h', dayStart) + now = moment().startOf('day').add('h', dayStart-1) #today + console.log {yesterday: yesterday.format('MM/DD HH:00'), now: now.format('MM/DD HH:00')} + console.log {diff: Math.abs(moment(yesterday).diff(moment(now), 'days'))} + expect(helpers.daysBetween(yesterday, now, dayStart)).to.eql 0 + now = moment().startOf('day').add('h', dayStart) + console.log {now: now.format('MM/DD HH:00')} + expect(helpers.daysBetween(yesterday, now, dayStart)).to.eql 1 + describe 'User', -> model = null diff --git a/views/app/settings.html b/views/app/settings.html index 0bba816477..d9715751f2 100644 --- a/views/app/settings.html +++ b/views/app/settings.html @@ -9,8 +9,19 @@
    API Token
    {_user.apiToken}
    +
    +

    Custom Day Start

    +
    + + :00 (24h) +
    +
    + Habit defaults to check and reset your dailies at midnight each day. You can customize that here (Enter number between 0 and 24). +
    + {{#if _user.auth.local}}
    +

    Change Password

    {{/}} From cc4d1b2508738b269e8409e4c223588f3c6d4648 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 3 Mar 2013 01:41:52 -0500 Subject: [PATCH 054/220] fix double-alerts bug --- src/app/browser.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/browser.coffee b/src/app/browser.coffee index b7dc450972..25872b0ee1 100644 --- a/src/app/browser.coffee +++ b/src/app/browser.coffee @@ -2,6 +2,7 @@ _ = require 'underscore' moment = require 'moment' #algos = require './algos' + module.exports.restoreRefs = restoreRefs = (model) -> # tnl function model.fn '_tnl', '_user.stats.lvl', (lvl) -> @@ -22,13 +23,13 @@ module.exports.resetDom = (model) -> module.exports.app = (appExports, model) -> reconstructPage model + setupGrowlNotifications(model) unless model.get('_view.mobileDevice') reconstructPage = (model) -> loadJavaScripts(model) setupSortable(model) setupTooltips(model) setupTour(model) - setupGrowlNotifications(model) unless model.get('_view.mobileDevice') $('.datepicker').datepicker({autoclose:true, todayBtn:true}) .on 'changeDate', (ev) -> #for some reason selecting a date doesn't fire a change event on the field, meaning our changes aren't saved From 69c8d7835211e5fdc2959d148ce4ff5e44c208c8 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 3 Mar 2013 11:34:09 -0500 Subject: [PATCH 055/220] purchase tokens accessControl bug fix --- src/server/store.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/store.coffee b/src/server/store.coffee index ac4ab491af..ad463ecbaa 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -53,9 +53,10 @@ userAccess = (store) -> oldBalance = @session.req?._racerModel?.get("users.#{id}.balance") || 0 purchasingSomethingOnClient = newBalance < oldBalance - accept(purchasingSomethingOnClient or @session.req?._isServer) + accept(purchasingSomethingOnClient or derbyAuth.isServer(@)) store.writeAccess "*", "users.*.flags.ads", -> # captures, value, accept, err -> + accept = arguments[arguments.length - 2] err = arguments[arguments.length - 1] # return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@) return accept(false) if derbyAuth.bustedSession(@) From afaf3580053a2bf27df063f7a3bc914e8eb8fc93 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 3 Mar 2013 12:15:25 -0500 Subject: [PATCH 056/220] update to chest favicon --- public/favicon.ico | Bin 398 -> 789 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/public/favicon.ico b/public/favicon.ico index a1783aac9903d3eed7aaed16713514300cd2f939..0a6f815a75432e3c2bbe73fa5192629e9edec0e7 100644 GIT binary patch delta 730 zcmeBUp2{{srJgOx+ueoXKL{?^yL>VO0|RG)M`SUO_5fqIli79*3`}~SE{-79P~ z&lU+3Ia>ey@65S(Ps%&rY1xn()vsZjmZfFgkQ7-Af{2f{?=N>W3p$*G!>y{Xw?{Ks8?sb#GC%dZhRjwuH@1ZFCmuQr`{v4dmcMTQlE{E7-n z5^DppmN4?vztvbA%+GcFK;@-{$pIEgtNouROBL6wIK^J}TebD`pL1MRcXiD^Gp_qo z!p|0y9jkIyTT650pOq6C9R+eiJ}vvqyLLgryd7bM-`yG+vZd=q=blrvt8louLY#w% zLrp(1-T&iv&~_S z&&LNJjT!Fkt8<;V^u&r~N&*~9W+o^JM`T&@?czKq`!x2~f%Ub=fBkZ2;QJ=)E1i6G zVx!@_lRJC1J3_?Qg??%y)o1c=IR*BGou0fSEoqeLBIu`?jr>mdKI;Vst05^XzO8@`> delta 339 zcmV-Z0j&O&295)eB$MX?B!2-gNklF;2ul40Iw|8s5MU?xf^?K!p&}DJ@Oj zl1JnPX=$K}=tz;2fOIqw9Ssy$BHgmHWFmV{fdWgrYuV#jd+kk}bMhZK!ItOw%+hsT zvIO=x92ScVvMh5iZwL4Od2rBSiU)!fU Date: Sun, 3 Mar 2013 12:22:56 -0500 Subject: [PATCH 057/220] remove ical link from new API. shouldn't even be in old API, really - it's just a link... don't know why I did that. but alas, people already have subscriptions on that link so if it aint broke --- src/server/api.coffee | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 287b354f5d..a7ff84d219 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -114,31 +114,4 @@ router.get '/user/tasks', auth, (req, res) -> res.json 200, tasks -router.get '/users/:uid/calendar.ics', (req, res) -> - #return next() #disable for now - {uid} = req.params - {apiToken} = req.query - - model = req.getModel() - query = model.query('users').withIdAndToken(uid, apiToken) - query.fetch (err, result) -> - return res.send(400, err) if err - tasks = result.get('tasks') - # tasks = result[0].tasks - tasksWithDates = _.filter tasks, (task) -> !!task.date - return res.send(400, "No events found") if _.isEmpty(tasksWithDates) - - ical = new icalendar.iCalendar() - ical.addProperty('NAME', 'HabitRPG') - _.each tasksWithDates, (task) -> - event = new icalendar.VEvent(task.id); - event.setSummary(task.text); - d = new Date(task.date) - d.date_only = true - event.setDate d - ical.addComponent event - res.type('text/calendar') - formattedIcal = ical.toString().replace(/DTSTART\:/g, 'DTSTART;VALUE=DATE:') - res.send(200, formattedIcal) - module.exports = router From 4b77daff0a80fd6738bb11a79c1f671da9483378 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 3 Mar 2013 12:36:05 -0500 Subject: [PATCH 058/220] redirect deprecated task-scoring to new API, some cleanup to score function to use switz auth --- src/server/api.coffee | 39 +++++++++++++++++++++++++++++- src/server/deprecated.coffee | 47 ++++++------------------------------ 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index a7ff84d219..7ab6d75e07 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -6,7 +6,6 @@ _ = require 'underscore' validator = require 'derby-auth/node_modules/validator' check = validator.check sanitize = validator.sanitize -icalendar = require 'icalendar' NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request" NO_USER_FOUND = err: "No user found." @@ -114,4 +113,42 @@ router.get '/user/tasks', auth, (req, res) -> res.json 200, tasks +### + This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login +### +scoreTask = (req, res, next) -> + auth req, res, -> + {taskId, direction} = req.params + {title, service, icon} = req.body + + # Send error responses for improper API call + return res.send(500, ':taskId required') unless taskId + return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] + + model = req.getModel() + {user, userObj} = req + + model.ref('_user', user) + + # Create task if doesn't exist + # TODO add service & icon to task + unless model.get("_user.tasks.#{taskId}") + model.refList "_habitList", "_user.tasks", "_user.habitIds" + model.at('_habitList').push + id: taskId + type: 'habit' + text: (title || taskId) + value: 0 + up: true + down: true + notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." + + delta = scoring.score(model, taskId, direction) + result = model.get ('_user.stats') + result.delta = delta + res.send(result) +router.post '/user/tasks/:taskId/:direction', scoreTask + module.exports = router +module.exports.scoreTask = scoreTask # export so deprecated can call it + diff --git a/src/server/deprecated.coffee b/src/server/deprecated.coffee index c781433aca..a1c1ff817c 100644 --- a/src/server/deprecated.coffee +++ b/src/server/deprecated.coffee @@ -4,6 +4,7 @@ router = new express.Router() scoring = require '../app/scoring' _ = require 'underscore' icalendar = require('icalendar') +api = require './api' # ---------- Deprecated Paths ------------ @@ -13,45 +14,11 @@ router.get '/:uid/up/:score?', (req, res) -> res.send(500, deprecatedMessage) router.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage) router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage) -router.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> - {uid, taskId, direction} = req.params - {apiToken, title, service, icon} = req.body - console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development' - - # Send error responses for improper API call - return res.send(500, 'request body "apiToken" required') unless apiToken - return res.send(500, ':uid required') unless uid - return res.send(500, ':taskId required') unless taskId - return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] - - model = req.getModel() - model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) -> - return res.send(500, err) if err - user = result - userObj = user.get() - if _.isEmpty(userObj) - return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id") - - model.ref('_user', user) - req._isServer = true - - # Create task if doesn't exist - # TODO add service & icon to task - unless model.get("_user.tasks.#{taskId}") - model.refList "_habitList", "_user.tasks", "_user.habitIds" - model.at('_habitList').push - id: taskId - type: 'habit' - text: (title || taskId) - value: 0 - up: true - down: true - notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." - - delta = scoring.score(model, taskId, direction) - result = model.get ('_user.stats') - result.delta = delta - res.send(result) +# Redirect to new API +router.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res, next) -> + req.headers['x-api-user'] = req.params.uid + req.headers['x-api-key'] = req.body.apiToken + api.scoreTask(req, res, next) router.get '/v1/users/:uid/calendar.ics', (req, res) -> #return next() #disable for now @@ -80,4 +47,4 @@ router.get '/v1/users/:uid/calendar.ics', (req, res) -> formattedIcal = ical.toString().replace(/DTSTART\:/g, 'DTSTART;VALUE=DATE:') res.send(200, formattedIcal) -module.exports = router +module.exports = router \ No newline at end of file From 6189fee74e48034e5852fda101212a815efe0d28 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 3 Mar 2013 16:14:16 -0500 Subject: [PATCH 059/220] algorightms notification (to warn people to restore their exp) --- src/app/character.coffee | 4 ++-- views/app/alerts.html | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app/character.coffee b/src/app/character.coffee index d9c3208c22..19a64d0fd3 100644 --- a/src/app/character.coffee +++ b/src/app/character.coffee @@ -57,8 +57,8 @@ module.exports.app = (appExports, model) -> batch.commit() browser.resetDom(model) - appExports.closeCelebrationNofitication = (e, el) -> - user.set('flags.celebrationEvent', 'hide') + appExports.closeAlgosNotification = (e, el) -> + user.set('flags.algosNotification', 'hide') appExports.customizeGender = (e, el) -> user.set 'preferences.gender', $(el).attr('data-value') diff --git a/views/app/alerts.html b/views/app/alerts.html index bf10bd2bf0..bb91206ced 100644 --- a/views/app/alerts.html +++ b/views/app/alerts.html @@ -3,4 +3,12 @@
      {#each _flash.error as :error}
    • {:error}
    • {/}
    + {/} + + {#if equal(_user.flags.algosNotification,'show')} +
    + [x] +

    New features! Custom day start, new and better algorithms, authentication enhancements. See details here. +  "Hey, I had more Exp before this roll-out!" Restore your Exp here.

    +
    {/} \ No newline at end of file From 1a335cc68a88c58f90c0395c5e329e468620aa8c Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 3 Mar 2013 17:54:14 -0500 Subject: [PATCH 060/220] add priority-multiplier to html & tasks.coffee (note, requires merge of @IvanGaravito /derby#hotfix-model_at_for_each_blocks --- src/app/tasks.coffee | 9 +++++++++ views/app/tasks.html | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index 50a7fa976b..02ee38901f 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -177,3 +177,12 @@ module.exports.app = (appExports, model) -> # When they update their notes, re-establish tooltip & popover $('[rel=tooltip]').tooltip() $('[rel=popover]').popover() + + appExports.tasksToggleAdvanced = (e, el) -> + $(el).next('.advanced').toggle() + + appExports.tasksSetMultiplier = (e, el) -> + dataId = $(el).parent('[data-id]').attr('data-id') + #"_user.tasks.#{dataId}" + model.at(e.target).set 'multiplier', parseInt($(el).attr('data-multiplier')) + debugger \ No newline at end of file diff --git a/views/app/tasks.html b/views/app/tasks.html index 4d11b210b5..2b881e8a71 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -207,6 +207,20 @@
    Enter as date, eg 02/19/2013 or 02-19-2013
    {/} + + {#unless equal(:task.type, 'reward')} +
    +
    + Advanced +
    + + + + +
    +
    +
    + {/} From c33c6c34807553243cedbc8778f0c9c59ffb2ed4 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 3 Mar 2013 18:18:49 -0500 Subject: [PATCH 061/220] add multiplier functionality to task scoring --- src/app/algos.coffee | 24 ++++++++++++------------ src/app/scoring.coffee | 5 +++-- views/app/tasks.html | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/app/algos.coffee b/src/app/algos.coffee index 205842d178..8ff90414ca 100644 --- a/src/app/algos.coffee +++ b/src/app/algos.coffee @@ -10,11 +10,11 @@ module.exports.tnl = (level) -> {weaponStrength) weapon strength {level} current user level ### -module.exports.expModifier = (value, weaponStrength, level) -> - levelModifier = (level-1) * MODIFIER - weaponModifier = weaponStrength / 100 - strength = 1 + weaponModifier + levelModifier - return value * strength +module.exports.expModifier = (value, weaponStrength, level, multiplier=1) -> + levelModifier = (level-1) * MODIFIER + weaponModifier = weaponStrength / 100 + strength = 1 + weaponModifier + levelModifier + return value * strength * multiplier ### Calculates HP modification based on level and armor defence @@ -23,17 +23,17 @@ module.exports.expModifier = (value, weaponStrength, level) -> {helmDefense} defense from helm {level} current user level ### -module.exports.hpModifier = (value, armorDefense, helmDefense, shieldDefense, level) -> - levelModifier = (level-1) * MODIFIER - armorModifier = (armorDefense + helmDefense + shieldDefense) / 100 - defense = 1 - levelModifier + armorModifier - return value * defense +module.exports.hpModifier = (value, armorDefense, helmDefense, shieldDefense, level, multiplier=1) -> + levelModifier = (level-1) * MODIFIER + armorModifier = (armorDefense + helmDefense + shieldDefense) / 100 + defense = 1 - levelModifier + armorModifier + return value * defense * multiplier ### Future use ### -module.exports.gpModifier = (value, modifier) -> - return value * modifier +module.exports.gpModifier = (value, modifier, multiplier=1) -> + return value * modifier * multiplier ### Calculates the next task.value based on direction diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee index f9f13a909c..f95d12c7f3 100644 --- a/src/app/scoring.coffee +++ b/src/app/scoring.coffee @@ -28,6 +28,7 @@ score = (model, taskId, direction, times, batch, cron) -> taskPath = "tasks.#{taskId}" taskObj = obj.tasks[taskId] {type, value} = taskObj + multiplier = if taskObj.multiplier then parseInt(taskObj.multiplier) else 1 # If they're trying to purhcase a too-expensive reward, confirm they want to take a hit for it if taskObj.value > obj.stats.gp and taskObj.type is 'reward' @@ -51,7 +52,7 @@ score = (model, taskId, direction, times, batch, cron) -> addPoints = -> level = user.get('stats.lvl') weaponStrength = items.items.weapon[user.get('items.weapon')].strength - modified = algos.expModifier(delta,weaponStrength,level) + modified = algos.expModifier(delta,weaponStrength,level, multiplier) exp += modified*10 gp += delta @@ -60,7 +61,7 @@ score = (model, taskId, direction, times, batch, cron) -> armorDefense = items.items.armor[user.get('items.armor')].defense helmDefense = items.items.head[user.get('items.head')].defense shieldDefense = items.items.shield[user.get('items.shield')].defense - modified = algos.hpModifier(delta,armorDefense,helmDefense,shieldDefense,level) + modified = algos.hpModifier(delta,armorDefense,helmDefense,shieldDefense,level, multiplier) hp += modified switch type diff --git a/views/app/tasks.html b/views/app/tasks.html index 2b881e8a71..b3b6b7e88e 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -214,7 +214,7 @@ Advanced
    - +
    From 120e058346e8e533763bcccb95bea2f26305f554 Mon Sep 17 00:00:00 2001 From: Philip How Date: Mon, 4 Mar 2013 00:18:04 +0000 Subject: [PATCH 062/220] onlies now reset their value every day --- src/app/scoring.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee index f9f13a909c..4726d7baaf 100644 --- a/src/app/scoring.coffee +++ b/src/app/scoring.coffee @@ -216,6 +216,9 @@ cron = (model) -> value = obj.tasks[taskObj.id].value #get updated value absVal = if (completed) then Math.abs(value) else value todoTally += absVal + else if type is 'habit' #reset 'onlies' value to 0 + if taskObj.up==false or taskObj.down==false + batch.set "tasks.#{taskObj.id}.value", 0 # Finished tallying obj.history ?= {}; obj.history.todos ?= []; obj.history.exp ?= [] From 4919cd22b55e4c0c0354f7d47f0f4b64ec75d316 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Sun, 3 Mar 2013 19:30:53 -0500 Subject: [PATCH 063/220] Move deprecated API back to deprecated --- src/server/api.coffee | 38 +----------------------------------- src/server/deprecated.coffee | 35 +++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 7ab6d75e07..e27a28fc4e 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -113,42 +113,6 @@ router.get '/user/tasks', auth, (req, res) -> res.json 200, tasks -### - This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login -### -scoreTask = (req, res, next) -> - auth req, res, -> - {taskId, direction} = req.params - {title, service, icon} = req.body - - # Send error responses for improper API call - return res.send(500, ':taskId required') unless taskId - return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] - - model = req.getModel() - {user, userObj} = req - - model.ref('_user', user) - - # Create task if doesn't exist - # TODO add service & icon to task - unless model.get("_user.tasks.#{taskId}") - model.refList "_habitList", "_user.tasks", "_user.habitIds" - model.at('_habitList').push - id: taskId - type: 'habit' - text: (title || taskId) - value: 0 - up: true - down: true - notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." - - delta = scoring.score(model, taskId, direction) - result = model.get ('_user.stats') - result.delta = delta - res.send(result) -router.post '/user/tasks/:taskId/:direction', scoreTask - module.exports = router -module.exports.scoreTask = scoreTask # export so deprecated can call it +module.exports.auth = auth # export so deprecated can call it diff --git a/src/server/deprecated.coffee b/src/server/deprecated.coffee index a1c1ff817c..e97d7e6710 100644 --- a/src/server/deprecated.coffee +++ b/src/server/deprecated.coffee @@ -15,10 +15,41 @@ router.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage) router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage) # Redirect to new API -router.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res, next) -> +initDeprecated = (req, res, next) -> req.headers['x-api-user'] = req.params.uid req.headers['x-api-key'] = req.body.apiToken - api.scoreTask(req, res, next) + next() + +router.post '/v1/users/:uid/tasks/:taskId/:direction', initDeprecated, api.auth, (req, res) -> + {taskId, direction} = req.params + {title, service, icon} = req.body + + # Send error responses for improper API call + return res.send(500, ':taskId required') unless taskId + return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] + + model = req.getModel() + {user, userObj} = req + + model.ref('_user', user) + + # Create task if doesn't exist + # TODO add service & icon to task + unless model.get("_user.tasks.#{taskId}") + model.refList "_habitList", "_user.tasks", "_user.habitIds" + model.at('_habitList').push + id: taskId + type: 'habit' + text: (title || taskId) + value: 0 + up: true + down: true + notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." + + delta = scoring.score(model, taskId, direction) + result = model.get ('_user.stats') + result.delta = delta + res.send(result) router.get '/v1/users/:uid/calendar.ics', (req, res) -> #return next() #disable for now From 30a3e46f20b576000c77611a9f80e987e004bd17 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Sun, 3 Mar 2013 19:37:54 -0500 Subject: [PATCH 064/220] Move deprecated api back into new --- src/server/api.coffee | 41 ++++++++++++++++++++++++++++++++++-- src/server/deprecated.coffee | 33 ++--------------------------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index e27a28fc4e..8927f6172b 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -113,6 +113,43 @@ router.get '/user/tasks', auth, (req, res) -> res.json 200, tasks -module.exports = router -module.exports.auth = auth # export so deprecated can call it +### + This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login +### +scoreTask = (req, res, next) -> + {taskId, direction} = req.params + {title, service, icon} = req.body + + # Send error responses for improper API call + return res.send(500, ':taskId required') unless taskId + return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] + + model = req.getModel() + {user, userObj} = req + + model.ref('_user', user) + + # Create task if doesn't exist + # TODO add service & icon to task + unless model.get("_user.tasks.#{taskId}") + model.refList "_habitList", "_user.tasks", "_user.habitIds" + model.at('_habitList').push + id: taskId + type: 'habit' + text: (title || taskId) + value: 0 + up: true + down: true + notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." + + delta = scoring.score(model, taskId, direction) + result = model.get ('_user.stats') + result.delta = delta + res.send(result) + +router.post '/user/tasks/:taskId/:direction', auth, scoreTask + +module.exports = router +module.exports.auth = auth +module.exports.scoreTask = scoreTask # export so deprecated can call it diff --git a/src/server/deprecated.coffee b/src/server/deprecated.coffee index e97d7e6710..3fcef2c8a3 100644 --- a/src/server/deprecated.coffee +++ b/src/server/deprecated.coffee @@ -20,36 +20,7 @@ initDeprecated = (req, res, next) -> req.headers['x-api-key'] = req.body.apiToken next() -router.post '/v1/users/:uid/tasks/:taskId/:direction', initDeprecated, api.auth, (req, res) -> - {taskId, direction} = req.params - {title, service, icon} = req.body - - # Send error responses for improper API call - return res.send(500, ':taskId required') unless taskId - return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] - - model = req.getModel() - {user, userObj} = req - - model.ref('_user', user) - - # Create task if doesn't exist - # TODO add service & icon to task - unless model.get("_user.tasks.#{taskId}") - model.refList "_habitList", "_user.tasks", "_user.habitIds" - model.at('_habitList').push - id: taskId - type: 'habit' - text: (title || taskId) - value: 0 - up: true - down: true - notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." - - delta = scoring.score(model, taskId, direction) - result = model.get ('_user.stats') - result.delta = delta - res.send(result) +router.post '/v1/users/:uid/tasks/:taskId/:direction', initDeprecated, api.auth, api.scoreTask router.get '/v1/users/:uid/calendar.ics', (req, res) -> #return next() #disable for now @@ -78,4 +49,4 @@ router.get '/v1/users/:uid/calendar.ics', (req, res) -> formattedIcal = ical.toString().replace(/DTSTART\:/g, 'DTSTART;VALUE=DATE:') res.send(200, formattedIcal) -module.exports = router \ No newline at end of file +module.exports = router From 35cea7fcbb086556e26db6e5ab19b719d9478ac2 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 3 Mar 2013 20:59:14 -0500 Subject: [PATCH 065/220] notification on "onlies" mechanic --- src/app/character.coffee | 3 +++ views/app/alerts.html | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/app/character.coffee b/src/app/character.coffee index 19a64d0fd3..3acbaba2b3 100644 --- a/src/app/character.coffee +++ b/src/app/character.coffee @@ -60,6 +60,9 @@ module.exports.app = (appExports, model) -> appExports.closeAlgosNotification = (e, el) -> user.set('flags.algosNotification', 'hide') + appExports.closeOnliesNotification = (e, el) -> + user.set('flags.onliesNotification', 'hide') + appExports.customizeGender = (e, el) -> user.set 'preferences.gender', $(el).attr('data-value') diff --git a/views/app/alerts.html b/views/app/alerts.html index bb91206ced..a230f65abd 100644 --- a/views/app/alerts.html +++ b/views/app/alerts.html @@ -7,8 +7,20 @@ {#if equal(_user.flags.algosNotification,'show')}
    - [x] -

    New features! Custom day start, new and better algorithms, authentication enhancements. See details here. + [x] +

    New features! Custom day start, new and better algorithms, authentication enhancements. See details here.  "Hey, I had more Exp before this roll-out!" Restore your Exp here.

    + {/} + + {#if equal(_user.flags.onliesNotification, 'show')} +
    + [x] +

    + "Onlies" now gain / lose value just like other habits, and are reset back to 0 at the beginning of each day. +  See details here, and +  chime in with your recommendations on this mechanic. +

    +
    + {/} \ No newline at end of file From 7f03a30fd59f3c42be4b0bad2f0a40e36c4ffe53 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 3 Mar 2013 21:38:21 -0500 Subject: [PATCH 066/220] remove "advanced" section, clean up the task-edit form a bit --- src/app/tasks.coffee | 6 +---- views/app/tasks.html | 58 +++++++++++++++++++++++--------------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index 02ee38901f..32afa0699f 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -178,11 +178,7 @@ module.exports.app = (appExports, model) -> $('[rel=tooltip]').tooltip() $('[rel=popover]').popover() - appExports.tasksToggleAdvanced = (e, el) -> - $(el).next('.advanced').toggle() - appExports.tasksSetMultiplier = (e, el) -> dataId = $(el).parent('[data-id]').attr('data-id') #"_user.tasks.#{dataId}" - model.at(e.target).set 'multiplier', parseInt($(el).attr('data-multiplier')) - debugger \ No newline at end of file + model.at(e.target).set 'multiplier', parseInt($(el).attr('data-multiplier')) \ No newline at end of file diff --git a/views/app/tasks.html b/views/app/tasks.html index b3b6b7e88e..112dfc4aaa 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -176,31 +176,36 @@ {#if equal(:task.type, 'habit')} -
    - - -
    +
    + +
    + + +
    {else if equal(:task.type, 'daily')} - -
    - - - - - - - - -
    +
    + +
    + + + + + + + + +
    {else if equal(:task.type, 'reward')} -
    -
    {else if equal(:task.type, 'todo')} +
    @@ -209,18 +214,17 @@ {/} {#unless equal(:task.type, 'reward')} +
    -
    - Advanced -
    - - - - -
    +
    + + + +
    {/} +
    From d925c43c7beffb4a9d5ae6a480fc6d1e0941a45e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 3 Mar 2013 22:01:59 -0500 Subject: [PATCH 067/220] multipy GP with priority-multiplier, too --- src/app/algos.coffee | 3 +++ src/app/scoring.coffee | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/algos.coffee b/src/app/algos.coffee index 8ff90414ca..fb366c313a 100644 --- a/src/app/algos.coffee +++ b/src/app/algos.coffee @@ -9,6 +9,7 @@ module.exports.tnl = (level) -> {value} task.value for exp gain {weaponStrength) weapon strength {level} current user level + {multiplier} user-defined priority multiplier ### module.exports.expModifier = (value, weaponStrength, level, multiplier=1) -> levelModifier = (level-1) * MODIFIER @@ -22,6 +23,7 @@ module.exports.expModifier = (value, weaponStrength, level, multiplier=1) -> {armorDefense} defense from armor {helmDefense} defense from helm {level} current user level + {multiplier} user-defined priority multiplier ### module.exports.hpModifier = (value, armorDefense, helmDefense, shieldDefense, level, multiplier=1) -> levelModifier = (level-1) * MODIFIER @@ -31,6 +33,7 @@ module.exports.hpModifier = (value, armorDefense, helmDefense, shieldDefense, le ### Future use + {multiplier} user-defined priority multiplier ### module.exports.gpModifier = (value, modifier, multiplier=1) -> return value * modifier * multiplier diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee index 4ae7dfd955..a728b2a640 100644 --- a/src/app/scoring.coffee +++ b/src/app/scoring.coffee @@ -54,7 +54,7 @@ score = (model, taskId, direction, times, batch, cron) -> weaponStrength = items.items.weapon[user.get('items.weapon')].strength modified = algos.expModifier(delta,weaponStrength,level, multiplier) exp += modified*10 - gp += delta + gp += algos.gpModifier(delta, 1, multiplier) subtractPoints = -> level = user.get('stats.lvl') From 1bd067f32f91b119c8ce9ff414e058e67f4d01ae Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 3 Mar 2013 22:06:34 -0500 Subject: [PATCH 068/220] add multiplier "new feature" alert --- src/app/character.coffee | 3 +++ views/app/alerts.html | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/src/app/character.coffee b/src/app/character.coffee index 3acbaba2b3..25c8ccd87c 100644 --- a/src/app/character.coffee +++ b/src/app/character.coffee @@ -63,6 +63,9 @@ module.exports.app = (appExports, model) -> appExports.closeOnliesNotification = (e, el) -> user.set('flags.onliesNotification', 'hide') + appExports.closeMultiplierNotification = (e, el) -> + user.set('flags.multiplierNotification', 'hide') + appExports.customizeGender = (e, el) -> user.set 'preferences.gender', $(el).attr('data-value') diff --git a/views/app/alerts.html b/views/app/alerts.html index a230f65abd..6e5769101a 100644 --- a/views/app/alerts.html +++ b/views/app/alerts.html @@ -22,5 +22,11 @@  chime in with your recommendations on this mechanic.

    + {/} + {#if equal(_user.flags.multiplierNotification, 'show')} +
    + [x] +

    New Feature: Priority Multiplier! You can now multiply "more important" tasks on a 1x, 2x, or 3x scale. See details.

    +
    {/} \ No newline at end of file From 233f8ea5950df5339b6ba641da7216031ee8a078 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 4 Mar 2013 00:41:59 -0500 Subject: [PATCH 069/220] data-target on modal instead of hrefd --- views/app/alerts.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/app/alerts.html b/views/app/alerts.html index a230f65abd..8032ca8373 100644 --- a/views/app/alerts.html +++ b/views/app/alerts.html @@ -9,7 +9,7 @@
    [x]

    New features! Custom day start, new and better algorithms, authentication enhancements. See details here. -  "Hey, I had more Exp before this roll-out!" Restore your Exp here.

    +  "Hey, I had more Exp before this roll-out!" Restore your Exp here.

    {/} From bd19e09fcf51a8d3b42a0270a36cc73a81a2035a Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 4 Mar 2013 11:17:17 -0500 Subject: [PATCH 070/220] possible fix for #324 - shuffle some function placement to be sure definition precedes function call. put re-rendering functionality (setupSortable, setupTour, etc) into app.on('render') --- src/app/browser.coffee | 43 +++++++++++++++++++++--------------------- src/app/index.coffee | 5 +++-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/app/browser.coffee b/src/app/browser.coffee index 25872b0ee1..7c5e47f59f 100644 --- a/src/app/browser.coffee +++ b/src/app/browser.coffee @@ -3,7 +3,7 @@ moment = require 'moment' #algos = require './algos' -module.exports.restoreRefs = restoreRefs = (model) -> +restoreRefs = module.exports.restoreRefs = (model) -> # tnl function model.fn '_tnl', '_user.stats.lvl', (lvl) -> # see https://github.com/lefnire/habitrpg/issues/4 @@ -15,27 +15,6 @@ module.exports.restoreRefs = restoreRefs = (model) -> _.each ['habit', 'daily', 'todo', 'reward'], (type) -> model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" -module.exports.resetDom = (model) -> - window.DERBY.app.dom.clear() - restoreRefs(model) - window.DERBY.app.view.render(model) - reconstructPage model - -module.exports.app = (appExports, model) -> - reconstructPage model - setupGrowlNotifications(model) unless model.get('_view.mobileDevice') - -reconstructPage = (model) -> - loadJavaScripts(model) - setupSortable(model) - setupTooltips(model) - setupTour(model) - $('.datepicker').datepicker({autoclose:true, todayBtn:true}) - .on 'changeDate', (ev) -> - #for some reason selecting a date doesn't fire a change event on the field, meaning our changes aren't saved - #FIXME also, it saves as a day behind?? - model.at(ev.target).set 'date', moment(ev.date).add('d',1).format('MM/DD/YYYY') - ### Loads JavaScript files from (1) public/js/* and (2) external sources We use this file (instead of or inside .html) so we can utilize require() to concatinate for @@ -218,3 +197,23 @@ setupGrowlNotifications = (model) -> statsNotification ' You died!', 'death' else statsNotification ' Level Up!', 'lvl' + + +module.exports.resetDom = (model) -> + window.DERBY.app.dom.clear() + window.DERBY.app.view.render(model) + +module.exports.app = (appExports, model, app) -> + loadJavaScripts(model) + + app.on 'render', (ctx) -> + #restoreRefs(model) + setupSortable(model) + setupTooltips(model) + setupGrowlNotifications(model) unless model.get('_view.mobileDevice') + setupTour(model) + $('.datepicker').datepicker({autoclose:true, todayBtn:true}) + .on 'changeDate', (ev) -> + #for some reason selecting a date doesn't fire a change event on the field, meaning our changes aren't saved + #FIXME also, it saves as a day behind?? + model.at(ev.target).set 'date', moment(ev.date).add('d',1).format('MM/DD/YYYY') \ No newline at end of file diff --git a/src/app/index.coffee b/src/app/index.coffee index 73a33d3c9b..eabdfbde0e 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -1,5 +1,6 @@ derby = require 'derby' -{get, view, ready} = derby.createApp module +app = derby.createApp module +{get, view, ready} = app derby.use require('derby-ui-boot'), {styles: ['bootstrap', 'responsive']} derby.use require '../../ui' derby.use require 'derby-auth/components' @@ -59,5 +60,5 @@ ready (model) -> profile.app(exports, model) require('../server/private').app(exports, model) require('./debug').app(exports, model) if model.get('_view.nodeEnv') != 'production' - browser.app(exports, model) + browser.app(exports, model, app) From 9517455a798f14df4ec2c99138fa9675701a71dd Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 4 Mar 2013 11:33:36 -0500 Subject: [PATCH 071/220] migrate "multiplier" setup to "priority" setup, with ! !! !!! instead of 1 2 3. multipliers are now 1x 1.5x 2x --- src/app/algos.coffee | 25 ++++++++++++++++--------- src/app/character.coffee | 4 ++-- src/app/scoring.coffee | 8 ++++---- src/app/tasks.coffee | 4 ++-- views/app/alerts.html | 6 +++--- views/app/tasks.html | 8 ++++---- 6 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/app/algos.coffee b/src/app/algos.coffee index fb366c313a..563ae7e5b0 100644 --- a/src/app/algos.coffee +++ b/src/app/algos.coffee @@ -1,6 +1,13 @@ MODIFIER = .02 +priorityValue = (priority='!') -> + switch priority + when '!' then 1 + when '!!' then 1.5 + when '!!!' then 2 + else 1 + module.exports.tnl = (level) -> return (Math.pow(level,2)*10)+(level*10)+80 @@ -9,13 +16,13 @@ module.exports.tnl = (level) -> {value} task.value for exp gain {weaponStrength) weapon strength {level} current user level - {multiplier} user-defined priority multiplier + {priority} user-defined priority multiplier ### -module.exports.expModifier = (value, weaponStrength, level, multiplier=1) -> +module.exports.expModifier = (value, weaponStrength, level, priority='!') -> levelModifier = (level-1) * MODIFIER weaponModifier = weaponStrength / 100 strength = 1 + weaponModifier + levelModifier - return value * strength * multiplier + return value * strength * priorityValue(priority) ### Calculates HP modification based on level and armor defence @@ -23,20 +30,20 @@ module.exports.expModifier = (value, weaponStrength, level, multiplier=1) -> {armorDefense} defense from armor {helmDefense} defense from helm {level} current user level - {multiplier} user-defined priority multiplier + {priority} user-defined priority multiplier ### -module.exports.hpModifier = (value, armorDefense, helmDefense, shieldDefense, level, multiplier=1) -> +module.exports.hpModifier = (value, armorDefense, helmDefense, shieldDefense, level, priority='!') -> levelModifier = (level-1) * MODIFIER armorModifier = (armorDefense + helmDefense + shieldDefense) / 100 defense = 1 - levelModifier + armorModifier - return value * defense * multiplier + return value * defense * priorityValue(priority) ### Future use - {multiplier} user-defined priority multiplier + {priority} user-defined priority multiplier ### -module.exports.gpModifier = (value, modifier, multiplier=1) -> - return value * modifier * multiplier +module.exports.gpModifier = (value, modifier, priority='!') -> + return value * modifier * priorityValue(priority) ### Calculates the next task.value based on direction diff --git a/src/app/character.coffee b/src/app/character.coffee index 25c8ccd87c..5e98588f94 100644 --- a/src/app/character.coffee +++ b/src/app/character.coffee @@ -63,8 +63,8 @@ module.exports.app = (appExports, model) -> appExports.closeOnliesNotification = (e, el) -> user.set('flags.onliesNotification', 'hide') - appExports.closeMultiplierNotification = (e, el) -> - user.set('flags.multiplierNotification', 'hide') + appExports.closePriorityNotification = (e, el) -> + user.set('flags.priorityNotification', 'hide') appExports.customizeGender = (e, el) -> user.set 'preferences.gender', $(el).attr('data-value') diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee index a728b2a640..e82f77e466 100644 --- a/src/app/scoring.coffee +++ b/src/app/scoring.coffee @@ -28,7 +28,7 @@ score = (model, taskId, direction, times, batch, cron) -> taskPath = "tasks.#{taskId}" taskObj = obj.tasks[taskId] {type, value} = taskObj - multiplier = if taskObj.multiplier then parseInt(taskObj.multiplier) else 1 + priority = taskObj.priority or '!' # If they're trying to purhcase a too-expensive reward, confirm they want to take a hit for it if taskObj.value > obj.stats.gp and taskObj.type is 'reward' @@ -52,16 +52,16 @@ score = (model, taskId, direction, times, batch, cron) -> addPoints = -> level = user.get('stats.lvl') weaponStrength = items.items.weapon[user.get('items.weapon')].strength - modified = algos.expModifier(delta,weaponStrength,level, multiplier) + modified = algos.expModifier(delta,weaponStrength,level, priority) exp += modified*10 - gp += algos.gpModifier(delta, 1, multiplier) + gp += algos.gpModifier(delta, 1, priority) subtractPoints = -> level = user.get('stats.lvl') armorDefense = items.items.armor[user.get('items.armor')].defense helmDefense = items.items.head[user.get('items.head')].defense shieldDefense = items.items.shield[user.get('items.shield')].defense - modified = algos.hpModifier(delta,armorDefense,helmDefense,shieldDefense,level, multiplier) + modified = algos.hpModifier(delta,armorDefense,helmDefense,shieldDefense,level, priority) hp += modified switch type diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index 32afa0699f..94d142ad03 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -178,7 +178,7 @@ module.exports.app = (appExports, model) -> $('[rel=tooltip]').tooltip() $('[rel=popover]').popover() - appExports.tasksSetMultiplier = (e, el) -> + appExports.tasksSetPriority = (e, el) -> dataId = $(el).parent('[data-id]').attr('data-id') #"_user.tasks.#{dataId}" - model.at(e.target).set 'multiplier', parseInt($(el).attr('data-multiplier')) \ No newline at end of file + model.at(e.target).set 'priority', $(el).attr('data-priority') \ No newline at end of file diff --git a/views/app/alerts.html b/views/app/alerts.html index 6e5769101a..5c96173b0a 100644 --- a/views/app/alerts.html +++ b/views/app/alerts.html @@ -24,9 +24,9 @@ {/} - {#if equal(_user.flags.multiplierNotification, 'show')} + {#if equal(_user.flags.priorityNotification, 'show')}
    - [x] -

    New Feature: Priority Multiplier! You can now multiply "more important" tasks on a 1x, 2x, or 3x scale. See details.

    + [x] +

    New Feature: Priority Multiplier! You can now multiply "more important" tasks on a 1x, 1.5x, or 2x scale. See details.

    {/} \ No newline at end of file diff --git a/views/app/tasks.html b/views/app/tasks.html index 112dfc4aaa..ccdd9bf407 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -217,10 +217,10 @@
    - - - - + + + +
    {/} From 378a03b6a09ddd1079a50e40e6f6e76040ad38d7 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 4 Mar 2013 12:47:33 -0500 Subject: [PATCH 072/220] add bootstrap tour as a submodule, some cleanup --- .gitmodules | 3 + public/vendor/bootstrap-tour | 1 + public/vendor/bootstrap-tour.js | 271 -------------------------------- src/app/browser.coffee | 10 +- 4 files changed, 7 insertions(+), 278 deletions(-) create mode 160000 public/vendor/bootstrap-tour delete mode 100644 public/vendor/bootstrap-tour.js diff --git a/.gitmodules b/.gitmodules index 5d6f25d3b3..d0d9ca48c5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "public/vendor/BrowserQuest"] path = public/vendor/BrowserQuest url = https://github.com/mozilla/BrowserQuest.git +[submodule "public/vendor/bootstrap-tour"] + path = public/vendor/bootstrap-tour + url = git://github.com/sorich87/bootstrap-tour.git diff --git a/public/vendor/bootstrap-tour b/public/vendor/bootstrap-tour new file mode 160000 index 0000000000..4f94fa056c --- /dev/null +++ b/public/vendor/bootstrap-tour @@ -0,0 +1 @@ +Subproject commit 4f94fa056c88c6099dea135b644d8f95d38ac9e1 diff --git a/public/vendor/bootstrap-tour.js b/public/vendor/bootstrap-tour.js deleted file mode 100644 index 3ed1b2bbf9..0000000000 --- a/public/vendor/bootstrap-tour.js +++ /dev/null @@ -1,271 +0,0 @@ - -/* ============================================================ - # bootstrap-tour.js v0.1 - # http://pushly.github.com/bootstrap-tour/ - # ============================================================== - # Copyright 2012 Push.ly - # - # Licensed under the Apache License, Version 2.0 (the "License"); - # you may not use this file except in compliance with the License. - # You may obtain a copy of the License at - # - # http://www.apache.org/licenses/LICENSE-2.0 - # - # Unless required by applicable law or agreed to in writing, software - # distributed under the License is distributed on an "AS IS" BASIS, - # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - # See the License for the specific language governing permissions and - # limitations under the License. - */ - -(function() { - - (function($, window) { - var Tour, document; - document = window.document; - Tour = (function() { - - function Tour(options) { - var _this = this; - this._options = $.extend({ - name: 'tour', - labels: { - end: 'End tour', - next: 'Next »', - prev: '« Prev' - }, - keyboard: true, - afterSetState: function(key, value) {}, - afterGetState: function(key, value) {}, - onShow: function(tour) {}, - onHide: function(tour) {}, - onShown: function(tour) {} - }, options); - this._steps = []; - this.setCurrentStep(); - this._onresize(function() { - if (!_this.ended) return _this.showStep(_this._current); - }); - } - - Tour.prototype.setState = function(key, value) { - $.cookie("" + this._options.name + "_" + key, value, { - expires: 36500, - path: '/' - }); - return this._options.afterSetState(key, value); - }; - - Tour.prototype.getState = function(key) { - var value; - value = $.cookie("" + this._options.name + "_" + key); - this._options.afterGetState(key, value); - return value; - }; - - Tour.prototype.addStep = function(step) { - return this._steps.push(step); - }; - - Tour.prototype.getStep = function(i) { - if (this._steps[i] != null) { - return $.extend({ - path: "", - placement: "right", - title: "", - content: "", - next: i === this._steps.length - 1 ? -1 : i + 1, - prev: i - 1, - animation: true, - onShow: this._options.onShow, - onHide: this._options.onHide, - onShown: this._options.onShown - }, this._steps[i]); - } - }; - - Tour.prototype.start = function(force) { - var _this = this; - if (force == null) force = false; - if (this.ended() && !force) return; - $(document).off("click.bootstrap-tour", ".popover .next").on("click.bootstrap-tour", ".popover .next", function(e) { - e.preventDefault(); - return _this.next(); - }); - $(document).off("click.bootstrap-tour", ".popover .prev").on("click.bootstrap-tour", ".popover .prev", function(e) { - e.preventDefault(); - return _this.prev(); - }); - $(document).off("click.bootstrap-tour", ".popover .end").on("click.bootstrap-tour", ".popover .end", function(e) { - e.preventDefault(); - return _this.end(); - }); - this._setupKeyboardNavigation(); - return this.showStep(this._current); - }; - - Tour.prototype.next = function() { - this.hideStep(this._current); - return this.showNextStep(); - }; - - Tour.prototype.prev = function() { - this.hideStep(this._current); - return this.showPrevStep(); - }; - - Tour.prototype.end = function() { - this.hideStep(this._current); - $(document).off(".bootstrap-tour"); - return this.setState("end", "yes"); - }; - - Tour.prototype.ended = function() { - return !!this.getState("end"); - }; - - Tour.prototype.restart = function() { - this.setState("current_step", null); - this.setState("end", null); - this.setCurrentStep(0); - return this.start(); - }; - - Tour.prototype.hideStep = function(i) { - var step; - step = this.getStep(i); - if (step.onHide != null) step.onHide(this); - return $(step.element).popover("hide"); - }; - - Tour.prototype.showStep = function(i) { - var step; - step = this.getStep(i); - if (!step) return; - this.setCurrentStep(i); - if (step.path !== "" && document.location.pathname !== step.path && document.location.pathname.replace(/^.*[\\\/]/, '') !== step.path) { - document.location.href = step.path; - return; - } - if (step.onShow != null) step.onShow(this); - if (!((step.element != null) && $(step.element).length !== 0 && $(step.element).is(":visible"))) { - this.showNextStep(); - return; - } - this._showPopover(step, i); - if (step.onShown != null) return step.onShown(this); - }; - - Tour.prototype.setCurrentStep = function(value) { - if (value != null) { - this._current = value; - return this.setState("current_step", value); - } else { - this._current = this.getState("current_step"); - if (this._current === null || this._current === "null") { - return this._current = 0; - } else { - return this._current = parseInt(this._current); - } - } - }; - - Tour.prototype.showNextStep = function() { - var step; - step = this.getStep(this._current); - return this.showStep(step.next); - }; - - Tour.prototype.showPrevStep = function() { - var step; - step = this.getStep(this._current); - return this.showStep(step.prev); - }; - - Tour.prototype._showPopover = function(step, i) { - var content, nav, options, tip, - _this = this; - content = "" + step.content + "

    "; - options = $.extend({}, this._options); - if (step.options) $.extend(options, step.options); - if (step.reflex) { - $(step.element).css("cursor", "pointer"); - $(step.element).on("click", function(e) { - $(step.element).css("cursor", "auto"); - return _this.next(); - }); - } - nav = []; - if (step.prev >= 0) { - nav.push(""); - } - if (step.next >= 0) { - nav.push(""); - } - content += nav.join(" | "); - content += "" + options.labels.end + ""; - $(step.element).popover({ - placement: step.placement, - trigger: "manual", - title: step.title, - content: content, - html: true, - animation: step.animation - }).popover("show"); - tip = $(step.element).data("popover").tip(); - this._reposition(tip); - return this._scrollIntoView(tip); - }; - - Tour.prototype._reposition = function(tip) { - var offsetBottom, offsetRight, tipOffset; - tipOffset = tip.offset(); - offsetBottom = $(document).outerHeight() - tipOffset.top - $(tip).outerHeight(); - if (offsetBottom < 0) tipOffset.top = tipOffset.top + offsetBottom; - offsetRight = $(document).outerWidth() - tipOffset.left - $(tip).outerWidth(); - if (offsetRight < 0) tipOffset.left = tipOffset.left + offsetRight; - if (tipOffset.top < 0) tipOffset.top = 0; - if (tipOffset.left < 0) tipOffset.left = 0; - return tip.offset(tipOffset); - }; - - Tour.prototype._scrollIntoView = function(tip) { - var tipRect; - tipRect = tip.get(0).getBoundingClientRect(); - if (!(tipRect.top > 0 && tipRect.bottom < $(window).height() && tipRect.left > 0 && tipRect.right < $(window).width())) { - return tip.get(0).scrollIntoView(true); - } - }; - - Tour.prototype._onresize = function(cb, timeout) { - return $(window).resize(function() { - clearTimeout(timeout); - return timeout = setTimeout(cb, 100); - }); - }; - - Tour.prototype._setupKeyboardNavigation = function() { - var _this = this; - if (this._options.keyboard) { - return $(document).on("keyup.bootstrap-tour", function(e) { - if (!e.which) return; - switch (e.which) { - case 39: - e.preventDefault(); - if (_this._current < _this._steps.length - 1) return _this.next(); - break; - case 37: - e.preventDefault(); - if (_this._current > 0) return _this.prev(); - } - }); - } - }; - - return Tour; - - })(); - return window.Tour = Tour; - })(jQuery, window); - -}).call(this); \ No newline at end of file diff --git a/src/app/browser.coffee b/src/app/browser.coffee index 7c5e47f59f..ef6ff8d9ec 100644 --- a/src/app/browser.coffee +++ b/src/app/browser.coffee @@ -39,7 +39,7 @@ loadJavaScripts = (model) -> require '../../public/vendor/jquery-cookie/jquery.cookie' - require '../../public/vendor/bootstrap-tour' #https://raw.github.com/pushly/bootstrap-tour/master/bootstrap-tour.js + require '../../public/vendor/bootstrap-tour/bootstrap-tour' require '../../public/vendor/bootstrap-datepicker/js/bootstrap-datepicker' require '../../public/vendor/bootstrap-growl/jquery.bootstrap-growl.min' @@ -133,12 +133,8 @@ setupTour = (model) -> $('.main-avatar').popover('destroy') #remove previous popovers tour = new Tour() _.each tourSteps, (step) -> - tour.addStep - html: true - element: step.element - title: step.title - content: step.content - placement: step.placement + tour.addStep _.defaults step, {html:true} + tour._current = 0 if isNaN(tour._current) #bootstrap-tour bug tour.start() ### From 721803d4dec07bde34a58a99f695ba08f53f9f14 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 4 Mar 2013 12:48:10 -0500 Subject: [PATCH 073/220] don't use bootstrap tour until #578 fixed --- src/app/browser.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/browser.coffee b/src/app/browser.coffee index ef6ff8d9ec..e4201a3d3e 100644 --- a/src/app/browser.coffee +++ b/src/app/browser.coffee @@ -207,7 +207,7 @@ module.exports.app = (appExports, model, app) -> setupSortable(model) setupTooltips(model) setupGrowlNotifications(model) unless model.get('_view.mobileDevice') - setupTour(model) + #setupTour(model) $('.datepicker').datepicker({autoclose:true, todayBtn:true}) .on 'changeDate', (ev) -> #for some reason selecting a date doesn't fire a change event on the field, meaning our changes aren't saved From bb5fe4520f0904548440c4c653d272e96077784e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 4 Mar 2013 12:50:49 -0500 Subject: [PATCH 074/220] only setup bootstrap growl notifications once. i still don't get this. this issue is caused after browser.resetDom(), which happens on (1) cron, (2) reset, (3) reroll. after which for some people this commit will cause notifications to disappear altogether, while for others reverting this commit will cause notifications to double. --- src/app/browser.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/browser.coffee b/src/app/browser.coffee index e4201a3d3e..ff3a4b71cc 100644 --- a/src/app/browser.coffee +++ b/src/app/browser.coffee @@ -201,12 +201,12 @@ module.exports.resetDom = (model) -> module.exports.app = (appExports, model, app) -> loadJavaScripts(model) + setupGrowlNotifications(model) unless model.get('_view.mobileDevice') app.on 'render', (ctx) -> #restoreRefs(model) setupSortable(model) setupTooltips(model) - setupGrowlNotifications(model) unless model.get('_view.mobileDevice') #setupTour(model) $('.datepicker').datepicker({autoclose:true, todayBtn:true}) .on 'changeDate', (ev) -> From 8c1c5bba1399d825236f1bf011d1a2e7db94aad3 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 4 Mar 2013 15:20:53 -0500 Subject: [PATCH 075/220] possible fix for #562 - reflists sometimes contain a null value --- src/app/character.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/character.coffee b/src/app/character.coffee index 5e98588f94..ef2f36fadd 100644 --- a/src/app/character.coffee +++ b/src/app/character.coffee @@ -175,7 +175,7 @@ module.exports.updateUser = (model) -> union = _.union obj[type + 'Ids'], taskIds # 2. remove empty (grey) tasks - preened = _.filter(union, (val) -> _.contains(taskIds, val)) + preened = _.filter union, (val) -> _.contains(taskIds, val) and val? # There were indeed issues found, set the new list batch.set("#{type}Ids", preened) # if _.difference(preened, userObj[path]).length != 0 From 87692e90b541cbc2203032ddac408d0471005aba Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 4 Mar 2013 16:10:36 -0500 Subject: [PATCH 076/220] too many emails! --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 28ca6ebcce..cb809a92e5 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ ##Contact ###[Bugs](https://github.com/lefnire/habitrpg/issues) ###[New Features](https://trello.com/board/habitrpg/50e5d3684fe3a7266b0036d6) -###[Email](mailto:tylerrenelle@gmail.com) ##License Code is licensed under GNU GPL v3. Content is licensed under CC-BY-SA 3.0. From 7c10aafb1cdff856455871a1e8055ef2955c458e Mon Sep 17 00:00:00 2001 From: Philip How Date: Mon, 4 Mar 2013 22:00:12 +0000 Subject: [PATCH 077/220] Fixes galore --- src/app/algos.coffee | 6 +----- src/app/scoring.coffee | 20 +++++++++++--------- src/app/tasks.coffee | 6 +++--- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/app/algos.coffee b/src/app/algos.coffee index 563ae7e5b0..d9cbf56d8c 100644 --- a/src/app/algos.coffee +++ b/src/app/algos.coffee @@ -56,9 +56,5 @@ module.exports.taskDeltaFormula = (currentValue, direction) -> delta = Math.max(Math.pow(0.95,currentValue),0.25) else delta = -Math.min(Math.pow(0.95,currentValue),5) - #sign = if (direction is 'up') then 1 else -1 - #delta = Math.pow(0.95,currentValue) * sign - #if delta < -5 then delta = -5 - #console.log("CurrentValue: " + currentValue + " delta: " + delta) - #delta = if (currentValue < 0) then (( -0.1 * currentValue + 1 ) * sign) else (( Math.pow(0.9,currentValue) ) * sign) + console.log("CV = " + currentValue + " Dir = " + direction + " delta = " + delta) return delta \ No newline at end of file diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee index e82f77e466..ce76393839 100644 --- a/src/app/scoring.coffee +++ b/src/app/scoring.coffee @@ -46,7 +46,7 @@ score = (model, taskId, direction, times, batch, cron) -> # (aka, the total delta). This weirdness won't be necessary when calculating mathematically # rather than iteratively nextDelta = algos.taskDeltaFormula(value, direction) - value += nextDelta if adjustvalue + value = Math.max(value + nextDelta, -31) if adjustvalue #cap values so we don't get silly values delta += nextDelta addPoints = -> @@ -66,10 +66,7 @@ score = (model, taskId, direction, times, batch, cron) -> switch type when 'habit' - # Don't adjust values for habits that don't have both + and - - #adjustvalue = if (taskObj.up==false or taskObj.down==false) then false else true - adjustvalue = true; - calculateDelta(adjustvalue) + calculateDelta() # Add habit value to habit-history (if different) if (delta > 0) then addPoints() else subtractPoints() taskObj.history ?= [] @@ -79,7 +76,6 @@ score = (model, taskId, direction, times, batch, cron) -> batch.set "#{taskPath}.history", taskObj.history when 'daily' - #calculateDelta() if cron? # cron calculateDelta() subtractPoints() @@ -92,7 +88,7 @@ score = (model, taskId, direction, times, batch, cron) -> calculateDelta() #don't touch stats on cron else - calculateDelta(false) + calculateDelta() addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes when 'reward' @@ -207,8 +203,11 @@ cron = (model) -> if repeat[helpers.dayMapping[thatDay.day()]]==true daysFailed++ score model, id, 'down', daysFailed, batch, true - if type == 'daily' + if completed #set OHV for completed dailies + newValue = taskObj.value + algos.taskDeltaFormula(taskObj.value,'up') + batch.set "tasks.#{taskObj.id}.value", newValue + taskObj.history ?= [] taskObj.history.push { date: +new Date, value: value } batch.set "tasks.#{taskObj.id}.history", taskObj.history @@ -219,7 +218,10 @@ cron = (model) -> todoTally += absVal else if type is 'habit' #reset 'onlies' value to 0 if taskObj.up==false or taskObj.down==false - batch.set "tasks.#{taskObj.id}.value", 0 + if Math.abs(taskObj.value) < 0.02 + batch.set "tasks.#{taskObj.id}.value", 0 + else + batch.set "tasks.#{taskObj.id}.value", taskObj.value / 2 # Finished tallying obj.history ?= {}; obj.history.todos ?= []; obj.history.exp ?= [] diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index 94d142ad03..d9d904d8ac 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -15,9 +15,9 @@ module.exports.view = (view) -> classes += " uncompleted" switch - when value<-8 then classes += ' color-worst' - when value>=-8 and value<-5 then classes += ' color-worse' - when value>=-5 and value<-1 then classes += ' color-bad' + when value<-20 then classes += ' color-worst' + when value>=-20 and value<-10 then classes += ' color-worse' + when value>=-10 and value<-1 then classes += ' color-bad' when value>=-1 and value<1 then classes += ' color-neutral' when value>=1 and value<5 then classes += ' color-good' when value>=5 and value<10 then classes += ' color-better' From 78ba0f52147080ba0260fb1f87462e5ee3400250 Mon Sep 17 00:00:00 2001 From: Philip How Date: Mon, 4 Mar 2013 22:04:02 +0000 Subject: [PATCH 078/220] comment logging --- src/app/algos.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/algos.coffee b/src/app/algos.coffee index d9cbf56d8c..bb51ea1741 100644 --- a/src/app/algos.coffee +++ b/src/app/algos.coffee @@ -56,5 +56,5 @@ module.exports.taskDeltaFormula = (currentValue, direction) -> delta = Math.max(Math.pow(0.95,currentValue),0.25) else delta = -Math.min(Math.pow(0.95,currentValue),5) - console.log("CV = " + currentValue + " Dir = " + direction + " delta = " + delta) + #console.log("CV = " + currentValue + " Dir = " + direction + " delta = " + delta) return delta \ No newline at end of file From 41fc027150c1cfa9d8bc4dce9fd918f966a720bd Mon Sep 17 00:00:00 2001 From: Zachary Kain Date: Mon, 4 Mar 2013 17:31:33 -0500 Subject: [PATCH 079/220] add npm-debug.log to gitignore because DANG --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 029b274b27..01f730f678 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules *.swp .idea* config.json +npm-debug.log \ No newline at end of file From 01971fba1e1187acbed11ee17f3dbc084ba2c4cb Mon Sep 17 00:00:00 2001 From: Zachary Kain Date: Mon, 4 Mar 2013 19:11:37 -0500 Subject: [PATCH 080/220] improve vertical centering of task checkbox and text --- styles/app/tasks.styl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/styles/app/tasks.styl b/styles/app/tasks.styl index 70d54c189d..adf7efd112 100644 --- a/styles/app/tasks.styl +++ b/styles/app/tasks.styl @@ -76,7 +76,11 @@ li:hover .task-meta-controls .hover-show .task-controls,.task-text display:inline - margin-right:10px + margin: 0 10px 0 0 + vertical-align: -4% + + input + margin-top: 0 .task-meta-controls .hover-show display: none; From b7d46b1e20a8a4611f33879880ca5148f62b4fc9 Mon Sep 17 00:00:00 2001 From: Zachary Kain Date: Mon, 4 Mar 2013 19:42:48 -0500 Subject: [PATCH 081/220] start new task column css, add classes --- styles/app/tasks.styl | 10 +++++- views/app/tasks.html | 75 +++++++++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/styles/app/tasks.styl b/styles/app/tasks.styl index adf7efd112..2fbdfacb0b 100644 --- a/styles/app/tasks.styl +++ b/styles/app/tasks.styl @@ -89,4 +89,12 @@ li:hover .task-meta-controls .hover-show text-decoration:none; .new-task-form - margin-bottom:5px; \ No newline at end of file + margin-bottom:5px; + + +// begin rewrite css 2013-03-04 + +.addtask-form + margin-bottom: 0.75em + .addtask-btn + float: right \ No newline at end of file diff --git a/views/app/tasks.html b/views/app/tasks.html index ccdd9bf407..ae4defe0bf 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -123,49 +123,54 @@ -

    - - + + +
    +
  • -
    -    
    +
    +                
    +                
    +
    + + + {#if :task.history} + + + + {/} +
    -
    - - - {#if :task.history} - - - - {/} -
    + {#if :task.notes} + + {/} +
    - {#if :task.notes} - - {/} -
    + +
    + + {#if equal(:task.type, 'habit')} + {#if :task.up}{/} + {#if :task.down}{/} + + {else if equal(:task.type, 'reward')} + {:task.value} + + {else} + + {/} +
    -
    - - {#if equal(:task.type, 'habit')} - {#if :task.up}{/} - {#if :task.down}{/} - - {else if equal(:task.type, 'reward')} - {:task.value} - - {else} - - {/} -
    -
    {:task.text}
    + +
    {:task.text}
    - -
    -
  • + + + +