diff --git a/migrations/20130518_setup_groups.js b/migrations/20130518_setup_groups.js new file mode 100644 index 0000000000..0e04bfa1b9 --- /dev/null +++ b/migrations/20130518_setup_groups.js @@ -0,0 +1,48 @@ +/** + * 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/lodash/lodash.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',true); +//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}); + +//migrate invitation mechanisms +db.users.update( + {}, + { + $remove:{party:1}, + $set:{invitations:{party:null,guilds:[]}} + }, + {multi:1} +); + +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/migrations/20130602_survey_rewards.js b/migrations/20130602_survey_rewards.js new file mode 100644 index 0000000000..5f883f7ad1 --- /dev/null +++ b/migrations/20130602_survey_rewards.js @@ -0,0 +1,25 @@ +//mongo habitrpg ./node_modules/lodash/lodash.js migrations/20130602_survey_rewards.js + +var members = [] +members = _.uniq(members); + +var query = { + _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}} + ] +}; + +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 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 diff --git a/src/app/challenges.coffee b/src/app/challenges.coffee new file mode 100644 index 0000000000..5d61133a7c --- /dev/null +++ b/src/app/challenges.coffee @@ -0,0 +1,83 @@ +_ = require 'lodash' +helpers = require 'habitrpg-shared/script/helpers' + +module.exports.app = (appExports, model) -> + browser = require './browser' + user = model.at '_user' + + $('#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 + 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')] + model.set '_challenge.new', + name: '' + habits: [] + dailys: [] + todos: [] + rewards: [] + id: model.id() + uid: user.get('id') + user: helpers.username(model.get('_user.auth'), model.get('_user.profile.name')) + group: {type, id:gid} + timestamp: +new Date + + appExports.challengeSave = -> + gid = model.get('_challenge.new.group.id') + model.unshift "groups.#{gid}.challenges", model.get('_challenge.new'), -> + 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) -> + chal = e.get() + + # 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, challenge: true} + + 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) -> + task.tags = tags + task.challenge = chal.id + task.group = {id: chal.group.id, type: chal.group.type} + model.push("_#{type}List", task) + true + + appExports.challengeUnsubscribe = (e) -> + 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", _.findIndex(model.get("_#{type}List",{id:task.id})) + model.del "_user.tasks.#{task.id}" + true diff --git a/src/app/groups.coffee b/src/app/groups.coffee new file mode 100644 index 0000000000..a580abbe5a --- /dev/null +++ b/src/app/groups.coffee @@ -0,0 +1,185 @@ +_ = require('lodash') +helpers = require('habitrpg-shared/script/helpers') + +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 + + user = model.at('_user') + + appExports.groupCreate = (e,el) -> + type = $(el).attr('data-type') + newGroup = + name: model.get("_new.group.name") + description: model.get("_new.group.description") + leader: user.get('id') + members: [user.get('id')] + type: type + + # parties - free + if type is 'party' + return model.add 'groups', newGroup, ->location.reload() + + # guilds - 4G + 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.incr 'balance', -1, ->location.reload() + + appExports.toggleGroupEdit = (e, el) -> + 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') + model.del '_newGroupWebsite' + + appExports.groupInvite = (e,el) -> + uid = model.get('_groupInvitee').replace(/[\s"]/g, '') + model.set '_groupInvitee', '' + return if _.isEmpty(uid) + + model.query('users').publicInfo([uid]).fetch (err, profiles) -> + throw err if err + 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 + group = e.get(); groups = g.get() + {type, name} = group; gid = group.id + groupError = (msg) -> model.set("_groupError", msg) + invite = -> + $.bootstrapGrowl "Invitation Sent." + 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() + + switch type + when 'guild' + 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 + return groupError("User already pending invitation.") + else if _.find(groups, {type:'party'}) + return groupError("User already in a party.") + else invite() + + joinGroup = (gid) -> + model.push("groups.#{gid}.members", user.get('id'), ->location.reload()) + + appExports.joinGroup = (e, el) -> joinGroup e.get('id') + + appExports.acceptInvitation = (e,el) -> + gid = e.get('id') + if $(el).attr('data-type') is 'party' + user.set 'invitations.party', null, ->joinGroup(gid) + else + e.at().remove ->joinGroup(gid) + + 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) -> + 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 + ### + + 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 + + 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', '') + + message = + id: model.id() + uuid: user.get('id') + contributor: user.get('backer.contributor') + npc: user.get('backer.npc') + text: text + user: helpers.username(model.get('_user.auth'), model.get('_user.profile.name')) + 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) + type = $(el).attr('data-type') + model.set '_user.party.lastMessageSeen', chat.get()[0].id if group.get('type') is 'party' + + appExports.chatKeyup = (e, el, next) -> + return next() unless e.keyCode is 13 + appExports.sendChat(e, el) + + appExports.deleteChatMessage = (e) -> + if confirm("Delete chat message?") is true + e.at().remove() #requires the {#with} + + app.on 'render', (ctx) -> + $('#party-tab-link').on 'shown', (e) -> + messages = model.get('_party.chat') + return false unless messages?.length > 0 + model.set '_user.party.lastMessageSeen', messages[0].id + + 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/src/app/index.coffee b/src/app/index.coffee index 69393f1e0d..89cd993836 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -30,12 +30,18 @@ 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 - partyQ = model.query('parties').withMember(uuid) - partyQ.fetch (err, party) -> + # 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 - finished = (descriptors, paths) -> + # Add public "Tavern" guild in + descriptors.unshift('groups.habitrpg'); paths.unshift('_habitRPG') + + # Subscribe to each descriptor model.subscribe.apply model, descriptors.concat -> [err, refs] = [arguments[0], arguments] return next(err) if err @@ -45,20 +51,40 @@ setupSubscriptions = (page, model, params, next, cb) -> return page.redirect('/logout') #delete model.session.userId return cb() - # (1) Solo player - return finished([selfQ, 'tavern'], ['_user', '_tavern']) unless party.get() + # 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 + 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 - ## (2) Party has members, subscribe to those users too - if m = party.get('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. - model.query('users').party(m).fetch (err, members) -> - return next(err) if err - model.ref '_partyMembers', members - return finished([partyQ, selfQ, 'tavern'], ['_party', '_user', '_tavern']) - else # 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']) + 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 ========== @@ -78,15 +104,13 @@ get '/', (page, model, params, next) -> # ========== CONTROLLER FUNCTIONS ========== ready (model) -> - 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('./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) @@ -94,6 +118,14 @@ ready (model) -> browser.app(exports, model, app) require('./unlock').app(exports, 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 @@ -102,7 +134,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) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index e1662ac04a..ce14ca9f2a 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -18,9 +18,24 @@ 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 +### + 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 @@ -29,8 +44,8 @@ module.exports.batchTxn = batchTxn = (model, cb, options) -> 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) -> + drop = undefined + delta = batchTxn model, (uObj, paths) -> tObj = uObj.tasks[taskId] # Stuff for undo @@ -44,10 +59,32 @@ 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 + drop = uObj._tmp?.drop + + # 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 + , done:-> + if drop and $? + model.set '_drop', drop $('#item-dropped-modal').modal 'show' - delta + + delta ### Make sure model.get() returns all properties, see https://github.com/codeparty/racer/issues/116 @@ -76,14 +113,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) - console.log {uniqPets, count:_.size(uniqPets)} + + 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) -> @@ -91,7 +129,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 @@ -128,12 +166,14 @@ module.exports.viewHelpers = (view) -> view.fn 'int', get: (num) -> num set: (num) -> [parseInt(num)] + view.fn 'indexedPath', indexedPath + #iCal 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 @@ -162,3 +202,15 @@ module.exports.viewHelpers = (view) -> #Tags view.fn 'noTags', helpers.noTags view.fn 'appliedTags', helpers.appliedTags + + #Challenges + view.fn 'taskInChallenge', (task) -> + taskInChallenge.call(@,task)?.get() + view.fn 'taskAttrFromChallenge', (task, attr) -> + 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/src/app/party.coffee b/src/app/party.coffee deleted file mode 100644 index 6823f212cc..0000000000 --- a/src/app/party.coffee +++ /dev/null @@ -1,147 +0,0 @@ -_ = require('lodash') -helpers = require('habitrpg-shared/script/helpers') - -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 - - user = model.at('_user') - - model.on 'set', '_user.party.invitation', (after, before) -> - if !before? and after? # they just got invited - partyQ = model.query('parties').withId(after) - partyQ.fetch (err, party) -> - return next(err) if err - model.ref '_party', party - browser.resetDom(model) - - 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}, -> - 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) -> - 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 - $.bootstrapGrowl "Invitation Sent." - model.set "users.#{id}.party.invitation", model.get('_party.id'), -> window.location.reload() - #model.set '_newPartyMember', '' - #partySubscribe model - - appExports.partyAccept = -> - 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) -> -# 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') - - 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') - 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) - else - window.location.reload true - - ### - Chat Functionality - ### - - sendChat = (path, input) -> - chat = model.at path - text = model.get input - # Check for non-whitespace characters - return unless /\S/.test text - model.set(input, '') - - message = - id: model.id() - uuid: user.get('id') - contributor: user.get('backer.contributor') - npc: user.get('backer.npc') - text: text - user: helpers.username(model.get('_user.auth'), model.get('_user.profile.name')) - 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) - - model.on 'unshift', '_party.chat', -> $('.chat-message').tooltip() - model.on 'unshift', '_tavern.chat.messages', -> $('.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') - - appExports.partyMessageKeyup = (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.deleteChatMessage = (e) -> - if confirm("Delete chat message?") is true - e.at().remove() #requires the {#with} - - app.on 'render', (ctx) -> - $('#party-tab-link').on 'shown', (e) -> - messages = model.get('_party.chat') - return false unless messages?.length > 0 - model.set '_user.party.lastMessageSeen', messages[0].id - - appExports.gotoPartyChat = -> - model.set '_gamePane', true, -> - $('#party-tab-link').tab('show') 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.' diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index 6d95a4e762..d3e644487c 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -18,8 +18,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, text, notes: '', value: 0} + newTask.tags = _.reduce user.get('filters'), ((memo,v,k) -> memo[k]=v if v; memo), {} + switch type when 'habit' newTask = _.defaults {up: true, down: true}, newTask @@ -29,7 +30,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) -> @@ -74,31 +75,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/src/server/store.coffee b/src/server/store.coffee index 7c2d5cb3fe..4c81c2775c 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) ### @@ -43,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) @@ -84,63 +89,63 @@ 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', - 'party', + 'invitations', 'profile', 'achievements', 'backer', '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 + ### + Read / Write groups, so they can create new groups + ### + store.readPathAccess "groups.*", publicAccess + store.writeAccess "*", "groups.*", publicAccess - store.query.expose "parties", "withId", (id) -> - @where("id").equals(id).findOne() + ### + Public HabitRPG Guild + ### + store.readPathAccess 'groups.habitrpg', publicAccess + store.writeAccess "*", "groups.habitrpg.chat.*", publicAccess + store.writeAccess "*", "groups.habitrpg.challenges.*", 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, 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 - store.readPathAccess "parties.*", -> - accept = arguments[arguments.length-2] - accept(true) - - 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.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) + ### + Public Groups Info + ### + store.query.expose "groups", "publicGroups", -> + @where('privacy').equals('public') + .where('type').equals('guild') + .only(['id', 'type', 'name', 'description', 'members' , 'privacy']) + store.queryAccess "groups", "publicGroups", 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 \ No newline at end of file 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/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 diff --git a/styles/app/index.styl b/styles/app/index.styl index c9128a3dc7..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 @@ -165,4 +166,5 @@ hr background-color #dfe9ea padding 1px 3px 1px 3px - +.nav li > a + cursor: pointer 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/alerts.html b/views/app/alerts.html index bd2de15273..e5be824953 100644 --- a/views/app/alerts.html +++ b/views/app/alerts.html @@ -14,6 +14,11 @@

+

6/03/2013

+ +

5/27/2013