mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-24 16:17:57 -05:00
Merge remote-tracking branch 'codecombat/master'
This commit is contained in:
commit
7bf7fdd0ce
106 changed files with 1188 additions and 542 deletions
|
@ -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();
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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++
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -70,3 +70,6 @@ module.exports =
|
|||
|
||||
'application:service-loaded': c.object {required: ['service']},
|
||||
service: {type: 'string'} # 'segment'
|
||||
|
||||
'test:update': c.object {},
|
||||
state: {type: 'string'}
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -5,4 +5,9 @@
|
|||
margin-bottom: 5px
|
||||
|
||||
p
|
||||
margin-top: 30px
|
||||
margin-top: 30px
|
||||
|
||||
.course-title
|
||||
white-space: nowrap
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
51
app/templates/editor/verifier/verifier-view.jade
Normal file
51
app/templates/editor/verifier/verifier-view.jade
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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/", "/"))
|
||||
|
|
|
@ -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()
|
||||
|
|
111
app/views/editor/verifier/VerifierTest.coffee
Normal file
111
app/views/editor/verifier/VerifierTest.coffee
Normal 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
|
35
app/views/editor/verifier/VerifierView.coffee
Normal file
35
app/views/editor/verifier/VerifierView.coffee
Normal 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()
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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: ->
|
||||
|
|
63
headless_client/cluster.coffee
Normal file
63
headless_client/cluster.coffee
Normal 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(' ')
|
|
@ -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
|
||||
|
|
3
headless_client/verifier.js
Normal file
3
headless_client/verifier.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
require('coffee-script');
|
||||
require('coffee-script/register');
|
||||
var server = require('../verifier.coffee');
|
|
@ -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",
|
||||
|
|
|
@ -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'}}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
40
server/commons/auth.coffee
Normal file
40
server/commons/auth.coffee
Normal 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)
|
||||
)
|
||||
))
|
|
@ -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)
|
||||
|
|
|
@ -47,7 +47,6 @@ module.exports.handlerUrlOverrides =
|
|||
module.exports.routes =
|
||||
[
|
||||
'routes/admin'
|
||||
'routes/auth'
|
||||
'routes/contact'
|
||||
'routes/db'
|
||||
'routes/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()
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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}))
|
|
@ -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}))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue