Implement POST /db/user/:handle/check-for-new-achievement, couple tweaks

* Enforce being logged in for POST /db/earned_achievement
* Extend timeout for race condition user tests
This commit is contained in:
Scott Erickson 2016-08-26 14:41:21 -07:00
parent f509c95a4b
commit 139efe4cf7
7 changed files with 157 additions and 47 deletions

View file

@ -26,32 +26,5 @@ exports.post = wrap (req, res) ->
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}))
finalEarned = yield EarnedAchievement.upsertFor(achievement, trigger, earned, req.user)
res.status(201).send(finalEarned.toObject({req}))

View file

@ -16,7 +16,10 @@ CourseInstance = require '../models/CourseInstance'
facebook = require '../lib/facebook'
gplus = require '../lib/gplus'
TrialRequest = require '../models/TrialRequest'
Achievement = require '../models/Achievement'
EarnedAchievement = require '../models/EarnedAchievement'
log = require 'winston'
LocalMongo = require '../../app/lib/LocalMongo'
module.exports =
fetchByGPlusID: wrap (req, res, next) ->
@ -254,3 +257,45 @@ module.exports =
yield user.update({ $unset: {role: ''}})
user.set('role', undefined)
return res.status(200).send(user.toObject({req: req}))
checkForNewAchievement: wrap (req, res) ->
user = req.user
lastAchievementChecked = user.get('lastAchievementChecked') or user._id
achievement = yield Achievement.findOne({ _id: { $gt: lastAchievementChecked }}).sort({_id:1})
if not achievement
userUpdate = { 'lastAchievementChecked': new mongoose.Types.ObjectId() }
user.update(userUpdate)
return res.send(userUpdate)
userUpdate = { 'lastAchievementChecked': achievement._id }
query = achievement.get('query')
collection = achievement.get('collection')
if collection is 'users'
triggers = [user]
else if collection is 'level.sessions' and query['level.original']
triggers = yield LevelSessions.find({
'level.original': query['level.original']
creator: user._id
})
else
userUpdate = { 'lastAchievementChecked': new mongoose.Types.ObjectId() }
user.update(userUpdate)
return res.send(userUpdate)
trigger = _.find(triggers, (trigger) -> LocalMongo.matchesQuery(trigger.toObject(), query))
if not trigger
user.update(userUpdate)
return res.send(userUpdate)
earned = yield EarnedAchievement.findOne({ achievement: achievement.id, user: req.user })
yield [
EarnedAchievement.upsertFor(achievement, trigger, earned, req.user)
user.update(userUpdate)
]
user = yield User.findById(user.id).select({points: 1, earned: 1})
return res.send(_.assign({}, userUpdate, user.toObject()))

View file

@ -6,6 +6,7 @@ plugins = require('../plugins/plugins')
AchievablePlugin = require '../plugins/achievements'
TreemaUtils = require '../../bower_components/treema/treema-utils.js'
config = require '../../server_config'
co = require 'co'
# `pre` and `post` are not called for update operations executed directly on the database,
# including `Model.update`,`.findByIdAndUpdate`,`.findOneAndUpdate`, `.findOneAndRemove`,and `.findByIdAndRemove`.order
@ -53,27 +54,29 @@ AchievementSchema.statics.achievementCollections = {}
# Reloads all achievements into memory.
# TODO might want to tweak this to only load new achievements
AchievementSchema.statics.loadAchievements = (done) ->
AchievementSchema.statics.loadAchievements = co.wrap ->
AchievementSchema.statics.resetAchievements()
t0 = new Date()
Achievement = require('./Achievement')
query = Achievement.find({collection: {$ne: 'level.sessions'}})
query.exec (err, docs) ->
_.each docs, (achievement) ->
collection = achievement.get 'collection'
AchievementSchema.statics.achievementCollections[collection] ?= []
if _.find AchievementSchema.statics.achievementCollections[collection], ((a) -> a.get('_id').toHexString() is achievement.get('_id').toHexString())
log.warn "Uh oh, we tried to add another copy of the same achievement #{achievement.get('_id')} #{achievement.get('name')} to the #{collection} achievement list..."
else
AchievementSchema.statics.achievementCollections[collection].push achievement
unless achievement.get('query')
log.error "Uh oh, there is an achievement with an empty query: #{achievement}"
done?(AchievementSchema.statics.achievementCollections) # TODO: Return with err as first parameter
achievements = yield Achievement.find({collection: {$ne: 'level.sessions'}})
return if t0 < @lastReset # if a test has run resetAchievements during the fetch, abort
for achievement in achievements
collection = achievement.get 'collection'
AchievementSchema.statics.achievementCollections[collection] ?= []
if _.find AchievementSchema.statics.achievementCollections[collection], ((a) -> a.get('_id').toHexString() is achievement.get('_id').toHexString())
log.warn "Uh oh, we tried to add another copy of the same achievement #{achievement.get('_id')} #{achievement.get('name')} to the #{collection} achievement list..."
else
AchievementSchema.statics.achievementCollections[collection].push achievement
unless achievement.get('query')
log.error "Uh oh, there is an achievement with an empty query: #{achievement}"
return AchievementSchema.statics.achievementCollections
AchievementSchema.statics.getLoadedAchievements = ->
AchievementSchema.statics.achievementCollections
AchievementSchema.statics.resetAchievements = ->
delete AchievementSchema.statics.achievementCollections[collection] for collection of AchievementSchema.statics.achievementCollections
@lastReset = new Date()
AchievementSchema.statics.editableProperties = [
'name'

View file

@ -17,6 +17,38 @@ 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.statics.upsertFor = (achievement, trigger, earned, user) ->
if achievement.get('proportionalTo') and earned
earnedAchievementDoc = yield @createForAchievement(achievement, trigger, {previouslyEarnedAchievement: earned})
return earnedAchievementDoc or earned
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 user.update(update)
return earned
else
earned = yield @createForAchievement(achievement, trigger)
if not earned
console.error "Couldn't create achievement", achievement, trigger
throw new errors.NotFound("Couldn't create achievement")
return earned
EarnedAchievementSchema.statics.createForAchievement = co.wrap (achievement, doc, options={}) ->
{ previouslyEarnedAchievement, originalDocObj } = options

View file

@ -98,7 +98,7 @@ module.exports.setup = (app) ->
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)
app.post('/db/earned_achievement', mw.auth.checkLoggedIn(), 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
@ -118,7 +118,8 @@ module.exports.setup = (app) ->
app.post('/db/user/:handle/signup-with-password', mw.users.signupWithPassword)
app.post('/db/user/:handle/destudent', mw.auth.checkHasPermission(['admin']), mw.users.destudent)
app.post('/db/user/:handle/deteacher', mw.auth.checkHasPermission(['admin']), mw.users.deteacher)
app.post('/db/user/:handle/check-for-new-achievement', mw.auth.checkLoggedIn(), mw.users.checkForNewAchievement)
app.get('/db/prepaid', mw.auth.checkLoggedIn(), mw.prepaids.fetchByCreator)
app.get('/db/prepaid/-/active-schools', mw.auth.checkHasPermission(['admin']), mw.prepaids.fetchActiveSchools)
app.post('/db/prepaid', mw.auth.checkHasPermission(['admin']), mw.prepaids.post)

View file

@ -240,7 +240,7 @@ describe 'automatically achieving achievements', ->
it 'happens when an object\'s properties meet achievement goals', utils.wrap (done) ->
# load achievements on server
@achievements = yield Achievement.loadAchievements()
expect(@achievements.length).toBe(2)
expect(@achievements.users.length).toBe(2)
loadedAchievements = Achievement.getLoadedAchievements()
expect(Object.keys(loadedAchievements).length).toBe(1)

View file

@ -13,6 +13,10 @@ facebook = require '../../../server/lib/facebook'
gplus = require '../../../server/lib/gplus'
sendwithus = require '../../../server/sendwithus'
Promise = require 'bluebird'
Achievement = require '../../../server/models/Achievement'
EarnedAchievement = require '../../../server/models/EarnedAchievement'
LevelSession = require '../../../server/models/LevelSession'
mongoose = require 'mongoose'
describe 'POST /db/user', ->
@ -832,7 +836,7 @@ describe 'POST /db/user/:handle/signup-with-facebook', ->
beforeEach utils.wrap (done) ->
yield utils.clearModels([User])
yield new Promise((resolve) -> setTimeout(resolve, 10))
yield new Promise((resolve) -> setTimeout(resolve, 50))
done()
it 'signs up the user with the facebookID and sends welcome emails', utils.wrap (done) ->
@ -921,7 +925,7 @@ describe 'POST /db/user/:handle/signup-with-gplus', ->
beforeEach utils.wrap (done) ->
yield utils.clearModels([User])
yield new Promise((resolve) -> setTimeout(resolve, 10))
yield new Promise((resolve) -> setTimeout(resolve, 50))
done()
it 'signs up the user with the gplusID and sends welcome emails', utils.wrap (done) ->
@ -1032,3 +1036,55 @@ describe 'POST /db/user/:handle/deteacher', ->
teacher = yield User.findById(teacher.id)
expect(teacher.get('role')).toBeUndefined()
done()
describe 'POST /db/user/:handle/check-for-new-achievements', ->
beforeEach utils.wrap (done) ->
yield utils.clearModels [Achievement, EarnedAchievement, LevelSession, User]
Achievement.resetAchievements()
done()
it 'finds new achievements and awards them to the user', utils.wrap (done) ->
user = yield utils.initUser({points: 100})
yield utils.loginUser(user)
url = utils.getURL("/db/user/#{user.id}/check-for-new-achievement")
json = true
[res, body] = yield request.postAsync({ url, json })
earned = yield EarnedAchievement.count()
expect(earned).toBe(0)
achievementURL = getURL('/db/achievement')
achievementJSON = {
collection: 'users'
query: {'points': {$gt: 50}}
userField: '_id'
recalculable: true
worth: 75
rewards: {
gems: 50
levels: [new mongoose.Types.ObjectId().toString()]
}
name: 'Dungeon Arena Started'
description: 'Started playing Dungeon Arena.'
related: 'a'
}
admin = yield utils.initAdmin()
yield utils.loginUser(admin)
[res, body] = yield request.postAsync { uri: achievementURL, json: achievementJSON }
achievementID = body._id
expect(res.statusCode).toBe(201)
user = yield User.findById(user.id)
expect(user.get('rewards')).toBeUndefined()
yield utils.loginUser(user)
[res, body] = yield request.postAsync({ url, json })
expect(body.points).toBe(175)
earned = yield EarnedAchievement.count()
expect(earned).toBe(1)
expect(body.lastAchievementChecked).toBe(achievementID)
done()