From 991fead0f6123f0878302b940193242829919fae Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 17 Nov 2015 11:15:56 -0800 Subject: [PATCH 01/60] Create specific campaign view for campaign selection screen. --- app/views/play/CampaignView.coffee | 2 +- server/campaigns/campaign_handler.coffee | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index 1ca3b4eb6..eccba8c54 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -33,7 +33,7 @@ class LevelSessionsCollection extends CocoCollection @url = "/db/user/#{me.id}/level.sessions?project=state.complete,levelID,state.difficulty,playtime" class CampaignsCollection extends CocoCollection - url: '/db/campaign' + url: '/db/campaign/-/overview' model: Campaign project: ['name', 'fullName', 'description', 'i18n'] diff --git a/server/campaigns/campaign_handler.coffee b/server/campaigns/campaign_handler.coffee index b06659f80..0dde1031e 100644 --- a/server/campaigns/campaign_handler.coffee +++ b/server/campaigns/campaign_handler.coffee @@ -52,8 +52,22 @@ CampaignHandler = class CampaignHandler extends Handler documents = (@formatEntity(req, doc) for doc in documents) @sendSuccess(res, documents) + getOverview: (req, res) -> + return @sendForbiddenError(res) if not @hasAccess(req) + q = @modelClass.find {}, slug: 1, adjacentCampaigns: 1, fullName: 1, description: 1, color: 1 + q.exec (err, documents) => + return @sendDatabaseError(res, err) if err + formatCampaign = (doc) => + obj = @formatEntity(req, doc) + obj.adjacentCampaigns = _.map(obj.adjacentCampaigns, (a) -> _.pick(a, ['showIfUnlocked', 'color', 'name', 'description' ])) + obj + documents = (formatCampaign(doc) for doc in documents) + @sendSuccess(res, documents) + getByRelationship: (req, res, args...) -> relationship = args[1] + return @getOverview(req,res) if args[0] is '-' and relationship is 'overview' + if relationship in ['levels', 'achievements'] projection = {} if req.query.project From 7f3426cc0977f49c60401f0b04c427631e28871a Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 17 Nov 2015 11:22:55 -0800 Subject: [PATCH 02/60] adjacentCampaigns should be an object not an array. --- server/campaigns/campaign_handler.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/campaigns/campaign_handler.coffee b/server/campaigns/campaign_handler.coffee index 0dde1031e..08bfe6623 100644 --- a/server/campaigns/campaign_handler.coffee +++ b/server/campaigns/campaign_handler.coffee @@ -59,7 +59,7 @@ CampaignHandler = class CampaignHandler extends Handler return @sendDatabaseError(res, err) if err formatCampaign = (doc) => obj = @formatEntity(req, doc) - obj.adjacentCampaigns = _.map(obj.adjacentCampaigns, (a) -> _.pick(a, ['showIfUnlocked', 'color', 'name', 'description' ])) + obj.adjacentCampaigns = _.mapValues(obj.adjacentCampaigns, (a) -> _.pick(a, ['showIfUnlocked', 'color', 'name', 'description' ])) obj documents = (formatCampaign(doc) for doc in documents) @sendSuccess(res, documents) From 7a86f754dfba67bf395df7158a3252997e411c92 Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 17 Nov 2015 11:27:08 -0800 Subject: [PATCH 03/60] Add flavor to overworld. --- app/views/play/CampaignView.coffee | 2 +- server/campaigns/campaign_handler.coffee | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index eccba8c54..18e475f3f 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -33,7 +33,7 @@ class LevelSessionsCollection extends CocoCollection @url = "/db/user/#{me.id}/level.sessions?project=state.complete,levelID,state.difficulty,playtime" class CampaignsCollection extends CocoCollection - url: '/db/campaign/-/overview' + url: '/db/campaign/-/overworld' model: Campaign project: ['name', 'fullName', 'description', 'i18n'] diff --git a/server/campaigns/campaign_handler.coffee b/server/campaigns/campaign_handler.coffee index 08bfe6623..31f8c5806 100644 --- a/server/campaigns/campaign_handler.coffee +++ b/server/campaigns/campaign_handler.coffee @@ -52,7 +52,7 @@ CampaignHandler = class CampaignHandler extends Handler documents = (@formatEntity(req, doc) for doc in documents) @sendSuccess(res, documents) - getOverview: (req, res) -> + getOverworld: (req, res) -> return @sendForbiddenError(res) if not @hasAccess(req) q = @modelClass.find {}, slug: 1, adjacentCampaigns: 1, fullName: 1, description: 1, color: 1 q.exec (err, documents) => @@ -66,7 +66,7 @@ CampaignHandler = class CampaignHandler extends Handler getByRelationship: (req, res, args...) -> relationship = args[1] - return @getOverview(req,res) if args[0] is '-' and relationship is 'overview' + return @getOverworld(req,res) if args[0] is '-' and relationship is 'overworld' if relationship in ['levels', 'achievements'] projection = {} From 32861b025a96c6acfe9714a1460aba5ad45a97b0 Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 17 Nov 2015 14:57:12 -0800 Subject: [PATCH 04/60] Support sending performance information to stats. --- package.json | 1 + server/commons/perfmon.coffee | 43 +++++++++++++++++++++++++++++++++++ server_config.coffee | 5 ++++ server_setup.coffee | 4 ++++ 4 files changed, 53 insertions(+) create mode 100644 server/commons/perfmon.coffee diff --git a/package.json b/package.json index 36a5dc034..0dfd4bc87 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "mongoose-cache": "~0.1.4", "node-force-domain": "~0.1.0", "node-gyp": "~0.13.0", + "node-statsd": "^0.1.1", "passport": "0.1.x", "passport-local": "0.1.x", "redis": "", diff --git a/server/commons/perfmon.coffee b/server/commons/perfmon.coffee new file mode 100644 index 000000000..e03b6145f --- /dev/null +++ b/server/commons/perfmon.coffee @@ -0,0 +1,43 @@ +fs = require 'fs' +path = require 'path' +config = require '../../server_config' +StatsD = require 'node-statsd' + +if config.statsd + realClient = new StatsD(config.statsd) +else + mock = new StatsD(mock: true) + +exports.client = realClient or mock +exports.middleware = (req, res, next) -> + + req.statsd = exports.client + if realClient + time = process.hrtime(); + cleanup = -> + res.removeListener 'finish', recordMetrics + res.removeListener 'error', cleanup + res.removeListener 'close', cleanup + + recordMetrics = -> + diff = process.hrtime(time); + ms = (diff[0] * 1000 + diff[1] / 1e6); + path = req.route?.path or '/*' + stat = req.method + "." + path.replace /[^A-Za-z0-9]+/g, '_' + realClient.timing stat, ms + + + res.once 'finish', recordMetrics + res.once 'error', cleanup + res.once 'close', cleanup + else + req.statsd = mock + + next() unless not next + +exports.trace = (name, callback) -> + return callback unless realClient + time = process.hrtime() + (args...) -> + realClient.timing name, ms + return callback.apply(this, args) \ No newline at end of file diff --git a/server_config.coffee b/server_config.coffee index 4507a8d7e..b8468f4c9 100644 --- a/server_config.coffee +++ b/server_config.coffee @@ -84,5 +84,10 @@ if not config.unittest and not config.isProduction # change artificially slow down non-static requests for testing config.slow_down = false +if process.env.COCO_STATSD_HOST + config.statsd = + host: process.env.COCO_STATSD_HOST + port: process.env.COCO_STATSD_PORT or 8125 + prefix: process.env.COCO_STATSD_PREFIX or '' module.exports = config diff --git a/server_setup.coffee b/server_setup.coffee index ee327a830..230f7c56b 100644 --- a/server_setup.coffee +++ b/server_setup.coffee @@ -8,6 +8,7 @@ compressible = require 'compressible' geoip = require 'geoip-lite' database = require './server/commons/database' +perfmon = require './server/commons/perfmon' baseRoute = require './server/routes/base' user = require './server/users/user_handler' logging = require './server/commons/logging' @@ -127,8 +128,11 @@ setupRedirectMiddleware = (app) -> nameOrID = req.path.split('/')[3] res.redirect 301, "/user/#{nameOrID}/profile" +setupPerfMonMiddleware = (app) -> + app.use perfmon.middleware exports.setupMiddleware = (app) -> + setupPerfMonMiddleware app setupCountryRedirectMiddleware app, "china", "CN", "zh", "tokyo" setupCountryRedirectMiddleware app, "brazil", "BR", "pt-BR", "saoPaulo" setupMiddlewareToSendOldBrowserWarningWhenPlayersViewLevelDirectly app From 7a377350e8384ab8466083383b7b3bef0cff0879 Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 17 Nov 2015 15:19:17 -0800 Subject: [PATCH 05/60] Tabs -> Spaces --- server/commons/perfmon.coffee | 54 +++++++++++++++++------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/server/commons/perfmon.coffee b/server/commons/perfmon.coffee index e03b6145f..8d66ba853 100644 --- a/server/commons/perfmon.coffee +++ b/server/commons/perfmon.coffee @@ -4,40 +4,40 @@ config = require '../../server_config' StatsD = require 'node-statsd' if config.statsd - realClient = new StatsD(config.statsd) + realClient = new StatsD(config.statsd) else - mock = new StatsD(mock: true) + mock = new StatsD(mock: true) exports.client = realClient or mock exports.middleware = (req, res, next) -> - req.statsd = exports.client - if realClient - time = process.hrtime(); - cleanup = -> - res.removeListener 'finish', recordMetrics - res.removeListener 'error', cleanup - res.removeListener 'close', cleanup + req.statsd = exports.client + if realClient + time = process.hrtime(); + cleanup = -> + res.removeListener 'finish', recordMetrics + res.removeListener 'error', cleanup + res.removeListener 'close', cleanup - recordMetrics = -> - diff = process.hrtime(time); - ms = (diff[0] * 1000 + diff[1] / 1e6); - path = req.route?.path or '/*' - stat = req.method + "." + path.replace /[^A-Za-z0-9]+/g, '_' - realClient.timing stat, ms - + recordMetrics = -> + diff = process.hrtime(time); + ms = (diff[0] * 1000 + diff[1] / 1e6); + path = req.route?.path or '/*' + stat = req.method + "." + path.replace /[^A-Za-z0-9]+/g, '_' + realClient.timing stat, ms + - res.once 'finish', recordMetrics - res.once 'error', cleanup - res.once 'close', cleanup - else - req.statsd = mock + res.once 'finish', recordMetrics + res.once 'error', cleanup + res.once 'close', cleanup + else + req.statsd = mock - next() unless not next + next() unless not next exports.trace = (name, callback) -> - return callback unless realClient - time = process.hrtime() - (args...) -> - realClient.timing name, ms - return callback.apply(this, args) \ No newline at end of file + return callback unless realClient + time = process.hrtime() + (args...) -> + realClient.timing name, ms + return callback.apply(this, args) \ No newline at end of file From 01c9948b5b927d7f97d66dc6e953815fe38df9b1 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 17 Nov 2015 15:23:34 -0800 Subject: [PATCH 06/60] Favor complete over incomplete sessions in CampaignView flag status --- app/views/play/CampaignView.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index 18e475f3f..9573d97f6 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -427,7 +427,8 @@ module.exports = class CampaignView extends RootView onSessionsLoaded: (e) -> return if @editorMode for session in @sessions.models - @levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started' + unless @levelStatusMap[session.get('levelID')] is 'complete' # Don't overwrite a complete session with an incomplete one + @levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started' @levelDifficultyMap[session.get('levelID')] = session.get('state').difficulty if session.get('state')?.difficulty @render() @loadUserPollsRecord() unless me.get 'anonymous' From 9b990ba452b1535f5d960ed7a851453e196c55cb Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 17 Nov 2015 15:23:34 -0800 Subject: [PATCH 07/60] Mark mirror match multiplayer levels complete on success --- app/lib/LevelBus.coffee | 6 ++++++ app/schemas/subscriptions/tome.coffee | 1 + 2 files changed, 7 insertions(+) diff --git a/app/lib/LevelBus.coffee b/app/lib/LevelBus.coffee index a3bbb577a..20e053527 100644 --- a/app/lib/LevelBus.coffee +++ b/app/lib/LevelBus.coffee @@ -20,6 +20,7 @@ module.exports = class LevelBus extends Bus 'tome:spell-changed': 'onSpellChanged' 'tome:spell-created': 'onSpellCreated' 'tome:cast-spells': 'onCastSpells' + 'tome:winnability-updated': 'onWinnabilityUpdated' 'application:idle-changed': 'onIdleChanged' 'goal-manager:new-goal-states': 'onNewGoalStates' 'god:new-world-created': 'onNewWorldCreated' @@ -119,6 +120,11 @@ module.exports = class LevelBus extends Bus @changedSessionProperties.state = true @saveSession() + onWinnabilityUpdated: (e) -> + return unless @onPoint() and e.winnable + return unless e.level.get('slug') in ['ace-of-coders'] # Mirror matches don't otherwise show victory, so we win here. + @onVictory() + onNewWorldCreated: (e) -> return unless @onPoint() # Record the flag history. diff --git a/app/schemas/subscriptions/tome.coffee b/app/schemas/subscriptions/tome.coffee index 6dd258cf4..7840b6e95 100644 --- a/app/schemas/subscriptions/tome.coffee +++ b/app/schemas/subscriptions/tome.coffee @@ -136,6 +136,7 @@ module.exports = 'tome:winnability-updated': c.object {title: 'Winnability Updated', description: 'When we think we can now win (or can no longer win), we may want to emphasize the submit button versus the run button (or vice versa), so this fires when we get new goal states (even preloaded goal states) suggesting success or failure change.', required: ['winnable']}, winnable: {type: 'boolean'} + level: {type: 'object'} # Problem Alert 'tome:show-problem-alert': c.object {title: 'Show Problem Alert', description: 'A problem alert needs to be shown.', required: ['problem']}, From a725cd70d82ab99fbb909fbe1011b4f8a16a7143 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 17 Nov 2015 15:23:35 -0800 Subject: [PATCH 08/60] Update comment color to Robin's favorite blue --- app/styles/play/level/tome/spell.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/play/level/tome/spell.sass b/app/styles/play/level/tome/spell.sass index 5a609e766..df05d8dab 100644 --- a/app/styles/play/level/tome/spell.sass +++ b/app/styles/play/level/tome/spell.sass @@ -190,7 +190,7 @@ // border-bottom: 1px dotted rgba(0, 51, 255, 0.25) .ace_text-layer .ace_comment - color: rgb(78, 38, 226) + color: #1900AD .ace_text-layer .ace_variable // https://github.com/codecombat/codecombat/issues/6 From a5fc31dbb3560ed6f619e83d60f7566a43657c92 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 17 Nov 2015 15:23:35 -0800 Subject: [PATCH 09/60] Allow multiplayer levels to earn achievements --- .../play/level/modal/hero-victory-modal.jade | 79 ++++++++++--------- app/views/core/RootView.coffee | 2 +- .../play/level/modal/HeroVictoryModal.coffee | 10 +-- .../play/modal/PlayAchievementsModal.coffee | 5 +- 4 files changed, 50 insertions(+), 46 deletions(-) diff --git a/app/templates/play/level/modal/hero-victory-modal.jade b/app/templates/play/level/modal/hero-victory-modal.jade index 2412c10e2..f46ea0d9d 100644 --- a/app/templates/play/level/modal/hero-victory-modal.jade +++ b/app/templates/play/level/modal/hero-victory-modal.jade @@ -26,48 +26,49 @@ block modal-body-content textarea(data-i18n="[placeholder]play_level.victory_review_placeholder") .clearfix - for achievement in achievements - - var animate = achievement.completed && !achievement.completedAWhileAgo - .achievement-panel(class=achievement.completedAWhileAgo ? 'earned' : '' data-achievement-id=achievement.id data-animate=animate) - - var rewards = achievement.get('rewards') || {}; - - div.achievement-description= achievement.description - - div.achievement-rewards - - var worth = achievement.worth; - - var previousWorth = achievement.previousWorth; - - var gems = achievement.gems; - - var previousGems = achievement.previousGems; - if worth - .reward-panel.numerical.xp(data-number=worth, data-number-unit='xp', data-previous-number=previousWorth || 0) - .reward-image-container(class=animate ? 'pending-reward-image' : 'show') - img(src="/images/pages/play/level/modal/reward_icon_xp.png") - .reward-text= animate ? '+0' : '+'+worth + if level.get('type', true) === 'hero' || level.get('type') == 'hero-ladder' + for achievement in achievements + - var animate = achievement.completed && !achievement.completedAWhileAgo + .achievement-panel(class=achievement.completedAWhileAgo ? 'earned' : '' data-achievement-id=achievement.id data-animate=animate) + - var rewards = achievement.get('rewards') || {}; - if gems - .reward-panel.numerical.gems(data-number=gems, data-number-unit='gem', data-previous-number=previousGems || 0) - .reward-image-container(class=animate ? 'pending-reward-image' : 'show') - img(src="/images/pages/play/level/modal/reward_icon_gems.png") - .reward-text= animate ? '+0' : '+'+gems - - if rewards.heroes - for hero in rewards.heroes - - var hero = thangTypes[hero]; - .reward-panel.hero(data-hero-thang-type=hero.get('original')) - .reward-image-container(class=animate ? 'pending-reward-image' : 'show') - img(src=hero.getPortraitURL()) - .reward-text= animate ? 'New Hero' : hero.get('name') + div.achievement-description= achievement.description - if rewards.items - for item in rewards.items - - var item = thangTypes[item]; - .reward-panel.item(data-item-thang-type=item.get('original')) + div.achievement-rewards + - var worth = achievement.worth; + - var previousWorth = achievement.previousWorth; + - var gems = achievement.gems; + - var previousGems = achievement.previousGems; + if worth + .reward-panel.numerical.xp(data-number=worth, data-number-unit='xp', data-previous-number=previousWorth || 0) .reward-image-container(class=animate ? 'pending-reward-image' : 'show') - img(src=item.getPortraitURL()) - if animate - .reward-text(data-i18n="play_level.victory_new_item") New Item - else - .reward-text= i18n(item.attributes, 'name') + img(src="/images/pages/play/level/modal/reward_icon_xp.png") + .reward-text= animate ? '+0' : '+'+worth + + if gems + .reward-panel.numerical.gems(data-number=gems, data-number-unit='gem', data-previous-number=previousGems || 0) + .reward-image-container(class=animate ? 'pending-reward-image' : 'show') + img(src="/images/pages/play/level/modal/reward_icon_gems.png") + .reward-text= animate ? '+0' : '+'+gems + + if rewards.heroes + for hero in rewards.heroes + - var hero = thangTypes[hero]; + .reward-panel.hero(data-hero-thang-type=hero.get('original')) + .reward-image-container(class=animate ? 'pending-reward-image' : 'show') + img(src=hero.getPortraitURL()) + .reward-text= animate ? 'New Hero' : hero.get('name') + + if rewards.items + for item in rewards.items + - var item = thangTypes[item]; + .reward-panel.item(data-item-thang-type=item.get('original')) + .reward-image-container(class=animate ? 'pending-reward-image' : 'show') + img(src=item.getPortraitURL()) + if animate + .reward-text(data-i18n="play_level.victory_new_item") New Item + else + .reward-text= i18n(item.attributes, 'name') block modal-footer-content #totals diff --git a/app/views/core/RootView.coffee b/app/views/core/RootView.coffee index fa7977eef..34235d9de 100644 --- a/app/views/core/RootView.coffee +++ b/app/views/core/RootView.coffee @@ -38,7 +38,7 @@ module.exports = class RootView extends CocoView showNewAchievement: (achievement, earnedAchievement) -> earnedAchievement.set('notified', true) earnedAchievement.patch() - return if achievement.get('collection') is 'level.sessions' + return if achievement.get('collection') is 'level.sessions' and not achievement.get('query')?.team #return if @isIE() # Some bugs in IE right now, TODO fix soon! # Maybe working now with not caching achievement fetches in CocoModel? new AchievementPopup achievement: achievement, earnedAchievement: earnedAchievement diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee index 3ec4181de..4eeaf3782 100644 --- a/app/views/play/level/modal/HeroVictoryModal.coffee +++ b/app/views/play/level/modal/HeroVictoryModal.coffee @@ -48,7 +48,7 @@ module.exports = class HeroVictoryModal extends ModalView @session = options.session @level = options.level @thangTypes = {} - if @level.get('type', true) is 'hero' + if @level.get('type', true) in ['hero', 'hero-ladder', 'course', 'course-ladder'] achievements = new CocoCollection([], { url: "/db/achievement?related=#{@session.get('level').original}" model: Achievement @@ -212,7 +212,7 @@ module.exports = class HeroVictoryModal extends ModalView afterRender: -> super() - @$el.toggleClass 'with-achievements', @level.get('type', true) is 'hero' + @$el.toggleClass 'with-achievements', @level.get('type', true) in ['hero', 'hero-ladder'] return unless @supermodel.finished() @playSelectionSound hero, true for original, hero of @thangTypes # Preload them @updateSavingProgressStatus() @@ -222,8 +222,8 @@ module.exports = class HeroVictoryModal extends ModalView @insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view') initializeAnimations: -> - if @level.get('type', true) is 'hero' - @updateXPBars 0 + return @endSequentialAnimations() unless @level.get('type', true) in ['hero', 'hero-ladder'] + @updateXPBars 0 #playVictorySound = => @playSound 'victory-title-appear' # TODO: actually add this @$el.find('#victory-header').delay(250).queue(-> $(@).removeClass('out').dequeue() @@ -253,7 +253,7 @@ module.exports = class HeroVictoryModal extends ModalView beginSequentialAnimations: -> return if @destroyed - return unless @level.get('type', true) is 'hero' + return unless @level.get('type', true) in ['hero', 'hero-ladder'] @sequentialAnimatedPanels = _.map(@animatedPanels.find('.reward-panel'), (panel) -> { number: $(panel).data('number') previousNumber: $(panel).data('previous-number') diff --git a/app/views/play/modal/PlayAchievementsModal.coffee b/app/views/play/modal/PlayAchievementsModal.coffee index 8cba2f08e..3375f6c59 100644 --- a/app/views/play/modal/PlayAchievementsModal.coffee +++ b/app/views/play/modal/PlayAchievementsModal.coffee @@ -31,6 +31,7 @@ module.exports = class PlayAchievementsModal extends ModalView 'rewards' 'collection' 'function' + 'query' ]) earnedAchievementsFetcher = new CocoCollection([], {url: '/db/earned_achievement', model: EarnedAchievement}) @@ -72,8 +73,10 @@ module.exports = class PlayAchievementsModal extends ModalView @onEverythingLoaded() onEverythingLoaded: => - @achievements.set(@achievements.filter((m) -> m.get('collection') isnt 'level.sessions')) + console.log 'got achievements', m.attributes for m in @achievements.models + @achievements.set(@achievements.filter((m) -> m.get('collection') isnt 'level.sessions' or m.get('query')?.team)) for achievement in @achievements.models + console.log 'kept achievement', achievement.attributes if earned = @earnedMap[achievement.id] achievement.earned = earned achievement.earnedDate = earned.getCreationDate() From f96a0a018cd37fc179c0956fa3e25b16fada0ff2 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 17 Nov 2015 15:23:35 -0800 Subject: [PATCH 10/60] Track ladderAchievementDifficulty in multiplayer human level sessions --- server/queues/scoring/scoringUtils.coffee | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/server/queues/scoring/scoringUtils.coffee b/server/queues/scoring/scoringUtils.coffee index 264ee52df..eaa5cec1b 100644 --- a/server/queues/scoring/scoringUtils.coffee +++ b/server/queues/scoring/scoringUtils.coffee @@ -52,12 +52,13 @@ module.exports.calculateSessionScores = (callback) -> retrieveOldSessionData = (sessionID, callback) -> formatOldScoreObject = (session) => - oldScoreObject = + oldScoreObject = standardDeviation: session.standardDeviation ? 25/3 meanStrength: session.meanStrength ? 25 totalScore: session.totalScore ? (25 - 1.8*(25/3)) id: sessionID submittedCodeLanguage: session.submittedCodeLanguage + ladderAchievementDifficulty: session.ladderAchievementDifficulty if session.leagues?.length _.find(@clientResponseObject.sessions, sessionID: sessionID).leagues = session.leagues oldScoreObject.leagues = [] @@ -74,7 +75,7 @@ retrieveOldSessionData = (sessionID, callback) -> return formatOldScoreObject @levelSession if sessionID is @levelSession?._id # No need to fetch again query = _id: sessionID - selection = 'standardDeviation meanStrength totalScore submittedCodeLanguage leagues' + selection = 'standardDeviation meanStrength totalScore submittedCodeLanguage leagues ladderAchievementDifficulty' LevelSession.findOne(query).select(selection).lean().exec (err, session) -> return callback err, {'error': 'There was an error retrieving the session.'} if err? callback err, formatOldScoreObject session @@ -150,6 +151,13 @@ module.exports.addMatchToSessionsAndUpdate = (newScoreObject, callback) -> async.each sessionIDs, updateMatchesInSession.bind(@, matchObject), (err) -> callback err +ladderBenchmarkAIs = + '564ba6cea33967be1312ae59': 0 + '564ba830a33967be1312ae61': 1 + '564ba91aa33967be1312ae65': 2 + '564ba95ca33967be1312ae69': 3 + '564ba9b7a33967be1312ae6d': 4 + updateMatchesInSession = (matchObject, sessionID, callback) -> currentMatchObject = {} currentMatchObject.date = matchObject.date @@ -163,6 +171,11 @@ updateMatchesInSession = (matchObject, sessionID, callback) -> #currentMatchObject.randomSeed = parseInt(@clientResponseObject.randomSeed or 0, 10) # Uncomment when actively debugging simulation mismatches sessionUpdateObject = @levelSessionUpdates[sessionID] sessionUpdateObject.$push.matches = {$each: [currentMatchObject], $slice: -200} + if currentMatchObject.metrics.rank is 0 and defeatedAI = ladderBenchmarkAIs[currentMatchObject.opponents[0].userID] + mySession = _.find @clientResponseObject.sessions, sessionID: sessionID + newLadderAchievementDifficulty = Math.max defeatedAI, mySession.ladderAchievementDifficulty || 0 + if newLadderAchievementDifficulty isnt mySession.ladderAchievementDifficulty + sessionUpdateObject.ladderAchievementDifficulty = newLadderAchievementDifficulty myScoreObject = @newScoresObject[sessionID] opponentSession = _.find @clientResponseObject.sessions, (session) -> session.sessionID isnt sessionID From f1b0b4f9c5f813575f6dcd435b9d31cd35a36f61 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 17 Nov 2015 15:24:33 -0800 Subject: [PATCH 11/60] Allow achievement query editing in achievement editor --- app/schemas/models/achievement.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/schemas/models/achievement.coffee b/app/schemas/models/achievement.coffee index fdf783754..c0cd52bc0 100644 --- a/app/schemas/models/achievement.coffee +++ b/app/schemas/models/achievement.coffee @@ -26,6 +26,7 @@ MongoFindQuerySchema = {type: 'object'} {type: 'boolean'} ] + properties: {} additionalProperties: false definitions: {} From 040f7c94abe54a586fcb346e457db57c775794d2 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 17 Nov 2015 15:25:22 -0800 Subject: [PATCH 12/60] Track ladderAchievementDifficulty in multiplayer human level sessions --- app/schemas/models/level_session.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/schemas/models/level_session.coffee b/app/schemas/models/level_session.coffee index e2caf8b87..1343aff15 100644 --- a/app/schemas/models/level_session.coffee +++ b/app/schemas/models/level_session.coffee @@ -169,6 +169,11 @@ _.extend LevelSessionSchema.properties, chat: type: 'array' + ladderAchievementDifficulty: + type: 'integer' + minimum: 0 + description: 'What ogre AI difficulty, 0-4, this human session has beaten in a multiplayer arena.' + meanStrength: type: 'number' From a0038602a1c96f72b2a4c8ac50cd2ee43939647e Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 17 Nov 2015 15:26:10 -0800 Subject: [PATCH 13/60] Fix some course level submit button behavior, also help mirror session scoring --- app/views/play/level/tome/CastButtonView.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/play/level/tome/CastButtonView.coffee b/app/views/play/level/tome/CastButtonView.coffee index b7b491e32..c4d6c5f3e 100644 --- a/app/views/play/level/tome/CastButtonView.coffee +++ b/app/views/play/level/tome/CastButtonView.coffee @@ -56,7 +56,7 @@ module.exports = class CastButtonView extends CocoView @$el.find('.submit-button').hide() # Hide Submit for the first few until they run it once. if @options.session.get('state')?.complete and @options.level.get 'hidesRealTimePlayback' @$el.find('.done-button').show() - if @options.level.get('slug') is 'thornbush-farm'# and not @options.session.get('state')?.complete + if @options.level.get('slug') in ['course-thornbush-farm', 'thornbush-farm'] @$el.find('.submit-button').hide() # Hide submit until first win so that script can explain it. @updateReplayability() @updateLadderSubmissionViews() @@ -114,17 +114,17 @@ module.exports = class CastButtonView extends CocoView return if @winnable is winnable @winnable = winnable @$el.toggleClass 'winnable', @winnable - Backbone.Mediator.publish 'tome:winnability-updated', winnable: @winnable + Backbone.Mediator.publish 'tome:winnability-updated', winnable: @winnable, level: @options.level if @options.level.get 'hidesRealTimePlayback' @$el.find('.done-button').toggle @winnable - else if @winnable and @options.level.get('slug') is 'thornbush-farm' + else if @winnable and @options.level.get('slug') in ['course-thornbush-farm', 'thornbush-farm'] @$el.find('.submit-button').show() # Hide submit until first win so that script can explain it. onGoalsCalculated: (e) -> # When preloading, with real-time playback enabled, we highlight the submit button when we think they'll win. return unless e.preload return if @options.level.get 'hidesRealTimePlayback' - return if @options.level.get('slug') is 'thornbush-farm' # Don't show it until they actually win for this first one. + return if @options.level.get('slug') in ['course-thornbush-farm', 'thornbush-farm'] # Don't show it until they actually win for this first one. @onNewGoalStates e updateCastButton: -> From 6a54c7cf54a79b5c8acbabbd0d1e3446a4331774 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Tue, 17 Nov 2015 16:09:29 -0800 Subject: [PATCH 14/60] Add classroom name, description, and language editing, and classroom language infrastructure --- app/schemas/models/classroom.schema.coffee | 2 ++ app/styles/courses/teacher-courses-view.sass | 5 +++ .../courses/classroom-settings-modal.jade | 24 +++++++++++++ app/templates/courses/course-details.jade | 31 +---------------- .../courses/teacher-courses-view.jade | 13 ++++++- .../courses/ClassroomSettingsModal.coffee | 26 ++++++++++++++ app/views/courses/CourseDetailsView.coffee | 14 -------- app/views/courses/TeacherCoursesView.coffee | 9 +++++ server/classrooms/Classroom.coffee | 1 + server/levels/level_handler.coffee | 15 +++++--- test/server/functional/level.spec.coffee | 34 +++++++++++++------ 11 files changed, 114 insertions(+), 60 deletions(-) create mode 100644 app/templates/courses/classroom-settings-modal.jade create mode 100644 app/views/courses/ClassroomSettingsModal.coffee diff --git a/app/schemas/models/classroom.schema.coffee b/app/schemas/models/classroom.schema.coffee index 49d655076..4b12a57fb 100644 --- a/app/schemas/models/classroom.schema.coffee +++ b/app/schemas/models/classroom.schema.coffee @@ -8,6 +8,8 @@ _.extend ClassroomSchema.properties, ownerID: c.objectId() description: {type: 'string'} code: c.shortString(title: "Unique code to redeem") + aceConfig: + language: {type: 'string', 'enum': ['python', 'javascript']} c.extendBasicProperties ClassroomSchema, 'Classroom' diff --git a/app/styles/courses/teacher-courses-view.sass b/app/styles/courses/teacher-courses-view.sass index f0bcbabf9..c1ce34b6f 100644 --- a/app/styles/courses/teacher-courses-view.sass +++ b/app/styles/courses/teacher-courses-view.sass @@ -3,6 +3,11 @@ img.media-object width: 300px + + .edit-classroom-small + cursor: pointer + &:hover + color: grey #fixed-area position: fixed diff --git a/app/templates/courses/classroom-settings-modal.jade b/app/templates/courses/classroom-settings-modal.jade new file mode 100644 index 000000000..88064bcd1 --- /dev/null +++ b/app/templates/courses/classroom-settings-modal.jade @@ -0,0 +1,24 @@ +extends /templates/core/modal-base + +block modal-header-content + button.close(data-dismiss='modal') + span × + h3.modal-title(data-i18n="courses.edit_settings1") + +block modal-body-content + .form + .form-group + label(data-i18n="courses.title") + input.form-control.settings-name-input(type='text', value="#{view.classroom.get('name') || ''}") + .form-group + label(data-i18n="courses.description") + textarea.form-control.settings-description-input(rows=2)= view.classroom.get('description') + .form-group + label(data-i18n="choose_hero.programming_language") + select.form-control#programming-language-select + - var aceConfig = view.classroom.get('aceConfig') || {}; + option(value="python", selected=aceConfig.language==='python') Python + option(value="javascript", selected=aceConfig.language==='javascript') JavaScript + +block modal-footer-content + button#save-settings-btn.btn(data-i18n="common.save_changes") \ No newline at end of file diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade index dcd5ea98d..5038ada6f 100644 --- a/app/templates/courses/course-details.jade +++ b/app/templates/courses/course-details.jade @@ -53,12 +53,7 @@ block content if courseInstance.get('description') each line in courseInstance.get('description').split('\n') div= line - // TODO: migrate these settings to classrooms - //if adminMode && courseInstance - // +settings-dialog - // p - // button.btn.btn-xs(data-toggle='modal', data-target='#settingsModal', data-i18n="courses.edit_settings") - + div.well.well-sm(role='tabpanel') ul.nav.nav-pills(role='tablist') if adminMode @@ -292,27 +287,3 @@ mixin levels-tab if levelConceptMap[levelID][concept] span.spr.progress-level-cell.progress-level-cell-not-started(data-i18n="concepts." + concept) -mixin settings-dialog - .modal#settingsModal - .modal-dialog - .modal-header - button.close(data-dismiss='modal') - span × - h3.modal-title(data-i18n="courses.edit_settings1") - .modal-body - .form - .form-group - label(data-i18n="courses.title") - input.form-control.settings-name-input(type='text', value="#{courseInstance.get('name') || ''}") - .form-group - label(data-i18n="courses.description") - textarea.form-control.settings-description-input(rows=2)= courseInstance.get('description') - .form-group - label(data-i18n="choose_hero.programming_language") - select.form-control#programming-language-select - - var aceConfig = view.courseInstance.get('aceConfig') || {}; - option(value="python", selected=aceConfig.language==='python') Python - option(value="javascript", selected=aceConfig.language==='javascript') JavaScript - - .modal-footer - button.btn.btn-save-settings(data-i18n="common.save_changes") diff --git a/app/templates/courses/teacher-courses-view.jade b/app/templates/courses/teacher-courses-view.jade index 4724fb692..196515000 100644 --- a/app/templates/courses/teacher-courses-view.jade +++ b/app/templates/courses/teacher-courses-view.jade @@ -102,7 +102,15 @@ block content a.btn.btn-default.btn-xs(href="/courses/purchase") Add for classroom in view.classrooms.models - h2= classroom.get('name') + h2 + span.spr= classroom.get('name') + - var language = (classroom.get('aceConfig') || {}).language || 'python'; + if language === 'python' + img(src="/images/common/code_languages/python_icon.png") + if language === 'javascript' + img(src="/images/common/code_languages/javascript_icon.png") + small.spl.edit-classroom-small(data-classroom-id=classroom.id) + span.glyphicon.glyphicon-pencil - var courseInstances = view.courseInstances.where({classroomID: classroom.id}) @@ -111,6 +119,9 @@ block content .progress-bar(style="width: 100%") else + - var description = classroom.get('description'); + if description + p= description table.table tr th Student diff --git a/app/views/courses/ClassroomSettingsModal.coffee b/app/views/courses/ClassroomSettingsModal.coffee new file mode 100644 index 000000000..bd4a5eb1b --- /dev/null +++ b/app/views/courses/ClassroomSettingsModal.coffee @@ -0,0 +1,26 @@ +ModalView = require 'views/core/ModalView' +template = require 'templates/courses/classroom-settings-modal' + +module.exports = class AddLevelSystemModal extends ModalView + id: 'classroom-settings-modal' + template: template + + events: + 'click #save-settings-btn': 'onClickSaveSettingsButton' + + initialize: (options) -> + @classroom = options.classroom + + onClickSaveSettingsButton: -> + return unless @classroom + if name = $('.settings-name-input').val() + @classroom.set('name', name) + description = $('.settings-description-input').val() + @classroom.set('description', description) + @classroom.set('aceConfig', { + language: @$('#programming-language-select').val() + }) + @classroom.patch() + @hide() + + \ No newline at end of file diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee index 2c66af160..aa6a0bd2a 100644 --- a/app/views/courses/CourseDetailsView.coffee +++ b/app/views/courses/CourseDetailsView.coffee @@ -19,7 +19,6 @@ module.exports = class CourseDetailsView extends RootView events: 'change .progress-expand-checkbox': 'onCheckExpandedProgress' 'click .btn-play-level': 'onClickPlayLevel' - 'click .btn-save-settings': 'onClickSaveSettings' 'click .btn-select-instance': 'onClickSelectInstance' 'click .progress-member-header': 'onClickMemberHeader' 'click .progress-header': 'onClickProgressHeader' @@ -228,19 +227,6 @@ module.exports = class CourseDetailsView extends RootView getLevelURL: (levelSlug) -> "/play/level/#{levelSlug}?course=#{@courseID}&course-instance=#{@courseInstanceID}" - onClickSaveSettings: (e) -> - return unless @courseInstance - if name = $('.settings-name-input').val() - @courseInstance.set('name', name) - description = $('.settings-description-input').val() - console.log 'onClickSaveSettings', description - @courseInstance.set('description', description) - @courseInstance.set('aceConfig', { - language: @$('#programming-language-select').val() - }) - @courseInstance.patch() - $('#settingsModal').modal('hide') - onClickSelectInstance: (e) -> courseInstanceID = $('.select-instance').val() @noCourseInstanceSelected = false diff --git a/app/views/courses/TeacherCoursesView.coffee b/app/views/courses/TeacherCoursesView.coffee index 26cf74ad4..8f6184efa 100644 --- a/app/views/courses/TeacherCoursesView.coffee +++ b/app/views/courses/TeacherCoursesView.coffee @@ -11,6 +11,7 @@ RootView = require 'views/core/RootView' template = require 'templates/courses/teacher-courses-view' utils = require 'core/utils' InviteToClassroomModal = require 'views/courses/InviteToClassroomModal' +ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal' module.exports = class TeacherCoursesView extends RootView id: 'teacher-courses-view' @@ -22,6 +23,7 @@ module.exports = class TeacherCoursesView extends RootView 'click .course-instance-membership-checkbox': 'onClickCourseInstanceMembershipCheckbox' 'click #save-changes-btn': 'onClickSaveChangesButton' 'click #manage-tab-link': 'onClickManageTabLink' + 'click .edit-classroom-small': 'onClickEditClassroomSmall' constructor: (options) -> super(options) @@ -71,6 +73,13 @@ module.exports = class TeacherCoursesView extends RootView @renderSelectors('#manage-tab-pane') @$('#manage-tab-pane').toggleClass('active', isActive) + onClickEditClassroomSmall: (e) -> + classroomID = $(e.target).closest('small').data('classroom-id') + classroom = @classrooms.get(classroomID) + modal = new ClassroomSettingsModal({classroom: classroom}) + @openModalView(modal) + @listenToOnce modal, 'hide', @renderManageTab + onClickAddStudentsButton: (e) -> classroomID = $(e.target).data('classroom-id') classroom = @classrooms.get(classroomID) diff --git a/server/classrooms/Classroom.coffee b/server/classrooms/Classroom.coffee index 38a2e3018..477d72a50 100644 --- a/server/classrooms/Classroom.coffee +++ b/server/classrooms/Classroom.coffee @@ -11,6 +11,7 @@ ClassroomSchema.statics.privateProperties = [] ClassroomSchema.statics.editableProperties = [ 'description' 'name' + 'aceConfig' ] ClassroomSchema.statics.generateNewCode = (done) -> diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index 5ff8732df..049c575e8 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -11,6 +11,7 @@ log = require 'winston' Campaign = require '../campaigns/Campaign' Course = require '../courses/Course' CourseInstance = require '../courses/CourseInstance' +Classroom = require '../classrooms/Classroom' LevelHandler = class LevelHandler extends Handler modelClass: Level @@ -129,10 +130,16 @@ LevelHandler = class LevelHandler extends Handler courses = _.filter(courses, (course) -> course.get('campaignID').toString() in campaignStrings) courseStrings = (course.id.toString() for course in courses) courseInstances = _.filter(courseInstances, (courseInstance) -> courseInstance.get('courseID').toString() in courseStrings) - aceConfigs = (ci.get('aceConfig') for ci in courseInstances) - aceConfig = _.filter(aceConfigs)[0] or {} - req.codeLanguage = aceConfig.language - @createAndSaveNewSession(sessionQuery, req, res) + classroomIDs = (courseInstance.get('classroomID') for courseInstance in courseInstances) + classroomIDs = _.filter _.uniq classroomIDs, false, (objectID='') -> objectID.toString() + if classroomIDs.length + Classroom.find({ _id: { $in: classroomIDs }}).exec (err, classrooms) => + aceConfigs = (c.get('aceConfig') for c in classrooms) + aceConfig = _.filter(aceConfigs)[0] or {} + req.codeLanguage = aceConfig.language + @createAndSaveNewSession(sessionQuery, req, res) + else + @createAndSaveNewSession(sessionQuery, req, res) else return @sendPaymentRequiredError(res, 'You must be in a course which includes this level to play it') diff --git a/test/server/functional/level.spec.coffee b/test/server/functional/level.spec.coffee index 15e87913f..acb8f000e 100644 --- a/test/server/functional/level.spec.coffee +++ b/test/server/functional/level.spec.coffee @@ -68,29 +68,41 @@ describe 'GET /db/level//session', -> course.save (err) -> expect(err).toBeNull() - + loginJoe (joe) -> - courseInstance = new CourseInstance({ - name: 'Course Instance' - members: [ - joe.get('_id') - ] - courseID: ObjectId(course.id) + classroom = new Classroom({ + name: 'Test Classroom' + members: [ joe.get('_id') ] + aceConfig: { language: 'javascript' } }) - courseInstance.save (err) -> - + classroom.save (err, classroom) -> + expect(err).toBeNull() - done() + + courseInstance = new CourseInstance({ + name: 'Course Instance' + members: [ + joe.get('_id') + ] + courseID: ObjectId(course.id) + classroomID: ObjectId(classroom.id) + }) + + courseInstance.save (err) -> + + expect(err).toBeNull() + done() it 'creates a new session if the user is in a course with that level', (done) -> loginJoe (joe) -> url = getURL("/db/level/#{levelID}/session") - request.get { uri: url }, (err, res, body) -> + request.get { uri: url, json: true }, (err, res, body) -> expect(res.statusCode).toBe(200) + expect(body.codeLanguage).toBe('javascript') done() it 'does not create a new session if the user is not in a course with that level', (done) -> From 4abace927adf4f2fed6597aa9c6cbb04690942af Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Tue, 17 Nov 2015 16:09:34 -0800 Subject: [PATCH 15/60] Add getPrepaidCodes API null check https://app.asana.com/0/53713686567836/63657760462910 --- server/users/user_handler.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index e1fad6c2b..5ac995577 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -459,6 +459,7 @@ UserHandler = class UserHandler extends Handler sendMail emailParams getPrepaidCodes: (req, res) -> + return @sendSuccess(res, []) unless req.user? orQuery = [{ creator: req.user._id }, { 'redeemers.userID' : req.user._id }] Prepaid.find({}).or(orQuery).exec (err, documents) => @sendSuccess(res, documents) From bc454191ecbb1d62c8a94e227ce72c522b28785b Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Tue, 17 Nov 2015 16:21:04 -0800 Subject: [PATCH 16/60] Update accounts prepaid list Needed clearer total and remaining columns. --- app/templates/account/prepaid-view.jade | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/templates/account/prepaid-view.jade b/app/templates/account/prepaid-view.jade index 789dd3c0a..fc6069300 100644 --- a/app/templates/account/prepaid-view.jade +++ b/app/templates/account/prepaid-view.jade @@ -80,28 +80,26 @@ block content span.spr(data-i18n="[title]account_prepaid.copy_link;general.code", title="You can copy the code's link and send it to someone.") span.glyphicon.glyphicon-question-sign(aria-hidden="true") th(data-i18n="account_prepaid.months") - th(data-i18n="account_prepaid.quantity") + th Remaining Users + th Total Users th(data-i18n="user.status") for code in codes.models if code.get('type') === 'terminal_subscription' - var owner = (code.get('creator') == me.id ? true : false) - var properties = code.get('properties') - var redeemers = code.get('redeemers') - if redeemers - - var redeemed = redeemers.length - else - - var redeemed = '0' + - var redeemed = redeemers ? redeemers.length : 0 tr td a(href="/account/prepaid?_ppc=#{code.get('code')}")= code.get('code') td= properties.months || '-' if owner td= code.get('maxRedeemers') - redeemed - else - td - - if owner + td= code.get('maxRedeemers') td(data-i18n="account.purchased") else + td - + td - td(data-i18n="account_prepaid.redeemed") else p(data-i18n="account_prepaid.no_codes") From 890d528d8032824ba325a655648153bdc4095baa Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Tue, 17 Nov 2015 16:53:57 -0800 Subject: [PATCH 17/60] Fix level session stats in CourseDetailsView --- app/templates/courses/course-details.jade | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade index 5038ada6f..bf7b20852 100644 --- a/app/templates/courses/course-details.jade +++ b/app/templates/courses/course-details.jade @@ -208,10 +208,10 @@ mixin progress-members-levels-expanded(memberID) if userLevelStateMap[memberID][levelID] === 'complete' span.progress-level-cell.progress-level-cell-complete(data-level-id=levelID, data-level-slug=level.slug, data-user-id=memberID) #{i + 1} span.spl= level.name.replace('Course: ', '') - +progress-members-popup-completed(i, level) + +progress-members-popup-completed(i, level, (view.userLevelSessionMap[memberID] || {})[levelID]) else if userLevelStateMap[memberID][levelID] === 'started' span.progress-level-cell.progress-level-cell-started(data-level-id=levelID, data-level-slug=level.slug, data-user-id=memberID) #{i + 1} #{level.name.replace('Course: ', '')} - +progress-members-popup-started(i, level) + +progress-members-popup-started(i, level, (view.userLevelSessionMap[memberID] || {})[levelID]) else span.progress-level-cell #{i + 1} #{level.name.replace('Course: ', '')} - i++ @@ -226,35 +226,35 @@ mixin progress-members-levels-condensed(memberID) each level, levelID in campaign.get('levels') if userLevelStateMap[memberID][levelID] === 'complete' span.progress-level-cell.progress-level-cell-complete(style="width:#{levelCellWidth}%;", data-level-id=levelID, data-level-slug=level.slug, data-user-id=memberID) #{i + 1} - +progress-members-popup-completed(i, level) + +progress-members-popup-completed(i, level, (view.userLevelSessionMap[memberID] || {})[levelID]) else if userLevelStateMap[memberID][levelID] === 'started' span.progress-level-cell.progress-level-cell-started(style="width:#{levelCellWidth}%;", data-level-id=levelID, data-level-slug=level.slug, data-user-id=memberID) #{i + 1} - +progress-members-popup-started(i, level) + +progress-members-popup-started(i, level, (view.userLevelSessionMap[memberID] || {})[levelID]) else break - i++ -mixin progress-members-popup-completed(i, level) +mixin progress-members-popup-completed(i, level, session) .progress-popup-container h3 #{i + 1}. #{level.name.replace('Course: ', '')} p span.spr(data-i18n="courses.play_time") - span #{moment.duration(level.playtime, "seconds").humanize()} + span #{moment.duration(session.get('playtime'), "seconds").humanize()} p span.spr(data-i18n="courses.completed") - span #{moment(level.changed).format('MMMM Do YYYY, h:mm:ss a')} + span #{moment(session.get('changed')).format('MMMM Do YYYY, h:mm:ss a')} if adminMode strong(data-i18n="clans.view_solution") -mixin progress-members-popup-started(i, level) +mixin progress-members-popup-started(i, level, session) .progress-popup-container h3 #{i + 1}. #{level.name.replace('Course: ', '')} p span.spr(data-i18n="courses.play_time") - span #{moment.duration(level.playtime, "seconds").humanize()} + span #{moment.duration(session.get('playtime'), "seconds").humanize()} p span.spr(data-i18n="clans.last_played") - span #{moment(level.changed).format('MMMM Do YYYY, h:mm:ss a')} + span #{moment(session.get('changed')).format('MMMM Do YYYY, h:mm:ss a')} if adminMode strong(data-i18n="clans.view_solution") From fe7eb89a0343e6af8b4654dd3346039c3fc1d306 Mon Sep 17 00:00:00 2001 From: Antonio Mancuso Date: Wed, 18 Nov 2015 22:37:40 +0100 Subject: [PATCH 18/60] Update it.coffee I would like contributing to this great game. I woul dlike to help completing the Italian translation asap to enable Italian kids to play and learn with CodeCombat thnx Antonio --- app/locale/it.coffee | 86 ++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/app/locale/it.coffee b/app/locale/it.coffee index 8899d95c3..0517d954b 100644 --- a/app/locale/it.coffee +++ b/app/locale/it.coffee @@ -80,7 +80,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t adjust_volume: "Regola il volume" campaign_multiplayer: "Arene multigiocatore" campaign_multiplayer_description: "... nelle quali programmi faccia a faccia contro altri giocatori." -# campaign_old_multiplayer: "(Deprecated) Old Multiplayer Arenas" + campaign_old_multiplayer: "(Deprecato) Vecchia Arena multiplayer" campaign_old_multiplayer_description: "Reliquie di un'epoca più civilizzata. Nessuna simulazione viene eseguita per queste arene multi-giocatore più vecchie e senza eroi" #"Relics of a more civilized age. No simulations are run for these older, hero-less multiplayer arenas." share_progress_modal: @@ -218,7 +218,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t play_level: done: "Fatto" next_game: "Prossimo gioco" #"Next game" -# show_menu: "Show game menu" + show_menu: "Visualizza menu gioco" home: "Pagina iniziale" # Not used any more, will be removed soon. level: "Livello" # Like "Level: Dungeons of Kithgard" skip: "Salta" @@ -306,8 +306,8 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t tip_baby_coders: "Nel futuro, persino i neonati saranno Arcimaghi." tip_morale_improves: "Il caricamento continuerà fino a che il morale migliora." #"Loading will continue until morale improves." tip_all_species: "Crediamo che chiunque debba avere le stesse opportunità di imparare a programmare." -# tip_reticulating: "Reticulating spines." -# tip_harry: "Yer a Wizard, " + tip_reticulating: "Reticolazione spine" + tip_harry: "Yer il mago, " tip_great_responsibility: "Da grandi abilità di programmazione derivano grandi responsabilità." tip_munchkin: "Se non mangi la tua verdura, un munchkin verrà a cercarti mentre dormi." tip_binary: "Ci sono solo 10 tipi di persone al mondo: quelli che capiscono il binario, e quelli che non lo capiscono." #"There are only 10 types of people in the world: those who understand binary, and those who don't." @@ -335,7 +335,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t tip_move_forward: "Qualsiasi cosa tu faccia, vai sempre avanti. - Martin Luther King Jr." tip_google: "Hai un problema che non riesci a risolvere? Cerca su Google!" tip_adding_evil: "Aggiungendo un pizzico di malvagità." -# tip_hate_computers: "That's the thing about people who think they hate computers. What they really hate is lousy programmers. - Larry Niven" + tip_hate_computers: "A proposito delle persone che pensano di odiare i computer. Cio' che odiano realmente sono i programmatori scarsi - Larry Niven" tip_open_source_contribute: "Puoi aiutare CodeCombat a migliorare!" tip_recurse: "Iterare e umano, usare la ricorsione è divino. - L. Peter Deutsch" tip_free_your_mind: "Devi liberarti di tutto questo, Neo. Paura, dubbio, sfiducia. Libera la tua mente. - Morpheus" @@ -431,60 +431,60 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t parents: "Per i genitori" parents_title: "Caro Genitore: Tuo figlio/a sta imparando a programmare. Vuoi continuare ad aiutarlo/a ? " parents_blurb1: "Tuo figlio/a ha giocato a _nLevels__ livelli ed ha imparato le basi della programmazione. Aiutalo/a a coltivare i suoi interessi ed acquistagli un’abbonamento così potrà continuare a giocare." -# parents_blurb1a: "Computer programming is an essential skill that your child will undoubtedly use as an adult. By 2020, basic software skills will be needed by 77% of jobs, and software engineers are in high demand across the world. Did you know that Computer Science is the highest-paid university degree?" -# parents_blurb2: "For $9.99 USD/mo, your child will get new challenges every week and personal email support from professional programmers." -# parents_blurb3: "No Risk: 100% money back guarantee, easy 1-click unsubscribe." + parents_blurb1a: "La programmazione e' una capacita' che indubbiamente sara' utile a tuo figlio da adulto. Nel 2020 una capacita' basilare di programmazione sara' richiesta dal 77% dei lavori e la richiesta a livello mondiale di ingegneri del software e' in continua crescita. Lo sai che la laurea in Informatica e' la piu' pagata sul mercato?" + parents_blurb2: "Per $9.99 dollari/mese, tuo figlio ricevera' settimanalmente nuove avventure e un supporto personale via email fornito d aprogrammatori professionisti." + parents_blurb3: "Nessun rischio: 100% restituzione dei soldi con un semplice processo di deregistrazione" payment_methods: "Metodi di Pagamento" payment_methods_title: "Metodi di Pagamento Accetati" payment_methods_blurb1: "Attualmente accettiamo come metodi di pagamento la carta di credito e Alipay." payment_methods_blurb2: "Se necessiti di un forma di pagamento diverso.Per favore contattaci" sale_already_subscribed: "Sei già abbonato!" #"You're already subscribed!" sale_blurb1: "Risparmia il 35%" -# sale_blurb2: "off regular subscription price of $120 for a whole year!" + sale_blurb2: "rispetto all'abbinamento ordinario di 120$ per l'intero anno!" sale_button: "Saldi!" #"Sale!" sale_button_title: "Risparmi il 35% quando compri l'abbonamento per 1 anno" sale_click_here: "Clicca qui" -# sale_ends: "Ends" -# sale_extended: "*Existing subscriptions will be extended by 1 year." -# sale_feature_here: "Here's what you'll get:" -# sale_feature2: "Access to 9 powerful new heroes with unique skills!" -# sale_feature4: "42,000 bonus gems awarded immediately!" -# sale_continue: "Ready to continue adventuring?" -# sale_limited_time: "Limited time offer!" -# sale_new_heroes: "New heroes!" -# sale_title: "Back to School Sale" -# sale_view_button: "Buy 1 year subscription for" + sale_ends: "Fine" + sale_extended: "*Gli abbonamenti in corso verranno estesi per 1 anno." + sale_feature_here: "Qui cio' che otterrai:" + sale_feature2: "Accesso a 9 nuovi potenti eroi con abilita' uniche!" + sale_feature4: "42,000 gemme premio riconosciute immediatamente!" + sale_continue: "Pronto per continuare l'avventura?" + sale_limited_time: "Offerta limitata!" + sale_new_heroes: "Nuovi eroi!" + sale_title: "Saldi di inizio scuola" + sale_view_button: "Compra un abbonamento annuale per " stripe_description: "Sottoscrizione mensile" -# stripe_description_year_sale: "1 Year Subscription (35% discount)" -# subscription_required_to_play: "You'll need a subscription to play this level." -# unlock_help_videos: "Subscribe to unlock all video tutorials." + stripe_description_year_sale: "Abbonamneto annuale (sconto 35%)" + subscription_required_to_play: "Devi essere abbonato per giocare su questo livello." + unlock_help_videos: "Abbonati per accedere a tutti i tutorial video." personal_sub: "Sottoscrizione Personale" # Accounts Subscription View below -# loading_info: "Loading subscription information..." -# managed_by: "Managed by" -# will_be_cancelled: "Will be cancelled on" -# currently_free: "You currently have a free subscription" -# currently_free_until: "You currently have a subscription until" -# was_free_until: "You had a free subscription until" -# managed_subs: "Managed Subscriptions" -# managed_subs_desc: "Add subscriptions for other players (students, children, etc.)" -# managed_subs_desc_2: "Recipients must have a CodeCombat account associated with the email address you provide." -# group_discounts: "Group discounts" -# group_discounts_1: "We also offer group discounts for bulk subscriptions." + loading_info: "Caricamento informazioni abbonamento..." + managed_by: "Gestito da" + will_be_cancelled: "Sara' rimosso il" + currently_free: "Al momento hai un abbonamento gratuito" + currently_free_until: "Hai un abbonamento valido fino al" + was_free_until: "Hai avuto un abbonamento gratuito fino al " + managed_subs: "Gestione Abbonamenti" + managed_subs_desc: "Aggiungi un abbonamento per altri giocatori (studenti, bambini, etc.)" + managed_subs_desc_2: "I beneficiari devono avere un abbonamento CodeCombat associato con l'email fornito." + group_discounts: "Sconto comitiva" + group_discounts_1: "Offriamo sconto comitiva anche per abbonamenti in grandi volumi." group_discounts_1st: "Prima sottoscrizione" group_discounts_full: "Prezzo completo" group_discounts_2nd: "Sottoscrizione 2-11" #"Subscriptions 2-11" group_discounts_20: "20% disconto" group_discounts_12th: "Sottoscrizione 12+" #"Subscriptions 12+" group_discounts_40: "40% di sconto" -# subscribing: "Subscribing..." -# recipient_emails_placeholder: "Enter email address to subscribe, one per line." + subscribing: "Abbonamento..." + recipient_emails_placeholder: "Inserisci l'indirizzo email per abbonarti, uno per linea." subscribe_users: "Iscrivere Utenti" users_subscribed: "Utenti iscritti:" -# no_users_subscribed: "No users subscribed, please double check your email addresses." -# current_recipients: "Current Recipients" -# unsubscribing: "Unsubscribing" -# subscribe_prepaid: "Click Subscribe to use prepaid code" -# using_prepaid: "Using prepaid code for monthly subscription" + no_users_subscribed: "Utente non abbonato, per favore verifica il tuo indirizzo di email." + current_recipients: "Destinatari attuali" + unsubscribing: "Deregistrazione" + subscribe_prepaid: "Clicca su Registrazione per usare un codice pre pagato" + using_prepaid: "Usa un codice pre pagato per un abbonamento mensile" choose_hero: choose_hero: "Scegli il tuo eroe" @@ -1612,7 +1612,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t # inactive_developers: "Inactive Developers" admin: -# av_espionage: "Espionage" # Really not important to translate /admin controls. + av_espionage: "Spionaggio" # Really not important to translate /admin controls. av_espionage_placeholder: "Email o nome utente" av_usersearch: "Cerca utenti" av_usersearch_placeholder: "Email, username, nome, qualsiasi cosa" @@ -1621,8 +1621,8 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t av_entities_sub_title: "Entità" av_entities_users_url: "Utenti" av_entities_active_instances_url: "Istanze attive" -# av_entities_employer_list_url: "Employer List" -# av_entities_candidates_list_url: "Candidate List" + av_entities_employer_list_url: "Lista datore di lavoro" + av_entities_candidates_list_url: "Lista Candidati" av_entities_user_code_problems_list_url: "Lista problemi codice utenti" av_other_sub_title: "Altro" av_other_debug_base_url: "Base (per il debug di base.jade)" From 7a6c42b89f1b29f34b836a38c2bb19c2f0930cb9 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Wed, 18 Nov 2015 14:02:45 -0800 Subject: [PATCH 19/60] Set up course-ladder level handling * LadderView displays course info, different style for course-ladder levels * LadderView hides simulate tab for course-ladder levels * HeroVictoryModal links to LadderView for course-ladder levels * CourseDetails page links to LadderView for course-ladder levels * Enable course instances for league simulation --- app/styles/play/ladder/ladder.sass | 22 +++++++++++++ app/templates/courses/course-details.jade | 2 +- app/templates/play/ladder/ladder.jade | 32 ++++++++++++++----- app/views/courses/CourseDetailsView.coffee | 17 +++++++--- app/views/ladder/LadderPlayModal.coffee | 1 + app/views/ladder/LadderView.coffee | 16 +++++++--- .../play/level/modal/HeroVictoryModal.coffee | 7 ++++ server/queues/scoring/createNewTask.coffee | 2 +- 8 files changed, 80 insertions(+), 19 deletions(-) diff --git a/app/styles/play/ladder/ladder.sass b/app/styles/play/ladder/ladder.sass index b134ec5c1..f6aefedd7 100644 --- a/app/styles/play/ladder/ladder.sass +++ b/app/styles/play/ladder/ladder.sass @@ -5,6 +5,22 @@ margin: -14px -12px 20px -12px padding-bottom: 30px border-bottom: 1px solid #888 + + #course-header + background-color: black + font-size: 24px + padding: 6px 4px 8px + font-weight: bold + + #course-details-link + position: absolute + background-color: white + padding: 2px 5px + a + color: black + + #course-name + color: white #level-column padding-top: 14px @@ -13,6 +29,12 @@ img margin-top: -14px width: 100% + + #course-h1 + color: black + font-size: 72px + text-transform: capitalize + margin-top: 0 h1 text-align: center diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade index bf7b20852..3e48d4f53 100644 --- a/app/templates/courses/course-details.jade +++ b/app/templates/courses/course-details.jade @@ -273,7 +273,7 @@ mixin levels-tab tr td if lastLevelCompleted || adminMode - button.btn.btn-success.btn-play-level(data-level-slug=level.slug, data-i18n="home.play") + button.btn.btn-success.btn-play-level(data-level-slug=level.slug, data-i18n="home.play", data-level-id=levelID) td if userLevelStateMap[me.id] div= userLevelStateMap[me.id][levelID] diff --git a/app/templates/play/ladder/ladder.jade b/app/templates/play/ladder/ladder.jade index 928604fa8..8a923a76e 100644 --- a/app/templates/play/ladder/ladder.jade +++ b/app/templates/play/ladder/ladder.jade @@ -3,14 +3,28 @@ block content - var base = "/images/pages/play/ladder/prize_"; div#ladder-top + + if leagueType == 'course' + #course-header + #course-details-link + a(href="/courses/"+view.course.id+"/"+view.league.id) + span.glyphicon.glyphicon-arrow-left + span.spl Levels + + .text-center + span#course-name + span= view.course.get('name') + span.spl - Arena + div#level-column + if leagueType === 'course' + h1#course-h1= (level.get('name') || '').toUpperCase() + if levelDescription div!= levelDescription - else - h1= level.get('name') - if league + if leagueType === 'clan' h1.league-header a(href="/#{leagueType == 'clan' ? 'clans' : leagueType}/#{league.id}")= league.get('name') span.spl(data-i18n="ladder.league") League @@ -132,9 +146,10 @@ block content span= team.displayName div.column.col-md-2 - .spectate-button-container - a(href="/play/spectate/#{level.get('slug')}" + (league ? "?league=" + league.id : "")).spectate-button.btn.btn-illustrated.btn-info.center - span(data-i18n="play.spectate") Spectate + if leagueType !== 'course' + .spectate-button-container + a(href="/play/spectate/#{level.get('slug')}" + (league ? "?league=" + league.id : "")).spectate-button.btn.btn-illustrated.btn-info.center + span(data-i18n="play.spectate") Spectate ul.nav.nav-pills li.active @@ -142,8 +157,9 @@ block content if !me.get('anonymous') li a(href="#my-matches", data-toggle="tab", data-i18n="ladder.my_matches") My Matches - li - a(href="#simulate", data-toggle="tab", data-i18n="ladder.simulate") Simulate + if leagueType !== 'course' + li + a(href="#simulate", data-toggle="tab", data-i18n="ladder.simulate") Simulate if level.get('name') == 'Greed' li a(href="#prizes", data-toggle="tab", data-i18n="ladder_prizes.prizes") Prizes diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee index aa6a0bd2a..954f6a771 100644 --- a/app/views/courses/CourseDetailsView.coffee +++ b/app/views/courses/CourseDetailsView.coffee @@ -218,11 +218,18 @@ module.exports = class CourseDetailsView extends RootView onClickPlayLevel: (e) -> levelSlug = $(e.target).data('level-slug') - Backbone.Mediator.publish 'router:navigate', { - route: @getLevelURL levelSlug - viewClass: 'views/play/level/PlayLevelView' - viewArgs: [{courseID: @courseID, courseInstanceID: @courseInstanceID}, levelSlug] - } + levelID = $(e.target).data('level-id') + level = @campaign.get('levels')[levelID] + if level.type is 'course-ladder' + Backbone.Mediator.publish 'router:navigate', { + route: '/play/ladder/' + levelSlug + '/course/' + @courseInstance.id + } + else + Backbone.Mediator.publish 'router:navigate', { + route: @getLevelURL levelSlug + viewClass: 'views/play/level/PlayLevelView' + viewArgs: [{courseID: @courseID, courseInstanceID: @courseInstanceID}, levelSlug] + } getLevelURL: (levelSlug) -> "/play/level/#{levelSlug}?course=#{@courseID}&course-instance=#{@courseInstanceID}" diff --git a/app/views/ladder/LadderPlayModal.coffee b/app/views/ladder/LadderPlayModal.coffee index 473843792..44c88a6fd 100644 --- a/app/views/ladder/LadderPlayModal.coffee +++ b/app/views/ladder/LadderPlayModal.coffee @@ -81,6 +81,7 @@ module.exports = class LadderPlayModal extends ModalView # PART 4: Render finishRendering: -> + return if @destroyed @checkTutorialLevelExists (exists) => @tutorialLevelExists = exists @render() diff --git a/app/views/ladder/LadderView.coffee b/app/views/ladder/LadderView.coffee index 423d54d92..437fb9664 100644 --- a/app/views/ladder/LadderView.coffee +++ b/app/views/ladder/LadderView.coffee @@ -13,7 +13,8 @@ LadderPlayModal = require './LadderPlayModal' CocoClass = require 'core/CocoClass' Clan = require 'models/Clan' -#CourseInstance = require 'models/CourseInstance' +CourseInstance = require 'models/CourseInstance' +Course = require 'models/Course' HIGHEST_SCORE = 1000000 @@ -44,13 +45,20 @@ module.exports = class LadderView extends RootView @sessions = @supermodel.loadCollection(new LevelSessionsCollection(@levelID), 'your_sessions', {cache: false}).model @teams = [] @loadLeague() + @course = new Course() loadLeague: -> - @leagueID = @leagueType = null unless @leagueType in ['clan'] #, 'course'] + @leagueID = @leagueType = null unless @leagueType in ['clan', 'course'] return unless @leagueID - modelClass = if @leagueType is 'clan' then Clan else null# else CourseInstance - resourceString = if @leagueType is 'clan' then 'clans.clan' else null# else 'courses.course' + modelClass = if @leagueType is 'clan' then Clan else CourseInstance + resourceString = if @leagueType is 'clan' then 'clans.clan' else 'courses.course' @league = @supermodel.loadModel(new modelClass(_id: @leagueID), resourceString).model + if @leagueType is 'course' + @listenToOnce @league, 'sync', @onCourseInstanceLoaded + + onCourseInstanceLoaded: (courseInstance) -> + course = new Course({_id: courseInstance.get('courseID')}) + @course = @supermodel.loadModel(course, 'courses.course').model onLoaded: -> @teams = teamDataFromLevel @level diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee index 4eeaf3782..8df5c81c7 100644 --- a/app/views/play/level/modal/HeroVictoryModal.coffee +++ b/app/views/play/level/modal/HeroVictoryModal.coffee @@ -441,6 +441,13 @@ module.exports = class HeroVictoryModal extends ModalView viewClass = require 'views/courses/CourseDetailsView' viewArgs.push @courseID viewArgs.push @courseInstanceID if @courseInstanceID + else if @level.get('type', true) is 'course-ladder' + leagueID = @getQueryVariable 'league' + link = "/play/ladder/"+@level.get('slug')+"/course/"+leagueID + Backbone.Mediator.publish 'router:navigate', { + route: link + } + return else viewClass = require 'views/play/CampaignView' viewArgs = [options, @getNextLevelCampaign()] diff --git a/server/queues/scoring/createNewTask.coffee b/server/queues/scoring/createNewTask.coffee index 6b6370c27..4ded373e5 100644 --- a/server/queues/scoring/createNewTask.coffee +++ b/server/queues/scoring/createNewTask.coffee @@ -67,7 +67,7 @@ updateSessionToSubmit = (transpiledCode, user, sessionToUpdate, callback) -> # Reset all league stats as well, and enter the session into any leagues the user is currently part of (not retroactive when joining new leagues) leagueIDs = user.get('clans') or [] - #leagueIDs = leagueIDs.concat user.get('courseInstances') or [] + leagueIDs = leagueIDs.concat user.get('courseInstances') or [] leagueIDs = (leagueID + '' for leagueID in leagueIDs) # Make sure to save them as strings. newLeagues = [] for leagueID in leagueIDs From ee1e484dda0c166af658debfb0ba716f75386f23 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Wed, 18 Nov 2015 14:08:25 -0800 Subject: [PATCH 20/60] Have play buttons show 'Compete!' for arena levels on CourseDetails levels list --- app/locale/en.coffee | 1 + app/templates/courses/course-details.jade | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/locale/en.coffee b/app/locale/en.coffee index c1268b7ed..b1d9be0b8 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -51,6 +51,7 @@ play: play_as: "Play As" # Ladder page + compete: "Compete!" # Course details page spectate: "Spectate" # Ladder page players: "players" # Hover over a level on /play hours_played: "hours played" # Hover over a level on /play diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade index 3e48d4f53..506664867 100644 --- a/app/templates/courses/course-details.jade +++ b/app/templates/courses/course-details.jade @@ -273,7 +273,8 @@ mixin levels-tab tr td if lastLevelCompleted || adminMode - button.btn.btn-success.btn-play-level(data-level-slug=level.slug, data-i18n="home.play", data-level-id=levelID) + - var i18n = level.type === 'course-ladder' ? 'play.compete' : 'home.play'; + button.btn.btn-success.btn-play-level(data-level-slug=level.slug, data-i18n=i18n, data-level-id=levelID) td if userLevelStateMap[me.id] div= userLevelStateMap[me.id][levelID] From 62345aeb1d4ae6bc8080826101ce403739673504 Mon Sep 17 00:00:00 2001 From: Viet Truong Date: Wed, 18 Nov 2015 15:49:35 -0800 Subject: [PATCH 21/60] Fix top padding and margin for game-menu for #2963 --- app/styles/play/menu/game-menu-modal.sass | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/styles/play/menu/game-menu-modal.sass b/app/styles/play/menu/game-menu-modal.sass index a9390ea01..2a8f08777 100644 --- a/app/styles/play/menu/game-menu-modal.sass +++ b/app/styles/play/menu/game-menu-modal.sass @@ -82,7 +82,8 @@ left: 219px top: 21px width: 571px - height: 514px + height: 495px + margin-top: 15px padding: 50px overflow-y: scroll From 2d584c527d99c3e30d8d22e32283521e4dd3c794 Mon Sep 17 00:00:00 2001 From: Imperadeiro98 Date: Thu, 19 Nov 2015 16:33:54 +0000 Subject: [PATCH 22/60] Fix #2966 --- app/lib/LevelSetupManager.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/lib/LevelSetupManager.coffee b/app/lib/LevelSetupManager.coffee index 1b1f92228..c70c6aca4 100644 --- a/app/lib/LevelSetupManager.coffee +++ b/app/lib/LevelSetupManager.coffee @@ -100,7 +100,6 @@ module.exports = class LevelSetupManager extends CocoClass @options.parent.openModalView(firstModal) # @inventoryModal.onShown() # replace? - @playSound 'game-menu-open' #- Modal events From c8ceca4d3d565bed300946aa90836606a89d1e80 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Thu, 19 Nov 2015 10:41:31 -0800 Subject: [PATCH 23/60] Add user.courseInstances, use it for course ladders --- app/schemas/models/user.coffee | 1 + app/templates/play/ladder/ladder.jade | 2 +- app/views/play/modal/PlayAchievementsModal.coffee | 2 -- server/courses/course_instance_handler.coffee | 10 ++++++---- server/queues/scoring/getTwoGames.coffee | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index fa9ab8fb5..0bce4751c 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -316,6 +316,7 @@ _.extend UserSchema.properties, country: { type: 'string', enum: ['brazil', 'china'] } # New, supports multiple countries for different versions--only set for specific countries where we have premium servers right now clans: c.array {}, c.objectId() + courseInstances: c.array {}, c.objectId() currentCourse: c.object {}, { # Old, can be removed after we deploy and delete it from all users courseID: c.objectId({}) courseInstanceID: c.objectId({}) diff --git a/app/templates/play/ladder/ladder.jade b/app/templates/play/ladder/ladder.jade index 8a923a76e..f09d6882c 100644 --- a/app/templates/play/ladder/ladder.jade +++ b/app/templates/play/ladder/ladder.jade @@ -26,7 +26,7 @@ block content if leagueType === 'clan' h1.league-header - a(href="/#{leagueType == 'clan' ? 'clans' : leagueType}/#{league.id}")= league.get('name') + a(href="/clans/#{league.id}")= league.get('name') span.spl(data-i18n="ladder.league") League if level.get('name') == 'Greed' diff --git a/app/views/play/modal/PlayAchievementsModal.coffee b/app/views/play/modal/PlayAchievementsModal.coffee index 3375f6c59..c952237dd 100644 --- a/app/views/play/modal/PlayAchievementsModal.coffee +++ b/app/views/play/modal/PlayAchievementsModal.coffee @@ -73,10 +73,8 @@ module.exports = class PlayAchievementsModal extends ModalView @onEverythingLoaded() onEverythingLoaded: => - console.log 'got achievements', m.attributes for m in @achievements.models @achievements.set(@achievements.filter((m) -> m.get('collection') isnt 'level.sessions' or m.get('query')?.team)) for achievement in @achievements.models - console.log 'kept achievement', achievement.attributes if earned = @earnedMap[achievement.id] achievement.earned = earned achievement.earnedDate = earned.getCreationDate() diff --git a/server/courses/course_instance_handler.coffee b/server/courses/course_instance_handler.coffee index 24a0e28d1..e7b99c321 100644 --- a/server/courses/course_instance_handler.coffee +++ b/server/courses/course_instance_handler.coffee @@ -60,7 +60,7 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler courseInstance.save (err, courseInstance) => return @sendDatabaseError(res, err) if err @sendCreated(res, courseInstance) - + addMember: (req, res, courseInstanceID) -> userID = req.body.userID return @sendBadInputError(res, 'Input must be a MongoDB ID') unless utils.isID(userID) @@ -88,8 +88,10 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler courseInstance.set('members', members) courseInstance.save (err, courseInstance) => return @sendDatabaseError(res, err) if err - @sendSuccess(res, @formatEntity(req, courseInstance)) - + User.update {_id: mongoose.Types.ObjectId(userID)}, {$addToSet: {courseInstances: courseInstance.get('_id')}}, (err) => + return @sendDatabaseError(res, err) if err + @sendSuccess(res, @formatEntity(req, courseInstance)) + post: (req, res) -> return @sendBadInputError(res, 'No classroomID') unless req.body.classroomID return @sendBadInputError(res, 'No courseID') unless req.body.courseID @@ -101,7 +103,7 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler return @sendDatabaseError(res, err) if err return @sendNotFoundError(res, 'Course not found') unless course super(req, res) - + makeNewInstance: (req) -> doc = new CourseInstance({ members: [] diff --git a/server/queues/scoring/getTwoGames.coffee b/server/queues/scoring/getTwoGames.coffee index ab1664145..709d9f309 100644 --- a/server/queues/scoring/getTwoGames.coffee +++ b/server/queues/scoring/getTwoGames.coffee @@ -38,7 +38,7 @@ getRandomSessions = (user, callback) -> # Only people in a league will end up simulating internal league matches (for leagues they're in) except by dumb chance. # If we don't like that, we can rework sampleByLevel to have an opportunity to switch to internal leagues if the first session had a league affiliation. leagueIDs = user?.get('clans') or [] - #leagueIDs = leagueIDs.concat user?.get('courseInstances') or [] + leagueIDs = leagueIDs.concat user?.get('courseInstances') or [] leagueIDs = (leagueID + '' for leagueID in leagueIDs) # Make sure to fetch them as strings. return sampleByLevel callback unless leagueIDs.length and Math.random() > 1 / leagueIDs.length leagueID = _.sample leagueIDs From 3d71451e10fb513af293113e8b78e4a838c73cf3 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Thu, 19 Nov 2015 12:10:20 -0800 Subject: [PATCH 24/60] Add script to migrate course instance ladder leagues --- ...-instances-to-users-and-session-leagues.js | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 scripts/mongodb/migrations/2015-11-19-add-course-instances-to-users-and-session-leagues.js diff --git a/scripts/mongodb/migrations/2015-11-19-add-course-instances-to-users-and-session-leagues.js b/scripts/mongodb/migrations/2015-11-19-add-course-instances-to-users-and-session-leagues.js new file mode 100644 index 000000000..0bfe0c951 --- /dev/null +++ b/scripts/mongodb/migrations/2015-11-19-add-course-instances-to-users-and-session-leagues.js @@ -0,0 +1,78 @@ +// Add user.courseInstances properties and then add those to session leagues +// Usage: +// mongo
:/