From 36809005771e12efeccebaf850bd359dac552e53 Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Sun, 21 Apr 2013 11:53:36 -0400 Subject: [PATCH 001/157] API Auth working + 1 test --- server.js | 2 ++ src/server/api.coffee | 30 ++++++++++++++++++++++++++++++ test/api.mocha.coffee | 27 ++++++++++++++++++++++++--- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/server.js b/server.js index 6412b78225..ddfc8bd6b7 100644 --- a/server.js +++ b/server.js @@ -33,6 +33,8 @@ if (process.env.NODE_ENV === 'development') { "\n\t(2) open http://c4milo.github.com/node-webkit-agent/21.0.1180.57/inspector.html?host=localhost:1337&page=0"); }*/ +if (process.env.NODE_ENV === 'development') Error.stackTraceLimit = Infinity; + process.on('uncaughtException', function (error) { function sendEmail(mailData) { diff --git a/src/server/api.coffee b/src/server/api.coffee index b7b8af798b..11be86b142 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -7,6 +7,7 @@ _ = require 'underscore' validator = require 'derby-auth/node_modules/validator' check = validator.check sanitize = validator.sanitize +utils = require 'derby-auth/utils' NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request" NO_USER_FOUND = err: "No user found." @@ -91,6 +92,35 @@ router.put '/user', auth, (req, res) -> userObj.tasks = _.toArray(userObj.tasks) # FIXME figure out how we're going to consistently handle this. should always be array res.json 201, userObj +### + POST /user/auth +### +router.post '/user/auth', (req, res) -> + username = req.body.username + password = req.body.password + return res.json 401, err: 'No username or password' unless username and password + + model = req.getModel() + + q = model.query("users").withUsername(username) + q.fetch (err, result1) -> + return res.json 401, { err } if err + u1 = result1.get() + return res.json 401, err: 'Username not found' unless u1 # user not found + + # We needed the whole user object first so we can get his salt to encrypt password comparison + q = model.query("users").withLogin(username, utils.encryptPassword(password, u1.auth.local.salt)) + q.fetch (err, result2) -> + return res.json 401, { err } if err + + # joshua tree? + u2 = result2.get() + return res.json 401, err: 'Incorrect password' unless u2 + + res.json + id: u2.id + token: u2.apiToken + ### GET /user/task/:id ### diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 0a488d57df..d09988b081 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -2,6 +2,7 @@ _ = require 'underscore' request = require 'superagent' expect = require 'expect.js' require 'coffee-script' +utils = require 'derby-auth/utils' conf = require("nconf") conf.argv().env().file({file: __dirname + '../config.json'}).defaults @@ -39,6 +40,7 @@ describe 'API', -> model = null user = null uid = null + username = null before (done) -> server = require '../src/server' @@ -51,6 +53,13 @@ describe 'API', -> user = character.newUserObject() user.apiToken = model.id() model.session = {userId:uid} + salt = utils.makeSalt() + username = 'jonfishman' + Math.random().toString().split('.')[1] + user.auth = + local: + username: username + hashed_password: utils.encryptPassword('icculus', salt) + salt: salt model.set "users.#{uid}", user delete model.session # Crappy hack to let server start before tests run @@ -374,11 +383,11 @@ describe 'API', -> tasks = res.body.tasks expect(_.findWhere(tasks,{id:habitId})).to.eql {id: habitId,text: 'hello2',notes: 'note2'} - + foundNewTask = _.findWhere(tasks,{text:'new task2'}) expect(foundNewTask.text).to.be 'new task2' expect(foundNewTask.notes).to.be 'notes2' - + found = _.findWhere(res.body.tasks, {id:dailyId}) expect(found).to.not.be.ok() @@ -390,4 +399,16 @@ describe 'API', -> done() - + it 'POST /api/v1/user/auth', (done) -> + userAuth = + username: username + password: 'icculus' + request.post("#{baseURL}/user/auth") + .set('Accept', 'application/json') + .send(userAuth) + .end (res) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + expect(res.body.id).to.be currentUser.id + expect(res.body.token).to.be currentUser.apiToken + done() From 836787906d4519d1709478b34ebaaab0bd4f78c7 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 12 May 2013 00:05:13 +0100 Subject: [PATCH 002/157] bstart building challenges --- src/app/challenges.coffee | 19 +++++++++++ src/app/index.coffee | 3 ++ styles/app/index.styl | 3 +- views/app/challenges.html | 67 +++++++++++++++++++++++++++++++++++++++ views/app/game-pane.html | 5 +++ views/app/index.html | 1 + 6 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/app/challenges.coffee create mode 100644 views/app/challenges.html diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee new file mode 100644 index 0000000000..2464631e70 --- /dev/null +++ b/src/app/challenges.coffee @@ -0,0 +1,19 @@ +module.exports.app = (appExports, model) -> + user = model.at '_user' + + appExports.challengeCreate = -> + model.set '_challenge.new', + name: '' + habits: [] + dailies: [] + todos: [] + rewards: [] + assignees: 'party' + model.set '_challenge.creating', true + + appExports.challengeSave = -> + #TODO + + appExports.challengeDiscard = -> + model.set '_challenge.new', {} + model.set '_challenge.creating', false \ No newline at end of file diff --git a/src/app/index.coffee b/src/app/index.coffee index 5ef298b46f..04ce6e1c2a 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -102,6 +102,8 @@ setupSubscriptions = (page, model, params, next, cb) -> get '/', (page, model, params, next) -> return page.redirect '/' if page.params?.query?.play? + model.set '_gamePane', true + # removed force-ssl (handled in nginx), see git for code setupSubscriptions page, model, params, next, -> cleanupCorruptTasks(model) # https://github.com/lefnire/habitrpg/issues/634 @@ -131,3 +133,4 @@ ready (model) -> require('./browser').app(exports, model, app) require('./unlock').app(exports, model) require('./filters').app(exports, model) + require('./challenges').app(exports, model) diff --git a/styles/app/index.styl b/styles/app/index.styl index c9128a3dc7..9995a66c94 100644 --- a/styles/app/index.styl +++ b/styles/app/index.styl @@ -165,4 +165,5 @@ hr background-color #dfe9ea padding 1px 3px 1px 3px - +.nav li > a + cursor: pointer diff --git a/views/app/challenges.html b/views/app/challenges.html new file mode 100644 index 0000000000..49a89d8dce --- /dev/null +++ b/views/app/challenges.html @@ -0,0 +1,67 @@ + + + + +
+ +
+ +
+ +
+ +
+
+ + +
+ + +
+ +
+ Mine +
+ +
+ Party +
+ +
+ Guild +
+ +
+ Public +
+ +
+
+ + +
+ {#unless _challenge.creating} + Create New Challenge + {else} +
+ + +
+ ... +
+ + + +
+ {/} + +
+ diff --git a/views/app/game-pane.html b/views/app/game-pane.html index 0e9728d943..6ab4f76658 100644 --- a/views/app/game-pane.html +++ b/views/app/game-pane.html @@ -14,6 +14,7 @@ {/if}
  • Tavern
  • Achievements
  • +
  • Challenges
  • {{#if _loggedIn}}
  • Settings
  • {{/}} @@ -64,6 +65,10 @@ +
    + +
    +
    {{#if _loggedIn}} diff --git a/views/app/index.html b/views/app/index.html index f8f64a866f..b1364ae829 100644 --- a/views/app/index.html +++ b/views/app/index.html @@ -10,6 +10,7 @@ + HabitRPG | Gamify Your Life From 0a37637dafb4b95fd0e56ef92ac9d7a71729594d Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 12 May 2013 13:12:02 +0100 Subject: [PATCH 003/157] challenges: fix up lists to use new list templates --- src/app/challenges.coffee | 2 +- views/app/challenges.html | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index 2464631e70..f6f5ea67cb 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -5,7 +5,7 @@ module.exports.app = (appExports, model) -> model.set '_challenge.new', name: '' habits: [] - dailies: [] + daily: [] todos: [] rewards: [] assignees: 'party' diff --git a/views/app/challenges.html b/views/app/challenges.html index 49a89d8dce..88051459a5 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -51,16 +51,19 @@ {#unless _challenge.creating} Create New Challenge {else} -
    - + -
    - ... -
    +
    + +
    - - -
    + + {/}
    From d28edaf30dce57e4ff443c86aea4e0dfed025c16 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 12 May 2013 13:36:30 +0100 Subject: [PATCH 004/157] challenges: list taken dynamically from scope so we can push/unshift to various task lists depending on location of add-task form. be careful with this commit, not the {{}} instead of {} - seems to be required otherwise things get weird --- src/app/tasks.coffee | 2 +- views/app/tasks.html | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index d9b2811bf9..095314b0f7 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -25,7 +25,7 @@ module.exports.app = (appExports, model) -> newTask = _.defaults {repeat:{su:true,m:true,t:true,w:true,th:true,f:true,s:true}, completed: false }, newTask when 'todo' newTask = _.defaults {completed: false }, newTask - model.unshift "_#{type}List", newTask + e.at().unshift newTask # e.at() in this case is the list, which was scoped here using {#with @list}...{/} newModel.set '' appExports.del = (e, el) -> diff --git a/views/app/tasks.html b/views/app/tasks.html index cf404e8445..6996b68382 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -135,10 +135,15 @@ {{#if equal(@type,'todo')}}{{/}} {{#if @editable}} -
    - - -
    + + + + {{#with @list}} +
    + + +
    + {{/}}
    {{/}}
      From 5ea6e55e1193aa529c1e1959ffd65d693ad3697a Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 12 May 2013 13:57:17 +0100 Subject: [PATCH 005/157] challenges: add assignTo option on challenge-creation --- src/app/challenges.coffee | 2 +- views/app/challenges.html | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index f6f5ea67cb..3f7562baf8 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -8,7 +8,7 @@ module.exports.app = (appExports, model) -> daily: [] todos: [] rewards: [] - assignees: 'party' + assignTo: 'Party' model.set '_challenge.creating', true appExports.challengeSave = -> diff --git a/views/app/challenges.html b/views/app/challenges.html index 88051459a5..a71cd30f12 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -62,6 +62,37 @@ editable=true /> +
      +
      + +
      +
      + {#if equal(_challenge.new.assignTo,'Party')} +
      +
      +
      All Party
      + No individual privacy on the challenge, all party members can see progress even if they decline the challenge. Any new party members can subscribe to this challenge. +
      +
      + Individual Members + {{#each _partyMembers as :member}} +
      {{username(:member.auth,:member.profile.name)}}
      + {{/}} + Only the invited party members can subscribe to this challenge. New party joins won't see this challenge. +
      +
      + + {/} + {#if equal(_challenge.new.assignTo,'Guild')} + Which Guild? + {/} +
      +
      + {/} From b8ef0789dfc1241e059f11ace856a135dc6a61d3 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 12 May 2013 16:47:18 +0100 Subject: [PATCH 006/157] challneges: semi-functional challenge creation for parties --- src/app/challenges.coffee | 16 ++++++++++++++-- views/app/challenges.html | 14 ++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index 3f7562baf8..e6461d3d84 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -1,4 +1,9 @@ +_ = require 'underscore' + module.exports.app = (appExports, model) -> + browser = require './browser' + helpers = require './helpers' + user = model.at '_user' appExports.challengeCreate = -> @@ -12,8 +17,15 @@ module.exports.app = (appExports, model) -> model.set '_challenge.creating', true appExports.challengeSave = -> - #TODO + challenge = _.defaults model.get('_challenge.new'), + id: model.id() + uuid: user.get('id') + user: helpers.username(model.get('_user.auth'), model.get('_user.profile.name')) + timestamp: +new Date + model.unshift '_party.challenges', challenge + challengeDiscard() + browser.growlNotification('Challenge Created','success') - appExports.challengeDiscard = -> + appExports.challengeDiscard = challengeDiscard = -> model.set '_challenge.new', {} model.set '_challenge.creating', false \ No newline at end of file diff --git a/views/app/challenges.html b/views/app/challenges.html index a71cd30f12..ff71e04a5f 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -32,7 +32,17 @@
      - Party + {#each _party.challenges as :challenge} +

      {:challenge.name} (by {:challenge.user})

      +
      + +
      +
      + {/}
      @@ -93,7 +103,7 @@
      - + {/} From 57f51048a16f014a7e699b07e20eabf722c78dd3 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 12 May 2013 17:22:07 +0100 Subject: [PATCH 007/157] challenges: basics for challenge subscription --- src/app/challenges.coffee | 29 ++++++++++++++++++++--------- src/app/helpers.coffee | 1 - views/app/challenges.html | 23 ++++++++++++++++++----- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index e6461d3d84..6eddf286b6 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -7,25 +7,36 @@ module.exports.app = (appExports, model) -> user = model.at '_user' appExports.challengeCreate = -> + id = model.id() model.set '_challenge.new', name: '' habits: [] - daily: [] + dailys: [] todos: [] rewards: [] assignTo: 'Party' - model.set '_challenge.creating', true - - appExports.challengeSave = -> - challenge = _.defaults model.get('_challenge.new'), - id: model.id() + id: id uuid: user.get('id') user: helpers.username(model.get('_user.auth'), model.get('_user.profile.name')) timestamp: +new Date - model.unshift '_party.challenges', challenge - challengeDiscard() + + model.set '_challenge.creating', true + + appExports.challengeSave = -> + model.unshift '_party.challenges', model.get('_challenge.new'), -> challengeDiscard() browser.growlNotification('Challenge Created','success') appExports.challengeDiscard = challengeDiscard = -> model.set '_challenge.new', {} - model.set '_challenge.creating', false \ No newline at end of file + model.set '_challenge.creating', false + + appExports.challengeSubscribe = (e) -> + userChallenges = user.get('challenges') + chal = e.get() + user.unshift('challenges', chal.id) unless userChallenges and (userChallenges.indexOf(chal.id) != -1) + _.each ['habit', 'daily', 'todo', 'reward'], (type) -> + _.each chal["#{type}s"], (task) -> model.push("_#{type}List", task) + + appExports.challengeUnsubscribe = (e) -> + i = user.get('challenges')?.indexOf e.get('id') + user.remove("challenges.#{i}") if i? and i != -1 diff --git a/src/app/helpers.coffee b/src/app/helpers.coffee index 16fa20b286..ef990d7d26 100644 --- a/src/app/helpers.coffee +++ b/src/app/helpers.coffee @@ -214,7 +214,6 @@ viewHelpers = (view) -> view.fn 'itemText', (type, item=0) -> items[type][parseInt(item)].text view.fn 'itemStat', (type, item=0) -> if type is 'weapon' then items[type][parseInt(item)].strength else items[type][parseInt(item)].defense - # view.fn 'activeFilters', (filters) -> # debugger # (_.find filters, (f) -> f)? diff --git a/views/app/challenges.html b/views/app/challenges.html index ff71e04a5f..d2a65965f0 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -33,7 +33,16 @@
      {#each _party.challenges as :challenge} +
      + {#if indexOf(_user.challenges,:challenge.id)} + Unsubscribe + {else} + Subscribe + {/} +

      {:challenge.name} (by {:challenge.user})

      + +
      No individual privacy on the challenge, all party members can see progress even if they decline the challenge. Any new party members can subscribe to this challenge.
      - Individual Members - {{#each _partyMembers as :member}} -
      {{username(:member.auth,:member.profile.name)}}
      - {{/}} - Only the invited party members can subscribe to this challenge. New party joins won't see this challenge. +
      Individual Members
      +
      + +
      +
      Only the invited party members can subscribe to this challenge. New party joins won't see this challenge.
      From ae8a283d0e6b022d15094b67f57f13b9e5b1f803 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 12 May 2013 18:54:45 +0100 Subject: [PATCH 008/157] challenges: chevron in collapsing title --- src/app/challenges.coffee | 7 +++++-- views/app/challenges.html | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index 6eddf286b6..0614dcb557 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -7,7 +7,6 @@ module.exports.app = (appExports, model) -> user = model.at '_user' appExports.challengeCreate = -> - id = model.id() model.set '_challenge.new', name: '' habits: [] @@ -15,7 +14,7 @@ module.exports.app = (appExports, model) -> todos: [] rewards: [] assignTo: 'Party' - id: id + id: model.id() uuid: user.get('id') user: helpers.username(model.get('_user.auth'), model.get('_user.profile.name')) timestamp: +new Date @@ -40,3 +39,7 @@ module.exports.app = (appExports, model) -> appExports.challengeUnsubscribe = (e) -> i = user.get('challenges')?.indexOf e.get('id') user.remove("challenges.#{i}") if i? and i != -1 + + appExports.challengeCollapse = (e, el) -> + $(el).next().toggle() + i = $(el).find('i').toggleClass 'icon-chevron-down' \ No newline at end of file diff --git a/views/app/challenges.html b/views/app/challenges.html index d2a65965f0..080c6a6b82 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -40,7 +40,7 @@ Subscribe {/} -

      {:challenge.name} (by {:challenge.user})

      +

      {:challenge.name} (by {:challenge.user})

      From 0a291bf8922d31e5e3300df3c8fa87d2506fa364 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 12 May 2013 19:23:16 +0100 Subject: [PATCH 009/157] challenges: add challenge.id to task so we can get the icon-bullhorn --- src/app/tasks.coffee | 6 ++++++ views/app/tasks.html | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index 095314b0f7..f5c307bb4d 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -16,6 +16,12 @@ module.exports.app = (appExports, model) -> activeFilters = _.reduce user.get('filters'), ((memo,v,k) -> memo[k]=v if v;memo), {} newTask = {id: model.id(), type: type, text: text, notes: '', value: 0, tags: activeFilters} + + isChallenge = e.at().path().indexOf('_challenge.new') != -1 + if isChallenge + activeFilters = {} + newTask.challenge = model.get '_challenge.new.id' + switch type when 'habit' newTask = _.defaults {up: true, down: true}, newTask diff --git a/views/app/tasks.html b/views/app/tasks.html index 6996b68382..e6b2083618 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -174,6 +174,8 @@ + + {{#if :task.challenge}}{{/}} From 6507795d99388bee2bfba70e21df6b6f65a2aad1 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 17 May 2013 20:59:29 +0100 Subject: [PATCH 010/157] challenges: prevent some task options (such as scoring, check-marking, tags, etc) for challenge edits --- views/app/tasks.html | 51 +++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/views/app/tasks.html b/views/app/tasks.html index e6b2083618..be26185091 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -147,7 +147,7 @@
      {{/}}
        - {#each @list as :task}{/} + {#each @list as :task}{/}
      {{@extra}}
      @@ -192,24 +192,41 @@
      - {#if equal(:task.type, 'habit')} - {#if :task.up}{/} - {#if :task.down}{/} + {{#if equal(:task.type,'habit')}} + {{#if @main}} + {#if :task.up}{/} + {#if :task.down}{/} + {{else}} + {#if :task.up}{/} + {#if :task.down}{/} + {{/}} - {else if equal(:task.type, 'reward')} - - {:task.value} - - + {{else if equal(:task.type,'reward')}} + {{#if @main}} + + {:task.value} + + + {{else}} + + {:task.value} + + + {{/}} - {else} - - - - - {/} + {{else}} + + {{#if @main}} + + + {{else}} + + + {{/}} + + {{/}}
      @@ -218,7 +235,7 @@

      - + @@ -279,7 +296,7 @@ {/} - + {{#if @main}}{{/}} From 9b41712ca6bfe9721af09f1293803e4b58542878 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 17 May 2013 23:32:24 +0100 Subject: [PATCH 011/157] challenges: fix to not apply user's active tags to new challenge tasks, but also not apply filters except to main user's list --- src/app/helpers.coffee | 12 +++++++----- src/app/tasks.coffee | 8 ++------ views/app/tasks.html | 4 ++-- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/app/helpers.coffee b/src/app/helpers.coffee index ef990d7d26..23da04f8f5 100644 --- a/src/app/helpers.coffee +++ b/src/app/helpers.coffee @@ -138,17 +138,19 @@ viewHelpers = (view) -> ### Tasks ### - view.fn 'taskClasses', (task, filters, dayStart, lastCron, showCompleted=false) -> + view.fn 'taskClasses', (task, filters, dayStart, lastCron, showCompleted=false, main) -> return unless task {type, completed, value, repeat} = task # completed / remaining toggle return 'hidden' if (type is 'todo') and (completed != showCompleted) - for filter, enabled of filters - if enabled and not task.tags?[filter] - # All the other classes don't matter - return 'hidden' + # Filters + if main # only show when on your own list + for filter, enabled of filters + if enabled and not task.tags?[filter] + # All the other classes don't matter + return 'hidden' classes = type diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index f5c307bb4d..763793f7ab 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -14,13 +14,9 @@ module.exports.app = (appExports, model) -> # Don't add a blank task; 20/02/13 Added a check for undefined value, more at issue #463 -lancemanfv return if /^(\s)*$/.test(text) || text == undefined - activeFilters = _.reduce user.get('filters'), ((memo,v,k) -> memo[k]=v if v;memo), {} - newTask = {id: model.id(), type: type, text: text, notes: '', value: 0, tags: activeFilters} - + newTask = {id: model.id(), type: type, text: text, notes: '', value: 0, tags:{}} isChallenge = e.at().path().indexOf('_challenge.new') != -1 - if isChallenge - activeFilters = {} - newTask.challenge = model.get '_challenge.new.id' + newTask.tags = if isChallenge then {} else _.reduce user.get('filters'), ((memo,v,k) -> memo[k]=v if v;memo), {} switch type when 'habit' diff --git a/views/app/tasks.html b/views/app/tasks.html index be26185091..be372908a9 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -147,7 +147,7 @@
      {{/}}
        - {#each @list as :task}{/} + {#each @list as :task}{/}
      {{@extra}}
      @@ -163,7 +163,7 @@ -
    • +
    • From e7558139e67001d3d06f107c0300b4566ac0909e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 17 May 2013 23:33:24 +0100 Subject: [PATCH 012/157] challenges: require challenge title, when user subscribe add new tag of challenge name. unsubscribe deletes tasks --- package.json | 4 +- src/app/challenges.coffee | 23 +++++++-- views/app/challenges.html | 103 ++++++++++++++++++++------------------ 3 files changed, 76 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index fd5f3a31de..39da71142d 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "guid": "*", "moment": "*", "stripe": "*", - "lodash": "1.0.x", "coffee-script": "1.4.x", "underscore": "*", "mongoskin": "*", @@ -26,7 +25,8 @@ "resolve": "~0.2.3", "expect.js": "~0.2.0", "derby-i18n": "git://github.com/switz/derby-i18n#master", - "relative-date": "~1.1.1" + "relative-date": "~1.1.1", + "lodash": "~1.2.1" }, "private": true, "subdomain": "habitrpg", diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index 0614dcb557..aeb2982d4f 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -1,4 +1,5 @@ _ = require 'underscore' +lodash = require 'lodash' module.exports.app = (appExports, model) -> browser = require './browser' @@ -30,15 +31,31 @@ module.exports.app = (appExports, model) -> model.set '_challenge.creating', false appExports.challengeSubscribe = (e) -> - userChallenges = user.get('challenges') chal = e.get() + + # Add challenge name as a tag for user + tags = user.get('tags') + unless tags and _.findWhere(tags,{id: chal.id}) + model.push('_user.tags', {id: chal.id, name: chal.name}) + + tags = {}; tags[chal.id] = true + # Add all challenge's tasks to user's tasks + userChallenges = user.get('challenges') user.unshift('challenges', chal.id) unless userChallenges and (userChallenges.indexOf(chal.id) != -1) _.each ['habit', 'daily', 'todo', 'reward'], (type) -> - _.each chal["#{type}s"], (task) -> model.push("_#{type}List", task) + _.each chal["#{type}s"], (task) -> + task.tags = tags + task.challenge = chal.id + model.push("_#{type}List", task) appExports.challengeUnsubscribe = (e) -> - i = user.get('challenges')?.indexOf e.get('id') + chal = e.get() + i = user.get('challenges')?.indexOf chal.id user.remove("challenges.#{i}") if i? and i != -1 + _.each ['habit', 'daily', 'todo', 'reward'], (type) -> + _.each chal["#{type}s"], (task) -> + model.remove "_#{type}List", lodash.findIndex(model.get("_#{type}List",{id:task.id})) + model.del "_user.tasks.#{task.id}" appExports.challengeCollapse = (e, el) -> $(el).next().toggle() diff --git a/views/app/challenges.html b/views/app/challenges.html index 080c6a6b82..9f66d21ada 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -33,24 +33,23 @@
      {#each _party.challenges as :challenge} -
      - {#if indexOf(_user.challenges,:challenge.id)} - Unsubscribe - {else} - Subscribe - {/} -
      -

      {:challenge.name} (by {:challenge.user})

      +
      + +

      {:challenge.name} (by {:challenge.user})

      -
      - +
      + +
      +
      -
      {/}
      @@ -70,7 +69,6 @@ {#unless _challenge.creating} Create New Challenge {else} -
      -
      -
      - -
      -
      - {#if equal(_challenge.new.assignTo,'Party')} -
      -
      -
      All Party
      - No individual privacy on the challenge, all party members can see progress even if they decline the challenge. Any new party members can subscribe to this challenge. -
      -
      -
      Individual Members
      -
      - +
      + + + +
      +
      + +
      +
      + {#if equal(_challenge.new.assignTo,'Party')} +
      +
      +
      All Party
      + No individual privacy on the challenge, all party members can see progress even if they decline the challenge. Any new party members can subscribe to this challenge. +
      +
      +
      Individual Members
      +
      + +
      +
      Only the invited party members can subscribe to this challenge. New party joins won't see this challenge.
      -
      Only the invited party members can subscribe to this challenge. New party joins won't see this challenge.
      -
      - {/} - {#if equal(_challenge.new.assignTo,'Guild')} - Which Guild? - {/} -
      -
      + {/} + {#if equal(_challenge.new.assignTo,'Guild')} + Which Guild? + {/} +
      + + + + + + - - {/}
      From 03c40c1571e9c6c18816328bd2eed3e1d3ea3918 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 18 May 2013 12:22:54 +0100 Subject: [PATCH 013/157] challenges: migration & store: rename parties to groups, create habitrpg guild, move tavern into habitrpg, overhaul of motifs and subscriptions --- migrations/20130518_setup_groups.js | 38 +++++++++++++ src/app/index.coffee | 35 ++++++++---- src/app/party.coffee | 14 ++--- src/server/store.coffee | 85 ++++++++++++++--------------- 4 files changed, 110 insertions(+), 62 deletions(-) create mode 100644 migrations/20130518_setup_groups.js diff --git a/migrations/20130518_setup_groups.js b/migrations/20130518_setup_groups.js new file mode 100644 index 0000000000..638d882295 --- /dev/null +++ b/migrations/20130518_setup_groups.js @@ -0,0 +1,38 @@ +/** + * In adding the Guilds feature (which supports the Challenges feature), we are consolidating parties and guilds + * into one collection: groups, with group.type either 'party' or 'guild'. We are also creating the 'habitrpg' guild, + * which everyone is auto-subscribed to, and moving tavern chat into that guild + * + * mongo habitrpg ./node_modules/underscore/underscore.js ./migrations/20130518_setup_groups.js + */ + +/** + * TODO + * 1) rename collection parties => groups + * 2) add group.type = 'party' for each current group + * 3) create habitrpg group, .type='guild' + * 4) move tavern.chat.chat into habitrpg guild + * 5) subscribe everyone to habitrpg (be sure to set that for default user too!) + */ + +db.parties.renameCollection('groups'); +//db.parties.dropCollection(); // doesn't seem to do this step during rename... +//db.parties.ensureIndex( { 'members': 1, 'background': 1} ); + +db.groups.update({}, {$set:{type:'party'}}, {multi:true}); + +tavern = db.tavern.findOne(); +db.tavern.drop(); + +//TODO make as a callback of previous, or make sure group.type is still 'guild' for habitrpg in the end +db.groups.insert({ + _id: "habitrpg", + leader: '9', + type: 'guild', + name: "HabitRPG", + chat: tavern.messages, + info: { + blurb: '', + websites: [] + } +}); \ No newline at end of file diff --git a/src/app/index.coffee b/src/app/index.coffee index 04ce6e1c2a..9fe2649698 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -67,11 +67,10 @@ cleanupCorruptTasks = (model) -> setupSubscriptions = (page, model, params, next, cb) -> uuid = model.get('_userId') or model.session.userId # see http://goo.gl/TPYIt selfQ = model.query('users').withId(uuid) #keep this for later - partyQ = model.query('parties').withMember(uuid) + groupsQ = model.query('groups').withMember(uuid) - partyQ.fetch (err, party) -> + groupsQ.fetch (err, groups, extra) -> return next(err) if err - finished = (descriptors, paths) -> model.subscribe.apply model, descriptors.concat -> [err, refs] = [arguments[0], arguments] @@ -80,22 +79,36 @@ setupSubscriptions = (page, model, params, next, cb) -> unless model.get('_user') console.error "User not found - this shouldn't be happening!" return page.redirect('/logout') #delete model.session.userId + extra(arguments) if extra return cb() + groupsObj = groups.get() + # (1) Solo player - return finished([selfQ, 'tavern'], ['_user', '_tavern']) unless party.get() + return finished([selfQ, "groups.habitrpg"], ['_user', '_habitRPG']) if _.isEmpty(groupsObj) - ## (2) Party has members, subscribe to those users too - membersQ = model.query('users').party(party.get('members')) + ## (2) Party or Guild has members, fetch those users too + groupsInfo = _.reduce groupsObj, ((m,g)-> + if g.type is 'guild' then m.guildIds.push(g.id) else m.partyId = g.id + m.members = m.members.concat(g.members) + m + ), {guildIds:[], partyId:null, members:[]} - # Fetch instead of subscribe. There's nothing dynamic we need from members just yet, they'll update _party instead. - # This may change in the future. - membersQ.fetch (err, members) -> + # Fetch, not subscribe. There's nothing dynamic we need from members, just the the Group (below) which includes chat, challenges, etc + model.query('users').publicInfo(groupsInfo.members).fetch (err, members) -> return next(err) if err - model.ref '_partyMembers', members + # we need _members as an object in the view, so we can iterate over _party.members as :id, and access _members[:id] for the info + mObj = members.get() + model.set "_members", _.object(_.pluck(mObj,'id'), mObj) + ## Then subscribe to the groups themselves. We separate them by _party, _guilds, and _habitRPG (the "global" guild). # Note - selfQ *must* come after membersQ in subscribe, otherwise _user will only get the fields restricted by party-members in store.coffee. Strang bug, but easy to get around - return finished([partyQ, selfQ, 'tavern'], ['_party', '_user', '_tavern']) + partyQ = model.query('groups').withIds(groupsInfo.partyId) + if _.isEmpty(groupsInfo.guildIds) + finished [partyQ, 'groups.habitrpg', selfQ], ['_party', '_habitRPG', '_user'] + else + guildsQ = model.query('groups').withIds(groupsInfo.guildIds) + finished [partyQ, guildsQ, 'groups.habitrpg', selfQ], ['_party', '_guilds', '_habitRPG', '_user'] # ========== ROUTES ========== diff --git a/src/app/party.coffee b/src/app/party.coffee index f84762ae5d..b2edbb2a89 100644 --- a/src/app/party.coffee +++ b/src/app/party.coffee @@ -20,7 +20,7 @@ module.exports.app = (appExports, model, app) -> model.on 'set', '_user.party.invitation', (after, before) -> if !before? and after? # they just got invited - partyQ = model.query('parties').withId(after) + partyQ = model.query('groups').withId(after) partyQ.fetch (err, party) -> return next(err) if err model.ref '_party', party @@ -28,15 +28,15 @@ module.exports.app = (appExports, model, app) -> appExports.partyCreate = -> newParty = model.get("_newParty") - id = model.add 'parties', { name: newParty, leader: user.get('id'), members: [user.get('id')], invites:[] } - user.set 'party', {current: id, invitation: null, leader: true}, -> + id = model.add 'groups', { name: newParty, leader: user.get('id'), members: [user.get('id')], invites:[] } + user.set 'party', {current: id, invitation: null}, -> window.location.reload true appExports.partyInvite = -> id = model.get('_newPartyMember').replace(/[\s"]/g, '') return if _.isEmpty(id) - model.query('users').party([id]).fetch (err, users) -> + model.query('users').publicInfo([id]).fetch (err, users) -> throw err if err u = users.at(0).get() if !u? @@ -55,8 +55,8 @@ module.exports.app = (appExports, model, app) -> partyId = user.get('party.invitation') user.set 'party.invitation', null user.set 'party.current', partyId - model.at("parties.#{partyId}.members").push user.get('id'), -> window.location.reload() -# model.query('parties').withId(partyId).fetch (err, p) -> + model.at("groups.#{partyId}.members").push user.get('id'), -> window.location.reload() +# model.query('groups').withId(partyId).fetch (err, p) -> # members = p.get('members') # members.push user.get('id') # p.set 'members', members, -> @@ -77,7 +77,7 @@ module.exports.app = (appExports, model, app) -> index = members.indexOf(user.get('id')) party.remove 'members', index, 1, -> if members.length is 1 # # last member out, kill the party - model.del "parties.#{id}", (-> window.location.reload true) + model.del "groups.#{id}", (-> window.location.reload true) else window.location.reload true diff --git a/src/server/store.coffee b/src/server/store.coffee index 7c2d5cb3fe..c7cc878384 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -5,10 +5,15 @@ Setup read / write access @param store ### +publicAccess = -> + accept = arguments[arguments.length-2] + #return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@) + return accept(false) if derbyAuth.bustedSession(@) + accept(true) + module.exports.customAccessControl = (store) -> userAccess(store) - partySystem(store) - tavernSystem(store) + groupSystem(store) REST(store) ### @@ -84,10 +89,14 @@ REST = (store) -> ### - Party permissions + Party & Guild Permissions ### -partySystem = (store) -> - store.query.expose "users", "party", (ids) -> +groupSystem = (store) -> + + ### + Public User Info + ### + store.query.expose "users", "publicInfo", (ids) -> @where("id").within(ids) .only('stats', 'items', @@ -98,49 +107,37 @@ partySystem = (store) -> 'preferences', 'auth.local.username', 'auth.facebook.displayName') + store.queryAccess "users", "publicInfo", publicAccess - store.queryAccess "users", "party", (ids, accept, err) -> -# return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@) - return accept(false) if derbyAuth.bustedSession(@) - accept(true) # no harm in public user stats + ### + Fetch group info (ie, they just got invited) + ### + store.query.expose "groups", "withIds", (ids) -> + if typeof ids is 'string' + @where("id").equals(ids).findOne() # find a single group + else + @where("id").within(ids) # find multiple groups + store.queryAccess "groups", "withIds", publicAccess - store.query.expose "parties", "withId", (id) -> - @where("id").equals(id).findOne() + ### + Read / Write groups, so they can create new groups + ### + store.readPathAccess "groups.*", publicAccess + store.writeAccess "*", "groups.*", publicAccess - store.queryAccess "parties", "withId", (id, accept, err) -> -# return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@) - return accept(false) if derbyAuth.bustedSession(@) - accept(true) + ### + Find group which has member by id + ### + store.query.expose "groups", "withMember", (id) -> + @where('members').contains([id]).only(['id','members']) + store.queryAccess 'groups', 'withMember', publicAccess - store.readPathAccess "parties.*", -> - accept = arguments[arguments.length-2] - accept(true) + ### + Public HabitRPG Guild + ### - store.writeAccess "*", "parties.*", -> - 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(@) - accept(true) + store.readPathAccess 'groups.habitrpg', publicAccess + store.writeAccess "*", "groups.habitrpg.chat.*", publicAccess + store.writeAccess "*", "groups.habitrpg.challenges.*", publicAccess - store.query.expose "parties", "withMember", (id) -> - @where('members').contains([id]).findOne() - - store.queryAccess 'parties', 'withMember', (id, accept, err) -> - return accept(false) if derbyAuth.bustedSession(@) - accept(true) - -### - LFG / tavern system -### -tavernSystem = (store) -> - store.readPathAccess 'tavern', -> - accept = arguments[arguments.length-2] - return accept(false) if derbyAuth.bustedSession(@) - accept(true) - - store.writeAccess "*", "tavern.*", -> - accept = arguments[arguments.length-2] - return accept(false) if derbyAuth.bustedSession(@) - accept(true) From 079e7eb30fb7eedef72a52f96b0f5b2142d7e218 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 26 May 2013 15:09:41 +0100 Subject: [PATCH 014/157] challenges: fix up htmls to use new party-member subscription methods --- views/app/avatar.html | 6 +++--- views/app/challenges.html | 4 ++-- views/app/header.html | 12 ++++++------ views/app/party.html | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/views/app/avatar.html b/views/app/avatar.html index 543b5053dc..b3a0c79619 100644 --- a/views/app/avatar.html +++ b/views/app/avatar.html @@ -1,7 +1,7 @@ - {{#each _partyMembers as :profile}} - - + {{#each _party.members as :memberId}} + + <@footer> diff --git a/views/app/challenges.html b/views/app/challenges.html index 9f66d21ada..eb3171e8ac 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -102,8 +102,8 @@
      Individual Members
      diff --git a/views/app/header.html b/views/app/header.html index 2238925593..02121a3efc 100644 --- a/views/app/header.html +++ b/views/app/header.html @@ -1,5 +1,5 @@ - diff --git a/views/app/party.html b/views/app/party.html index a2afcc2fa3..d0bd00f54f 100644 --- a/views/app/party.html +++ b/views/app/party.html @@ -1,11 +1,11 @@ - {#if _partyMembers} + {#if _party.members}

      {{_party.name}}

      - {{#each _partyMembers as :member}} - + {{#each _party.members as :memberId}} + {{/}}
      {{username(:member.auth, :member.profile.name)}}({{:member.id}})
      {{username(_party[:memberId].auth, _party[:memberId].profile.name)}}({{:memberId}})
      From 9dfdb52c988b90ab61d9371a99008a85c46650e2 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 26 May 2013 15:33:01 +0100 Subject: [PATCH 015/157] challenges: rename party.html to groups.html --- views/app/game-pane.html | 4 ++-- views/app/{party.html => groups.html} | 2 +- views/app/header.html | 2 +- views/app/index.html | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename views/app/{party.html => groups.html} (97%) diff --git a/views/app/game-pane.html b/views/app/game-pane.html index b6615e0f9e..c3d02cffda 100644 --- a/views/app/game-pane.html +++ b/views/app/game-pane.html @@ -37,7 +37,7 @@
      - +
      @@ -132,7 +132,7 @@
        {#each _tavern.chat.messages as :message} - + {/}
      diff --git a/views/app/party.html b/views/app/groups.html similarity index 97% rename from views/app/party.html rename to views/app/groups.html index 01a8fb51f6..7c7880fa6f 100644 --- a/views/app/party.html +++ b/views/app/groups.html @@ -27,7 +27,7 @@
        {#each _party.chat as :message} - + {/}
      diff --git a/views/app/header.html b/views/app/header.html index 02121a3efc..9a326ca5c7 100644 --- a/views/app/header.html +++ b/views/app/header.html @@ -31,7 +31,7 @@
      {{#unless equal(:memberId, _userId)}} - + {{/}} diff --git a/views/app/index.html b/views/app/index.html index b1364ae829..4cfd3048e3 100644 --- a/views/app/index.html +++ b/views/app/index.html @@ -6,7 +6,7 @@ - + From e32ee04baa27b93905eb562304216e50c8ef005e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 26 May 2013 15:35:58 +0100 Subject: [PATCH 016/157] guilds: bug fix on _tavern => _habitRPG --- src/app/party.coffee | 4 ++-- views/app/game-pane.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/party.coffee b/src/app/party.coffee index 3a7a16d5fa..7205c55569 100644 --- a/src/app/party.coffee +++ b/src/app/party.coffee @@ -116,14 +116,14 @@ module.exports.app = (appExports, model, app) -> chat.remove(200) model.on 'unshift', '_party.chat', -> $('.chat-message').tooltip() - model.on 'unshift', '_tavern.chat.messages', -> $('.chat-message').tooltip() + model.on 'unshift', '_habitrpg.chat', -> $('.chat-message').tooltip() appExports.partySendChat = -> sendChat('_party.chat', '_chatMessage') model.set '_user.party.lastMessageSeen', model.get('_party.chat')[0].id appExports.tavernSendChat = -> - sendChat('_tavern.chat.messages', '_tavernMessage') + sendChat('_habitRPG.chat', '_tavernMessage') appExports.partyMessageKeyup = (e, el, next) -> return next() unless e.keyCode is 13 diff --git a/views/app/game-pane.html b/views/app/game-pane.html index c3d02cffda..b7c1ff4036 100644 --- a/views/app/game-pane.html +++ b/views/app/game-pane.html @@ -131,7 +131,7 @@
        - {#each _tavern.chat.messages as :message} + {#each _habitRPG.chat as :message} {/}
      From 0f8dbd044e0549dba70262f7da202a4a949a1ee9 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 26 May 2013 20:21:07 +0100 Subject: [PATCH 017/157] guilds: got all groups html generalized (using same party html as tavern, will add guilds soon) --- src/app/party.coffee | 33 ++++---- views/app/game-pane.html | 102 +++++-------------------- views/app/groups.html | 159 +++++++++++++++++++++++++++++---------- 3 files changed, 150 insertions(+), 144 deletions(-) diff --git a/src/app/party.coffee b/src/app/party.coffee index 7205c55569..05fd657967 100644 --- a/src/app/party.coffee +++ b/src/app/party.coffee @@ -84,12 +84,17 @@ module.exports.app = (appExports, model, app) -> Chat Functionality ### - sendChat = (path, input) -> - chat = model.at path - text = model.get input + model.on 'unshift', '_party.chat', -> $('.chat-message').tooltip() + model.on 'unshift', '_habitrpg.chat', -> $('.chat-message').tooltip() + + appExports.sendChat = (e,el) -> + text = model.get '_chatMessage' # Check for non-whitespace characters return unless /\S/.test text - model.set(input, '') + + group = e.at() + chat = group.at('chat') + model.set('_chatMessage', '') message = id: model.id() @@ -114,24 +119,12 @@ module.exports.app = (appExports, model, app) -> chat.set messages else chat.remove(200) + type = $(el).attr('data-type') + model.set '_user.party.lastMessageSeen', chat.get()[0].id if group.get('type') is 'party' - model.on 'unshift', '_party.chat', -> $('.chat-message').tooltip() - model.on 'unshift', '_habitrpg.chat', -> $('.chat-message').tooltip() - - appExports.partySendChat = -> - sendChat('_party.chat', '_chatMessage') - model.set '_user.party.lastMessageSeen', model.get('_party.chat')[0].id - - appExports.tavernSendChat = -> - sendChat('_habitRPG.chat', '_tavernMessage') - - appExports.partyMessageKeyup = (e, el, next) -> + appExports.chatKeyup = (e, el, next) -> return next() unless e.keyCode is 13 - appExports.partySendChat() - - appExports.tavernMessageKeyup = (e, el, next) -> - return next() unless e.keyCode is 13 - appExports.tavernSendChat() + appExports.sendChat(e, el) appExports.deleteChatMessage = (e) -> if confirm("Delete chat message?") is true diff --git a/views/app/game-pane.html b/views/app/game-pane.html index b7c1ff4036..d96f1ca136 100644 --- a/views/app/game-pane.html +++ b/views/app/game-pane.html @@ -6,23 +6,21 @@
      -
      +
      @@ -36,15 +34,15 @@
      -
      - +
      +
      -
      +
      -
      +

      Inventory

      @@ -57,87 +55,25 @@
      -
      +
      -
      - +
      +
      -
      +
      -
      - {{#if _loggedIn}} - - {{/}} +
      +
      - -
      -
      -
      - - - -
      -
      -
      -

      Daniel Johansson

      -
      - Welcome to the Tavern! I'm Daniel, the bar keep. If you want to rest a while (going on vacation? sudden illness?), I'll set you up at the inn - dailies won't hurt you while you're resting. Stay a while & meet the locals. -
      -
      -
      -
      -
      - -
      Whilst resting your dailies are saved and aren't effected by day turn-over. Whether you check out tomorrow or in a weeks time you'll continue in the same state as when you checked in.
      - - - -
      -
      -

      Tavern Talk & LFG

      -
      - -
      -
      -
      - -
      -
      -
      - -
        - {#each _habitRPG.chat as :message} - - {/} -
      -
      -
      - diff --git a/views/app/groups.html b/views/app/groups.html index 7c7880fa6f..300d3c7c2f 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -1,11 +1,87 @@ - - {#if _party.members} -
      -
      -

      {{_party.name}}

      + + + +
      +
      + {#if _party.id} + + {else if _user.party.invitation} + +

      You're Invited To {_party.name}

      + Accept + Reject + {else} +

      Create A Party

      + +

      You are not in a party. You can either create one and invite friends, or if you want to join an existing party, have them enter:

      +
      {_user.id}
      +
      + {#if _partyError} +
      {_partyError}
      + {/} +
      + + +
      +
      + {/} +
      + +
      + + + {#each _guilds as :guild} +
      + +
      + {/} +
      +
      + + +
      +
      + {{#if equal(@group.id,'habitrpg')}} +
      + + + +
      +
      +
      +

      Daniel Johansson

      +
      + Welcome to the Tavern! I'm Daniel, the bar keep. If you want to rest a while (going on vacation? sudden illness?), I'll set you up at the inn - dailies won't hurt you while you're resting. Stay a while & meet the locals. +
      +
      +
      +
      +
      +
      Whilst resting your dailies are saved and aren't effected by day turn-over. Whether you check out tomorrow or in a weeks time you'll continue in the same state as when you checked in.
      + + + {{else}} +

      {{@group.name}}

      - {{#each _party.members as :memberId}} - + {{#each @group.members as :memberId}} + {{/}}
      {{username(_party[:memberId].auth, _party[:memberId].profile.name)}}({{:memberId}})
      {{username(_members[:memberId].auth, _members[:memberId].profile.name)}}({{:memberId}})
      @@ -18,44 +94,45 @@
      Leave -
      -
      -

      Chat

      -
      -
      - -
      -
        - {#each _party.chat as :message} - - {/} -
      -
      + {{/}} +
      +
      - {else if _user.party.invitation} - -

      You're Invited To {_party.name}

      - Accept - Reject + {{#if equal(@group.id,'habitrpg')}} +

      Tavern Talk & LFG

      +
      + +
      + +
      +
      + {{else}} +

      Chat

      + + {{/}} - {else} -

      Create A Party

      - -

      You are not in a party. You can either create one and invite friends, or if you want to join an existing party, have them enter:

      -
      {_user.id}
      -
      - {#if _partyError} -
      {_partyError}
      - {/} -
      - - -
      -
      - - {/} +
        + {#each @group.chat as :message} + + {/} +
      +
      +
      + + {{#with @group as :group}} +
      +
      + +
      + {{/}}
    • From 619b92563ea8a5db5311e7b4db1e9063b934949d Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 26 May 2013 20:25:15 +0100 Subject: [PATCH 018/157] guilds: rename party.coffee => groups.coffee --- src/app/{party.coffee => groups.coffee} | 0 src/app/index.coffee | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/app/{party.coffee => groups.coffee} (100%) diff --git a/src/app/party.coffee b/src/app/groups.coffee similarity index 100% rename from src/app/party.coffee rename to src/app/groups.coffee diff --git a/src/app/index.coffee b/src/app/index.coffee index 2e8bc783e1..f26604ee76 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -140,7 +140,7 @@ ready (model) -> require('./character').app(exports, model) require('./tasks').app(exports, model) require('./items').app(exports, model) - require('./party').app(exports, model, app) + require('./groups').app(exports, model, app) require('./profile').app(exports, model) require('./pets').app(exports, model) require('../server/private').app(exports, model) From ff35cbf1e782e132bd2edf6d4091ace724abf43e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 26 May 2013 21:32:21 +0100 Subject: [PATCH 019/157] guilds: generalize more groups stuff, creation invitation leaving, etc. also fixes party invite bugs --- src/app/groups.coffee | 78 +++++++++++++++++------------------------ src/server/store.coffee | 5 +-- views/app/groups.html | 18 +++++----- 3 files changed, 45 insertions(+), 56 deletions(-) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index 05fd657967..d2d3989324 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -6,14 +6,9 @@ module.exports.app = (appExports, model, app) -> browser = require './browser' _currentTime = model.at '_currentTime' - - _currentTime.setNull +new Date() - - # Every 60 seconds, reset the current time so that the chat - # can update relative times - setInterval -> - _currentTime.set +new Date() - , 60000 + _currentTime.setNull +new Date + # Every 60 seconds, reset the current time so that the chat can update relative times + setInterval (->_currentTime.set +new Date), 60000 user = model.at('_user') @@ -25,60 +20,51 @@ module.exports.app = (appExports, model, app) -> model.ref '_party', party browser.resetDom(model) - appExports.partyCreate = -> - newParty = model.get("_newParty") - id = model.add 'groups', { name: newParty, leader: user.get('id'), members: [user.get('id')], invites:[] } - user.set 'party', {current: id, invitation: null}, -> - window.location.reload true + appExports.groupCreate = (e,el) -> + model.add('groups', + name: model.get("_newGroup") + leader: user.get('id') + members: [user.get('id')] + type: $(el).attr('data-type') + , ->location.reload()) - appExports.partyInvite = -> - id = model.get('_newPartyMember').replace(/[\s"]/g, '') + appExports.groupInvite = (e,el) -> + id = model.get('_groupInvitee').replace(/[\s"]/g, '') + model.set '_groupInvitee', '' return if _.isEmpty(id) - model.query('users').publicInfo([id]).fetch (err, users) -> + model.query('users').publicInfo([id]).fetch (err, profiles) -> throw err if err - u = users.at(0).get() - if !u? - model.set "_partyError", "User with id #{id} not found." - return - else if u.party.current? or u.party.invitation? - model.set "_partyError", "User already in a party or pending invitation." - return - else + profile = profiles.at(0) + return model.set("_groupError", "User with id #{id} not found.") unless profile.get() + + invite = -> $.bootstrapGrowl "Invitation Sent." - model.set "users.#{id}.party.invitation", model.get('_party.id'), -> window.location.reload() - #model.set '_newPartyMember', '' - #partySubscribe model + model.set("users.#{id}.party.invitation", e.get('id'), ->location.reload()) + if e.get('type') is 'party' + model.query('groups').withMember(id).fetch (err,groups) -> + if profile.get('party.invitation') or !_.isEmpty(groups.get()) + return model.set("_groupError", "User already in a party or pending invitation.") + else invite() + else invite() appExports.partyAccept = -> partyId = user.get('party.invitation') - user.set 'party.invitation', null - user.set 'party.current', partyId - model.at("groups.#{partyId}.members").push user.get('id'), -> window.location.reload() -# model.query('groups').withId(partyId).fetch (err, p) -> -# members = p.get('members') -# members.push user.get('id') -# p.set 'members', members, -> -# window.location.reload true - -# partySubscribe model, -> -# p = model.at('_party') -# p.push 'members', user.get('id') + user.set 'party.invitation', null, -> + model.push("groups.#{partyId}.members", user.get('id'), ->location.reload()) appExports.partyReject = -> user.set 'party.invitation', null browser.resetDom(model) - appExports.partyLeave = -> - id = user.set 'party.current', null - party = model.at '_party' - members = party.get('members') + appExports.groupLeave = (e,el) -> + members = e.get('members') index = members.indexOf(user.get('id')) - party.remove 'members', index, 1, -> + e.at().remove 'members', index, 1, -> if members.length is 1 # # last member out, kill the party - model.del "groups.#{id}", (-> window.location.reload true) + model.del("groups.#{id}", ->location.reload()) else - window.location.reload true + location.reload() ### Chat Functionality diff --git a/src/server/store.coffee b/src/server/store.coffee index c7cc878384..62ac1024d9 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -128,8 +128,9 @@ groupSystem = (store) -> ### Find group which has member by id ### - store.query.expose "groups", "withMember", (id) -> - @where('members').contains([id]).only(['id','members']) + store.query.expose "groups", "withMember", (id, type) -> + q = @where('members').contains([id]).only(['id','members']) + q = q.where('type').equals(type) if type? store.queryAccess 'groups', 'withMember', publicAccess ### diff --git a/views/app/groups.html b/views/app/groups.html index 300d3c7c2f..db4df02678 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -18,9 +18,9 @@

      You are not in a party. You can either create one and invite friends, or if you want to join an existing party, have them enter:

      {_user.id}
      -
      - {#if _partyError} -
      {_partyError}
      + + {#if _groupError} +
      {_groupError}
      {/}
      @@ -84,16 +84,18 @@ {{username(_members[:memberId].auth, _members[:memberId].profile.name)}}({{:memberId}}) {{/}} - - {#if _partyError} -
      {_partyError}
      + {#with @group as :group} + + {#if _groupError} +
      {_groupError}
      {/}
      - +
      - Leave + Leave + {/} {{/}}
      From d4275867ccb2730ce8f24767722114514664c722 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 26 May 2013 22:26:25 +0100 Subject: [PATCH 020/157] guilds: html & store.coffee bug fixes --- src/server/store.coffee | 3 ++- views/app/groups.html | 42 ++++++++++++++++++++++++----------------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/server/store.coffee b/src/server/store.coffee index 62ac1024d9..5219ec7133 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -113,6 +113,7 @@ groupSystem = (store) -> Fetch group info (ie, they just got invited) ### store.query.expose "groups", "withIds", (ids) -> + return unless ids #FIXME this is sometimes null when ids is array (guilds) if typeof ids is 'string' @where("id").equals(ids).findOne() # find a single group else @@ -129,7 +130,7 @@ groupSystem = (store) -> Find group which has member by id ### store.query.expose "groups", "withMember", (id, type) -> - q = @where('members').contains([id]).only(['id','members']) + q = @where('members').contains([id]).only(['id','members','type']) q = q.where('type').equals(type) if type? store.queryAccess 'groups', 'withMember', publicAccess diff --git a/views/app/groups.html b/views/app/groups.html index db4df02678..ff73e768ab 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -18,33 +18,41 @@

      You are not in a party. You can either create one and invite friends, or if you want to join an existing party, have them enter:

      {_user.id}
      -
      - {#if _groupError} -
      {_groupError}
      - {/} -
      - - -
      -
      + {/}
    • - - {#each _guilds as :guild} -
      - +
      +
      +
      - {/} + {#each _guilds as :guild} +
      + +
      + {/} +
      + +
      + {#if _groupError} +
      {_groupError}
      + {/} +
      + + +
      +
      +
      From d3dd20cdf8784dd2a81d76b4cbbe68d582edbd43 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 26 May 2013 22:36:30 +0100 Subject: [PATCH 021/157] guilds: allow clicking guild-members for stats. needed to include _membersArray in model ref --- src/app/index.coffee | 1 + views/app/avatar.html | 6 +++--- views/app/groups.html | 6 +++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/app/index.coffee b/src/app/index.coffee index f26604ee76..23552b805a 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -104,6 +104,7 @@ setupSubscriptions = (page, model, params, next, cb) -> # we need _members as an object in the view, so we can iterate over _party.members as :id, and access _members[:id] for the info mObj = members.get() model.set "_members", _.object(_.pluck(mObj,'id'), mObj) + model.set "_membersArray", mObj # Note - selfQ *must* come after membersQ in subscribe, otherwise _user will only get the fields restricted by party-members in store.coffee. Strang bug, but easy to get around partyQ = model.query('groups').withIds(groupsInfo.partyId) diff --git a/views/app/avatar.html b/views/app/avatar.html index b3a0c79619..e567203114 100644 --- a/views/app/avatar.html +++ b/views/app/avatar.html @@ -1,7 +1,7 @@ - {{#each _party.members as :memberId}} - - + {{#each _membersArray as :member}} + + <@footer> diff --git a/views/app/groups.html b/views/app/groups.html index ff73e768ab..57a7dbeb70 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -89,7 +89,11 @@

      {{@group.name}}

      {{#each @group.members as :memberId}} - + {{/}}
      {{username(_members[:memberId].auth, _members[:memberId].profile.name)}}({{:memberId}})
      + + {{username(_members[:memberId].auth, _members[:memberId].profile.name)}}({{:memberId}}) + +
      {#with @group as :group} From 67643f796ac15fca396c2a40c28dfa7ad2166c42 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 27 May 2013 15:14:21 +0100 Subject: [PATCH 022/157] challenges: underscore => lodash --- src/app/challenges.coffee | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index 161167b216..4e742e85e4 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -1,5 +1,4 @@ -_ = require 'underscore' -lodash = require 'lodash' +_ = require 'lodash' helpers = require 'habitrpg-shared/script/helpers' module.exports.app = (appExports, model) -> @@ -34,7 +33,7 @@ module.exports.app = (appExports, model) -> # Add challenge name as a tag for user tags = user.get('tags') - unless tags and _.findWhere(tags,{id: chal.id}) + unless tags and _.find(tags,{id: chal.id}) model.push('_user.tags', {id: chal.id, name: chal.name}) tags = {}; tags[chal.id] = true @@ -46,6 +45,7 @@ module.exports.app = (appExports, model) -> task.tags = tags task.challenge = chal.id model.push("_#{type}List", task) + true appExports.challengeUnsubscribe = (e) -> chal = e.get() @@ -53,8 +53,9 @@ module.exports.app = (appExports, model) -> user.remove("challenges.#{i}") if i? and i != -1 _.each ['habit', 'daily', 'todo', 'reward'], (type) -> _.each chal["#{type}s"], (task) -> - model.remove "_#{type}List", lodash.findIndex(model.get("_#{type}List",{id:task.id})) + model.remove "_#{type}List", _.findIndex(model.get("_#{type}List",{id:task.id})) model.del "_user.tasks.#{task.id}" + true appExports.challengeCollapse = (e, el) -> $(el).next().toggle() From 695bb301f4489961f1f0aff0edfc150295bcde87 Mon Sep 17 00:00:00 2001 From: Mickael Date: Mon, 27 May 2013 18:40:26 +0100 Subject: [PATCH 023/157] Adding a QRCode with the user's information QRCode retrieved from Google APIs, and parsed with json like that: {address:"",user:"",key:""} --- views/app/settings.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/views/app/settings.html b/views/app/settings.html index 2d46a63301..7da782b3fd 100644 --- a/views/app/settings.html +++ b/views/app/settings.html @@ -35,6 +35,11 @@
      API Token
      {_user.apiToken}
      + +
      QR Code
      + qrcode +
      From 4d6ca749af995934ad3fec9548bacce6d34155cc Mon Sep 17 00:00:00 2001 From: Mickael Date: Mon, 27 May 2013 20:52:09 +0100 Subject: [PATCH 024/157] double bracket on user id and token for the qrcode --- 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 7da782b3fd..104614fd3a 100644 --- a/views/app/settings.html +++ b/views/app/settings.html @@ -38,7 +38,7 @@
      QR Code
      qrcode + %7Baddress%3A%22https%3A%2F%2Fhabitrpg.com%22%2Cuser%3A%22{{_user.id}}%22%2Ckey%3A%22{{_user.apiToken}}%22%7D,&choe=UTF-8&chld=L' alt="qrcode"/> From e9eb509682d9698022451e95d3ea1c100261f785 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 27 May 2013 21:14:04 +0100 Subject: [PATCH 025/157] challenges: generalized listings for groups/parties/public --- src/app/challenges.coffee | 8 +++- views/app/challenges.html | 99 ++++++++++++++++++++++----------------- 2 files changed, 63 insertions(+), 44 deletions(-) diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index 4e742e85e4..73927ccef7 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -16,12 +16,18 @@ module.exports.app = (appExports, model) -> id: model.id() uuid: user.get('id') user: helpers.username(model.get('_user.auth'), model.get('_user.profile.name')) + # FIXME group is a stop-gap since derby's not picking up the initial select option `selected={}` until it's changed + group: type:'party', id:model.get('_guilds.0.id') timestamp: +new Date model.set '_challenge.creating', true appExports.challengeSave = -> - model.unshift '_party.challenges', model.get('_challenge.new'), -> challengeDiscard() + gid = + if model.get('_challenge.new.group.type') is 'party' then model.get('_party.id') + else model.get('_challenge.new.group.id') + debugger + model.unshift "groups.#{gid}.challenges", model.get('_challenge.new'), challengeDiscard browser.growlNotification('Challenge Created','success') appExports.challengeDiscard = challengeDiscard = -> diff --git a/views/app/challenges.html b/views/app/challenges.html index eb3171e8ac..1f631fbb31 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -33,37 +33,46 @@
      {#each _party.challenges as :challenge} -
      - -

      {:challenge.name} (by {:challenge.user})

      - - -
      - -
      -
      -
      + {/}
      - Guild + {#each _guilds as :guild} +

      {:guild.name}

      + {#each :guild.challenges as :challenge} + + {/} +
      + {/}
      - Public + {#each _habitRPG.challenges as :challenge} + + {/}
      + +
      + +

      {@challenge.name} (by {@challenge.user})

      + +
      + +
      +
      +
      {#unless _challenge.creating} @@ -86,34 +95,38 @@
      - {#if equal(_challenge.new.assignTo,'Party')} -
      -
      -
      All Party
      - No individual privacy on the challenge, all party members can see progress even if they decline the challenge. Any new party members can subscribe to this challenge. -
      -
      -
      Individual Members
      -
      - -
      -
      Only the invited party members can subscribe to this challenge. New party joins won't see this challenge.
      -
      -
      + + + + + + + + + + + + + + + + + + - {/} - {#if equal(_challenge.new.assignTo,'Guild')} - Which Guild? + + {#if equal(_challenge.new.group.type,'guild')} + {/}
      From fa9e6f5453a9b9d185f5e3dd8a9ab1e207b35be7 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 27 May 2013 21:14:22 +0100 Subject: [PATCH 026/157] challenges: migrated drop groups collection if exists (mostly for debugging) --- migrations/20130518_setup_groups.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/20130518_setup_groups.js b/migrations/20130518_setup_groups.js index 638d882295..85f3bbb431 100644 --- a/migrations/20130518_setup_groups.js +++ b/migrations/20130518_setup_groups.js @@ -3,7 +3,7 @@ * into one collection: groups, with group.type either 'party' or 'guild'. We are also creating the 'habitrpg' guild, * which everyone is auto-subscribed to, and moving tavern chat into that guild * - * mongo habitrpg ./node_modules/underscore/underscore.js ./migrations/20130518_setup_groups.js + * mongo habitrpg ./node_modules/lodash/lodash.js ./migrations/20130518_setup_groups.js */ /** @@ -15,7 +15,7 @@ * 5) subscribe everyone to habitrpg (be sure to set that for default user too!) */ -db.parties.renameCollection('groups'); +db.parties.renameCollection('groups',true); //db.parties.dropCollection(); // doesn't seem to do this step during rename... //db.parties.ensureIndex( { 'members': 1, 'background': 1} ); From 2666ebb1fc06ecb4630f693d3554931d90d1c401 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 28 May 2013 00:46:51 +0100 Subject: [PATCH 027/157] guilds: generic handling of invitations (incl. accept, reject, invite, etc) --- migrations/20130518_setup_groups.js | 10 ++++ src/app/groups.coffee | 71 +++++++++++++++++------------ src/app/index.coffee | 2 +- src/server/store.coffee | 4 +- views/app/challenges.html | 2 +- views/app/groups.html | 17 +++++-- 6 files changed, 70 insertions(+), 36 deletions(-) diff --git a/migrations/20130518_setup_groups.js b/migrations/20130518_setup_groups.js index 85f3bbb431..0e04bfa1b9 100644 --- a/migrations/20130518_setup_groups.js +++ b/migrations/20130518_setup_groups.js @@ -21,6 +21,16 @@ db.parties.renameCollection('groups',true); db.groups.update({}, {$set:{type:'party'}}, {multi:true}); +//migrate invitation mechanisms +db.users.update( + {}, + { + $remove:{party:1}, + $set:{invitations:{party:null,guilds:[]}} + }, + {multi:1} +); + tavern = db.tavern.findOne(); db.tavern.drop(); diff --git a/src/app/groups.coffee b/src/app/groups.coffee index d2d3989324..87242fc59b 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -12,14 +12,6 @@ module.exports.app = (appExports, model, app) -> user = model.at('_user') - model.on 'set', '_user.party.invitation', (after, before) -> - if !before? and after? # they just got invited - partyQ = model.query('groups').withId(after) - partyQ.fetch (err, party) -> - return next(err) if err - model.ref '_party', party - browser.resetDom(model) - appExports.groupCreate = (e,el) -> model.add('groups', name: model.get("_newGroup") @@ -29,33 +21,56 @@ module.exports.app = (appExports, model, app) -> , ->location.reload()) appExports.groupInvite = (e,el) -> - id = model.get('_groupInvitee').replace(/[\s"]/g, '') + uid = model.get('_groupInvitee').replace(/[\s"]/g, '') model.set '_groupInvitee', '' - return if _.isEmpty(id) + return if _.isEmpty(uid) - model.query('users').publicInfo([id]).fetch (err, profiles) -> + model.query('users').publicInfo([uid]).fetch (err, profiles) -> throw err if err - profile = profiles.at(0) - return model.set("_groupError", "User with id #{id} not found.") unless profile.get() + profile = profiles.at(0).get() + return model.set("_groupError", "User with id #{uid} not found.") unless profile + model.query('groups').withMember(uid).fetch (err, g) -> + throw err if err - invite = -> - $.bootstrapGrowl "Invitation Sent." - model.set("users.#{id}.party.invitation", e.get('id'), ->location.reload()) - if e.get('type') is 'party' - model.query('groups').withMember(id).fetch (err,groups) -> - if profile.get('party.invitation') or !_.isEmpty(groups.get()) - return model.set("_groupError", "User already in a party or pending invitation.") + {type, name} = e.get() + gid = e.get('id') + groups = g.get() + groupError =(msg) -> model.set("_groupError", msg) + invite = -> + debugger + $.bootstrapGrowl "Invitation Sent." + if type is 'guild' + model.push("users.#{uid}.invitations.guilds", {id:gid, name}, ->location.reload()) + else model.set "users.#{uid}.invitations.party", {id:gid, name}, -> + debugger + location.reload() + + if type is 'guild' + if _.find(profile.invitations.guilds, {id:gid}) + return groupError("User already invited to that group") + else if _.find groups, ((group)-> uid in group.members) + return groupError("User already in that group") else invite() - else invite() + if type is 'party' + if profile.invitations.party + return groupError("User already pending invitation.") + else if _.find(groups, {type:'party'}) + return groupError("User already in a party.") + else invite() - appExports.partyAccept = -> - partyId = user.get('party.invitation') - user.set 'party.invitation', null, -> - model.push("groups.#{partyId}.members", user.get('id'), ->location.reload()) + appExports.acceptInvitation = (e,el) -> + group = e.at().get() + pushMember = -> model.push("groups.#{group.id}.members", user.get('id'), ->location.reload()) + if $(el).attr('data-type') is 'party' + user.set 'invitations.party', null, pushMember + else + e.at().remove pushMember - appExports.partyReject = -> - user.set 'party.invitation', null - browser.resetDom(model) + appExports.rejectInvitation = (e, el) -> + clear = -> browser.resetDom(model) + if e.at().path().indexOf('party') != -1 + model.del e.at().path(), clear + else e.at().remove clear appExports.groupLeave = (e,el) -> members = e.get('members') diff --git a/src/app/index.coffee b/src/app/index.coffee index dcfdb9856b..f83c9dd18a 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -88,7 +88,7 @@ setupSubscriptions = (page, model, params, next, cb) -> groupsObj = groups.get() # (1) Solo player - return finished([selfQ, "groups.habitrpg"], ['_user', '_habitRPG']) if _.isEmpty(groupsObj) + return finished(["groups.habitrpg", selfQ], ['_habitRPG', '_user']) if _.isEmpty(groupsObj) ## (2) Party or Guild has members, fetch those users too # Subscribe to the groups themselves. We separate them by _party, _guilds, and _habitRPG (the "global" guild). diff --git a/src/server/store.coffee b/src/server/store.coffee index 5219ec7133..5d93b2dcf4 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -48,7 +48,7 @@ userAccess = (store) -> return accept(false) # we can only manually set this stuff in the database # public access to users.*.party.invitation (TODO, lock down a bit more) - if attrPath is 'party.invitation' + if attrPath.indexOf('invitations.') is 0 return accept(true) # Same session (user.id = this.session.userId) @@ -100,7 +100,7 @@ groupSystem = (store) -> @where("id").within(ids) .only('stats', 'items', - 'party', + 'invitations', 'profile', 'achievements', 'backer', diff --git a/views/app/challenges.html b/views/app/challenges.html index 1f631fbb31..a6bb089394 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -49,7 +49,7 @@
      {#each _habitRPG.challenges as :challenge} - + {/}
      diff --git a/views/app/groups.html b/views/app/groups.html index 57a7dbeb70..5870afe288 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -8,11 +8,13 @@
      {#if _party.id} - {else if _user.party.invitation} + {else if _user.invitations.party} -

      You're Invited To {_party.name}

      - Accept - Reject +

      You're Invited To {_user.invitations.party.name}

      + {#with _user.invitations.party} + Accept + Reject + {/} {else}

      Create A Party

      @@ -30,9 +32,16 @@ {/}
    +
    + {#each _user.invitations.guilds as :invitation} +

    You're Invited To {:invitation.name}

    + Accept + Reject + {/}
    + {#each _guilds as :guild}
    From 2fbff2a0bc839e18e204fcffe438ee687f3b1eae Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 28 May 2013 11:52:32 +0100 Subject: [PATCH 028/157] challenges: lock down editing capabilities on certain task properties, instead showing the challenge's properties (in case they change challenge-side).This includes editing the tag name --- src/app/challenges.coffee | 3 ++- src/app/misc.coffee | 19 +++++++++++++++- src/app/tasks.coffee | 5 ++--- views/app/filters.html | 2 +- views/app/tasks.html | 46 ++++++++++++++++++++++++++++----------- 5 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index 73927ccef7..3ef9b3e910 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -40,7 +40,7 @@ module.exports.app = (appExports, model) -> # Add challenge name as a tag for user tags = user.get('tags') unless tags and _.find(tags,{id: chal.id}) - model.push('_user.tags', {id: chal.id, name: chal.name}) + model.push '_user.tags', {id: chal.id, name: chal.name, challenge: true} tags = {}; tags[chal.id] = true # Add all challenge's tasks to user's tasks @@ -50,6 +50,7 @@ module.exports.app = (appExports, model) -> _.each chal["#{type}s"], (task) -> task.tags = tags task.challenge = chal.id + task.group = {id: chal.group.id, type: chal.group.type} model.push("_#{type}List", task) true diff --git a/src/app/misc.coffee b/src/app/misc.coffee index 735addb804..30a4246368 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -101,4 +101,21 @@ module.exports.viewHelpers = (view) -> #Tags view.fn 'noTags', helpers.noTags - view.fn 'appliedTags', helpers.appliedTags \ No newline at end of file + view.fn 'appliedTags', helpers.appliedTags + + #Challenges + view.fn 'taskAttrFromChallenge', (task, attr) -> + [tid, gid, cid, tType, gType] = [task.id, task.group.id, task.challenge, task.type, task.group.type] + findAttr = (challenges) -> + challenge = _.find(challenges,{id:cid}) + val = _.find(challenge["#{tType}s"],{id:tid})[attr] + if attr is 'priority' + val = switch val + when '!!!' then 'Hard' + when '!!' then 'Medium' + else 'Easy' + return val + if gType is 'party' + findAttr @model.get("_party.challenges") + else if gType is 'guild' + findAttr _.find(@model.get("_guilds"),{id:gid}).challenges \ No newline at end of file diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index 8fa4ebe27d..8023bdc636 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -19,9 +19,8 @@ module.exports.app = (appExports, model) -> # Don't add a blank task; 20/02/13 Added a check for undefined value, more at issue #463 -lancemanfv return if /^(\s)*$/.test(text) || text == undefined - newTask = {id: model.id(), type: type, text: text, notes: '', value: 0, tags:{}} - isChallenge = e.at().path().indexOf('_challenge.new') != -1 - newTask.tags = if isChallenge then {} else _.reduce user.get('filters'), ((memo,v,k) -> memo[k]=v if v;memo), {} + newTask = {id: model.id(), type, text, notes: '', value: 0} + newTask.tags = _.reduce user.get('filters'), ((memo,v,k) -> memo[k]=v if v; memo), {} switch type when 'habit' diff --git a/views/app/filters.html b/views/app/filters.html index 1a04d02a3a..b2e58dc26e 100644 --- a/views/app/filters.html +++ b/views/app/filters.html @@ -9,7 +9,7 @@ {#each _user.tags as :tag}
  • - {#if _editingTags} + {#if and(_editingTags,not(:tag.challenge))}
    diff --git a/views/app/tasks.html b/views/app/tasks.html index 656a3fb6d2..bb361ce32f 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -175,11 +175,13 @@ - {{#if :task.challenge}}{{/}} - - + {{#if :task.challenge}} + + {{else}} + + + {{/}} - {#if :task.history} {/} @@ -231,7 +233,11 @@

    - {:task.text} + {{#if :task.challenge}} + {{taskAttrFromChallenge(:task,'text')}} + {{else}} + {:task.text} + {{/}}

    @@ -244,12 +250,19 @@
    - - + {{#unless :task.challenge}} + + {{/}} + + {{#if :task.challenge}} + + {{else}} + + {{/}}
    - {#if equal(:task.type, 'habit')} + {#if and(equal(:task.type, 'habit'),not(:task.challenge))}
    Direction/Actions @@ -304,11 +317,18 @@

    Advanced Options

    Difficulty -
    - - - -
    + + {{#if :task.challenge}} + + {{else}} +
    + + + +
    + {{/}} {{#if equal(:task.type,'daily')}} Restore Streak From 21a6ea19f536dbd8258436a4db681f656fae8060 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 28 May 2013 11:59:09 +0100 Subject: [PATCH 029/157] challenges: remove "mine" challenges tab --- views/app/challenges.html | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/views/app/challenges.html b/views/app/challenges.html index a6bb089394..0fe7584b39 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -19,19 +19,14 @@
    -
    - Mine -
    - -
    +
    {#each _party.challenges as :challenge} {/} From 60bdc3666e9901e83656757445ad6422e2673bf6 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 28 May 2013 11:59:19 +0100 Subject: [PATCH 030/157] challenges: fix public-challnege creation --- src/app/challenges.coffee | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index 3ef9b3e910..ffffd39729 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -12,9 +12,8 @@ module.exports.app = (appExports, model) -> dailys: [] todos: [] rewards: [] - assignTo: 'Party' id: model.id() - uuid: user.get('id') + uid: user.get('id') user: helpers.username(model.get('_user.auth'), model.get('_user.profile.name')) # FIXME group is a stop-gap since derby's not picking up the initial select option `selected={}` until it's changed group: type:'party', id:model.get('_guilds.0.id') @@ -23,10 +22,10 @@ module.exports.app = (appExports, model) -> model.set '_challenge.creating', true appExports.challengeSave = -> - gid = - if model.get('_challenge.new.group.type') is 'party' then model.get('_party.id') - else model.get('_challenge.new.group.id') - debugger + gid = switch model.get('_challenge.new.group.type') + when 'party' then model.get('_party.id') + when 'guild' then model.get('_challenge.new.group.id') + when 'public' then 'habitrpg' model.unshift "groups.#{gid}.challenges", model.get('_challenge.new'), challengeDiscard browser.growlNotification('Challenge Created','success') From 12fccbc99d03af2f9b6242019097c4f3c8eb6d6a Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 28 May 2013 12:32:57 +0100 Subject: [PATCH 031/157] challenges: move challenge-creation to their respective tabs, instead of separate "create" tab --- src/app/challenges.coffee | 25 +++--- views/app/challenges.html | 175 +++++++++++++++++++------------------- 2 files changed, 96 insertions(+), 104 deletions(-) diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index ffffd39729..011567e340 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -5,7 +5,8 @@ module.exports.app = (appExports, model) -> browser = require './browser' user = model.at '_user' - appExports.challengeCreate = -> + appExports.challengeCreate = (e,el) -> + [type, gid] = [$(el).attr('data-type'), $(el).attr('data-gid')] model.set '_challenge.new', name: '' habits: [] @@ -15,23 +16,17 @@ module.exports.app = (appExports, model) -> id: model.id() uid: user.get('id') user: helpers.username(model.get('_user.auth'), model.get('_user.profile.name')) - # FIXME group is a stop-gap since derby's not picking up the initial select option `selected={}` until it's changed - group: type:'party', id:model.get('_guilds.0.id') + group: {type, id:gid} timestamp: +new Date - model.set '_challenge.creating', true - appExports.challengeSave = -> - gid = switch model.get('_challenge.new.group.type') - when 'party' then model.get('_party.id') - when 'guild' then model.get('_challenge.new.group.id') - when 'public' then 'habitrpg' - model.unshift "groups.#{gid}.challenges", model.get('_challenge.new'), challengeDiscard - browser.growlNotification('Challenge Created','success') + gid = model.get('_challenge.new.group.id') + debugger + model.unshift "groups.#{gid}.challenges", model.get('_challenge.new'), -> + browser.growlNotification('Challenge Created','success') + challengeDiscard() - appExports.challengeDiscard = challengeDiscard = -> - model.set '_challenge.new', {} - model.set '_challenge.creating', false + appExports.challengeDiscard = challengeDiscard = -> model.del '_challenge.new' appExports.challengeSubscribe = (e) -> chal = e.get() @@ -65,4 +60,4 @@ module.exports.app = (appExports, model) -> appExports.challengeCollapse = (e, el) -> $(el).next().toggle() - i = $(el).find('i').toggleClass 'icon-chevron-down' \ No newline at end of file + i = $(el).find('i').toggleClass('icon-chevron-right icon-chevron-down') \ No newline at end of file diff --git a/views/app/challenges.html b/views/app/challenges.html index 0fe7584b39..3c9c1f9a76 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -1,24 +1,6 @@ - - - -
    - -
    - -
    - -
    - -
    -
    - -
    -
    + {/} + {#if equal(_challenge.new.group.type,'guild')} + + {/} +
    + --> - - - - - - {/} + +
    +
    + From 21b6cd952d0ff175d8d5be502931e47fdd6bd01a Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 28 May 2013 12:47:41 +0100 Subject: [PATCH 032/157] challenges: guild listings in tabs, just like guilds-groups --- views/app/challenges.html | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/views/app/challenges.html b/views/app/challenges.html index 3c9c1f9a76..5229a8b412 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -20,18 +20,26 @@
  • - {{#each _guilds as :guild}} -

    {:guild.name}

    - {#if _challenge.new} - - {else} - - {#each :guild.challenges as :challenge} - - {/} -
    + +
    + {{#each _guilds as :guild}} +
    + {#if _challenge.new} + + {else} + + {#each :guild.challenges as :challenge} + + {/} +
    + {/} +
    {/} - {{/}} +
    From bd1866742036f2018ffb2705f2fb33dd69b41bf1 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 28 May 2013 16:43:16 +0100 Subject: [PATCH 033/157] convert task-editing state to private path so we don't have conflicting ids --- src/app/tasks.coffee | 35 ++++++++++++++++++++--------------- views/app/tasks.html | 14 +++++++------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index 8023bdc636..a0efc2d088 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -76,31 +76,36 @@ module.exports.app = (appExports, model) -> task.set('repeat.' + $(el).attr('data-day'), true) appExports.toggleTaskEdit = (e, el) -> - hideId = $(el).attr('data-hide-id') - toggleId = $(el).attr('data-toggle-id') - $(document.getElementById(hideId)).addClass('visuallyhidden') - $(document.getElementById(toggleId)).toggleClass('visuallyhidden') + id = e.get('id') + path = "_tasks.editing.#{id}" + model.set path, !model.get(path) + $(".#{id}-chart").hide() appExports.toggleChart = (e, el) -> - hideSelector = $(el).attr('data-hide-id') - chartSelector = $(el).attr('data-toggle-id') - historyPath = $(el).attr('data-history-path') - $(document.getElementById(hideSelector)).hide() - $(document.getElementById(chartSelector)).toggle() + id = $(el).attr('data-id') + history = [] + + if id is 'todos' + model.set "_tasks.charts.todos", !model.get("_tasks.charts.todos") + history = model.get("_user.history.todos") + $(".#{id}-chart").toggle() + else + [id, path] = [$(el).attr('data-id'), "_tasks.charts.#{id}"] + model.set path, !model.get(path) + model.set "_tasks.editing.#{id}", false + $(".#{id}-chart").toggle() + history = model.get("_user.tasks.#{id}.history") matrix = [['Date', 'Score']] - for obj in model.get(historyPath) + for obj in history date = +new Date(obj.date) readableDate = moment(date).format('MM/DD') matrix.push [ readableDate, obj.value ] data = google.visualization.arrayToDataTable matrix - - options = { + options = title: 'History' backgroundColor: { fill:'transparent' } - } - - chart = new google.visualization.LineChart(document.getElementById( chartSelector )) + chart = new google.visualization.LineChart $(".#{id}-chart")[0] chart.draw(data, options) appExports.todosShowRemaining = -> model.set '_showCompleted', false diff --git a/views/app/tasks.html b/views/app/tasks.html index bb361ce32f..e30465aa90 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -53,7 +53,7 @@ {#if _user.history.todos} - + {/} @@ -132,7 +132,7 @@

    {{t(@header)}}

    - {{#if equal(@type,'todo')}}{{/}} + {{#if equal(@type,'todo')}}{{/}} {{#if @editable}} @@ -173,7 +173,7 @@
    - + {{#if :task.challenge}} @@ -183,7 +183,7 @@ {{/}} {#if :task.history} - + {/} {#if :task.notes} @@ -246,8 +246,8 @@ -
    -
    +
    +
    {{#unless :task.challenge}} @@ -342,4 +342,4 @@
    - + From cce03c8312630cb57d271482aa932695747b6e2c Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 29 May 2013 18:19:38 +0100 Subject: [PATCH 034/157] guilds: prettier group-creation --- views/app/groups.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/views/app/groups.html b/views/app/groups.html index 5870afe288..187787c696 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -56,8 +56,8 @@ {#if _groupError}
    {_groupError}
    {/} -
    - +
    +
    From 838e81fd583909e3c2f019116eda4a3cc7532163 Mon Sep 17 00:00:00 2001 From: zeroos <232002+github@gmail.com> Date: Wed, 29 May 2013 19:34:01 +0200 Subject: [PATCH 035/157] Fixed bug related to padding of addtask-field The field was too wide and because of that the text from the end of the input was below "+" button. --- styles/app/tasks.styl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/app/tasks.styl b/styles/app/tasks.styl index 08db9535e9..43ba1821a0 100644 --- a/styles/app/tasks.styl +++ b/styles/app/tasks.styl @@ -71,7 +71,7 @@ for $stage in $stages box-shadow: none background-color: white height: 3em - padding: 0 0 0 0.5em + padding: 0 3.3em 0 0.5em width: 100% &:focus box-shadow: inset 0 0 3px darken($best, 20%),inset -1px 0 1px darken($best, 30%) From dbdb4911af6c6b4362556f7a8c4205d7ba90d48b Mon Sep 17 00:00:00 2001 From: zeroos <232002+git@gmail.com> Date: Wed, 29 May 2013 20:00:17 +0200 Subject: [PATCH 036/157] Breaking long words in task-text Previously, if you inputted a long word in your task it overflew the box. --- styles/app/tasks.styl | 1 + 1 file changed, 1 insertion(+) diff --git a/styles/app/tasks.styl b/styles/app/tasks.styl index 43ba1821a0..7a9be98804 100644 --- a/styles/app/tasks.styl +++ b/styles/app/tasks.styl @@ -117,6 +117,7 @@ for $stage in $stages display: block padding: 0.75em 0 0.75em 3.5em line-height: 1.4 + word-wrap: break-word .habit-wide .task-text padding-left: 7em From 88ff245aecd9d61a99622d38af013a17dd027070 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 29 May 2013 18:57:50 +0100 Subject: [PATCH 037/157] challenges: add taskInChallenge() view helper and broken task.challenge link --- src/app/misc.coffee | 35 ++++++++++++++++++++--------------- views/app/tasks.html | 20 ++++++++++++++------ 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index 268bd5796a..5ae183c5dc 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -116,19 +116,24 @@ module.exports.viewHelpers = (view) -> view.fn 'noTags', helpers.noTags view.fn 'appliedTags', helpers.appliedTags - #Challenges - view.fn 'taskAttrFromChallenge', (task, attr) -> - [tid, gid, cid, tType, gType] = [task.id, task.group.id, task.challenge, task.type, task.group.type] - findAttr = (challenges) -> + #TODO put this in habitrpg-shared + taskInChallenge = (task) -> + return false unless task?.challenge + [gid, cid, gType] = [task.group.id, task.challenge, task.group.type] + getTask = (challenges) -> challenge = _.find(challenges,{id:cid}) - val = _.find(challenge["#{tType}s"],{id:tid})[attr] - if attr is 'priority' - val = switch val - when '!!!' then 'Hard' - when '!!' then 'Medium' - else 'Easy' - return val - if gType is 'party' - findAttr @model.get("_party.challenges") - else if gType is 'guild' - findAttr _.find(@model.get("_guilds"),{id:gid}).challenges + challenge and _.find(challenge["#{task.type}s"],{id:task.id}) + switch gType + when 'party' + party = @model.get('_party') + return party?.challenges and getTask(party.challenges) + when 'guild' + guilds = @model.get("_guilds") + return guilds and getTask(_.find(guilds,{id:gid}).challenges) + + #Challenges + view.fn 'taskInChallenge', taskInChallenge + view.fn 'taskAttrFromChallenge', (task, attr) -> + t = taskInChallenge(task) + t and t[attr] + view.fn 'brokenChallengeLink', (task) -> task?.challenge and !taskInChallenge.call(@,task) diff --git a/views/app/tasks.html b/views/app/tasks.html index e30465aa90..a4d5e58613 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -176,7 +176,9 @@ {{#if :task.challenge}} - + {{#if brokenChallengeLink(:task)}} + + {{/}} {{else}} @@ -233,7 +235,7 @@

    - {{#if :task.challenge}} + {{#if taskInChallenge(:task)}} {{taskAttrFromChallenge(:task,'text')}} {{else}} {:task.text} @@ -247,14 +249,20 @@

    + {{#if brokenChallengeLink(:task)}} +
    +

    Broken Challenge Link: this task was part of a challenge, but (a) challenge (or containing group) has been deleted, or (b) the task was deleted from the challenge.

    +

    Keep | Keep all from challenge | Delete | Delete all from challenge

    +
    + {{/}}
    - {{#unless :task.challenge}} + {{#unless taskInChallenge(:task)}} {{/}} - {{#if :task.challenge}} + {{#if taskInChallenge(:task)}} {{else}} @@ -262,7 +270,7 @@
    - {#if and(equal(:task.type, 'habit'),not(:task.challenge))} + {#if and(equal(:task.type, 'habit'),not(taskInChallenge(:task)))}
    Direction/Actions @@ -318,7 +326,7 @@
    Difficulty - {{#if :task.challenge}} + {{#if taskInChallenge(:task)}} From a213d6e9971586b5921b8d2c5763b665fb2a8d11 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 29 May 2013 22:47:07 +0100 Subject: [PATCH 038/157] groups: accordion information on left side, more edit options for group information --- src/app/groups.coffee | 9 +++ src/app/index.coffee | 2 + src/app/misc.coffee | 10 ++-- views/app/groups.html | 126 +++++++++++++++++++++++++++++++++++------- 4 files changed, 120 insertions(+), 27 deletions(-) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index 7578e42ba1..1d74906ab2 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -19,6 +19,15 @@ module.exports.app = (appExports, model, app) -> type: $(el).attr('data-type') , ->location.reload()) + appExports.toggleGroupEdit = (e, el) -> + path = "_editing.groups.#{$(el).attr('data-gid')}" + model.set path, !model.get(path) + + appExports.groupAddWebsite = (e, el) -> + test = e.get() + e.at().unshift 'websites', model.get('_newGroupWebsite') + model.del '_newGroupWebsite' + appExports.groupInvite = (e,el) -> uid = model.get('_groupInvitee').replace(/[\s"]/g, '') model.set '_groupInvitee', '' diff --git a/src/app/index.coffee b/src/app/index.coffee index 5eabf24199..da5e3279d9 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -133,6 +133,8 @@ ready (model) -> user = model.at('_user') browser = require './browser' + exports.removeAt = (e) -> e.at().remove() # used for things like remove website, chat, etc + require('./tasks').app(exports, model) require('./items').app(exports, model) require('./groups').app(exports, model, app) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index 5ae183c5dc..8c960323ce 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -119,17 +119,15 @@ module.exports.viewHelpers = (view) -> #TODO put this in habitrpg-shared taskInChallenge = (task) -> return false unless task?.challenge - [gid, cid, gType] = [task.group.id, task.challenge, task.group.type] + [tid, gid, cid, gType] = [task.id, task.group.id, task.challenge, task.group.type] getTask = (challenges) -> challenge = _.find(challenges,{id:cid}) - challenge and _.find(challenge["#{task.type}s"],{id:task.id}) + challenge and _.find(challenge["#{task.type}s"],{id:tid}) switch gType when 'party' - party = @model.get('_party') - return party?.challenges and getTask(party.challenges) + (party = @model.get '_party') and party and (getTask party.challenges) when 'guild' - guilds = @model.get("_guilds") - return guilds and getTask(_.find(guilds,{id:gid}).challenges) + (guilds = @model.get "_guilds") and (guild = _.find guilds,{id:gid}) and guild and (getTask guild.challenges) #Challenges view.fn 'taskInChallenge', taskInChallenge diff --git a/views/app/groups.html b/views/app/groups.html index 187787c696..6b1ed5c40c 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -62,9 +62,10 @@
    +
    -
    +
    {{#if equal(@group.id,'habitrpg')}}
    @@ -95,28 +96,111 @@ {{else}} -

    {{@group.name}}

    -
    - {{#each @group.members as :memberId}} - - {{/}} -
    - - {{username(_members[:memberId].auth, _members[:memberId].profile.name)}}({{:memberId}}) - -
    - {#with @group as :group} -
    - {#if _groupError} -
    {_groupError}
    - {/} -
    - - +
    +
    + +
    +
    + {#if _editing.groups[@group.id]} +
    + +
    +
    + +
    + + + {#with @group} + + + + + {#if @group.websites} +

    Resources

    +
      + {#each @group.websites as :website} +
    • {:website}
    • + {/} +
    + {/} + {/} + {else} + {#if @group.logo}{/} + {{#if equal(@group.leader,_user.id)}} + + {{/}} +

    {@group.name}

    +
    {@group.description}
    + {/} + {#if @group.websites} +

    Resources

    +
      + {#each @group.websites as :website} +
    • {:website}
    • + {/} +
    + {/} +
    +
    - + +
    +
    + Members +
    +
    +
    + + {{#each @group.members as :memberId}} + + + + {{/}} +
    + + {{username(_members[:memberId].auth, _members[:memberId].profile.name)}} + + + ({{:memberId}}) +
    + {#with @group as :group} +
    + {#if _groupError} +
    {_groupError}
    + {/} +
    + + +
    +
    + {/} +
    +
    +
    + +
    + +
    +
    + + {{#each @group.challenges as :challenge}} + + {{/}} +
    + {{:challenge.name}} +
    +
    + +
    +
    + + +
    Leave - {/} {{/}}
    From 8ad2bf56870a0b6b86d56baaf838cf2a3d3f3704 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 29 May 2013 22:55:20 +0100 Subject: [PATCH 039/157] groups: don't show websites twice when editing --- views/app/groups.html | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/views/app/groups.html b/views/app/groups.html index 6b1ed5c40c..281cf1a846 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -117,14 +117,14 @@ - {#if @group.websites} -

    Resources

    -
      - {#each @group.websites as :website} -
    • {:website}
    • - {/} -
    {/} + {#if @group.websites} +

    Resources

    +
      + {#each @group.websites as :website} +
    • {:website}
    • + {/} +
    {/} {else} {#if @group.logo}{/} @@ -133,14 +133,14 @@ {{/}}

    {@group.name}

    {@group.description}
    - {/} - {#if @group.websites} -

    Resources

    -
      - {#each @group.websites as :website} -
    • {:website}
    • + {#if @group.websites} +

      Resources

      +
        + {#each @group.websites as :website} +
      • {:website}
      • + {/} +
      {/} -
    {/}
    From 48e302763823216d94fb566dc4cde5658642f479 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 29 May 2013 23:06:07 +0100 Subject: [PATCH 040/157] hotfix: getting a lot of "DERBY is not defined" --- src/app/browser.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/browser.coffee b/src/app/browser.coffee index 685621c993..44dcedb810 100644 --- a/src/app/browser.coffee +++ b/src/app/browser.coffee @@ -195,8 +195,8 @@ setupGrowlNotifications = (model) -> statsNotification ' Level Up!', 'lvl' module.exports.resetDom = (model) -> - DERBY.app.dom.clear() - DERBY.app.view.render(model, DERBY.app.view._lastRender.ns, DERBY.app.view._lastRender.context); + window.DERBY.app.dom.clear() + window.DERBY.app.view.render(model, window.DERBY.app.view._lastRender.ns, window.DERBY.app.view._lastRender.context); # Note, Google Analyatics giving beef if in this file. Moved back to index.html. It's ok, it's async - really the # syncronous requires up top are what benefit the most from this file. From 64a99e734f9eba7bcf3cffb039f1f72baf524887 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 29 May 2013 23:18:44 +0100 Subject: [PATCH 041/157] challenges: use bootstrap accordiong, much cleaner --- views/app/challenges.html | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/views/app/challenges.html b/views/app/challenges.html index 5229a8b412..4a2adff061 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -57,19 +57,26 @@
    -
    -
    - Unsubscribe - Subscribe +
    + -

    {@challenge.name} (by {@challenge.user})

    +
    +
    -
    From b34caaabd6a9d2502dfd0ee48036d0678f3f4ddf Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 29 May 2013 23:23:17 +0100 Subject: [PATCH 042/157] challenges: view.fn bug fix --- src/app/misc.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index 8c960323ce..6187bfc9f1 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -132,6 +132,6 @@ module.exports.viewHelpers = (view) -> #Challenges view.fn 'taskInChallenge', taskInChallenge view.fn 'taskAttrFromChallenge', (task, attr) -> - t = taskInChallenge(task) + t = taskInChallenge.call(@,task) t and t[attr] view.fn 'brokenChallengeLink', (task) -> task?.challenge and !taskInChallenge.call(@,task) From ea8f245b845d8f6c7d9f316333e3b229b0e16c71 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 29 May 2013 23:33:47 +0100 Subject: [PATCH 043/157] challenges: edit challenge title & description --- src/app/challenges.coffee | 8 ++++---- views/app/challenges.html | 10 ++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index 011567e340..25d1f696f4 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -26,6 +26,10 @@ module.exports.app = (appExports, model) -> browser.growlNotification('Challenge Created','success') challengeDiscard() + appExports.toggleChallengeEdit = (e, el) -> + path = "_editing.challenges.#{$(el).attr('data-id')}" + model.set path, !model.get(path) + appExports.challengeDiscard = challengeDiscard = -> model.del '_challenge.new' appExports.challengeSubscribe = (e) -> @@ -57,7 +61,3 @@ module.exports.app = (appExports, model) -> model.remove "_#{type}List", _.findIndex(model.get("_#{type}List",{id:task.id})) model.del "_user.tasks.#{task.id}" true - - appExports.challengeCollapse = (e, el) -> - $(el).next().toggle() - i = $(el).find('i').toggleClass('icon-chevron-right icon-chevron-down') \ No newline at end of file diff --git a/views/app/challenges.html b/views/app/challenges.html index 4a2adff061..2be554c6b3 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -68,6 +68,16 @@
    + {#if _editing.challenges[@challenge.id]} + Done +
    + + +
    + {else if equal(@challenge.uid,_user.id)} + Edit + {/} + {#if @challenge.description}
    {@challenge.description}
    {/}
    Date: Wed, 29 May 2013 23:40:13 +0100 Subject: [PATCH 044/157] challenges: add in challenge prize, just for show-purposes for now --- views/app/challenges.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/views/app/challenges.html b/views/app/challenges.html index 2be554c6b3..6b44a33af7 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -73,11 +73,18 @@
    +
    {else if equal(@challenge.uid,_user.id)} Edit {/} - {#if @challenge.description}
    {@challenge.description}
    {/} + {#if @challenge.prize} + +
    {@challenge.prize} Gem Prize
    +
    + {/} + {#if @challenge.description}
    {@challenge.description}
    {/} +
    Date: Thu, 30 May 2013 08:41:07 +0100 Subject: [PATCH 045/157] challenges: couple bug fixes --- views/app/challenges.html | 14 ++++++++------ views/app/tasks.html | 2 ++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/views/app/challenges.html b/views/app/challenges.html index 6b44a33af7..4f36bc8415 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -67,6 +67,11 @@
    + {#if @challenge.prize} + +
    {@challenge.prize} Gem Prize
    +
    + {/} {#if _editing.challenges[@challenge.id]} Done @@ -75,13 +80,10 @@
    - {else if equal(@challenge.uid,_user.id)} - Edit {/} - {#if @challenge.prize} - -
    {@challenge.prize} Gem Prize
    -
    + + {#if and(not(_editing.challenges[@challenge.id]),equal(@challenge.uid,_user.id))} + Edit {/} {#if @challenge.description}
    {@challenge.description}
    {/} diff --git a/views/app/tasks.html b/views/app/tasks.html index a4d5e58613..152a53dc07 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -178,6 +178,8 @@ {{#if :task.challenge}} {{#if brokenChallengeLink(:task)}} + {{else}} + {{/}} {{else}} From 15d76d93f48119a7951015a00fe31fe08e16e4e8 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 May 2013 18:07:00 +0100 Subject: [PATCH 046/157] return delta bug fix --- src/app/misc.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index 6187bfc9f1..261a0e38df 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -11,7 +11,7 @@ module.exports.batchTxn = batchTxn = (model, cb, options) -> get: (k) -> helpers.dotGet(k,uObj) paths = {} model._dontPersist = true - cb uObj, paths, batch + ret = cb uObj, paths, batch _.each paths, (v,k) -> user.pass({cron:options?.cron}).set(k,helpers.dotGet(k, uObj));true model._dontPersist = false # some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js @@ -19,6 +19,8 @@ module.exports.batchTxn = batchTxn = (model, cb, options) -> unless _.isEmpty paths setOps = _.reduce paths, ((m,v,k)-> m[k] = helpers.dotGet(k,uObj);m), {} user.set "update__", setOps + ret + ### algos.score wrapper for habitrpg-helpers to work in Derby. We need to do model.set() instead of simply setting the From 9399ca1d9848707ba9946166520aac1aa14776cc Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 May 2013 14:56:28 -0400 Subject: [PATCH 047/157] remove console.log --- src/app/misc.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index e2b0743aa0..b0428bd9b0 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -82,7 +82,6 @@ module.exports.fixCorruptUser = (model) -> ## fix https://github.com/lefnire/habitrpg/issues/1086 uniqPets = _.uniq(uObj.items.pets) batch.set('items.pets', uniqPets) if !_.isEqual(uniqPets, uObj.items.pets) - console.log {uniqPets, count:_.size(uniqPets)} ## Task List Cleanup ['habit','daily','todo','reward'].forEach (type) -> From 9180c9dfd41b1bd74370dbb3e20cba8026b45103 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 May 2013 17:10:35 -0400 Subject: [PATCH 048/157] challenges: add indexedPath() function & view helper, which lets us look up array indices by object id, similar to reflist benefits. very handy, using for challenges & challnege tasks --- src/app/misc.coffee | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index 66c50c67ca..8378d38b43 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -21,6 +21,21 @@ module.exports.batchTxn = batchTxn = (model, cb, options) -> user.set "update__", setOps ret +#TODO put this in habitrpg-shared +### + We can't always use refLists, but we often still need to get a positional path by id: eg, users.1234.tasks.5678.value + For arrays (which use indexes, not id-paths), here's a helper function so we can run indexedPath('users',:user.id,'tasks',:task.id,'value) +### +indexedPath = -> + _.reduce arguments, (m,v) => + return v if !m #first iteration + return "#{m}.#{v}" if _.isString v #string paths + return "#{m}." + _.findIndex(@model.get(m),v) + , '' + +taskInChallenge = (task) -> + return undefined unless task?.challenge + @model.at indexedPath.call(@, "groups.#{task.group.id}.challenges", {id:task.challenge}, "#{task.type}s", {id:task.id}) ### algos.score wrapper for habitrpg-helpers to work in Derby. We need to do model.set() instead of simply setting the @@ -124,6 +139,8 @@ module.exports.viewHelpers = (view) -> view.fn 'int', get: (num) -> num set: (num) -> [parseInt(num)] + view.fn 'indexedPath', indexedPath + #iCal view.fn "encodeiCalLink", helpers.encodeiCalLink @@ -159,22 +176,10 @@ module.exports.viewHelpers = (view) -> view.fn 'noTags', helpers.noTags view.fn 'appliedTags', helpers.appliedTags - #TODO put this in habitrpg-shared - taskInChallenge = (task) -> - return false unless task?.challenge - [tid, gid, cid, gType] = [task.id, task.group.id, task.challenge, task.group.type] - getTask = (challenges) -> - challenge = _.find(challenges,{id:cid}) - challenge and _.find(challenge["#{task.type}s"],{id:tid}) - switch gType - when 'party' - (party = @model.get '_party') and party and (getTask party.challenges) - when 'guild' - (guilds = @model.get "_guilds") and (guild = _.find guilds,{id:gid}) and guild and (getTask guild.challenges) - #Challenges - view.fn 'taskInChallenge', taskInChallenge + view.fn 'taskInChallenge', (task) -> + taskInChallenge.call(@,task)?.get() view.fn 'taskAttrFromChallenge', (task, attr) -> - t = taskInChallenge.call(@,task) - t and t[attr] - view.fn 'brokenChallengeLink', (task) -> task?.challenge and !taskInChallenge.call(@,task) + taskInChallenge.call(@,task)?.get(attr) + view.fn 'brokenChallengeLink', (task) -> + task?.challenge and !(taskInChallenge.call(@,task)?.get()) From 6e672078358dc37bcec758102891f99c22430bf1 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 May 2013 18:42:27 -0400 Subject: [PATCH 049/157] groups: fix leave button --- src/app/groups.coffee | 7 ++++--- views/app/groups.html | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index 1d74906ab2..dd01c9d23d 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -81,11 +81,12 @@ module.exports.app = (appExports, model, app) -> else e.at().remove clear appExports.groupLeave = (e,el) -> - members = e.get('members') + group = model.at "groups.#{$(el).attr('data-id')}" + members = group.get('members') index = members.indexOf(user.get('id')) - e.at().remove 'members', index, 1, -> + group.remove 'members', index, 1, -> if members.length is 1 # # last member out, kill the party - model.del("groups.#{id}", ->location.reload()) + group.del ->location.reload() else location.reload() diff --git a/views/app/groups.html b/views/app/groups.html index 281cf1a846..123d35bdf2 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -200,7 +200,7 @@
    - Leave + Leave {{/}}
    From 842ac45ab58a738d88f6c806e21ace7a1d409411 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 May 2013 18:43:39 -0400 Subject: [PATCH 050/157] move fixCorruptUser to the client --- src/app/index.coffee | 10 ++++------ src/app/misc.coffee | 3 +++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/app/index.coffee b/src/app/index.coffee index aad5420dbf..4e09f78cb3 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -78,11 +78,8 @@ setupSubscriptions = (page, model, params, next, cb) -> get '/', (page, model, params, next) -> return page.redirect '/' if page.params?.query?.play? - model.set '_gamePane', true - # removed force-ssl (handled in nginx), see git for code setupSubscriptions page, model, params, next, -> - misc.fixCorruptUser(model) # https://github.com/lefnire/habitrpg/issues/634 require('./items').server(model) #refLists _.each ['habit', 'daily', 'todo', 'reward'], (type) -> @@ -94,11 +91,12 @@ get '/', (page, model, params, next) -> # ========== CONTROLLER FUNCTIONS ========== ready (model) -> - user = model.at('_user') - browser = require './browser' - exports.removeAt = (e) -> e.at().remove() # used for things like remove website, chat, etc + user = model.at('_user') + misc.fixCorruptUser(model) # https://github.com/lefnire/habitrpg/issues/634 + + browser = require './browser' require('./tasks').app(exports, model) require('./items').app(exports, model) require('./groups').app(exports, model, app) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index 8378d38b43..0bc69625f5 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -92,6 +92,7 @@ module.exports.fixCorruptUser = (model) -> delete tasks[key] true + resetDom = false batchTxn model, (uObj, paths, batch) -> ## fix https://github.com/lefnire/habitrpg/issues/1086 @@ -115,6 +116,8 @@ module.exports.fixCorruptUser = (model) -> batch.set("#{type}Ids", preened) console.error uObj.id + "'s #{type}s were corrupt." true + resetDom = !_.isEmpty(paths) + require('./browser').resetDom(model) if resetDom module.exports.viewHelpers = (view) -> From 1411d045180857d67e00c7b4cd2a60ca52523dd0 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 May 2013 09:35:50 -0400 Subject: [PATCH 051/157] challenges: add basic task stats for challenge subscribers --- src/app/challenges.coffee | 15 +++++++++++++++ src/app/misc.coffee | 27 +++++++++++++++++++++++---- views/app/challenges.html | 25 +++++++++++++++++++++++++ views/app/game-pane.html | 2 +- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index 25d1f696f4..dff8653269 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -5,6 +5,21 @@ module.exports.app = (appExports, model) -> browser = require './browser' user = model.at '_user' + appExports.renderChallengeGraphs = -> + challenges = model.get('_party.challenges') + _.each model.get('_guilds'), (g) -> challenges.concat(g.challenges) + _.each challenges, (chal) -> + _.each ['habit','daily','todo'], (type) -> + _.each chal["#{type}s"], (task) -> + _.each chal.users, (member) -> + if (history = member["#{type}s"][task.id].history) and !!history + data = google.visualization.arrayToDataTable _.map(history, (h)-> [h.date,h.value]) + options = + backgroundColor: { fill:'transparent' } + axisTitlesPosition: 'none' + chart = new google.visualization.LineChart $(".challenge-#{chal.id}-member-#{member.id}-history-#{task.id}")[0] + chart.draw(data, options) + appExports.challengeCreate = (e,el) -> [type, gid] = [$(el).attr('data-type'), $(el).attr('data-gid')] model.set '_challenge.new', diff --git a/src/app/misc.coffee b/src/app/misc.coffee index 0bc69625f5..1964b42625 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -44,8 +44,7 @@ taskInChallenge = (task) -> perform the updates while tracking paths, then all the values at those paths ### module.exports.score = (model, taskId, direction, allowUndo=false) -> - #return setTimeout( (-> score(taskId, direction)), 500) if model._txnQueue.length > 0 - batchTxn model, (uObj, paths) -> + delta = batchTxn model, (uObj, paths) -> tObj = uObj.tasks[taskId] # Stuff for undo @@ -62,7 +61,27 @@ module.exports.score = (model, taskId, direction, allowUndo=false) -> if uObj._tmp?.drop and $? model.set '_drop', uObj._tmp.drop $('#item-dropped-modal').modal 'show' - delta + + # Update challenge statistics + # FIXME put this in it's own batchTxn, make batchTxn model.at() ref aware (not just _user) + # FIXME use reflists for users & challenges + if (chalTask = taskInChallenge.call({model}, tObj)) and chalTask?.get() + model._dontPersist = false + chalTask.incr "value", delta + chal = model.at indexedPath.call({model}, "groups.#{tObj.group.id}.challenges", {id:tObj.challenge}) + chalUser = -> indexedPath.call({model}, chal.path(), 'users', {id:uObj.id}) + cu = model.at chalUser() + unless cu?.get() + chal.push "users", {id: uObj.id, name: helpers.username(uObj.auth, uObj.profile?.name)} + cu = model.at chalUser() + else + cu.set 'name', helpers.username(uObj.auth, uObj.profile?.name) # update their name incase it changed + cu.set "#{tObj.type}s.#{tObj.id}", + value: tObj.value + history: tObj.history + model._dontPersist = true + + delta ### Make sure model.get() returns all properties, see https://github.com/codeparty/racer/issues/116 @@ -105,7 +124,7 @@ module.exports.fixCorruptUser = (model) -> # 1. remove duplicates # 2. restore missing zombie tasks back into list idList = uObj["#{type}Ids"] - taskIds = _.pluck( _.where(tasks, {type:type}), 'id') + taskIds = _.pluck( _.where(tasks, {type}), 'id') union = _.union idList, taskIds # 2. remove empty (grey) tasks diff --git a/views/app/challenges.html b/views/app/challenges.html index 4f36bc8415..b281667f06 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -95,10 +95,35 @@ todos={@challenge.todos} rewards={@challenge.rewards} />
    + +

    Statistics

    + {#each @challenge.users as :member} +

    {:member.name}

    +
    +
    + +
    +
    + +
    +
    + +
    +
    + {/}
    + +
    {@header}
    + {#each @challenge[@taskType]s as :task} +
    + {:task.text}: {round(@member[@taskType]s[:task.id].value)} +
    +
    + {/} + Create {{@text}} Challenge diff --git a/views/app/game-pane.html b/views/app/game-pane.html index d96f1ca136..9211621806 100644 --- a/views/app/game-pane.html +++ b/views/app/game-pane.html @@ -14,7 +14,7 @@ {/if}
  • Tavern
  • Achievements
  • -
  • Challenges
  • +
  • Challenges
  • Settings
  • From 39d29f50f412d644c8543c4e4f886d6bd3ee8c38 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 May 2013 09:36:00 -0400 Subject: [PATCH 052/157] challenge: don't allow to create new challenge unless in group --- views/app/challenges.html | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/views/app/challenges.html b/views/app/challenges.html index b281667f06..df2be92dc7 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -9,12 +9,16 @@
    - {#if _challenge.new} - + {#unless _party.id} + Join a party first {else} - - {#each _party.challenges as :challenge} - + {#if _challenge.new} + + {else} + + {#each _party.challenges as :challenge} + + {/} {/} {/}
    From 81f2b6789ef66bd21f3fbaafa43ded19a02865b8 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 May 2013 11:05:09 -0400 Subject: [PATCH 053/157] WIP addTask in api.coffee --- src/server/api.coffee | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 45b0250b17..aefffbc11d 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -170,10 +170,7 @@ updateTasks = (tasks, user, model) -> else user.set "tasks.#{task.id}", task else - type = task.type || 'habit' - model.ref '_user', user - model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" - model.at("_#{type}List").push task + task = addTask(model,task) tasks[idx] = task return tasks @@ -181,19 +178,19 @@ router.post '/user/tasks', auth, (req, res) -> tasks = updateTasks req.body, req.user, req.getModel() res.json 201, tasks +addTask = module.exports.addTask = (model, task) -> + type = task.type || 'habit'{ + model.ref '_user', req.user + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + model.at("_#{type}List").push task + task ### POST /user/task/ ### router.post '/user/task', auth, validateTask, (req, res) -> - task = req.task - type = task.type - model = req.getModel() - model.ref '_user', req.user - model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" - model.at("_#{type}List").push task - + task = addTask(model, req.task) res.json 201, task ### From 413abdb3455d82b84cc8b1a3257ada49d92021ee Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 May 2013 11:12:18 -0400 Subject: [PATCH 054/157] api_v2 bug fix --- src/server/apiv2.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 90e419e464..db4e2d1a02 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -49,9 +49,9 @@ router.post '/', auth, (req, res) -> if _.isArray actions actions.forEach (action)-> switch action.op - when score then + when score {} - when newTask then + when newTask req.user.set "tasks.#{req.task.id}", action.task console.log util.inspect req.body From cdc7b06f92ffdef2dbc356b613737d7d601712da Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 May 2013 11:12:39 -0400 Subject: [PATCH 055/157] api_v2 bug fix --- 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 aefffbc11d..5f68ce4e42 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -179,7 +179,7 @@ router.post '/user/tasks', auth, (req, res) -> res.json 201, tasks addTask = module.exports.addTask = (model, task) -> - type = task.type || 'habit'{ + type = task.type || 'habit' model.ref '_user', req.user model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" model.at("_#{type}List").push task From 9ca97173871ddfb8e304ce7d1d7eda7a5f90249f Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 May 2013 11:20:40 -0400 Subject: [PATCH 056/157] Revert "WIP addTask in api.coffee" This reverts commit 81f2b6789ef66bd21f3fbaafa43ded19a02865b8. --- src/server/api.coffee | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 5f68ce4e42..45b0250b17 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -170,7 +170,10 @@ updateTasks = (tasks, user, model) -> else user.set "tasks.#{task.id}", task else - task = addTask(model,task) + type = task.type || 'habit' + model.ref '_user', user + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + model.at("_#{type}List").push task tasks[idx] = task return tasks @@ -178,19 +181,19 @@ router.post '/user/tasks', auth, (req, res) -> tasks = updateTasks req.body, req.user, req.getModel() res.json 201, tasks -addTask = module.exports.addTask = (model, task) -> - type = task.type || 'habit' - model.ref '_user', req.user - model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" - model.at("_#{type}List").push task - task ### POST /user/task/ ### router.post '/user/task', auth, validateTask, (req, res) -> + task = req.task + type = task.type + model = req.getModel() - task = addTask(model, req.task) + model.ref '_user', req.user + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + model.at("_#{type}List").push task + res.json 201, task ### From 268157ca246280aace22dff949367691d7db96d6 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 May 2013 16:19:57 -0400 Subject: [PATCH 057/157] tags: use explicit {#each users[_userId].tags} instead of model.ref private path {#each _user.tags}. fixed many tags issues. see https://github.com/codeparty/derby/issues/267 --- src/app/filters.coffee | 4 +--- views/app/filters.html | 16 ++++++++-------- views/app/tasks.html | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/app/filters.coffee b/src/app/filters.coffee index 605e4ebb63..a7c1a87dcf 100644 --- a/src/app/filters.coffee +++ b/src/app/filters.coffee @@ -14,9 +14,7 @@ module.exports.app = (appExports, model) -> model.set '_newTag', '' appExports.toggleEditingTags = -> - before = model.get('_editingTags') - model.set '_editingTags', !before, -> - location.reload() if before is true #when they're done, refresh the page + model.set '_editingTags', !model.get('_editingTags') appExports.clearFilters = -> user.set 'filters', {} diff --git a/views/app/filters.html b/views/app/filters.html index 1a04d02a3a..23581d7fc9 100644 --- a/views/app/filters.html +++ b/views/app/filters.html @@ -7,8 +7,8 @@
  • - {#each _user.tags as :tag} -
  • + {#each users[_userId].tags as :tag} +
  • {#if _editingTags}
    @@ -29,7 +29,7 @@ {/}
  • - +
  • @@ -37,15 +37,15 @@
    - + @@ -53,9 +53,9 @@
    Tags - {{#each _user.tags as :tag}} + {#each users[_userId].tags as :tag} - {{/}} + {/}
    \ No newline at end of file diff --git a/views/app/tasks.html b/views/app/tasks.html index 7df21248f3..5a7eab3899 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -158,7 +158,7 @@ -
  • +
  • From fa0813dc9e4093445fa77a70e5fa72f296ba84cd Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 May 2013 17:53:17 -0400 Subject: [PATCH 058/157] challenges: use tags fix (268157c) to allow guild info editing (not working for _party for some reason) --- views/app/challenges.html | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/views/app/challenges.html b/views/app/challenges.html index df2be92dc7..fcd09c8ed7 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -9,18 +9,19 @@
    - {#unless _party.id} - Join a party first - {else} + {{#unless _party.id}} + Join a party first. + {{else}} {#if _challenge.new} {else} + {#each _party.challenges as :challenge} {/} {/} - {/} + {{/}}
    @@ -37,7 +38,7 @@ {else} {#each :guild.challenges as :challenge} - + {/}
    {/} @@ -52,7 +53,7 @@ {else} {#each _habitRPG.challenges as :challenge} - + {/} {/}
    From a4e60251851df6a2dc97f3bfdee2a3124b003601 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 May 2013 18:31:37 -0400 Subject: [PATCH 059/157] challenges: slightly prettier graphs (i'm still not satisfied, but later) --- src/app/challenges.coffee | 32 ++++++++++++++++++-------------- views/app/challenges.html | 14 ++++++++++---- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index dff8653269..7d5d1b1ffd 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -6,19 +6,24 @@ module.exports.app = (appExports, model) -> user = model.at '_user' appExports.renderChallengeGraphs = -> - challenges = model.get('_party.challenges') - _.each model.get('_guilds'), (g) -> challenges.concat(g.challenges) - _.each challenges, (chal) -> - _.each ['habit','daily','todo'], (type) -> - _.each chal["#{type}s"], (task) -> - _.each chal.users, (member) -> - if (history = member["#{type}s"][task.id].history) and !!history - data = google.visualization.arrayToDataTable _.map(history, (h)-> [h.date,h.value]) - options = - backgroundColor: { fill:'transparent' } - axisTitlesPosition: 'none' - chart = new google.visualization.LineChart $(".challenge-#{chal.id}-member-#{member.id}-history-#{task.id}")[0] - chart.draw(data, options) + _.each model.get('groups'), (g) -> + _.each g.challenges, (chal) -> + _.each ['habit','daily','todo'], (type) -> + _.each chal["#{type}s"], (task) -> + _.each chal.users, (member) -> + if (history = member["#{type}s"][task.id].history) and !!history + data = google.visualization.arrayToDataTable _.map(history, (h)-> [h.date,h.value]) + options = + backgroundColor: { fill:'transparent' } + width: 150 + height: 50 + chartArea: width: '80%', height: '80%' + axisTitlePosition: 'none' + legend: position: 'bottom' + hAxis: gridlines: color: 'transparent' # since you can't seem to *remove* gridlines... + vAxis: gridlines: color: 'transparent' + chart = new google.visualization.LineChart $(".challenge-#{chal.id}-member-#{member.id}-history-#{task.id}")[0] + chart.draw(data, options) appExports.challengeCreate = (e,el) -> [type, gid] = [$(el).attr('data-type'), $(el).attr('data-gid')] @@ -36,7 +41,6 @@ module.exports.app = (appExports, model) -> appExports.challengeSave = -> gid = model.get('_challenge.new.group.id') - debugger model.unshift "groups.#{gid}.challenges", model.get('_challenge.new'), -> browser.growlNotification('Challenge Created','success') challengeDiscard() diff --git a/views/app/challenges.html b/views/app/challenges.html index fcd09c8ed7..4c65e1828b 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -122,12 +122,18 @@
    {@header}
    +
    {#each @challenge[@taskType]s as :task} -
    - {:task.text}: {round(@member[@taskType]s[:task.id].value)} -
    -
    + + + +
    + {:task.text}: {round(@member[@taskType]s[:task.id].value)} + +
    +
    {/} +
    Create {{@text}} Challenge From 28f45baf7bd012823949a9c17c1e12b359ae0890 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 May 2013 18:31:41 -0400 Subject: [PATCH 060/157] challenges: can delete challenge --- views/app/challenges.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/views/app/challenges.html b/views/app/challenges.html index 4c65e1828b..f63ea6b89e 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -85,6 +85,9 @@
    + {{#with @challenge}} + Delete + {{/}} {/} {#if and(not(_editing.challenges[@challenge.id]),equal(@challenge.uid,_user.id))} From 0fd74680c20df34eee1a70bd37c7b3a4fd6a82f5 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 May 2013 18:42:02 -0400 Subject: [PATCH 061/157] challenges: float-right edit button, us icons for consistency --- views/app/challenges.html | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/views/app/challenges.html b/views/app/challenges.html index f63ea6b89e..e3ecd093f8 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -72,30 +72,39 @@
    + + {#if @challenge.prize} - +
    {@challenge.prize} Gem Prize
    {/} + + + {#if and(not(_editing.challenges[@challenge.id]),equal(@challenge.uid,_user.id))} + + {else} + + {/} + + {#if _editing.challenges[@challenge.id]} - Done
    {{#with @challenge}} - Delete + Delete {{/}} {/} - - {#if and(not(_editing.challenges[@challenge.id]),equal(@challenge.uid,_user.id))} - Edit - {/} {#if @challenge.description}
    {@challenge.description}
    {/} -
    Date: Fri, 31 May 2013 18:47:49 -0400 Subject: [PATCH 062/157] challenges: more html futzing --- views/app/challenges.html | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/views/app/challenges.html b/views/app/challenges.html index e3ecd093f8..00bdaa1d14 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -64,15 +64,16 @@
    -
    +
    + + {#if @challenge.prize} From 5f78d801a8485318bdf1b93263bcfc301326e9f1 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 May 2013 18:59:20 -0400 Subject: [PATCH 063/157] challenges: make @editable dynamic so we can add tasks when in edit mode --- views/app/challenges.html | 1 + views/app/tasks.html | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/views/app/challenges.html b/views/app/challenges.html index 00bdaa1d14..216ccfd3ea 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -108,6 +108,7 @@
    <@ads>The Power of Habit: Why We Do What We Do in Life and Business @@ -38,7 +38,7 @@ placeHolder="New Daily" list={@dailys} main={{@main}} - editable={{@editable}} + editable={@editable} > <@ads>Getting Things Done: The Art of Stress-Free Productivity @@ -68,7 +68,7 @@ placeHolder="New Todo" list={@todos} main={{@main}} - editable={{@editable}} + editable={@editable} > <@ads>The Checklist Manifesto: How to Get Things Right @@ -108,7 +108,7 @@ placeHolder="New Reward" list={@rewards} main={{@main}} - editable={{@editable}} + editable={@editable} > <@extra> {{#if @main}} @@ -134,7 +134,7 @@ {{#if equal(@type,'todo')}}{{/}} - {{#if @editable}} + {#if @editable} @@ -145,9 +145,9 @@ {{/}}
    - {{/}} + {/}
      - {#each @list as :task}{/} + {#each @list as :task}{/}
    {{@extra}}
    From 780e3ccabcdb07574a9d34605ca068d717ac0298 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 May 2013 19:26:11 -0400 Subject: [PATCH 064/157] challenges: move challenge specs to accordion header, add subscribers count --- styles/app/challenges.styl | 7 +++++++ styles/app/index.styl | 1 + styles/app/inventory.styl | 8 ++++++++ views/app/challenges.html | 29 +++++++++++++++++++---------- 4 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 styles/app/challenges.styl diff --git a/styles/app/challenges.styl b/styles/app/challenges.styl new file mode 100644 index 0000000000..c28ee8be0e --- /dev/null +++ b/styles/app/challenges.styl @@ -0,0 +1,7 @@ +ul.challenge-accordion-header-specs + list-style:none + + li + background-color: darken($neutral, 10%) + margin: 2px 5px + float:left diff --git a/styles/app/index.styl b/styles/app/index.styl index 9995a66c94..cf5a6f2fb4 100644 --- a/styles/app/index.styl +++ b/styles/app/index.styl @@ -26,6 +26,7 @@ @import "./game-pane.styl"; @import "./backer.styl"; @import "./npcs.styl"; +@import "./challenges.styl"; // fix exploding to very wide for some reason .datepicker diff --git a/styles/app/inventory.styl b/styles/app/inventory.styl index 911dcead61..789d2d42d9 100644 --- a/styles/app/inventory.styl +++ b/styles/app/inventory.styl @@ -42,6 +42,14 @@ width:34px height:34px +.Pet_Currency_Gem, .Pet_Currency_Gem2x, .Pet_Currency_Gem1x + background: url("/img/sprites/Egg_Sprite_Sheet.png") no-repeat + display:block + +.Pet_Currency_Gem {background-position: 0px -510px; width: 51px; height: 45px} /* Not an egg or potion so has a different size */ +.Pet_Currency_Gem2x {background-position: -55px -513px; width: 34px; height: 30px} +.Pet_Currency_Gem1x {background-position: -63px -542px; width: 19px; height: 17px} + .inventory-list li clear:both .pets-menu > div diff --git a/views/app/challenges.html b/views/app/challenges.html index 216ccfd3ea..222c8452fe 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -64,22 +64,31 @@
    +
      +
    • + {count(@challenge.users)} Subscribers +
    • +
    • + + {#if @challenge.prize} +
      {@challenge.prize} Prize
      + {/} +
    • +
    • + + Unsubscribe + Subscribe +
    • +
    {@challenge.name} (by {@challenge.user}) + +
    - - - {#if @challenge.prize} - -
    {@challenge.prize} Gem Prize
    -
    - {/} + From 2ab5772c0a26367d395d40f7524ac3058f2d6354 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 May 2013 19:44:29 -0400 Subject: [PATCH 065/157] groups: make edit button consistent too --- views/app/groups.html | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/views/app/groups.html b/views/app/groups.html index 123d35bdf2..f4fd55b0ce 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -104,12 +104,10 @@
    {#if _editing.groups[@group.id]} -
    - -
    -
    - +
    +
    + {#with @group} @@ -129,7 +127,7 @@ {else} {#if @group.logo}{/} {{#if equal(@group.leader,_user.id)}} - + {{/}}

    {@group.name}

    {@group.description}
    From 178d3846a41b61a99b9d599b99534efc4b7046eb Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 1 Jun 2013 10:57:38 -0400 Subject: [PATCH 066/157] challenges: html bug --- views/app/challenges.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/app/challenges.html b/views/app/challenges.html index 222c8452fe..126dd44f66 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -43,7 +43,7 @@
    {/}
    - {/} + {{/}}
    From 363a77cbe78c03c79e7003b0dbb769851fab9148 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 1 Jun 2013 12:35:20 -0400 Subject: [PATCH 067/157] guilds: add public guilds --- src/app/groups.coffee | 16 +++++++++---- src/app/index.coffee | 16 ++++++++++--- src/server/store.coffee | 14 +++++++++++ views/app/challenges.html | 4 +--- views/app/groups.html | 49 +++++++++++++++++++++++++++++++++++---- 5 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index dd01c9d23d..93a249cc02 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -13,7 +13,9 @@ module.exports.app = (appExports, model, app) -> appExports.groupCreate = (e,el) -> model.add('groups', - name: model.get("_newGroup") + name: model.get("_new.group.name") + description: model.get("_new.group.description") + privacy: model.get("_new.group.privacy") || 'public' leader: user.get('id') members: [user.get('id')] type: $(el).attr('data-type') @@ -66,13 +68,17 @@ module.exports.app = (appExports, model, app) -> return groupError("User already in a party.") else invite() + joinGroup = (gid) -> + model.push("groups.#{gid}.members", user.get('id'), ->location.reload()) + + appExports.joingGroup = (e, el) -> joinGroup e.get('id') + appExports.acceptInvitation = (e,el) -> - group = e.at().get() - pushMember = -> model.push("groups.#{group.id}.members", user.get('id'), ->location.reload()) + gid = e.get('id') if $(el).attr('data-type') is 'party' - user.set 'invitations.party', null, pushMember + user.set 'invitations.party', null, ->joinGroup(gid) else - e.at().remove pushMember + e.at().remove ->joinGroup(gid) appExports.rejectInvitation = (e, el) -> clear = -> browser.resetDom(model) diff --git a/src/app/index.coffee b/src/app/index.coffee index 4e09f78cb3..308484d4a7 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -35,6 +35,10 @@ setupSubscriptions = (page, model, params, next, cb) -> groupsQ.fetch (err, groups) -> return next(err) if err finished = (descriptors, paths) -> + # Add public "Tavern" guild in + descriptors.push('groups.habitrpg'); paths.push('_habitRPG') + + # Subscribe to each descriptor model.subscribe.apply model, descriptors.concat -> [err, refs] = [arguments[0], arguments] return next(err) if err @@ -42,12 +46,18 @@ setupSubscriptions = (page, model, params, next, cb) -> unless model.get('_user') console.error "User not found - this shouldn't be happening!" return page.redirect('/logout') #delete model.session.userId + + # Fetch public groups as _publicGroups - not it has to come at very end due to racer bug + model.query('groups').publicGroups().fetch (err, pg) -> + return next(err) if err + model.set '_publicGroups', _.sortBy pg.get(), (g) -> -_.size(g.members) + return cb() groupsObj = groups.get() # (1) Solo player - return finished(["groups.habitrpg", selfQ], ['_habitRPG', '_user']) if _.isEmpty(groupsObj) + return finished([selfQ], ['_user']) if _.isEmpty(groupsObj) ## (2) Party or Guild has members, fetch those users too # Subscribe to the groups themselves. We separate them by _party, _guilds, and _habitRPG (the "global" guild). @@ -68,10 +78,10 @@ setupSubscriptions = (page, model, params, next, cb) -> # Note - selfQ *must* come after membersQ in subscribe, otherwise _user will only get the fields restricted by party-members in store.coffee. Strang bug, but easy to get around partyQ = model.query('groups').withIds(groupsInfo.partyId) if _.isEmpty(groupsInfo.guildIds) - finished [partyQ, 'groups.habitrpg', selfQ], ['_party', '_habitRPG', '_user'] + finished [partyQ, selfQ], ['_party', '_user'] else guildsQ = model.query('groups').withIds(groupsInfo.guildIds) - finished [partyQ, guildsQ, 'groups.habitrpg', selfQ], ['_party', '_guilds', '_habitRPG', '_user'] + finished [partyQ, guildsQ, selfQ], ['_party', '_guilds', '_user'] # ========== ROUTES ========== diff --git a/src/server/store.coffee b/src/server/store.coffee index 5d93b2dcf4..32a152d773 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -134,6 +134,20 @@ groupSystem = (store) -> q = q.where('type').equals(type) if type? store.queryAccess 'groups', 'withMember', publicAccess + ### + Public Groups Info + ### + store.query.expose "groups", "publicGroups", -> + @where("privacy").equals('public') + .only [ + 'name' + 'description' + 'users' + 'members' + 'privacy' + ] + store.queryAccess "groups", "publicGroups", publicAccess + ### Public HabitRPG Guild ### diff --git a/views/app/challenges.html b/views/app/challenges.html index 126dd44f66..272a66f355 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -159,7 +159,7 @@
    - Create {{@text}} Challenge + Create {{@text}} Challenge
    @@ -172,8 +172,6 @@
    - - + Leave + Join +
  • + + + {:group.name} + +
    +
    +
    +
    {{:group.description}}
    +
    +
    +
    + {/} +
    + {#if _groupError}
    {_groupError}
    {/}
    - + + + +
    + Public + Invite Only +
    From 8fdeccf43cb87a1ccf2ed441c622972009cd628e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 10:07:15 -0400 Subject: [PATCH 068/157] challenges: fix disappearing repeat bug --- views/app/tasks.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/views/app/tasks.html b/views/app/tasks.html index de9fe50106..08e2229c41 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -272,7 +272,8 @@ - {#if and(equal(:task.type, 'habit'),not(taskInChallenge(:task)))} + {#if equal(:task.type, 'habit')} + {#unless taskInChallenge(:task)}
    Direction/Actions @@ -284,6 +285,7 @@
    + {/} {else if equal(:task.type, 'daily')} From dc36651029da45c58fe7ed77540706cce800e247 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 11:20:57 -0400 Subject: [PATCH 069/157] groups: only add privacy field on guilds --- src/app/groups.coffee | 9 +++++---- views/app/groups.html | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index 93a249cc02..e24f59abe7 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -12,14 +12,15 @@ module.exports.app = (appExports, model, app) -> user = model.at('_user') appExports.groupCreate = (e,el) -> - model.add('groups', + type = $(el).attr('data-type') + newGroup = name: model.get("_new.group.name") description: model.get("_new.group.description") - privacy: model.get("_new.group.privacy") || 'public' leader: user.get('id') members: [user.get('id')] - type: $(el).attr('data-type') - , ->location.reload()) + type: type + newGroup.privacy = (model.get("_new.group.privacy") || 'public') if type is 'guild' + model.add 'groups', newGroup, ->location.reload() appExports.toggleGroupEdit = (e, el) -> path = "_editing.groups.#{$(el).attr('data-gid')}" diff --git a/views/app/groups.html b/views/app/groups.html index 38cc01eb9f..bc61de23db 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -91,12 +91,14 @@ {/}
    - + + {{#if equal(@type,'guild')}}
    Public Invite Only
    + {{/}}
    From 38781cc012a02e9b34bcf2aac579411166a92600 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 11:22:33 -0400 Subject: [PATCH 070/157] groups: some static bindings where possible --- views/app/groups.html | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/views/app/groups.html b/views/app/groups.html index bc61de23db..5674dcf7ed 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -58,30 +58,29 @@
    -
    - {#each _publicGroups as :group} + {{#each _publicGroups as :public}}
    - - {:group.name} + + {{:public.name}}
    -
    +
    -
    {{:group.description}}
    +
    {{:public.description}}
    - {/} + {{/}}
    From 01b9a420b8609680bfc976811e7ef9763efdd8f4 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 11:23:02 -0400 Subject: [PATCH 071/157] groups: only show invitations if there are any (was previously iterating empty array) --- views/app/groups.html | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/views/app/groups.html b/views/app/groups.html index 5674dcf7ed..57383870f2 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -35,10 +35,14 @@
    - {#each _user.invitations.guilds as :invitation} -

    You're Invited To {:invitation.name}

    - Accept - Reject + {#if _user.invitations.guilds} + {#each _user.invitations.guilds as :invitation} +
    +

    You're Invited To {:invitation.name}

    + Accept + Reject +
    + {/} {/}
    From 8a1aae37a8977fccabef8a99ba03d3f075b90004 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 11:24:03 -0400 Subject: [PATCH 072/157] groups: very precarious placement of query motifs and subscription / fetch order due to https://github.com/codeparty/racer/issues/57 for guilds, party, habitRPG group, and publicGroups. --- src/app/index.coffee | 12 +++++++----- src/server/store.coffee | 21 +++++++++++---------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/app/index.coffee b/src/app/index.coffee index 308484d4a7..116b8357de 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -32,11 +32,18 @@ setupSubscriptions = (page, model, params, next, cb) -> selfQ = model.query('users').withId(uuid) #keep this for later groupsQ = model.query('groups').withMember(uuid) + # Fetch public groups as _publicGroups - not it has to come at very end due to racer bug + model.query('groups').publicGroups().fetch (err, pg) -> + return next(err) if err + model.set '_publicGroups', _.sortBy(pg.get(), (g) -> -_.size(g.members)) + groupsQ.fetch (err, groups) -> return next(err) if err finished = (descriptors, paths) -> # Add public "Tavern" guild in descriptors.push('groups.habitrpg'); paths.push('_habitRPG') +# descriptors.unshift model.query('groups').publicGroups() +# paths.unshift('_publicGroups') # Subscribe to each descriptor model.subscribe.apply model, descriptors.concat -> @@ -47,11 +54,6 @@ setupSubscriptions = (page, model, params, next, cb) -> console.error "User not found - this shouldn't be happening!" return page.redirect('/logout') #delete model.session.userId - # Fetch public groups as _publicGroups - not it has to come at very end due to racer bug - model.query('groups').publicGroups().fetch (err, pg) -> - return next(err) if err - model.set '_publicGroups', _.sortBy pg.get(), (g) -> -_.size(g.members) - return cb() groupsObj = groups.get() diff --git a/src/server/store.coffee b/src/server/store.coffee index 32a152d773..bab1a5c64c 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -130,22 +130,23 @@ groupSystem = (store) -> Find group which has member by id ### store.query.expose "groups", "withMember", (id, type) -> - q = @where('members').contains([id]).only(['id','members','type']) + q = @where('members').contains([id])#.only(['id','members','type']) q = q.where('type').equals(type) if type? store.queryAccess 'groups', 'withMember', publicAccess ### - Public Groups Info + Public Groups Info ### store.query.expose "groups", "publicGroups", -> - @where("privacy").equals('public') - .only [ - 'name' - 'description' - 'users' - 'members' - 'privacy' - ] + @where('privacy').equals('public') + .where('type').equals('guild') +# .only [ +# 'name' +# 'description' +# 'users' +# 'members' +# 'privacy' +# ] store.queryAccess "groups", "publicGroups", publicAccess ### From ad2b37fe50b60c3f8b81f7a5061ce509b9385b97 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 11:49:56 -0400 Subject: [PATCH 073/157] groups: some join / leave bug fixes --- src/app/groups.coffee | 15 +++++++-------- views/app/groups.html | 7 +++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index e24f59abe7..d8222c7b36 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -72,7 +72,7 @@ module.exports.app = (appExports, model, app) -> joinGroup = (gid) -> model.push("groups.#{gid}.members", user.get('id'), ->location.reload()) - appExports.joingGroup = (e, el) -> joinGroup e.get('id') + appExports.joinGroup = (e, el) -> joinGroup e.get('id') appExports.acceptInvitation = (e,el) -> gid = e.get('id') @@ -89,13 +89,12 @@ module.exports.app = (appExports, model, app) -> appExports.groupLeave = (e,el) -> group = model.at "groups.#{$(el).attr('data-id')}" - members = group.get('members') - index = members.indexOf(user.get('id')) - group.remove 'members', index, 1, -> - if members.length is 1 # # last member out, kill the party - group.del ->location.reload() - else - location.reload() + index = group.get('members').indexOf(user.get('id')) + if index != -1 + group.remove 'members', index, 1, -> + if _.isEmpty group.get('members') # last member out, delete the party + group.del ->location.reload() + else location.reload() ### Chat Functionality diff --git a/views/app/groups.html b/views/app/groups.html index 57383870f2..e9a68495ca 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -70,8 +70,11 @@
  • {{count(:public.members)}} member(s)
  • - Leave - Join + {{#if indexOf(:public.members,_user.id)}} + Leave + {{else}} + Join + {{/}}
  • From 89c6192e284432d99c220d344399408bdcacbe5e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 12:50:43 -0400 Subject: [PATCH 074/157] groups: possible solution to 8a1aae3 (fixes groups permissioning) --- src/app/index.coffee | 93 ++++++++++++++++++++--------------------- src/server/store.coffee | 10 +---- 2 files changed, 48 insertions(+), 55 deletions(-) diff --git a/src/app/index.coffee b/src/app/index.coffee index 116b8357de..125fb5566a 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -30,60 +30,59 @@ algos = require 'habitrpg-shared/script/algos' setupSubscriptions = (page, model, params, next, cb) -> uuid = model.get('_userId') or model.session.userId # see http://goo.gl/TPYIt selfQ = model.query('users').withId(uuid) #keep this for later - groupsQ = model.query('groups').withMember(uuid) - # Fetch public groups as _publicGroups - not it has to come at very end due to racer bug + # Fetch public groups as _publicGroups + # Note: due to https://github.com/codeparty/racer/issues/57, this has to come at the very beginning. The more limited + # the returned fields in motifs, the sooner they must come in fetch / subscribes. model.query('groups').publicGroups().fetch (err, pg) -> return next(err) if err model.set '_publicGroups', _.sortBy(pg.get(), (g) -> -_.size(g.members)) - groupsQ.fetch (err, groups) -> - return next(err) if err - finished = (descriptors, paths) -> - # Add public "Tavern" guild in - descriptors.push('groups.habitrpg'); paths.push('_habitRPG') -# descriptors.unshift model.query('groups').publicGroups() -# paths.unshift('_publicGroups') - - # Subscribe to each descriptor - model.subscribe.apply model, descriptors.concat -> - [err, refs] = [arguments[0], arguments] - return next(err) if err - _.each paths, (path, idx) -> model.ref path, refs[idx+1]; true - unless model.get('_user') - console.error "User not found - this shouldn't be happening!" - return page.redirect('/logout') #delete model.session.userId - - return cb() - - groupsObj = groups.get() - - # (1) Solo player - return finished([selfQ], ['_user']) if _.isEmpty(groupsObj) - - ## (2) Party or Guild has members, fetch those users too - # Subscribe to the groups themselves. We separate them by _party, _guilds, and _habitRPG (the "global" guild). - groupsInfo = _.reduce groupsObj, ((m,g)-> - if g.type is 'guild' then m.guildIds.push(g.id) else m.partyId = g.id - m.members = m.members.concat(g.members) - m - ), {guildIds:[], partyId:null, members:[]} - - # Fetch, not subscribe. There's nothing dynamic we need from members, just the the Group (below) which includes chat, challenges, etc - model.query('users').publicInfo(groupsInfo.members).fetch (err, members) -> + model.query('groups').withMember(uuid).fetch (err, groups) -> return next(err) if err - # we need _members as an object in the view, so we can iterate over _party.members as :id, and access _members[:id] for the info - mObj = members.get() - model.set "_members", _.object(_.pluck(mObj,'id'), mObj) - model.set "_membersArray", mObj + finished = (descriptors, paths) -> + # Add public "Tavern" guild in + descriptors.push('groups.habitrpg'); paths.push('_habitRPG') - # Note - selfQ *must* come after membersQ in subscribe, otherwise _user will only get the fields restricted by party-members in store.coffee. Strang bug, but easy to get around - partyQ = model.query('groups').withIds(groupsInfo.partyId) - if _.isEmpty(groupsInfo.guildIds) - finished [partyQ, selfQ], ['_party', '_user'] - else - guildsQ = model.query('groups').withIds(groupsInfo.guildIds) - finished [partyQ, guildsQ, selfQ], ['_party', '_guilds', '_user'] + # Subscribe to each descriptor + model.subscribe.apply model, descriptors.concat -> + [err, refs] = [arguments[0], arguments] + return next(err) if err + _.each paths, (path, idx) -> model.ref path, refs[idx+1]; true + unless model.get('_user') + console.error "User not found - this shouldn't be happening!" + return page.redirect('/logout') #delete model.session.userId + + return cb() + + groupsObj = groups.get() + + # (1) Solo player + return finished([selfQ], ['_user']) if _.isEmpty(groupsObj) + + ## (2) Party or Guild has members, fetch those users too + # Subscribe to the groups themselves. We separate them by _party, _guilds, and _habitRPG (the "global" guild). + groupsInfo = _.reduce groupsObj, ((m,g)-> + if g.type is 'guild' then m.guildIds.push(g.id) else m.partyId = g.id + m.members = m.members.concat(g.members) + m + ), {guildIds:[], partyId:null, members:[]} + + # Fetch, not subscribe. There's nothing dynamic we need from members, just the the Group (below) which includes chat, challenges, etc + model.query('users').publicInfo(groupsInfo.members).fetch (err, members) -> + return next(err) if err + # we need _members as an object in the view, so we can iterate over _party.members as :id, and access _members[:id] for the info + mObj = members.get() + model.set "_members", _.object(_.pluck(mObj,'id'), mObj) + model.set "_membersArray", mObj + + # Note - selfQ *must* come after membersQ in subscribe, otherwise _user will only get the fields restricted by party-members in store.coffee. Strang bug, but easy to get around + partyQ = model.query('groups').withIds(groupsInfo.partyId) + if _.isEmpty(groupsInfo.guildIds) + finished [partyQ, selfQ], ['_party', '_user'] + else + guildsQ = model.query('groups').withIds(groupsInfo.guildIds) + finished [partyQ, guildsQ, selfQ], ['_party', '_guilds', '_user'] # ========== ROUTES ========== diff --git a/src/server/store.coffee b/src/server/store.coffee index bab1a5c64c..df8d3e1b61 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -130,7 +130,7 @@ groupSystem = (store) -> Find group which has member by id ### store.query.expose "groups", "withMember", (id, type) -> - q = @where('members').contains([id])#.only(['id','members','type']) + q = @where('members').contains([id]).only(['id', 'type', 'name', 'description', 'members' , 'privacy']) q = q.where('type').equals(type) if type? store.queryAccess 'groups', 'withMember', publicAccess @@ -140,13 +140,7 @@ groupSystem = (store) -> store.query.expose "groups", "publicGroups", -> @where('privacy').equals('public') .where('type').equals('guild') -# .only [ -# 'name' -# 'description' -# 'users' -# 'members' -# 'privacy' -# ] + .only(['id', 'type', 'name', 'description', 'members' , 'privacy']) store.queryAccess "groups", "publicGroups", publicAccess ### From f9a978d0434aec148cc17253e7e0f3cb0168caad Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 13:06:03 -0400 Subject: [PATCH 075/157] groups: bug-fix to private party invitation --- src/app/groups.coffee | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index d8222c7b36..18f4217830 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -42,27 +42,23 @@ module.exports.app = (appExports, model, app) -> return model.set("_groupError", "User with id #{uid} not found.") unless profile model.query('groups').withMember(uid).fetch (err, g) -> throw err if err - - {type, name} = e.get() - gid = e.get('id') - groups = g.get() - groupError =(msg) -> model.set("_groupError", msg) + group = e.get(); groups = g.get() + {type, name} = group; gid = group.id + groupError = (msg) -> model.set("_groupError", msg) invite = -> - debugger $.bootstrapGrowl "Invitation Sent." - if type is 'guild' - model.push("users.#{uid}.invitations.guilds", {id:gid, name}, ->location.reload()) - else model.set "users.#{uid}.invitations.party", {id:gid, name}, -> - debugger - location.reload() + switch type + when 'guild' then model.push "users.#{uid}.invitations.guilds", {id:gid, name}, ->location.reload() + when 'party' then model.set "users.#{uid}.invitations.party", {id:gid, name}, ->location.reload() - if type is 'guild' - if _.find(profile.invitations.guilds, {id:gid}) - return groupError("User already invited to that group") - else if _.find groups, ((group)-> uid in group.members) - return groupError("User already in that group") - else invite() - if type is 'party' + switch type + when 'guild' + if _.find(profile.invitations.guilds, {id:gid}) + return groupError("User already invited to that group") + else if uid in group.members + return groupError("User already in that group") + else invite() + when 'party' if profile.invitations.party return groupError("User already pending invitation.") else if _.find(groups, {type:'party'}) From 770af3c4d45c808a5223c8903537097486236e4b Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 13:27:00 -0400 Subject: [PATCH 076/157] groups: group-leave bug fixes (most things have to be static-bound, when somethign is deleted it causes errors on dom listeners). also make second-most-recent-member the new leader if the leaver is leader --- src/app/groups.coffee | 10 ++++++++-- views/app/groups.html | 14 +++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index 18f4217830..d1a6201bb6 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -84,12 +84,18 @@ module.exports.app = (appExports, model, app) -> else e.at().remove clear appExports.groupLeave = (e,el) -> + uid = user.get('id') group = model.at "groups.#{$(el).attr('data-id')}" - index = group.get('members').indexOf(user.get('id')) + index = group.get('members').indexOf(uid) if index != -1 group.remove 'members', index, 1, -> - if _.isEmpty group.get('members') # last member out, delete the party + updated = group.get() + # last member out, delete the party + if _.isEmpty(updated.members) group.del ->location.reload() + # assign new leader, so the party is editable #TODO allow old leader to assign new leader, this is just random + else if (updated.leader is uid) + group.set "leader", updated.members[0], ->location.reload() else location.reload() ### diff --git a/views/app/groups.html b/views/app/groups.html index e9a68495ca..89954a907a 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -27,9 +27,9 @@
    @@ -47,11 +47,11 @@
    - {#each _guilds as :guild} -
    - + {{#each _guilds as :guild}} +
    +
    - {/} + {{/}}
    From e4c727520149ee380d348a9d942c06c497cc0323 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 14:58:19 -0400 Subject: [PATCH 077/157] groups: move title above info box --- views/app/groups.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/views/app/groups.html b/views/app/groups.html index 89954a907a..7fdbeb0939 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -7,7 +7,7 @@
    {#if _party.id} - + {else if _user.invitations.party}

    You're Invited To {_user.invitations.party.name}

    @@ -28,7 +28,7 @@ @@ -143,6 +143,7 @@
    {{else}} +

    {@group.name}

    @@ -167,7 +168,7 @@

    Resources

    {/} @@ -176,7 +177,6 @@ {{#if equal(@group.leader,_user.id)}} {{/}} -

    {@group.name}

    {@group.description}
    {#if @group.websites}

    Resources

    From 47453d6133e5cbeb7b083bab5fcd488116f2e296 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 18:28:33 -0400 Subject: [PATCH 078/157] challenges: simply display names of challenges in groups, with "visit the challenges tab for more info". --- views/app/groups.html | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/views/app/groups.html b/views/app/groups.html index 7fdbeb0939..22cb51a343 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -101,8 +101,8 @@ {{#if equal(@type,'guild')}}
    - Public - Invite Only + Public + Invite Only
    {{/}} @@ -231,15 +231,19 @@
    - - {{#each @group.challenges as :challenge}} - - {{/}} -
    - {{:challenge.name}} -
    + {#if @group.challenges} + + {#each @group.challenges as :challenge} + + {/} +
    + {:challenge.name} +
    + Visit the Challenges for more information. + {else} + No challenges yet, visit the Challenges tab to create one. + {/}
    -
    From f92ef9935552dcd92b6933c461faa47c15b6b404 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 19:03:03 -0400 Subject: [PATCH 079/157] groups: show groups as striped table instead of accordion. cleaner this way --- views/app/groups.html | 46 ++++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/views/app/groups.html b/views/app/groups.html index 22cb51a343..68fb5e75d2 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -62,33 +62,25 @@
    -
    - {{#each _publicGroups as :public}} -
    -
    -
      -
    • {{count(:public.members)}} member(s)
    • -
    • - - {{#if indexOf(:public.members,_user.id)}} - Leave - {{else}} - Join - {{/}} -
    • -
    - - {{:public.name}} - -
    -
    -
    -
    {{:public.description}}
    -
    -
    -
    - {{/}} -
    + + {#each _publicGroups as :public} + + {/} +
    +
      +
    • {count(:public.members)} member(s)
    • +
    • + + {#if indexOf(:public.members,_user.id)} + Leave + {else} + Join + {/} +
    • +
    +

    {:public.name}

    +

    {:public.description}

    +
    From c47291fe7c7042f0f00673f76fc440ae74f119c1 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 22:37:59 -0400 Subject: [PATCH 080/157] challenges: fix dynamic user-challenge score --- src/app/misc.coffee | 4 ++++ views/app/challenges.html | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index 1964b42625..1236949db5 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -205,3 +205,7 @@ module.exports.viewHelpers = (view) -> taskInChallenge.call(@,task)?.get(attr) view.fn 'brokenChallengeLink', (task) -> task?.challenge and !(taskInChallenge.call(@,task)?.get()) + + view.fn 'challengeMemberScore', (member, tType, tid) -> + Math.round(member["#{tType}s"]?[tid]?.value) + diff --git a/views/app/challenges.html b/views/app/challenges.html index 272a66f355..c5ef9cba1e 100644 --- a/views/app/challenges.html +++ b/views/app/challenges.html @@ -149,7 +149,8 @@ {#each @challenge[@taskType]s as :task}
    - {:task.text}: {round(@member[@taskType]s[:task.id].value)} + + {:task.text}: {challengeMemberScore(@member,@taskType,:task.id)}
    From c6df323b07d7e429c8858cba34d90fa16f6f6c2b Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 22:57:07 -0400 Subject: [PATCH 081/157] challenges: graph rendering now dynamic --- src/app/challenges.coffee | 5 +++-- views/app/game-pane.html | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee index 7d5d1b1ffd..5d61133a7c 100644 --- a/src/app/challenges.coffee +++ b/src/app/challenges.coffee @@ -5,13 +5,13 @@ module.exports.app = (appExports, model) -> browser = require './browser' user = model.at '_user' - appExports.renderChallengeGraphs = -> + $('#profile-challenges-tab-link').on 'show', (e) -> _.each model.get('groups'), (g) -> _.each g.challenges, (chal) -> _.each ['habit','daily','todo'], (type) -> _.each chal["#{type}s"], (task) -> _.each chal.users, (member) -> - if (history = member["#{type}s"][task.id].history) and !!history + if (history = member?["#{type}s"]?[task.id]?.history) and !!history data = google.visualization.arrayToDataTable _.map(history, (h)-> [h.date,h.value]) options = backgroundColor: { fill:'transparent' } @@ -25,6 +25,7 @@ module.exports.app = (appExports, model) -> chart = new google.visualization.LineChart $(".challenge-#{chal.id}-member-#{member.id}-history-#{task.id}")[0] chart.draw(data, options) + appExports.challengeCreate = (e,el) -> [type, gid] = [$(el).attr('data-type'), $(el).attr('data-gid')] model.set '_challenge.new', diff --git a/views/app/game-pane.html b/views/app/game-pane.html index 9211621806..42ab861773 100644 --- a/views/app/game-pane.html +++ b/views/app/game-pane.html @@ -14,7 +14,7 @@ {/if}
  • Tavern
  • Achievements
  • -
  • Challenges
  • +
  • Challenges
  • Settings
  • From 33ac1788fd3118472df1a57972e596675244b81d Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 23:12:31 -0400 Subject: [PATCH 082/157] groups: add group leader message --- src/app/groups.coffee | 4 ++++ views/app/groups.html | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index d1a6201bb6..2ff6617802 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -26,6 +26,10 @@ module.exports.app = (appExports, model, app) -> path = "_editing.groups.#{$(el).attr('data-gid')}" model.set path, !model.get(path) + appExports.toggleLeaderMessageEdit = (e, el) -> + path = "_editing.leaderMessage.#{$(el).attr('data-gid')}" + model.set path, !model.get(path) + appExports.groupAddWebsite = (e, el) -> test = e.get() e.at().unshift 'websites', model.get('_newGroupWebsite') diff --git a/views/app/groups.html b/views/app/groups.html index 68fb5e75d2..85670693a7 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -262,6 +262,26 @@ {{else}} + {{#if equal(@group.leader,_user.id)}} + {#if _editing.leaderMessage[@group.id]} + + + {else} + Edit leader message + {/} + {{/}} + {#if @group.leaderMessage} + + + +
    +
    +
    +

    {{username(_members[@group.leader].auth,_members[@group.leader].profile.name)}}

    +
    {@group.leaderMessage}
    +
    +
    + {/}

    Chat

    {{/}} From 9be6f1a0ddbaccc2fc901adbc141c6abf4baab28 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 23:35:36 -0400 Subject: [PATCH 083/157] groups: static-binding of _party, was causing "Uncaught TypeError:Cannot read property '0' of undefined" since groups[_party.id] doesn't exist pre-subcription --- views/app/groups.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/views/app/groups.html b/views/app/groups.html index 85670693a7..0d8ab12f1b 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -6,22 +6,22 @@
    - {#if _party.id} + {{#if _party.id}} - {else if _user.invitations.party} + {{else if _user.invitations.party}}

    You're Invited To {_user.invitations.party.name}

    {#with _user.invitations.party} Accept Reject {/} - {else} + {{else}}

    Create A Party

    You are not in a party. You can either create one and invite friends, or if you want to join an existing party, have them enter:

    {_user.id}
    - {/} + {{/}}
    From 00d62edbc9a72dff78e246c5ba453430d7e358bf Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 23:41:47 -0400 Subject: [PATCH 084/157] !IMPORTANT! xhr-polling for heroku, while testing challenges. remember to revert when pushing to prod --- src/server/index.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/index.coffee b/src/server/index.coffee index 3f707666e2..57e777128d 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -16,7 +16,7 @@ helpers = require("habitrpg-shared/script/helpers") ## RACER CONFIGURATION ## -#racer.io.set('transports', ['xhr-polling']) +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' From e565659ef33fd56ec20a65d606fdb92893fd831a Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 23:56:33 -0400 Subject: [PATCH 085/157] change "report a problem" link to go to FAQ, to cut back on all these duplicates --- views/app/groups.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/app/groups.html b/views/app/groups.html index 0d8ab12f1b..7ab3a1c29c 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -129,7 +129,7 @@
  • LFG Posts

  • Tutorial

  • FAQ

  • -
  • Report a Problem

  • +
  • Report a Problem

  • Request a Feature

  • Community Forum

  • From 4bd6246e75a9ef1dbe4ea32fbd53c51bb1b78a5b Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 2 Jun 2013 23:57:59 -0400 Subject: [PATCH 086/157] change "report a problem" link to go to FAQ, to cut back on all these duplicates --- views/app/groups.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/app/groups.html b/views/app/groups.html index 7ab3a1c29c..197c8d08b8 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -253,7 +253,7 @@ From fbcf21cd9d15a53568c6ee56c7f11fe65dd41dfc Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 00:16:02 -0400 Subject: [PATCH 087/157] groups: optimize public groups fetch by putting in same fetch as myGroups (but before, due to projections bug) --- src/app/index.coffee | 89 ++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/src/app/index.coffee b/src/app/index.coffee index 125fb5566a..2da29431fd 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -34,55 +34,54 @@ setupSubscriptions = (page, model, params, next, cb) -> # Fetch public groups as _publicGroups # Note: due to https://github.com/codeparty/racer/issues/57, this has to come at the very beginning. The more limited # the returned fields in motifs, the sooner they must come in fetch / subscribes. - model.query('groups').publicGroups().fetch (err, pg) -> + publicGroupsQuery = model.query('groups').publicGroups() + myGroupsQuery = model.query('groups').withMember(uuid) + model.fetch publicGroupsQuery, myGroupsQuery, (err, publicGroups, groups) -> return next(err) if err - model.set '_publicGroups', _.sortBy(pg.get(), (g) -> -_.size(g.members)) + model.set '_publicGroups', _.sortBy(publicGroups.get(), (g) -> -_.size(g.members)) + finished = (descriptors, paths) -> + # Add public "Tavern" guild in + descriptors.push('groups.habitrpg'); paths.push('_habitRPG') - model.query('groups').withMember(uuid).fetch (err, groups) -> - return next(err) if err - finished = (descriptors, paths) -> - # Add public "Tavern" guild in - descriptors.push('groups.habitrpg'); paths.push('_habitRPG') - - # Subscribe to each descriptor - model.subscribe.apply model, descriptors.concat -> - [err, refs] = [arguments[0], arguments] - return next(err) if err - _.each paths, (path, idx) -> model.ref path, refs[idx+1]; true - unless model.get('_user') - console.error "User not found - this shouldn't be happening!" - return page.redirect('/logout') #delete model.session.userId - - return cb() - - groupsObj = groups.get() - - # (1) Solo player - return finished([selfQ], ['_user']) if _.isEmpty(groupsObj) - - ## (2) Party or Guild has members, fetch those users too - # Subscribe to the groups themselves. We separate them by _party, _guilds, and _habitRPG (the "global" guild). - groupsInfo = _.reduce groupsObj, ((m,g)-> - if g.type is 'guild' then m.guildIds.push(g.id) else m.partyId = g.id - m.members = m.members.concat(g.members) - m - ), {guildIds:[], partyId:null, members:[]} - - # Fetch, not subscribe. There's nothing dynamic we need from members, just the the Group (below) which includes chat, challenges, etc - model.query('users').publicInfo(groupsInfo.members).fetch (err, members) -> + # Subscribe to each descriptor + model.subscribe.apply model, descriptors.concat -> + [err, refs] = [arguments[0], arguments] return next(err) if err - # we need _members as an object in the view, so we can iterate over _party.members as :id, and access _members[:id] for the info - mObj = members.get() - model.set "_members", _.object(_.pluck(mObj,'id'), mObj) - model.set "_membersArray", mObj + _.each paths, (path, idx) -> model.ref path, refs[idx+1]; true + unless model.get('_user') + console.error "User not found - this shouldn't be happening!" + return page.redirect('/logout') #delete model.session.userId - # Note - selfQ *must* come after membersQ in subscribe, otherwise _user will only get the fields restricted by party-members in store.coffee. Strang bug, but easy to get around - partyQ = model.query('groups').withIds(groupsInfo.partyId) - if _.isEmpty(groupsInfo.guildIds) - finished [partyQ, selfQ], ['_party', '_user'] - else - guildsQ = model.query('groups').withIds(groupsInfo.guildIds) - finished [partyQ, guildsQ, selfQ], ['_party', '_guilds', '_user'] + return cb() + + groupsObj = groups.get() + + # (1) Solo player + return finished([selfQ], ['_user']) if _.isEmpty(groupsObj) + + ## (2) Party or Guild has members, fetch those users too + # Subscribe to the groups themselves. We separate them by _party, _guilds, and _habitRPG (the "global" guild). + groupsInfo = _.reduce groupsObj, ((m,g)-> + if g.type is 'guild' then m.guildIds.push(g.id) else m.partyId = g.id + m.members = m.members.concat(g.members) + m + ), {guildIds:[], partyId:null, members:[]} + + # Fetch, not subscribe. There's nothing dynamic we need from members, just the the Group (below) which includes chat, challenges, etc + model.query('users').publicInfo(groupsInfo.members).fetch (err, members) -> + return next(err) if err + # we need _members as an object in the view, so we can iterate over _party.members as :id, and access _members[:id] for the info + mObj = members.get() + model.set "_members", _.object(_.pluck(mObj,'id'), mObj) + model.set "_membersArray", mObj + + # Note - selfQ *must* come after membersQ in subscribe, otherwise _user will only get the fields restricted by party-members in store.coffee. Strang bug, but easy to get around + partyQ = model.query('groups').withIds(groupsInfo.partyId) + if _.isEmpty(groupsInfo.guildIds) + finished [partyQ, selfQ], ['_party', '_user'] + else + guildsQ = model.query('groups').withIds(groupsInfo.guildIds) + finished [partyQ, guildsQ, selfQ], ['_party', '_guilds', '_user'] # ========== ROUTES ========== From f8e546ae2af42ddf1d65c23b7fafbbc470100ccb Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 14:08:54 -0400 Subject: [PATCH 088/157] groups: bug fixes: {#if _party} dynamic binding, subscription allowed for either party or guild with out requiring the other --- src/app/index.coffee | 23 +++++++++++++---------- src/server/store.coffee | 33 +++++++++++++++------------------ views/app/groups.html | 8 ++++---- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/app/index.coffee b/src/app/index.coffee index 2da29431fd..bde4b038d7 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -31,17 +31,15 @@ setupSubscriptions = (page, model, params, next, cb) -> uuid = model.get('_userId') or model.session.userId # see http://goo.gl/TPYIt selfQ = model.query('users').withId(uuid) #keep this for later - # Fetch public groups as _publicGroups # Note: due to https://github.com/codeparty/racer/issues/57, this has to come at the very beginning. The more limited # the returned fields in motifs, the sooner they must come in fetch / subscribes. publicGroupsQuery = model.query('groups').publicGroups() myGroupsQuery = model.query('groups').withMember(uuid) model.fetch publicGroupsQuery, myGroupsQuery, (err, publicGroups, groups) -> return next(err) if err - model.set '_publicGroups', _.sortBy(publicGroups.get(), (g) -> -_.size(g.members)) finished = (descriptors, paths) -> # Add public "Tavern" guild in - descriptors.push('groups.habitrpg'); paths.push('_habitRPG') + descriptors.unshift('groups.habitrpg'); paths.unshift('_habitRPG') # Subscribe to each descriptor model.subscribe.apply model, descriptors.concat -> @@ -51,9 +49,11 @@ setupSubscriptions = (page, model, params, next, cb) -> unless model.get('_user') console.error "User not found - this shouldn't be happening!" return page.redirect('/logout') #delete model.session.userId - return cb() + # Get public groups first, order most-to-least # subscribers + model.set '_publicGroups', _.sortBy(publicGroups.get(), (g) -> -_.size(g.members)) + groupsObj = groups.get() # (1) Solo player @@ -76,12 +76,15 @@ setupSubscriptions = (page, model, params, next, cb) -> model.set "_membersArray", mObj # Note - selfQ *must* come after membersQ in subscribe, otherwise _user will only get the fields restricted by party-members in store.coffee. Strang bug, but easy to get around - partyQ = model.query('groups').withIds(groupsInfo.partyId) - if _.isEmpty(groupsInfo.guildIds) - finished [partyQ, selfQ], ['_party', '_user'] - else - guildsQ = model.query('groups').withIds(groupsInfo.guildIds) - finished [partyQ, guildsQ, selfQ], ['_party', '_guilds', '_user'] + descriptors = [selfQ]; paths = ['_user'] + if groupsInfo.partyId + descriptors.unshift model.query('groups').withIds(groupsInfo.partyId) + paths.unshift '_party' + unless _.isEmpty(groupsInfo.guildIds) + descriptors.unshift model.query('groups').withIds(groupsInfo.guildIds) + paths.unshift '_guilds' + finished descriptors, paths + # ========== ROUTES ========== diff --git a/src/server/store.coffee b/src/server/store.coffee index df8d3e1b61..4c81c2775c 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -109,23 +109,19 @@ groupSystem = (store) -> 'auth.facebook.displayName') store.queryAccess "users", "publicInfo", publicAccess - ### - Fetch group info (ie, they just got invited) - ### - store.query.expose "groups", "withIds", (ids) -> - return unless ids #FIXME this is sometimes null when ids is array (guilds) - if typeof ids is 'string' - @where("id").equals(ids).findOne() # find a single group - else - @where("id").within(ids) # find multiple groups - store.queryAccess "groups", "withIds", publicAccess - ### Read / Write groups, so they can create new groups ### store.readPathAccess "groups.*", publicAccess store.writeAccess "*", "groups.*", publicAccess + ### + Public HabitRPG Guild + ### + store.readPathAccess 'groups.habitrpg', publicAccess + store.writeAccess "*", "groups.habitrpg.chat.*", publicAccess + store.writeAccess "*", "groups.habitrpg.challenges.*", publicAccess + ### Find group which has member by id ### @@ -144,11 +140,12 @@ groupSystem = (store) -> store.queryAccess "groups", "publicGroups", publicAccess ### - Public HabitRPG Guild + Fetch group info (ie, they just got invited) ### - - store.readPathAccess 'groups.habitrpg', publicAccess - store.writeAccess "*", "groups.habitrpg.chat.*", publicAccess - store.writeAccess "*", "groups.habitrpg.challenges.*", publicAccess - - + store.query.expose "groups", "withIds", (ids) -> + return unless ids #FIXME this is sometimes null when ids is array (guilds) + if typeof ids is 'string' + @where("id").equals(ids).findOne() # find a single group + else + @where("id").within(ids) # find multiple groups + store.queryAccess "groups", "withIds", publicAccess \ No newline at end of file diff --git a/views/app/groups.html b/views/app/groups.html index 197c8d08b8..7e5ae23f2e 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -6,22 +6,22 @@
    - {{#if _party.id}} + {#if _party.id} - {{else if _user.invitations.party}} + {else if _user.invitations.party}

    You're Invited To {_user.invitations.party.name}

    {#with _user.invitations.party} Accept Reject {/} - {{else}} + {else}

    Create A Party

    You are not in a party. You can either create one and invite friends, or if you want to join an existing party, have them enter:

    {_user.id}
    - {{/}} + {/}
    From aa684fe52e08613d435e33b0b0f4303525ed2888 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 14:20:22 -0400 Subject: [PATCH 089/157] comments on cron stuff --- src/app/index.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/index.coffee b/src/app/index.coffee index bde4b038d7..ea0d31781a 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -129,7 +129,10 @@ ready (model) -> # habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks _.each ['habit','daily','todo','reward'], (type) -> uObj["#{type}s"] = _.where(uObj.tasks, {type}); true algos.cron uObj, {paths} + # for new user, just set lastCron - no need to reset dom. + # remember that the properties are set from uObj & paths AFTER the return of this callback return if _.isEmpty(paths) or (paths['lastCron'] and _.size(paths) is 1) + # for everyone else, we need to reset dom - too many changes have been made and won't it breaks dom listeners. if lostHp = delete paths['stats.hp'] # we'll set this manually so we can get a cool animation setTimeout -> browser.resetDom(model) From f91427b112020147eaf56fb2482b042cce9fa5c4 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 14:36:11 -0400 Subject: [PATCH 090/157] groups: small html modifications --- views/app/groups.html | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/views/app/groups.html b/views/app/groups.html index 7e5ae23f2e..c88a71ebb0 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -6,18 +6,17 @@
    - {#if _party.id} + {#if _party.id} {else if _user.invitations.party} - -

    You're Invited To {_user.invitations.party.name}

    - {#with _user.invitations.party} - Accept - Reject + + {#with _user.invitations.party as :party} +

    You're Invited To {:party.name}

    + Accept + Reject {/} {else}

    Create A Party

    -

    You are not in a party. You can either create one and invite friends, or if you want to join an existing party, have them enter:

    {_user.id}
    From 2bb7ed37b99253ce3a9bbaebd6eb884465bec1a5 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 14:41:48 -0400 Subject: [PATCH 091/157] challenges: hide challenges tab for now --- views/app/game-pane.html | 2 +- views/app/groups.html | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/views/app/game-pane.html b/views/app/game-pane.html index 42ab861773..fb568f859c 100644 --- a/views/app/game-pane.html +++ b/views/app/game-pane.html @@ -14,7 +14,7 @@ {/if}
  • Tavern
  • Achievements
  • -
  • Challenges
  • +
  • Settings
  • diff --git a/views/app/groups.html b/views/app/groups.html index c88a71ebb0..bb3855441b 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -222,7 +222,8 @@
    - {#if @group.challenges} + Challenges coming soon! Details +
    From 0d875969080b64de570293c210d24ccc9ceff645 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 14:42:52 -0400 Subject: [PATCH 092/157] Revert "!IMPORTANT! xhr-polling for heroku, while testing challenges. remember" This reverts commit 00d62edbc9a72dff78e246c5ba453430d7e358bf. --- src/server/index.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/index.coffee b/src/server/index.coffee index 57e777128d..3f707666e2 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -16,7 +16,7 @@ helpers = require("habitrpg-shared/script/helpers") ## RACER CONFIGURATION ## -racer.io.set('transports', ['xhr-polling']) +#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' From d6069e71749981a6589d35967535d105102b0832 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 14:51:58 -0400 Subject: [PATCH 093/157] guilds: bailey. --- views/app/alerts.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/views/app/alerts.html b/views/app/alerts.html index bd2de15273..89dde6ce33 100644 --- a/views/app/alerts.html +++ b/views/app/alerts.html @@ -14,6 +14,11 @@

    +

    5/03/2013

    +
      +
    • Guilds! You can now belong to multiple groups, not just your party. There are public and private guilds, think "Subreddits" v "multiple friend groups".
    • +
    +

    5/27/2013

    • Get the "Helped Habit Grow" badge by filling out this survey.
    • From 7c829afb0957db870d9dc5d0c9987247a25210d2 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 15:45:49 -0400 Subject: [PATCH 094/157] guilds: bug fix for inconsistently invisible public guilds --- views/app/groups.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/views/app/groups.html b/views/app/groups.html index bb3855441b..ec3ee12a5b 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -34,7 +34,8 @@
      - {#if _user.invitations.guilds} + + {#if and(_user.invitations,_user.invitations.guilds)} {#each _user.invitations.guilds as :invitation}

      You're Invited To {:invitation.name}

      From 658b4aa2a4788381a0e2bb17bdb866e2876604b9 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 16:09:37 -0400 Subject: [PATCH 095/157] groups: bug fix to can't invite new user for users who haven't already been migrated --- src/app/groups.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index 2ff6617802..222579f268 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -57,13 +57,13 @@ module.exports.app = (appExports, model, app) -> switch type when 'guild' - if _.find(profile.invitations.guilds, {id:gid}) + if profile.invitations?.guilds and _.find(profile.invitations.guilds, {id:gid}) return groupError("User already invited to that group") else if uid in group.members return groupError("User already in that group") else invite() when 'party' - if profile.invitations.party + if profile.invitations?.party return groupError("User already pending invitation.") else if _.find(groups, {type:'party'}) return groupError("User already in a party.") From 42b7848a6090f07d112fea36bb0466de6186abed Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 16:55:57 -0400 Subject: [PATCH 096/157] guilds: now costs 4G to create a guild, as per http://community.habitrpg.com/content/charge-gems-guild-creation --- src/app/groups.coffee | 17 +++++++++++++++-- views/app/groups.html | 39 +++++++++++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index 222579f268..150a107e9e 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -19,8 +19,21 @@ module.exports.app = (appExports, model, app) -> leader: user.get('id') members: [user.get('id')] type: type - newGroup.privacy = (model.get("_new.group.privacy") || 'public') if type is 'guild' - model.add 'groups', newGroup, ->location.reload() + + # parties - free + if type is 'party' + return model.add 'groups', newGroup, ->location.reload() + + # guilds - 4G + balance = user.get('balance') + unless balance >= 1 + return $('#more-gems-modal').modal 'show' + if confirm "Create Guild for 4 Gems?" + newGroup.privacy = (model.get("_new.group.privacy") || 'public') if type is 'guild' + newGroup.balance = 1 # they spent $ to open the guild, it goes into their guild bank + model.add 'groups', newGroup, -> + user.set 'balance', (balance - 1) + location.reload() appExports.toggleGroupEdit = (e, el) -> path = "_editing.groups.#{$(el).attr('data-gid')}" diff --git a/views/app/groups.html b/views/app/groups.html index ec3ee12a5b..bd56cd9cd7 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -83,21 +83,44 @@
    - + {#if _groupError}
    {_groupError}
    {/}
    - - +
    + +
    + +
    +
    +
    + +
    + +
    +
    {{#if equal(@type,'guild')}} -
    - Public - Invite Only -
    +
    +
    + + + 4 Gems +
    +
    + {{else}} +
    +
    + +
    +
    {{/}} - +
    From b52888f4911c3d6f8446f4626d421925ab52535d Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 16:58:19 -0400 Subject: [PATCH 097/157] fix bailey date --- 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 89dde6ce33..e5be824953 100644 --- a/views/app/alerts.html +++ b/views/app/alerts.html @@ -14,7 +14,7 @@

    -

    5/03/2013

    +

    6/03/2013

    • Guilds! You can now belong to multiple groups, not just your party. There are public and private guilds, think "Subreddits" v "multiple friend groups".
    From 501e016e099150583a92cf1d8bd662bb815d90d5 Mon Sep 17 00:00:00 2001 From: Slappybag Date: Mon, 3 Jun 2013 23:08:42 +0100 Subject: [PATCH 098/157] CSV Export supports null values --- migrations/csvexport.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/migrations/csvexport.py b/migrations/csvexport.py index f50799e73b..4aa607cb33 100644 --- a/migrations/csvexport.py +++ b/migrations/csvexport.py @@ -1,10 +1,10 @@ import csv -data = csv.reader(open('/home/slappybag/backrs/800dollar.csv', 'rb'), delimiter=",", quotechar='|') -column = [] +with open(r"/home/slappybag/Documents/SurveyScrape.csv") as f: + reader = csv.reader(f, delimiter=',', quotechar='"') + column = [] + for row in reader: + if row: + column.append(row[4]) -for row in data: - column.append(row[9]) - -print "one:" print column \ No newline at end of file From 81297823da5f1db23edd9abd267cbf1820282f84 Mon Sep 17 00:00:00 2001 From: Slappybag Date: Mon, 3 Jun 2013 23:08:58 +0100 Subject: [PATCH 099/157] Added gem explanation text --- views/app/groups.html | 1 + 1 file changed, 1 insertion(+) diff --git a/views/app/groups.html b/views/app/groups.html index bd56cd9cd7..a91396ef6d 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -111,6 +111,7 @@ Invite Only 4 Gems +

    The Gem cost promotes high quality guilds and is transferred into your guild's bank so you can use as rewards in the upcoming challenges feature!

    {{else}} From bba008bd6139f1832fa61a75c9071e9ad7f76a03 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 19:15:05 -0400 Subject: [PATCH 100/157] guild: add guild bank popover --- views/app/groups.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/views/app/groups.html b/views/app/groups.html index bd56cd9cd7..373fff2a69 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -126,6 +126,12 @@ + {{#if equal(@group.type,'guild')}} + + +
    {{@group.balance}} Guild Gems
    +
    + {{/}}
    {{#if equal(@group.id,'habitrpg')}} From c1cda305824770a73bc19cec8a0534fcb2423af3 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 19:15:48 -0400 Subject: [PATCH 101/157] achievements: add migration for survey achievements --- migrations/20130602_survey_rewards.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 migrations/20130602_survey_rewards.js diff --git a/migrations/20130602_survey_rewards.js b/migrations/20130602_survey_rewards.js new file mode 100644 index 0000000000..0329643f4f --- /dev/null +++ b/migrations/20130602_survey_rewards.js @@ -0,0 +1,20 @@ +//mongo habitrpg ./node_modules/lodash/lodash.js migrations/20130602_survey_rewards.js + +var members = []; +members = _.uniq(members); +print(members.length) + +db.users.update({ + _id: {$exists:1}, + $or:[ + {_id: {$in: members}}, +// {'profile.name': {$in: members}}, + {'auth.facebook.name': {$in: members}}, + {'auth.local.username': {$in: members}}, + {'auth.local.email': {$in: members}} + ] +}, +{ + $set: { 'achievements.helpedHabit': true }, + $inc: { balance: (2.5) } +}) \ No newline at end of file From 35e0e404c7e7d49174e377969ca87ed673175460 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 20:18:11 -0400 Subject: [PATCH 102/157] groups: don't show gems for tavern --- views/app/groups.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/app/groups.html b/views/app/groups.html index d8ffae656c..c9306bd446 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -127,7 +127,7 @@ - {{#if equal(@group.type,'guild')}} + {{#if and(equal(@group.type,'guild'),not(equal(@group.id,'habitrpg')))}}
    {{@group.balance}} Guild Gems
    From d13db1623f3d75e365ade38a33875ef42ae316d3 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 20:22:51 -0400 Subject: [PATCH 103/157] fix survey gem inc --- migrations/20130602_survey_rewards.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20130602_survey_rewards.js b/migrations/20130602_survey_rewards.js index 0329643f4f..02ea768b0e 100644 --- a/migrations/20130602_survey_rewards.js +++ b/migrations/20130602_survey_rewards.js @@ -16,5 +16,5 @@ db.users.update({ }, { $set: { 'achievements.helpedHabit': true }, - $inc: { balance: (2.5) } + $inc: { balance: 2.5 } }) \ No newline at end of file From 9ec6455f2a77a5b2cbbd26dbe481367f7f078dcc Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 20:46:56 -0400 Subject: [PATCH 104/157] groups: some ridiculous workarounds on the model.push/unshift bug --- src/app/groups.coffee | 5 +++++ src/app/misc.coffee | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index 150a107e9e..a30dad6f79 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -128,6 +128,11 @@ module.exports.app = (appExports, model, app) -> return unless /\S/.test text group = e.at() + + # get rid of duplicate member ids - this is a weird place to put it, but works for now + members = group.get('members'); uniqMembers = _.uniq(members) + group.set('members', uniqMembers) if !_.isEqual(uniqMembers, members) + chat = group.at('chat') model.set('_chatMessage', '') diff --git a/src/app/misc.coffee b/src/app/misc.coffee index 1236949db5..91c3b4f7ae 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -110,14 +110,15 @@ module.exports.fixCorruptUser = (model) -> user.del("tasks.#{key}") delete tasks[key] true - resetDom = false batchTxn model, (uObj, paths, batch) -> - ## fix https://github.com/lefnire/habitrpg/issues/1086 uniqPets = _.uniq(uObj.items.pets) batch.set('items.pets', uniqPets) if !_.isEqual(uniqPets, uObj.items.pets) + uniqInvites = _.uniq(uObj.invitations?.guilds) + batch.set('invitations.guilds', uniqInvites) if !_.isEqual(uniqInvites, uObj.invitations?.guilds) + ## Task List Cleanup ['habit','daily','todo','reward'].forEach (type) -> From 985b7676d8c0f8c887795bf6e38351d64f1e9d32 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 20:52:56 -0400 Subject: [PATCH 105/157] groups: subscriptions setup as callback after public members information fetched. otherwise we hit client/server snapshots different, and we get the field permissions error --- src/app/index.coffee | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/index.coffee b/src/app/index.coffee index ea0d31781a..069eef103b 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -75,15 +75,15 @@ setupSubscriptions = (page, model, params, next, cb) -> model.set "_members", _.object(_.pluck(mObj,'id'), mObj) model.set "_membersArray", mObj - # Note - selfQ *must* come after membersQ in subscribe, otherwise _user will only get the fields restricted by party-members in store.coffee. Strang bug, but easy to get around - descriptors = [selfQ]; paths = ['_user'] - if groupsInfo.partyId - descriptors.unshift model.query('groups').withIds(groupsInfo.partyId) - paths.unshift '_party' - unless _.isEmpty(groupsInfo.guildIds) - descriptors.unshift model.query('groups').withIds(groupsInfo.guildIds) - paths.unshift '_guilds' - finished descriptors, paths + # Note - selfQ *must* come after membersQ in subscribe, otherwise _user will only get the fields restricted by party-members in store.coffee. Strang bug, but easy to get around + descriptors = [selfQ]; paths = ['_user'] + if groupsInfo.partyId + descriptors.unshift model.query('groups').withIds(groupsInfo.partyId) + paths.unshift '_party' + unless _.isEmpty(groupsInfo.guildIds) + descriptors.unshift model.query('groups').withIds(groupsInfo.guildIds) + paths.unshift '_guilds' + finished descriptors, paths # ========== ROUTES ========== From 59f4c88940a3b6ef0fda724004ecaea99d335bb7 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 21:28:03 -0400 Subject: [PATCH 106/157] groups: guild bank is in gems, not balance --- src/app/misc.coffee | 2 +- views/app/groups.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index 91c3b4f7ae..f83ea7f8dd 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -169,7 +169,7 @@ module.exports.viewHelpers = (view) -> view.fn "encodeiCalLink", helpers.encodeiCalLink #User - view.fn "gems", (balance) -> return balance/0.25 + view.fn "gems", (balance) -> balance * 4 view.fn "username", helpers.username view.fn "tnl", algos.tnl view.fn 'equipped', helpers.equipped diff --git a/views/app/groups.html b/views/app/groups.html index c9306bd446..e033266d22 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -130,7 +130,7 @@ {{#if and(equal(@group.type,'guild'),not(equal(@group.id,'habitrpg')))}}
    -
    {{@group.balance}} Guild Gems
    +
    {{gems(@group.balance)}} Guild Gems
    {{/}}
    From 7fe86ac924ac18d69320f46f23944b7bcf116916 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 3 Jun 2013 22:38:03 -0400 Subject: [PATCH 107/157] fixes #1130 --- src/app/misc.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index f83ea7f8dd..3a41a22aae 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -116,8 +116,9 @@ module.exports.fixCorruptUser = (model) -> uniqPets = _.uniq(uObj.items.pets) batch.set('items.pets', uniqPets) if !_.isEqual(uniqPets, uObj.items.pets) - uniqInvites = _.uniq(uObj.invitations?.guilds) - batch.set('invitations.guilds', uniqInvites) if !_.isEqual(uniqInvites, uObj.invitations?.guilds) + if uObj.invitations?.guilds + uniqInvites = _.uniq(uObj.invitations.guilds) + batch.set('invitations.guilds', uniqInvites) if !_.isEqual(uniqInvites, uObj.invitations.guilds) ## Task List Cleanup ['habit','daily','todo','reward'].forEach (type) -> From a5a57a244bcbddb5bcded9fb9c6c73f69cc3de13 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 4 Jun 2013 10:32:33 -0400 Subject: [PATCH 108/157] fix survey migration --- migrations/20130602_survey_rewards.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/migrations/20130602_survey_rewards.js b/migrations/20130602_survey_rewards.js index 02ea768b0e..5f883f7ad1 100644 --- a/migrations/20130602_survey_rewards.js +++ b/migrations/20130602_survey_rewards.js @@ -1,20 +1,25 @@ //mongo habitrpg ./node_modules/lodash/lodash.js migrations/20130602_survey_rewards.js -var members = []; +var members = [] members = _.uniq(members); -print(members.length) -db.users.update({ +var query = { _id: {$exists:1}, $or:[ {_id: {$in: members}}, -// {'profile.name': {$in: members}}, + //{'profile.name': {$in: members}}, {'auth.facebook.name': {$in: members}}, {'auth.local.username': {$in: members}}, {'auth.local.email': {$in: members}} ] -}, -{ - $set: { 'achievements.helpedHabit': true }, - $inc: { balance: 2.5 } -}) \ No newline at end of file +}; + +print(db.users.count(query)); + +db.users.update(query, + { + $set: { 'achievements.helpedHabit': true }, + $inc: { balance: 2.5 } + }, + {multi:true} +) \ No newline at end of file From ac8312677214c127a22ac036e8e3d8d961a869c1 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 4 Jun 2013 13:10:55 -0400 Subject: [PATCH 109/157] grammar --- views/app/groups.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/app/groups.html b/views/app/groups.html index e033266d22..86e137afb1 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -151,7 +151,7 @@
    -
    Whilst resting your dailies are saved and aren't effected by day turn-over. Whether you check out tomorrow or in a weeks time you'll continue in the same state as when you checked in.
    +
    Whilst resting your dailies are saved and aren't affected by day turn-over. Whether you check out tomorrow or in a weeks time you'll continue in the same state as when you checked in.

    Resources

    From 6f1ac0c79e75ddbe00df7f06199144801fd873ae Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 5 Jun 2013 08:25:11 -0400 Subject: [PATCH 110/157] groups: confirm dialog for leaving group, only delete group on 0-members if party (since they paid for guild) --- src/app/groups.coffee | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index a30dad6f79..13d30ce87f 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -101,19 +101,20 @@ module.exports.app = (appExports, model, app) -> else e.at().remove clear appExports.groupLeave = (e,el) -> - uid = user.get('id') - group = model.at "groups.#{$(el).attr('data-id')}" - index = group.get('members').indexOf(uid) - if index != -1 - group.remove 'members', index, 1, -> - updated = group.get() - # last member out, delete the party - if _.isEmpty(updated.members) - group.del ->location.reload() - # assign new leader, so the party is editable #TODO allow old leader to assign new leader, this is just random - else if (updated.leader is uid) - group.set "leader", updated.members[0], ->location.reload() - else location.reload() + if confirm("Leave this group, are you sure?") is true + uid = user.get('id') + group = model.at "groups.#{$(el).attr('data-id')}" + index = group.get('members').indexOf(uid) + if index != -1 + group.remove 'members', index, 1, -> + updated = group.get() + # last member out, delete the party + if _.isEmpty(updated.members) and (updated.type is 'party') + group.del ->location.reload() + # assign new leader, so the party is editable #TODO allow old leader to assign new leader, this is just random + else if (updated.leader is uid) + group.set "leader", updated.members[0], ->location.reload() + else location.reload() ### Chat Functionality From 385704aad709eab70ff038cc406decdf4ada79f1 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 5 Jun 2013 11:50:30 -0400 Subject: [PATCH 111/157] add optional confirm dialog to removeAt() --- src/app/index.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/index.coffee b/src/app/index.coffee index 069eef103b..0b464a7b13 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -104,7 +104,12 @@ get '/', (page, model, params, next) -> # ========== CONTROLLER FUNCTIONS ========== ready (model) -> - exports.removeAt = (e) -> e.at().remove() # used for things like remove website, chat, etc + # used for things like remove website, chat, etc + exports.removeAt = (e, el) -> + if (confirmMessage = $(el).attr 'data-confirm')? + return unless confirm(confirmMessage) is true + debugger + e.at().remove() user = model.at('_user') misc.fixCorruptUser(model) # https://github.com/lefnire/habitrpg/issues/634 From fcf02d33d9d90990fc1bc49b087e05407d84a7ba Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 5 Jun 2013 11:55:09 -0400 Subject: [PATCH 112/157] groups: add "ban" feature for group leaders. unfortunately not updating dom, so require a refresh for now --- src/app/index.coffee | 14 +++++++------- views/app/groups.html | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/app/index.coffee b/src/app/index.coffee index 0b464a7b13..89cd993836 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -104,13 +104,6 @@ get '/', (page, model, params, next) -> # ========== CONTROLLER FUNCTIONS ========== ready (model) -> - # used for things like remove website, chat, etc - exports.removeAt = (e, el) -> - if (confirmMessage = $(el).attr 'data-confirm')? - return unless confirm(confirmMessage) is true - debugger - e.at().remove() - user = model.at('_user') misc.fixCorruptUser(model) # https://github.com/lefnire/habitrpg/issues/634 @@ -127,6 +120,13 @@ ready (model) -> require('./filters').app(exports, model) require('./challenges').app(exports, model) + # used for things like remove website, chat, etc + exports.removeAt = (e, el) -> + if (confirmMessage = $(el).attr 'data-confirm')? + return unless confirm(confirmMessage) is true + e.at().remove() + browser.resetDom(model) if $(el).attr('data-refresh') + ### Cron ### diff --git a/views/app/groups.html b/views/app/groups.html index 86e137afb1..164f59a71c 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -49,7 +49,7 @@ {{#each _guilds as :guild}}
    - +
    {{/}} @@ -220,9 +220,18 @@
    - {{#each @group.members as :memberId}} + {#each @group.members as :memberId} @@ -230,7 +239,7 @@ ({{:memberId}}) - {{/}} + {/}
    + + {{#if equal(@group.leader,_user.id)}} + {{#with @group.members[$index]}} + + + + {{/}} +   + {{/}} {{username(_members[:memberId].auth, _members[:memberId].profile.name)}}
    {#with @group as :group}
    From a7c8c843cde2de5e41659b6efdbb24392f75b82b Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 5 Jun 2013 14:14:00 -0400 Subject: [PATCH 113/157] groups: balance deduction cleanup --- src/app/groups.coffee | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index 13d30ce87f..ef1f363e24 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -25,15 +25,13 @@ module.exports.app = (appExports, model, app) -> return model.add 'groups', newGroup, ->location.reload() # guilds - 4G - balance = user.get('balance') - unless balance >= 1 + unless user.get('balance') >= 1 return $('#more-gems-modal').modal 'show' if confirm "Create Guild for 4 Gems?" newGroup.privacy = (model.get("_new.group.privacy") || 'public') if type is 'guild' newGroup.balance = 1 # they spent $ to open the guild, it goes into their guild bank model.add 'groups', newGroup, -> - user.set 'balance', (balance - 1) - location.reload() + user.incr 'balance', -1, ->location.reload() appExports.toggleGroupEdit = (e, el) -> path = "_editing.groups.#{$(el).attr('data-gid')}" From d944dc1687e54d0ab200cf906dfb46854a69110f Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 5 Jun 2013 14:18:09 -0400 Subject: [PATCH 114/157] groups: can't ban self. rename to "boot" (since i ahven't implemented full-ban) --- views/app/groups.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/views/app/groups.html b/views/app/groups.html index 164f59a71c..f6f240190f 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -222,16 +222,16 @@ {#each @group.members as :memberId} From 6480be63eb0e65dd1029c0f29a9d1a18700ab2c6 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 5 Jun 2013 14:44:59 -0400 Subject: [PATCH 115/157] groups: highlight leader in member list --- views/app/groups.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/views/app/groups.html b/views/app/groups.html index f6f240190f..388d79c7c5 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -232,7 +232,9 @@   {{/}} - {{username(_members[:memberId].auth, _members[:memberId].profile.name)}} + + {{username(_members[:memberId].auth, _members[:memberId].profile.name)}} +
    - - - {{#if equal(@group.leader,_user.id)}} - {{#with @group.members[$index]}} - - - - {{/}} -   + + {{#if and(equal(@group.leader,_user.id),not(equal(_user.id,:memberId)))}} + {{#with @group.members[$index]}} + + + {{/}} +   + {{/}} + {{username(_members[:memberId].auth, _members[:memberId].profile.name)}} From ef39dd8a0f0b603e23165692d4c1b1fcb91c45f9 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 5 Jun 2013 14:45:05 -0400 Subject: [PATCH 116/157] groups: can assign new leader --- src/app/groups.coffee | 5 +++++ views/app/groups.html | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index ef1f363e24..a580abbe5a 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -178,3 +178,8 @@ module.exports.app = (appExports, model, app) -> appExports.gotoPartyChat = -> model.set '_gamePane', true, -> $('#party-tab-link').tab('show') + + appExports.assignGroupLeader = (e, el) -> + newLeader = model.get('_new.groupLeader') + if newLeader and (confirm("Assign new leader, you sure?") is true) + e.at().set('leader', newLeader, ->browser.resetDom(model)) if newLeader diff --git a/views/app/groups.html b/views/app/groups.html index 388d79c7c5..6cd9c97600 100644 --- a/views/app/groups.html +++ b/views/app/groups.html @@ -185,6 +185,14 @@ +

    Assign Group Leader

    + + + {/} {#if @group.websites}

    Resources

    From a4de2e5ec5f916f054f7d84164fe21992c98d8e4 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 5 Jun 2013 16:20:03 -0400 Subject: [PATCH 117/157] don't show drop notification unless it went through to the database. oohhh this is glorious!! @Shaners @Slappybag @lemoness , you're gonna love this one. --- src/app/misc.coffee | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index 3a41a22aae..ce14ca9f2a 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -18,7 +18,7 @@ module.exports.batchTxn = batchTxn = (model, cb, options) -> # pass true if we have levelled to supress xp notification unless _.isEmpty paths setOps = _.reduce paths, ((m,v,k)-> m[k] = helpers.dotGet(k,uObj);m), {} - user.set "update__", setOps + user.set "update__", setOps, options?.done ret #TODO put this in habitrpg-shared @@ -44,6 +44,7 @@ taskInChallenge = (task) -> perform the updates while tracking paths, then all the values at those paths ### module.exports.score = (model, taskId, direction, allowUndo=false) -> + drop = undefined delta = batchTxn model, (uObj, paths) -> tObj = uObj.tasks[taskId] @@ -58,9 +59,7 @@ module.exports.score = (model, taskId, direction, allowUndo=false) -> delta = algos.score(uObj, tObj, direction, {paths}) model.set('_streakBonus', uObj._tmp.streakBonus) if uObj._tmp?.streakBonus - if uObj._tmp?.drop and $? - model.set '_drop', uObj._tmp.drop - $('#item-dropped-modal').modal 'show' + drop = uObj._tmp?.drop # Update challenge statistics # FIXME put this in it's own batchTxn, make batchTxn model.at() ref aware (not just _user) @@ -80,6 +79,10 @@ module.exports.score = (model, taskId, direction, allowUndo=false) -> value: tObj.value history: tObj.history model._dontPersist = true + , done:-> + if drop and $? + model.set '_drop', drop + $('#item-dropped-modal').modal 'show' delta From 6cc412313cc9ad8966224c442e946cd316aa9b71 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 5 Jun 2013 16:28:43 -0400 Subject: [PATCH 118/157] pets: only remove hatching potion & egg on successful user.push('pets') --- src/app/pets.coffee | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/pets.coffee b/src/app/pets.coffee index 2528672995..46288b5164 100644 --- a/src/app/pets.coffee +++ b/src/app/pets.coffee @@ -25,12 +25,11 @@ module.exports.app = (appExports, model) -> return alert "You don't own that egg yet, complete more tasks!" if eggIdx is -1 return alert "You already have that pet, hatch a different combo." if myPets and myPets.indexOf("#{egg.name}-#{hatchingPotionName}") != -1 - user.push 'items.pets', egg.name + '-' + hatchingPotionName - - eggs.splice eggIdx, 1 - myHatchingPotion.splice hatchingPotionIdx, 1 - user.set 'items.eggs', eggs - user.set 'items.hatchingPotions', myHatchingPotion + user.push 'items.pets', egg.name + '-' + hatchingPotionName, -> + eggs.splice eggIdx, 1 + myHatchingPotion.splice hatchingPotionIdx, 1 + user.set 'items.eggs', eggs + user.set 'items.hatchingPotions', myHatchingPotion alert 'Your egg hatched! Visit your stable to equip your pet.' From 205bf24e02ab2486cf3356ae272b8a0a5cbd7d10 Mon Sep 17 00:00:00 2001 From: Stan Lindsey Date: Wed, 5 Jun 2013 23:31:56 +0200 Subject: [PATCH 119/157] fixes 1116 --- styles/app/game-pane.styl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/styles/app/game-pane.styl b/styles/app/game-pane.styl index 1c2f139106..c24db0d7d6 100644 --- a/styles/app/game-pane.styl +++ b/styles/app/game-pane.styl @@ -36,4 +36,7 @@ height: 40px .buttonList li - margin: 5px; \ No newline at end of file + margin: 5px; + +.option-group .option-time + padding: 0px 5px From 903587b77650a43179bc9be0c161cc7376e64d29 Mon Sep 17 00:00:00 2001 From: Slappybag Date: Fri, 7 Jun 2013 11:50:24 +0100 Subject: [PATCH 120/157] Reworded 500.html to better represent the most likely cause of display - (server restart) --- public/500.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/500.html b/public/500.html index 9d4d7270d1..6539426274 100644 --- a/public/500.html +++ b/public/500.html @@ -26,8 +26,10 @@
    -

    The server is experiencing issues.

    -

    Try again in a few, the developer has been notified. The most likely culprit is this issue which Tyler is working to fix. (Any memory leak experts?)

    +

    The server is restarting.

    +

    Try again in a few. We restart often due to this issue which we're is working to fix. (Any memory leak experts?)

    + +

    If this page persists the server may be experiencing issues; the developers have been notified. Head over to the beta site.

    From 8aee73c79a0fbb3b5b7579d388424e5d67dd83cc Mon Sep 17 00:00:00 2001 From: Stan Lindsey Date: Fri, 7 Jun 2013 13:13:11 +0200 Subject: [PATCH 121/157] Slight rewording --- public/500.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/500.html b/public/500.html index 6539426274..163ae8e444 100644 --- a/public/500.html +++ b/public/500.html @@ -29,7 +29,7 @@

    The server is restarting.

    Try again in a few. We restart often due to this issue which we're is working to fix. (Any memory leak experts?)

    -

    If this page persists the server may be experiencing issues; the developers have been notified. Head over to the beta site.

    +

    If this page persists the server may be experiencing issues; the developers have been notified. Try switching to the beta site or the main site

    From 7c680af36862d7346654c63493ab3bf6116c45da Mon Sep 17 00:00:00 2001 From: Andre Casey Date: Fri, 7 Jun 2013 08:04:25 -0700 Subject: [PATCH 122/157] Fixes #1028: weird inventory item spacing for eggs and potions. My test instance would only generate 1 dropped potion (after so much clicking!), so perfect alignment of dropped potions is not guaranteed with this commit. --- styles/app/inventory.styl | 17 +++++++++++++++-- views/app/game-pane.html | 2 +- views/app/pets.html | 4 ++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/styles/app/inventory.styl b/styles/app/inventory.styl index 789d2d42d9..1cedbb12a7 100644 --- a/styles/app/inventory.styl +++ b/styles/app/inventory.styl @@ -20,13 +20,20 @@ width: 100% td padding: 0.5em - width: 25% + //width: 25% &.active-pet background-color: $bad outline: 1px solid rgba(0,0,0,0.1) outline-offset: -1px &:hover, &:focus background-color: darken($better, 10%) + > div + margin:auto + margin-bottom:.5em + p + text-align:center + width:6.5em + height:2.5em .current-pet left: 0px @@ -54,8 +61,14 @@ clear:both .pets-menu > div float:left -.hatchingPotions-menu > div + padding:.3em + p + text-align:center +.hatchingPotion-menu > div float:left + padding:.3em + p + text-align:center .pet-button border: none diff --git a/views/app/game-pane.html b/views/app/game-pane.html index fb568f859c..5f148db398 100644 --- a/views/app/game-pane.html +++ b/views/app/game-pane.html @@ -111,7 +111,7 @@ {/}

    Hatching Potions

    {#with _items.hatchingPotions as :hatchingPotion} - +
    {#with :hatchingPotion[0]}{/} {#with :hatchingPotion[1]}{/} diff --git a/views/app/pets.html b/views/app/pets.html index 59bef36f0a..494518f545 100644 --- a/views/app/pets.html +++ b/views/app/pets.html @@ -33,13 +33,13 @@ From d8fe775f8e4041aa7c80bea374149585b7af02c0 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 8 Jun 2013 12:43:10 -0400 Subject: [PATCH 123/157] task-deletion: confirm deletion no matterion what, don't dock HP if it's a red task they're deleting (http://community.habitrpg.com/node/350) --- src/app/tasks.coffee | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index d3e644487c..bc2a5e1331 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -34,30 +34,10 @@ module.exports.app = (appExports, model) -> newModel.set '' appExports.del = (e) -> - # Derby extends model.at to support creation from DOM nodes - task = e.at() - id = task.get('id') - - history = task.get('history') - if history and history.length > 2 - # prevent delete-and-recreate hack on red tasks - if task.get('value') < 0 - if confirm("Are you sure? Deleting this task will hurt you (to prevent deleting, then re-creating red tasks).") is true - task.set('type','habit') # hack to make sure it hits HP, instead of performing "undo checkbox" - misc.score(model, id, 'down', true) - else - return # Cancel. Don't delete, don't hurt user - - # prevent accidently deleting long-standing tasks - else - return unless confirm("Are you sure you want to delete this task?") is true - - #TODO bug where I have to delete from _users.tasks AND _{type}List, - # fix when query subscriptions implemented properly + return unless confirm("Are you sure you want to delete this task?") is true $('[rel=tooltip]').tooltip('hide') - - user.del('tasks.'+id) - task.remove() + user.del "tasks.#{e.get('id')}" + e.at().remove() appExports.clearCompleted = (e, el) -> From 3af13e69afab372792b1c9d0f06096ad847a7b8f Mon Sep 17 00:00:00 2001 From: Tim Ullrich Date: Mon, 10 Jun 2013 23:31:12 -0500 Subject: [PATCH 124/157] fix rounding discrepancy between header hp meter and profile hp --- views/app/header.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/app/header.html b/views/app/header.html index 9a326ca5c7..c271a475be 100644 --- a/views/app/header.html +++ b/views/app/header.html @@ -9,7 +9,7 @@
    - {ceil(_user.stats.hp)} / 50 + {floor(_user.stats.hp)} / 50
    From 5117a376e95d80510c2e113e9c63f7b261c8b816 Mon Sep 17 00:00:00 2001 From: Andre Casey Date: Wed, 12 Jun 2013 02:03:40 -0700 Subject: [PATCH 125/157] More fixes to inventory spacing. This time for long names of eggs and potions. Was able to contain the text similar to how it is done in the market. Currently, long words (i.e. skeleton) do not center due to the size of images and divs, which still needs to be fixed. More attention will be necessary, but thus should help with Panda Cub Eggs and the like. --- styles/app/inventory.styl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/styles/app/inventory.styl b/styles/app/inventory.styl index 1cedbb12a7..891d570a68 100644 --- a/styles/app/inventory.styl +++ b/styles/app/inventory.styl @@ -64,11 +64,13 @@ padding:.3em p text-align:center + width:3em .hatchingPotion-menu > div float:left padding:.3em p text-align:center + width:3em .pet-button border: none From dfc055696521639ea48b274f2111c7ada680d6a5 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 12 Jun 2013 14:22:43 -0400 Subject: [PATCH 126/157] survey rewards: add individual migration for missing users --- migrations/20130612_survey_rewards_individual.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 migrations/20130612_survey_rewards_individual.js diff --git a/migrations/20130612_survey_rewards_individual.js b/migrations/20130612_survey_rewards_individual.js new file mode 100644 index 0000000000..1d3fbf3317 --- /dev/null +++ b/migrations/20130612_survey_rewards_individual.js @@ -0,0 +1,9 @@ +//mongo habitrpg migrations/20130612_survey_rewards_individual.js + +var query = {_id: ""}; + +db.users.update(query, + { + $set: { 'achievements.helpedHabit': true }, + $inc: { balance: 2.5 } + }) \ No newline at end of file From 3a8f6086adef5c1d3ad0f0732bfa82cfc24d3d05 Mon Sep 17 00:00:00 2001 From: MoofinSeeker Date: Thu, 13 Jun 2013 00:34:59 +0200 Subject: [PATCH 127/157] Update privacy.html Added spacing before line breaks in body of text. --- views/static/privacy.html | 382 +++++++++++++++++++------------------- 1 file changed, 191 insertions(+), 191 deletions(-) diff --git a/views/static/privacy.html b/views/static/privacy.html index 61253c9da3..2e95e7266b 100644 --- a/views/static/privacy.html +++ b/views/static/privacy.html @@ -22,307 +22,307 @@

    PLEASE READ THIS PRIVACY POLICY CAREFULLY.
    - By accessing or otherwise using habitrpg.com or any sub domains thereto ("the Sites"), - or using a habitrpg.com or HabitRPG application on a mobile device ("the Applications"), - you agree to be bound contractually by this Privacy Policy. Individually - or collectively, the Applications and the Sites may be referred to as - the "Services." + By accessing or otherwise using habitrpg.com or any sub domains thereto ("the Sites"), + or using a habitrpg.com or HabitRPG application on a mobile device ("the Applications"), + you agree to be bound contractually by this Privacy Policy. Individually + or collectively, the Applications and the Sites may be referred to as + the "Services."

    - To review material modifications and their effective dates scroll + To review material modifications and their effective dates scroll to the bottom of the page.

    - 1. Privacy Statement; Collection of Personal + 1. Privacy Statement; Collection of Personal Information.
    - 1.1 OCDevel owns and operates this business. All + 1.1 OCDevel owns and operates this business. All references to "we", "us", shall be construed to mean OCDevel.

    - 1.2 We understand that visitors to this website are concerned - about the privacy of information. The following describes our privacy - policy regarding information, including personal information, that we + 1.2 We understand that visitors to this website are concerned + about the privacy of information. The following describes our privacy + policy regarding information, including personal information, that we collect through this website.

    2. Modification of Privacy Policy.
    - We reserve the right to modify this Privacy Policy at any time, - and without prior notice, by posting an amended Privacy Policy that is - always accessible by clicking on the "Privacy Policy" link on this - site's home page. Your continued use of this site indicates your - acceptance of the amended Privacy Policy. You should check the Privacy - Policy through this link periodically for modifications by clicking on - the link provided near the top of the Privacy Policy for a listing of - material modifications and their effective dates. Regarding personal - information, if any modifications are materially less restrictive on our - use or disclosure of the personal information previously disclosed by - you, we will obtain your consent before implementing such revisions with - respect to such information. + We reserve the right to modify this Privacy Policy at any time, + and without prior notice, by posting an amended Privacy Policy that is + always accessible by clicking on the "Privacy Policy" link on this + site's home page. Your continued use of this site indicates your + acceptance of the amended Privacy Policy. You should check the Privacy + Policy through this link periodically for modifications by clicking on + the link provided near the top of the Privacy Policy for a listing of + material modifications and their effective dates. Regarding personal + information, if any modifications are materially less restrictive on our + use or disclosure of the personal information previously disclosed by + you, we will obtain your consent before implementing such revisions with + respect to such information.

    3. Collection of Anonymous, Passive Information.
    - We reserve the right to monitor your use of the services. As you - navigate through the services, certain anonymous information may be - passively collected (that is, gathered without your actively providing - the information) using various technologies, such as cookies, Internet - tags or web beacons, and navigational data collection (log files, server - logs, clickstream). The following is a listing and a brief explanation - of passive information collection methodologies which we may use from - time to time to better understand how the Services are being used. + We reserve the right to monitor your use of the services. As you + navigate through the services, certain anonymous information may be + passively collected (that is, gathered without your actively providing + the information) using various technologies, such as cookies, Internet + tags or web beacons, and navigational data collection (log files, server + logs, clickstream). The following is a listing and a brief explanation + of passive information collection methodologies which we may use from + time to time to better understand how the Services are being used.

    - 3.1 A "cookie" is a text file that this site sends to your - browser in the form of a text file The information generated by the - cookie about your use of this site (including your IP address) will be - transmitted to and stored. Most browsers automatically accept cookies, - but they usually can be modified to decline cookies if you prefer; + 3.1 A "cookie" is a text file that this site sends to your + browser in the form of a text file The information generated by the + cookie about your use of this site (including your IP address) will be + transmitted to and stored. Most browsers automatically accept cookies, + but they usually can be modified to decline cookies if you prefer; however, certain features of this site might not work without cookies.

    - 3.2 "Session" cookies are temporary bits of information that are - used to improve navigation, block visitors from providing information - where inappropriate (the Services "remembers" previous entries of age or - country of origin that were outside the specified parameters and blocks - subsequent changes), and collect aggregate statistical information on - the Services. They are erased once you exit your Web browser or + 3.2 "Session" cookies are temporary bits of information that are + used to improve navigation, block visitors from providing information + where inappropriate (the Services "remembers" previous entries of age or + country of origin that were outside the specified parameters and blocks + subsequent changes), and collect aggregate statistical information on + the Services. They are erased once you exit your Web browser or otherwise turn off your computer.

    - 3.3 "Persistent" cookies are more permanent bits of information - that are placed on the hard drive of your computer and stay there unless - you delete the cookie. Persistent cookies store information on your - computer for a number of purposes, such as retrieving certain - information you have previously provided, helping to determine what - areas of the Services you may find most valuable, and customizing the - Services based on your preferences on an ongoing basis. Persistent - cookies placed by this site in your computer do not hold personal + 3.3 "Persistent" cookies are more permanent bits of information + that are placed on the hard drive of your computer and stay there unless + you delete the cookie. Persistent cookies store information on your + computer for a number of purposes, such as retrieving certain + information you have previously provided, helping to determine what + areas of the Services you may find most valuable, and customizing the + Services based on your preferences on an ongoing basis. Persistent + cookies placed by this site in your computer do not hold personal information.

    - 3.4 You can set your browser to accept all cookies, to reject all - cookies, or to notify you whenever a cookie is offered so that you can - decide each time whether to accept it. To learn more about cookies and - how to specify your preferences, please search for "cookie" in the - "Help" portion of your browser. + 3.4 You can set your browser to accept all cookies, to reject all + cookies, or to notify you whenever a cookie is offered so that you can + decide each time whether to accept it. To learn more about cookies and + how to specify your preferences, please search for "cookie" in the + "Help" portion of your browser.

    - 3.5 An Internet Protocol (IP) address is a number assigned to - your computer by your Internet service provider so you can access the - Internet and is generally considered to be non-personally identifiable - information, because in most cases an IP address is dynamic (changing - each time you connect to the Internet), rather than static (unique to a - particular user's computer). The IP address can be used to diagnose - problems with a server, report aggregate information, determine the - fastest route for your computer to use in connecting to a site, and + 3.5 An Internet Protocol (IP) address is a number assigned to + your computer by your Internet service provider so you can access the + Internet and is generally considered to be non-personally identifiable + information, because in most cases an IP address is dynamic (changing + each time you connect to the Internet), rather than static (unique to a + particular user's computer). The IP address can be used to diagnose + problems with a server, report aggregate information, determine the + fastest route for your computer to use in connecting to a site, and administer and improve the Services.

    - 3.6 "Internet tags" (also known as Web Beacons, single-pixel - GIFs, clear GIFs, invisible GIFs, and 1-by-1 GIFs) are smaller than - cookies and tell the Web site server information such as the IP address - and browser type related to the visitor's computer. Tags may be placed - both on online advertisements that bring people to the Services and on - different pages of the Services. Such tags indicate how many times a + 3.6 "Internet tags" (also known as Web Beacons, single-pixel + GIFs, clear GIFs, invisible GIFs, and 1-by-1 GIFs) are smaller than + cookies and tell the Web site server information such as the IP address + and browser type related to the visitor's computer. Tags may be placed + both on online advertisements that bring people to the Services and on + different pages of the Services. Such tags indicate how many times a page is opened and which information is consulted.

    - 3.7 "Navigational data" (log files, server logs, and clickstream - data) are used for system management, to improve the content of the - Services, market research purposes, and to communicate information to + 3.7 "Navigational data" (log files, server logs, and clickstream + data) are used for system management, to improve the content of the + Services, market research purposes, and to communicate information to visitors.

    4. Use and Sharing of Anonymous, Passive Information.
    - The Services may make full use of passively collected anonymous - information, including without limitation the right to use such - information to provide better service to Service users, customize the - Services based on your preferences, compile and analyze statistics and - trends, and otherwise administer and improve the Services for your use. We - reserve the right to share this anonymous, passive information in + The Services may make full use of passively collected anonymous + information, including without limitation the right to use such + information to provide better service to Service users, customize the + Services based on your preferences, compile and analyze statistics and + trends, and otherwise administer and improve the Services for your use. We + reserve the right to share this anonymous, passive information in aggregated form.

    5. 3rd Party Behavioral Ads; Google's AdSense Network.
    - 5.1 We reserve the right to use anonymous, passive information - about your visits to this and other websites (not including your name, - address, email address or telephone number) for purposes of serving our - ads and third party ads that are targeted to your interests ("3rd Party - Behavioral Ads"). We reserve the right to share anonymous, passive - information collected on the services with third parties for purposes of - serving 3rd Party Behavioral Ads. These 3rd Party Behavioral Ads do not - identify you personally. Instead, they associate your behavioral data on - visited sites with your browser, so that the ads your computer sees on - this site are more likely to be relevant to your interests. 3rd Party - Behavioral Ads require that that you be served with a cookie containing - a tracking code. You may refuse the use of cookies by selecting the - appropriate settings on your browser; however, please note that if you - do this you may not be able to use the full functionality of this site. + 5.1 We reserve the right to use anonymous, passive information + about your visits to this and other websites (not including your name, + address, email address or telephone number) for purposes of serving our + ads and third party ads that are targeted to your interests ("3rd Party + Behavioral Ads"). We reserve the right to share anonymous, passive + information collected on the services with third parties for purposes of + serving 3rd Party Behavioral Ads. These 3rd Party Behavioral Ads do not + identify you personally. Instead, they associate your behavioral data on + visited sites with your browser, so that the ads your computer sees on + this site are more likely to be relevant to your interests. 3rd Party + Behavioral Ads require that that you be served with a cookie containing + a tracking code. You may refuse the use of cookies by selecting the + appropriate settings on your browser; however, please note that if you + do this you may not be able to use the full functionality of this site.

    - 5.2 We reserve the right to participate in Google's AdSense - network for purposes of serving 3rd Party Behavioral Ads. Google uses - DoubleClick's DART cookie for serving 3rd Party Behavioral Ads over the - AdSense network. You may opt out of the use of the DART cookie. For - information regarding how to opt out, go to + 5.2 We reserve the right to participate in Google's AdSense + network for purposes of serving 3rd Party Behavioral Ads. Google uses + DoubleClick's DART cookie for serving 3rd Party Behavioral Ads over the + AdSense network. You may opt out of the use of the DART cookie. For + information regarding how to opt out, go to http://www.google.com/privacy_ads.html.

    6. Use of 3rd Party Analytics.
    - We reserve the right to use analytics services provided by - third parties. These services use 3rd party cookies to collect - anonymous, passive information about your use of this site (see - explanation of cookies in Collection of Anonymous, Passive Information - above). We use this information for the purpose of evaluating your use - of the Services, compiling reports on activity, and providing other - services. These web analytics services may also transfer this - information to third parties where required to do so by law, or where + We reserve the right to use analytics services provided by + third parties. These services use 3rd party cookies to collect + anonymous, passive information about your use of this site (see + explanation of cookies in Collection of Anonymous, Passive Information + above). We use this information for the purpose of evaluating your use + of the Services, compiling reports on activity, and providing other + services. These web analytics services may also transfer this + information to third parties where required to do so by law, or where such third parties process the information on the service's behalf.

    7. Collection of Personal Information; Categories.
    - We will ask you for personal information when you sign up for any - specific benefit or purpose that requires registration. Personal - information that we collect may vary with the each registration, and it - may include one or more of the following categories: name, physical - address, an email address, phone number, and credit card information - including credit card number, expiration date, and billing address, - emergency contact information, current medications, allergies, medical + We will ask you for personal information when you sign up for any + specific benefit or purpose that requires registration. Personal + information that we collect may vary with the each registration, and it + may include one or more of the following categories: name, physical + address, an email address, phone number, and credit card information + including credit card number, expiration date, and billing address, + emergency contact information, current medications, allergies, medical insurance information.

    - 8. Use And Sharing of Personal Information: General + 8. Use And Sharing of Personal Information: General Policy And Exceptions.
    - Our general policy is that we will use your personal information, - including combining your personal information with passive information - collected from this site, only for: the performance of the services or - transaction for which it was given, our private, internal reporting for - this site, and security assessments for this site, and we will not - share, sell, or rent your personal information to others. The only - exceptions to this general policy: (i) are described in the subsections + Our general policy is that we will use your personal information, + including combining your personal information with passive information + collected from this site, only for: the performance of the services or + transaction for which it was given, our private, internal reporting for + this site, and security assessments for this site, and we will not + share, sell, or rent your personal information to others. The only + exceptions to this general policy: (i) are described in the subsections below, and (ii) if you explicitly approve through our site.

    - 8.1 Affiliates And Service Providers. We reserve the right to - provide such information to our affiliates or subsidiaries, or trusted - service providers for the purpose of hosting our servers or processing - or archiving personal information for us. We require that these parties - agree to privacy and security safeguards for this information that are + 8.1 Affiliates And Service Providers. We reserve the right to + provide such information to our affiliates or subsidiaries, or trusted + service providers for the purpose of hosting our servers or processing + or archiving personal information for us. We require that these parties + agree to privacy and security safeguards for this information that are consistent with this Privacy Policy.

    - 8.2 Acquisition; Bankruptcy. In the event that we are acquired by - or merged with a third party entity, we reserve the right to transfer - such information as part of such merger, acquisition, sale, or other - change of control. In the unlikely event of our bankruptcy, insolvency, - reorganization, receivership, or assignment for the benefit of - creditors, or the application of laws or equitable principles affecting - creditors' rights generally, we reserve the right to transfer such + 8.2 Acquisition; Bankruptcy. In the event that we are acquired by + or merged with a third party entity, we reserve the right to transfer + such information as part of such merger, acquisition, sale, or other + change of control. In the unlikely event of our bankruptcy, insolvency, + reorganization, receivership, or assignment for the benefit of + creditors, or the application of laws or equitable principles affecting + creditors' rights generally, we reserve the right to transfer such information to protect our rights or as required by law.

    - 8.3 Enforcement; Legal Process. We reserve the right to transfer - such information if we have a good faith belief that access, use, - preservation or disclosure of such information is reasonably necessary - (i) to satisfy any applicable law, regulation, legal process or - enforceable governmental request, or (ii) to investigate or enforce + 8.3 Enforcement; Legal Process. We reserve the right to transfer + such information if we have a good faith belief that access, use, + preservation or disclosure of such information is reasonably necessary + (i) to satisfy any applicable law, regulation, legal process or + enforceable governmental request, or (ii) to investigate or enforce violations of our rights or the security of this site.

    - 8.4 Miscellaneous. We reserve the right to share personal - information with the following additional parties: online organizers - using our tools and resellers of our products and services from whose - site the sale originated (even though the sale originates at site of the - reseller, registration and collection of personal information occurs at + 8.4 Miscellaneous. We reserve the right to share personal + information with the following additional parties: online organizers + using our tools and resellers of our products and services from whose + site the sale originated (even though the sale originates at site of the + reseller, registration and collection of personal information occurs at this site).

    - 9. Onward Transfer of Personal Information Outside Your + 9. Onward Transfer of Personal Information Outside Your Country of Residence.
    - Any personal information which we may collect on this site will - be stored and processed in our servers located only in the United - States. By using this site, if you reside outside the United States, you - consent to the transfer of personal information outside your country of + Any personal information which we may collect on this site will + be stored and processed in our servers located only in the United + States. By using this site, if you reside outside the United States, you + consent to the transfer of personal information outside your country of residence to the United States.

    10. Security of Personal Information.
    - We follow reasonable and appropriate industry standards to - protect your personal information and data. Unfortunately, no data - transmission over the Internet or method of data storage can be - guaranteed 100% secure. Therefore, while we strive to protect your - personal information by following generally accepted industry standards, - we cannot ensure or warrant the absolute security of any information you + We follow reasonable and appropriate industry standards to + protect your personal information and data. Unfortunately, no data + transmission over the Internet or method of data storage can be + guaranteed 100% secure. Therefore, while we strive to protect your + personal information by following generally accepted industry standards, + we cannot ensure or warrant the absolute security of any information you transmit to us or archive at this site.

    11. Changing And Updating Personal Information.
    - Upon request, we will permit you to request or make changes or - updates to your personal information for legitimate purposes. We request - identification prior to approving such requests. We reserve the right to - decline any requests that are unreasonably repetitive or systematic, - require unreasonable time or effort of our technical or administrative - personnel, or undermine the privacy rights of others. We reserve the - right to permit you to access your personal information in any account - you establish with this site for purposes of making your own changes or - updates, and in such case, instructions for making such changes or + Upon request, we will permit you to request or make changes or + updates to your personal information for legitimate purposes. We request + identification prior to approving such requests. We reserve the right to + decline any requests that are unreasonably repetitive or systematic, + require unreasonable time or effort of our technical or administrative + personnel, or undermine the privacy rights of others. We reserve the + right to permit you to access your personal information in any account + you establish with this site for purposes of making your own changes or + updates, and in such case, instructions for making such changes or updates will be provided where necessary.

    12. Email From This Site; Opt-Out Rights.
    - If you supply us with your e-mail address you may receive - periodic messages from us with information specific to the Services and - required for the normal functioning of the Services as well as for new - products or services or upcoming events. If you prefer not to receive - periodic email messages, you may opt-out by following the instructions + If you supply us with your e-mail address you may receive + periodic messages from us with information specific to the Services and + required for the normal functioning of the Services as well as for new + products or services or upcoming events. If you prefer not to receive + periodic email messages, you may opt-out by following the instructions on the email.

    13. Children's Online Policy.
    - We are committed to preserving online privacy for all of its - website visitors, including children. This site is a general audience - site. Consistent with the Children's Online Privacy Protection Act - (COPPA), we will not knowingly collect any information from, or sell to, - children under the age of 13. If you are a parent or guardian who has - discovered that your child under the age of 13 has submitted his or her - personally identifiable information without your permission or consent, - we will remove the information from our active list, at your request. To - request the removal of your child's information, please send contact our - site as provided below under “Contact Us”, and be sure to include in - your message the same login information that your child submitted. + We are committed to preserving online privacy for all of its + website visitors, including children. This site is a general audience + site. Consistent with the Children's Online Privacy Protection Act + (COPPA), we will not knowingly collect any information from, or sell to, + children under the age of 13. If you are a parent or guardian who has + discovered that your child under the age of 13 has submitted his or her + personally identifiable information without your permission or consent, + we will remove the information from our active list, at your request. To + request the removal of your child's information, please send contact our + site as provided below under “Contact Us”, and be sure to include in + your message the same login information that your child submitted.

    - 14. Email And Other Messages Through This Site; ECPA + 14. Email And Other Messages Through This Site; ECPA Notice.
    - This site treats email messages and other electronic messages - that are sent through this site and not viewable by others as - confidential and private, except as required by law, including without - limitation, the Electronic Communications Privacy Act of 1986, 18 U.S.C. - Sections 2701-2711 (the "ECPA"). The ECPA permits this site's limited - ability to intercept and/or disclose electronic messages, for example - (i) as necessary to operate our system or to protect our rights or - property, (ii) upon legal demand (court orders, warrants, subpoenas), or - (iii) where we receive information inadvertently which appears to - pertain to the commission of a crime. This site is not considered a + This site treats email messages and other electronic messages + that are sent through this site and not viewable by others as + confidential and private, except as required by law, including without + limitation, the Electronic Communications Privacy Act of 1986, 18 U.S.C. + Sections 2701-2711 (the "ECPA"). The ECPA permits this site's limited + ability to intercept and/or disclose electronic messages, for example + (i) as necessary to operate our system or to protect our rights or + property, (ii) upon legal demand (court orders, warrants, subpoenas), or + (iii) where we receive information inadvertently which appears to + pertain to the commission of a crime. This site is not considered a "secure communications medium" under the ECPA.

    15. Contact Us.
    - If you have any questions regarding this Privacy Policy, please + If you have any questions regarding this Privacy Policy, please contact the owner and operator of this website business:

    @@ -344,4 +344,4 @@
    - \ No newline at end of file + From 88557668291472d4372e041eb59e89b7da5f21e1 Mon Sep 17 00:00:00 2001 From: MoofinSeeker Date: Thu, 13 Jun 2013 00:53:14 +0200 Subject: [PATCH 128/157] Update terms.html Added spaces before line breaks in text body. --- views/static/terms.html | 784 ++++++++++++++++++++-------------------- 1 file changed, 392 insertions(+), 392 deletions(-) diff --git a/views/static/terms.html b/views/static/terms.html index ff32271bfe..471dfcd1f6 100644 --- a/views/static/terms.html +++ b/views/static/terms.html @@ -19,211 +19,211 @@ Last updated September 2, 2012

    - HabitRPG (or "we") provides services through our - software applications for various devices and platforms ("HabitRPG - Applications") and the HabitRPG.com domain, and any sub domains thereto - (the "Sites"). Individually or collectively, HabitRPG Applications and + HabitRPG (or "we") provides services through our + software applications for various devices and platforms ("HabitRPG + Applications") and the HabitRPG.com domain, and any sub domains thereto + (the "Sites"). Individually or collectively, HabitRPG Applications and Sites may be referred to as the "Services".

    - Please read the following terms and conditions ("Terms of - Service") carefully. These Terms of Service govern your access to and - use of the Services and HabitRPG Content (defined below) and set forth - the legally binding terms for your use of the Services and HabitRPG + Please read the following terms and conditions ("Terms of + Service") carefully. These Terms of Service govern your access to and + use of the Services and HabitRPG Content (defined below) and set forth + the legally binding terms for your use of the Services and HabitRPG Content, whether or not you have registered as a Member.

    - Certain areas of the Services (and your access to or use of - HabitRPG Content) may have different terms and conditions posted or may - require you to agree with and accept additional terms and conditions. If - there is a conflict between these Terms of Service and terms and - conditions posted for a specific area of the Services or HabitRPG - Content, the latter terms and conditions will take precedence with - respect to your use of or access to that area of the Services or HabitRPG + Certain areas of the Services (and your access to or use of + HabitRPG Content) may have different terms and conditions posted or may + require you to agree with and accept additional terms and conditions. If + there is a conflict between these Terms of Service and terms and + conditions posted for a specific area of the Services or HabitRPG + Content, the latter terms and conditions will take precedence with + respect to your use of or access to that area of the Services or HabitRPG Content.

    - YOU ACKNOWLEDGE AND AGREE THAT, BY CLICKING ON THE "I AGREE" OR - "I ACCEPT" BUTTON, OR BY ACCESSING OR USING THE SERVICES OR BY - DOWNLOADING OR POSTING ANY CONTENT FROM OR ON THE SITES OR THROUGH THE - SERVICES, YOU ARE INDICATING THAT YOU HAVE READ, UNDERSTAND AND AGREE TO - BE BOUND BY THESE TERMS, WHETHER OR NOT YOU HAVE REGISTERED AS A MEMBER, - AND AGREE TO OUR PRIVACY POLICY AS DESCRIBED BELOW. IF YOU DO NOT AGREE - TO THESE TERMS, THEN YOU HAVE NO RIGHT TO ACCESS OR USE THE SERVICES OR + YOU ACKNOWLEDGE AND AGREE THAT, BY CLICKING ON THE "I AGREE" OR + "I ACCEPT" BUTTON, OR BY ACCESSING OR USING THE SERVICES OR BY + DOWNLOADING OR POSTING ANY CONTENT FROM OR ON THE SITES OR THROUGH THE + SERVICES, YOU ARE INDICATING THAT YOU HAVE READ, UNDERSTAND AND AGREE TO + BE BOUND BY THESE TERMS, WHETHER OR NOT YOU HAVE REGISTERED AS A MEMBER, + AND AGREE TO OUR PRIVACY POLICY AS DESCRIBED BELOW. IF YOU DO NOT AGREE + TO THESE TERMS, THEN YOU HAVE NO RIGHT TO ACCESS OR USE THE SERVICES OR HABITRPG CONTENT.

    Modification
    - HabitRPG reserves the right, at its sole discretion, to modify, - discontinue or terminate the Services, including any portion thereof on - a global or individual basis, or to modify these Terms of Service, at - any time and without prior notice. If we modify these Terms of Service, - we will update the "Last Updated Date" above and post the modification - on the Sites and perhaps elsewhere within the Services. By continuing to - access or use the Services after we have posted a modification to these - Terms of Service or have provided you with notice of a modification, you - are indicating that you agree to be bound by the modified Terms of - Service. If the modified Terms of Service are not acceptable to you, - your only recourse is to cease using the Services. + HabitRPG reserves the right, at its sole discretion, to modify, + discontinue or terminate the Services, including any portion thereof on + a global or individual basis, or to modify these Terms of Service, at + any time and without prior notice. If we modify these Terms of Service, + we will update the "Last Updated Date" above and post the modification + on the Sites and perhaps elsewhere within the Services. By continuing to + access or use the Services after we have posted a modification to these + Terms of Service or have provided you with notice of a modification, you + are indicating that you agree to be bound by the modified Terms of + Service. If the modified Terms of Service are not acceptable to you, + your only recourse is to cease using the Services.

    Eligibility and HabitRPG Account Registration
    - In order to access certain features of the Sites and Services, and to - post any Public User Content (defined below) on the Sites or through the - Services, you must register to create an account ("HabitRPG Account") and - become a "Member". In compliance with privacy laws, we do not allow - people below the age of 14 to create accounts; please see our Privacy - Policy for further information. During the registration process, you - will be required to provide certain information and you will establish a - username and a password. You agree to provide accurate, current and - complete information during the registration process and to update such - information to keep it accurate, current and complete. HabitRPG reserves - the right to suspend or terminate your HabitRPG Account if any - information provided during the registration process or thereafter - proves to be inaccurate, not current or incomplete. If you are not a - Member you may browse all areas of the Sites or use the parts of the - Services that are not limited to Members only. You are responsible for - safeguarding your password. You agree not to disclose your password to - any third party and to take sole responsibility for any activities or - actions under your HabitRPG Account, whether or not you have authorized - such activities or actions. You agree to immediately notify HabitRPG of - any unauthorized use of your HabitRPG Account. We are not liable for any - damages or losses caused by someone using your account without your + In order to access certain features of the Sites and Services, and to + post any Public User Content (defined below) on the Sites or through the + Services, you must register to create an account ("HabitRPG Account") and + become a "Member". In compliance with privacy laws, we do not allow + people below the age of 14 to create accounts; please see our Privacy + Policy for further information. During the registration process, you + will be required to provide certain information and you will establish a + username and a password. You agree to provide accurate, current and + complete information during the registration process and to update such + information to keep it accurate, current and complete. HabitRPG reserves + the right to suspend or terminate your HabitRPG Account if any + information provided during the registration process or thereafter + proves to be inaccurate, not current or incomplete. If you are not a + Member you may browse all areas of the Sites or use the parts of the + Services that are not limited to Members only. You are responsible for + safeguarding your password. You agree not to disclose your password to + any third party and to take sole responsibility for any activities or + actions under your HabitRPG Account, whether or not you have authorized + such activities or actions. You agree to immediately notify HabitRPG of + any unauthorized use of your HabitRPG Account. We are not liable for any + damages or losses caused by someone using your account without your permission.

    Privacy
    - See HabitRPG's Privacy Policy at http://www.HabitRPG.com/privacy for - information and notices concerning HabitRPG's collection and use of your - personal information. If you have any questions about the HabitRPG - Privacy Policy, please contact HabitRPG at privacy AT HabitRPG.com. By - accessing the Services you are agreeing to the terms of our Privacy + See HabitRPG's Privacy Policy at http://www.HabitRPG.com/privacy for + information and notices concerning HabitRPG's collection and use of your + personal information. If you have any questions about the HabitRPG + Privacy Policy, please contact HabitRPG at privacy AT HabitRPG.com. By + accessing the Services you are agreeing to the terms of our Privacy Policy.

    Content
    - Certain types of content are made available through the Services. - "HabitRPG Content" means, collectively, the text, data, graphics, images, - illustrations, forms and look and feel attributes, HabitRPG trademarks - and logos and other content made available through the Services, - including any technology or code making up the Services, excluding User - Content. "Public User Content" means the text, data, graphics, images, photos, - video or audiovisual content, hypertext links and any other content uploaded, - transmitted or submitted by a Member via the Services with the intent to share - with other users. "Private User Content" means data created through the services - exclusively for personal use or private sharing. - This includes tasks and related data created in HabitRPG Tasks that have not - been explicitly shared publicly. - You understand that by using any of the Services, you may encounter content - that may be deemed offensive, indecent, or objectionable, which content - may or may not be identified as having explicit language, and that the - results of any search or entering of a particular URL may automatically - and unintentionally generate links or references to objectionable - material. Nevertheless, you agree to use the Services at your sole risk - and that we shall not have any liability to you for content that may be + Certain types of content are made available through the Services. + "HabitRPG Content" means, collectively, the text, data, graphics, images, + illustrations, forms and look and feel attributes, HabitRPG trademarks + and logos and other content made available through the Services, + including any technology or code making up the Services, excluding User + Content. "Public User Content" means the text, data, graphics, images, photos, + video or audiovisual content, hypertext links and any other content uploaded, + transmitted or submitted by a Member via the Services with the intent to share + with other users. "Private User Content" means data created through the services + exclusively for personal use or private sharing. + This includes tasks and related data created in HabitRPG Tasks that have not + been explicitly shared publicly. + You understand that by using any of the Services, you may encounter content + that may be deemed offensive, indecent, or objectionable, which content + may or may not be identified as having explicit language, and that the + results of any search or entering of a particular URL may automatically + and unintentionally generate links or references to objectionable + material. Nevertheless, you agree to use the Services at your sole risk + and that we shall not have any liability to you for content that may be found to be offensive, indecent, or objectionable.

    Ownership
    - The Services and HabitRPG Content are protected by copyright, trademark, - and other laws of the United States and foreign countries. Except as - expressly provided in these Terms of Service, HabitRPG and its licensors - exclusively own all right, title and interest in and to the Services and - HabitRPG Content, including all associated intellectual property rights. - You will not remove, alter or obscure any copyright, trademark, service - mark or other proprietary rights notices incorporated in or accompanying + The Services and HabitRPG Content are protected by copyright, trademark, + and other laws of the United States and foreign countries. Except as + expressly provided in these Terms of Service, HabitRPG and its licensors + exclusively own all right, title and interest in and to the Services and + HabitRPG Content, including all associated intellectual property rights. + You will not remove, alter or obscure any copyright, trademark, service + mark or other proprietary rights notices incorporated in or accompanying the Services or HabitRPG Content.

    HabitRPG License
    - Subject to your compliance with the terms and conditions of these Terms - of Service, HabitRPG grants you a limited, non-exclusive, - non-transferable license, without the right to sublicense, to access, - use, view, download and print, where applicable, the Services and any - HabitRPG Content solely for your personal and non-commercial purposes. - You will not use, copy, adapt, modify, prepare derivative works based - upon, distribute, license, sell, transfer, publicly display, publicly - perform, transmit, stream, broadcast or otherwise exploit the Services - or HabitRPG Content, except as expressly permitted in these Terms of - Service. No licenses or rights are granted to you by implication or - otherwise under any intellectual property rights owned or controlled by - HabitRPG or its licensors, except for the licenses and rights expressly - granted in these Terms of Service. With respect to HabitRPG Applications, - your license is limited to use of such applications on platforms and - devices that you own or control, and you may not distribute or make the - HabitRPG Applications available over a network where it could be used by + Subject to your compliance with the terms and conditions of these Terms + of Service, HabitRPG grants you a limited, non-exclusive, + non-transferable license, without the right to sublicense, to access, + use, view, download and print, where applicable, the Services and any + HabitRPG Content solely for your personal and non-commercial purposes. + You will not use, copy, adapt, modify, prepare derivative works based + upon, distribute, license, sell, transfer, publicly display, publicly + perform, transmit, stream, broadcast or otherwise exploit the Services + or HabitRPG Content, except as expressly permitted in these Terms of + Service. No licenses or rights are granted to you by implication or + otherwise under any intellectual property rights owned or controlled by + HabitRPG or its licensors, except for the licenses and rights expressly + granted in these Terms of Service. With respect to HabitRPG Applications, + your license is limited to use of such applications on platforms and + devices that you own or control, and you may not distribute or make the + HabitRPG Applications available over a network where it could be used by multiple devices at the same time.

    Public User Content
    - By making available any Public User Content through the Services, you hereby - grant to HabitRPG a worldwide, irrevocable, perpetual, non-exclusive, - transferable, royalty-free license, with the right to sublicense, to - use, copy, adapt, modify, distribute, license, sell, transfer, publicly - display, publicly perform, transmit, stream, broadcast and otherwise - exploit such Public User Content only on, through or by means of the Services. - HabitRPG does not claim any ownership rights in any such Public User Content and - nothing in these Terms of Service will be deemed to restrict any rights - that you may have to use and exploit any such Public User Content. + By making available any Public User Content through the Services, you hereby + grant to HabitRPG a worldwide, irrevocable, perpetual, non-exclusive, + transferable, royalty-free license, with the right to sublicense, to + use, copy, adapt, modify, distribute, license, sell, transfer, publicly + display, publicly perform, transmit, stream, broadcast and otherwise + exploit such Public User Content only on, through or by means of the Services. + HabitRPG does not claim any ownership rights in any such Public User Content and + nothing in these Terms of Service will be deemed to restrict any rights + that you may have to use and exploit any such Public User Content.

    - You acknowledge and agree that you are solely responsible for all - Public User Content that you make available through the Services. Accordingly, - you represent and warrant that: (i) you either are the sole and - exclusive owner of all Public User Content that you make available through the - Services or you have all rights, licenses, consents and releases that - are necessary to grant to HabitRPG the rights in such Public User Content, as - contemplated under these Terms of Service; and (ii) neither the User - Content nor your posting, uploading, publication, submission or - transmittal of the Public User Content or HabitRPG's use of the Public User Content (or - any portion thereof) on, through or by means of the Services will - infringe, misappropriate or violate a third party's patent, copyright, - trademark, trade secret, moral rights or other intellectual property - rights, or rights of publicity or privacy, or result in the violation of + You acknowledge and agree that you are solely responsible for all + Public User Content that you make available through the Services. Accordingly, + you represent and warrant that: (i) you either are the sole and + exclusive owner of all Public User Content that you make available through the + Services or you have all rights, licenses, consents and releases that + are necessary to grant to HabitRPG the rights in such Public User Content, as + contemplated under these Terms of Service; and (ii) neither the User + Content nor your posting, uploading, publication, submission or + transmittal of the Public User Content or HabitRPG's use of the Public User Content (or + any portion thereof) on, through or by means of the Services will + infringe, misappropriate or violate a third party's patent, copyright, + trademark, trade secret, moral rights or other intellectual property + rights, or rights of publicity or privacy, or result in the violation of any applicable law or regulation.

    - Copyrighted Materials: No Infringing Use. You will not use the - Services to offer, display, distribute, transmit, route, provide - connections to or store any material that infringes copyrighted works or - otherwise violates or promotes the violation of the intellectual - property rights of any third party. HabitRPG has adopted and implemented - a policy that provides for the termination in appropriate circumstances - of the accounts of users who repeatedly infringe or are believed to be - or are charged with repeatedly infringing the rights of copyright + Copyrighted Materials: No Infringing Use. You will not use the + Services to offer, display, distribute, transmit, route, provide + connections to or store any material that infringes copyrighted works or + otherwise violates or promotes the violation of the intellectual + property rights of any third party. HabitRPG has adopted and implemented + a policy that provides for the termination in appropriate circumstances + of the accounts of users who repeatedly infringe or are believed to be + or are charged with repeatedly infringing the rights of copyright holders.

    Notify Us of Infringers
    - If you believe that something on the Services violates your copyright, - notify our copyright agent in writing. The contact information for our + If you believe that something on the Services violates your copyright, + notify our copyright agent in writing. The contact information for our copyright agent is at the bottom of this section.

    - In order for us to take action, you must do the following in your + In order for us to take action, you must do the following in your notice:

    - (1) provide your physical or electronic signature; (2) identify - the copyrighted work that you believe is being infringed; (3) identify - the item on the Services that you think is infringing your work and - include sufficient information about where the material is located on - the Services (including which website and URL) so that we can find it; - (4) provide us with a way to contact you, such as your address, - telephone number, or e-mail; (5) provide a statement that you believe in - good faith that the item you have identified as infringing is not - authorized by the copyright owner, its agent, or the law to be used on - the Services; and (6) provide a statement that the information you - provide in your notice is accurate, and that (under penalty of perjury), - you are authorized to act on behalf of the copyright owner whose work is + (1) provide your physical or electronic signature; (2) identify + the copyrighted work that you believe is being infringed; (3) identify + the item on the Services that you think is infringing your work and + include sufficient information about where the material is located on + the Services (including which website and URL) so that we can find it; + (4) provide us with a way to contact you, such as your address, + telephone number, or e-mail; (5) provide a statement that you believe in + good faith that the item you have identified as infringing is not + authorized by the copyright owner, its agent, or the law to be used on + the Services; and (6) provide a statement that the information you + provide in your notice is accurate, and that (under penalty of perjury), + you are authorized to act on behalf of the copyright owner whose work is being infringed.

    @@ -243,398 +243,398 @@ E-Mail: tylerrenelle at gmail dot com

    - Again, we cannot take action unless you give us all the required + Again, we cannot take action unless you give us all the required information.

    Ratings and Comments & Feedback.
    - You can rate and make comments about content made available through the - Services ("Comments"). HabitRPG advises you to exercise caution and good - judgment when leaving such Comments. Once you complete and submit your - Comments to the Services you will not be able to go back and edit your - Comments. You should also be aware that you could be held legally - responsible for damages to someone's reputation if your Comments are - deemed to be defamatory. Without limiting any other terms of this Terms - of Service, HabitRPG may, but is under no obligation to, monitor or - censor Comments and disclaims any and all liability relating thereto. - Notwithstanding the foregoing, HabitRPG does reserve the right, in its - sole discretion, to remove any Comments that it deems to be improper, - inappropriate or inconsistent with the online activities that are - permitted under these Terms of Service. We welcome and encourage you to - provide feedback, comments and suggestions for improvements to the - Services ("Feedback"). You may submit Feedback by emailing us at support - AT HabitRPG.com. You acknowledge and agree that all Comments and Feedback - will be the sole and exclusive property of HabitRPG and you hereby - irrevocably assign to HabitRPG and agree to irrevocably assign to HabitRPG - all of your right, title, and interest in and to all Comments and - Feedback, including without limitation all worldwide patent rights, - copyright rights, trade secret rights, and other proprietary or - intellectual property rights therein. At HabitRPG's request and expense, - you will execute documents and take such further acts as HabitRPG may - reasonably request to assist HabitRPG to acquire, perfect, and maintain - its intellectual property rights and other legal protections for the + You can rate and make comments about content made available through the + Services ("Comments"). HabitRPG advises you to exercise caution and good + judgment when leaving such Comments. Once you complete and submit your + Comments to the Services you will not be able to go back and edit your + Comments. You should also be aware that you could be held legally + responsible for damages to someone's reputation if your Comments are + deemed to be defamatory. Without limiting any other terms of this Terms + of Service, HabitRPG may, but is under no obligation to, monitor or + censor Comments and disclaims any and all liability relating thereto. + Notwithstanding the foregoing, HabitRPG does reserve the right, in its + sole discretion, to remove any Comments that it deems to be improper, + inappropriate or inconsistent with the online activities that are + permitted under these Terms of Service. We welcome and encourage you to + provide feedback, comments and suggestions for improvements to the + Services ("Feedback"). You may submit Feedback by emailing us at support + AT HabitRPG.com. You acknowledge and agree that all Comments and Feedback + will be the sole and exclusive property of HabitRPG and you hereby + irrevocably assign to HabitRPG and agree to irrevocably assign to HabitRPG + all of your right, title, and interest in and to all Comments and + Feedback, including without limitation all worldwide patent rights, + copyright rights, trade secret rights, and other proprietary or + intellectual property rights therein. At HabitRPG's request and expense, + you will execute documents and take such further acts as HabitRPG may + reasonably request to assist HabitRPG to acquire, perfect, and maintain + its intellectual property rights and other legal protections for the Comments and Feedback.

    Interactions between Users
    - You are solely responsible for your interactions (including any - disputes) with other users. You understand that HabitRPG does not in any - way screen HabitRPG users, except to only allow people aged 14 and over - to create accounts. You are solely responsible for, and will exercise - caution, discretion, common sense and judgment in, using the Services - and disclosing personal information to other HabitRPG users. You agree to - take reasonable precautions in all interactions with other HabitRPG - users, particularly if you decide to meet a HabitRPG user offline, or in - person. Your use of the Services, HabitRPG Content and any other content - made available through the Services is at your sole risk and discretion - and HabitRPG hereby disclaims any and all liability to you or any third - party relating thereto. HabitRPG reserves the right to contact Members, - in compliance with applicable law, in order to evaluate compliance with - the rules and policies in these Terms of Service. You will cooperate - fully with HabitRPG to investigate any suspected unlawful, fraudulent or - improper activity, including, without limitation, granting authorized - HabitRPG representatives access to any password-protected portions of + You are solely responsible for your interactions (including any + disputes) with other users. You understand that HabitRPG does not in any + way screen HabitRPG users, except to only allow people aged 14 and over + to create accounts. You are solely responsible for, and will exercise + caution, discretion, common sense and judgment in, using the Services + and disclosing personal information to other HabitRPG users. You agree to + take reasonable precautions in all interactions with other HabitRPG + users, particularly if you decide to meet a HabitRPG user offline, or in + person. Your use of the Services, HabitRPG Content and any other content + made available through the Services is at your sole risk and discretion + and HabitRPG hereby disclaims any and all liability to you or any third + party relating thereto. HabitRPG reserves the right to contact Members, + in compliance with applicable law, in order to evaluate compliance with + the rules and policies in these Terms of Service. You will cooperate + fully with HabitRPG to investigate any suspected unlawful, fraudulent or + improper activity, including, without limitation, granting authorized + HabitRPG representatives access to any password-protected portions of your HabitRPG Account.

    General Prohibitions
    - You agree not to do any of the following while using the Services or + You agree not to do any of the following while using the Services or HabitRPG Content:

    • - Post, upload, publish, submit or transmit any text, graphics, - images, software, music, audio, video, information or other material - that: (i) infringes, misappropriates or violates a third party's - patent, copyright, trademark, trade secret, moral rights or other - intellectual property rights, or rights of publicity or privacy; (ii) - violates, or encourages any conduct that would violate, any applicable - law or regulation or would give rise to civil liability; (iii) is - fraudulent, false, misleading or deceptive; (iv) is defamatory, - obscene, pornographic, vulgar or offensive; (v) promotes - discrimination, bigotry, racism, hatred, harassment or harm against any - individual or group; (vi) is violent or threatening or promotes - violence or actions that are threatening to any other person; or (vii) - promotes illegal or harmful activities or substances (including but not - limited to activities that promote or provide instructional information - regarding the manufacture or purchase of illegal weapons or illegal + Post, upload, publish, submit or transmit any text, graphics, + images, software, music, audio, video, information or other material + that: (i) infringes, misappropriates or violates a third party's + patent, copyright, trademark, trade secret, moral rights or other + intellectual property rights, or rights of publicity or privacy; (ii) + violates, or encourages any conduct that would violate, any applicable + law or regulation or would give rise to civil liability; (iii) is + fraudulent, false, misleading or deceptive; (iv) is defamatory, + obscene, pornographic, vulgar or offensive; (v) promotes + discrimination, bigotry, racism, hatred, harassment or harm against any + individual or group; (vi) is violent or threatening or promotes + violence or actions that are threatening to any other person; or (vii) + promotes illegal or harmful activities or substances (including but not + limited to activities that promote or provide instructional information + regarding the manufacture or purchase of illegal weapons or illegal substances).
    • - Use, display, mirror, frame or utilize framing techniques to - enclose the Services, or any individual element or materials within the - Services, HabitRPG's name, any HabitRPG trademark, logo or other - proprietary information, the content of any text or the layout and - design of any page or form contained on a page, without HabitRPG's - express written consent; + Use, display, mirror, frame or utilize framing techniques to + enclose the Services, or any individual element or materials within the + Services, HabitRPG's name, any HabitRPG trademark, logo or other + proprietary information, the content of any text or the layout and + design of any page or form contained on a page, without HabitRPG's + express written consent;
    • - Access, tamper with, or use non-public areas of the Services, - HabitRPG's computer systems, or the technical delivery systems of + Access, tamper with, or use non-public areas of the Services, + HabitRPG's computer systems, or the technical delivery systems of HabitRPG's providers;
    • - Attempt to probe, scan, or test the vulnerability of any - HabitRPG system or network or breach any security or authentication + Attempt to probe, scan, or test the vulnerability of any + HabitRPG system or network or breach any security or authentication measures;
    • - Avoid, bypass, remove, deactivate, impair, descramble or - otherwise circumvent any technological measure implemented by HabitRPG - or any of HabitRPG's providers or any other third party (including + Avoid, bypass, remove, deactivate, impair, descramble or + otherwise circumvent any technological measure implemented by HabitRPG + or any of HabitRPG's providers or any other third party (including another user) to protect the Services or HabitRPG Content;
    • - Attempt to access or search the Services or HabitRPG Content or - download HabitRPG Content from the Services through the use of any - engine, software, tool, agent, device or mechanism (including spiders, - robots, crawlers, data mining tools or the like) other than the - software and/or search agents provided by HabitRPG or other generally - available third party web browsers (such as Google Chrome, Microsoft + Attempt to access or search the Services or HabitRPG Content or + download HabitRPG Content from the Services through the use of any + engine, software, tool, agent, device or mechanism (including spiders, + robots, crawlers, data mining tools or the like) other than the + software and/or search agents provided by HabitRPG or other generally + available third party web browsers (such as Google Chrome, Microsoft Internet Explorer, Mozilla Firefox, Safari or Opera);
    • - Send any unsolicited or unauthorized advertising, promotional - materials, email, junk mail, spam, chain letters or other form of + Send any unsolicited or unauthorized advertising, promotional + materials, email, junk mail, spam, chain letters or other form of solicitation;
    • - Use any meta tags or other hidden text or metadata utilizing a - HabitRPG trademark, logo URL or product name without HabitRPG's express + Use any meta tags or other hidden text or metadata utilizing a + HabitRPG trademark, logo URL or product name without HabitRPG's express written consent;
    • - Use the Services or HabitRPG Content for any commercial purpose - or the benefit of any third party or in any manner not permitted by + Use the Services or HabitRPG Content for any commercial purpose + or the benefit of any third party or in any manner not permitted by these Terms of Service;
    • - Forge any TCP/IP packet header or any part of the header - information in any email or newsgroup posting, or in any way use the - Services or HabitRPG Content to send altered, deceptive or false + Forge any TCP/IP packet header or any part of the header + information in any email or newsgroup posting, or in any way use the + Services or HabitRPG Content to send altered, deceptive or false source-identifying information;
    • - Attempt to decipher, decompile, disassemble or reverse - engineer any of the software used to provide the Services or HabitRPG + Attempt to decipher, decompile, disassemble or reverse + engineer any of the software used to provide the Services or HabitRPG Content;
    • - Interfere with, or attempt to interfere with, the access of - any user, host or network, including, without limitation, sending a + Interfere with, or attempt to interfere with, the access of + any user, host or network, including, without limitation, sending a virus, overloading, flooding, spamming, or mail-bombing the Services;
    • - Collect or store any personally identifiable information from - the Services from other users of the Services without their express + Collect or store any personally identifiable information from + the Services from other users of the Services without their express permission;
    • - Impersonate or misrepresent your affiliation with any person - or entity; Violate any applicable law or regulation; or + Impersonate or misrepresent your affiliation with any person + or entity; Violate any applicable law or regulation; or
    • - Encourage or enable any other individual to do any of the + Encourage or enable any other individual to do any of the foregoing.

    - HabitRPG will have the right to investigate and prosecute - violations of any of the above, including intellectual property rights - infringement and Services security issues, to the fullest extent of the - law. HabitRPG may involve and cooperate with law enforcement authorities - in prosecuting users who violate these Terms of Service. You acknowledge - that HabitRPG has no obligation to monitor your access to or use of the - Services or HabitRPG Content or to review or edit any Public User Content, but - has the right to do so for the purpose of operating the Services, to - ensure your compliance with these Terms of Service, or to comply with - applicable law or the order or requirement of a court, administrative - agency or other governmental body. HabitRPG reserves the right, at any - time and without prior notice, to remove or disable access to any - HabitRPG Content, including, any Public User Content, that HabitRPG, in its sole - discretion, considers to be in violation of these Terms of Service or + HabitRPG will have the right to investigate and prosecute + violations of any of the above, including intellectual property rights + infringement and Services security issues, to the fullest extent of the + law. HabitRPG may involve and cooperate with law enforcement authorities + in prosecuting users who violate these Terms of Service. You acknowledge + that HabitRPG has no obligation to monitor your access to or use of the + Services or HabitRPG Content or to review or edit any Public User Content, but + has the right to do so for the purpose of operating the Services, to + ensure your compliance with these Terms of Service, or to comply with + applicable law or the order or requirement of a court, administrative + agency or other governmental body. HabitRPG reserves the right, at any + time and without prior notice, to remove or disable access to any + HabitRPG Content, including, any Public User Content, that HabitRPG, in its sole + discretion, considers to be in violation of these Terms of Service or otherwise harmful to the Services.

    Links
    - The Services may contain links to third-party websites or resources. You - acknowledge and agree that HabitRPG is not responsible or liable for: (i) - the availability or accuracy of such websites or resources; or (ii) the - content, products, or services on or available from such websites or - resources. Links to such websites or resources do not imply any - endorsement by HabitRPG of such websites or resources or the content, - products, or services available from such websites or resources. You - acknowledge sole responsibility for and assume all risk arising from + The Services may contain links to third-party websites or resources. You + acknowledge and agree that HabitRPG is not responsible or liable for: (i) + the availability or accuracy of such websites or resources; or (ii) the + content, products, or services on or available from such websites or + resources. Links to such websites or resources do not imply any + endorsement by HabitRPG of such websites or resources or the content, + products, or services available from such websites or resources. You + acknowledge sole responsibility for and assume all risk arising from your use of any such websites or resources.

    Termination and HabitRPG Account; Cancellation
    - Without limiting other remedies, HabitRPG may at any time suspend or - terminate your HabitRPG Account and refuse to provide access to the - Services. In addition, HabitRPG may notify authorities or take any - actions it deems appropriate, without notice to you, if HabitRPG suspects - or determines, in its own discretion, that you may have or there is a - significant risk that you have (i) failed to comply with any provision - of these Terms of Service or any policies or rules established by - HabitRPG; or (ii) engaged in actions relating to or in the course of - using the Services that may be illegal or cause liability, harm, - embarrassment, harassment, abuse or disruption for you, HabitRPG Users, - HabitRPG or any other third parties or the Services. + Without limiting other remedies, HabitRPG may at any time suspend or + terminate your HabitRPG Account and refuse to provide access to the + Services. In addition, HabitRPG may notify authorities or take any + actions it deems appropriate, without notice to you, if HabitRPG suspects + or determines, in its own discretion, that you may have or there is a + significant risk that you have (i) failed to comply with any provision + of these Terms of Service or any policies or rules established by + HabitRPG; or (ii) engaged in actions relating to or in the course of + using the Services that may be illegal or cause liability, harm, + embarrassment, harassment, abuse or disruption for you, HabitRPG Users, + HabitRPG or any other third parties or the Services.

    - You may terminate your HabitRPG Account at any time and for any - reason by sending email to support AT HabitRPG.com. Upon any termination - by a Member, the related account will no longer be accessible. + You may terminate your HabitRPG Account at any time and for any + reason by sending email to support AT HabitRPG.com. Upon any termination + by a Member, the related account will no longer be accessible.

    - After any termination, you understand and acknowledge that we - will have no further obligation to provide the Services and all licenses - and other rights granted to you by these Terms of Service will - immediately cease. HabitRPG will not be liable to you or any third party - for termination of the Services or termination of your use of either. - UPON ANY TERMINATION OR SUSPENSION, ANY CONTENT, MATERIALS OR - INFORMATION (INCLUDING PUBLIC USER CONTENT) THAT YOU HAVE SUBMITTED ON THE - SERVICES OR THAT WHICH IS RELATED TO YOUR ACCOUNT MAY NO LONGER BE - ACCESSED BY YOU. Furthermore, HabitRPG will have no obligation to - maintain any information stored in our database related to your account + After any termination, you understand and acknowledge that we + will have no further obligation to provide the Services and all licenses + and other rights granted to you by these Terms of Service will + immediately cease. HabitRPG will not be liable to you or any third party + for termination of the Services or termination of your use of either. + UPON ANY TERMINATION OR SUSPENSION, ANY CONTENT, MATERIALS OR + INFORMATION (INCLUDING PUBLIC USER CONTENT) THAT YOU HAVE SUBMITTED ON THE + SERVICES OR THAT WHICH IS RELATED TO YOUR ACCOUNT MAY NO LONGER BE + ACCESSED BY YOU. Furthermore, HabitRPG will have no obligation to + maintain any information stored in our database related to your account or to forward any information to you or any third party.

    - Any suspension, termination or cancellation will not affect your - obligations to HabitRPG under these Terms of Service (including, without - limitation, proprietary rights and ownership, indemnification and - limitation of liability), which by their sense and context are intended + Any suspension, termination or cancellation will not affect your + obligations to HabitRPG under these Terms of Service (including, without + limitation, proprietary rights and ownership, indemnification and + limitation of liability), which by their sense and context are intended to survive such suspension, termination or cancellation.

    Disclaimers
    - THE SERVICES, HABITRPG CONTENT AND PUBLIC USER CONTENT ARE PROVIDED "AS IS", - WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED. WITHOUT - LIMITING THE FOREGOING, HABITRPG EXPLICITLY DISCLAIMS ANY WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR - NON-INFRINGEMENT, AND ANY WARRANTIES ARISING OUT OF COURSE OF DEALING OR + THE SERVICES, HABITRPG CONTENT AND PUBLIC USER CONTENT ARE PROVIDED "AS IS", + WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED. WITHOUT + LIMITING THE FOREGOING, HABITRPG EXPLICITLY DISCLAIMS ANY WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR + NON-INFRINGEMENT, AND ANY WARRANTIES ARISING OUT OF COURSE OF DEALING OR USAGE OF TRADE.

    - HABITRPG MAKES NO WARRANTY THAT THE SERVICES, HABITRPG CONTENT OR - PUBLIC USER CONTENT WILL MEET YOUR REQUIREMENTS OR BE AVAILABLE ON AN - UNINTERRUPTED, SECURE, OR ERROR-FREE BASIS. HABITRPG MAKES NO WARRANTY - REGARDING THE QUALITY OF ANY PRODUCTS, SERVICES OR CONTENT PURCHASED OR - OBTAINED THROUGH THE SERVICES OR THE ACCURACY, TIMELINESS, TRUTHFULNESS, - COMPLETENESS OR RELIABILITY OF ANY CONTENT OBTAINED THROUGH THE + HABITRPG MAKES NO WARRANTY THAT THE SERVICES, HABITRPG CONTENT OR + PUBLIC USER CONTENT WILL MEET YOUR REQUIREMENTS OR BE AVAILABLE ON AN + UNINTERRUPTED, SECURE, OR ERROR-FREE BASIS. HABITRPG MAKES NO WARRANTY + REGARDING THE QUALITY OF ANY PRODUCTS, SERVICES OR CONTENT PURCHASED OR + OBTAINED THROUGH THE SERVICES OR THE ACCURACY, TIMELINESS, TRUTHFULNESS, + COMPLETENESS OR RELIABILITY OF ANY CONTENT OBTAINED THROUGH THE SERVICES.

    - NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED FROM - HABITRPG OR THROUGH THE SERVICES, HABITRPG CONTENT OR PUBLIC USER CONTENT, WILL + NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED FROM + HABITRPG OR THROUGH THE SERVICES, HABITRPG CONTENT OR PUBLIC USER CONTENT, WILL CREATE ANY WARRANTY NOT EXPRESSLY MADE HEREIN.

    Indemnity
    - You agree to defend, indemnify, and hold HabitRPG, its officers, - directors, employees and agents, harmless from and against any claims, - liabilities, damages, losses, and expenses, including, without - limitation, reasonable legal and accounting fees, arising out of or in - any way connected with Public User Content you submit to HabitRPG, your access - to or use of the Services or HabitRPG Content, or your violation of these + You agree to defend, indemnify, and hold HabitRPG, its officers, + directors, employees and agents, harmless from and against any claims, + liabilities, damages, losses, and expenses, including, without + limitation, reasonable legal and accounting fees, arising out of or in + any way connected with Public User Content you submit to HabitRPG, your access + to or use of the Services or HabitRPG Content, or your violation of these Terms of Service.

    Limitation of Liability
    - YOU ACKNOWLEDGE AND AGREE THAT, TO THE MAXIMUM EXTENT PERMITTED BY LAW, - THE ENTIRE RISK ARISING OUT OF YOUR ACCESS TO AND USE OF THE SERVICES - AND CONTENT THEREIN REMAINS WITH YOU. NEITHER HABITRPG NOR ANY OTHER - PARTY INVOLVED IN CREATING, PRODUCING, OR DELIVERING THE SERVICES OR - HABITRPG CONTENT WILL BE LIABLE FOR ANY INCIDENTAL, SPECIAL, EXEMPLARY OR - CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, LOSS OF DATA OR LOSS OF - GOODWILL, SERVICE INTERRUPTION, COMPUTER DAMAGE OR SYSTEM FAILURE OR THE - COST OF SUBSTITUTE PRODUCTS OR SERVICES, ARISING OUT OF OR IN CONNECTION - WITH THESE TERMS OR FROM THE USE OF OR INABILITY TO USE THE SERVICES OR - CONTENT THEREIN, WHETHER BASED ON WARRANTY, CONTRACT, TORT (INCLUDING - NEGLIGENCE), PRODUCT LIABILITY OR ANY OTHER LEGAL THEORY, AND WHETHER OR - NOT HABITRPG HAS BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGE, EVEN IF - A LIMITED REMEDY SET FORTH HEREIN IS FOUND TO HAVE FAILED OF ITS - ESSENTIAL PURPOSE. YOU SPECIFICALLY ACKNOWLEDGE THAT HABITRPG IS NOT - LIABLE FOR THE DEFAMATORY, OFFENSIVE OR ILLEGAL CONDUCT OF OTHER USERS - OR THIRD PARTIES AND THAT THE RISK OF INJURY FROM THE FOREGOING RESTS - ENTIRELY WITH YOU. FURTHER, HABITRPG WILL HAVE NO LIABILITY TO YOU OR TO - ANY THIRD PARTY FOR ANY PUBLIC USER CONTENT OR THIRD-PARTY CONTENT UPLOADED + YOU ACKNOWLEDGE AND AGREE THAT, TO THE MAXIMUM EXTENT PERMITTED BY LAW, + THE ENTIRE RISK ARISING OUT OF YOUR ACCESS TO AND USE OF THE SERVICES + AND CONTENT THEREIN REMAINS WITH YOU. NEITHER HABITRPG NOR ANY OTHER + PARTY INVOLVED IN CREATING, PRODUCING, OR DELIVERING THE SERVICES OR + HABITRPG CONTENT WILL BE LIABLE FOR ANY INCIDENTAL, SPECIAL, EXEMPLARY OR + CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, LOSS OF DATA OR LOSS OF + GOODWILL, SERVICE INTERRUPTION, COMPUTER DAMAGE OR SYSTEM FAILURE OR THE + COST OF SUBSTITUTE PRODUCTS OR SERVICES, ARISING OUT OF OR IN CONNECTION + WITH THESE TERMS OR FROM THE USE OF OR INABILITY TO USE THE SERVICES OR + CONTENT THEREIN, WHETHER BASED ON WARRANTY, CONTRACT, TORT (INCLUDING + NEGLIGENCE), PRODUCT LIABILITY OR ANY OTHER LEGAL THEORY, AND WHETHER OR + NOT HABITRPG HAS BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGE, EVEN IF + A LIMITED REMEDY SET FORTH HEREIN IS FOUND TO HAVE FAILED OF ITS + ESSENTIAL PURPOSE. YOU SPECIFICALLY ACKNOWLEDGE THAT HABITRPG IS NOT + LIABLE FOR THE DEFAMATORY, OFFENSIVE OR ILLEGAL CONDUCT OF OTHER USERS + OR THIRD PARTIES AND THAT THE RISK OF INJURY FROM THE FOREGOING RESTS + ENTIRELY WITH YOU. FURTHER, HABITRPG WILL HAVE NO LIABILITY TO YOU OR TO + ANY THIRD PARTY FOR ANY PUBLIC USER CONTENT OR THIRD-PARTY CONTENT UPLOADED ONTO OR DOWNLOADED FROM THE SITES OR THROUGH THE SERVICES.

    - IN NO EVENT WILL HABITRPG'S AGGREGATE LIABILITY ARISING OUT OF OR - IN CONNECTION WITH THESE TERMS OF SERVICE OR FROM THE USE OF OR - INABILITY TO USE THE SITE, SERVICES OR CONTENT THEREIN EXCEED ONE - HUNDRED U.S. DOLLARS ($100). THE LIMITATIONS OF DAMAGES SET FORTH ABOVE - ARE FUNDAMENTAL ELEMENTS OF THE BASIS OF THE BARGAIN BETWEEN HABITRPG AND - YOU. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF - LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, SO THE ABOVE + IN NO EVENT WILL HABITRPG'S AGGREGATE LIABILITY ARISING OUT OF OR + IN CONNECTION WITH THESE TERMS OF SERVICE OR FROM THE USE OF OR + INABILITY TO USE THE SITE, SERVICES OR CONTENT THEREIN EXCEED ONE + HUNDRED U.S. DOLLARS ($100). THE LIMITATIONS OF DAMAGES SET FORTH ABOVE + ARE FUNDAMENTAL ELEMENTS OF THE BASIS OF THE BARGAIN BETWEEN HABITRPG AND + YOU. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF + LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, SO THE ABOVE LIMITATION MAY NOT APPLY TO YOU.

    Proprietary Rights Notices
    - All trademarks, service marks, logos, trade names and any other - proprietary designations of HabitRPG used herein are trademarks or - registered trademarks of HabitRPG. Any other trademarks, service marks, - logos, trade names and any other proprietary designations are the - trademarks or registered trademarks of their respective parties. + All trademarks, service marks, logos, trade names and any other + proprietary designations of HabitRPG used herein are trademarks or + registered trademarks of HabitRPG. Any other trademarks, service marks, + logos, trade names and any other proprietary designations are the + trademarks or registered trademarks of their respective parties.

    Controlling Law and Jurisdiction
    - These Terms of Service and any action related thereto will be governed - by the laws of the State of California without regard to its conflict of - laws provisions. The exclusive jurisdiction and venue of any action with - respect to the subject matter of these Terms of Service will be the - courts having jurisdiction over disputes arising in Santa Clara County, - California, and each of the parties hereto waives any objection to + These Terms of Service and any action related thereto will be governed + by the laws of the State of California without regard to its conflict of + laws provisions. The exclusive jurisdiction and venue of any action with + respect to the subject matter of these Terms of Service will be the + courts having jurisdiction over disputes arising in Santa Clara County, + California, and each of the parties hereto waives any objection to jurisdiction and venue in such courts.

    - YOU AGREE THAT IF YOU WANT TO SUE US, YOU MUST FILE YOUR LAWSUIT - WITHIN ONE YEAR AFTER THE EVENT THAT GAVE RISE TO YOUR LAWSUIT. + YOU AGREE THAT IF YOU WANT TO SUE US, YOU MUST FILE YOUR LAWSUIT + WITHIN ONE YEAR AFTER THE EVENT THAT GAVE RISE TO YOUR LAWSUIT. OTHERWISE, YOUR LAWSUIT WILL BE PERMANENTLY BARRED.

    Export Control
    - You may not use or otherwise export or re-export the Services except as - authorized by United States law and the laws of the jurisdiction in - which the Services were obtained. In particular, but without limitation, - the Services may not be exported or re-exported (a) into any U.S. - embargoed countries or (b) to anyone on the U.S. Treasury Department's - list of Specially Designated Nationals or the U.S. Department of - Commerce Denied Person's List or Entity List. By using the Services, you - represent and warrant that you are not located in any such country or on - any such list. You also agree that you will not use these products for - any purposes prohibited by United States law, including, without - limitation, the development, design, manufacture or production of + You may not use or otherwise export or re-export the Services except as + authorized by United States law and the laws of the jurisdiction in + which the Services were obtained. In particular, but without limitation, + the Services may not be exported or re-exported (a) into any U.S. + embargoed countries or (b) to anyone on the U.S. Treasury Department's + list of Specially Designated Nationals or the U.S. Department of + Commerce Denied Person's List or Entity List. By using the Services, you + represent and warrant that you are not located in any such country or on + any such list. You also agree that you will not use these products for + any purposes prohibited by United States law, including, without + limitation, the development, design, manufacture or production of nuclear, missiles, or chemical or biological weapons.

    Entire Agreement
    - These Terms of Service constitute the entire and exclusive understanding - and agreement between HabitRPG and you regarding the Services and HabitRPG - Content, and these Terms of Service supersede and replace any and all - prior oral or written understandings or agreements between HabitRPG and + These Terms of Service constitute the entire and exclusive understanding + and agreement between HabitRPG and you regarding the Services and HabitRPG + Content, and these Terms of Service supersede and replace any and all + prior oral or written understandings or agreements between HabitRPG and you regarding the Services and HabitRPG Content.

    Assignment
    - You may not assign or transfer these Terms of Service, by operation of - law or otherwise, without HabitRPG's prior written consent. Any attempt - by you to assign or transfer these Terms of Service, without such - consent, will be null and of no effect. HabitRPG may freely assign these - Terms of Service. Subject to the foregoing, these Terms of Service will - bind and inure to the benefit of the parties, their successors and + You may not assign or transfer these Terms of Service, by operation of + law or otherwise, without HabitRPG's prior written consent. Any attempt + by you to assign or transfer these Terms of Service, without such + consent, will be null and of no effect. HabitRPG may freely assign these + Terms of Service. Subject to the foregoing, these Terms of Service will + bind and inure to the benefit of the parties, their successors and permitted assigns.

    Notices
    - You consent to the use of: (i) electronic means to complete these Terms - of Service and to deliver any notices or other communications permitted - or required hereunder; and (ii) electronic records to store information - related to these Terms of Service or your use of the Services. Any - notices or other communications permitted to required hereunder, - including those regarding modifications to these Terms of Service, will - be in writing and given: (x) by HabitRPG via email (in each case to the - address that you provide) or (y) by posting to the Sites or Services. - For notices made by e-mail, the date of receipt will be deemed the date + You consent to the use of: (i) electronic means to complete these Terms + of Service and to deliver any notices or other communications permitted + or required hereunder; and (ii) electronic records to store information + related to these Terms of Service or your use of the Services. Any + notices or other communications permitted to required hereunder, + including those regarding modifications to these Terms of Service, will + be in writing and given: (x) by HabitRPG via email (in each case to the + address that you provide) or (y) by posting to the Sites or Services. + For notices made by e-mail, the date of receipt will be deemed the date on which such notice is transmitted.

    General
    - The failure of HabitRPG to enforce any right or provision of these Terms - of Service will not constitute a waiver of future enforcement of that - right or provision. The waiver of any such right or provision will be - effective only if in writing and signed by a duly authorized - representative of HabitRPG. Except as expressly set forth in these Terms - of Service, the exercise by either party of any of its remedies under - these Terms of Service will be without prejudice to its other remedies - under these Terms of Service or otherwise. If for any reason a court of - competent jurisdiction finds any provision of these Terms of Service - invalid or unenforceable, that provision will be enforced to the maximum - extent permissible and the other provisions of these Terms of Service + The failure of HabitRPG to enforce any right or provision of these Terms + of Service will not constitute a waiver of future enforcement of that + right or provision. The waiver of any such right or provision will be + effective only if in writing and signed by a duly authorized + representative of HabitRPG. Except as expressly set forth in these Terms + of Service, the exercise by either party of any of its remedies under + these Terms of Service will be without prejudice to its other remedies + under these Terms of Service or otherwise. If for any reason a court of + competent jurisdiction finds any provision of these Terms of Service + invalid or unenforceable, that provision will be enforced to the maximum + extent permissible and the other provisions of these Terms of Service will remain in full force and effect.

    Contacting Us
    - If you have any questions about these Terms of Service, please contact + If you have any questions about these Terms of Service, please contact us at tylerrenelle@gmail.com.

    - \ No newline at end of file + From 6ab64e2df13e96789d4933a602e8da052efe7218 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 14 Jun 2013 15:56:27 -0400 Subject: [PATCH 129/157] add migration script for moving facebook auth'd users to local auth (seems people hate facebook) --- migrations/20130212_preen_cron.js | 2 +- migrations/facebook_to_local.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 migrations/facebook_to_local.js diff --git a/migrations/20130212_preen_cron.js b/migrations/20130212_preen_cron.js index 72261f246a..95b13b0c8a 100644 --- a/migrations/20130212_preen_cron.js +++ b/migrations/20130212_preen_cron.js @@ -1,7 +1,7 @@ /** * Set this up as a midnight cron script * - * mongo habitrpg node_modules/moment/moment.js migrations/json.js migrations/20130212_preen_cron.js + * mongo habitrpg node_modules/moment/moment.js migrations/20130212_preen_cron.js */ diff --git a/migrations/facebook_to_local.js b/migrations/facebook_to_local.js new file mode 100644 index 0000000000..c8238a77fc --- /dev/null +++ b/migrations/facebook_to_local.js @@ -0,0 +1,10 @@ +var oldId = "", + newId = "", + newUser = db.users.findOne({_id: newId}) + +db.users.update({_id: oldId}, {$set:{auth: newUser.auth}}); + +// remove the auth on the new user (which is a template account). The account will be preened automatically later, +// this allows us to keep the account around a few days in case there was a mistake +db.users.update({_id: newId}, {$unset:{auth:1}}); + From 38b4e3322e355eea440f7dd227c082a8a8e0dfd9 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 14 Jun 2013 16:09:26 -0400 Subject: [PATCH 130/157] preen-script: some cleanup and better checking of lastCron --- migrations/20130212_preen_cron.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/migrations/20130212_preen_cron.js b/migrations/20130212_preen_cron.js index 95b13b0c8a..ac3b1e9e44 100644 --- a/migrations/20130212_preen_cron.js +++ b/migrations/20130212_preen_cron.js @@ -23,19 +23,15 @@ var un_registered = { }, today = +new Date; -// isValidDate = (d) -> -// return false if Object::toString.call(d) isnt "[object Date]" -// not isNaN(d.getTime()) - - db.users.find(un_registered).forEach(function(user) { - if (!user) return; - if (!!user.lastCron) { - if (Math.abs(moment(today).diff(user.lastCron, 'days')) > 7) { - return db.users.remove({_id:user._id}); + //if (!user) return; + var lastCron = user.lastCron; + if (lastCron && moment(lastCron).isValid()) { + if (Math.abs(moment(today).diff(lastCron, 'days')) > 5) { + return db.users.remove({_id: user._id}); } } else { - return db.users.update({_id: user._id}, {$set: {lastCron: today}}); + return db.users.update({_id: user._id}, {$set: {'lastCron': today}}); } }); From 20196a93831b2f31b1a4ca4ab438da4c09d56872 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 14 Jun 2013 16:09:55 -0400 Subject: [PATCH 131/157] preen-cron: remove the date, it's not a migration but a cron script --- migrations/{20130212_preen_cron.js => preen_cron.js} | 6 ------ 1 file changed, 6 deletions(-) rename migrations/{20130212_preen_cron.js => preen_cron.js} (90%) diff --git a/migrations/20130212_preen_cron.js b/migrations/preen_cron.js similarity index 90% rename from migrations/20130212_preen_cron.js rename to migrations/preen_cron.js index ac3b1e9e44..b514643973 100644 --- a/migrations/20130212_preen_cron.js +++ b/migrations/preen_cron.js @@ -15,12 +15,6 @@ var un_registered = { "auth.local": {$exists: false}, "auth.facebook": {$exists: false} }, - registered = { - $or: [ - { 'auth.local': { $exists: true }}, - { 'auth.facebook': { $exists: true }} - ] - }, today = +new Date; db.users.find(un_registered).forEach(function(user) { From 8c27f96d086a2bfabf80f4974200c2d09fbc2d7f Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 14 Jun 2013 16:17:41 -0400 Subject: [PATCH 132/157] start with metrics mongo script - still need to add "active users" --- migrations/metrics.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 migrations/metrics.js diff --git a/migrations/metrics.js b/migrations/metrics.js new file mode 100644 index 0000000000..116cc4648c --- /dev/null +++ b/migrations/metrics.js @@ -0,0 +1,27 @@ +var + corrupt = { + $or: [ + {lastCron: {$exists:false}}, + {lastCron: 'new'} + ] + } + + un_registered = { + "auth.local": {$exists: false}, + "auth.facebook": {$exists: false} + }, + + registered = { + $or: [ + { 'auth.local': { $exists: true }}, + { 'auth.facebook': { $exists: true }} + ] + }; + +print('corrupt: ' + db.users.count(corrupt)); +print('unregistered: ' + db.users.count(un_registered)); +print('registered: ' + db.users.count(registered)); + +// TODO active users +// - history > 14 entries +// - lastCron < 14d \ No newline at end of file From 67b2174c3dfee46a9c1bcac208dc5c2ccd05e73a Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 14 Jun 2013 16:56:42 -0400 Subject: [PATCH 133/157] finish metrics script. current total: unregistered: 180931 registered: 52101 active: 4182 (@Slappybag) --- migrations/metrics.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/migrations/metrics.js b/migrations/metrics.js index 116cc4648c..cee72908ec 100644 --- a/migrations/metrics.js +++ b/migrations/metrics.js @@ -1,4 +1,11 @@ +// mongo habitrpg ./migrations/metrics.js + +load('./node_modules/moment/moment.js'); + var + today = +new Date, + twoWeeksAgo = +moment().subtract(14, 'days'); + corrupt = { $or: [ {lastCron: {$exists:false}}, @@ -16,12 +23,20 @@ var { 'auth.local': { $exists: true }}, { 'auth.facebook': { $exists: true }} ] + }, + + active = { + $or: [ + { 'auth.local': { $exists: true }}, + { 'auth.facebook': { $exists: true }} + ], + $where: function(){ + return this.history && this.history.exp && this.history.exp.length > 14; + }, + 'lastCron': {$gt: twoWeeksAgo} }; print('corrupt: ' + db.users.count(corrupt)); print('unregistered: ' + db.users.count(un_registered)); print('registered: ' + db.users.count(registered)); - -// TODO active users -// - history > 14 entries -// - lastCron < 14d \ No newline at end of file +print('active: ' + db.users.count(active)); \ No newline at end of file From 9361b015f6eaf6e7875bf4ab6613f452c2e62692 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 14 Jun 2013 17:01:50 -0400 Subject: [PATCH 134/157] preen-script: also remove empty parties --- migrations/preen_cron.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/migrations/preen_cron.js b/migrations/preen_cron.js index b514643973..8696bee78f 100644 --- a/migrations/preen_cron.js +++ b/migrations/preen_cron.js @@ -1,9 +1,10 @@ /** * Set this up as a midnight cron script * - * mongo habitrpg node_modules/moment/moment.js migrations/20130212_preen_cron.js + * mongo habitrpg migrations/preen_cron.js */ +load('./node_modules/moment/moment.js'); /* Users are allowed to experiment with the site before registering. Every time a new browser visits habitrpg, a new @@ -11,11 +12,19 @@ This function removes all staged accounts that have been abandoned - either older than a month, or corrupted in some way (lastCron==undefined) */ -var un_registered = { +var + today = +new Date, + + un_registered = { "auth.local": {$exists: false}, "auth.facebook": {$exists: false} }, - today = +new Date; + + emptyParties = { + $where: function(){ + return this.type === 'party' && this.members.length === 0; + } + }; db.users.find(un_registered).forEach(function(user) { //if (!user) return; @@ -29,6 +38,8 @@ db.users.find(un_registered).forEach(function(user) { } }); +db.users.groups.remove(emptyParties); + /** * Don't remove missing user auths anymore. This was previously necessary due to data corruption, From 42be939b121ce176c1e8060372972014af44db2e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 14 Jun 2013 19:05:26 -0400 Subject: [PATCH 135/157] preen-script: start implementing history-preening --- migrations/preen_cron.js | 61 ++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/migrations/preen_cron.js b/migrations/preen_cron.js index 8696bee78f..dd2c2faa1d 100644 --- a/migrations/preen_cron.js +++ b/migrations/preen_cron.js @@ -4,6 +4,7 @@ * mongo habitrpg migrations/preen_cron.js */ +load('./node_modules/lodash/lodash.js'); load('./node_modules/moment/moment.js'); /* @@ -26,21 +27,57 @@ var } }; -db.users.find(un_registered).forEach(function(user) { - //if (!user) return; - var lastCron = user.lastCron; - if (lastCron && moment(lastCron).isValid()) { - if (Math.abs(moment(today).diff(lastCron, 'days')) > 5) { - return db.users.remove({_id: user._id}); - } - } else { - return db.users.update({_id: user._id}, {$set: {'lastCron': today}}); +//db.users.find(un_registered).forEach(function(user) { +// //if (!user) return; +// var lastCron = user.lastCron; +// if (lastCron && moment(lastCron).isValid()) { +// if (Math.abs(moment(today).diff(lastCron, 'days')) > 5) { +// return db.users.remove({_id: user._id}); +// } +// } else { +// return db.users.update({_id: user._id}, {$set: {'lastCron': today}}); +// } +//}); + +//db.users.groups.remove(emptyParties); + +//FIXME make sure this doesn't conflict with preen un_registered above + +function preenHistory(history) { + var newHistory = []; + function preen(amount, format) { + var group, sliced, avg; + group = _(history) + .groupBy(function(h){ return moment(h.date).format(format) }) + .sortBy(function(h,k) {return k;}); // TODO make sure this is the right order + if (_.size(group) === 0) return; + sliced = _.toArray(group).slice(_.size(group) - 1, -amount); + avg = _.reduce(sliced, function(mem, obj){ return mem + obj.value }) / _.size(group); + newHistory.concat({date: sliced[0].date, value: avg}); } + + preen(50,'YYYY'); // last 50 years + preen(12,'MMYYYY'); // last 12 months + preen(4,'wYYYY'); // last 4 weeks + newHistory.concat(history.slice(-7)); // last 7 days + return newHistory; +} + +db.users.find({ + $where: function(){ return this.history && this.history.exp && this.history.exp.length > 7; } +}).forEach(function(user) { + var update = {$set:{}}; + + _.each(user.tasks, function(task) { + if (task.type === 'habit' || task.type === 'daily') + update['$set']['tasks.' + task.id + '.history'] = preenHistory(task.history); + }) + + // TODO user.history.exp, user.history.todos + + if (!_.isEmpty(update['$set'])) db.users.update({_id:user.id}, update); }); -db.users.groups.remove(emptyParties); - - /** * Don't remove missing user auths anymore. This was previously necessary due to data corruption, * revisit if needs be From 6b307cd52c1cf1aa2323ef21f9d1678fc035f635 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 14 Jun 2013 19:19:08 -0400 Subject: [PATCH 136/157] preen-script - we can't use load() because we don't know mongo shell pwd() at server run-time --- migrations/preen_cron.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/migrations/preen_cron.js b/migrations/preen_cron.js index 8696bee78f..d17f241c5c 100644 --- a/migrations/preen_cron.js +++ b/migrations/preen_cron.js @@ -1,11 +1,9 @@ /** * Set this up as a midnight cron script * - * mongo habitrpg migrations/preen_cron.js + * mongo habitrpg ./node_modules/moment/moment.js migrations/preen_cron.js */ -load('./node_modules/moment/moment.js'); - /* Users are allowed to experiment with the site before registering. Every time a new browser visits habitrpg, a new "staged" account is created - and if the user later registeres, that staged account is considered a "production" account. From 22d4bb27acc53a58724c338679df3c62e9898117 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 15 Jun 2013 12:01:03 -0400 Subject: [PATCH 137/157] preen-cron: fix up history cron, should be fully functional now --- migrations/preen_cron.js | 97 ++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 43 deletions(-) diff --git a/migrations/preen_cron.js b/migrations/preen_cron.js index dd2c2faa1d..ff49151b72 100644 --- a/migrations/preen_cron.js +++ b/migrations/preen_cron.js @@ -7,64 +7,74 @@ load('./node_modules/lodash/lodash.js'); load('./node_modules/moment/moment.js'); -/* - Users are allowed to experiment with the site before registering. Every time a new browser visits habitrpg, a new - "staged" account is created - and if the user later registeres, that staged account is considered a "production" account. - This function removes all staged accounts that have been abandoned - either older than a month, or corrupted in some way (lastCron==undefined) +var today = +new Date; + +/** + * Users are allowed to experiment with the site before registering. Every time a new browser visits habitrpg, a new + * "staged" account is created - and if the user later registeres, that staged account is considered a "production" account. + * This function removes all staged accounts that have been abandoned - either older than a month, or corrupted in some way (lastCron==undefined) */ +db.users.find({ -var - today = +new Date, + // Un-registered users + "auth.local": {$exists: false}, + "auth.facebook": {$exists: false} - un_registered = { - "auth.local": {$exists: false}, - "auth.facebook": {$exists: false} - }, - - emptyParties = { - $where: function(){ - return this.type === 'party' && this.members.length === 0; +}).forEach(function(user) { + //if (!user) return; + var lastCron = user.lastCron; + if (lastCron && moment(lastCron).isValid()) { + if (Math.abs(moment(today).diff(lastCron, 'days')) > 5) { + return db.users.remove({_id: user._id}); } - }; + } else { + return db.users.update({_id: user._id}, {$set: {'lastCron': today}}); + } +}); -//db.users.find(un_registered).forEach(function(user) { -// //if (!user) return; -// var lastCron = user.lastCron; -// if (lastCron && moment(lastCron).isValid()) { -// if (Math.abs(moment(today).diff(lastCron, 'days')) > 5) { -// return db.users.remove({_id: user._id}); -// } -// } else { -// return db.users.update({_id: user._id}, {$set: {'lastCron': today}}); -// } -//}); - -//db.users.groups.remove(emptyParties); - -//FIXME make sure this doesn't conflict with preen un_registered above +/** + * Remove empty parties + */ +db.users.groups.remove({ + // Empty Parties + $where: function(){ return this.type === 'party' && this.members.length === 0; } +}); +/** + * Preen history for users with > 7 history entries + */ function preenHistory(history) { var newHistory = []; function preen(amount, format) { - var group, sliced, avg; - group = _(history) + var groups, avg, start; + groups = _(history) .groupBy(function(h){ return moment(h.date).format(format) }) - .sortBy(function(h,k) {return k;}); // TODO make sure this is the right order - if (_.size(group) === 0) return; - sliced = _.toArray(group).slice(_.size(group) - 1, -amount); - avg = _.reduce(sliced, function(mem, obj){ return mem + obj.value }) / _.size(group); - newHistory.concat({date: sliced[0].date, value: avg}); + .sortBy(function(h,k) {return k;}) // TODO make sure this is the right order + .value() + start = (groups.length - amount > 0) ? groups.length - amount : 0; + groups = groups.slice(start, groups.length - 1) + _.each(groups, function(group){ + avg = _.reduce(group, function(mem, obj){ return mem + obj.value }, 0) / group.length; + newHistory.push({date: +moment(group[0].date), value: avg}); + }) } - preen(50,'YYYY'); // last 50 years - preen(12,'MMYYYY'); // last 12 months - preen(4,'wYYYY'); // last 4 weeks - newHistory.concat(history.slice(-7)); // last 7 days + preen(50, 'YYYY', 'YYYY'); // last 50 years + preen(12, 'YYYYMM', 'MMM YYYY'); // last 12 months + preen(4, 'YYYYww', 'WW YYYY'); // last 4 weeks + newHistory = newHistory.concat(history.slice(-7)); // last 7 days return newHistory; } db.users.find({ + + // Registered users with > 7 history entries + $or: [ + { 'auth.local': { $exists: true }}, + { 'auth.facebook': { $exists: true }} + ], $where: function(){ return this.history && this.history.exp && this.history.exp.length > 7; } + }).forEach(function(user) { var update = {$set:{}}; @@ -73,9 +83,10 @@ db.users.find({ update['$set']['tasks.' + task.id + '.history'] = preenHistory(task.history); }) - // TODO user.history.exp, user.history.todos + update['$set']['history.exp'] = preenHistory(user.history.exp); + update['$set']['history.todos'] = preenHistory(user.history.todos); - if (!_.isEmpty(update['$set'])) db.users.update({_id:user.id}, update); + db.users.update({_id: user._id}, update); }); /** From d5d8a9ef69d93982f3e40b0d7b56a16b5efad384 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 15 Jun 2013 12:35:55 -0400 Subject: [PATCH 138/157] charts: fix exp & todos charts --- src/app/tasks.coffee | 34 ++++++++++++++++------------------ views/app/header.html | 2 +- views/app/index.html | 2 +- views/app/tasks.html | 4 ++-- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index bc2a5e1331..4403bb027b 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -56,30 +56,28 @@ module.exports.app = (appExports, model) -> appExports.toggleTaskEdit = (e, el) -> id = e.get('id') - path = "_tasks.editing.#{id}" - model.set path, !model.get(path) - $(".#{id}-chart").hide() + [editPath, chartPath] = ["_tasks.editing.#{id}", "_page.charts.#{id}"] + model.set editPath, !(model.get editPath) + model.set chartPath, false appExports.toggleChart = (e, el) -> id = $(el).attr('data-id') - history = [] + [historyPath, togglePath] = ['',''] - if id is 'todos' - model.set "_tasks.charts.todos", !model.get("_tasks.charts.todos") - history = model.get("_user.history.todos") - $(".#{id}-chart").toggle() - else - [id, path] = [$(el).attr('data-id'), "_tasks.charts.#{id}"] - model.set path, !model.get(path) - model.set "_tasks.editing.#{id}", false - $(".#{id}-chart").toggle() - history = model.get("_user.tasks.#{id}.history") + switch id + when 'exp' + [togglePath, historyPath] = ['_page.charts.exp', '_user.history.exp'] + when 'todos' + [togglePath, historyPath] = ['_page.charts.todos', '_user.history.todos'] + else + [togglePath, historyPath] = ["_page.charts.#{id}", "_user.tasks.#{id}.history"] + model.set "_tasks.editing.#{id}", false + + history = model.get(historyPath) + model.set togglePath, !(model.get togglePath) matrix = [['Date', 'Score']] - for obj in history - date = +new Date(obj.date) - readableDate = moment(date).format('MM/DD') - matrix.push [ readableDate, obj.value ] + _.each history, (obj) -> matrix.push([ moment(obj.date).format('MM/DD/YY'), obj.value ]) data = google.visualization.arrayToDataTable matrix options = title: 'History' diff --git a/views/app/header.html b/views/app/header.html index c271a475be..f2c5e045e9 100644 --- a/views/app/header.html +++ b/views/app/header.html @@ -16,7 +16,7 @@
    {#if _user.history.exp} -   +   {/} {floor(_user.stats.exp)} / {tnl(_user.stats.lvl)} diff --git a/views/app/index.html b/views/app/index.html index 4cfd3048e3..dd60c0896d 100644 --- a/views/app/index.html +++ b/views/app/index.html @@ -40,7 +40,7 @@
    {#if _user.preferences.hideHeader}{/} - +
    diff --git a/views/app/tasks.html b/views/app/tasks.html index 08e2229c41..8468064a61 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -132,7 +132,7 @@

    {{t(@header)}}

    - {{#if equal(@type,'todo')}}{{/}} + {{#if equal(@type,'todo')}}
    {{/}} {#if @editable} @@ -354,4 +354,4 @@
    - +
    From c797ea59998cbbbf11244771e81e2138d61bc76c Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 15 Jun 2013 12:35:55 -0400 Subject: [PATCH 139/157] charts: fix exp & todos charts --- src/app/tasks.coffee | 34 ++++++++++++++++------------------ views/app/header.html | 2 +- views/app/index.html | 2 +- views/app/tasks.html | 4 ++-- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index bc2a5e1331..4403bb027b 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -56,30 +56,28 @@ module.exports.app = (appExports, model) -> appExports.toggleTaskEdit = (e, el) -> id = e.get('id') - path = "_tasks.editing.#{id}" - model.set path, !model.get(path) - $(".#{id}-chart").hide() + [editPath, chartPath] = ["_tasks.editing.#{id}", "_page.charts.#{id}"] + model.set editPath, !(model.get editPath) + model.set chartPath, false appExports.toggleChart = (e, el) -> id = $(el).attr('data-id') - history = [] + [historyPath, togglePath] = ['',''] - if id is 'todos' - model.set "_tasks.charts.todos", !model.get("_tasks.charts.todos") - history = model.get("_user.history.todos") - $(".#{id}-chart").toggle() - else - [id, path] = [$(el).attr('data-id'), "_tasks.charts.#{id}"] - model.set path, !model.get(path) - model.set "_tasks.editing.#{id}", false - $(".#{id}-chart").toggle() - history = model.get("_user.tasks.#{id}.history") + switch id + when 'exp' + [togglePath, historyPath] = ['_page.charts.exp', '_user.history.exp'] + when 'todos' + [togglePath, historyPath] = ['_page.charts.todos', '_user.history.todos'] + else + [togglePath, historyPath] = ["_page.charts.#{id}", "_user.tasks.#{id}.history"] + model.set "_tasks.editing.#{id}", false + + history = model.get(historyPath) + model.set togglePath, !(model.get togglePath) matrix = [['Date', 'Score']] - for obj in history - date = +new Date(obj.date) - readableDate = moment(date).format('MM/DD') - matrix.push [ readableDate, obj.value ] + _.each history, (obj) -> matrix.push([ moment(obj.date).format('MM/DD/YY'), obj.value ]) data = google.visualization.arrayToDataTable matrix options = title: 'History' diff --git a/views/app/header.html b/views/app/header.html index c271a475be..f2c5e045e9 100644 --- a/views/app/header.html +++ b/views/app/header.html @@ -16,7 +16,7 @@
    {#if _user.history.exp} -   +   {/} {floor(_user.stats.exp)} / {tnl(_user.stats.lvl)} diff --git a/views/app/index.html b/views/app/index.html index 4cfd3048e3..dd60c0896d 100644 --- a/views/app/index.html +++ b/views/app/index.html @@ -40,7 +40,7 @@
    {#if _user.preferences.hideHeader}{/} - +
    diff --git a/views/app/tasks.html b/views/app/tasks.html index 08e2229c41..8468064a61 100644 --- a/views/app/tasks.html +++ b/views/app/tasks.html @@ -132,7 +132,7 @@

    {{t(@header)}}

    - {{#if equal(@type,'todo')}}{{/}} + {{#if equal(@type,'todo')}}
    {{/}} {#if @editable} @@ -354,4 +354,4 @@
    - +
    From cc0206e54ecb463274b7de35d8ef33e049a21b8f Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 15 Jun 2013 12:52:21 -0400 Subject: [PATCH 140/157] preen-cron: some history-preening cleanup & coments --- migrations/preen_cron.js | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/migrations/preen_cron.js b/migrations/preen_cron.js index 2b67c07f4d..a22aebde31 100644 --- a/migrations/preen_cron.js +++ b/migrations/preen_cron.js @@ -11,8 +11,9 @@ var today = +new Date; /** * Users are allowed to experiment with the site before registering. Every time a new browser visits habitrpg, a new - * "staged" account is created - and if the user later registeres, that staged account is considered a "production" account. - * This function removes all staged accounts that have been abandoned - either older than a month, or corrupted in some way (lastCron==undefined) + * "staged" account is created - and if the user later registers, that staged account is considered a "production" account. + * This function removes all staged accounts that have been abandoned - either older than a month, or corrupted in + * some way (lastCron==undefined) */ db.users.find({ @@ -42,15 +43,20 @@ db.users.groups.remove({ /** * Preen history for users with > 7 history entries + * This takes an infinite array of single day entries [day day day day day...], and turns it into a condensed array + * of averages, condensing more the further back in time we go. Eg, 7 entries each for last 7 days; 4 entries for last + * 4 weeks; 12 entries for last 12 months; 1 entry per year before that: [day*7 week*4 month*12 year*infinite] */ function preenHistory(history) { var newHistory = []; - function preen(amount, format) { + function preen(amount, groupBy) { var groups, avg, start; groups = _(history) - .groupBy(function(h){ return moment(h.date).format(format) }) - .sortBy(function(h,k) {return k;}) // TODO make sure this is the right order - .value() + .filter(function(h){return !!h}) // discard nulls (corrupted somehow) + .groupBy(function(h){ return moment(h.date).format(groupBy) }) // get date groupings to average against + .sortBy(function(h,k) {return k;}) // sort by date + .value(); // turn into an array + amount++; // if we want the last 4 weeks, we're going 4 weeks back excluding this week. so +1 to account for exclusion start = (groups.length - amount > 0) ? groups.length - amount : 0; groups = groups.slice(start, groups.length - 1) _.each(groups, function(group){ @@ -59,9 +65,9 @@ function preenHistory(history) { }) } - preen(50, 'YYYY', 'YYYY'); // last 50 years - preen(12, 'YYYYMM', 'MMM YYYY'); // last 12 months - preen(4, 'YYYYww', 'WW YYYY'); // last 4 weeks + preen(50, 'YYYY'); // last 50 years + preen(12, 'YYYYMM'); // last 12 months + preen(4, 'YYYYww'); // last 4 weeks newHistory = newHistory.concat(history.slice(-7)); // last 7 days return newHistory; } @@ -79,12 +85,14 @@ db.users.find({ var update = {$set:{}}; _.each(user.tasks, function(task) { - if (task.type === 'habit' || task.type === 'daily') + if ( task.history && task.history.length > 7 ) update['$set']['tasks.' + task.id + '.history'] = preenHistory(task.history); }) - update['$set']['history.exp'] = preenHistory(user.history.exp); - update['$set']['history.todos'] = preenHistory(user.history.todos); + if (user.history.exp && user.history.exp.length > 7) + update['$set']['history.exp'] = preenHistory(user.history.exp); + if (user.history.todos && user.history.todos.length > 7) + update['$set']['history.todos'] = preenHistory(user.history.todos); db.users.update({_id: user._id}, update); }); From 4f948d401b1f077bbd52a4e4fe67b6195ba2fd94 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 15 Jun 2013 13:21:41 -0400 Subject: [PATCH 141/157] Move exp progress button to right of numbers, css rollover was covering it before --- views/app/header.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/views/app/header.html b/views/app/header.html index f2c5e045e9..ae80e5fe5b 100644 --- a/views/app/header.html +++ b/views/app/header.html @@ -15,10 +15,11 @@
    - {#if _user.history.exp} -   - {/} {floor(_user.stats.exp)} / {tnl(_user.stats.lvl)} + + {{#if _user.history.exp}} +   + {{/}}
    From 00e2841b787cd8f9b5b08e4fc3b7bd90775ce2fe Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 15 Jun 2013 13:32:50 -0400 Subject: [PATCH 142/157] history preen cleanup --- migrations/preen_cron.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/migrations/preen_cron.js b/migrations/preen_cron.js index a22aebde31..fea1ef5e4f 100644 --- a/migrations/preen_cron.js +++ b/migrations/preen_cron.js @@ -37,7 +37,6 @@ db.users.find({ * Remove empty parties */ db.users.groups.remove({ - // Empty Parties $where: function(){ return this.type === 'party' && this.members.length === 0; } }); @@ -48,12 +47,12 @@ db.users.groups.remove({ * 4 weeks; 12 entries for last 12 months; 1 entry per year before that: [day*7 week*4 month*12 year*infinite] */ function preenHistory(history) { + history = _.filter(history, function(h) {return !!h}); // discard nulls (corrupted somehow) var newHistory = []; function preen(amount, groupBy) { var groups, avg, start; groups = _(history) - .filter(function(h){return !!h}) // discard nulls (corrupted somehow) - .groupBy(function(h){ return moment(h.date).format(groupBy) }) // get date groupings to average against + .groupBy(function(h) { return moment(h.date).format(groupBy) }) // get date groupings to average against .sortBy(function(h,k) {return k;}) // sort by date .value(); // turn into an array amount++; // if we want the last 4 weeks, we're going 4 weeks back excluding this week. so +1 to account for exclusion @@ -72,29 +71,30 @@ function preenHistory(history) { return newHistory; } +var minHistLen = 7; db.users.find({ - // Registered users with > 7 history entries + // Registered users with some history $or: [ { 'auth.local': { $exists: true }}, { 'auth.facebook': { $exists: true }} ], - $where: function(){ return this.history && this.history.exp && this.history.exp.length > 7; } + 'history': {$exists: true} }).forEach(function(user) { var update = {$set:{}}; _.each(user.tasks, function(task) { - if ( task.history && task.history.length > 7 ) + if ( task.history && task.history.length > minHistLen ) update['$set']['tasks.' + task.id + '.history'] = preenHistory(task.history); }) - if (user.history.exp && user.history.exp.length > 7) + if (user.history.exp && user.history.exp.length > minHistLen) update['$set']['history.exp'] = preenHistory(user.history.exp); - if (user.history.todos && user.history.todos.length > 7) + if (user.history.todos && user.history.todos.length > minHistLen) update['$set']['history.todos'] = preenHistory(user.history.todos); - db.users.update({_id: user._id}, update); + if (!_.isEmpty(update['$set'])) db.users.update({_id: user._id}, update); }); /** From 989d5839c4fbd3948c328ee89ab21aff53c3f38f Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 15 Jun 2013 13:52:10 -0400 Subject: [PATCH 143/157] more accurate "active users": 7417 --- migrations/metrics.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/migrations/metrics.js b/migrations/metrics.js index cee72908ec..e424cec117 100644 --- a/migrations/metrics.js +++ b/migrations/metrics.js @@ -30,9 +30,9 @@ var { 'auth.local': { $exists: true }}, { 'auth.facebook': { $exists: true }} ], - $where: function(){ - return this.history && this.history.exp && this.history.exp.length > 14; - }, +// $where: function(){ +// return this.history && this.history.exp && this.history.exp.length > 7; +// }, 'lastCron': {$gt: twoWeeksAgo} }; From 013cc7e5b70245471904b63832aa3dc54c6a9e33 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 15 Jun 2013 16:47:50 -0400 Subject: [PATCH 144/157] migrations: try adding a coffeescript method, which uses mongoskin to run the migrations. Looks like it's not quite there, collection.toArray() pulls all objects into memory which is too big to run the migrations - might have to stick to mongo scripts --- migrate.coffee | 13 ++++ migrations/preen_cron.coffee | 120 +++++++++++++++++++++++++++++++++++ package.json | 4 +- 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 migrate.coffee create mode 100644 migrations/preen_cron.coffee diff --git a/migrate.coffee b/migrate.coffee new file mode 100644 index 0000000000..cfa1d42c98 --- /dev/null +++ b/migrate.coffee @@ -0,0 +1,13 @@ +mongo = require 'mongoskin' +argv = require('optimist').argv +conf = require 'nconf' +conf.argv().env().file({ file: __dirname + "/config.json" }) + +migration = argv.f +throw 'Please specify migration file' unless migration + +db = mongo.db(conf.get("NODE_DB_URI") + '?auto_reconnect', {safe:true}) +require("./migrations/#{migration}") db, -> + console.log 'all done' + db.close() + process.exit() \ No newline at end of file diff --git a/migrations/preen_cron.coffee b/migrations/preen_cron.coffee new file mode 100644 index 0000000000..a250fa20f9 --- /dev/null +++ b/migrations/preen_cron.coffee @@ -0,0 +1,120 @@ +### + Set this up as a midnight cron script + coffee migrate.coffee -f "preen_cron" +### + +_ = require('lodash') +moment = require('moment') +async = require('async') +today = +new Date +minHistLen = 7 + +module.exports = (db, allMigrationsComplete) -> + + doneCounter = 0 + oneMigrationDown = (err, results) -> + throw err if err + console.log 'One migration down' + allMigrationsComplete() if ++doneCounter is 3 + + users = db.collection('users') + + ### + Remove empty parties + ### + db.collection('groups').remove + $where: -> @type is 'party' and @members.length is 0 + , oneMigrationDown + + ### + Users are allowed to experiment with the site before registering. Every time a new browser visits habitrpg, a new + "staged" account is created - and if the user later registers, that staged account is considered a "production" account. + This function removes all staged accounts that have been abandoned - either older than a month, or corrupted in + some way (lastCron==undefined) + ### + $statingUsersQ = async.queue (user, done) -> + lastCron = user.lastCron; + if lastCron && moment(lastCron).isValid() + if Math.abs(moment(today).diff(lastCron, 'days')) > 5 + users.remove {_id: user._id}, done + else done() + else + users.update {_id: user._id}, {$set: {'lastCron': today}}, done + , 1000 + $statingUsersQ.drain = oneMigrationDown #final callback + + users.find({ + # Un-registered users + "auth.local": {$exists: false} + "auth.facebook": {$exists: false} + }).each (err, user) -> + throw err if err + $statingUsersQ.push(user) + + + ### + Preen history for users with > 7 history entries + This takes an infinite array of single day entries [day day day day day...], and turns it into a condensed array + of averages, condensing more the further back in time we go. Eg, 7 entries each for last 7 days; 4 entries for last + 4 weeks; 12 entries for last 12 months; 1 entry per year before that: [day*7 week*4 month*12 year*infinite] + ### + preenHistory = (history) -> + history = _.filter(history, (h) -> !!h) # discard nulls (corrupted somehow) + newHistory = [] + preen = (amount, groupBy) -> + groups = _(history) + .groupBy((h) -> moment(h.date).format(groupBy)) # get date groupings to average against + .sortBy((h,k) -> k) # sort by date + .value() # turn into an array + amount++; # if we want the last 4 weeks, we're going 4 weeks back excluding this week. so +1 to account for exclusion + start = if (groups.length - amount > 0) then groups.length - amount else 0 + groups = groups.slice(start, groups.length - 1) + _.each groups, (group) -> + avg = _.reduce(group, ((mem, obj) -> mem + obj.value), 0) / group.length; + newHistory.push {date: +moment(group[0].date), value: avg} + + preen(50, 'YYYY') # last 50 years + preen(12, 'YYYYMM') # last 12 months + preen(4, 'YYYYww') # last 4 weeks + newHistory = newHistory.concat history.slice(-7) # last 7 days + return newHistory + + $preenHistoryQ = async.queue (user, done) -> + update = {$set:{}} + + _.each user.tasks, (task) -> + if task.history?.length > minHistLen + update['$set']['tasks.' + task.id + '.history'] = preenHistory(task.history) + + if user.history?.exp?.length > minHistLen + update['$set']['history.exp'] = preenHistory(user.history.exp) + if user.history?.todos?.length > minHistLen + update['$set']['history.todos'] = preenHistory(user.history.todos) + + if _.isEmpty(update['$set']) then done() + else users.update {_id: user._id}, update, done + , 1000 + $preenHistoryQ.drain = oneMigrationDown + + users.find({ + # Registered users with some history + $or: [ + { 'auth.local': { $exists: true }}, + { 'auth.facebook': { $exists: true }} + ], + 'history': {$exists: true} + }).each (err, user) -> + throw err if err + $preenHistoryQ.push(user) + +# /** +# * Don't remove missing user auths anymore. This was previously necessary due to data corruption, +# * revisit if needs be +# */ +# /*db.sessions.find().forEach(function(sess){ +# var uid = JSON.parse(sess.session).userId; +# if (!uid || db.users.count({_id:uid}) === 0) { +# db.sessions.remove({_id:sess._id}); +# } +# });*/ + diff --git a/package.json b/package.json index 4c80dc0ef7..726c66b963 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "expect.js": "~0.2.0", "derby-i18n": "git://github.com/switz/derby-i18n#master", "relative-date": "~1.1.1", - "lodash": "~1.2.1" + "lodash": "~1.2.1", + "async": "~0.2.9", + "optimist": "~0.5.2" }, "private": true, "subdomain": "habitrpg", From d25c9d24c0d554f887c73d9f63880d7ff7b2d340 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 15 Jun 2013 17:31:31 -0400 Subject: [PATCH 145/157] groups-preennig typo --- migrations/preen_cron.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/preen_cron.js b/migrations/preen_cron.js index fea1ef5e4f..4d141d3ed6 100644 --- a/migrations/preen_cron.js +++ b/migrations/preen_cron.js @@ -36,7 +36,7 @@ db.users.find({ /** * Remove empty parties */ -db.users.groups.remove({ +db.groups.remove({ $where: function(){ return this.type === 'party' && this.members.length === 0; } }); From b7d5c0808bee86deb755b929c31bc0dd55f639a2 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 15 Jun 2013 18:17:08 -0400 Subject: [PATCH 146/157] delete the "migrate" script for now. can dig up this commit later, the mongoskin async integration wasn't working for our purposes --- migrate.coffee | 13 ---- migrations/preen_cron.coffee | 120 ----------------------------------- 2 files changed, 133 deletions(-) delete mode 100644 migrate.coffee delete mode 100644 migrations/preen_cron.coffee diff --git a/migrate.coffee b/migrate.coffee deleted file mode 100644 index cfa1d42c98..0000000000 --- a/migrate.coffee +++ /dev/null @@ -1,13 +0,0 @@ -mongo = require 'mongoskin' -argv = require('optimist').argv -conf = require 'nconf' -conf.argv().env().file({ file: __dirname + "/config.json" }) - -migration = argv.f -throw 'Please specify migration file' unless migration - -db = mongo.db(conf.get("NODE_DB_URI") + '?auto_reconnect', {safe:true}) -require("./migrations/#{migration}") db, -> - console.log 'all done' - db.close() - process.exit() \ No newline at end of file diff --git a/migrations/preen_cron.coffee b/migrations/preen_cron.coffee deleted file mode 100644 index a250fa20f9..0000000000 --- a/migrations/preen_cron.coffee +++ /dev/null @@ -1,120 +0,0 @@ -### - Set this up as a midnight cron script - coffee migrate.coffee -f "preen_cron" -### - -_ = require('lodash') -moment = require('moment') -async = require('async') -today = +new Date -minHistLen = 7 - -module.exports = (db, allMigrationsComplete) -> - - doneCounter = 0 - oneMigrationDown = (err, results) -> - throw err if err - console.log 'One migration down' - allMigrationsComplete() if ++doneCounter is 3 - - users = db.collection('users') - - ### - Remove empty parties - ### - db.collection('groups').remove - $where: -> @type is 'party' and @members.length is 0 - , oneMigrationDown - - ### - Users are allowed to experiment with the site before registering. Every time a new browser visits habitrpg, a new - "staged" account is created - and if the user later registers, that staged account is considered a "production" account. - This function removes all staged accounts that have been abandoned - either older than a month, or corrupted in - some way (lastCron==undefined) - ### - $statingUsersQ = async.queue (user, done) -> - lastCron = user.lastCron; - if lastCron && moment(lastCron).isValid() - if Math.abs(moment(today).diff(lastCron, 'days')) > 5 - users.remove {_id: user._id}, done - else done() - else - users.update {_id: user._id}, {$set: {'lastCron': today}}, done - , 1000 - $statingUsersQ.drain = oneMigrationDown #final callback - - users.find({ - # Un-registered users - "auth.local": {$exists: false} - "auth.facebook": {$exists: false} - }).each (err, user) -> - throw err if err - $statingUsersQ.push(user) - - - ### - Preen history for users with > 7 history entries - This takes an infinite array of single day entries [day day day day day...], and turns it into a condensed array - of averages, condensing more the further back in time we go. Eg, 7 entries each for last 7 days; 4 entries for last - 4 weeks; 12 entries for last 12 months; 1 entry per year before that: [day*7 week*4 month*12 year*infinite] - ### - preenHistory = (history) -> - history = _.filter(history, (h) -> !!h) # discard nulls (corrupted somehow) - newHistory = [] - preen = (amount, groupBy) -> - groups = _(history) - .groupBy((h) -> moment(h.date).format(groupBy)) # get date groupings to average against - .sortBy((h,k) -> k) # sort by date - .value() # turn into an array - amount++; # if we want the last 4 weeks, we're going 4 weeks back excluding this week. so +1 to account for exclusion - start = if (groups.length - amount > 0) then groups.length - amount else 0 - groups = groups.slice(start, groups.length - 1) - _.each groups, (group) -> - avg = _.reduce(group, ((mem, obj) -> mem + obj.value), 0) / group.length; - newHistory.push {date: +moment(group[0].date), value: avg} - - preen(50, 'YYYY') # last 50 years - preen(12, 'YYYYMM') # last 12 months - preen(4, 'YYYYww') # last 4 weeks - newHistory = newHistory.concat history.slice(-7) # last 7 days - return newHistory - - $preenHistoryQ = async.queue (user, done) -> - update = {$set:{}} - - _.each user.tasks, (task) -> - if task.history?.length > minHistLen - update['$set']['tasks.' + task.id + '.history'] = preenHistory(task.history) - - if user.history?.exp?.length > minHistLen - update['$set']['history.exp'] = preenHistory(user.history.exp) - if user.history?.todos?.length > minHistLen - update['$set']['history.todos'] = preenHistory(user.history.todos) - - if _.isEmpty(update['$set']) then done() - else users.update {_id: user._id}, update, done - , 1000 - $preenHistoryQ.drain = oneMigrationDown - - users.find({ - # Registered users with some history - $or: [ - { 'auth.local': { $exists: true }}, - { 'auth.facebook': { $exists: true }} - ], - 'history': {$exists: true} - }).each (err, user) -> - throw err if err - $preenHistoryQ.push(user) - -# /** -# * Don't remove missing user auths anymore. This was previously necessary due to data corruption, -# * revisit if needs be -# */ -# /*db.sessions.find().forEach(function(sess){ -# var uid = JSON.parse(sess.session).userId; -# if (!uid || db.users.count({_id:uid}) === 0) { -# db.sessions.remove({_id:sess._id}); -# } -# });*/ - From d4d7c32fa887300e72d7a8a6a11926a71053e6aa Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 15 Jun 2013 18:20:52 -0400 Subject: [PATCH 147/157] simpler empty parties preen --- migrations/preen_cron.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/migrations/preen_cron.js b/migrations/preen_cron.js index 4d141d3ed6..ec1d71bb9c 100644 --- a/migrations/preen_cron.js +++ b/migrations/preen_cron.js @@ -37,7 +37,8 @@ db.users.find({ * Remove empty parties */ db.groups.remove({ - $where: function(){ return this.type === 'party' && this.members.length === 0; } + 'type': 'party', + $where: "return this.members.length === 0" }); /** From 302721456df2e9477d33772f248de1e51b37a8b6 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 15 Jun 2013 19:14:49 -0400 Subject: [PATCH 148/157] fix up some db indexes --- migrations/20130508_fix_duff_party_subscriptions.js | 2 +- migrations/20130615_add_extra_indexes.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 migrations/20130615_add_extra_indexes.js diff --git a/migrations/20130508_fix_duff_party_subscriptions.js b/migrations/20130508_fix_duff_party_subscriptions.js index 8012b472b2..cd218e785b 100644 --- a/migrations/20130508_fix_duff_party_subscriptions.js +++ b/migrations/20130508_fix_duff_party_subscriptions.js @@ -8,7 +8,7 @@ // since our primary subscription will first hit parties now, we *definitely* need an index there -db.parties.ensureIndex( { 'members': 1, 'background': 1} ); +db.parties.ensureIndex( { 'members': 1}, {background: true} ); db.parties.find().forEach(function(party){ diff --git a/migrations/20130615_add_extra_indexes.js b/migrations/20130615_add_extra_indexes.js new file mode 100644 index 0000000000..2673568184 --- /dev/null +++ b/migrations/20130615_add_extra_indexes.js @@ -0,0 +1,4 @@ +db.users.ensureIndex( { _id: 1, apiToken: 1 }, {background: true} ) +db.groups.ensureIndex( { members: 1 }, {background: true} ) +db.groups.ensureIndex( { type: 1 }, {background: true} ) +db.groups.ensureIndex( { type: 1, privacy: 1 }, {background: true} ) \ No newline at end of file From 0529f26323de25e77ededc9d48b77e9b41204609 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 18 Jun 2013 14:52:39 -0400 Subject: [PATCH 149/157] shorter day-period preen_cron --- migrations/preen_cron.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/preen_cron.js b/migrations/preen_cron.js index ec1d71bb9c..51f2491db5 100644 --- a/migrations/preen_cron.js +++ b/migrations/preen_cron.js @@ -25,7 +25,7 @@ db.users.find({ //if (!user) return; var lastCron = user.lastCron; if (lastCron && moment(lastCron).isValid()) { - if (Math.abs(moment(today).diff(lastCron, 'days')) > 5) { + if (Math.abs(moment(today).diff(lastCron, 'days')) > 3) { return db.users.remove({_id: user._id}); } } else { From 7370331b8c7f22873c4187bd9e5f3450db6c4694 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 16 Jun 2013 15:38:53 -0400 Subject: [PATCH 150/157] fix max gear achievement for backer gear --- src/app/unlock.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/unlock.coffee b/src/app/unlock.coffee index 4348aa3d5e..5cf5b1a08c 100644 --- a/src/app/unlock.coffee +++ b/src/app/unlock.coffee @@ -75,9 +75,9 @@ module.exports.app = (appExports, model) -> user.on 'set', 'items.*', (after, before) -> return if user.get('achievements.ultimateGear') items = user.get('items') - if parseInt(items.weapon) == 6 and parseInt(items.armor) == 5 and parseInt(items.head) == 5 and parseInt(items.shield) == 5 - dontPersist = model._dontPersist; model._dontPersist = false - user.set 'achievements.ultimateGear', true, (-> model._dontPersist = dontPersist) + if parseInt(items.weapon) >= 6 and parseInt(items.armor) >= 5 and parseInt(items.head) >= 5 and parseInt(items.shield) >= 5 + [dontPersist, model._dontPersist] = [model._dontPersist; false] + user.set 'achievements.ultimateGear', true, ->model._dontPersist = dontPersist $('#max-gear-achievement-modal').modal('show') user.on 'set', 'tasks.*.streak', (id, after, before) -> From 1c5d61a617cd7cef8d75c8b779e6fd3572d2f2db Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 18 Jun 2013 15:14:01 -0400 Subject: [PATCH 151/157] fix to maxGear achievement --- src/app/unlock.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/unlock.coffee b/src/app/unlock.coffee index 5cf5b1a08c..73527b9d8a 100644 --- a/src/app/unlock.coffee +++ b/src/app/unlock.coffee @@ -76,8 +76,8 @@ module.exports.app = (appExports, model) -> return if user.get('achievements.ultimateGear') items = user.get('items') if parseInt(items.weapon) >= 6 and parseInt(items.armor) >= 5 and parseInt(items.head) >= 5 and parseInt(items.shield) >= 5 - [dontPersist, model._dontPersist] = [model._dontPersist; false] - user.set 'achievements.ultimateGear', true, ->model._dontPersist = dontPersist + dontPersist = model._dontPersist; model._dontPersist = false + user.set 'achievements.ultimateGear', true, (->model._dontPersist = dontPersist) $('#max-gear-achievement-modal').modal('show') user.on 'set', 'tasks.*.streak', (id, after, before) -> From 3461d794a1d1737dba92430cc1c439951f51a985 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 21 Jun 2013 12:19:33 -0400 Subject: [PATCH 152/157] chat: bad fix for the duplicates messages, but safer than before --- src/app/groups.coffee | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/app/groups.coffee b/src/app/groups.coffee index a580abbe5a..f026131fe6 100644 --- a/src/app/groups.coffee +++ b/src/app/groups.coffee @@ -145,19 +145,12 @@ module.exports.app = (appExports, model, app) -> timestamp: +new Date # FIXME - sometimes racer will send many duplicates via chat.unshift. I think because it can't make connection, keeps - # trying, but all attempts go through. Unfortunately we can't do chat.set without potentially clobbering other chatters, - # and we can't make chat an object without using refLists. hack solution for now is to unshift, and if there are dupes - # after we set to unique - chat.unshift message, -> - messages = chat.get() || [] - count = messages.length - messages =_.uniq messages, true, ((m) -> m?.id) # get rid of dupes - #There were a bunch of duplicates, let's clean it up - if messages.length != count - messages.splice(200) - chat.set messages - else - chat.remove(200) + # trying, but all attempts go through. Unfortunately we can't do chat.set without potentially clobbering other chatters + messages = chat.get() or [] + messages.unshift(message) + messages.splice(200) + chat.set messages + type = $(el).attr('data-type') model.set '_user.party.lastMessageSeen', chat.get()[0].id if group.get('type') is 'party' From 7af9553aefea0cca89a6239bdac1576b731628ed Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 6 Jul 2013 17:18:18 -0400 Subject: [PATCH 153/157] remove tour (and sortable, stickyheader, tooltips) from mobile devices #995 --- src/app/browser.coffee | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/browser.coffee b/src/app/browser.coffee index 44dcedb810..bf7e214dfb 100644 --- a/src/app/browser.coffee +++ b/src/app/browser.coffee @@ -222,10 +222,12 @@ module.exports.app = (appExports, model, app) -> app.on 'render', (ctx) -> #restoreRefs(model) - setupSortable(model) - setupTooltips(model) - setupTour(model) - initStickyHeader(model) unless model.get('_mobileDevice') + unless model.get('_mobileDevice') + setupTooltips(model) + initStickyHeader(model) + setupSortable(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 8e922bb1515ffc5cbee9cd4b2ef908bca6b8a2a0 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 12 Jul 2013 17:15:11 -0400 Subject: [PATCH 154/157] API: add facebook auth route --- src/server/api.coffee | 25 +++++++++++++++++++++++-- test/api.mocha.coffee | 24 +++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 97c8892b00..ca2807a73b 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -94,9 +94,9 @@ router.put '/user', auth, (req, res) -> res.json 201, userObj ### - POST /user/auth + POST /user/auth/local ### -router.post '/user/auth', (req, res) -> +router.post '/user/auth/local', (req, res) -> username = req.body.username password = req.body.password return res.json 401, err: 'No username or password' unless username and password @@ -122,6 +122,27 @@ router.post '/user/auth', (req, res) -> id: u2.id token: u2.apiToken +### + POST /user/auth/facebook +### +router.post '/user/auth/facebook', (req, res) -> + {facebook_id, email, name} = req.body + return res.json 401, err: 'No facebook id provided' unless facebook_id + model = req.getModel() + q = model.query("users").withProvider('facebook', facebook_id) + q.fetch (err, result) -> + return res.json 401, { err } if err + u = result.get() + console.log {facebook_id, u} + if u + return res.json + id: u.id + token: u.apiToken + else + # FIXME: create a new user instead + return res.json 403, err: "Please register with Facebook on https://habitrpg.com, then come back here and log in." + + ### GET /user/task/:id ### diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index d523be7b16..0b10eb7a93 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -399,7 +399,7 @@ describe 'API', -> done() - it 'POST /api/v1/user/auth', (done) -> + it 'POST /api/v1/user/auth/local', (done) -> userAuth = username: username password: 'icculus' @@ -412,3 +412,25 @@ describe 'API', -> expect(res.body.id).to.be currentUser.id expect(res.body.token).to.be currentUser.apiToken done() + + it 'POST /api/v1/user/auth/facebook', (done) -> + id = model.id() + userAuth = facebook_id: 12345, name: 'Tyler Renelle', email: 'x@y.com' + newUser = helpers.newUser(true) + newUser.id = id + newUser.auth = facebook: + id: userAuth.facebook_id + name: userAuth.name + email: userAuth.email + console.log {newUser} + model.set "users.#{id}", newUser, -> + + request.post("#{baseURL}/user/auth/facebook") + .set('Accept', 'application/json') + .send(userAuth) + .end (res) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + expect(res.body.id).to.be newUser.id + #expect(res.body.token).to.be newUser.apiToken + done() From 2ec5a930a65491fbb2f3c98f7b76489890b0fe3c Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 15 Jul 2013 21:52:00 -0400 Subject: [PATCH 155/157] add beta redirect to main site until we get a new beta up --- src/server/middleware.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/server/middleware.coffee b/src/server/middleware.coffee index fa2ca2aef5..fddcef1779 100644 --- a/src/server/middleware.coffee +++ b/src/server/middleware.coffee @@ -1,4 +1,8 @@ splash = (req, res, next) -> + + return res.redirect("https://habitrpg.com") + + isStatic = req.url.split('/')[1] is 'static' unless req.query?.play? or req.getModel().get('_userId') or isStatic res.redirect('/static/front') From 861c7a1aa1d5c4f05ec2cdc5300c6a6729479ffe Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 15 Jul 2013 23:07:58 -0400 Subject: [PATCH 156/157] Revert "add beta redirect to main site until we get a new beta up" This reverts commit 2ec5a930a65491fbb2f3c98f7b76489890b0fe3c. --- src/server/middleware.coffee | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/server/middleware.coffee b/src/server/middleware.coffee index fddcef1779..fa2ca2aef5 100644 --- a/src/server/middleware.coffee +++ b/src/server/middleware.coffee @@ -1,8 +1,4 @@ splash = (req, res, next) -> - - return res.redirect("https://habitrpg.com") - - isStatic = req.url.split('/')[1] is 'static' unless req.query?.play? or req.getModel().get('_userId') or isStatic res.redirect('/static/front') From 5017be09848a5e473103c012967c68133103b7a6 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 16 Jul 2013 18:29:09 -0400 Subject: [PATCH 157/157] API: remove all private paths from server ops, they're unecessary and might be causing issues --- src/server/api.coffee | 64 ++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index ca2807a73b..48c812dbb2 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -7,12 +7,16 @@ helpers = require 'habitrpg-shared/script/helpers' validator = require 'derby-auth/node_modules/validator' check = validator.check sanitize = validator.sanitize -misc = require '../app/misc' utils = require 'derby-auth/utils' NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request" NO_USER_FOUND = err: "No user found." +addTask = (user, task) -> + task.type ?= 'habit' + tid = user.add "tasks", task + user.push "#{task.type}Ids", tid + # ---------- /api/v1 API ------------ # Every url added beneath router is prefaced by /api/v1 @@ -43,7 +47,7 @@ auth = (req, res, next) -> return res.json err: err if err req.user = user req.userObj = user.get() - return res.json 401, NO_USER_FOUND if !req.userObj || _.isEmpty(req.userObj) + return res.json 401, NO_USER_FOUND if _.isEmpty(req.userObj) req._isServer = true next() @@ -214,17 +218,17 @@ updateTasks = (tasks, user, model) -> if task.id if task.del user.del "tasks.#{task.id}" - if task.type # TODO we should enforce they pass in type, so we can properly remove from idList - i = model.get("_#{task.type}List").indexOf(task.id) - model.remove("_#{task.type}List", i, 1) # doens't work when task.type isn't passed up + + # Delete from id list, only if type is passed up + # TODO we should enforce they pass in type, so we can properly remove from idList + if task.type and ~(i = user.get("#{task.type}Ids").indexOf task.id) + user.remove("#{task.type}Ids", i, 1) + task = deleted: true else user.set "tasks.#{task.id}", task else - type = task.type || 'habit' - model.ref '_user', user - model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" - model.at("_#{type}List").push task + addTask(user, task) tasks[idx] = task return tasks @@ -238,31 +242,19 @@ router.post '/user/tasks', auth, (req, res) -> ### router.post '/user/task', auth, validateTask, (req, res) -> task = req.task - type = task.type - - model = req.getModel() - model.ref '_user', req.user - model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" - model.at("_#{type}List").push task - + addTask req.user, task res.json 201, task ### GET /user/tasks ### router.get '/user/tasks', auth, (req, res) -> - user = req.userObj - return res.json 400, NO_USER_FOUND if !user || _.isEmpty(user) + return res.json 400, NO_USER_FOUND if _.isEmpty(req.userObj) - model = req.getModel() - model.ref '_user', req.user - tasks = [] - 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") + types = + if /^(habit|todo|daily|reward)$/.test(req.query.type) then [req.query.type] + else ['habit','todo','daily','reward'] + tasks = _.toArray (_.filter req.user.get('tasks'), (t)-> t.type in types) res.json 200, tasks @@ -281,9 +273,7 @@ scoreTask = (req, res, next) -> model = req.getModel() {user, userObj} = req - model.ref('_user', user) - - existingTask = model.at "_user.tasks.#{taskId}" + existingTask = user.at "tasks.#{taskId}" # TODO add service & icon to task # If task exists, set it's compltion if existingTask.get() @@ -304,12 +294,16 @@ scoreTask = (req, res, next) -> when 'daily', 'todo' task.completed = direction is 'up' - model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" - model.at("_#{type}List").push task + addTask user, task - #FIXME - delta = misc.score(model, taskId, direction) - result = model.get '_user.stats' + # TODO - could modify batchTxn to conform to this better + uObj = req.user.get() + tObj = uObj.tasks[taskId] + paths = {} + delta = algos.score(uObj, tObj, direction, {paths}) + _.each paths, (v,k) -> user.set(k,helpers.dotGet(k, uObj));true + + result = uObj.stats result.delta = delta res.json result
    - {.text}
    {.value} Gems +

    {.text}
    {.value} Gems

    - {.text}
    {.value} Gems +

    {.text}
    {.value} Gems