Merge remote-tracking branch 'codecombat/master'

This commit is contained in:
Cat Sync 2016-04-14 16:19:14 -04:00
commit 7bf7fdd0ce
106 changed files with 1188 additions and 542 deletions

View file

@ -312,6 +312,7 @@ self.setupDebugWorldToRunUntilFrame = function (args) {
self.debugWorld = new World(args.userCodeMap);
self.debugWorld.levelSessionIDs = args.levelSessionIDs;
self.debugWorld.submissionCount = args.submissionCount;
self.debugWorld.fixedSeed = args.fixedSeed;
self.debugWorld.flagHistory = args.flagHistory;
self.debugWorld.difficulty = args.difficulty;
if (args.level)
@ -373,6 +374,7 @@ self.runWorld = function runWorld(args) {
self.world = new World(args.userCodeMap);
self.world.levelSessionIDs = args.levelSessionIDs;
self.world.submissionCount = args.submissionCount;
self.world.fixedSeed = args.fixedSeed;
self.world.flagHistory = args.flagHistory || [];
self.world.difficulty = args.difficulty || 0;
if(args.level)
@ -412,15 +414,17 @@ self.onWorldLoaded = function onWorldLoaded() {
self.goalManager.worldGenerationEnded();
var goalStates = self.goalManager.getGoalStates();
var overallStatus = self.goalManager.checkOverallStatus();
if(self.world.ended)
self.postMessage({type: 'end-load-frames', goalStates: goalStates, overallStatus: overallStatus});
var totalFrames = self.world.totalFrames;
if(self.world.ended) {
var lastFrameHash = self.world.frames[totalFrames - 2].hash
self.postMessage({type: 'end-load-frames', goalStates: goalStates, overallStatus: overallStatus, totalFrames: totalFrames, lastFrameHash: lastFrameHash});
}
var t1 = new Date();
var diff = t1 - self.t0;
if(self.world.headless)
return console.log('Headless simulation completed in ' + diff + 'ms.');
var worldEnded = self.world.ended;
var totalFrames = self.world.totalFrames;
var transferableSupported = self.transferableSupported();
try {
var serialized = self.world.serialize();

View file

@ -1,6 +1,6 @@
go = (path, options) -> -> @routeDirectly path, arguments, options
redirect = (path) -> -> @navigate(path, { trigger: true, replace: true })
module.exports = class CocoRouter extends Backbone.Router
initialize: ->
@ -87,6 +87,8 @@ module.exports = class CocoRouter extends Backbone.Router
'editor/poll': go('editor/poll/PollSearchView')
'editor/poll/:articleID': go('editor/poll/PollEditView')
'editor/thang-tasks': go('editor/ThangTasksView')
'editor/verifier': go('editor/verifier/VerifierView')
'editor/verifier/:levelID': go('editor/verifier/VerifierView')
'file/*path': 'routeToServer'
@ -156,7 +158,7 @@ module.exports = class CocoRouter extends Backbone.Router
return @routeDirectly('teachers/RestrictedToTeachersView')
if options.studentsOnly and me.isTeacher()
return @routeDirectly('courses/RestrictedToStudentsView')
path = 'play/CampaignView' if window.serverConfig.picoCTF and not /^(views)?\/?play/.test(path)
path = "views/#{path}" if not _.string.startsWith(path, 'views/')
ViewClass = @tryToLoadModule path
@ -210,7 +212,7 @@ module.exports = class CocoRouter extends Backbone.Router
application.facebookHandler.renderButtons()
application.gplusHandler.renderButtons()
twttr?.widgets?.load?()
activateTab: ->
base = _.string.words(document.location.pathname[1..], '/')[0]
$("ul.nav li.#{base}").addClass('active')

View file

@ -82,7 +82,7 @@ module.exports = class Angel extends CocoClass
clearTimeout @condemnTimeout
when 'end-load-frames'
clearTimeout @condemnTimeout
@beholdGoalStates event.data.goalStates, event.data.overallStatus # Work ends here if we're headless.
@beholdGoalStates event.data.goalStates, event.data.overallStatus, false, event.data.totalFrames, event.data.lastFrameHash # Work ends here if we're headless.
when 'end-preload-frames'
clearTimeout @condemnTimeout
@beholdGoalStates event.data.goalStates, event.data.overallStatus, true
@ -125,10 +125,13 @@ module.exports = class Angel extends CocoClass
else
@log 'Received unsupported message:', event.data
beholdGoalStates: (goalStates, overallStatus, preload=false) ->
beholdGoalStates: (goalStates, overallStatus, preload=false, totalFrames=undefined, lastFrameHash=undefined) ->
return if @aborting
Backbone.Mediator.publish 'god:goals-calculated', goalStates: goalStates, preload: preload, overallStatus: overallStatus, god: @shared.god
@shared.god.trigger 'goals-calculated', goalStates: goalStates, preload: preload, overallStatus: overallStatus
event = goalStates: goalStates, preload: preload, overallStatus: overallStatus, god: @shared.god
event.totalFrames = totalFrames if totalFrames?
event.lastFrameHash = lastFrameHash if lastFrameHash?
Backbone.Mediator.publish 'god:goals-calculated', event
@shared.god.trigger 'goals-calculated', event
@finishWork() if @shared.headless
beholdWorld: (serialized, goalStates, startFrame, endFrame, streamingWorld) ->
@ -274,15 +277,16 @@ module.exports = class Angel extends CocoClass
simulateSync: (work) =>
console?.profile? "World Generation #{(Math.random() * 1000).toFixed(0)}" if imitateIE9?
work.t0 = now()
work.testWorld = testWorld = new World work.userCodeMap
work.testWorld.levelSessionIDs = work.levelSessionIDs
work.testWorld.submissionCount = work.submissionCount
work.testWorld.flagHistory = work.flagHistory ? []
work.testWorld.difficulty = work.difficulty
testWorld.loadFromLevel work.level
work.testWorld.preloading = work.preload
work.testWorld.headless = work.headless
work.testWorld.realTime = work.realTime
work.world = testWorld = new World work.userCodeMap
work.world.levelSessionIDs = work.levelSessionIDs
work.world.submissionCount = work.submissionCount
work.world.fixedSeed = work.fixedSeed
work.world.flagHistory = work.flagHistory ? []
work.world.difficulty = work.difficulty
work.world.loadFromLevel work.level
work.world.preloading = work.preload
work.world.headless = work.headless
work.world.realTime = work.realTime
if @shared.goalManager
testGM = new GoalManager(testWorld)
testGM.setGoals work.goals
@ -295,8 +299,13 @@ module.exports = class Angel extends CocoClass
# If performance was really a priority in IE9, we would rework things to be able to skip this step.
goalStates = testGM?.getGoalStates()
work.testWorld.goalManager.worldGenerationEnded() if work.testWorld.ended
serialized = testWorld.serialize()
work.world.goalManager.worldGenerationEnded() if work.world.ended
if work.headless
@beholdGoalStates goalStates, testGM.checkOverallStatus(), false, work.world.totalFrames, work.world.frames[work.world.totalFrames - 2]?.hash
return
serialized = world.serialize()
window.BOX2D_ENABLED = false
World.deserialize serialized.serializedWorld, @shared.worldClassMap, @shared.lastSerializedWorldFrames, @finishBeholdingWorld(goalStates), serialized.startFrame, serialized.endFrame, work.level
window.BOX2D_ENABLED = true
@ -304,14 +313,14 @@ module.exports = class Angel extends CocoClass
doSimulateWorld: (work) ->
work.t1 = now()
Math.random = work.testWorld.rand.randf # so user code is predictable
Math.random = work.world.rand.randf # so user code is predictable
Aether.replaceBuiltin('Math', Math)
replacedLoDash = _.runInContext(window)
_[key] = replacedLoDash[key] for key, val of replacedLoDash
i = 0
while i < work.testWorld.totalFrames
frame = work.testWorld.getFrame i++
while i < work.world.totalFrames
frame = work.world.getFrame i++
Backbone.Mediator.publish 'god:world-load-progress-changed', progress: 1, god: @shared.god
work.testWorld.ended = true
system.finish work.testWorld.thangs for system in work.testWorld.systems
work.world.ended = true
system.finish work.world.thangs for system in work.world.systems
work.t2 = now()

View file

@ -64,6 +64,7 @@ module.exports = class God extends CocoClass
onTomeCast: (e) ->
return unless e.god is @
@lastSubmissionCount = e.submissionCount
@lastFixedSeed = e.fixedSeed
@lastFlagHistory = (flag for flag in e.flagHistory when flag.source isnt 'code')
@lastDifficulty = e.difficulty
@createWorld e.spells, e.preload, e.realTime
@ -89,11 +90,12 @@ module.exports = class God extends CocoClass
return if hadPreloader
@angelsShare.workQueue = []
@angelsShare.workQueue.push
work =
userCodeMap: userCodeMap
level: @level
levelSessionIDs: @levelSessionIDs
submissionCount: @lastSubmissionCount
fixedSeed: @lastFixedSeed
flagHistory: @lastFlagHistory
difficulty: @lastDifficulty
goals: @angelsShare.goalManager?.getGoals()
@ -101,8 +103,10 @@ module.exports = class God extends CocoClass
preload: preload
synchronous: not Worker? # Profiling world simulation is easier on main thread, or we are IE9.
realTime: realTime
@angelsShare.workQueue.push work
angel.workIfIdle() for angel in @angelsShare.angels
work
getUserCodeMap: (spells) ->
userCodeMap = {}
for spellKey, spell of spells
@ -126,6 +130,7 @@ module.exports = class God extends CocoClass
level: @level
levelSessionIDs: @levelSessionIDs
submissionCount: @lastSubmissionCount
fixedSeed: @fixedSeed
flagHistory: @lastFlagHistory
difficulty: @lastDifficulty
goals: @goalManager?.getGoals()

View file

@ -247,6 +247,7 @@ module.exports = class LevelBus extends Bus
return if _.isEmpty @changedSessionProperties
# don't let peeking admins mess with the session accidentally
return unless @session.get('multiplayer') or @session.get('creator') is me.id
return if @session.fake
Backbone.Mediator.publish 'level:session-will-save', session: @session
patch = {}
patch[prop] = @session.get(prop) for prop of @changedSessionProperties

View file

@ -33,6 +33,7 @@ module.exports = class LevelLoader extends CocoClass
@team = options.team
@headless = options.headless
@sessionless = options.sessionless
@fakeSessionConfig = options.fakeSessionConfig
@spectateMode = options.spectateMode ? false
@observing = options.observing
@courseID = options.courseID
@ -68,11 +69,46 @@ module.exports = class LevelLoader extends CocoClass
@supermodel.addRequestResource(url: '/picoctf/problems', success: (picoCTFProblems) =>
@level?.picoCTFProblem = _.find picoCTFProblems, pid: @level.get('picoCTFProblem')
).load()
@loadSession() unless @sessionless
if @sessionless
null
else if @fakeSessionConfig?
@loadFakeSession()
else
@loadSession()
@populateLevel()
# Session Loading
loadFakeSession: ->
if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop']
@sessionDependenciesRegistered = {}
initVals =
level:
original: @level.get('original')
majorVersion: @level.get('version').major
creator: me.id
state:
complete: false
scripts: {}
permissions: [
{target: me.id, access: 'owner'}
{target: 'public', access: 'write'}
]
codeLanguage: @fakeSessionConfig.codeLanguage or me.get('aceConfig')?.language or 'python'
_id: 'A Fake Session ID'
@session = new LevelSession initVals
@session.loaded = true
@fakeSessionConfig.callback? @session, @level
# TODO: set the team if we need to, for multiplayer
# TODO: just finish the part where we make the submit button do what is right when we are fake
# TODO: anything else to make teacher session-less play make sense when we are fake
# TODO: make sure we are not actually calling extra save/patch/put things throwing warnings because we know we are fake and so we shouldn't try to do that
for method in ['save', 'patch', 'put']
@session[method] = -> console.error "We shouldn't be doing a session.#{method}, since it's a fake session."
@session.fake = true
@loadDependenciesForSession @session
loadSession: ->
if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop']
@sessionDependenciesRegistered = {}
@ -171,7 +207,7 @@ module.exports = class LevelLoader extends CocoClass
browser['platform'] = $.browser.platform if $.browser.platform
browser['version'] = $.browser.version if $.browser.version
session.set 'browser', browser
session.patch()
session.patch() unless session.fake
consolidateFlagHistory: ->
state = @session.get('state') ? {}
@ -341,7 +377,7 @@ module.exports = class LevelLoader extends CocoClass
resource.markLoaded() if resource.spriteSheetKeys.length is 0
denormalizeSession: ->
return if @headless or @sessionDenormalized or @spectateMode or @sessionless
return if @headless or @sessionDenormalized or @spectateMode or @sessionless or me.isTeacher()
# This is a way (the way?) PUT /db/level.sessions/undefined was happening
# See commit c242317d9
return if not @session.id

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "العربية", englishDescription: "Arabi
# completed_level: "Completed Level:"
# course: "Course:"
done: "انتهاء"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "български език", englishDescri
# completed_level: "Completed Level:"
# course: "Course:"
done: "Готово"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
home: "На главната" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Català", englishDescription: "Catalan", tr
completed_level: "Nivell completat:"
course: "Curs:"
done: "Fet"
next_level: "Següent nivell:"
next_level: "Següent nivell"
next_game: "Següent joc"
show_menu: "Mostrar menú del joc"
home: "Inici" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "čeština", englishDescription: "Czech", tr
# completed_level: "Completed Level:"
# course: "Course:"
done: "Hotovo"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
home: "Domů" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "dansk", englishDescription: "Danish", trans
# completed_level: "Completed Level:"
# course: "Course:"
done: "Færdig"
# next_level: "Next Level:"
# next_level: "Next Level"
next_game: "Næste spil"
show_menu: "Vis spil menu"
home: "Hjem" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription:
# completed_level: "Completed Level:"
# course: "Course:"
done: "Fertig"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
home: "Startseite" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Dütsch (Schwiiz)", englishDescription: "Ge
# completed_level: "Completed Level:"
# course: "Course:"
done: "Fertig"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
completed_level: "Abgeschlossene Level:"
course: "Kurse:"
done: "Fertig"
next_level: "Nächster Level:"
next_level: "Nächster Level"
next_game: "Nächstes Spiel"
show_menu: "Menü anzeigen"
home: "Startseite" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Ελληνικά", englishDescription: "Gre
completed_level: "Ολοκληρωμένο Επίπεδο:"
course: "Μάθημα:"
done: "Έτοιμο"
next_level: "Επομένο Επίπεδο:"
next_level: "Επομένο Επίπεδο"
next_game: "Επόμενο παιχνίδι"
show_menu: "Εμφάνιση μενού παιχνιδιού"
home: "Αρχική" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "English (UK)", englishDescription: "English
# completed_level: "Completed Level:"
# course: "Course:"
# done: "Done"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "English (US)", englishDescription: "English
# completed_level: "Completed Level:"
# course: "Course:"
# done: "Done"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -335,10 +335,11 @@
years: "years"
play_level:
level_complete: "Level Complete"
completed_level: "Completed Level:"
course: "Course:"
done: "Done"
next_level: "Next Level:"
next_level: "Next Level"
next_game: "Next game"
show_menu: "Show game menu"
home: "Home" # Not used any more, will be removed soon.
@ -378,6 +379,7 @@
victory_new_item: "New Item"
victory_viking_code_school: "Holy smokes, that was a hard level you just beat! If you aren't already a software developer, you should be. You just got fast-tracked for acceptance with Viking Code School, where you can take your skills to the next level and become a professional web developer in 14 weeks."
victory_become_a_viking: "Become a Viking"
victory_no_progress_for_teachers: "Progress is not saved for teachers. But, you can add a student account to your classroom for yourself."
guide_title: "Guide"
tome_cast_button_run: "Run"
tome_cast_button_running: "Running"
@ -1798,4 +1800,4 @@
one_month_coupon: "coupon: choose either Rails or HTML"
one_month_discount: "discount, 30% off: choose either Rails or HTML"
license: "license"
oreilly: "ebook of your choice"
oreilly: "ebook of your choice"

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Esperanto", englishDescription: "Esperanto"
# completed_level: "Completed Level:"
# course: "Course:"
# done: "Done"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
completed_level: "Nivel Completado:"
course: "Curso:"
done: "Listo"
next_level: "Siguiente Nivel:"
next_level: "Siguiente Nivel"
next_game: "Siguiente juego"
show_menu: "Mostrar menú de juego"
home: "Inicio" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
# completed_level: "Completed Level:"
# course: "Course:"
done: "Hecho"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
home: "Inicio" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Eesti", englishDescription: "Estonian", tra
# completed_level: "Completed Level:"
# course: "Course:"
# done: "Done"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "فارسی", englishDescription: "Persian",
# completed_level: "Completed Level:"
# course: "Course:"
# done: "Done"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "suomi", englishDescription: "Finnish", tran
completed_level: "Suoritit tason:"
course: "Kurssi:"
done: "Valmis"
next_level: "Seuraava taso:"
next_level: "Seuraava taso"
next_game: "Seuraava peli"
show_menu: "Näytä pelivalikko"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
completed_level: "Niveau terminé:"
course: "Cours:"
done: "Fait"
next_level: "Niveau Suivant:"
next_level: "Niveau Suivant"
next_game: "Prochain jeu"
show_menu: "Afficher le menu"
home: "Accueil" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Galego", englishDescription: "Galician", tr
# completed_level: "Completed Level:"
# course: "Course:"
done: "Feito"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
home: "Inicio" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "עברית", englishDescription: "Hebrew",
completed_level: "שלב שהושלם:"
course: "מסלול:"
done: "סיים"
next_level: "השלב הבא:"
next_level: "השלב הבא"
next_game: "המשחק הבא"
show_menu: "הצג תפריט משחק"
home: "בית" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "मानक हिन्दी", englishDe
# completed_level: "Completed Level:"
# course: "Course:"
# done: "Done"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "magyar", englishDescription: "Hungarian", t
completed_level: "Teljesített pálya:"
course: "Kurzus:"
done: "Kész"
next_level: "Következő pálya:"
next_level: "Következő pálya"
next_game: "Következő játék"
show_menu: "Játék Menü"
home: "Kezdőlap" # Not used any more, will be removed soon.

View file

@ -375,7 +375,7 @@ module.exports = nativeDescription: "Bahasa Indonesia", englishDescription: "Ind
# completed_level: "Completed Level:"
# course: "Course:"
# done: "Done"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
completed_level: "Livello completato:"
course: "Corso:"
done: "Fatto"
next_level: "Prossimo livello:"
next_level: "Prossimo livello"
next_game: "Prossimo gioco"
show_menu: "Visualizza menu gioco"
home: "Pagina iniziale" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
completed_level: "コンプリートレベル:"
course: "コース:"
done: "完了"
next_level: "次のレベル:"
next_level: "次のレベル"
next_game: "次のゲーム"
show_menu: "ゲームメニューを見る"
home: "ホーム" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "한국어", englishDescription: "Korean", t
completed_level: "완료된 레벨:"
course: "코스:"
done: "완료"
next_level: "다음 레벨:"
next_level: "다음 레벨"
next_game: "다음 게임"
show_menu: "게임 매뉴 보이기"
home: "" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "lietuvių kalba", englishDescription: "Lith
completed_level: "Įveiktas Lygis:"
course: "Kursas:"
done: "Gerai"
next_level: "Kitas Lygis:"
next_level: "Kitas Lygis"
next_game: "Kitas žaidimas"
show_menu: "Parodyti žaidimo meniu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Македонски", englishDescription:
# completed_level: "Completed Level:"
# course: "Course:"
done: "Готово"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
home: "Дома" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Bahasa Melayu", englishDescription: "Bahasa
# completed_level: "Completed Level:"
# course: "Course:"
# done: "Done"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "မြန်မာစကား", englishDes
# completed_level: "Completed Level:"
# course: "Course:"
# done: "Done"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Norsk Bokmål", englishDescription: "Norweg
# completed_level: "Completed Level:"
# course: "Course:"
done: "Ferdig"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
home: "Hjem" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Nederlands (België)", englishDescription:
completed_level: "Voltooid Level:"
course: "Les:"
done: "Klaar"
next_level: "Volgende Level:"
next_level: "Volgende Level"
next_game: "Volgend spel"
show_menu: "Geef spelmenu weer"
home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription
completed_level: "Voltooid Level:"
course: "Les:"
done: "Klaar"
next_level: "Volgende Level:"
next_level: "Volgende Level"
next_game: "Volgende spel"
show_menu: "Geef spelmenu weer"
home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Norsk Nynorsk", englishDescription: "Norweg
# completed_level: "Completed Level:"
# course: "Course:"
# done: "Done"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "polski", englishDescription: "Polish", tran
completed_level: "Ukończony poziom:"
course: "Kurs:"
done: "Zrobione"
next_level: "Następny poziom:"
next_level: "Następny poziom"
next_game: "Następna gra"
show_menu: "Pokaż menu gry"
home: "Strona główna" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription:
completed_level: "Nivel Completo:"
course: "Curso:"
done: "Pronto"
next_level: "Proximo Nivel:"
next_level: "Proximo Nivel"
next_game: "Próximo jogo"
show_menu: "Mostrar menu do jogo"
home: "Início" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
completed_level: "Nível Completo:"
course: "Curso:"
done: "Concluir"
next_level: "Próximo Nível:"
next_level: "Próximo Nível"
next_game: "Próximo jogo"
show_menu: "Mostrar o menu do jogo"
home: "Início" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "limba română", englishDescription: "Roman
# completed_level: "Completed Level:"
# course: "Course:"
done: "Gata"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
home: "Acasă" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
completed_level: "Завершённый уровень:"
course: "Курс:"
done: "Готово"
next_level: "Следующий уровень:"
next_level: "Следующий уровень"
next_game: "Следующая игра"
show_menu: "Показать меню игры"
home: "На главную" # Not used any more, will be removed soon.

View file

@ -26,12 +26,12 @@ module.exports = nativeDescription: "slovenčina", englishDescription: "Slovak",
learn_more: "Ďalšie informácie"
classroom_in_a_box: "Virtuálna trieda pre výuku programovania."
codecombat_is: "CodeCombat je platforma <strong>pre študentov</strong>, kde sa naučia programovať hrou. "
our_courses: "Naše kurzy boli špeciálne testované <strong>v reálnych triedach/strong> a to dokonca aj učiteľmi, ktorí nemali predchádzajúce skúsenosti v programovaní."
our_courses: "Naše kurzy boli špeciálne testované <strong>v reálnych triedach</strong> a to dokonca aj učiteľmi, ktorí nemali predchádzajúce skúsenosti v programovaní."
top_screenshots_hint: "Zmeny v správaní programu vidia študenti v reálnom čase."
designed_with: "Navrhnuté s ohľadom na potreby učiteľov"
real_code: "Skutočný kód"
from_the_first_level: "od úplného začiatku"
getting_students: "Umožniť študentom písať kód, čo najrýchlejšie, je kritické pri výuke syntaxe a správnej štruktúry programu."
getting_students: "Umožniť študentom písať kód, čo najrýchlejšie,čo je kritické pri výuke syntaxe a správnej štruktúry programu."
educator_resources: "Zdroje pre učiteľov"
course_guides: "a príručky ku kurzu"
teaching_computer_science: "Výučbu programovania zvládne s nami každý učiteľ"
@ -316,8 +316,8 @@ module.exports = nativeDescription: "slovenčina", englishDescription: "Slovak",
course: "Kurz:"
done: "Hotovo"
next_level: "Ďalšia úroveň:"
# next_game: "Next game"
# show_menu: "Show game menu"
next_game: "Ďalšia hra"
show_menu: "Ukáž menu hry"
home: "Domov" # Not used any more, will be removed soon.
level: "Úroveň" # Like "Level: Dungeons of Kithgard"
skip: "Preskočiť"
@ -347,13 +347,13 @@ module.exports = nativeDescription: "slovenčina", englishDescription: "Slovak",
victory_saving_progress: "Stav ukladania"
victory_go_home: "Návrat Domov"
victory_review: "Povedz nám viac!"
# victory_review_placeholder: "How was the level?"
victory_review_placeholder: "Ako sa ti páčilo?"
victory_hour_of_code_done: "Skončil si?"
victory_hour_of_code_done_yes: "Áno, pre dnešok som skončil™!"
victory_experience_gained: "Získaných XP"
victory_gems_gained: "Získaných kryštálov"
# victory_new_item: "New Item"
# victory_viking_code_school: "Holy smokes, that was a hard level you just beat! If you aren't already a software developer, you should be. You just got fast-tracked for acceptance with Viking Code School, where you can take your skills to the next level and become a professional web developer in 14 weeks."
victory_new_item: "Nový predmet"
victory_viking_code_school: "No teda, podarilo sa ti prejsť veľmi ťažkú úroveň! Ak nie si vývojar softvéru, tak je najvyšší čas. Si prijatý do Vikingskej školy programovania,kde môžeš ďalej rozvinúť svoje programovacie schopnosti a stať sa profesionálnym webovým vývojarom za 14 týždňov."
victory_become_a_viking: "Staň sa vikingom!"
guide_title: "Návod"
tome_cast_button_run: "Spustiť"
@ -376,11 +376,11 @@ module.exports = nativeDescription: "slovenčina", englishDescription: "Slovak",
time_current: "Teraz:"
time_total: "Max:"
time_goto: "Choď na:"
# non_user_code_problem_title: "Unable to Load Level"
# infinite_loop_title: "Infinite Loop Detected"
# infinite_loop_description: "The initial code to build the world never finished running. It's probably either really slow or has an infinite loop. Or there might be a bug. You can either try running this code again or reset the code to the default state. If that doesn't fix it, please let us know."
# check_dev_console: "You can also open the developer console to see what might be going wrong."
# check_dev_console_link: "(instructions)"
non_user_code_problem_title: "Nie je možné nahrať úroveň."
infinite_loop_title: "Odhalená nekonečná slučka"
infinite_loop_description: "Úvodný kód vytvorenia sveta neskončil. Je buď neskutočne pomalý alebo obsahuje nekonečnú slučku. Možná je aj chyba. Skús spustiť program znovu alebo obnov kód do pôvodného stavu. Ak nič nepomôže, oznám nám to, prosím."
check_dev_console: "Môžeš otvoriť aj Nástroje pre vývojárov a pozri sa, v čom by mohla byť chyba."
check_dev_console_link: "(inštrukcie)"
infinite_loop_try_again: "Skús znova"
infinite_loop_reset_level: "Reštartuj level"
infinite_loop_comment_out: "Zakomentovať môj kód"
@ -442,13 +442,13 @@ module.exports = nativeDescription: "slovenčina", englishDescription: "Slovak",
tip_sharpening_swords: "Naostri si meče."
tip_ratatouille: "Nenechaj nikomu definovať svoje hranice len z dôvodu tvojho pôvodu. Tvojím jediným obmedzením si iba ty sám. - Gusteau, Ratatouille"
tip_nemo: "Ak ťa život zráža dolu, chceš vedieť čo ti pomôže? Plávaj, len stále plávaj. - Dory, Finding Nemo"
# tip_internet_weather: "Just move to the internet, it's great here. We get to live inside where the weather is always awesome. - John Green"
# tip_nerds: "Nerds are allowed to love stuff, like jump-up-and-down-in-the-chair-can't-control-yourself love it. - John Green"
# tip_self_taught: "I taught myself 90% of what I've learned. And that's normal! - Hank Green"
# tip_luna_lovegood: "Don't worry, you're just as sane as I am. - Luna Lovegood"
# tip_good_idea: "The best way to have a good idea is to have a lot of ideas. - Linus Pauling"
# tip_programming_not_about_computers: "Computer Science is no more about computers than astronomy is about telescopes. - Edsger Dijkstra"
# tip_mulan: "Believe you can, then you will. - Mulan"
tip_internet_weather: "Choď na internet, je tam skvele. Ži doma, kde je vždy nádherné počasie. - John Green"
tip_nerds: "Nerdi milujú skákanie na stoličku a zo stoličky. Strácajú pritom kontrolu. - John Green"
tip_self_taught: "90% toho, čo potrebujem, som sa naučil sám. A je to normálne! - Hank Green"
tip_luna_lovegood: "Don't worry, you're just as sane as I am. - Luna Lovegood"
tip_good_idea: "Najlepším spôsobom ako mať dobrý nápad, je mať veľa dobrých nápadov. - Linus Pauling"
tip_programming_not_about_computers: "Veda o počítačoch je o počítačoch v tej miere ako je astronómia o teleskopoch. - Edsger Dijkstra"
tip_mulan: "Ver, že môžeš a potom budeš aj chcieť. - Mulan"
game_menu:
inventory_tab: "Inventár"
@ -513,49 +513,49 @@ module.exports = nativeDescription: "slovenčina", englishDescription: "Slovak",
feature4: "<strong>{{gems}} bonusových diamantov</strong> každý mesiac !"
feature5: "Video tutoriály"
feature6: "Prémiová emailová podpora"
# feature7: "Private <strong>Clans</strong>"
# feature8: "<strong>No ads!</strong>"
feature7: "Súkromné<strong>klany</strong>"
feature8: "<strong>Žiadne reklamy!</strong>"
free: "Zdarma"
month: "mesiac"
# must_be_logged: "You must be logged in first. Please create an account or log in from the menu above."
must_be_logged: "Najskôr sa musíš prihlásiť. Vytvor si účet alebo sa prihlás."
subscribe_title: "Predplatné"
unsubscribe: "Zrušiť predplatné"
confirm_unsubscribe: "Potvrdiť zrušenie predplatného"
never_mind: "Nevadí, stále ťa máme radi"
thank_you_months_prefix: "Ďakujeme za tvoju podporu v posledných"
thank_you_months_suffix: "mesiacoch."
# thank_you: "Thank you for supporting CodeCombat."
# sorry_to_see_you_go: "Sorry to see you go! Please let us know what we could have done better."
# unsubscribe_feedback_placeholder: "O, what have we done?"
# parent_button: "Ask your parent"
# parent_email_description: "We'll email them so they can buy you a CodeCombat subscription."
# parent_email_input_invalid: "Email address invalid."
# parent_email_input_label: "Parent email address"
# parent_email_input_placeholder: "Enter parent email"
# parent_email_send: "Send Email"
# parent_email_sent: "Email sent!"
# parent_email_title: "What's your parent's email?"
# parents: "For Parents"
# parents_title: "Dear Parent: Your child is learning to code. Will you help them continue?"
# parents_blurb1: "Your child has played __nLevels__ levels and learned programming basics. Help cultivate their interest and buy them a subscription so they can keep playing."
# 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 ${{price}} 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."
# payment_methods: "Payment Methods"
# payment_methods_title: "Accepted Payment Methods"
# payment_methods_blurb1: "We currently accept credit cards and Alipay. You can also PayPal {{three_month_price}} USD to nick@codecombat.com with your account email in the memo to purchase three months' subscription and gems, or ${{year_price}} for a year."
# payment_methods_blurb2: "If you require an alternate form of payment, please contact"
# sale_button: "Sale!"
# sale_button_title: "Save $21 when you purchase a 1 year subscription"
# stripe_description: "Monthly Subscription"
# stripe_description_year_sale: "1 Year Subscription (${{discount}} discount)"
# subscription_required_to_play: "You'll need a subscription to play this level."
# unlock_help_videos: "Subscribe to unlock all video tutorials."
# personal_sub: "Personal Subscription" # 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"
thank_you: "Ďakujeme za podporu CodeCombatu."
sorry_to_see_you_go: "Je nám ľúto, že odchádzaš. Čo sme mali urobiť lepšie?"
unsubscribe_feedback_placeholder: "Ó, čo sme ti urobili?"
parent_button: "Spýtaj sa rodičov"
parent_email_description: "Pošleme im email, aby ti mohli predplatiť CodeCombat."
parent_email_input_invalid: "Neplatný email."
parent_email_input_label: "Email rodiča"
parent_email_input_placeholder: "Zadaj email rodiča"
parent_email_send: "Pošli email"
parent_email_sent: "Email odoslaný!"
parent_email_title: "Aký je email jedného z tvojích rodičov?"
parents: "Pre rodičov"
parents_title: "Drahý rodič: Vaše dieťa sa učí programovať. Chcete, aby v tom pokračovalo?"
parents_blurb1: "Vaše dieťa už prešlo __nLevels__ úrovňami a naučilo sa základy programovania. Pomôžte mu v rozvíjaní jeho záujmov a zaplaťte mu predplatné."
parents_blurb1a: "Programovanie je základná zručnosť, ktorú Vaše dieťa určite využije v dospelosti. V roku 2020 budú základné softvérové zručnosti potrebné v 77% povolaní. Po programátoroch je veľký dopyt.Je to tiež najlepšie platené miesto pre ľudí s vysokoškolským vzdelaním."
parents_blurb2: "Za ${{price}} USD/mesiac získa Vaše dieťa nové výzvy každý mesiac a osobnú podporu cez email od profesionálnych programátorov."
parents_blurb3: "Žiadne riziko: 100% garancia vrátenia peňazí,ľahké odhlásenie predplatného."
payment_methods: "Metódy platby"
payment_methods_title: "Akceptované metódy platby"
payment_methods_blurb1: "V súčasmosti akceptujeme kreditné karty a Alipay. Môžete tiež použiť PayPal a poslať {{three_month_price}} USD na email nick@codecombat.com. Uveďte v poznámke ku platbe registračný email a predplaťťe si 3 mesiace alebo za cenu ${{year_price}} si zakúpte ročné predplatné."
payment_methods_blurb2: "Ak požadujete iný spôsob platby, spojte sa s nami"
sale_button: "Kúp!"
sale_button_title: "Objednaj si ročné predplatné a ušetri 21$"
stripe_description: "Mesačné predplatné"
stripe_description_year_sale: "Ročné predplatné (zľava ${{discount}})"
subscription_required_to_play: "Potrebuješ predplatné, ak chceš hrať túto úroveň."
unlock_help_videos: "Predplať si Codecombat a získaj prístup ku videonávodom."
personal_sub: "Predplatné" # Accounts Subscription View below
loading_info: "Nahrávam informácie o predplatnom..."
managed_by: "Riadené"
will_be_cancelled: "Končí"
currently_free: "Nemáš platené predplatné"
# currently_free_until: "You currently have a subscription until"
# was_free_until: "You had a free subscription until"
# managed_subs: "Managed Subscriptions"

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "slovenščina", englishDescription: "Sloven
# completed_level: "Completed Level:"
# course: "Course:"
# done: "Done"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian
completed_level: "Завршен ниво:"
course: "Курс:"
done: "Урађено"
next_level: "Следећи ниво:"
next_level: "Следећи ниво"
next_game: "Следећа игра"
show_menu: "Види мени игре"
home: "Почетна" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Svenska", englishDescription: "Swedish", tr
completed_level: "Avklarad nivå:"
course: "Lektion:"
done: "Klar"
next_level: "Nästa nivå:"
next_level: "Nästa nivå"
next_game: "Nästa spel"
show_menu: "Visa spelmeny"
home: "Hem" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "ไทย", englishDescription: "Thai", tra
# completed_level: "Completed Level:"
# course: "Course:"
done: "เสร็จสิ้น"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
home: "หน้าแรก" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Українська", englishDescription:
# completed_level: "Completed Level:"
# course: "Course:"
done: "Готово"
# next_level: "Next Level:"
# next_level: "Next Level"
next_game: "Наступна гра"
show_menu: "Показати меню гри"
home: "На головну" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "اُردُو", englishDescription: "Urdu",
# completed_level: "Completed Level:"
# course: "Course:"
# done: "Done"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "O'zbekcha", englishDescription: "Uzbek", tr
# completed_level: "Completed Level:"
# course: "Course:"
# done: "Done"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
completed_level: "Hoàn thành Level:"
course: "Khoá học:"
done: "Hoàn thành"
next_level: "Level tiếp theo:"
next_level: "Level tiếp theo"
next_game: "Game kế tiếp"
show_menu: "Hiện game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
completed_level: "完成关卡:"
course: "课程:"
done: "完成"
next_level: "下一个关卡:"
next_level: "下一个关卡"
next_game: "下一场游戏"
show_menu: "显示游戏菜单"
home: "主页" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "吴语", englishDescription: "Wuu (Simplifi
# completed_level: "Completed Level:"
# course: "Course:"
# done: "Done"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
# home: "Home" # Not used any more, will be removed soon.

View file

@ -315,7 +315,7 @@ module.exports = nativeDescription: "吳語", englishDescription: "Wuu (Traditio
# completed_level: "Completed Level:"
# course: "Course:"
done: "妝下落"
# next_level: "Next Level:"
# next_level: "Next Level"
# next_game: "Next game"
# show_menu: "Show game menu"
home: "主頁" # Not used any more, will be removed soon.

View file

@ -248,6 +248,6 @@ module.exports = class Level extends CocoModel
width = c.width if c.width? and c.width > width
height = c.height if c.height? and c.height > height
return {width: width, height: height}
isLadder: ->
return @get('type')?.indexOf('ladder') > -1

View file

@ -23,7 +23,7 @@ module.exports = class SuperModel extends Backbone.Model
console.info "#{_.values(@resources).length} resources."
unfinished = []
for resource in _.values(@resources) when resource
console.info "\t", resource.name, 'loaded', resource.isLoaded
console.info "\t", resource.name, 'loaded', resource.isLoaded, resource.model
unfinished.push resource unless resource.isLoaded
unfinished
@ -158,7 +158,7 @@ module.exports = class SuperModel extends Backbone.Model
# Tracking resources being loaded for this supermodel
finished: ->
return (@progress is 1.0) or (not @denom) or @failed
return (@progress is 1.0) or (not @denom) or @failed
addModelResource: (modelOrCollection, name, fetchOptions, value=1) ->
# Deprecating name. Handle if name is not included
@ -192,8 +192,8 @@ module.exports = class SuperModel extends Backbone.Model
return res
checkName: (name) ->
if _.isString(name)
console.warn("SuperModel name property deprecated. Remove '#{name}' from code.")
#if _.isString(name)
# console.warn("SuperModel name property deprecated. Remove '#{name}' from code.")
storeResource: (resource, value) ->
@rid++

View file

@ -49,6 +49,8 @@ module.exports =
goalStates: goalStatesSchema
preload: {type: 'boolean'}
overallStatus: {type: ['string', 'null'], enum: ['success', 'failure', 'incomplete', null]}
totalFrames: {type: ['integer', 'undefined']}
lastFrameHash: {type: ['number', 'undefined']}
'god:world-load-progress-changed': c.object {required: ['progress', 'god']},
god: {type: 'object'}

View file

@ -70,3 +70,6 @@ module.exports =
'application:service-loaded': c.object {required: ['service']},
service: {type: 'string'} # 'segment'
'test:update': c.object {},
state: {type: 'string'}

View file

@ -12,6 +12,7 @@ module.exports =
preload: {type: 'boolean'}
realTime: {type: 'boolean'}
submissionCount: {type: 'integer'}
fixedSeed: {type: ['integer', 'undefined']}
flagHistory: {type: 'array'}
difficulty: {type: 'integer'}
god: {type: 'object'}

View file

@ -5,4 +5,9 @@
margin-bottom: 5px
p
margin-top: 30px
margin-top: 30px
.course-title
white-space: nowrap
text-overflow: ellipsis
overflow: hidden

View file

@ -20,20 +20,20 @@ block content
.courses.container
- var courses = view.courses.models;
- var i = 0;
while i < courses.length
- var course = courses[i];
- i++;
- var courseIndex = 0;
while courseIndex < courses.length
- var course = courses[courseIndex];
- courseIndex++;
.course.row
.col-sm-9
+course-info(course)
.col-sm-3.hidden
.play-level-form
.col-sm-3
.play-level-form(data-course-id=course.id)
.form-group
label.control-label
span(data-i18n="courses.select_language")
| :
select.form-control
select.language-select.form-control
// TODO: Automate this list @scott
option(value="python")
| Python
@ -51,11 +51,17 @@ block content
label.control-label
span(data-i18n="courses.select_level")
| :
select.form-control
// TODO: Automate this list @scott
option(value='TODO')
| 1. Dungeons of Kithgard
a.btn.btn-lg.btn-primary
select.level-select.form-control
if view.campaigns.loaded
each level, levelIndex in view.campaigns.get(course.get('campaignID')).getLevels().models
option(value=level.get('slug'))
span
= levelIndex + 1
span
| .
span
= level.get('name')
a.play-level-button.btn.btn-lg.btn-primary
span(data-i18n="courses.play_level")
.clearfix

View file

@ -0,0 +1,51 @@
extends /templates/base-flat
block content
.container
each test, id in view.tests
if test.level
if !test.goals
h2(style='color: orange')= test.level.get('name')
small= ' in ' + test.language + ''
else if test.isSucessful()
h2(style='color: green')= test.level.get('name')
small= ' in ' + test.language + ''
else
h2(style='color: red')= test.level.get('name')
small= ' in ' + test.language + ''
div.row(class=(test.isSucessful() && id > 1 ? 'collapse' : 'collapse in'))
div.col-xs-8
if test.solution
pre #{test.solution.source}
else
h4 Solution not found...
div.col-xs-4.well
if test.goals
if test.frames == test.solution.frameCount
div(style='color: green') ✓ Frames: #{test.frames}
else
div(style='color: red') ✘ Frames: #{test.frames} vs #{test.solution.frameCount}
each v,k in test.goals || []
if !test.solution.goals
div(style='color: orange') ? #{k} (#{v.status})
else if v.status == test.solution.goals[k]
div(style='color: green') ✓ #{k} (#{v.status})
else
div(style='color: red') ✘ #{k} (#{v.status} vs #{test.solution.goals[k]})
else
h3 Running....
else
h1 Loading Level...
div#tome-view
div#goals-veiw
br
// TODO: show errors
// TODO: frame length
// TODO: show last frame hash

View file

@ -7,7 +7,7 @@
.levels-link-area
a.levels-link(href=homeLink || "/")
.glyphicon.glyphicon-play
span(data-i18n=ladderGame ? "general.ladder" : "nav.play").home-text Levels
span(data-i18n=me.isTeacher() ? "nav.courses" : (ladderGame ? "general.ladder" : "nav.play")).home-text Levels
if isMultiplayerLevel && !observing
.multiplayer-area-container

View file

@ -2,7 +2,7 @@
#close-modal.well.well-sm.well-parchment(data-dismiss="modal")
span.glyphicon.glyphicon-remove
.well.well-sm.well-parchment
h1 Level Complete
h1(data-i18n='play_level.level_complete')
.modal-body
.container-fluid
@ -10,23 +10,32 @@
- var colClass = view.nextLevel ? 'col-sm-7' : 'col-sm-12'
div(class=colClass)
.well.well-sm.well-parchment
h3.text-uppercase Completed Level:
h3.text-uppercase(data-i18n='play_level.completed_level')
h2.text-uppercase.text-center= i18n(view.level.attributes, 'name')
.well.well-sm.well-parchment
h3.text-uppercase Course:
.row
.col-sm-8
h3.text-uppercase.text-center= i18n(view.course.attributes, 'name')
.col-sm-4
- var stats = view.campaign.statsForSessions(view.levelSessions)
h1
span #{stats.levels.numDone}/#{stats.levels.size}
if me.isTeacher()
h3.course-title
span.text-uppercase.spr(data-i18n='play_level.course')
span.text-uppercase.text-center= i18n(view.course.attributes, 'name')
span(data-i18n='play_level.victory_no_progress_for_teachers')
else
h3.text-uppercase(data-i18n='play_level.course')
.row
.col-sm-8
h3.text-uppercase.text-center= i18n(view.course.attributes, 'name')
.col-sm-4
- var stats = view.campaign.statsForSessions(view.levelSessions)
h1
span #{stats.levels.numDone}/#{stats.levels.size}
if view.nextLevel
.col-sm-5
.well.well-sm.well-parchment
h3.text-uppercase Next Level:
h3.text-uppercase
span(data-i18n='play_level.next_level')
span :
h2.text-uppercase= i18n(view.nextLevel.attributes, 'name')
p= i18n(view.nextLevel.attributes, 'description')
@ -37,6 +46,6 @@
// button#continue-btn.btn.btn-illustrated.btn-default.btn-block.btn-lg.text-uppercase View Leaderboards
.col-sm-5
if view.nextLevel
button#next-level-btn.btn.btn-illustrated.btn-primary.btn-block.btn-lg.text-uppercase Next Level
button#next-level-btn.btn.btn-illustrated.btn-primary.btn-block.btn-lg.text-uppercase(data-i18n='play_level.next_level')
else
button#done-btn.btn.btn-illustrated.btn-primary.btn-block.btn-lg.text-uppercase Done
button#done-btn.btn.btn-illustrated.btn-primary.btn-block.btn-lg.text-uppercase(data-i18n='play_level.done')

View file

@ -48,5 +48,5 @@ module.exports = class ContactModal extends ModalView
updateScreenshot: ->
return unless @screenshotURL
screenshotEl = @$el.find('#contact-screenshot').removeClass('secret')
screenshotEl.find('a').prop('href', @screenshotURL)
screenshotEl.find('img').prop('src', @screenshotURL)
screenshotEl.find('a').prop('href', @screenshotURL.replace("http://codecombat.com/", "/"))
screenshotEl.find('img').prop('src', @screenshotURL.replace("http://codecombat.com/", "/"))

View file

@ -3,6 +3,7 @@ app = require 'core/application'
CocoCollection = require 'collections/CocoCollection'
CocoModel = require 'models/CocoModel'
Course = require 'models/Course'
Campaigns = require 'collections/Campaigns'
Classroom = require 'models/Classroom'
Classrooms = require 'collections/Classrooms'
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
@ -22,6 +23,7 @@ module.exports = class TeacherCoursesView extends RootView
'click .btn-add-students': 'onClickAddStudents'
'click .create-new-class': 'onClickCreateNewClassButton'
'click .edit-classroom-small': 'onClickEditClassroomSmall'
'click .play-level-button': 'onClickPlayLevel'
guideLinks:
{
@ -43,6 +45,9 @@ module.exports = class TeacherCoursesView extends RootView
@classrooms.comparator = '_id'
@listenToOnce @classrooms, 'sync', @onceClassroomsSync
@supermodel.loadCollection(@classrooms, 'classrooms', {data: {ownerID: me.id}})
@campaigns = new Campaigns()
@campaigns.fetch()
@supermodel.trackCollection(@campaigns)
@courseInstances = new CocoCollection([], { url: "/db/course_instance", model: CourseInstance })
@courseInstances.comparator = 'courseID'
@courseInstances.sliceWithMembers = -> return @filter (courseInstance) -> _.size(courseInstance.get('members')) and courseInstance.get('classroomID')
@ -99,6 +104,14 @@ module.exports = class TeacherCoursesView extends RootView
modal = new ClassroomSettingsModal({classroom: classroom})
@openModalView(modal)
@listenToOnce modal, 'hide', @render
onClickPlayLevel: (e) ->
form = $(e.currentTarget).closest('.play-level-form')
levelSlug = form.find('.level-select').val()
courseID = form.data('course-id')
language = form.find('.language-select').val()
url = "/play/level/#{levelSlug}?course=#{courseID}&codeLanguage=#{language}"
application.router.navigate(url, { trigger: true })
onLoaded: ->
super()

View file

@ -0,0 +1,111 @@
CocoClass = require 'core/CocoClass'
SuperModel = require 'models/SuperModel'
{createAetherOptions} = require 'lib/aether_utils'
God = require 'lib/God'
GoalManager = require 'lib/world/GoalManager'
LevelLoader = require 'lib/LevelLoader'
module.exports = class VerifierTest extends CocoClass
constructor: (@levelID, @updateCallback, @supermodel, @language) ->
super()
# TODO: turn this into a Subview
# TODO: listen to Backbone.Mediator.publish 'god:non-user-code-problem', problem: event.data.problem, god: @shared.god from Angel to detect when we can't load the thing
# TODO: listen to the progress report from Angel to show a simulation progress bar (maybe even out of the number of frames we actually know it'll take)
@supermodel ?= new SuperModel()
@language ?= 'python'
@load()
load: ->
@loadStartTime = new Date()
@god = new God maxAngels: 1, headless: true
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, headless: true, fakeSessionConfig: {codeLanguage: @language, callback: @configureSession}
@listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded
onWorldNecessitiesLoaded: ->
# Called when we have enough to build the world, but not everything is loaded
@grabLevelLoaderData()
unless @solution
@updateCallback? state: 'error'
@error = 'No solution present...'
@state = 'error'
return
me.team = @team = 'humans'
@setupGod()
@initGoalManager()
@register()
configureSession: (session, level) =>
# TODO: reach into and find hero and get the config from the solution
try
hero = _.find level.get("thangs"), id: "Hero Placeholder"
programmable = _.find(hero.components, (x) -> x.config?.programmableMethods?.plan).config.programmableMethods.plan
session.solution = _.find (programmable.solutions ? []), language: session.get('codeLanguage')
session.set 'heroConfig', session.solution.heroConfig
session.set 'code', {'hero-placeholder': plan: session.solution.source}
state = session.get 'state'
state.flagHistory = session.solution.flagHistory
state.difficulty = session.solution.difficulty or 0
session.solution.seed = undefined unless _.isNumber session.solution.seed # TODO: migrate away from submissionCount/sessionID seed objects
catch e
@state = 'error'
@error = "Could not load the session solution for #{level.get('name')}: " + e.toString()
grabLevelLoaderData: ->
@world = @levelLoader.world
@level = @levelLoader.level
@session = @levelLoader.session
@solution = @levelLoader.session.solution
setupGod: ->
@god.setLevel @level.serialize @supermodel, @session
@god.setLevelSessionIDs [@session.id]
@god.setWorldClassMap @world.classMap
@god.lastFlagHistory = @session.get('state').flagHistory
@god.lastDifficulty = @session.get('state').difficulty
@god.lastFixedSeed = @session.solution.seed
@god.lastSubmissionCount = 0
initGoalManager: ->
@goalManager = new GoalManager(@world, @level.get('goals'), @team)
@god.setGoalManager @goalManager
register: ->
@listenToOnce @god, 'infinite-loop', @fail # TODO: have one of these
@listenToOnce @god, 'goals-calculated', @processSingleGameResults
@god.createWorld @generateSpellsObject()
@updateCallback? state: 'running'
processSingleGameResults: (e) ->
console.log(e)
@goals = e.goalStates
@frames = e.totalFrames
@lastFrameHash = e.lastFrameHash
@state = 'complete'
@updateCallback? state: @state
isSucessful: () ->
return false unless @frames == @solution.frameCount
if @goals and @solution.goals
for k of @goals
continue if not @solution.goals[k]
return false if @solution.goals[k] != @goals[k].status
return true
fail: (e) ->
@error = 'Failed due to infinate loop.'
@state = 'error'
@updateCallback? state: @state
generateSpellsObject: ->
aetherOptions = createAetherOptions functionName: 'plan', codeLanguage: @session.get('codeLanguage')
spellThang = aether: new Aether aetherOptions
spells = "hero-placeholder/plan": thangs: {'Hero Placeholder': spellThang}, name: 'plan'
source = @session.get('code')['hero-placeholder'].plan
try
spellThang.aether.transpile source
catch e
console.log "Couldn't transpile!\n#{source}\n", e
spellThang.aether.transpile ''
spells

View file

@ -0,0 +1,35 @@
RootView = require 'views/core/RootView'
template = require 'templates/editor/verifier/verifier-view'
VerifierTest = require './VerifierTest'
module.exports = class VerifierView extends RootView
className: 'style-flat'
template: template
id: 'verifier-view'
events:
'input input': 'searchUpdate'
'change input': 'searchUpdate'
constructor: (options, @levelID) ->
super options
# TODO: rework to handle N at a time instead of all at once
# TODO: sort tests by unexpected result first
testLevels = ["dungeons-of-kithgard", "gems-in-the-deep", "shadow-guard", "kounter-kithwise", "crawlways-of-kithgard", "enemy-mine", "illusory-interruption", "forgetful-gemsmith", "signs-and-portents", "favorable-odds", "true-names", "the-prisoner", "banefire", "the-raised-sword", "kithgard-librarian", "fire-dancing", "loop-da-loop", "haunted-kithmaze", "riddling-kithmaze", "descending-further", "the-second-kithmaze", "dread-door", "cupboards-of-kithgard", "hack-and-dash", "known-enemy", "master-of-names", "lowly-kithmen", "closing-the-distance", "tactical-strike", "the-skeleton", "a-mayhem-of-munchkins", "the-final-kithmaze", "the-gauntlet", "radiant-aura", "kithgard-gates", "destroying-angel", "deadly-dungeon-rescue", "kithgard-brawl", "cavern-survival", "breakout", "attack-wisely", "kithgard-mastery", "kithgard-apprentice", "robot-ragnarok", "defense-of-plainswood", "peasant-protection", "forest-fire-dancing"]
#testLevels = testLevels.slice 0, 15
levelIDs = if @levelID then [@levelID] else testLevels
#supermodel = if @levelID then @supermodel else undefined
@tests = []
async.eachSeries levelIDs, (levelID, lnext) =>
async.eachSeries ['python','javascript'], (lang, next) =>
@tests.unshift new VerifierTest levelID, (e) =>
@update(e)
next() if e.state in ['complete', 'error']
, @supermodel, lang
, -> lnext()
update: (event) =>
# TODO: show unworkable tests instead of hiding them
# TODO: destroy them Tests after or something
console.log 'got event', event, 'on some test'
@tests = _.filter @tests, (test) -> test.state isnt 'error'
@render()

View file

@ -75,7 +75,10 @@ module.exports = class ControlBarView extends CocoView
c.spectateGame = @spectateGame
c.observing = @observing
@homeViewArgs = [{supermodel: if @hasReceivedMemoryWarning then null else @supermodel}]
if @level.get('type', true) in ['ladder', 'ladder-tutorial', 'hero-ladder', 'course-ladder']
if me.isTeacher()
@homeLink = "/teachers/courses"
@homeViewClass = "views/courses/TeacherCoursesView"
else if @level.get('type', true) in ['ladder', 'ladder-tutorial', 'hero-ladder', 'course-ladder']
levelID = @level.get('slug')?.replace(/\-tutorial$/, '') or @level.id
@homeLink = '/play/ladder/' + levelID
@homeViewClass = 'views/ladder/LadderView'

View file

@ -135,8 +135,11 @@ module.exports = class PlayLevelView extends RootView
load: ->
@loadStartTime = new Date()
@god = new God debugWorker: true
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @opponentSessionID, team: @getQueryVariable('team'), observing: @observing, courseID: @courseID
@god = new God()
levelLoaderOptions = supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @opponentSessionID, team: @getQueryVariable('team'), observing: @observing, courseID: @courseID
if me.isTeacher()
levelLoaderOptions.fakeSessionConfig = {}
@levelLoader = new LevelLoader levelLoaderOptions
@listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded
trackLevelLoadEnd: ->
@ -512,7 +515,7 @@ module.exports = class PlayLevelView extends RootView
break
Backbone.Mediator.publish 'tome:cast-spell', {}
onWindowResize: (e) =>
onWindowResize: (e) =>
@endHighlight()
onDisableControls: (e) ->
@ -549,7 +552,7 @@ module.exports = class PlayLevelView extends RootView
@endHighlight()
options = {level: @level, supermodel: @supermodel, session: @session, hasReceivedMemoryWarning: @hasReceivedMemoryWarning, courseID: @courseID, courseInstanceID: @courseInstanceID, world: @world}
ModalClass = if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] then HeroVictoryModal else VictoryModal
ModalClass = CourseVictoryModal if @isCourseMode()
ModalClass = CourseVictoryModal if @isCourseMode() or me.isTeacher()
ModalClass = PicoCTFVictoryModal if window.serverConfig.picoCTF
victoryModal = new ModalClass(options)
@openModalView(victoryModal)

View file

@ -11,6 +11,7 @@ EarnedAchievement = require 'models/EarnedAchievement'
LocalMongo = require 'lib/LocalMongo'
ProgressView = require './ProgressView'
NewItemView = require './NewItemView'
utils = require 'core/utils'
module.exports = class CourseVictoryModal extends ModalView
id: 'course-victory-modal'
@ -100,20 +101,23 @@ module.exports = class CourseVictoryModal extends ModalView
triggeredBy: @session.id
achievement: achievement.id
})
ea.save()
# Can't just add models to supermodel because each ea has the same url
ea.sr = @supermodel.addSomethingResource(ea.cid)
@newEarnedAchievements.push ea
@listenToOnce ea, 'sync', (model) ->
model.sr.markLoaded()
if _.all((ea.id for ea in @newEarnedAchievements))
unless me.loading
@supermodel.loadModel(me, {cache: false})
@newEarnedAchievementsResource.markLoaded()
if me.isTeacher()
@newEarnedAchievements.push ea
else
ea.save()
# Can't just add models to supermodel because each ea has the same url
ea.sr = @supermodel.addSomethingResource(ea.cid)
@newEarnedAchievements.push ea
@listenToOnce ea, 'sync', (model) ->
model.sr.markLoaded()
if _.all((ea.id for ea in @newEarnedAchievements))
unless me.loading
@supermodel.loadModel(me, {cache: false})
@newEarnedAchievementsResource.markLoaded()
# have to use a something resource because addModelResource doesn't handle models being upserted/fetched via POST like we're doing here
@newEarnedAchievementsResource = @supermodel.addSomethingResource('earned achievements') if @newEarnedAchievements.length
unless me.isTeacher()
# have to use a something resource because addModelResource doesn't handle models being upserted/fetched via POST like we're doing here
@newEarnedAchievementsResource = @supermodel.addSomethingResource('earned achievements') if @newEarnedAchievements.length
onLoaded: ->
@ -160,9 +164,15 @@ module.exports = class CourseVictoryModal extends ModalView
@showView(@views[index+1])
onNextLevel: ->
link = "/play/level/#{@nextLevel.get('slug')}?course=#{@courseID}&course-instance=#{@courseInstanceID}"
if me.isTeacher()
link = "/play/level/#{@nextLevel.get('slug')}?course=#{@courseID}&codeLanguage=#{utils.getQueryVariable('codeLanguage', 'python')}"
else
link = "/play/level/#{@nextLevel.get('slug')}?course=#{@courseID}&course-instance=#{@courseInstanceID}"
application.router.navigate(link, {trigger: true})
onDone: ->
link = "/courses/#{@courseID}/#{@courseInstanceID}"
if me.isTeacher()
link = "/teachers/courses"
else
link = "/courses/#{@courseID}/#{@courseInstanceID}"
application.router.navigate(link, {trigger: true})

View file

@ -517,6 +517,17 @@ module.exports = class SpellView extends CocoView
when 'python' then 'while True'
when 'coffeescript' then 'loop'
else 'while true'
# For now, update autocomplete to use hero instead of self/this, if hero is already used in the source.
# Later, we should make this happen all the time - or better yet update the snippets.
source = @getSource()
if /hero/.test(source)
thisToken =
'python': /self/,
'javascript': /this/,
'lua': /self/
if thisToken[e.language] and thisToken[e.language].test(content)
content = content.replace thisToken[e.language], 'hero'
entry =
content: content
meta: $.i18n.t('keyboard_shortcuts.press_enter', defaultValue: 'press enter')
@ -1193,7 +1204,7 @@ module.exports = class SpellView extends CocoView
onSpellBeautify: (e) ->
return unless @spellThang and (@ace.isFocused() or e.spell is @spell)
ugly = @getSource()
pretty = @spellThang.aether.beautify ugly
pretty = @spellThang.aether.beautify(ugly.replace /\bloop\b/g, 'while (__COCO_LOOP_CONSTRUCT__)').replace /while \(__COCO_LOOP_CONSTRUCT__\)/g, 'loop'
@ace.setValue pretty
onMaximizeToggled: (e) ->

View file

@ -169,7 +169,7 @@ module.exports = class TomeView extends CocoView
difficulty = sessionState.difficulty ? 0
if @options.observing
difficulty = Math.max 0, difficulty - 1 # Show the difficulty they won, not the next one.
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload, realTime: realTime, submissionCount: sessionState.submissionCount ? 0, flagHistory: sessionState.flagHistory ? [], difficulty: difficulty, god: @options.god
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload, realTime: realTime, submissionCount: sessionState.submissionCount ? 0, flagHistory: sessionState.flagHistory ? [], difficulty: difficulty, god: @options.god, fixedSeed: @options.fixedSeed
onToggleSpellList: (e) ->
@spellList?.rerenderEntries()

View file

@ -29,7 +29,7 @@ options =
simulateOnlyOneGame: simulateOneGame
options.heapdump = require('heapdump') if options.heapdump
server = if options.testing then 'http://127.0.0.1:3000' else 'https://codecombat.com'
server = if options.testing then 'http://127.0.0.1:3000' else 'http://direct.codecombat.com'
# Use direct instead of live site because jQlone's requests proxy doesn't do caching properly and CloudFlare gets too aggressive.
# Disabled modules
@ -43,22 +43,25 @@ disable = [
# Global emulated stuff
GLOBAL.window = GLOBAL
GLOBAL.document = location: pathname: 'headless_client'
GLOBAL.document =
location:
pathname: 'headless_client'
search: 'esper=1'
GLOBAL.console.debug = console.log
GLOBAL.serverConfig =
picoCTF: false
production: false
try
GLOBAL.Worker = require('webworker-threads').Worker
Worker::removeEventListener = (what) ->
if what is 'message'
@onmessage = -> #This webworker api has only one event listener at a time.
catch
console.log ""
console.log "Headless client needs the webworker-threads package from NPM to function."
console.log "Try installing it with the command:"
console.log ""
console.log " npm install webworker-threads"
console.log ""
process.exit(1)
# Fall back to IE compatibility mode where it runs synchronously with no web worker.
# (Which we will be doing now always because webworker-threads doesn't run in newer node versions.)
eval require('fs').readFileSync('./vendor/scripts/Box2dWeb-2.1.a.3.js', 'utf8')
GLOBAL.Box2D = Box2D
Worker::removeEventListener = (what) ->
if what is 'message'
@onmessage = -> #This webworker api has only one event listener at a time.
GLOBAL.tv4 = require('tv4').tv4
GLOBAL.TreemaUtils = require bowerComponentsPath + 'treema/treema-utils'
GLOBAL.marked = setOptions: ->

View file

@ -0,0 +1,63 @@
child_process = require 'child_process'
chalk = require 'chalk'
_ = require 'lodash'
Promise = require 'bluebird'
path = require 'path'
cores = 4
list = [
"dungeons-of-kithgard", "gems-in-the-deep", "shadow-guard", "kounter-kithwise", "crawlways-of-kithgard",
"enemy-mine", "illusory-interruption", "forgetful-gemsmith", "signs-and-portents", "favorable-odds",
"true-names", "the-prisoner", "banefire", "the-raised-sword", "kithgard-librarian", "fire-dancing",
"loop-da-loop", "haunted-kithmaze", "riddling-kithmaze", "descending-further", "the-second-kithmaze",
"dread-door", "cupboards-of-kithgard", "hack-and-dash", "known-enemy", "master-of-names",
"lowly-kithmen", "closing-the-distance", "tactical-strike", "the-skeleton", "a-mayhem-of-munchkins",
"the-final-kithmaze", "the-gauntlet", "radiant-aura", "kithgard-gates", "destroying-angel", "deadly-dungeon-rescue",
"kithgard-brawl", "cavern-survival", "breakout", "attack-wisely", "kithgard-mastery", "kithgard-apprentice",
"robot-ragnarok", "defense-of-plainswood", "peasant-protection", "forest-fire-dancing"
]
c1 = ["dungeons-of-kithgard", "gems-in-the-deep", "shadow-guard", "enemy-mine", "true-names", "fire-dancing", "loop-da-loop", "haunted-kithmaze", "the-second-kithmaze", "dread-door", "cupboards-of-kithgard", "breakout", "known-enemy", "master-of-names", "a-mayhem-of-munchkins", "the-gauntlet", "the-final-kithmaze", "kithgard-gates", "wakka-maul"]
c2 = ["defense-of-plainswood", "course-winding-trail", "patrol-buster", "endangered-burl", "thumb-biter", "gems-or-death", "village-guard", "thornbush-farm", "back-to-back", "ogre-encampment", "woodland-cleaver", "shield-rush", "range-finder", "peasant-protection", "munchkin-swarm", "forest-fire-dancing", "stillness-in-motion", "the-agrippa-defense", "backwoods-bombardier", "coinucopia", "copper-meadows", "drop-the-flag", "mind-the-trap", "signal-corpse", "rich-forager", "cross-bones"]
list = [].concat(c1, c2)
list = c1
list = _.shuffle(list);
lpad = (s, l, color = 'white') ->
return chalk[color](s.substring(0, l)) if s.length >= l
return chalk[color](s + new Array(l - s.length).join(' '))
chunks = _.groupBy list, (v,i) -> i%cores
_.forEach chunks, (list, cid) ->
console.log(list)
cp = child_process.fork path.join(__dirname, './verifier.js'), list, silent: true
cp.on 'message', (m) ->
return if m.state is 'running'
okay = true
goals = _.map m.observed.goals, (v,k) ->
return lpad('No Goals Set', 15, 'yellow') unless m.solution.goals
lpad(k, 15, if v == m.solution.goals[k] then 'green' else 'red')
extra = []
if m.observed.frameCount == m.solution.frameCount
extra.push lpad('F:' + m.observed.frameCount, 15, 'green')
else
extra.push lpad('F:' + m.observed.frameCount + ' vs ' + m.solution.frameCount , 15, 'red')
okay = false
if m.observed.lastHash == m.solution.lastHash
extra.push lpad('Hash', 5, 'green')
else
extra.push lpad('Hash' , 5, 'red')
okay = false
col = if okay then 'green' else 'red'
if m.state is 'error' or m.error
console.log lpad(m.level, 30, 'red') + lpad(m.language, 15, 'cyan') + chalk.red(m.error)
else
console.log lpad(m.level, 30, col) + lpad(m.language, 15, 'cyan') + ' ' + extra.join(' ') + ' ' + goals.join(' ')

View file

@ -8,8 +8,6 @@ module.exports = $ = (input) ->
append: (input)-> exports: ()->
# Non-standard jQuery stuff. Don't use outside of server.
$._debug = false
$._server = 'https://codecombat.com'
$._cookies = request.jar()
$.when = Deferred.when

View file

@ -0,0 +1,3 @@
require('coffee-script');
require('coffee-script/register');
var server = require('../verifier.coffee');

View file

@ -58,6 +58,7 @@
"aws-sdk": "~2.0.0",
"bayesian-battle": "0.0.7",
"bluebird": "^3.2.1",
"chalk": "^1.1.3",
"co-express": "^1.2.1",
"coffee-script": "1.9.x",
"connect": "2.7.x",

View file

@ -3,30 +3,57 @@
// Set all users with trial requests to a teacher or teacher-like role, depending on trial request.
var hasTrialRequest = {};
var project = {role:1, name:1, email:1, permissions: 1};
db.trial.requests.find().forEach(function(trialRequest) {
print('Inspecting trial request', trialRequest._id);
var role = trialRequest.properties.role || 'teacher';
var user = db.users.findOne({_id: trialRequest.applicant}, {role:1, name:1, email:1});
print(JSON.stringify(user), JSON.stringify(trialRequest.properties), role);
if (!user.role) {
print(db.users.update({_id: trialRequest.applicant}, {$set: {role: role}}));
var user = null;
if(!trialRequest.applicant) {
print('\tNO APPLICANT INCLUDED', JSON.stringify(trialRequest));
if(!trialRequest.properties.email) {
print('\tNO EMAIL EITHER');
return;
}
user = db.users.findOne({emailLower: trialRequest.properties.email.toLowerCase()}, project);
if(!user) {
print('\tUSER WITH EMAIL NOT FOUND, CONTINUE');
return;
}
else {
print("\tOKAY GOT USER, UPDATE TRIAL REQUEST", JSON.stringify(user));
db.trial.requests.update({_id: trialRequest._id}, {$set: {applicant: user._id}});
}
}
else {
user = db.users.findOne({_id: trialRequest.applicant}, project);
}
if (!user.role && (user.permissions||[]).indexOf('admin') === -1) {
print('\tUpdating', JSON.stringify(user), 'to', role);
print(db.users.update({_id: user._id}, {$set: {role: role}}));
}
hasTrialRequest[user._id.str] = true;
});
var teacherRoles = ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent'];
// Unset all teacher-like roles for users without a trial request.
// AND removes all remaining users with a teacher-like role from classroom membership (after conversion period)
var hasTrialRequest = {};
var teacherRoles = ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent'];
db.trial.requests.find().forEach(function(trialRequest) {
if(!trialRequest.applicant) { return; }
hasTrialRequest[trialRequest.applicant.str] = true;
});
print(Object.keys(hasTrialRequest).length);
db.users.find({'role': {$in: teacherRoles}}, {_id: 1, name: 1, email: 1, role: 1}).forEach(function(user) {
print('Updating user', JSON.stringify(user));
if (!hasTrialRequest.user._id.str) {
print('\tunset role');
//db.users.update({_id: user._id}, {$unset: {role: ''}});
print('Got user with teacher role', user._id);
if (!hasTrialRequest[user._id.str]) {
print('\tUnset role', JSON.stringify(user));
db.users.update({_id: user._id}, {$unset: {role: ''}});
}
else {
return; // TODO: Run when we've moved completely to separate user roles
var count = db.classrooms.count({members: user._id}, {name: 1});
if (count) {
print('\tWill remove from classrooms');
@ -44,9 +71,10 @@ db.classrooms.find({}, {members: 1}).forEach(function(classroom) {
if(!classroom.members) {
return;
}
print('Updating for classroom', classroom._id, 'with members', classroom.members.length);
for (var i in classroom.members) {
var memberID = classroom.members[i];
print('updating member', memberID);
print('\tupdating member', memberID);
print(db.users.update({_id: memberID, role: {$exists: false}}, {$set: {role: 'student'}}));
}
});
}
});

View file

@ -0,0 +1,40 @@
authentication = require 'passport'
LocalStrategy = require('passport-local').Strategy
User = require '../models/User'
config = require '../../server_config'
errors = require '../commons/errors'
module.exports.setup = ->
authentication.serializeUser((user, done) -> done(null, user._id))
authentication.deserializeUser((id, done) ->
User.findById(id, (err, user) -> done(err, user)))
if config.picoCTF
pico = require('../lib/picoctf');
authentication.use new pico.PicoStrategy()
return
authentication.use(new LocalStrategy(
(username, password, done) ->
# kind of a hacky way to make it possible for iPads to 'log in' with their unique device id
if username.length is 36 and '@' not in username # must be an identifier for vendor
q = { iosIdentifierForVendor: username }
else
q = { emailLower: username.toLowerCase() }
User.findOne(q).exec((err, user) ->
return done(err) if err
if not user
return done(new errors.Unauthorized('not found', { property: 'email' }))
passwordReset = (user.get('passwordReset') or '').toLowerCase()
if passwordReset and password.toLowerCase() is passwordReset
User.update {_id: user.get('_id')}, {$unset: {passwordReset: ''}}, {}, ->
return done(null, user)
hash = User.hashPassword(password)
unless user.get('passwordHash') is hash
return done(new errors.Unauthorized('is wrong', { property: 'password' }))
return done(null, user)
)
))

View file

@ -88,6 +88,10 @@ errorResponseSchema = {
type: 'string'
description: 'Property which is related to the error (conflict, validation).'
}
name: {
type: 'string'
description: 'Provided for /auth/name.' # TODO: refactor out
}
}
}
errorProps = _.keys(errorResponseSchema.properties)

View file

@ -47,7 +47,6 @@ module.exports.handlerUrlOverrides =
module.exports.routes =
[
'routes/admin'
'routes/auth'
'routes/contact'
'routes/db'
'routes/file'

View file

@ -7,24 +7,10 @@ mongoose = require 'mongoose'
CampaignHandler = class CampaignHandler extends Handler
modelClass: Campaign
editableProperties: [
'name'
'fullName'
'description'
'type'
'i18n'
'i18nCoverage'
'ambientSound'
'backgroundImage'
'backgroundColor'
'backgroundColorTransparent'
'adjacentCampaigns'
'levels'
]
jsonSchema: require '../../app/schemas/models/campaign.schema'
hasAccess: (req) ->
req.method in ['GET', 'PUT'] or req.user?.isAdmin()
req.method in ['GET'] or req.user?.isAdmin()
hasAccessToDocument: (req, document, method=null) ->
return true if req.user?.isAdmin()
@ -124,10 +110,6 @@ CampaignHandler = class CampaignHandler extends Handler
return @sendDatabaseError(res, err) if err
return @sendSuccess(res, (achievement.toObject() for achievement in achievements))
onPutSuccess: (req, doc) ->
docLink = "http://codecombat.com#{req.headers['x-current-path']}"
@sendChangedSlackMessage creator: req.user, target: doc, docLink: docLink
getNamesByIDs: (req, res) -> @getNamesByOriginals req, res, true
module.exports = new CampaignHandler()

View file

@ -10,12 +10,11 @@ UserHandler = require './user_handler'
ClassroomHandler = class ClassroomHandler extends Handler
modelClass: Classroom
jsonSchema: require '../../app/schemas/models/classroom.schema'
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE']
allowedMethods: ['GET', 'PUT', 'DELETE']
hasAccess: (req) ->
return false unless req.user
return true if req.method is 'GET'
return false if req.method is 'POST' and not req.user?.isTeacher()
req.method in @allowedMethods or req.user?.isAdmin()
hasAccessToDocument: (req, document, method=null) ->
@ -27,12 +26,6 @@ ClassroomHandler = class ClassroomHandler extends Handler
return true if isGet and isMember
false
makeNewInstance: (req) ->
instance = super(req)
instance.set 'ownerID', req.user._id
instance.set 'members', []
instance
getByRelationship: (req, res, args...) ->
method = req.method.toLowerCase()
return @inviteStudents(req, res, args[0]) if args[1] is 'invite-members'

View file

@ -8,6 +8,9 @@ request = require 'request'
User = require '../models/User'
utils = require '../lib/utils'
mongoose = require 'mongoose'
authentication = require 'passport'
sendwithus = require '../sendwithus'
LevelSession = require '../models/LevelSession'
module.exports =
checkDocumentPermissions: (req, res, next) ->
@ -34,8 +37,26 @@ module.exports =
if not _.size(_.intersection(req.user.get('permissions'), permissions))
return next new errors.Forbidden('You do not have permissions necessary.')
next()
whoAmI: wrap (req, res) ->
if not req.user
user = User.makeNew(req)
yield user.save()
req.logInAsync = Promise.promisify(req.logIn)
yield req.logInAsync(user)
if req.query.callback
res.jsonp(req.user.toObject({req, publicOnly: true}))
else
res.send(req.user.toObject({req, publicOnly: false}))
res.end()
loginByGPlus: wrap (req, res) ->
afterLogin: wrap (req, res, next) ->
activity = req.user.trackActivity 'login', 1
yield req.user.update {activity: activity}
res.status(200).send(req.user.toObject({req: req}))
loginByGPlus: wrap (req, res, next) ->
gpID = req.body.gplusID
gpAT = req.body.gplusAccessToken
throw new errors.UnprocessableEntity('gplusID and gplusAccessToken required.') unless gpID and gpAT
@ -48,9 +69,9 @@ module.exports =
throw new errors.NotFound('No user with that G+ ID') unless user
req.logInAsync = Promise.promisify(req.logIn)
yield req.logInAsync(user)
res.status(200).send(user.formatEntity(req))
next()
loginByFacebook: wrap (req, res) ->
loginByFacebook: wrap (req, res, next) ->
fbID = req.body.facebookID
fbAT = req.body.facebookAccessToken
throw new errors.UnprocessableEntity('facebookID and facebookAccessToken required.') unless fbID and fbAT
@ -63,7 +84,7 @@ module.exports =
throw new errors.NotFound('No user with that Facebook ID') unless user
req.logInAsync = Promise.promisify(req.logIn)
yield req.logInAsync(user)
res.status(200).send(user.formatEntity(req))
next()
spy: wrap (req, res) ->
throw new errors.Unauthorized('You must be logged in to enter espionage mode') unless req.user
@ -94,3 +115,84 @@ module.exports =
req.loginAsync = Promise.promisify(req.login)
yield req.loginAsync user
res.status(200).send(user.toObject({req: req}))
logout: (req, res) ->
req.logout()
res.send({})
reset: wrap (req, res) ->
unless req.body.email
throw new errors.UnprocessableEntity('Need an email specified.', {property: 'email'})
user = yield User.findOne({emailLower: req.body.email.toLowerCase()})
if not user
throw new errors.NotFound('not found', {property: 'email'})
user.set('passwordReset', utils.getCodeCamel())
yield user.save()
context =
email_id: sendwithus.templates.password_reset
recipient:
address: req.body.email
email_data:
tempPassword: user.get('passwordReset')
sendwithus.api.sendAsync = Promise.promisify(sendwithus.api.send)
yield sendwithus.api.sendAsync(context)
res.end()
unsubscribe: wrap (req, res) ->
email = req.query.email
unless email
throw new errors.UnprocessableEntity 'No email provided to unsubscribe.'
email = decodeURIComponent(email)
if req.query.session
# Unsubscribe from just one session's notifications instead.
session = yield LevelSession.findOne({_id: req.query.session})
if not session
throw new errors.NotFound "Level session not found"
session.set 'unsubscribed', true
yield session.save()
res.send "Unsubscribed #{email} from CodeCombat emails for #{session.get('levelName')} #{session.get('team')} ladder updates. Sorry to see you go! <p><a href='/play/ladder/#{session.levelID}#my-matches'>Ladder preferences</a></p>"
res.end()
return
user = yield User.findOne({emailLower: email.toLowerCase()})
if not user
throw new errors.NotFound "No user found with email '#{email}'"
emails = _.clone(user.get('emails')) or {}
msg = ''
if req.query.recruitNotes
emails.recruitNotes ?= {}
emails.recruitNotes.enabled = false
msg = "Unsubscribed #{email} from recruiting emails."
else if req.query.employerNotes
emails.employerNotes ?= {}
emails.employerNotes.enabled = false
msg = "Unsubscribed #{email} from employer emails."
else
msg = "Unsubscribed #{email} from all CodeCombat emails. Sorry to see you go!"
emailSettings.enabled = false for emailSettings in _.values(emails)
emails.generalNews ?= {}
emails.generalNews.enabled = false
emails.anyNotes ?= {}
emails.anyNotes.enabled = false
yield user.update {$set: {emails: emails}}
res.send msg + '<p><a href="/account/settings">Account settings</a></p>'
res.end()
name: wrap (req, res) ->
if not req.params.name
throw new errors.UnprocessableEntity 'No name provided.'
originalName = req.params.name
User.unconflictNameAsync = Promise.promisify(User.unconflictName)
name = yield User.unconflictNameAsync originalName
response = name: name
if originalName is name
res.send 200, response
else
throw new errors.Conflict('Name is taken', response)

View file

@ -7,6 +7,7 @@ mongoose = require 'mongoose'
Campaign = require '../models/Campaign'
parse = require '../commons/parse'
LevelSession = require '../models/LevelSession'
slack = require '../slack'
module.exports =
fetchByType: wrap (req, res, next) ->
@ -19,3 +20,18 @@ module.exports =
campaigns = yield dbq.exec()
campaigns = (campaign.toObject({req: req}) for campaign in campaigns)
res.status(200).send(campaigns)
put: wrap (req, res) ->
campaign = yield database.getDocFromHandle(req, Campaign)
if not campaign
throw new errors.NotFound('Campaign not found.')
hasPermission = req.user.isAdmin()
unless hasPermission or database.isJustFillingTranslations(req, campaign)
throw new errors.Forbidden('Must be an admin or submitting translations to edit a campaign')
database.assignBody(req, campaign)
database.validateDoc(campaign)
campaign = yield campaign.save()
res.status(200).send(campaign.toObject())
docLink = "http://codecombat.com#{req.headers['x-current-path']}"
slack.sendChangedSlackMessage creator: req.user, target: campaign, docLink: docLink

View file

@ -62,3 +62,14 @@ module.exports =
memberObjects = (member.toObject({ req: req, includedPrivates: ["name", "email"] }) for member in members)
res.status(200).send(memberObjects)
post: wrap (req, res) ->
throw new errors.Unauthorized() unless req.user and not req.user.isAnonymous()
throw new errors.Forbidden() unless req.user?.isTeacher()
classroom = database.initDoc(req, Classroom)
classroom.set 'ownerID', req.user._id
classroom.set 'members', []
database.assignBody(req, classroom)
database.validateDoc(classroom)
classroom = yield classroom.save()
res.status(201).send(classroom.toObject({req: req}))

View file

@ -20,7 +20,7 @@ module.exports =
throw new errors.UnprocessableEntity('Invalid G+ Access Token.') unless idsMatch
user = yield User.findOne({gplusID: gpID})
throw new errors.NotFound('No user with that G+ ID') unless user
res.status(200).send(user.formatEntity(req))
res.status(200).send(user.toObject({req: req}))
fetchByFacebookID: wrap (req, res, next) ->
fbID = req.query.facebookID
@ -31,10 +31,8 @@ module.exports =
dbq.select(parse.getProjectFromReq(req))
url = "https://graph.facebook.com/me?access_token=#{fbAT}"
[facebookRes, body] = yield request.getAsync(url, {json: true})
console.log '...', body, facebookRes.statusCode
idsMatch = fbID is body.id
throw new errors.UnprocessableEntity('Invalid Facebook Access Token.') unless idsMatch
user = yield User.findOne({facebookID: fbID})
throw new errors.NotFound('No user with that Facebook ID') unless user
console.log 'okay done'
res.status(200).send(user.formatEntity(req))
res.status(200).send(user.toObject({req: req}))

View file

@ -37,5 +37,19 @@ CampaignSchema.statics.updateAdjacentCampaigns = (savedCampaign) ->
CampaignSchema.post 'save', -> @constructor.updateAdjacentCampaigns @
CampaignSchema.statics.jsonSchema = jsonSchema
CampaignSchema.statics.editableProperties = [
'name'
'fullName'
'description'
'type'
'i18n'
'i18nCoverage'
'ambientSound'
'backgroundImage'
'backgroundColor'
'backgroundColorTransparent'
'adjacentCampaigns'
'levels'
]
module.exports = mongoose.model('campaign', CampaignSchema)

View file

@ -54,7 +54,7 @@ ClassroomSchema.set('toObject', {
transform: (doc, ret, options) ->
return ret unless options.req
user = options.req.user
unless user?.isAdmin() or user?.get('_id').equals(doc.get('ownerID'))
unless user and (user.isAdmin() or user._id.equals(doc.get('ownerID')))
delete ret.code
delete ret.codeCamel
return ret

View file

@ -6,6 +6,7 @@ mail = require '../commons/mail'
log = require 'winston'
plugins = require '../plugins/plugins'
AnalyticsUsersActive = require './AnalyticsUsersActive'
languages = require '../routes/languages'
config = require '../../server_config'
stripe = require('stripe')(config.stripe.secretKey)
@ -253,16 +254,6 @@ UserSchema.methods.isPremium = ->
return true if @hasSubscription()
return false
UserSchema.methods.formatEntity = (req, publicOnly=false) ->
obj = @toObject()
serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset', 'lastIP']
delete obj[prop] for prop in serverProperties
candidateProperties = ['jobProfile', 'jobProfileApproved', 'jobProfileNotes']
delete obj[prop] for prop in candidateProperties
includePrivates = not publicOnly and (req.user and (req.user.isAdmin() or req.user._id.equals(@_id)))
delete obj[prop] for prop in User.privateProperties unless includePrivates
return obj
UserSchema.methods.isOnPremiumServer = ->
@get('country') in ['china', 'brazil']
@ -374,9 +365,27 @@ UserSchema.set('toObject', {
delete ret[prop] for prop in User.candidateProperties
return ret
})
UserSchema.statics.makeNew = (req) ->
user = new User({anonymous: true})
if global.testing
# allows tests some control over user id creation
newID = _.pad((User.idCounter++).toString(16), 24, '0')
user.set('_id', newID)
user.set 'testGroupNumber', Math.floor(Math.random() * 256) # also in app/core/auth
lang = languages.languageCodeFromAcceptedLanguages req.acceptedLanguages
user.set 'preferredLanguage', lang if lang[...2] isnt 'en'
user.set 'preferredLanguage', 'pt-BR' if not user.get('preferredLanguage') and /br\.codecombat\.com/.test(req.get('host'))
user.set 'preferredLanguage', 'zh-HANS' if not user.get('preferredLanguage') and /cn\.codecombat\.com/.test(req.get('host'))
user.set 'lastIP', (req.headers['x-forwarded-for'] or req.connection.remoteAddress)?.split(/,? /)[0]
user.set 'country', req.country if req.country
user
UserSchema.plugin plugins.NamedPlugin
module.exports = User = mongoose.model('User', UserSchema)
User.idCounter = 0
AchievablePlugin = require '../plugins/achievements'
UserSchema.plugin(AchievablePlugin)

View file

@ -1,203 +0,0 @@
authentication = require 'passport'
LocalStrategy = require('passport-local').Strategy
User = require '../models/User'
UserHandler = require '../handlers/user_handler'
LevelSession = require '../models/LevelSession'
config = require '../../server_config'
errors = require '../commons/errors'
languages = require '../routes/languages'
sendwithus = require '../sendwithus'
log = require 'winston'
utils = require '../lib/utils'
module.exports.setup = (app) ->
authentication.serializeUser((user, done) -> done(null, user._id))
authentication.deserializeUser((id, done) ->
User.findById(id, (err, user) -> done(err, user)))
if config.picoCTF
pico = require('../lib/picoctf');
authentication.use new pico.PicoStrategy()
return
authentication.use(new LocalStrategy(
(username, password, done) ->
# kind of a hacky way to make it possible for iPads to 'log in' with their unique device id
if username.length is 36 and '@' not in username # must be an identifier for vendor
q = { iosIdentifierForVendor: username }
else
q = { emailLower: username.toLowerCase() }
User.findOne(q).exec((err, user) ->
return done(err) if err
return done(null, false, {message: 'not found', property: 'email'}) if not user
passwordReset = (user.get('passwordReset') or '').toLowerCase()
if passwordReset and password.toLowerCase() is passwordReset
User.update {_id: user.get('_id')}, {passwordReset: ''}, {}, ->
return done(null, user)
hash = User.hashPassword(password)
unless user.get('passwordHash') is hash
return done(null, false, {message: 'is wrong', property: 'password'})
return done(null, user)
)
))
app.post('/auth/login', (req, res, next) ->
authentication.authenticate('local', (err, user, info) ->
return next(err) if err
if not user
return errors.unauthorized(res, [{message: info.message, property: info.property}])
req.logIn(user, (err) ->
return next(err) if (err)
activity = req.user.trackActivity 'login', 1
user.update {activity: activity}, (err) ->
return next(err) if (err)
res.send(UserHandler.formatEntity(req, req.user))
return res.end()
)
)(req, res, next)
)
app.get('/auth/whoami', (req, res) ->
if req.user
sendSelf(req, res)
else
user = makeNewUser(req)
makeNext = (req, res) -> -> sendSelf(req, res)
next = makeNext(req, res)
loginUser(req, res, user, false, next)
)
sendSelf = (req, res) ->
res.setHeader('Content-Type', 'text/json')
if req.query.callback
res.jsonp UserHandler.formatEntity(req, req.user, true)
else
res.send UserHandler.formatEntity(req, req.user, false)
res.end()
app.post('/auth/logout', (req, res) ->
req.logout()
res.send({})
)
app.post('/auth/reset', (req, res) ->
unless req.body.email
return errors.badInput(res, [{message: 'Need an email specified.', property: 'email'}])
User.findOne({emailLower: req.body.email.toLowerCase()}).exec((err, user) ->
if not user
return errors.notFound(res, [{message: 'not found', property: 'email'}])
user.set('passwordReset', utils.getCodeCamel())
emailContent = "<h3>Your temporary password: <b>#{user.get('passwordReset')}</b></h3>"
emailContent += "<p>Reset your password at <a href=\"http://codecombat.com/account/settings\">http://codecombat.com/account/settings</a></p>"
emailContent += "<p>Your old password cannot be retrieved.</p>"
user.save (err) =>
return errors.serverError(res) if err
context =
email_id: sendwithus.templates.generic_email
recipient:
address: req.body.email
email_data:
subject: 'CodeCombat Recovery Password'
title: ''
content: emailContent
sendwithus.api.send context, (err, result) ->
if err
console.error "Error sending password reset email: #{err.message or err}"
res.end()
)
)
app.get '/auth/unsubscribe', (req, res) ->
req.query.email = decodeURIComponent(req.query.email)
email = req.query.email
unless req.query.email
return errors.badInput res, 'No email provided to unsubscribe.'
if req.query.session
# Unsubscribe from just one session's notifications instead.
return LevelSession.findOne({_id: req.query.session}).exec (err, session) ->
return errors.serverError res, 'Could not unsubscribe: #{req.query.session}, #{req.query.email}: #{err}' if err
session.set 'unsubscribed', true
session.save (err) ->
return errors.serverError res, 'Database failure.' if err
res.send "Unsubscribed #{req.query.email} from CodeCombat emails for #{session.get('levelName')} #{session.get('team')} ladder updates. Sorry to see you go! <p><a href='/play/ladder/#{session.levelID}#my-matches'>Ladder preferences</a></p>"
res.end()
User.findOne({emailLower: req.query.email.toLowerCase()}).exec (err, user) ->
if not user
return errors.notFound res, "No user found with email '#{req.query.email}'"
emails = _.clone(user.get('emails')) or {}
msg = ''
if req.query.recruitNotes
emails.recruitNotes ?= {}
emails.recruitNotes.enabled = false
msg = "Unsubscribed #{req.query.email} from recruiting emails."
else if req.query.employerNotes
emails.employerNotes ?= {}
emails.employerNotes.enabled = false
msg = "Unsubscribed #{req.query.email} from employer emails."
else
msg = "Unsubscribed #{req.query.email} from all CodeCombat emails. Sorry to see you go!"
emailSettings.enabled = false for emailSettings in _.values(emails)
emails.generalNews ?= {}
emails.generalNews.enabled = false
emails.anyNotes ?= {}
emails.anyNotes.enabled = false
user.update {$set: {emails: emails}}, {}, =>
return errors.serverError res, 'Database failure.' if err
res.send msg + '<p><a href="/account/settings">Account settings</a></p>'
res.end()
app.get '/auth/name*', (req, res) ->
parts = req.path.split '/'
originalName = decodeURI parts[3]
return errors.badInput res, 'No name provided.' unless parts.length > 3 and originalName? and originalName isnt ''
return errors.notFound res if parts.length isnt 4
User.unconflictName originalName, (err, name) ->
return errors.serverError res, err if err
response = name: name
if originalName is name
res.send 200, response
else
errors.conflict res, response
module.exports.loginUser = loginUser = (req, res, user, send=true, next=null) ->
user.save((err) ->
return errors.serverError res, err if err?
req.logIn(user, (err) ->
return errors.serverError res, err if err?
return res.send(user) and res.end() if send
next() if next
)
)
module.exports.idCounter = 0
module.exports.makeNewUser = makeNewUser = (req) ->
user = new User({anonymous: true})
if global.testing
# allows tests some control over user id creation
newID = _.pad((module.exports.idCounter++).toString(16), 24, '0')
user.set('_id', newID)
user.set 'testGroupNumber', Math.floor(Math.random() * 256) # also in app/core/auth
lang = languages.languageCodeFromAcceptedLanguages req.acceptedLanguages
user.set 'preferredLanguage', lang if lang[...2] isnt 'en'
user.set 'preferredLanguage', 'pt-BR' if not user.get('preferredLanguage') and /br\.codecombat\.com/.test(req.get('host'))
user.set 'preferredLanguage', 'zh-HANS' if not user.get('preferredLanguage') and /cn\.codecombat\.com/.test(req.get('host'))
user.set 'lastIP', (req.headers['x-forwarded-for'] or req.connection.remoteAddress)?.split(/,? /)[0]
user.set 'country', req.country if req.country
#log.info "making new user #{user.get('_id')} with language #{user.get('preferredLanguage')} of #{req.acceptedLanguages} and country #{req.country} on #{if config.tokyo then 'Tokyo' else (if config.saoPaulo then 'Brazil' else 'US')} server and lastIP #{user.get('lastIP')}."
user

View file

@ -2,10 +2,17 @@ mw = require '../middleware'
module.exports.setup = (app) ->
app.post('/auth/login-facebook', mw.auth.loginByFacebook)
app.post('/auth/login-gplus', mw.auth.loginByGPlus)
passport = require('passport')
app.post('/auth/login', passport.authenticate('local'), mw.auth.afterLogin)
app.post('/auth/login-facebook', mw.auth.loginByFacebook, mw.auth.afterLogin)
app.post('/auth/login-gplus', mw.auth.loginByGPlus, mw.auth.afterLogin)
app.post('/auth/logout', mw.auth.logout)
app.get('/auth/name/?(:name)?', mw.auth.name)
app.post('/auth/reset', mw.auth.reset)
app.post('/auth/spy', mw.auth.spy)
app.post('/auth/stop-spying', mw.auth.stopSpying)
app.get('/auth/unsubscribe', mw.auth.unsubscribe)
app.get('/auth/whoami', mw.auth.whoAmI)
Achievement = require '../models/Achievement'
app.get('/db/achievement', mw.achievements.fetchByRelated, mw.rest.get(Achievement))
@ -33,7 +40,11 @@ module.exports.setup = (app) ->
app.get('/db/article/:handle/patches', mw.patchable.patches(Article))
app.post('/db/article/:handle/watchers', mw.patchable.joinWatchers(Article))
app.delete('/db/article/:handle/watchers', mw.patchable.leaveWatchers(Article))
app.get('/db/campaign', mw.campaigns.fetchByType)
app.put('/db/campaign/:handle', mw.campaigns.put)
app.post('/db/classroom', mw.classrooms.post)
app.get('/db/classroom', mw.classrooms.getByOwner)
app.get('/db/classroom/:handle/member-sessions', mw.classrooms.fetchMemberSessions)
app.get('/db/classroom/:handle/members', mw.classrooms.fetchMembers) # TODO: Use mw.auth?
@ -43,8 +54,6 @@ module.exports.setup = (app) ->
app.get('/db/course', mw.rest.get(Course))
app.get('/db/course/:handle', mw.rest.getByHandle(Course))
app.get('/db/campaign', mw.campaigns.fetchByType) #TODO
app.post('/db/course_instance/:handle/members', mw.auth.checkLoggedIn(), mw.courseInstances.addMembers)
app.get('/db/user', mw.users.fetchByGPlusID, mw.users.fetchByFacebookID)

View file

@ -1,14 +1,20 @@
config = require '../server_config'
sendwithusAPI = require 'sendwithus'
swuAPIKey = config.mail.sendwithusAPIKey
log = require 'winston'
module.exports.setupRoutes = (app) ->
return
debug = not config.isProduction
module.exports.api = new sendwithusAPI swuAPIKey, debug
if config.unittest
module.exports.api.send = ->
module.exports.api =
send: (context, cb) ->
log.debug('Tried to send email with context: ', JSON.stringify(context, null, '\t'))
setTimeout(cb, 10)
if swuAPIKey
module.exports.api = new sendwithusAPI swuAPIKey, debug
module.exports.templates =
parent_subscribe_email: 'tem_2APERafogvwKhmcnouigud'
share_progress_email: 'tem_VHE3ihhGmVa3727qds9zY8'
@ -25,3 +31,4 @@ module.exports.templates =
teacher_free_trial: 'tem_R7d9Hpoba9SceQNiYSXBak'
teacher_free_trial_hoc: 'tem_4ZSY9wsA9Qwn4wBFmZgPdc'
teacher_request_demo: 'tem_cwG3HZjEyb6QE493hZuUra'
password_reset: 'tem_wbQUMRtLY9xhec8BSCykLA'

View file

@ -5,8 +5,13 @@ log = require 'winston'
roomChannelMap =
main: '#general'
artisans: '#artisan'
module.exports.sendChangedSlackMessage = (options) ->
message = "#{options.creator.get('name')} saved a change to #{options.target.get('name')}: #{options.target.get('commitMessage') or '(no commit message)'} #{options.docLink}"
rooms = if /Diplomat submission/.test(message) then ['dev-feed'] else ['dev-feed', 'artisans']
@sendSlackMessage message, rooms
module.exports.sendSlackMessage = sendSlackMessage = (message, rooms=['tower'], options={}) ->
module.exports.sendSlackMessage = (message, rooms=['tower'], options={}) ->
unless config.isProduction
log.info "Slack msg: #{message}"
return

View file

@ -3,10 +3,8 @@ config = {}
config.unittest = global.testing
config.proxy = process.env.COCO_PROXY
config.tokyo = process.env.TOKYO or false
config.saoPaulo = process.env.SAOPAULO or false
config.chinaDomain = "http://cn.codecombat.com"
config.brazilDomain = "http://br.codecombat.com"
config.chinaDomain = "cn.codecombat.com"
config.brazilDomain = "br.codecombat.com"
config.port = process.env.COCO_PORT or process.env.COCO_NODE_PORT or process.env.PORT or 3000
config.ssl_port = process.env.COCO_SSL_PORT or process.env.COCO_SSL_NODE_PORT or 3443
config.cloudflare =

Some files were not shown because too many files have changed in this diff Show more