From 73698129a0b5b1e19541e4feee8d4b11fed3cda1 Mon Sep 17 00:00:00 2001 From: Matt Lott <mattlott@live.com> Date: Tue, 21 Jun 2016 16:41:47 -0700 Subject: [PATCH 1/8] Make level practice threshold available in editor --- app/views/editor/level/settings/SettingsTabView.coffee | 2 +- server/handlers/level_handler.coffee | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/editor/level/settings/SettingsTabView.coffee b/app/views/editor/level/settings/SettingsTabView.coffee index 842978d87..f042b5ab1 100644 --- a/app/views/editor/level/settings/SettingsTabView.coffee +++ b/app/views/editor/level/settings/SettingsTabView.coffee @@ -15,7 +15,7 @@ module.exports = class SettingsTabView extends CocoView editableSettings: [ 'name', 'description', 'documentation', 'nextLevel', 'background', 'victory', 'i18n', 'icon', 'goals', 'type', 'terrain', 'showsGuide', 'banner', 'employerDescription', 'loadingTip', 'requiresSubscription', - 'helpVideos', 'replayable', 'scoreTypes', 'concepts', 'picoCTFProblem' + 'helpVideos', 'replayable', 'scoreTypes', 'concepts', 'picoCTFProblem', 'practiceThresholdMinutes' ] subscriptions: diff --git a/server/handlers/level_handler.coffee b/server/handlers/level_handler.coffee index 5fd573dd1..a44a4e34b 100644 --- a/server/handlers/level_handler.coffee +++ b/server/handlers/level_handler.coffee @@ -67,6 +67,7 @@ LevelHandler = class LevelHandler extends Handler 'scoreTypes' 'concepts' 'picoCTFProblem' + 'practiceThresholdMinutes' ] postEditableProperties: ['name'] From 1d70837309b110534f43092bc0eb83c9235999b0 Mon Sep 17 00:00:00 2001 From: Matt Lott <mattlott@live.com> Date: Tue, 21 Jun 2016 21:10:59 -0700 Subject: [PATCH 2/8] Exclude practice levels from teacher course guides --- app/templates/courses/teacher-courses-view.jade | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/templates/courses/teacher-courses-view.jade b/app/templates/courses/teacher-courses-view.jade index 9707e0170..42634b962 100644 --- a/app/templates/courses/teacher-courses-view.jade +++ b/app/templates/courses/teacher-courses-view.jade @@ -57,6 +57,8 @@ block content select.level-select.form-control if view.campaigns.loaded each level, levelIndex in view.campaigns.get(course.get('campaignID')).getLevels().models + if level.get('type') === 'hero-practice' + - continue; option(value=level.get('slug')) span = levelIndex + 1 From 6a03163fcb0b7425e30638daa7607235a3239a4c Mon Sep 17 00:00:00 2001 From: Rob <rob@codecombat.com> Date: Wed, 22 Jun 2016 15:07:43 -0700 Subject: [PATCH 3/8] - Use modern esper engine if we detect browser support it. - Change streaming batch size depending on how far into simulation we are. - Hoist try catch out of onWorldLoad so Chrome and JIT it. --- .../javascripts/workers/aether_worker.js | 13 +++++++- .../javascripts/workers/worker_world.js | 31 +++++++++++++++---- app/lib/world/world.coffee | 16 +++++++++- config.coffee | 1 + 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/workers/aether_worker.js b/app/assets/javascripts/workers/aether_worker.js index 810537599..5e45c7eda 100644 --- a/app/assets/javascripts/workers/aether_worker.js +++ b/app/assets/javascripts/workers/aether_worker.js @@ -1,7 +1,18 @@ var window = self; var Global = self; -importScripts("/javascripts/lodash.js", "/javascripts/aether.js", "/javascripts/esper.js"); +importScripts("/javascripts/lodash.js", "/javascripts/aether.js"); + +try { + //Detect very modern javascript support. + (0,eval("'use strict'; let test = (class Test { *gen() { yield yield * () => WeakMap; } });")); + console.log("Modern javascript detected, aw yeah!"); + self.importScripts('/javascripts/esper.modern.js'); +} catch (e) { + console.log("Legacy javascript detected, falling back...", e.message); + self.importScripts('/javascripts/esper.js'); +} + //console.log("Aether Tome worker has finished importing scripts."); var aethers = {}; var languagesImported = {}; diff --git a/app/assets/javascripts/workers/worker_world.js b/app/assets/javascripts/workers/worker_world.js index 26217cb16..f76a07483 100644 --- a/app/assets/javascripts/workers/worker_world.js +++ b/app/assets/javascripts/workers/worker_world.js @@ -63,7 +63,18 @@ var console = { console.error = console.warn = console.info = console.debug = console.log; self.console = console; -self.importScripts('/javascripts/lodash.js', '/javascripts/world.js', '/javascripts/aether.js', '/javascripts/esper.js'); +self.importScripts('/javascripts/lodash.js', '/javascripts/world.js', '/javascripts/aether.js'); +try { + //Detect very modern javascript support. + (0,eval("'use strict'; let test = (class Test { *gen() { yield yield * () => WeakMap; } });")); + console.log("Modern javascript detected, aw yeah!"); + self.importScripts('/javascripts/esper.modern.js'); +} catch (e) { + console.log("Legacy javascript detected, falling back...", e.message); + self.importScripts('/javascripts/esper.js'); +} + + var myImportScripts = importScripts; var languagesImported = {}; @@ -402,6 +413,17 @@ self.serializeFramesSoFar = function serializeFramesSoFar() { self.world.framesSerializedSoFar = self.world.frames.length; }; +function trySerialize() { + try { + var serialized = self.world.serialize(); + } + catch(error) { + console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace); + return false; + } + return serialized; +} + self.onWorldLoaded = function onWorldLoaded() { if(self.world.framesSerializedSoFar == self.world.frames.length) return; if(self.world.ended) @@ -419,12 +441,9 @@ self.onWorldLoaded = function onWorldLoaded() { return console.log('Headless simulation completed in ' + diff + 'ms.'); var worldEnded = self.world.ended; + var serialized; var transferableSupported = self.transferableSupported(); - try { - var serialized = self.world.serialize(); - } - catch(error) { - console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace); + if ( !( serialized = trySerialize()) ) { self.destroyWorld(); return; } diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee index af7e2b65b..266f4bc1e 100644 --- a/app/lib/world/world.coffee +++ b/app/lib/world/world.coffee @@ -162,11 +162,25 @@ module.exports = class World shouldContinueLoading: (t1, loadProgressCallback, skipDeferredLoading, continueLaterFn) -> t2 = now() + chunkSize = @frames.length - @framesSerializedSoFar + simedTime = @frames.length / @frameRate + + chunkTime = switch + when simedTime > 15 then 7 + when simedTime > 10 then 5 + when simedTime > 5 then 3 + when simedTime > 2 then 1 + else 0.5 + + bailoutTime = Math.max(2000*chunkTime, 10000) + + dt = t2 - t1 + if @realTime shouldUpdateProgress = @shouldUpdateRealTimePlayback t2 shouldDelayRealTimeSimulation = not shouldUpdateProgress and @shouldDelayRealTimeSimulation t2 else - shouldUpdateProgress = t2 - t1 > PROGRESS_UPDATE_INTERVAL# and (@frames.length - @framesSerializedSoFar >= @frameRate or t2 - t1 > 1000) + shouldUpdateProgress = (dt > PROGRESS_UPDATE_INTERVAL and (chunkSize / @frameRate >= chunkTime) or dt > bailoutTime) shouldDelayRealTimeSimulation = false return true unless shouldUpdateProgress or shouldDelayRealTimeSimulation # Stop loading frames for now; continue in a moment. diff --git a/config.coffee b/config.coffee index 6690d8e2e..75e614786 100644 --- a/config.coffee +++ b/config.coffee @@ -207,6 +207,7 @@ exports.config = copyTo: 'lib/ace': ['node_modules/ace-builds/src-min-noconflict/*'] 'fonts': ['bower_components/openSansCondensed/*', 'bower_components/openSans/*'] + 'javascripts': ['bower_components/esper.js/esper.modern.js'] autoReload: delay: 1000 From f100e7ab52e52bb148eb79a5bf4a8f60f316d0d7 Mon Sep 17 00:00:00 2001 From: Rob <rob@codecombat.com> Date: Wed, 22 Jun 2016 15:08:20 -0700 Subject: [PATCH 4/8] Non-integer indentation levels are impossible. --- app/views/play/level/tome/SpellView.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index 9cddc2178..cd7cc1c18 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -316,7 +316,7 @@ module.exports = class SpellView extends CocoView xstart = startOfRow(row) if language is 'python' - requiredIndent = new RegExp '^' + new Array(xstart / 4 + 1).join('( |\t)') + '( |\t)+(\\S|\\s*$)' + requiredIndent = new RegExp '^' + new Array(Math.floor(xstart / 4 + 1)).join('( |\t)') + '( |\t)+(\\S|\\s*$)' for crow in [docRange.start.row+1..docRange.end.row] unless requiredIndent.test lines[crow] docRange.end.row = crow - 1 From 0ad8fddff6785fa82b62a3e9989a5accce5a1558 Mon Sep 17 00:00:00 2001 From: Josh Callebaut <josh.callebaut@gmail.com> Date: Wed, 22 Jun 2016 15:11:27 -0700 Subject: [PATCH 5/8] Clicking on level in treema makes the level flash --- app/views/editor/campaign/CampaignEditorView.coffee | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index 4cff9c845..9af98d510 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -239,6 +239,14 @@ module.exports = class CampaignEditorView extends RootView @campaign.set key, value for key, value of @treema.data @campaignView.setCampaign(@campaign) + onTreemaSelectionChanged: (e, node) => + return unless node[0]?.data?.original? + elem = @$('div').find('[data-level-original="'+node[0].data.original+'"]') + elem.toggle('pulsate') + setTimeout ()-> + elem.toggle('pulsate') + , 1000 + onTreemaDoubleClicked: (e, node) => path = node.getPath() return unless _.string.startsWith path, '/levels/' From e953f7fc607cd7f87c5027f4c0371a0d6a8d6387 Mon Sep 17 00:00:00 2001 From: Josh Callebaut <josh.callebaut@gmail.com> Date: Wed, 22 Jun 2016 15:36:42 -0700 Subject: [PATCH 6/8] Cleaner jQuery selection --- app/views/editor/campaign/CampaignEditorView.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index 9af98d510..ed78d3b8f 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -241,7 +241,7 @@ module.exports = class CampaignEditorView extends RootView onTreemaSelectionChanged: (e, node) => return unless node[0]?.data?.original? - elem = @$('div').find('[data-level-original="'+node[0].data.original+'"]') + elem = @$("div[data-level-original='#{node[0].data.original}']") elem.toggle('pulsate') setTimeout ()-> elem.toggle('pulsate') From 18de9ab2989d58037ae988ba3f48759b10eb4e25 Mon Sep 17 00:00:00 2001 From: Matt Lott <mattlott@live.com> Date: Thu, 23 Jun 2016 06:12:51 -0700 Subject: [PATCH 7/8] Create call tasks and opps for license requests --- .../teachers/TeachersContactModal.coffee | 2 +- server/lib/closeIO.coffee | 82 +++++++++++++++---- server/routes/contact.coffee | 22 +++-- 3 files changed, 82 insertions(+), 24 deletions(-) diff --git a/app/views/teachers/TeachersContactModal.coffee b/app/views/teachers/TeachersContactModal.coffee index ab8fbf06b..52d4488c7 100644 --- a/app/views/teachers/TeachersContactModal.coffee +++ b/app/views/teachers/TeachersContactModal.coffee @@ -62,7 +62,7 @@ module.exports = class TeachersContactModal extends ModalView return unless _.isEmpty(formErrors) @state.set('sendingState', 'sending') - data = _.extend({ country: me.get('country'), recipientID: 'schools@codecombat.com' }, formValues) + data = _.extend({ country: me.get('country') }, formValues) contact.send({ data context: @ diff --git a/server/lib/closeIO.coffee b/server/lib/closeIO.coffee index c72c4b8c4..8de1f78fa 100644 --- a/server/lib/closeIO.coffee +++ b/server/lib/closeIO.coffee @@ -66,33 +66,81 @@ module.exports = return done("Unexpected activities format: " + body) unless activities.data? for activity in activities.data when activity._type is 'Email' if /@codecombat\.(?:com)|(?:nl)/ig.test(activity.sender) and not activity.sender?.indexOf(config.mail.username) >= 0 - return done(null, activity.sender, lead.id) + return done(null, activity.sender, activity.user_id, lead.id) return done(null, config.mail.supportSchools, lead.id) catch error log.error("closeIO.getSalesContactEmail Error for #{email}: #{JSON.stringify(error)}") return done(error) - sendMail: (fromAddress, subject, content, done) -> + sendMail: (fromAddress, subject, content, salesContactEmail, leadID, done) -> # log.info("DEBUG: closeIO.sendMail #{fromAddress} #{subject} #{content}") - @getSalesContactEmail fromAddress, (err, salesContactEmail, leadID) -> - return done("Error getting sales contact for #{fromAddress}: #{err}") if err - matches = salesContactEmail.match(/^[a-zA-Z_]+ <(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3})>$|(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3})/i) - salesContactEmail = matches?[1] ? matches?[2] ? config.mail.supportSchools - salesContactEmail = config.mail.supportSchools if salesContactEmail?.indexOf('brian@codecombat.com') >= 0 + matches = salesContactEmail.match(/^[a-zA-Z_]+ <(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3})>$|(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3})/i) + salesContactEmail = matches?[1] ? matches?[2] ? config.mail.supportSchools + salesContactEmail = config.mail.supportSchools if salesContactEmail?.indexOf('brian@codecombat.com') >= 0 + + postData = + to: [salesContactEmail] + sender: config.mail.username + subject: subject + body_text: content + lead_id: leadID + status: 'outbox' + options = + uri: "https://#{apiKey}:X@app.close.io/api/v1/activity/email/" + body: JSON.stringify(postData) + request.post options, (error, response, body) => + return done(error) if error + result = JSON.parse(body) + if result.errors or result['field-errors'] + errorMessage = "Close.io Send email POST error for #{fromAddress} #{JSON.stringify(result.errors)} #{JSON.stringify(result['field-errors'])}" + return done(errorMessage) + return done() + + processLicenseRequest: (teacherEmail, userID, leadID, licensesRequested, amount, done) -> + # Update lead with licenses requested + putData = 'custom.licensesRequested': licensesRequested + options = + uri: "https://#{apiKey}:X@app.close.io/api/v1/lead/#{leadID}/" + body: JSON.stringify(putData) + request.put options, (error, response, body) => + return done(error) if error + result = JSON.parse(body) + if result.errors or result['field-errors'] + errorMessage = "Update Close.io lead PUT error for #{teacherEmail} #{leadID}" + return done(errorMessage) + + # Create call task postData = - to: [salesContactEmail] - sender: config.mail.username - subject: subject - body_text: content + _type: "lead" lead_id: leadID - status: 'outbox' + assigned_to: userID + text: "Call #{teacherEmail}" + is_complete: false options = - uri: "https://#{apiKey}:X@app.close.io/api/v1/activity/email/" + uri: "https://#{apiKey}:X@app.close.io/api/v1/task/" body: JSON.stringify(postData) request.post options, (error, response, body) => - return done(error) if error - result = JSON.parse(body); + return done(error) if error + result = JSON.parse(body) if result.errors or result['field-errors'] - errorMessage = "Close.io Send email POST error for #{fromAddress} #{JSON.stringify(result.errors)} #{JSON.stringify(result['field-errors'])}"; + errorMessage = "Create Close.io call task POST error for #{teacherEmail} #{leadID}" return done(errorMessage) - return done() + + # Create opportunity + postData = + note: "#{licensesRequested} licenses requested" + confidence: 5 + lead_id: leadID + status: 'Active' + value: parseInt(licensesRequested) * amount + value_period: "annual" + options = + uri: "https://#{apiKey}:X@app.close.io/api/v1/opportunity/" + body: JSON.stringify(postData) + request.post options, (error, response, body) => + return done(error) if error + result = JSON.parse(body) + if result.errors or result['field-errors'] + errorMessage = "Create Close.io opportunity POST error for #{teacherEmail} #{leadID}" + return done(errorMessage) + return done() diff --git a/server/routes/contact.coffee b/server/routes/contact.coffee index f391a9050..8cbf51dbe 100644 --- a/server/routes/contact.coffee +++ b/server/routes/contact.coffee @@ -3,8 +3,9 @@ log = require 'winston' User = require '../models/User' sendwithus = require '../sendwithus' async = require 'async' -LevelSession = require '../models/LevelSession' moment = require 'moment' +LevelSession = require '../models/LevelSession' +Product = require '../models/Product' closeIO = require '../lib/closeIO' module.exports.setup = (app) -> @@ -13,10 +14,19 @@ module.exports.setup = (app) -> # log.info "Sending mail from #{req.body.email} saying #{req.body.message}" fromAddress = req.body.sender or req.body.email or req.user.get('email') createMailContent req, fromAddress, (subject, content) -> - if req.body.recipientID is 'schools@codecombat.com' or req.user.isTeacher() - req.user.update({$set: { enrollmentRequestSent: true }}).exec(_.noop) if req.body.recipientID is 'schools@codecombat.com' - closeIO.sendMail fromAddress, subject, content, (err) -> - log.error "Error sending contact form email via Close.io: #{err.message or err}" if err + if req.body.licensesNeeded or req.user.isTeacher() + closeIO.getSalesContactEmail fromAddress, (err, salesContactEmail, userID, leadID) -> + return log.error("Error getting sales contact for #{fromAddress}: #{err.message or err}") if err + closeIO.sendMail fromAddress, subject, content, salesContactEmail, leadID, (err) -> + return log.error("Error sending contact form email via Close.io: #{err.message or err}") if err + if licensesNeeded = req.body.licensesNeeded + Product.findOne({name: 'course'}).exec (err, product) => + return log.error(err) if err + return log.error('course product not found') if not product + amount = product.get('amount') + closeIO.processLicenseRequest fromAddress, userID, leadID, licensesNeeded, amount, (err) -> + return log.error("Error processing license request via Close.io: #{err.message or err}") if err + req.user.update({$set: { enrollmentRequestSent: true }}).exec(_.noop) else createSendWithUsContext req, fromAddress, subject, content, (context) -> sendwithus.api.send context, (err, result) -> @@ -53,7 +63,7 @@ createSendWithUsContext = (req, fromAddress, subject, content, done) -> premium = user?.isPremium() teacher = user?.isTeacher() - if recipientID is 'schools@codecombat.com' or teacher + if teacher or req.body.licensesNeeded return done("Tried to send a teacher contact us email via sendwithus #{fromAddress} #{subject}") toAddress = switch From bbb70fd486758ed1cfc96d39f22050a29655ea9d Mon Sep 17 00:00:00 2001 From: Rob <rob@codecombat.com> Date: Thu, 23 Jun 2016 09:34:37 -0700 Subject: [PATCH 8/8] Tighten modernness requirements (mostly to exclude Edge 13) --- app/assets/javascripts/workers/aether_worker.js | 2 +- app/assets/javascripts/workers/worker_world.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/workers/aether_worker.js b/app/assets/javascripts/workers/aether_worker.js index 5e45c7eda..4d2db81d4 100644 --- a/app/assets/javascripts/workers/aether_worker.js +++ b/app/assets/javascripts/workers/aether_worker.js @@ -5,7 +5,7 @@ importScripts("/javascripts/lodash.js", "/javascripts/aether.js"); try { //Detect very modern javascript support. - (0,eval("'use strict'; let test = (class Test { *gen() { yield yield * () => WeakMap; } });")); + (0,eval("'use strict'; let test = (class Test { *gen(a=7) { yield yield * () => WeakMap; } });")); console.log("Modern javascript detected, aw yeah!"); self.importScripts('/javascripts/esper.modern.js'); } catch (e) { diff --git a/app/assets/javascripts/workers/worker_world.js b/app/assets/javascripts/workers/worker_world.js index f76a07483..dcfc5b13b 100644 --- a/app/assets/javascripts/workers/worker_world.js +++ b/app/assets/javascripts/workers/worker_world.js @@ -66,7 +66,7 @@ self.console = console; self.importScripts('/javascripts/lodash.js', '/javascripts/world.js', '/javascripts/aether.js'); try { //Detect very modern javascript support. - (0,eval("'use strict'; let test = (class Test { *gen() { yield yield * () => WeakMap; } });")); + (0,eval("'use strict'; let test = (class Test { *gen(a=7) { yield yield * () => WeakMap; } });")); console.log("Modern javascript detected, aw yeah!"); self.importScripts('/javascripts/esper.modern.js'); } catch (e) {