log = require 'winston' mongoose = require 'mongoose' async = require 'async' Achievement = require './Achievement' EarnedAchievement = require './EarnedAchievement' User = require '../users/User' Handler = require '../commons/Handler' LocalMongo = require '../../app/lib/LocalMongo' class EarnedAchievementHandler extends Handler modelClass: EarnedAchievement # Don't allow POSTs or anything yet hasAccess: (req) -> req.method is 'GET' # or req.user.isAdmin() recalculate: (req, res) -> onSuccess = (data) => log.debug "Finished recalculating achievements" if 'achievements' of req.body # Support both slugs and IDs separated by commas achievementSlugsOrIDs = req.body.achievements EarnedAchievementHandler.recalculate achievementSlugsOrIDs, onSuccess else EarnedAchievementHandler.recalculate onSuccess @sendSuccess res, {} # Returns success: boolean # TODO call onFinished @recalculate: (callbackOrSlugsOrIDs, onFinished) -> if _.isArray callbackOrSlugsOrIDs achievementSlugs = (thing for thing in callbackOrSlugsOrIDs when not Handler.isID(thing)) achievementIDs = (thing for thing in callbackOrSlugsOrIDs when Handler.isID(thing)) else onFinished = callbackOrSlugsOrIDs filter = {} filter.$or = [ {_id: $in: achievementIDs}, {slug: $in: achievementSlugs} ] if achievementSlugs? or achievementIDs? # Fetch all relevant achievements Achievement.find filter, (err, achievements) -> return log.error err if err? # Fetch every single user User.find {}, (err, users) -> _.each users, (user) -> # Keep track of a user's already achieved in order to set the notified values correctly userID = user.get('_id').toHexString() # Fetch all of a user's earned achievements EarnedAchievement.find {user: userID}, (err, alreadyEarned) -> alreadyEarnedIDs = [] previousPoints = 0 _.each alreadyEarned, (earned) -> if (_.find achievements, (single) -> earned.get('achievement') is single.get('_id').toHexString()) alreadyEarnedIDs.push earned.get('achievement') previousPoints += earned.get 'earnedPoints' # TODO maybe also delete earned? Make sure you don't delete too many newTotalPoints = 0 earnedAchievementSaverGenerator = (achievement) -> (callback) -> isRepeatable = achievement.get('proportionalTo')? model = mongoose.model(achievement.get('collection')) if not model? log.error "Model #{achievement.get 'collection'} doesn't even exist." return callback() model.findOne achievement.query, (err, something) -> return callback() unless something log.debug "Matched an achievement: #{achievement.get 'name'}" earned = user: userID achievement: achievement._id.toHexString() achievementName: achievement.get 'name' notified: achievement._id in alreadyEarnedIDs if isRepeatable earned.achievedAmount = something.get(achievement.get 'proportionalTo') earned.previouslyAchievedAmount = 0 expFunction = achievement.getExpFunction() newPoints = expFunction(earned.achievedAmount) * achievement.get('worth') else newPoints = achievement.get 'worth' earned.earnedPoints = newPoints newTotalPoints += newPoints EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) -> log.error err if err? callback() saveUserPoints = (callback) -> # In principle it is enough to deduct the old amount of points and add the new amount, # but just to be entirely safe let's start from 0 in case we're updating all of a user's achievements log.debug "Matched a total of #{newTotalPoints} new points" if _.isEmpty filter # Completely clean User.update {_id: userID}, {$set: points: newTotalPoints}, {}, (err) -> log.error err if err? else log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}" User.update {_id: userID}, {$inc: points: newTotalPoints - previousPoints}, {}, (err) -> log.error err if err? earnedAchievementSavers = (earnedAchievementSaverGenerator(achievement) for achievement in achievements) earnedAchievementSavers.push saveUserPoints # We need to have all these database updates chained so we know the final score async.series earnedAchievementSavers module.exports = new EarnedAchievementHandler()