Merge branch 'master' into production

This commit is contained in:
Scott Erickson 2016-09-06 10:55:13 -07:00
commit 011f0191dc
16 changed files with 292 additions and 57 deletions

View file

@ -62,7 +62,24 @@ Application = {
$(document).bind 'keydown', preventBackspace $(document).bind 'keydown', preventBackspace
preload(COMMON_FILES) preload(COMMON_FILES)
CocoModel.pollAchievements() CocoModel.pollAchievements()
# @checkForNewAchievement() # TODO: Enable once thoroughly tested unless me.get('anonymous')
# TODO: Remove logging later, once this system has proved stable
me.on 'change:earned', (user, newEarned) ->
oldEarned = user.previous('earned') ? {}
if oldEarned.gems isnt newEarned.gems
console.log 'Gems changed', oldEarned.gems, '->', newEarned.gems
newLevels = _.difference(newEarned.levels, oldEarned.levels)
if newLevels.length
console.log 'Levels added', newLevels
newItems = _.difference(newEarned.items, oldEarned.items)
if newItems.length
console.log 'Items added', newItems
newHeroes = _.difference(newEarned.heroes, oldEarned.heroes)
if newHeroes.length
console.log 'Heroes added', newHeroes
me.on 'change:points', (user, newPoints) ->
console.log 'Points changed', user.previous('points'), '->', newPoints
@checkForNewAchievement()
$.i18n.init { $.i18n.init {
lng: me.get('preferredLanguage', true) lng: me.get('preferredLanguage', true)
fallbackLng: 'en' fallbackLng: 'en'
@ -84,12 +101,14 @@ Application = {
@idleTracker.start() @idleTracker.start()
checkForNewAchievement: -> checkForNewAchievement: ->
id = me.get('lastAchievementChecked') or me.id if me.get('lastAchievementChecked')
lastAchievementChecked = new Date(parseInt(id.substring(0, 8), 16) * 1000) startFrom = new Date(me.get('lastAchievementChecked'))
daysSince = moment.duration(new Date() - lastAchievementChecked).asDays() else
startFrom = me.created()
daysSince = moment.duration(new Date() - startFrom).asDays()
if daysSince > 1 if daysSince > 1
me.checkForNewAchievement() me.checkForNewAchievement().then => @checkForNewAchievement()
setTimeout(_.bind(@checkForNewAchievement, @), moment.duration(1, 'minute').asMilliseconds())
} }
module.exports = Application module.exports = Application

View file

@ -6,10 +6,10 @@ mapred = (left, right, func) ->
result or (_.reduce (_.map right, (singleRight) -> func(singleLeft, singleRight)), result or (_.reduce (_.map right, (singleRight) -> func(singleLeft, singleRight)),
((intermediate, value) -> intermediate or value), false)), false) ((intermediate, value) -> intermediate or value), false)), false)
doQuerySelector = (value, operatorObj) -> doQuerySelector = (originalValue, operatorObj) ->
value = [value] unless _.isArray value # left hand can be an array too value = if _.isArray originalValue then originalValue else [originalValue] # left hand can be an array too
for operator, body of operatorObj for operator, originalBody of operatorObj
body = [body] unless _.isArray body # right hand can be an array too body = if _.isArray originalBody then originalBody else [originalBody] # right hand can be an array too
switch operator switch operator
when '$gt' then return false unless mapred value, body, (l, r) -> l > r when '$gt' then return false unless mapred value, body, (l, r) -> l > r
when '$gte' then return false unless mapred value, body, (l, r) -> l >= r when '$gte' then return false unless mapred value, body, (l, r) -> l >= r
@ -19,7 +19,9 @@ doQuerySelector = (value, operatorObj) ->
when '$in' then return false unless _.reduce value, ((result, val) -> result or val in body), false when '$in' then return false unless _.reduce value, ((result, val) -> result or val in body), false
when '$nin' then return false if _.reduce value, ((result, val) -> result or val in body), false when '$nin' then return false if _.reduce value, ((result, val) -> result or val in body), false
when '$exists' then return false if value[0]? isnt body[0] when '$exists' then return false if value[0]? isnt body[0]
else return false else
trimmedOperator = _.pick(operatorObj, operator)
return false unless _.isObject(originalValue) and matchesQuery(originalValue, trimmedOperator)
true true
matchesQuery = (target, queryObj) -> matchesQuery = (target, queryObj) ->

View file

@ -13,6 +13,7 @@ module.exports =
instance.numCompleted = 0 instance.numCompleted = 0
instance.started = false instance.started = false
levels = classroom.getLevels({courseID: course.id}) levels = classroom.getLevels({courseID: course.id})
levels.remove(levels.filter((level) => level.get('practice')))
for userID in instance.get('members') for userID in instance.get('members')
instance.started ||= _.any levels.models, (level) -> instance.started ||= _.any levels.models, (level) ->
session = _.find classroom.sessions.models, (session) -> session = _.find classroom.sessions.models, (session) ->
@ -180,7 +181,7 @@ module.exports =
if _.find(sessions, (s) -> s.completed()) # have finished this level if _.find(sessions, (s) -> s.completed()) # have finished this level
courseProgress.completed &&= true #no-op courseProgress.completed &&= true #no-op
courseProgress[userID].completed &&= true #no-op courseProgress[userID].completed &&= true #no-op
courseProgress[userID].levelsCompleted += 1 courseProgress[userID].levelsCompleted += 1 unless level.get('practice')
courseProgress[levelID].completed &&= true #no-op courseProgress[levelID].completed &&= true #no-op
# courseProgress[levelID].numCompleted += 1 # courseProgress[levelID].numCompleted += 1
courseProgress[levelID][userID].completed = true courseProgress[levelID][userID].completed = true

View file

@ -82,6 +82,7 @@ _.extend AchievementSchema.properties,
i18n: {type: 'object', format: 'i18n', props: ['name', 'description'], description: 'Help translate this achievement'} i18n: {type: 'object', format: 'i18n', props: ['name', 'description'], description: 'Help translate this achievement'}
rewards: c.RewardSchema 'awarded by this achievement' rewards: c.RewardSchema 'awarded by this achievement'
hidden: {type: 'boolean', description: 'Hide achievement from user if true'} hidden: {type: 'boolean', description: 'Hide achievement from user if true'}
updated: c.stringDate({ description: 'When the achievement was changed in such a way that earned achievements should get updated.' })
_.extend AchievementSchema, # Let's have these on the bottom _.extend AchievementSchema, # Let's have these on the bottom

View file

@ -344,7 +344,7 @@ _.extend UserSchema.properties,
schoolName: {type: 'string'} schoolName: {type: 'string'}
role: {type: 'string', enum: ["God", "advisor", "parent", "principal", "student", "superintendent", "teacher", "technology coordinator"]} role: {type: 'string', enum: ["God", "advisor", "parent", "principal", "student", "superintendent", "teacher", "technology coordinator"]}
birthday: c.stringDate({title: "Birthday"}) birthday: c.stringDate({title: "Birthday"})
lastAchievementChecked: c.objectId({ name: 'Last Achievement Checked' }) lastAchievementChecked: c.stringDate({ name: 'Last Achievement Checked' })
c.extendBasicProperties UserSchema, 'user' c.extendBasicProperties UserSchema, 'user'

View file

@ -226,7 +226,7 @@ mixin studentRow(student)
- var instance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id }) - var instance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
if instance && instance.hasMember(student) if instance && instance.hasMember(student)
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, user: student }) - var progress = state.get('progressData').get({ classroom: view.classroom, course: course, user: student })
- var levelsTotal = trimCourse.levels.length - var levelsTotal = _.reject(trimCourse.levels, 'practice').length
//- - var level = ??? //- - var level = ???
- var label = courseLabelsArray[index]; - var label = courseLabelsArray[index];
+studentCourseProgressDot(progress, levelsTotal, level, label) +studentCourseProgressDot(progress, levelsTotal, level, label)

