mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-05-04 09:53:55 -04:00
Merge remote-tracking branch 'codecombat/master'
This commit is contained in:
commit
70fe21f645
12 changed files with 148 additions and 34 deletions
app
assets/javascripts/workers
lib/world
templates/courses
views
editor
play/level/tome
teachers
server
|
@ -1,7 +1,18 @@
|
||||||
var window = self;
|
var window = self;
|
||||||
var Global = 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.");
|
//console.log("Aether Tome worker has finished importing scripts.");
|
||||||
var aethers = {};
|
var aethers = {};
|
||||||
var languagesImported = {};
|
var languagesImported = {};
|
||||||
|
|
|
@ -63,7 +63,18 @@ var console = {
|
||||||
console.error = console.warn = console.info = console.debug = console.log;
|
console.error = console.warn = console.info = console.debug = console.log;
|
||||||
self.console = console;
|
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 myImportScripts = importScripts;
|
||||||
|
|
||||||
var languagesImported = {};
|
var languagesImported = {};
|
||||||
|
@ -402,6 +413,17 @@ self.serializeFramesSoFar = function serializeFramesSoFar() {
|
||||||
self.world.framesSerializedSoFar = self.world.frames.length;
|
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() {
|
self.onWorldLoaded = function onWorldLoaded() {
|
||||||
if(self.world.framesSerializedSoFar == self.world.frames.length) return;
|
if(self.world.framesSerializedSoFar == self.world.frames.length) return;
|
||||||
if(self.world.ended)
|
if(self.world.ended)
|
||||||
|
@ -419,12 +441,9 @@ self.onWorldLoaded = function onWorldLoaded() {
|
||||||
return console.log('Headless simulation completed in ' + diff + 'ms.');
|
return console.log('Headless simulation completed in ' + diff + 'ms.');
|
||||||
|
|
||||||
var worldEnded = self.world.ended;
|
var worldEnded = self.world.ended;
|
||||||
|
var serialized;
|
||||||
var transferableSupported = self.transferableSupported();
|
var transferableSupported = self.transferableSupported();
|
||||||
try {
|
if ( !( serialized = trySerialize()) ) {
|
||||||
var serialized = self.world.serialize();
|
|
||||||
}
|
|
||||||
catch(error) {
|
|
||||||
console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace);
|
|
||||||
self.destroyWorld();
|
self.destroyWorld();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,11 +162,25 @@ module.exports = class World
|
||||||
|
|
||||||
shouldContinueLoading: (t1, loadProgressCallback, skipDeferredLoading, continueLaterFn) ->
|
shouldContinueLoading: (t1, loadProgressCallback, skipDeferredLoading, continueLaterFn) ->
|
||||||
t2 = now()
|
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
|
if @realTime
|
||||||
shouldUpdateProgress = @shouldUpdateRealTimePlayback t2
|
shouldUpdateProgress = @shouldUpdateRealTimePlayback t2
|
||||||
shouldDelayRealTimeSimulation = not shouldUpdateProgress and @shouldDelayRealTimeSimulation t2
|
shouldDelayRealTimeSimulation = not shouldUpdateProgress and @shouldDelayRealTimeSimulation t2
|
||||||
else
|
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
|
shouldDelayRealTimeSimulation = false
|
||||||
return true unless shouldUpdateProgress or shouldDelayRealTimeSimulation
|
return true unless shouldUpdateProgress or shouldDelayRealTimeSimulation
|
||||||
# Stop loading frames for now; continue in a moment.
|
# Stop loading frames for now; continue in a moment.
|
||||||
|
|
|
@ -57,6 +57,8 @@ block content
|
||||||
select.level-select.form-control
|
select.level-select.form-control
|
||||||
if view.campaigns.loaded
|
if view.campaigns.loaded
|
||||||
each level, levelIndex in view.campaigns.get(course.get('campaignID')).getLevels().models
|
each level, levelIndex in view.campaigns.get(course.get('campaignID')).getLevels().models
|
||||||
|
if level.get('type') === 'hero-practice'
|
||||||
|
- continue;
|
||||||
option(value=level.get('slug'))
|
option(value=level.get('slug'))
|
||||||
span
|
span
|
||||||
= levelIndex + 1
|
= levelIndex + 1
|
||||||
|
|
|
@ -239,6 +239,14 @@ module.exports = class CampaignEditorView extends RootView
|
||||||
@campaign.set key, value for key, value of @treema.data
|
@campaign.set key, value for key, value of @treema.data
|
||||||
@campaignView.setCampaign(@campaign)
|
@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) =>
|
onTreemaDoubleClicked: (e, node) =>
|
||||||
path = node.getPath()
|
path = node.getPath()
|
||||||
return unless _.string.startsWith path, '/levels/'
|
return unless _.string.startsWith path, '/levels/'
|
||||||
|
|
|
@ -15,7 +15,7 @@ module.exports = class SettingsTabView extends CocoView
|
||||||
editableSettings: [
|
editableSettings: [
|
||||||
'name', 'description', 'documentation', 'nextLevel', 'background', 'victory', 'i18n', 'icon', 'goals',
|
'name', 'description', 'documentation', 'nextLevel', 'background', 'victory', 'i18n', 'icon', 'goals',
|
||||||
'type', 'terrain', 'showsGuide', 'banner', 'employerDescription', 'loadingTip', 'requiresSubscription',
|
'type', 'terrain', 'showsGuide', 'banner', 'employerDescription', 'loadingTip', 'requiresSubscription',
|
||||||
'helpVideos', 'replayable', 'scoreTypes', 'concepts', 'picoCTFProblem'
|
'helpVideos', 'replayable', 'scoreTypes', 'concepts', 'picoCTFProblem', 'practiceThresholdMinutes'
|
||||||
]
|
]
|
||||||
|
|
||||||
subscriptions:
|
subscriptions:
|
||||||
|
|
|
@ -316,7 +316,7 @@ module.exports = class SpellView extends CocoView
|
||||||
|
|
||||||
xstart = startOfRow(row)
|
xstart = startOfRow(row)
|
||||||
if language is 'python'
|
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]
|
for crow in [docRange.start.row+1..docRange.end.row]
|
||||||
unless requiredIndent.test lines[crow]
|
unless requiredIndent.test lines[crow]
|
||||||
docRange.end.row = crow - 1
|
docRange.end.row = crow - 1
|
||||||
|
|
|
@ -62,7 +62,7 @@ module.exports = class TeachersContactModal extends ModalView
|
||||||
return unless _.isEmpty(formErrors)
|
return unless _.isEmpty(formErrors)
|
||||||
|
|
||||||
@state.set('sendingState', 'sending')
|
@state.set('sendingState', 'sending')
|
||||||
data = _.extend({ country: me.get('country'), recipientID: 'schools@codecombat.com' }, formValues)
|
data = _.extend({ country: me.get('country') }, formValues)
|
||||||
contact.send({
|
contact.send({
|
||||||
data
|
data
|
||||||
context: @
|
context: @
|
||||||
|
|
|
@ -207,6 +207,7 @@ exports.config =
|
||||||
copyTo:
|
copyTo:
|
||||||
'lib/ace': ['node_modules/ace-builds/src-min-noconflict/*']
|
'lib/ace': ['node_modules/ace-builds/src-min-noconflict/*']
|
||||||
'fonts': ['bower_components/openSansCondensed/*', 'bower_components/openSans/*']
|
'fonts': ['bower_components/openSansCondensed/*', 'bower_components/openSans/*']
|
||||||
|
'javascripts': ['bower_components/esper.js/esper.modern.js']
|
||||||
autoReload:
|
autoReload:
|
||||||
delay: 1000
|
delay: 1000
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,7 @@ LevelHandler = class LevelHandler extends Handler
|
||||||
'scoreTypes'
|
'scoreTypes'
|
||||||
'concepts'
|
'concepts'
|
||||||
'picoCTFProblem'
|
'picoCTFProblem'
|
||||||
|
'practiceThresholdMinutes'
|
||||||
]
|
]
|
||||||
|
|
||||||
postEditableProperties: ['name']
|
postEditableProperties: ['name']
|
||||||
|
|
|
@ -66,33 +66,81 @@ module.exports =
|
||||||
return done("Unexpected activities format: " + body) unless activities.data?
|
return done("Unexpected activities format: " + body) unless activities.data?
|
||||||
for activity in activities.data when activity._type is 'Email'
|
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
|
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)
|
return done(null, config.mail.supportSchools, lead.id)
|
||||||
catch error
|
catch error
|
||||||
log.error("closeIO.getSalesContactEmail Error for #{email}: #{JSON.stringify(error)}")
|
log.error("closeIO.getSalesContactEmail Error for #{email}: #{JSON.stringify(error)}")
|
||||||
return done(error)
|
return done(error)
|
||||||
|
|
||||||
sendMail: (fromAddress, subject, content, done) ->
|
sendMail: (fromAddress, subject, content, salesContactEmail, leadID, done) ->
|
||||||
# log.info("DEBUG: closeIO.sendMail #{fromAddress} #{subject} #{content}")
|
# log.info("DEBUG: closeIO.sendMail #{fromAddress} #{subject} #{content}")
|
||||||
@getSalesContactEmail fromAddress, (err, salesContactEmail, leadID) ->
|
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)
|
||||||
return done("Error getting sales contact for #{fromAddress}: #{err}") if err
|
salesContactEmail = matches?[1] ? matches?[2] ? config.mail.supportSchools
|
||||||
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 = config.mail.supportSchools if salesContactEmail?.indexOf('brian@codecombat.com') >= 0
|
||||||
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 =
|
postData =
|
||||||
to: [salesContactEmail]
|
_type: "lead"
|
||||||
sender: config.mail.username
|
|
||||||
subject: subject
|
|
||||||
body_text: content
|
|
||||||
lead_id: leadID
|
lead_id: leadID
|
||||||
status: 'outbox'
|
assigned_to: userID
|
||||||
|
text: "Call #{teacherEmail}"
|
||||||
|
is_complete: false
|
||||||
options =
|
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)
|
body: JSON.stringify(postData)
|
||||||
request.post options, (error, response, body) =>
|
request.post options, (error, response, body) =>
|
||||||
return done(error) if error
|
return done(error) if error
|
||||||
result = JSON.parse(body);
|
result = JSON.parse(body)
|
||||||
if result.errors or result['field-errors']
|
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(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()
|
||||||
|
|
|
@ -3,8 +3,9 @@ log = require 'winston'
|
||||||
User = require '../models/User'
|
User = require '../models/User'
|
||||||
sendwithus = require '../sendwithus'
|
sendwithus = require '../sendwithus'
|
||||||
async = require 'async'
|
async = require 'async'
|
||||||
LevelSession = require '../models/LevelSession'
|
|
||||||
moment = require 'moment'
|
moment = require 'moment'
|
||||||
|
LevelSession = require '../models/LevelSession'
|
||||||
|
Product = require '../models/Product'
|
||||||
closeIO = require '../lib/closeIO'
|
closeIO = require '../lib/closeIO'
|
||||||
|
|
||||||
module.exports.setup = (app) ->
|
module.exports.setup = (app) ->
|
||||||
|
@ -13,10 +14,19 @@ module.exports.setup = (app) ->
|
||||||
# log.info "Sending mail from #{req.body.email} saying #{req.body.message}"
|
# 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')
|
fromAddress = req.body.sender or req.body.email or req.user.get('email')
|
||||||
createMailContent req, fromAddress, (subject, content) ->
|
createMailContent req, fromAddress, (subject, content) ->
|
||||||
if req.body.recipientID is 'schools@codecombat.com' or req.user.isTeacher()
|
if req.body.licensesNeeded or req.user.isTeacher()
|
||||||
req.user.update({$set: { enrollmentRequestSent: true }}).exec(_.noop) if req.body.recipientID is 'schools@codecombat.com'
|
closeIO.getSalesContactEmail fromAddress, (err, salesContactEmail, userID, leadID) ->
|
||||||
closeIO.sendMail fromAddress, subject, content, (err) ->
|
return log.error("Error getting sales contact for #{fromAddress}: #{err.message or err}") if err
|
||||||
log.error "Error sending contact form email via Close.io: #{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
|
else
|
||||||
createSendWithUsContext req, fromAddress, subject, content, (context) ->
|
createSendWithUsContext req, fromAddress, subject, content, (context) ->
|
||||||
sendwithus.api.send context, (err, result) ->
|
sendwithus.api.send context, (err, result) ->
|
||||||
|
@ -53,7 +63,7 @@ createSendWithUsContext = (req, fromAddress, subject, content, done) ->
|
||||||
premium = user?.isPremium()
|
premium = user?.isPremium()
|
||||||
teacher = user?.isTeacher()
|
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}")
|
return done("Tried to send a teacher contact us email via sendwithus #{fromAddress} #{subject}")
|
||||||
|
|
||||||
toAddress = switch
|
toAddress = switch
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue