Achievements now popup when polled for by the client
This commit is contained in:
parent
aa3fedeb02
commit
d766a24e11
12 changed files with 106 additions and 13 deletions
app
models
schemas/models
styles
templates
views/kinds
server
achievements
commons
plugins
users
|
@ -3,4 +3,8 @@ CocoModel = require './CocoModel'
|
||||||
module.exports = class Achievement extends CocoModel
|
module.exports = class Achievement extends CocoModel
|
||||||
@className: 'Achievement'
|
@className: 'Achievement'
|
||||||
@schema: require 'schemas/models/achievement'
|
@schema: require 'schemas/models/achievement'
|
||||||
urlRoot: '/db/achievement'
|
urlRoot: '/db/achievement'
|
||||||
|
|
||||||
|
initialize: (id) ->
|
||||||
|
super()
|
||||||
|
@set('_id', id) if id?
|
|
@ -1,6 +1,10 @@
|
||||||
storage = require 'lib/storage'
|
storage = require 'lib/storage'
|
||||||
deltasLib = require 'lib/deltas'
|
deltasLib = require 'lib/deltas'
|
||||||
|
|
||||||
|
class NewAchievementCollection extends Backbone.Collection
|
||||||
|
initialize: (me = require('lib/auth').me) ->
|
||||||
|
@url = "/db/user/#{me.id}/achievements?notified=false"
|
||||||
|
|
||||||
class CocoModel extends Backbone.Model
|
class CocoModel extends Backbone.Model
|
||||||
idAttribute: "_id"
|
idAttribute: "_id"
|
||||||
loaded: false
|
loaded: false
|
||||||
|
@ -86,6 +90,7 @@ class CocoModel extends Backbone.Model
|
||||||
success(@, res) if success
|
success(@, res) if success
|
||||||
@markToRevert() if @_revertAttributes
|
@markToRevert() if @_revertAttributes
|
||||||
@clearBackup()
|
@clearBackup()
|
||||||
|
CocoModel.pollAchievements()
|
||||||
options.error = (model, res) =>
|
options.error = (model, res) =>
|
||||||
error(@, res) if error
|
error(@, res) if error
|
||||||
return unless @notyErrors
|
return unless @notyErrors
|
||||||
|
@ -266,4 +271,14 @@ class CocoModel extends Backbone.Model
|
||||||
getURL: ->
|
getURL: ->
|
||||||
return if _.isString @url then @url else @url()
|
return if _.isString @url then @url else @url()
|
||||||
|
|
||||||
|
@pollAchievements: ->
|
||||||
|
achievements = new NewAchievementCollection
|
||||||
|
achievements.fetch(
|
||||||
|
success: (collection) ->
|
||||||
|
Backbone.Mediator.publish('achievements:new', collection) unless _.isEmpty(collection.models)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CocoModel.pollAchievements = _.debounce CocoModel.pollAchievements, 500
|
||||||
|
|
||||||
module.exports = CocoModel
|
module.exports = CocoModel
|
||||||
|
|
|
@ -21,12 +21,12 @@ MongoFindQuerySchema =
|
||||||
type: 'object'
|
type: 'object'
|
||||||
patternProperties:
|
patternProperties:
|
||||||
#'^[-a-zA-Z0-9_]*$':
|
#'^[-a-zA-Z0-9_]*$':
|
||||||
'^[-a-zA-Z0-9]*$':
|
'^[-a-zA-Z0-9\.]*$':
|
||||||
oneOf: [
|
oneOf: [
|
||||||
#{ $ref: '#/definitions/' + MongoQueryOperatorSchema.id},
|
#{ $ref: '#/definitions/' + MongoQueryOperatorSchema.id},
|
||||||
{ type: 'string' }
|
{ type: 'string' }
|
||||||
]
|
]
|
||||||
additionalProperties: true
|
additionalProperties: true # TODO make Treema accept new pattern matched keys
|
||||||
definitions: {}
|
definitions: {}
|
||||||
|
|
||||||
MongoFindQuerySchema.definitions[MongoQueryOperatorSchema.id] = MongoQueryOperatorSchema
|
MongoFindQuerySchema.definitions[MongoQueryOperatorSchema.id] = MongoQueryOperatorSchema
|
||||||
|
|
19
app/styles/notify.sass
Normal file
19
app/styles/notify.sass
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
.notifyjs-achievement-base
|
||||||
|
opacity: 0.85
|
||||||
|
width: 200px
|
||||||
|
background: #F5F5F5
|
||||||
|
padding: 5px
|
||||||
|
border-radius: 10px
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
.achievement-title
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
.achievement-image
|
||||||
|
// pass
|
||||||
|
|
||||||
|
.achievement-name
|
||||||
|
// pass
|
||||||
|
|
||||||
|
.achievement-description
|
||||||
|
// pass
|
6
app/templates/achievement_notify.jade
Normal file
6
app/templates/achievement_notify.jade
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
div
|
||||||
|
div.clearfix
|
||||||
|
div.achievement-title(data-notify-html="title")
|
||||||
|
div.achievement-image(data-notify-html="image")
|
||||||
|
div.achievement-name(data-notify-html="name")
|
||||||
|
div.achievement-description(data-notify-html="description")
|
|
@ -12,7 +12,7 @@ body
|
||||||
|
|
||||||
a.navbar-brand(href='/')
|
a.navbar-brand(href='/')
|
||||||
img(src="/images/pages/base/logo.png", title="CodeCombat - Learn how to code by playing a game", alt="CodeCombat")
|
img(src="/images/pages/base/logo.png", title="CodeCombat - Learn how to code by playing a game", alt="CodeCombat")
|
||||||
.collapse.navbar-collapse#collapsible-navbar
|
.collapse.navbar-collapse#collapsible-navbar
|
||||||
ul.nav.navbar-nav
|
ul.nav.navbar-nav
|
||||||
li.play
|
li.play
|
||||||
a.header-font(href='/play', data-i18n="nav.play") Levels
|
a.header-font(href='/play', data-i18n="nav.play") Levels
|
||||||
|
|
|
@ -6,6 +6,9 @@ CocoView = require './CocoView'
|
||||||
{logoutUser, me} = require('lib/auth')
|
{logoutUser, me} = require('lib/auth')
|
||||||
locale = require 'locale/locale'
|
locale = require 'locale/locale'
|
||||||
|
|
||||||
|
AchievementNotify = require '../../templates/achievement_notify'
|
||||||
|
Achievement = require '../../models/Achievement'
|
||||||
|
|
||||||
filterKeyboardEvents = (allowedEvents, func) ->
|
filterKeyboardEvents = (allowedEvents, func) ->
|
||||||
return (splat...) ->
|
return (splat...) ->
|
||||||
e = splat[0]
|
e = splat[0]
|
||||||
|
@ -19,6 +22,41 @@ module.exports = class RootView extends CocoView
|
||||||
'click .toggle-fullscreen': 'toggleFullscreen'
|
'click .toggle-fullscreen': 'toggleFullscreen'
|
||||||
'click .auth-button': 'onClickAuthbutton'
|
'click .auth-button': 'onClickAuthbutton'
|
||||||
|
|
||||||
|
subscriptions:
|
||||||
|
'achievements:new': 'handleNewAchievements'
|
||||||
|
|
||||||
|
initialize: ->
|
||||||
|
$ ->
|
||||||
|
$.notify.addStyle 'achievement',
|
||||||
|
html: $(AchievementNotify())
|
||||||
|
|
||||||
|
showNewAchievement: (achievement) ->
|
||||||
|
imageURL = '/file/' + achievement.get('icon')
|
||||||
|
data =
|
||||||
|
title: 'Achievement Unlocked'
|
||||||
|
name: achievement.get('name')
|
||||||
|
image: $("<img src='#{imageURL}' />")
|
||||||
|
description: achievement.get('description')
|
||||||
|
|
||||||
|
options =
|
||||||
|
autoHideDelay: 15000
|
||||||
|
globalPosition: 'bottom right'
|
||||||
|
showDuration: 400
|
||||||
|
style: 'achievement'
|
||||||
|
autoHide: true
|
||||||
|
clickToHide: true
|
||||||
|
|
||||||
|
$.notify( data, options )
|
||||||
|
|
||||||
|
handleNewAchievements: (earnedAchievements) ->
|
||||||
|
# TODO performance?
|
||||||
|
_.each(earnedAchievements.models, (earnedAchievement) =>
|
||||||
|
achievement = new Achievement(earnedAchievement.get('achievement'))
|
||||||
|
achievement.fetch(
|
||||||
|
success: @showNewAchievement
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
logoutAccount: ->
|
logoutAccount: ->
|
||||||
logoutUser($('#login-email').val())
|
logoutUser($('#login-email').val())
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ AchievementSchema.methods.objectifyQuery = () ->
|
||||||
try
|
try
|
||||||
@set('query', JSON.parse(@get('query'))) if typeof @get('query') == "string"
|
@set('query', JSON.parse(@get('query'))) if typeof @get('query') == "string"
|
||||||
catch error
|
catch error
|
||||||
#log.error "Couldn't convert query string to object because of #{error}"
|
log.error "Couldn't convert query string to object because of #{error}"
|
||||||
@set('query', {})
|
@set('query', {})
|
||||||
|
|
||||||
AchievementSchema.methods.stringifyQuery = () ->
|
AchievementSchema.methods.stringifyQuery = () ->
|
||||||
|
|
|
@ -13,7 +13,7 @@ EarnedAchievementSchema = new mongoose.Schema({
|
||||||
default: false
|
default: false
|
||||||
}, {strict:false})
|
}, {strict:false})
|
||||||
|
|
||||||
# Maybe consider indexing on changed: -1 as well?
|
|
||||||
EarnedAchievementSchema.index({user: 1, achievement: 1}, {unique: true, name: 'earned achievement index'})
|
EarnedAchievementSchema.index({user: 1, achievement: 1}, {unique: true, name: 'earned achievement index'})
|
||||||
|
EarnedAchievementSchema.index({user: 1, changed: -1}, {name: 'latest '})
|
||||||
|
|
||||||
module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema)
|
module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema)
|
|
@ -167,7 +167,7 @@ module.exports = class Handler
|
||||||
projection = PROJECT
|
projection = PROJECT
|
||||||
else if req.query.project
|
else if req.query.project
|
||||||
if @modelClass.className is 'User'
|
if @modelClass.className is 'User'
|
||||||
projection = PROJECTION
|
projection = PROJECT
|
||||||
log.warn "Whoa, we haven't yet thought about public properties for User projection yet."
|
log.warn "Whoa, we haven't yet thought about public properties for User projection yet."
|
||||||
else
|
else
|
||||||
projection = {}
|
projection = {}
|
||||||
|
|
|
@ -18,8 +18,6 @@ loadAchievements = ->
|
||||||
|
|
||||||
loadAchievements()
|
loadAchievements()
|
||||||
|
|
||||||
|
|
||||||
# TODO make a difference between '$userID' and '$userObjectID' ?
|
|
||||||
module.exports = AchievablePlugin = (schema, options) ->
|
module.exports = AchievablePlugin = (schema, options) ->
|
||||||
checkForAchievement = (doc) ->
|
checkForAchievement = (doc) ->
|
||||||
collectionName = doc.constructor.modelName
|
collectionName = doc.constructor.modelName
|
||||||
|
@ -43,9 +41,11 @@ module.exports = AchievablePlugin = (schema, options) ->
|
||||||
for achievement in achievements[category]
|
for achievement in achievements[category]
|
||||||
query = achievement.get('query')
|
query = achievement.get('query')
|
||||||
isRepeatable = achievement.get('proportionalTo')?
|
isRepeatable = achievement.get('proportionalTo')?
|
||||||
console.log 'isRepeatable: ' + isRepeatable
|
|
||||||
alreadyAchieved = if isNew then false else LocalMongo.matchesQuery originalDocObj, query
|
alreadyAchieved = if isNew then false else LocalMongo.matchesQuery originalDocObj, query
|
||||||
newlyAchieved = LocalMongo.matchesQuery(docObj, query)
|
newlyAchieved = LocalMongo.matchesQuery(docObj, query)
|
||||||
|
console.log 'isRepeatable: ' + isRepeatable
|
||||||
|
console.log 'alreadyAchieved: ' + alreadyAchieved
|
||||||
|
console.log 'newlyAchieved: ' + newlyAchieved
|
||||||
|
|
||||||
userObjectID = doc.get(achievement.get('userField'))
|
userObjectID = doc.get(achievement.get('userField'))
|
||||||
userID = if _.isObject userObjectID then userObjectID.toHexString() else userObjectID # Standardize! Use strings, not ObjectId's
|
userID = if _.isObject userObjectID then userObjectID.toHexString() else userObjectID # Standardize! Use strings, not ObjectId's
|
||||||
|
|
|
@ -21,6 +21,12 @@ candidateProperties = [
|
||||||
'jobProfile', 'jobProfileApproved', 'jobProfileNotes'
|
'jobProfile', 'jobProfileApproved', 'jobProfileNotes'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
parseLiteral = (literalString) ->
|
||||||
|
return true if literalString is 'true'
|
||||||
|
return false if literalString is 'false'
|
||||||
|
return number if (number = Number(literalString)) isnt NaN
|
||||||
|
literalString
|
||||||
|
|
||||||
UserHandler = class UserHandler extends Handler
|
UserHandler = class UserHandler extends Handler
|
||||||
modelClass: User
|
modelClass: User
|
||||||
|
|
||||||
|
@ -238,11 +244,16 @@ UserHandler = class UserHandler extends Handler
|
||||||
@sendSuccess(res, documents)
|
@sendSuccess(res, documents)
|
||||||
|
|
||||||
getEarnedAchievements: (req, res, userID) ->
|
getEarnedAchievements: (req, res, userID) ->
|
||||||
query = EarnedAchievement.find(user: userID)
|
queryObject = {$query: {user: userID}, $orderby: {changed: -1}}
|
||||||
|
queryObject.$query[key] = parseLiteral(val) for key, val of req.query
|
||||||
|
query = EarnedAchievement.find(queryObject)
|
||||||
query.exec (err, documents) =>
|
query.exec (err, documents) =>
|
||||||
return @sendDatabaseError(res, err) if err?
|
return @sendDatabaseError(res, err) if err?
|
||||||
documents = (@formatEntity(req, doc) for doc in documents)
|
cleandocs = (@formatEntity(req, doc) for doc in documents)
|
||||||
@sendSuccess(res, documents)
|
for doc in documents # Maybe move this logic elsewhere
|
||||||
|
doc.set('notified', true)
|
||||||
|
doc.save()
|
||||||
|
@sendSuccess(res, cleandocs)
|
||||||
|
|
||||||
agreeToEmployerAgreement: (req, res) ->
|
agreeToEmployerAgreement: (req, res) ->
|
||||||
userIsAnonymous = req.user?.get('anonymous')
|
userIsAnonymous = req.user?.get('anonymous')
|
||||||
|
|
Reference in a new issue