View file

@ -34,6 +34,7 @@ module.exports = class TeacherClassesView extends RootView
initialize: (options) -> initialize: (options) ->
super(options) super(options)
@classrooms = new Classrooms() @classrooms = new Classrooms()
@classrooms.comparator = (a, b) -> b.id.localeCompare(a.id)
@classrooms.fetchMine() @classrooms.fetchMine()
@supermodel.trackCollection(@classrooms) @supermodel.trackCollection(@classrooms)
@listenTo @classrooms, 'sync', -> @listenTo @classrooms, 'sync', ->

View file

@ -0,0 +1,7 @@
var d = new Date();
db.achievements.find({}, {_id:1}).sort({_id:1}).forEach(function(achievement) {
db.achievements.update({_id: achievement._id}, {$set: {updated: d.toISOString()}})
print('set', achievement._id, 'to', d);
d.setSeconds(d.getSeconds() + 1)
});

View file

@ -19,7 +19,12 @@ module.exports =
unless hasPermission or database.isJustFillingTranslations(req, achievement) unless hasPermission or database.isJustFillingTranslations(req, achievement)
throw new errors.Forbidden('Must be an admin, artisan or submitting translations to edit an achievement') throw new errors.Forbidden('Must be an admin, artisan or submitting translations to edit an achievement')
propsWatching = ['query', 'proportionalTo', 'rewards', 'worth', 'function']
oldCopy = _.pick(achievement.toObject(), propsWatching)
database.assignBody(req, achievement) database.assignBody(req, achievement)
newCopy = _.pick(achievement.toObject(), propsWatching)
unless _.isEqual(oldCopy, newCopy)
achievement.set('updated', new Date().toISOString())
database.validateDoc(achievement) database.validateDoc(achievement)
achievement = yield achievement.save() achievement = yield achievement.save()
res.status(200).send(achievement.toObject({req: req})) res.status(200).send(achievement.toObject({req: req}))

View file

@ -20,6 +20,7 @@ Achievement = require '../models/Achievement'
EarnedAchievement = require '../models/EarnedAchievement' EarnedAchievement = require '../models/EarnedAchievement'
log = require 'winston' log = require 'winston'
LocalMongo = require '../../app/lib/LocalMongo' LocalMongo = require '../../app/lib/LocalMongo'
LevelSession = require '../models/LevelSession'
module.exports = module.exports =
fetchByGPlusID: wrap (req, res, next) -> fetchByGPlusID: wrap (req, res, next) ->
@ -261,28 +262,27 @@ module.exports =
checkForNewAchievement: wrap (req, res) -> checkForNewAchievement: wrap (req, res) ->
user = req.user user = req.user
lastAchievementChecked = user.get('lastAchievementChecked') or user._id.getTimestamp().toISOString()
lastAchievementChecked = user.get('lastAchievementChecked') or user._id checkTimestamp = new Date().toISOString()
achievement = yield Achievement.findOne({ _id: { $gt: lastAchievementChecked }}).sort({_id:1}) achievement = yield Achievement.findOne({ updated: { $gt: lastAchievementChecked }}).sort({updated:1})
if not achievement if not achievement
userUpdate = { 'lastAchievementChecked': new mongoose.Types.ObjectId() } userUpdate = { 'lastAchievementChecked': checkTimestamp }
user.update({$set: userUpdate}).exec() user.update({$set: userUpdate}).exec()
return res.send(userUpdate) return res.send(userUpdate)
userUpdate = { 'lastAchievementChecked': achievement._id } userUpdate = { 'lastAchievementChecked': achievement.get('updated') }
query = achievement.get('query') query = achievement.get('query')
collection = achievement.get('collection') collection = achievement.get('collection')
if collection is 'users' if collection is 'users'
triggers = [user] triggers = [user]
else if collection is 'level.sessions' and query['level.original'] else if collection is 'level.sessions' and query['level.original']
triggers = yield LevelSessions.find({ triggers = yield LevelSession.find({
'level.original': query['level.original'] 'level.original': query['level.original']
creator: user._id creator: user.id
}) })
else else
userUpdate = { 'lastAchievementChecked': new mongoose.Types.ObjectId() }
user.update({$set: userUpdate}).exec() user.update({$set: userUpdate}).exec()
return res.send(userUpdate) return res.send(userUpdate)
@ -292,10 +292,8 @@ module.exports =
user.update({$set: userUpdate}).exec() user.update({$set: userUpdate}).exec()
return res.send(userUpdate) return res.send(userUpdate)
earned = yield EarnedAchievement.findOne({ achievement: achievement.id, user: req.user }) earned = yield EarnedAchievement.findOne({ achievement: achievement.id, user: req.user.id })
yield [ yield EarnedAchievement.upsertFor(achievement, trigger, earned, req.user)
EarnedAchievement.upsertFor(achievement, trigger, earned, req.user) yield user.update({$set: userUpdate})
user.update({$set: userUpdate})
]
user = yield User.findById(user.id).select({points: 1, earned: 1}) user = yield User.findById(user.id).select({points: 1, earned: 1})
return res.send(_.assign({}, userUpdate, user.toObject())) return res.send(_.assign({}, userUpdate, user.toObject()))

