mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-12 00:31:21 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
011f0191dc
16 changed files with 292 additions and 57 deletions
|
@ -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
|
||||||
|
|
|
@ -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) ->
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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', ->
|
||||||
|
|
|
@ -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)
|
||||||
|
});
|
|
@ -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}))
|
||||||
|
|
|
@ -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()
|
||||||
|
checkTimestamp = new Date().toISOString()
|
||||||
|
achievement = yield Achievement.findOne({ updated: { $gt: lastAchievementChecked }}).sort({updated:1})
|
||||||
|
|
||||||
lastAchievementChecked = user.get('lastAchievementChecked') or user._id
|
|
||||||
achievement = yield Achievement.findOne({ _id: { $gt: lastAchievementChecked }}).sort({_id: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()))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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) ->
|
||||||
|
|
|
@ -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,36 +1071,132 @@ 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')
|
|
||||||
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()
|
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
|
achievementUpdated = res.body.updated
|
||||||
expect(res.statusCode).toBe(201)
|
expect(res.statusCode).toBe(201)
|
||||||
|
|
||||||
user = yield User.findById(user.id)
|
user = yield User.findById(user.id)
|
||||||
expect(user.get('rewards')).toBeUndefined()
|
expect(user.get('earned')).toBeUndefined()
|
||||||
|
|
||||||
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(body.points).toBe(175)
|
||||||
earned = yield EarnedAchievement.count()
|
earned = yield EarnedAchievement.count()
|
||||||
expect(earned).toBe(1)
|
expect(earned).toBe(1)
|
||||||
expect(body.lastAchievementChecked).toBe(achievementID)
|
expect(body.lastAchievementChecked).toBe(achievementUpdated)
|
||||||
|
|
||||||
done()
|
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()
|
||||||
|
yield utils.loginUser(admin)
|
||||||
|
[res, body] = yield request.postAsync { uri: achievementURL, json: achievementJSON }
|
||||||
|
achievement = yield Achievement.findById(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(res.body.points).toBe(175)
|
||||||
|
expect(res.body.earned.gems).toBe(50)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
|
@ -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'
|
||||||
|
@ -98,6 +99,38 @@ module.exports = mw =
|
||||||
request.post { uri: getURL('/db/level'), json: data }, (err, res) ->
|
request.post { uri: getURL('/db/level'), json: data }, (err, res) ->
|
||||||
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)
|
||||||
|
@ -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) ->
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue