diff --git a/app/assets/javascripts/workers/aether_worker.js b/app/assets/javascripts/workers/aether_worker.js index 810537599..4d2db81d4 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(a=7) { 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..dcfc5b13b 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(a=7) { 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/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 diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index 4cff9c845..ed78d3b8f 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[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/' 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/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 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/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 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'] 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