View file

@ -33,6 +33,7 @@ AchievementSchema.index(
AchievementSchema.index({i18nCoverage: 1}, {name: 'translation coverage index', sparse: true}) AchievementSchema.index({i18nCoverage: 1}, {name: 'translation coverage index', sparse: true})
AchievementSchema.index({slug: 1}, {name: 'slug index', sparse: true, unique: true}) AchievementSchema.index({slug: 1}, {name: 'slug index', sparse: true, unique: true})
AchievementSchema.index({related: 1}, {name: 'related index', sparse: true}) AchievementSchema.index({related: 1}, {name: 'related index', sparse: true})
AchievementSchema.index({updated: 1}, {name: 'updated index'})
AchievementSchema.methods.objectifyQuery = -> AchievementSchema.methods.objectifyQuery = ->
try try
@ -105,6 +106,8 @@ AchievementSchema.post 'init', (doc) -> doc.objectifyQuery()
AchievementSchema.pre 'save', (next) -> AchievementSchema.pre 'save', (next) ->
@stringifyQuery() @stringifyQuery()
if not @get('updated')
@set('updated', new Date().toISOString())
next() next()
# Reload achievements upon save # Reload achievements upon save

View file

@ -3,6 +3,7 @@ jsonschema = require '../../app/schemas/models/earned_achievement'
util = require '../../app/core/utils' util = require '../../app/core/utils'
log = require 'winston' log = require 'winston'
co = require 'co' co = require 'co'
errors = require '../commons/errors'
EarnedAchievementSchema = new mongoose.Schema({ EarnedAchievementSchema = new mongoose.Schema({
notified: notified:
@ -27,15 +28,19 @@ EarnedAchievementSchema.statics.upsertFor = (achievement, trigger, earned, user)
else if earned else if earned
achievementEarned = achievement.get('rewards') achievementEarned = achievement.get('rewards')
actuallyEarned = earned.get('earnedRewards') actuallyEarned = earned.get('earnedRewards')
if not _.isEqual(achievementEarned, actuallyEarned) oldPoints = earned.get('earnedPoints') ? 0
earned.set('earnedRewards', achievementEarned) newPoints = achievement.get('worth') ? 10
yield earned.save() if (not _.isEqual(achievementEarned, actuallyEarned)) or (oldPoints isnt newPoints)
earned.set('earnedRewards', achievementEarned)
earned.set('earnedPoints', newPoints)
yield earned.save()
# make sure user has all the levels, heroes, gems, items and points they should have
update = {$inc: { points: newPoints - oldPoints }}
# make sure user has all the levels and items they should have
update = {}
for rewardType, rewards of achievement.get('rewards') ? {} for rewardType, rewards of achievement.get('rewards') ? {}
if rewardType is 'gems' if rewardType is 'gems'
update.$inc = { 'earned.gems': rewards - (actuallyEarned.gems ? 0) } update.$inc['earned.gems'] = rewards - (actuallyEarned.gems ? 0)
else if rewards.length else if rewards.length
update.$addToSet ?= {} update.$addToSet ?= {}
update.$addToSet["earned.#{rewardType}"] = { $each: rewards } update.$addToSet["earned.#{rewardType}"] = { $each: rewards }

View file

@ -109,6 +109,45 @@ describe 'PUT /db/achievement', ->
expect(res.body.name).toBe('whatev') expect(res.body.name).toBe('whatev')
done() done()
it 'touches "updated" if query, proportionalTo, worth, rewards or function change', utils.wrap (done) ->
lastUpdated = @unlockable.get('updated')
expect(lastUpdated).toBeDefined()
[res, body] = yield request.putAsync {uri: url + '/'+@unlockable.id, json: {
name: 'whatev'
rewards: @unlockable.get('rewards')
query: @unlockable.get('query')
proportionalTo: @unlockable.get('proportionalTo')
}}
achievement = yield Achievement.findById(@unlockable.id)
expect(achievement.get('updated')).toBeDefined()
expect(achievement.get('updated')).toBe(lastUpdated) # unchanged
newRewards = _.assign({}, @unlockable.get('rewards'), {gems: 1000})
[res, body] = yield request.putAsync {uri: url + '/'+@unlockable.id, json: {rewards: newRewards}}
expect(res.body.updated).not.toBe(lastUpdated)
lastUpdated = res.body.updated
newQuery = _.assign({}, @unlockable.get('query'), {'state.complete': true})
[res, body] = yield request.putAsync {uri: url + '/'+@unlockable.id, json: {query: newQuery}}
expect(res.body.updated).not.toBe(lastUpdated)
lastUpdated = res.body.updated
newProportionalTo = 'playtime'
[res, body] = yield request.putAsync {uri: url + '/'+@unlockable.id, json: {proportionalTo: newProportionalTo}}
expect(res.body.updated).not.toBe(lastUpdated)
lastUpdated = res.body.updated
newWorth = 1000
[res, body] = yield request.putAsync {uri: url + '/'+@unlockable.id, json: {worth: newWorth}}
expect(res.body.updated).not.toBe(lastUpdated)
lastUpdated = res.body.updated
newFunction = { kind: 'logarithmic', parameters: { a: 1, b: 2, c: 3 } }
[res, body] = yield request.putAsync {uri: url + '/'+@unlockable.id, json: {function: newFunction}}
expect(res.body.updated).not.toBe(lastUpdated)
done()
describe 'GET /db/achievement', -> describe 'GET /db/achievement', ->
beforeEach addAllAchievements beforeEach addAllAchievements
@ -187,7 +226,7 @@ describe 'POST /db/earned_achievement', ->
expect(body.achievement).toBe @unlockable.id expect(body.achievement).toBe @unlockable.id
expect(body.user).toBe user.id expect(body.user).toBe user.id
expect(body.notified).toBeFalsy() expect(body.notified).toBeFalsy()
expect(body.earnedPoints).toBe unlockable.worth expect(body.earnedPoints).toBe @unlockable.get('worth')
expect(body.achievedAmount).toBeUndefined() expect(body.achievedAmount).toBeUndefined()
expect(body.previouslyAchievedAmount).toBeUndefined() expect(body.previouslyAchievedAmount).toBeUndefined()
earnedAchievements = yield EarnedAchievement.find() earnedAchievements = yield EarnedAchievement.find()
@ -344,7 +383,7 @@ describe 'POST /admin/earned_achievement/recalculate', ->
expect(earnedAchievements.length).toBe(1) expect(earnedAchievements.length).toBe(1)
user = yield User.findById(@admin.id) user = yield User.findById(@admin.id)
expect(user.get 'points').toBe unlockable.worth expect(user.get 'points').toBe @unlockable.get('worth')
done() done()
it 'can recalculate all achievements', utils.wrap (done) -> it 'can recalculate all achievements', utils.wrap (done) ->
@ -371,8 +410,8 @@ describe 'POST /admin/earned_achievement/recalculate', ->
earnedAchievements = yield EarnedAchievement.find({}) earnedAchievements = yield EarnedAchievement.find({})
expect(earnedAchievements.length).toBe 3 expect(earnedAchievements.length).toBe 3
user = yield User.findById(@admin.id) user = yield User.findById(@admin.id)
expect(user.get 'points').toBe unlockable.worth + 4 * repeatable.worth + (Math.log(.5 * (4 + .5)) + 1) * diminishing.worth expect(user.get 'points').toBe @unlockable.get('worth') + 4 * @repeatable.get('worth') + (Math.log(.5 * (4 + .5)) + 1) * @diminishing.get('worth')
expect(user.get('earned').gems).toBe 4 * repeatable.rewards.gems expect(user.get('earned').gems).toBe 4 * @repeatable.get('rewards').gems
done() done()
afterEach utils.wrap (done) -> afterEach utils.wrap (done) ->

View file

@ -1039,6 +1039,22 @@ describe 'POST /db/user/:handle/deteacher', ->
describe 'POST /db/user/:handle/check-for-new-achievements', -> describe 'POST /db/user/:handle/check-for-new-achievements', ->
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'
}
beforeEach utils.wrap (done) -> beforeEach utils.wrap (done) ->
yield utils.clearModels [Achievement, EarnedAchievement, LevelSession, User] yield utils.clearModels [Achievement, EarnedAchievement, LevelSession, User]
@ -1055,26 +1071,35 @@ describe 'POST /db/user/:handle/check-for-new-achievements', ->
earned = yield EarnedAchievement.count() earned = yield EarnedAchievement.count()
expect(earned).toBe(0) expect(earned).toBe(0)
achievementURL = getURL('/db/achievement') admin = yield utils.initAdmin()
achievementJSON = { yield utils.loginUser(admin)
collection: 'users' [res, body] = yield request.postAsync { uri: achievementURL, json: achievementJSON }
query: {'points': {$gt: 50}} achievementUpdated = res.body.updated
userField: '_id' expect(res.statusCode).toBe(201)
recalculable: true
worth: 75 user = yield User.findById(user.id)
rewards: { expect(user.get('earned')).toBeUndefined()
gems: 50
levels: [new mongoose.Types.ObjectId().toString()] yield utils.loginUser(user)
} [res, body] = yield request.postAsync({ url, json })
name: 'Dungeon Arena Started' expect(body.points).toBe(175)
description: 'Started playing Dungeon Arena.' earned = yield EarnedAchievement.count()
related: 'a' expect(earned).toBe(1)
} expect(body.lastAchievementChecked).toBe(achievementUpdated)
done()
it 'updates the user if they already earned the achievement but the rewards changed', 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 })
admin = yield utils.initAdmin() admin = yield utils.initAdmin()
yield utils.loginUser(admin) yield utils.loginUser(admin)
[res, body] = yield request.postAsync { uri: achievementURL, json: achievementJSON } [res, body] = yield request.postAsync { uri: achievementURL, json: achievementJSON }
achievementID = body._id achievement = yield Achievement.findById(body._id)
expect(res.statusCode).toBe(201) expect(res.statusCode).toBe(201)
user = yield User.findById(user.id) user = yield User.findById(user.id)
@ -1082,9 +1107,96 @@ describe 'POST /db/user/:handle/check-for-new-achievements', ->
yield utils.loginUser(user) yield utils.loginUser(user)
[res, body] = yield request.postAsync({ url, json }) [res, body] = yield request.postAsync({ url, json })
expect(body.points).toBe(175) expect(res.body.points).toBe(175)
earned = yield EarnedAchievement.count() expect(res.body.earned.gems).toBe(50)
expect(earned).toBe(1)
expect(body.lastAchievementChecked).toBe(achievementID)
achievement.set({
updated: new Date().toISOString()
rewards: { gems: 100 }
worth: 200
})
yield achievement.save()
[res, body] = yield request.postAsync({ url, json })
expect(res.body.earned.gems).toBe(100)
expect(res.body.points).toBe(300)
expect(res.statusCode).toBe(200)
# special case: no worth, should default to 10
yield achievement.update({
$set: {updated: new Date().toISOString()},
$unset: {worth:''}
})
[res, body] = yield request.postAsync({ url, json })
expect(res.body.earned.gems).toBe(100)
expect(res.body.points).toBe(110)
expect(res.statusCode).toBe(200)
done()
it 'works for level sessions', utils.wrap (done) ->
admin = yield utils.initAdmin()
yield utils.loginUser(admin)
level = yield utils.makeLevel()
achievement = yield utils.makeAchievement({
collection: 'level.sessions'
userField: 'creator'
query: {
'level.original': level.get('original').toString()
'state': {complete: true}
}
worth: 100
proportionalTo: 'state.difficulty'
})
levelSession = yield utils.makeLevelSession({state: {complete: true, difficulty:2}}, { creator:admin, level })
url = utils.getURL("/db/user/#{admin.id}/check-for-new-achievement")
json = true
[res, body] = yield request.postAsync({ url, json })
expect(body.points).toBe(200)
# check idempotency
achievement.set('updated', new Date().toISOString())
yield achievement.save()
[res, body] = yield request.postAsync({ url, json })
expect(body.points).toBe(200)
admin = yield User.findById(admin.id)
done()
it 'skips achievements which have not been satisfied', utils.wrap (done) ->
admin = yield utils.initAdmin()
yield utils.loginUser(admin)
level = yield utils.makeLevel()
achievement = yield utils.makeAchievement({
collection: 'level.sessions'
userField: 'creator'
query: {
'level.original': 'does not matter'
}
worth: 100
})
expect(admin.get('lastAchievementChecked')).toBeUndefined()
url = utils.getURL("/db/user/#{admin.id}/check-for-new-achievement")
json = true
[res, body] = yield request.postAsync({ url, json })
expect(body.points).toBeUndefined()
admin = yield User.findById(admin.id)
expect(admin.get('lastAchievementChecked')).toBe(achievement.get('updated'))
done()
it 'skips achievements which are not either for the users collection or the level sessions collection with level.original included', utils.wrap (done) ->
admin = yield utils.initAdmin()
yield utils.loginUser(admin)
achievement = yield utils.makeAchievement({
collection: 'not.supported'
userField: 'creator'
query: {}
worth: 100
})
expect(admin.get('lastAchievementChecked')).toBeUndefined()
url = utils.getURL("/db/user/#{admin.id}/check-for-new-achievement")
json = true
[res, body] = yield request.postAsync({ url, json })
expect(body.points).toBeUndefined()
admin = yield User.findById(admin.id)
expect(admin.get('lastAchievementChecked')).toBe(achievement.get('updated'))
done() done()

View file

@ -4,6 +4,7 @@ co = require 'co'
Promise = require 'bluebird' Promise = require 'bluebird'
User = require '../../server/models/User' User = require '../../server/models/User'
Level = require '../../server/models/Level' Level = require '../../server/models/Level'
LevelSession = require '../../server/models/LevelSession'
Achievement = require '../../server/models/Achievement' Achievement = require '../../server/models/Achievement'
Campaign = require '../../server/models/Campaign' Campaign = require '../../server/models/Campaign'
Course = require '../../server/models/Course' Course = require '../../server/models/Course'
@ -99,6 +100,38 @@ module.exports = mw =
return done(err) if err return done(err) if err
Level.findById(res.body._id).exec done Level.findById(res.body._id).exec done
makeLevelSession: Promise.promisify (data, sources, done) ->
args = Array.from(arguments)
[done, [data, sources]] = [args.pop(), args]
data = _.extend({}, {
state:
complete: false
scripts:
currentScript: null
}, data)
if sources?.level and not data.level
data.level = {
original: sources.level.get('original').toString()
majorVersion: sources.level.get('version').major
}
if sources?.creator and not data.creator
data.creator = sources.creator.id
if data.creator and not data.permissions
data.permissions = [
{ target: data.creator, access: 'owner' }
{ target: 'public', access: 'write' }
]
if not data.codeLanguage
data.codeLanguage = 'javascript'
session = new LevelSession(data)
session.save(done)
makeAchievement: Promise.promisify (data, sources, done) -> makeAchievement: Promise.promisify (data, sources, done) ->
args = Array.from(arguments) args = Array.from(arguments)
[done, [data, sources]] = [args.pop(), args] [done, [data, sources]] = [args.pop(), args]
@ -106,12 +139,13 @@ module.exports = mw =
data = _.extend({}, { data = _.extend({}, {
name: _.uniqueId('Achievement ') name: _.uniqueId('Achievement ')
}, data) }, data)
if sources.related and not data.related if sources?.related and not data.related
related = sources.related related = sources.related
data.related = (related.get('original') or related._id).valueOf() data.related = (related.get('original') or related._id).valueOf()
request.post { uri: getURL('/db/achievement'), json: data }, (err, res) -> request.post { uri: getURL('/db/achievement'), json: data }, (err, res) ->
return done(err) if err return done(err) if err
expect(res.statusCode).toBe(201)
Achievement.findById(res.body._id).exec done Achievement.findById(res.body._id).exec done
makeCampaign: Promise.promisify (data, sources, done) -> makeCampaign: Promise.promisify (data, sources, done) ->

View file

@ -9,6 +9,9 @@ describe 'Local Mongo queries', ->
'worth': 6 'worth': 6
'type': 'unicorn' 'type': 'unicorn'
'likes': ['poptarts', 'popsicles', 'popcorn'] 'likes': ['poptarts', 'popsicles', 'popcorn']
nested: {
str:'ing'
}
@fixture2 = this: is: so: 'deep' @fixture2 = this: is: so: 'deep'
@ -24,6 +27,11 @@ describe 'Local Mongo queries', ->
it 'nested match', -> it 'nested match', ->
expect(LocalMongo.matchesQuery(@fixture2, 'this.is.so': 'deep')).toBeTruthy() expect(LocalMongo.matchesQuery(@fixture2, 'this.is.so': 'deep')).toBeTruthy()
expect(LocalMongo.matchesQuery(@fixture2, {this:{is:{so: 'deep'}}})).toBeTruthy()
expect(LocalMongo.matchesQuery(@fixture2, {'this.is':{so: 'deep'}})).toBeTruthy()
mixedQuery = { nested: {str:'ing'}, worth: {$gt:3} }
expect(LocalMongo.matchesQuery(@fixture1, mixedQuery)).toBeTruthy()
expect(LocalMongo.matchesQuery(@fixture2, mixedQuery)).toBeFalsy()
it '$gt selector', -> it '$gt selector', ->
expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gt': 8000)).toBeTruthy() expect(LocalMongo.matchesQuery(@fixture1, 'value': '$gt': 8000)).toBeTruthy()