From f509c95a4b2241eca70227973592ddb2473bc0d6 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 25 Aug 2016 15:24:27 -0700 Subject: [PATCH] Refactor POST /db/earned_achievement --- .../earned_achievement_handler.coffee | 68 +------------------ server/middleware/earned-achievements.coffee | 57 ++++++++++++++++ server/middleware/index.coffee | 1 + server/routes/index.coffee | 5 +- .../server/functional/achievement.spec.coffee | 40 ++++++++++- 5 files changed, 100 insertions(+), 71 deletions(-) create mode 100644 server/middleware/earned-achievements.coffee diff --git a/server/handlers/earned_achievement_handler.coffee b/server/handlers/earned_achievement_handler.coffee index 90c578805..04ae2ffaf 100644 --- a/server/handlers/earned_achievement_handler.coffee +++ b/server/handlers/earned_achievement_handler.coffee @@ -15,10 +15,9 @@ class EarnedAchievementHandler extends Handler editableProperties: ['notified'] - # Don't allow POSTs or anything yet hasAccess: (req) -> return false unless req.user - req.method in ['GET', 'POST', 'PUT'] # or req.user.isAdmin() + req.method in ['GET', 'PUT'] # or req.user.isAdmin() get: (req, res) -> return @getByAchievementIDs(req, res) if req.query.view is 'get-by-achievement-ids' @@ -45,71 +44,6 @@ class EarnedAchievementHandler extends Handler documents = (@formatEntity(req, doc) for doc in documents) @sendSuccess(res, documents) - post: (req, res) -> - achievementID = req.body.achievement - triggeredBy = req.body.triggeredBy - collection = req.body.collection - if collection isnt 'level.sessions' and not testing # TODO: remove this restriction - return @sendBadInputError(res, 'Only doing level session achievements for now.') - - model = mongoose.modelNameByCollection(collection) - - async.parallel({ - achievement: (callback) -> - Achievement.findById achievementID, (err, achievement) -> callback(err, achievement) - - trigger: (callback) -> - model.findById triggeredBy, (err, trigger) -> callback(err, trigger) - - earned: (callback) -> - q = { achievement: achievementID, user: req.user._id+'' } - EarnedAchievement.findOne q, (err, earned) -> callback(err, earned) - }, (err, { achievement, trigger, earned } ) => - return @sendDatabaseError(res, err) if err - if not achievement - return @sendNotFoundError(res, 'Could not find achievement.') - else if not trigger - return @sendNotFoundError(res, 'Could not find trigger.') - else if achievement.get('proportionalTo') and earned - EarnedAchievement.createForAchievement(achievement, trigger, {previouslyEarnedAchievement: earned}).then (earnedAchievementDoc) => - @sendCreated(res, (earnedAchievementDoc or earned)?.toObject()) - else if earned - achievementEarned = achievement.get('rewards') - actuallyEarned = earned.get('earnedRewards') - if not _.isEqual(achievementEarned, actuallyEarned) - earned.set('earnedRewards', achievementEarned) - earned.save((err) => - return @sendDatabaseError(res, err) if err - @upsertNonNumericRewards(req.user, achievement, (err) => - return @sendDatabaseError(res, err) if err - return @sendSuccess(res, earned.toObject()) - ) - ) - else - @upsertNonNumericRewards(req.user, achievement, (err) => - return @sendDatabaseError(res, err) if err - return @sendSuccess(res, earned.toObject()) - ) - else - EarnedAchievement.createForAchievement(achievement, trigger).then (earnedAchievementDoc) => - if earnedAchievementDoc - @sendCreated(res, earnedAchievementDoc.toObject()) - else - console.error "Couldn't create achievement", achievement, trigger - @sendNotFoundError res, "Couldn't create achievement" - ) - - upsertNonNumericRewards: (user, achievement, done) -> - update = {} - for rewardType, rewards of achievement.get('rewards') ? {} - continue if rewardType is 'gems' - if rewards.length - update.$addToSet ?= {} - update.$addToSet["earned.#{rewardType}"] = $each: rewards - User.update {_id: user._id}, update, {}, (err, result) -> - log.error err if err? - done?(err) - getByAchievementIDs: (req, res) -> query = { user: req.user._id+''} ids = req.query.achievementIDs diff --git a/server/middleware/earned-achievements.coffee b/server/middleware/earned-achievements.coffee new file mode 100644 index 000000000..eccd5dcbb --- /dev/null +++ b/server/middleware/earned-achievements.coffee @@ -0,0 +1,57 @@ +log = require 'winston' +mongoose = require 'mongoose' +Achievement = require './../models/Achievement' +EarnedAchievement = require './../models/EarnedAchievement' +errors = require '../commons/errors' +wrap = require 'co-express' + + +exports.post = wrap (req, res) -> + achievementID = req.body.achievement + triggeredBy = req.body.triggeredBy + collection = req.body.collection + if collection isnt 'level.sessions' and not testing # TODO: remove this restriction + throw new errors.UnprocessableEntity('Only doing level session achievements for now.') + + model = mongoose.modelNameByCollection(collection) + + [achievement, trigger, earned] = yield [ + Achievement.findById(achievementID), + model.findById(triggeredBy) + EarnedAchievement.findOne({ achievement: achievementID, user: req.user.id }) + ] + + if not achievement + throw new errors.NotFound('Could not find achievement.') + if not trigger + throw new errors.NotFound('Could not find trigger.') + + if achievement.get('proportionalTo') and earned + earnedAchievementDoc = yield EarnedAchievement.createForAchievement(achievement, trigger, {previouslyEarnedAchievement: earned}) + res.status(201).send((earnedAchievementDoc or earned)?.toObject({req})) + + else if earned + achievementEarned = achievement.get('rewards') + actuallyEarned = earned.get('earnedRewards') + if not _.isEqual(achievementEarned, actuallyEarned) + earned.set('earnedRewards', achievementEarned) + yield earned.save() + + # make sure user has all the levels and items they should have + update = {} + for rewardType, rewards of achievement.get('rewards') ? {} + continue if rewardType is 'gems' + if rewards.length + update.$addToSet ?= {} + update.$addToSet["earned.#{rewardType}"] = { $each: rewards } + yield req.user.update(update) + + return res.status(200).send(earned.toObject({req})) + + else + earnedAchievementDoc = yield EarnedAchievement.createForAchievement(achievement, trigger) + if not earnedAchievementDoc + console.error "Couldn't create achievement", achievement, trigger + throw new errors.NotFound("Couldn't create achievement") + res.status(201).send(earnedAchievementDoc.toObject({res})) + diff --git a/server/middleware/index.coffee b/server/middleware/index.coffee index b057f60d3..65b7f89b6 100644 --- a/server/middleware/index.coffee +++ b/server/middleware/index.coffee @@ -7,6 +7,7 @@ module.exports = contact: require './contact' courseInstances: require './course-instances' courses: require './courses' + earnedAchievements: require './earned-achievements' files: require './files' healthcheck: require './healthcheck' levels: require './levels' diff --git a/server/routes/index.coffee b/server/routes/index.coffee index b2ab4ba2e..ffe6704e0 100644 --- a/server/routes/index.coffee +++ b/server/routes/index.coffee @@ -96,7 +96,10 @@ module.exports.setup = (app) -> app.post('/db/course_instance/:handle/members', mw.auth.checkLoggedIn(), mw.courseInstances.addMembers) app.get('/db/course_instance/:handle/classroom', mw.auth.checkLoggedIn(), mw.courseInstances.fetchClassroom) app.get('/db/course_instance/:handle/course', mw.auth.checkLoggedIn(), mw.courseInstances.fetchCourse) - + + EarnedAchievement = require '../models/EarnedAchievement' + app.post('/db/earned_achievement', mw.earnedAchievements.post) + Level = require '../models/Level' app.post('/db/level/:handle', mw.auth.checkLoggedIn(), mw.versions.postNewVersion(Level, { hasPermissionsOrTranslations: 'artisan' })) # TODO: add /new-version to route like Article has app.get('/db/level/:handle/session', mw.auth.checkHasUser(), mw.levels.upsertSession) diff --git a/spec/server/functional/achievement.spec.coffee b/spec/server/functional/achievement.spec.coffee index 154be63f9..6a8d2232b 100644 --- a/spec/server/functional/achievement.spec.coffee +++ b/spec/server/functional/achievement.spec.coffee @@ -7,12 +7,15 @@ LevelSession = require '../../../server/models/LevelSession' User = require '../../../server/models/User' request = require '../request' EarnedAchievementHandler = require '../../../server/handlers/earned_achievement_handler' +mongoose = require 'mongoose' url = getURL('/db/achievement') # Fixtures +lockedLevelID = new mongoose.Types.ObjectId().toString() + unlockable = name: 'Dungeon Arena Started' description: 'Started playing Dungeon Arena.' @@ -22,6 +25,9 @@ unlockable = userField: 'creator' recalculable: true related: 'a' + rewards: { + levels: [lockedLevelID] + } unlockable2 = _.clone unlockable unlockable2.name = 'This one is obsolete' @@ -163,6 +169,7 @@ describe 'DELETE /db/achievement/:handle', -> describe 'POST /db/earned_achievement', -> beforeEach addAllAchievements + eaURL = getURL('/db/earned_achievement') it 'manually creates earned achievements for level achievements, which do not happen automatically', utils.wrap (done) -> session = new LevelSession({ @@ -174,7 +181,7 @@ describe 'POST /db/earned_achievement', -> earnedAchievements = yield EarnedAchievement.find() expect(earnedAchievements.length).toBe(0) json = {achievement: @unlockable.id, triggeredBy: session._id, collection: 'level.sessions'} - [res, body] = yield request.postAsync {uri: getURL('/db/earned_achievement'), json: json} + [res, body] = yield request.postAsync { url: eaURL, json } expect(res.statusCode).toBe(201) expect(body.achievement).toBe @unlockable.id expect(body.user).toBe @admin.id @@ -191,14 +198,41 @@ describe 'POST /db/earned_achievement', -> yield utils.loginUser(user) yield user.update({simulatedBy: 10}) json = {achievement: @repeatable.id, triggeredBy: user.id, collection: 'users'} - [res, body] = yield request.postAsync {uri: getURL('/db/earned_achievement'), json: json} + [res, body] = yield request.postAsync { url: eaURL, json } expect(res.statusCode).toBe(201) expect(body.earnedPoints).toBe(10) yield user.update({simulatedBy: 30}) - [res, body] = yield request.postAsync {uri: getURL('/db/earned_achievement'), json: json} + [res, body] = yield request.postAsync { url: eaURL, json } expect(res.statusCode).toBe(201) expect(body.earnedPoints).toBe(20) # this is kinda weird, TODO: just return total amounts done() + + it 'ensures the user has the rewards they earned', utils.wrap (done) -> + user = yield utils.initUser() + yield utils.loginUser(user) + + # get the User the unlockable achievement, check they got their reward + session = new LevelSession({ + permissions: simplePermissions + creator: user._id + level: original: 'dungeon-arena' + }) + yield session.save() + json = {achievement: @unlockable.id, triggeredBy: session._id, collection: 'level.sessions'} + [res, body] = yield request.postAsync { url: eaURL, json } + user = yield User.findById(user.id) + expect(user.get('earned').levels[0]).toBe(lockedLevelID) + + # mess with the user's earned levels, make sure they don't have it anymore + yield user.update({$unset: {earned:1}}) + user = yield User.findById(user.id) + expect(user.get('earned')).toBeUndefined() + + # hit the endpoint again, make sure the level was restored + [res, body] = yield request.postAsync { url: eaURL, json } + user = yield User.findById(user.id) + expect(user.get('earned').levels[0]).toBe(lockedLevelID) + done() describe 'automatically achieving achievements', -> beforeEach addAllAchievements