From 8d37309de18d5d4ee370b8380389cdb6900b69ff Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 30 Sep 2014 19:32:11 -0700 Subject: [PATCH] Several fixes for achievement recalculation. --- app/schemas/models/achievement.coffee | 2 +- app/templates/editor/achievement/edit.jade | 1 + .../achievement/AchievementEditView.coffee | 12 ++- server/achievements/Achievement.coffee | 2 - .../achievements/achievement_handler.coffee | 2 +- .../earned_achievement_handler.coffee | 73 ++++++++++++------- 6 files changed, 60 insertions(+), 32 deletions(-) diff --git a/app/schemas/models/achievement.coffee b/app/schemas/models/achievement.coffee index d364c0277..e8f64c7a3 100644 --- a/app/schemas/models/achievement.coffee +++ b/app/schemas/models/achievement.coffee @@ -61,7 +61,7 @@ _.extend AchievementSchema.properties, description: 'For repeatables only. Denotes the field a repeatable achievement needs for its calculations' recalculable: type: 'boolean' - description: 'Needs to be set to true before it is eligible for recalculation.' + description: 'Deprecated: all achievements must be recalculable now. Used to need to be set to true before it is eligible for recalculation.' function: type: 'object' description: 'Function that gives total experience for X amount achieved' diff --git a/app/templates/editor/achievement/edit.jade b/app/templates/editor/achievement/edit.jade index a7058b835..8e811b30c 100644 --- a/app/templates/editor/achievement/edit.jade +++ b/app/templates/editor/achievement/edit.jade @@ -10,6 +10,7 @@ block content li.active | #{achievement.attributes.name} + button.achievement-tool-button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-all-button Recalculate All button.achievement-tool-button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-button Recalculate button.achievement-tool-button(data-i18n="common.delete", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#delete-button Delete button.achievement-tool-button(data-i18n="common.save", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#save-button Save diff --git a/app/views/editor/achievement/AchievementEditView.coffee b/app/views/editor/achievement/AchievementEditView.coffee index fbe1a60f1..49115c7f0 100644 --- a/app/views/editor/achievement/AchievementEditView.coffee +++ b/app/views/editor/achievement/AchievementEditView.coffee @@ -14,6 +14,7 @@ module.exports = class AchievementEditView extends RootView events: 'click #save-button': 'saveAchievement' 'click #recalculate-button': 'confirmRecalculation' + 'click #recalculate-all-button': 'confirmAllRecalculation' 'click #delete-button': 'confirmDeletion' constructor: (options, @achievementID) -> @@ -82,17 +83,21 @@ module.exports = class AchievementEditView extends RootView url = "/editor/achievement/#{@achievement.get('slug') or @achievement.id}" document.location.href = url - confirmRecalculation: -> + confirmRecalculation: (e, all=false) -> renderData = 'confirmTitle': 'Are you really sure?' - 'confirmBody': 'This will trigger recalculation of the achievement for all users. Are you really sure you want to go down this path?' + 'confirmBody': "This will trigger recalculation of #{if all then 'all achievements' else 'the achievement'} for all users. Are you really sure you want to go down this path?" 'confirmDecline': 'Not really' 'confirmConfirm': 'Definitely' confirmModal = new ConfirmModal renderData confirmModal.on 'confirm', @recalculateAchievement + @recalculatingAll = all @openModalView confirmModal + confirmAllRecalculation: (e) -> + @confirmRecalculation e, true + confirmDeletion: -> renderData = 'confirmTitle': 'Are you really sure?' @@ -105,8 +110,9 @@ module.exports = class AchievementEditView extends RootView @openModalView confirmModal recalculateAchievement: => + data = if @recalculatingAll then {} else {achievements: [@achievement.get('slug') or @achievement.get('_id')]} $.ajax - data: JSON.stringify(earnedAchievements: [@achievement.get('slug') or @achievement.get('_id')]) + data: JSON.stringify data success: (data, status, jqXHR) -> noty timeout: 5000 diff --git a/server/achievements/Achievement.coffee b/server/achievements/Achievement.coffee index 649bc4527..73d0b69ac 100644 --- a/server/achievements/Achievement.coffee +++ b/server/achievements/Achievement.coffee @@ -30,8 +30,6 @@ AchievementSchema.methods.getExpFunction = -> TreemaUtils.populateDefaults(func, jsonschema.properties.function) return utils.functionCreators[func.kind](func.parameters) if func.kind of utils.functionCreators -AchievementSchema.methods.isRecalculable = -> @get('recalculable') isnt false - AchievementSchema.statics.jsonschema = jsonschema AchievementSchema.statics.earnedAchievements = {} diff --git a/server/achievements/achievement_handler.coffee b/server/achievements/achievement_handler.coffee index 7aed663ea..d9256cb1a 100644 --- a/server/achievements/achievement_handler.coffee +++ b/server/achievements/achievement_handler.coffee @@ -5,7 +5,7 @@ class AchievementHandler extends Handler modelClass: Achievement # Used to determine which properties requests may edit - editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function', 'related', 'difficulty', 'category', 'recalculable', 'rewards'] + editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function', 'related', 'difficulty', 'category', 'rewards'] allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] jsonSchema = require '../../app/schemas/models/achievement.coffee' diff --git a/server/achievements/earned_achievement_handler.coffee b/server/achievements/earned_achievement_handler.coffee index b61e05b1e..144d01266 100644 --- a/server/achievements/earned_achievement_handler.coffee +++ b/server/achievements/earned_achievement_handler.coffee @@ -16,8 +16,9 @@ class EarnedAchievementHandler extends Handler recalculate: (req, res) -> onSuccess = (data) => log.debug 'Finished recalculating achievements' + console.log 'req.body.achievements is', req.body.achievements if 'achievements' of req.body # Support both slugs and IDs separated by commas - achievementSlugsOrIDs = req.body.earnedAchievements + achievementSlugsOrIDs = req.body.achievements EarnedAchievementHandler.recalculate achievementSlugsOrIDs, onSuccess else EarnedAchievementHandler.recalculate onSuccess @@ -27,8 +28,10 @@ class EarnedAchievementHandler extends Handler if _.isArray callbackOrSlugsOrIDs # slugs or ids achievementSlugs = (thing for thing in callbackOrSlugsOrIDs when not Handler.isID(thing)) achievementIDs = (thing for thing in callbackOrSlugsOrIDs when Handler.isID(thing)) + recalculatingAll = false else # just a callback callback = callbackOrSlugsOrIDs + recalculatingAll = true t0 = new Date().getTime() total = 100000 User.count {anonymous:false}, (err, count) -> total = count @@ -52,7 +55,7 @@ class EarnedAchievementHandler extends Handler log.info "Recalculating a total of #{achievements.length} achievements..." # Fetch every single user. This tends to get big so do it in a streaming fashion. - userStream = User.find().sort('_id').stream() + userStream = User.find(slug: 'nick').sort('_id').stream() streamFinished = false usersTotal = 0 usersFinished = 0 @@ -94,8 +97,6 @@ class EarnedAchievementHandler extends Handler newTotalRewards = heroes: [], items: [], levels: [], gems: 0 async.each achievements, ((achievement, doneWithAchievement) -> - return doneWithAchievement() unless achievement.isRecalculable() - isRepeatable = achievement.get('proportionalTo')? model = mongoose.modelNameByCollection(achievement.get('collection')) return doneWithAchievement new Error "Model with collection '#{achievement.get 'collection'}' doesn't exist." unless model? @@ -137,36 +138,58 @@ class EarnedAchievementHandler extends Handler doneWithAchievement err ), (err) -> # Wrap up a user, save points log.error err if err - # Since some achievements cannot be recalculated it's important to deduct the old amount of exp - # and add the new amount, instead of just setting to the new amount - #console.log 'User', user.get('name'), 'had newTotalPoints', newTotalPoints, 'and newTotalRewards', newTotalRewards + #console.log 'User', user.get('name'), 'had newTotalPoints', newTotalPoints, 'and newTotalRewards', newTotalRewards, 'previousRewards', previousRewards return doneWithUser(user) unless newTotalPoints or newTotalRewards.gems or _.some(newTotalRewards, (r) -> r.length) # log.debug "Matched a total of #{newTotalPoints} new points" # log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}" + pointDelta = newTotalPoints - previousPoints pctDone = (100 * usersFinished / total).toFixed(2) - console.log "Updated points to #{newTotalPoints}(+#{newTotalPoints - previousPoints}) for #{user.get('name') or '???'} (#{user.get('_id')}) (#{pctDone}%)" - update = {$inc: {points: newTotalPoints - previousPoints}} + console.log "Updated points to #{newTotalPoints} (#{if pointDelta < 0 then '' else '+'}#{pointDelta}) for #{user.get('name') or '???'} (#{user.get('_id')}) (#{pctDone}%)" + if recalculatingAll + update = {$set: {points: newTotalPoints, 'earned.gems': 0, 'earned.heroes': [], 'earned.items': [], 'earned.levels': []}} + else + update = {$inc: {points: pointDelta}} + secondUpdate = {} # In case we need to pull, then push. for rewardType, rewards of newTotalRewards + updateKey = "earned.#{rewardType}" if rewardType is 'gems' - update.$inc['earned.gems'] = rewards - previousRewards.gems + if recalculatingAll + update.$set[updateKey] = rewards + else + update.$inc[updateKey] = rewards - previousRewards.gems else - previousCounts = _.countBy previousRewards[rewardType] - newCounts = _.countBy rewards - relevantRewards = _.union _.keys(previousCounts), _.keys(newCounts) - for reward in relevantRewards - [previousCount, newCount] = [previousCounts[reward], newCounts[reward]] - if newCount and not previousCount - update.$addToSet ?= {} - update.$addToSet["earned.#{rewardType}"] ?= {$each: []} - update.$addToSet["earned.#{rewardType}"].$each.push reward - else if previousCount and not newCount - # Might $pull $each also work here? - update.$pullAll ?= {} - update.$pullAll["earned.#{rewardType}"] ?= [] - update.$pullAll["earned.#{rewardType}"].push reward + if recalculatingAll + update.$set[updateKey] = _.uniq rewards + else + previousCounts = _.countBy previousRewards[rewardType] + newCounts = _.countBy rewards + relevantRewards = _.union _.keys(previousCounts), _.keys(newCounts) + for reward in relevantRewards + [previousCount, newCount] = [previousCounts[reward], newCounts[reward]] + if newCount and not previousCount + update.$addToSet ?= {} + update.$addToSet[updateKey] ?= {$each: []} + update.$addToSet[updateKey].$each.push reward + else if previousCount and not newCount + # Might $pull $each also work here? + update.$pullAll ?= {} + update.$pullAll[updateKey] ?= [] + update.$pullAll[updateKey].push reward + if update.$addToSet?[updateKey] and update.$pullAll?[updateKey] + # Perform the update in two calls to avoid "MongoError: Cannot update 'earned.levels' and 'earned.levels' at the same time" + secondUpdate.$addToSet ?= {} + secondUpdate.$addToSet[updateKey] = update.$addToSet[updateKey] + delete update.$addToSet[updateKey] + delete update.$addToSet unless _.size update.$addToSet + #console.log 'recalculatingAll?', recalculatingAll, 'so update is', update, 'secondUpdate', secondUpdate User.update {_id: userID}, update, {}, (err) -> log.error err if err? - doneWithUser(user) + if _.size secondUpdate + User.update {_id: userID}, secondUpdate, {}, (err) -> + log.error err if err? + doneWithUser user + else + doneWithUser user module.exports = new EarnedAchievementHandler()