diff --git a/app/models/Achievement.coffee b/app/models/Achievement.coffee index 2240345c7..22788e3b2 100644 --- a/app/models/Achievement.coffee +++ b/app/models/Achievement.coffee @@ -3,4 +3,8 @@ CocoModel = require './CocoModel' module.exports = class Achievement extends CocoModel @className: 'Achievement' @schema: require 'schemas/models/achievement' - urlRoot: '/db/achievement' \ No newline at end of file + urlRoot: '/db/achievement' + + initialize: (id) -> + super() + @set('_id', id) if id? \ No newline at end of file diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 68a836c3d..19530c174 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -1,6 +1,10 @@ storage = require 'lib/storage' 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 idAttribute: "_id" loaded: false @@ -86,6 +90,7 @@ class CocoModel extends Backbone.Model success(@, res) if success @markToRevert() if @_revertAttributes @clearBackup() + CocoModel.pollAchievements() options.error = (model, res) => error(@, res) if error return unless @notyErrors @@ -266,4 +271,14 @@ class CocoModel extends Backbone.Model getURL: -> 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 diff --git a/app/schemas/models/achievement.coffee b/app/schemas/models/achievement.coffee index f8cfa8e2b..38f4a4e50 100644 --- a/app/schemas/models/achievement.coffee +++ b/app/schemas/models/achievement.coffee @@ -21,12 +21,12 @@ MongoFindQuerySchema = type: 'object' patternProperties: #'^[-a-zA-Z0-9_]*$': - '^[-a-zA-Z0-9]*$': + '^[-a-zA-Z0-9\.]*$': oneOf: [ #{ $ref: '#/definitions/' + MongoQueryOperatorSchema.id}, { type: 'string' } ] - additionalProperties: true + additionalProperties: true # TODO make Treema accept new pattern matched keys definitions: {} MongoFindQuerySchema.definitions[MongoQueryOperatorSchema.id] = MongoQueryOperatorSchema diff --git a/app/styles/notify.sass b/app/styles/notify.sass new file mode 100644 index 000000000..ed6c5e6bd --- /dev/null +++ b/app/styles/notify.sass @@ -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 \ No newline at end of file diff --git a/app/templates/achievement_notify.jade b/app/templates/achievement_notify.jade new file mode 100644 index 000000000..6b19da70c --- /dev/null +++ b/app/templates/achievement_notify.jade @@ -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") \ No newline at end of file diff --git a/app/templates/base.jade b/app/templates/base.jade index dc5d79286..01a67687c 100644 --- a/app/templates/base.jade +++ b/app/templates/base.jade @@ -12,7 +12,7 @@ body a.navbar-brand(href='/') 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 li.play a.header-font(href='/play', data-i18n="nav.play") Levels diff --git a/app/views/kinds/RootView.coffee b/app/views/kinds/RootView.coffee index 3350dc48e..40bc765da 100644 --- a/app/views/kinds/RootView.coffee +++ b/app/views/kinds/RootView.coffee @@ -6,6 +6,9 @@ CocoView = require './CocoView' {logoutUser, me} = require('lib/auth') locale = require 'locale/locale' +AchievementNotify = require '../../templates/achievement_notify' +Achievement = require '../../models/Achievement' + filterKeyboardEvents = (allowedEvents, func) -> return (splat...) -> e = splat[0] @@ -19,6 +22,41 @@ module.exports = class RootView extends CocoView 'click .toggle-fullscreen': 'toggleFullscreen' '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: -> logoutUser($('#login-email').val()) diff --git a/server/achievements/Achievement.coffee b/server/achievements/Achievement.coffee index e7fa4a214..eff3bc857 100644 --- a/server/achievements/Achievement.coffee +++ b/server/achievements/Achievement.coffee @@ -16,7 +16,7 @@ AchievementSchema.methods.objectifyQuery = () -> try @set('query', JSON.parse(@get('query'))) if typeof @get('query') == "string" 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', {}) AchievementSchema.methods.stringifyQuery = () -> diff --git a/server/achievements/EarnedAchievement.coffee b/server/achievements/EarnedAchievement.coffee index 80eee59af..c4f017e38 100644 --- a/server/achievements/EarnedAchievement.coffee +++ b/server/achievements/EarnedAchievement.coffee @@ -13,7 +13,7 @@ EarnedAchievementSchema = new mongoose.Schema({ default: 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, changed: -1}, {name: 'latest '}) module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema) \ No newline at end of file diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index 4fa2da2d9..199ae90b8 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -167,7 +167,7 @@ module.exports = class Handler projection = PROJECT else if req.query.project if @modelClass.className is 'User' - projection = PROJECTION + projection = PROJECT log.warn "Whoa, we haven't yet thought about public properties for User projection yet." else projection = {} diff --git a/server/plugins/achievements.coffee b/server/plugins/achievements.coffee index ef29fcf26..134b76dc2 100644 --- a/server/plugins/achievements.coffee +++ b/server/plugins/achievements.coffee @@ -18,8 +18,6 @@ loadAchievements = -> loadAchievements() - -# TODO make a difference between '$userID' and '$userObjectID' ? module.exports = AchievablePlugin = (schema, options) -> checkForAchievement = (doc) -> collectionName = doc.constructor.modelName @@ -43,9 +41,11 @@ module.exports = AchievablePlugin = (schema, options) -> for achievement in achievements[category] query = achievement.get('query') isRepeatable = achievement.get('proportionalTo')? - console.log 'isRepeatable: ' + isRepeatable alreadyAchieved = if isNew then false else LocalMongo.matchesQuery originalDocObj, query newlyAchieved = LocalMongo.matchesQuery(docObj, query) + console.log 'isRepeatable: ' + isRepeatable + console.log 'alreadyAchieved: ' + alreadyAchieved + console.log 'newlyAchieved: ' + newlyAchieved userObjectID = doc.get(achievement.get('userField')) userID = if _.isObject userObjectID then userObjectID.toHexString() else userObjectID # Standardize! Use strings, not ObjectId's diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index 48895ec87..54f87aaf5 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -21,6 +21,12 @@ candidateProperties = [ '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 modelClass: User @@ -238,11 +244,16 @@ UserHandler = class UserHandler extends Handler @sendSuccess(res, documents) 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) => return @sendDatabaseError(res, err) if err? - documents = (@formatEntity(req, doc) for doc in documents) - @sendSuccess(res, documents) + cleandocs = (@formatEntity(req, doc) for doc in documents) + for doc in documents # Maybe move this logic elsewhere + doc.set('notified', true) + doc.save() + @sendSuccess(res, cleandocs) agreeToEmployerAgreement: (req, res) -> userIsAnonymous = req.user?.get('anonymous')