diff --git a/app/schemas/models/earned_achievement.coffee b/app/schemas/models/earned_achievement.coffee index f976be719..e250834d4 100644 --- a/app/schemas/models/earned_achievement.coffee +++ b/app/schemas/models/earned_achievement.coffee @@ -25,5 +25,6 @@ module.exports = created: type: 'date' changed: type: 'date' achievedAmount: type: 'number' + earnedPoints: type: 'number' previouslyAchievedAmount: {type: 'number', default: 0} notified: type: 'boolean' diff --git a/server/achievements/EarnedAchievement.coffee b/server/achievements/EarnedAchievement.coffee index 088a919b3..e02174c29 100644 --- a/server/achievements/EarnedAchievement.coffee +++ b/server/achievements/EarnedAchievement.coffee @@ -1,5 +1,6 @@ mongoose = require 'mongoose' jsonschema = require '../../app/schemas/models/earned_achievement' +User = require '../users/User' EarnedAchievementSchema = new mongoose.Schema({ created: @@ -20,8 +21,6 @@ EarnedAchievementSchema.pre 'save', (next) -> EarnedAchievementSchema.index({user: 1, achievement: 1}, {unique: true, name: 'earned achievement index'}) EarnedAchievementSchema.index({user: 1, changed: -1}, {name: 'latest '}) -EarnedAchievementSchema.static 'recalculate', (callback) -> - callback('pass') module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema) diff --git a/server/achievements/earned_achievement_handler.coffee b/server/achievements/earned_achievement_handler.coffee index 8869f5c6d..80570b8d0 100644 --- a/server/achievements/earned_achievement_handler.coffee +++ b/server/achievements/earned_achievement_handler.coffee @@ -1,16 +1,104 @@ log = require 'winston' -mongoose = require('mongoose') +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' + req.method is 'GET' # or req.user.isAdmin() recalculate: (req, res) -> - EarnedAchievement.recalculate (data) => @sendSuccess(res, data) + onSuccess = (data) => @sendSuccess(res, data) + if 'achievements' of req.query # Support both slugs and IDs separated by commas + achievementSlugsOrIDs = req.query.id.split(',') + EarnedAchievementHandler.recalculate achievementSlugsOrIDs, onSuccess + else + EarnedAchievementHandler.recalculate onSuccess + @sendSuccess res + + # Returns success: boolean + @recalculate: (callbackOrSlugsOrIDs, callback) -> + if _.isArray callbackOrSlugsOrIDs + achievementSlugs = (thing unless Handler.isID(thing) for thing in callbackOrSlugsOrIDs) + achievementIDs = (thing if Handler.isID(thing) for thing in callbackOrSlugsOrIDs) + else + callback = callbackOrSlugsOrIDs + + filter = {} + filter.$or = [ + _id: $in: achievementIDs + slug: $in: achievementSlugs + ] if achievementSlugs? or achievementIDs? + + Achievement.find filter, (err, achievements) -> + return false and log.error err if err? + User.find {}, (err, users) -> + _.each users, (user) -> + # Keep track of a user's already achieved so as to set the notified values correctly + userID = user.get('_id').toHexString() + EarnedAchievement.find {user: userID}, (err, alreadyEarned) -> + alreadyEarnedIDs = [] + previousPoints = 0 + _.each alreadyEarned, (earned) -> + 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) -> + log.debug 'Checking out tha fancy achievement' + 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() + newTotalPoints += expFunction(earned.achievedAmount) * achievement.get('worth') + else + newTotalPoints += achievement.get 'worth' + + 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 + User.update {_id: userID}, {$inc: points: newTotalPoints - previousPoints}, {}, (err) -> log.error err if err? + + earnedAchievementSavers = (earnedAchievementSaverGenerator(achievement) for achievement in achievements) + earnedAchievementSavers.push saveUserPoints + + async.series earnedAchievementSavers + module.exports = new EarnedAchievementHandler() diff --git a/server/plugins/achievements.coffee b/server/plugins/achievements.coffee index a684b2384..c68136983 100644 --- a/server/plugins/achievements.coffee +++ b/server/plugins/achievements.coffee @@ -73,17 +73,18 @@ module.exports = AchievablePlugin = (schema, options) -> expFunction = achievement.getExpFunction() earned.notified = false earned.achievedAmount = newAmount + earned.earnedPoints = (expFunction(newAmount) - expFunction(originalAmount)) * worth earned.previouslyAchievedAmount = originalAmount - EarnedAchievement.findOneAndUpdate({achievement:earned.achievement, user:earned.user}, earned, upsert:true, (err, docs) -> - return log.debug err if err? - ) + EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) -> + return log.debug err if err? - earnedPoints = (expFunction(newAmount) - expFunction(originalAmount)) * worth + earnedPoints = earned.earnedPoints log.debug earnedPoints wrapUp() else # not alreadyAchieved log.debug 'Creating a new earned achievement called \'' + (achievement.get 'name') + '\' for ' + userID + earned.earnedPoints = worth (new EarnedAchievement(earned)).save (err, doc) -> return log.debug err if err? earnedPoints = worth diff --git a/server/routes/admin.coffee b/server/routes/admin.coffee index 012f0d865..c2ae7612a 100644 --- a/server/routes/admin.coffee +++ b/server/routes/admin.coffee @@ -2,8 +2,11 @@ log = require 'winston' errors = require '../commons/errors' handlers = require('../commons/mapping').handlers +mongoose = require('mongoose') + module.exports.setup = (app) -> - app.all '/admin/*', (req, res) -> + app.post '/admin/*', (req, res) -> + # TODO apparently I can leave this out as long as I use res.send res.setHeader('Content-Type', 'application/json') module = req.path[7..]