From 8326b5e18257433c8591e594a035d6f6246b6f31 Mon Sep 17 00:00:00 2001 From: UltCombo Date: Sun, 27 Mar 2016 23:55:55 -0300 Subject: [PATCH 01/13] JS beautifier: fix loop construct, fixes #3510 As JavaScript is the only language supported by the beautifier, and [CodeCombat is in the process of converting things to while (true)](https://github.com/codecombat/codecombat/issues/3510#issuecomment-201965310), I guess adding this quick hack to the view's code should suffice for now. Hopefully, the non-standard `loop` construct will be removed from other languages before they receive beautifier support. If this doesn't seem good, I can move this hack to `aether.beautify`, where it is possible to perform this substitution in a language-aware way (i.e. inside Aether's JavaScript implementation's [`beautify` method](https://github.com/codecombat/aether/blob/418ccf2414c87fa23bf1edcc838266097a2df559/src/languages/javascript.coffee#L137)). Seeing as the `loop` construct is specific to CodeCombat, I'm not sure if that would be a good idea. Passing an options object with a flag to support the `loop` construct seems a bit overkill. Let me know what works best for you. --- app/views/play/level/tome/SpellView.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index a8ad0cef8..b507d272f 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -1193,7 +1193,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) -> From fddba4e0cee0c36ee8bf8fdf3d9927baea0b44dc Mon Sep 17 00:00:00 2001 From: Cat Sync Date: Tue, 12 Apr 2016 14:31:50 -0400 Subject: [PATCH 02/13] Hacks autocomplete to use hero instead of self/this MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses hero instead of self/this if “hero” is already in the code, in javascript, python, and lua. --- app/views/play/level/tome/SpellView.coffee | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index a8ad0cef8..5ee4d9712 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -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') From f1f1c23fd404271659552dfd00ee36189344f408 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Mon, 11 Apr 2016 16:51:51 -0700 Subject: [PATCH 03/13] Refactor /auth endpoints for #3469 * Take `/server/routes/auth` and move most of the logic to `/server/middleware/auth`, refactoring to use generators. * List all `/auth/*` endpoints in `/server/routes/index.coffee`. * Fill in testing gaps for `/auth/unsubscribe`. * Add debug log when `sendwithus` is not operational, so it 'works' in development and testing. * Use passport properly! * Track Facebook and G+ logins in user activity as well as passport logins. --- server/commons/auth.coffee | 40 +++++ server/commons/errors.coffee | 4 + server/commons/mapping.coffee | 1 - server/middleware/auth.coffee | 115 +++++++++++++- server/middleware/users.coffee | 6 +- server/models/User.coffee | 29 ++-- server/routes/auth.coffee | 203 ------------------------ server/routes/index.coffee | 11 +- server/sendwithus.coffee | 12 +- server_setup.coffee | 3 +- spec/server/common.coffee | 3 +- spec/server/functional/auth.spec.coffee | 79 +++++++-- 12 files changed, 263 insertions(+), 243 deletions(-) create mode 100644 server/commons/auth.coffee delete mode 100644 server/routes/auth.coffee diff --git a/server/commons/auth.coffee b/server/commons/auth.coffee new file mode 100644 index 000000000..323b71d30 --- /dev/null +++ b/server/commons/auth.coffee @@ -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) + ) + )) diff --git a/server/commons/errors.coffee b/server/commons/errors.coffee index 58f45babb..a82eb7b0f 100644 --- a/server/commons/errors.coffee +++ b/server/commons/errors.coffee @@ -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) diff --git a/server/commons/mapping.coffee b/server/commons/mapping.coffee index d72b9ddce..1c35e6676 100644 --- a/server/commons/mapping.coffee +++ b/server/commons/mapping.coffee @@ -47,7 +47,6 @@ module.exports.handlerUrlOverrides = module.exports.routes = [ 'routes/admin' - 'routes/auth' 'routes/contact' 'routes/db' 'routes/file' diff --git a/server/middleware/auth.coffee b/server/middleware/auth.coffee index a8d087780..af3b9c821 100644 --- a/server/middleware/auth.coffee +++ b/server/middleware/auth.coffee @@ -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,89 @@ 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()) + emailContent = "

Your temporary password: #{user.get('passwordReset')}

" + emailContent += "

Reset your password at http://codecombat.com/account/settings

" + emailContent += "

Your old password cannot be retrieved.

" + yield user.save() + context = + email_id: sendwithus.templates.generic_email + recipient: + address: req.body.email + email_data: + subject: 'CodeCombat Recovery Password' + title: '' + content: emailContent + 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!

Ladder preferences

" + 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 + '

Account settings

' + 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) \ No newline at end of file diff --git a/server/middleware/users.coffee b/server/middleware/users.coffee index 5b4f30d50..0744df455 100644 --- a/server/middleware/users.coffee +++ b/server/middleware/users.coffee @@ -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})) diff --git a/server/models/User.coffee b/server/models/User.coffee index bdb734152..a4f7c34c9 100644 --- a/server/models/User.coffee +++ b/server/models/User.coffee @@ -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) diff --git a/server/routes/auth.coffee b/server/routes/auth.coffee deleted file mode 100644 index 6e3cee149..000000000 --- a/server/routes/auth.coffee +++ /dev/null @@ -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 = "

Your temporary password: #{user.get('passwordReset')}

" - emailContent += "

Reset your password at http://codecombat.com/account/settings

" - emailContent += "

Your old password cannot be retrieved.

" - 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!

Ladder preferences

" - 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 + '

Account settings

' - 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 diff --git a/server/routes/index.coffee b/server/routes/index.coffee index 1e7945e10..7802cad58 100644 --- a/server/routes/index.coffee +++ b/server/routes/index.coffee @@ -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)) diff --git a/server/sendwithus.coffee b/server/sendwithus.coffee index 75a5ead1a..40c666b5f 100644 --- a/server/sendwithus.coffee +++ b/server/sendwithus.coffee @@ -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' diff --git a/server_setup.coffee b/server_setup.coffee index 675321c3f..f60590c22 100644 --- a/server_setup.coffee +++ b/server_setup.coffee @@ -13,7 +13,7 @@ baseRoute = require './server/routes/base' user = require './server/handlers/user_handler' logging = require './server/commons/logging' config = require './server_config' -auth = require './server/routes/auth' +auth = require './server/commons/auth' routes = require './server/routes' UserHandler = require './server/handlers/user_handler' slack = require './server/slack' @@ -108,6 +108,7 @@ setupPassportMiddleware = (app) -> require('./server/lib/picoctf').init app else app.use(authentication.session()) + auth.setup() setupCountryRedirectMiddleware = (app, country="china", countryCode="CN", languageCode="zh", serverID="tokyo") -> shouldRedirectToCountryServer = (req) -> diff --git a/spec/server/common.coffee b/spec/server/common.coffee index 3ef743b0b..ae6fa307c 100644 --- a/spec/server/common.coffee +++ b/spec/server/common.coffee @@ -211,6 +211,5 @@ _drop = (done) -> done() GLOBAL.resetUserIDCounter = (number=0) -> - auth = require '../../server/routes/auth' - auth.idCounter = number + User.idCounter = number diff --git a/spec/server/functional/auth.spec.coffee b/spec/server/functional/auth.spec.coffee index 076dd1d2a..b97227676 100644 --- a/spec/server/functional/auth.spec.coffee +++ b/spec/server/functional/auth.spec.coffee @@ -6,6 +6,7 @@ Promise = require 'bluebird' nock = require 'nock' request = require '../request' sendwithus = require '../../../server/sendwithus' +LevelSession = require '../../../server/models/LevelSession' urlLogin = getURL('/auth/login') urlReset = getURL('/auth/reset') @@ -24,7 +25,7 @@ describe 'POST /auth/login', -> done() it 'allows logging in by iosIdentifierForVendor', utils.wrap (done) -> - user = yield utils.initUser({ + yield utils.initUser({ 'iosIdentifierForVendor': '012345678901234567890123456789012345' 'password': '12345' }) @@ -44,7 +45,7 @@ describe 'POST /auth/login', -> done() it 'returns 200 when the user does exist', utils.wrap (done) -> - user = yield utils.initUser({ + yield utils.initUser({ 'email': 'some@email.com' 'password': '12345' }) @@ -56,7 +57,7 @@ describe 'POST /auth/login', -> done() it 'rejects wrong passwords', utils.wrap (done) -> - user = yield utils.initUser({ + yield utils.initUser({ 'email': 'some@email.com' 'password': '12345' }) @@ -68,7 +69,7 @@ describe 'POST /auth/login', -> done() it 'is completely case insensitive', utils.wrap (done) -> - user = yield utils.initUser({ + yield utils.initUser({ 'email': 'Some@Email.com' 'password': 'AbCdE' }) @@ -104,7 +105,9 @@ describe 'POST /auth/reset', -> done() it 'resets the user password', utils.wrap (done) -> - spyOn(sendwithus.api, 'send') + spyOn(sendwithus.api, 'send').and.callFake (options, cb) -> + expect(options.recipient.address).toBe('some@email.com') + cb() [res, body] = yield request.postAsync( {uri: urlReset, json: {email: 'some@email.com'}} ) @@ -120,9 +123,11 @@ describe 'POST /auth/reset', -> expect(res.statusCode).toBe(200) done() - # TODO: Finish refactoring the rest of these old tests it 'resetting password is not idempotent', utils.wrap (done) -> + spyOn(sendwithus.api, 'send').and.callFake (options, cb) -> + expect(options.recipient.address).toBe('some@email.com') + cb() [res, body] = yield request.postAsync( {uri: urlReset, json: {email: 'some@email.com'}} ) @@ -145,17 +150,65 @@ describe 'GET /auth/unsubscribe', -> beforeEach utils.wrap (done) -> yield utils.clearModels([User]) + @user = yield utils.initUser() + done() + + it 'returns 422 if email is not included', utils.wrap (done) -> + url = getURL('/auth/unsubscribe') + [res, body] = yield request.getAsync(url) + expect(res.statusCode).toBe(422) done() - it 'removes just recruitment emails if you include ?recruitNotes=1', utils.wrap (done) -> - user = yield utils.initUser() - url = getURL('/auth/unsubscribe?recruitNotes=1&email='+user.get('email')) + it 'returns 404 if email is not found', utils.wrap (done) -> + url = getURL('/auth/unsubscribe?email=ladeeda') [res, body] = yield request.getAsync(url) - expect(res.statusCode).toBe(200) - user = yield User.findOne(user._id) - expect(user.get('emails').recruitNotes.enabled).toBe(false) - expect(user.isEmailSubscriptionEnabled('generalNews')).toBeTruthy() + expect(res.statusCode).toBe(404) done() + + describe '?recruitNotes=1', -> + + it 'unsubscribes the user from recruitment emails', utils.wrap (done) -> + url = getURL('/auth/unsubscribe?recruitNotes=1&email='+@user.get('email')) + [res, body] = yield request.getAsync(url) + expect(res.statusCode).toBe(200) + user = yield User.findOne(@user._id) + expect(user.get('emails').recruitNotes.enabled).toBe(false) + expect(user.isEmailSubscriptionEnabled('generalNews')).toBeTruthy() + done() + + describe '?employerNotes=1', -> + + it 'unsubscribes the user from employer emails', utils.wrap (done) -> + url = getURL('/auth/unsubscribe?employerNotes=1&email='+@user.get('email')) + [res, body] = yield request.getAsync(url) + expect(res.statusCode).toBe(200) + user = yield User.findOne(@user._id) + expect(user.get('emails').employerNotes.enabled).toBe(false) + expect(user.isEmailSubscriptionEnabled('generalNews')).toBeTruthy() + done() + + describe '?session=:id', -> + + it 'sets the given LevelSession\'s unsubscribed property to true', utils.wrap (done) -> + session = new LevelSession({permissions:[target: @user._id, access: 'owner']}) + yield session.save() + url = getURL("/auth/unsubscribe?session=#{session.id}&email=#{@user.get('email')}") + [res, body] = yield request.getAsync(url) + expect(res.statusCode).toBe(200) + session = yield LevelSession.findById(session.id) + expect(session.get('unsubscribed')).toBe(true) + done() + + describe 'no GET query params', -> + + it 'unsubscribes the user from all emails', utils.wrap (done) -> + url = getURL("/auth/unsubscribe?email=#{@user.get('email')}") + [res, body] = yield request.getAsync(url) + expect(res.statusCode).toBe(200) + user = yield User.findOne(@user._id) + expect(user.get('emails').generalNews.enabled).toBe(false) + expect(user.get('emails').anyNotes.enabled).toBe(false) + done() describe 'GET /auth/name', -> url = '/auth/name' From 61dd93917c085fa6adbb5d066659721e9afb8977 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 12 Apr 2016 13:28:38 -0700 Subject: [PATCH 04/13] Improve password reset email, make a sendwithus template for it --- server/middleware/auth.coffee | 9 ++------- server/sendwithus.coffee | 1 + 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/server/middleware/auth.coffee b/server/middleware/auth.coffee index af3b9c821..8ac1894ab 100644 --- a/server/middleware/auth.coffee +++ b/server/middleware/auth.coffee @@ -129,18 +129,13 @@ module.exports = throw new errors.NotFound('not found', {property: 'email'}) user.set('passwordReset', utils.getCodeCamel()) - emailContent = "

Your temporary password: #{user.get('passwordReset')}

" - emailContent += "

Reset your password at http://codecombat.com/account/settings

" - emailContent += "

Your old password cannot be retrieved.

" yield user.save() context = - email_id: sendwithus.templates.generic_email + email_id: sendwithus.templates.password_reset recipient: address: req.body.email email_data: - subject: 'CodeCombat Recovery Password' - title: '' - content: emailContent + tempPassword: user.get('passwordReset') sendwithus.api.sendAsync = Promise.promisify(sendwithus.api.send) yield sendwithus.api.sendAsync(context) res.end() diff --git a/server/sendwithus.coffee b/server/sendwithus.coffee index 40c666b5f..1cf6fea98 100644 --- a/server/sendwithus.coffee +++ b/server/sendwithus.coffee @@ -31,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' From a7114a271946750879efe6cfbe9e3cf9b29d9699 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Thu, 7 Apr 2016 19:06:57 -0700 Subject: [PATCH 05/13] Implement headless verifier; fix headless client --- .../javascripts/workers/worker_world.js | 10 +- app/core/Router.coffee | 8 +- app/lib/Angel.coffee | 49 +++--- app/lib/God.coffee | 3 + app/lib/LevelBus.coffee | 1 + app/lib/LevelLoader.coffee | 40 ++++- app/models/Level.coffee | 2 +- app/models/SuperModel.coffee | 4 +- app/schemas/subscriptions/god.coffee | 2 + app/schemas/subscriptions/misc.coffee | 3 + app/schemas/subscriptions/tome.coffee | 1 + .../editor/verifier/verifier-view.jade | 33 ++++ app/views/editor/verifier/VerifierTest.coffee | 102 ++++++++++++ app/views/editor/verifier/VerifierView.coffee | 35 +++++ app/views/play/level/PlayLevelView.coffee | 4 +- app/views/play/level/tome/TomeView.coffee | 2 +- headless_client.coffee | 27 ++-- headless_client/cluster.coffee | 63 ++++++++ headless_client/jQlone.coffee | 2 - headless_client/verifier.js | 3 + package.json | 1 + verifier.coffee | 147 ++++++++++++++++++ 22 files changed, 494 insertions(+), 48 deletions(-) create mode 100644 app/templates/editor/verifier/verifier-view.jade create mode 100644 app/views/editor/verifier/VerifierTest.coffee create mode 100644 app/views/editor/verifier/VerifierView.coffee create mode 100644 headless_client/cluster.coffee create mode 100644 headless_client/verifier.js create mode 100644 verifier.coffee diff --git a/app/assets/javascripts/workers/worker_world.js b/app/assets/javascripts/workers/worker_world.js index 7d03e647e..4d6b8c56b 100644 --- a/app/assets/javascripts/workers/worker_world.js +++ b/app/assets/javascripts/workers/worker_world.js @@ -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(); diff --git a/app/core/Router.coffee b/app/core/Router.coffee index 09de17224..560009438 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -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') diff --git a/app/lib/Angel.coffee b/app/lib/Angel.coffee index 6c651c44c..412d86a9f 100644 --- a/app/lib/Angel.coffee +++ b/app/lib/Angel.coffee @@ -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() diff --git a/app/lib/God.coffee b/app/lib/God.coffee index 5fe349a76..717b62225 100644 --- a/app/lib/God.coffee +++ b/app/lib/God.coffee @@ -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 @@ -94,6 +95,7 @@ module.exports = class God extends CocoClass level: @level levelSessionIDs: @levelSessionIDs submissionCount: @lastSubmissionCount + fixedSeed: @lastFixedSeed flagHistory: @lastFlagHistory difficulty: @lastDifficulty goals: @angelsShare.goalManager?.getGoals() @@ -126,6 +128,7 @@ module.exports = class God extends CocoClass level: @level levelSessionIDs: @levelSessionIDs submissionCount: @lastSubmissionCount + fixedSeed: @fixedSeed flagHistory: @lastFlagHistory difficulty: @lastDifficulty goals: @goalManager?.getGoals() diff --git a/app/lib/LevelBus.coffee b/app/lib/LevelBus.coffee index 0792e3b44..741420cca 100644 --- a/app/lib/LevelBus.coffee +++ b/app/lib/LevelBus.coffee @@ -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 diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index 0f3d86af7..e016ce770 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -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') ? {} diff --git a/app/models/Level.coffee b/app/models/Level.coffee index 914b9f54f..ff0783cab 100644 --- a/app/models/Level.coffee +++ b/app/models/Level.coffee @@ -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 diff --git a/app/models/SuperModel.coffee b/app/models/SuperModel.coffee index f0b6d5c76..1a18e88fd 100644 --- a/app/models/SuperModel.coffee +++ b/app/models/SuperModel.coffee @@ -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++ diff --git a/app/schemas/subscriptions/god.coffee b/app/schemas/subscriptions/god.coffee index de6f0c606..2a6ccdced 100644 --- a/app/schemas/subscriptions/god.coffee +++ b/app/schemas/subscriptions/god.coffee @@ -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'} diff --git a/app/schemas/subscriptions/misc.coffee b/app/schemas/subscriptions/misc.coffee index 9fb7ace35..50b94822e 100644 --- a/app/schemas/subscriptions/misc.coffee +++ b/app/schemas/subscriptions/misc.coffee @@ -70,3 +70,6 @@ module.exports = 'application:service-loaded': c.object {required: ['service']}, service: {type: 'string'} # 'segment' + + 'test:update': c.object {}, + state: {type: 'string'} diff --git a/app/schemas/subscriptions/tome.coffee b/app/schemas/subscriptions/tome.coffee index d2e2d1acc..983b746a3 100644 --- a/app/schemas/subscriptions/tome.coffee +++ b/app/schemas/subscriptions/tome.coffee @@ -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'} diff --git a/app/templates/editor/verifier/verifier-view.jade b/app/templates/editor/verifier/verifier-view.jade new file mode 100644 index 000000000..7d491c19c --- /dev/null +++ b/app/templates/editor/verifier/verifier-view.jade @@ -0,0 +1,33 @@ +extends /templates/base-flat + +block content + .container + each test in view.tests + if test.level + h2= test.level.get('name') + small= ' in ' + test.language + '' + div.well(style='width: 300px; float: right') + if test.goals + each v,k in test.goals || [] + case v.status + when 'success': div(style='color: green') ✓ #{k} + when 'incomplete': div(style='color: orange') ✘ #{k} + when 'failure': div(style='color: red') ✘ #{k} + default: div(style='color: blue') #{k} + else + h3 Running.... + if test.solution + pre(style='margin-right: 350px') #{test.solution.source} + else + h4 Solution not found... + else + h1 Loading Level... + + div#tome-view + div#goals-veiw + + br + + // TODO: show errors + // TODO: frame length + // TODO: show last frame hash diff --git a/app/views/editor/verifier/VerifierTest.coffee b/app/views/editor/verifier/VerifierTest.coffee new file mode 100644 index 000000000..ee1c97276 --- /dev/null +++ b/app/views/editor/verifier/VerifierTest.coffee @@ -0,0 +1,102 @@ +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) -> + @goals = e.goalStates + @frames = e.totalFrames + @lastFrameHash = e.lastFrameHash + @state = 'complete' + @updateCallback? state: @state + + 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 diff --git a/app/views/editor/verifier/VerifierView.coffee b/app/views/editor/verifier/VerifierView.coffee new file mode 100644 index 000000000..1fc6a6b3f --- /dev/null +++ b/app/views/editor/verifier/VerifierView.coffee @@ -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() diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index 71c134ca8..c2ed0f687 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -135,7 +135,7 @@ module.exports = class PlayLevelView extends RootView load: -> @loadStartTime = new Date() - @god = new God debugWorker: true + @god = new God() @levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @opponentSessionID, team: @getQueryVariable('team'), observing: @observing, courseID: @courseID @listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded @@ -512,7 +512,7 @@ module.exports = class PlayLevelView extends RootView break Backbone.Mediator.publish 'tome:cast-spell', {} - onWindowResize: (e) => + onWindowResize: (e) => @endHighlight() onDisableControls: (e) -> diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee index 3e8bf1fb3..27766e578 100644 --- a/app/views/play/level/tome/TomeView.coffee +++ b/app/views/play/level/tome/TomeView.coffee @@ -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() diff --git a/headless_client.coffee b/headless_client.coffee index c0e545a09..24ebd26a6 100644 --- a/headless_client.coffee +++ b/headless_client.coffee @@ -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: -> diff --git a/headless_client/cluster.coffee b/headless_client/cluster.coffee new file mode 100644 index 000000000..3a985cce8 --- /dev/null +++ b/headless_client/cluster.coffee @@ -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(' ') diff --git a/headless_client/jQlone.coffee b/headless_client/jQlone.coffee index 0d914f094..2e950ebd7 100644 --- a/headless_client/jQlone.coffee +++ b/headless_client/jQlone.coffee @@ -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 diff --git a/headless_client/verifier.js b/headless_client/verifier.js new file mode 100644 index 000000000..67fbed286 --- /dev/null +++ b/headless_client/verifier.js @@ -0,0 +1,3 @@ +require('coffee-script'); +require('coffee-script/register'); +var server = require('../verifier.coffee'); diff --git a/package.json b/package.json index 0f28fce31..6b4856eff 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/verifier.coffee b/verifier.coffee new file mode 100644 index 000000000..f6ccd5fa7 --- /dev/null +++ b/verifier.coffee @@ -0,0 +1,147 @@ +useEsper = false +bowerComponentsPath = './bower_components/' +headlessClientPath = './headless_client/' + +# SETTINGS +options = + workerCode: require headlessClientPath + 'worker_world' + debug: false # Enable logging of ajax calls mainly + testing: false # Instead of simulating 'real' games, use the same one over and over again. Good for leak hunting. + testFile: require headlessClientPath + 'test.js' + leakTest: false # Install callback that tries to find leaks automatically + exitOnLeak: false # Exit if leak is found. Only useful if leaktest is set to true, obviously. + heapdump: false # Dumps the whole heap after every pass. The heap dumps can then be viewed in Chrome browser. + headlessClient: true + +options.heapdump = require('heapdump') if options.heapdump +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 +disable = [ + 'lib/AudioPlayer' + 'locale/locale' + '../locale/locale' +] + +# Start of the actual code. Setting up the enivronment to match the environment of the browser + +# Global emulated stuff +GLOBAL.window = GLOBAL +GLOBAL.document = + location: + pathname: 'headless_client' + search: '' + +GLOBAL.console.debug = console.log +GLOBAL.serverConfig = + picoCTF: false + production: false + +#try +# GLOBAL.Worker = require('webworker-threads').Worker +#catch e +# GLOBAL.Worker = require('./headless_client/fork_web_worker').Worker +# options.workerCode = './worker_world.coffee' +# +#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: -> +store = {} +GLOBAL.localStorage = + getItem: (key) => store[key] + setItem: (key, s) => store[key] = s + removeItem: (key) => delete store[key] +GLOBAL.lscache = require bowerComponentsPath + 'lscache/lscache' +GLOBAL.esper = require bowerComponentsPath + 'esper.js/esper' + +# Hook node.js require. See https://github.com/mfncooper/mockery/blob/master/mockery.js +# The signature of this function *must* match that of Node's Module._load, +# since it will replace that. +# (Why is there no easier way?) +# the path used for the loader. __dirname is module dependent. +path = __dirname +m = require 'module' +originalLoader = m._load +hookedLoader = (request, parent, isMain) -> + if request in disable or ~request.indexOf('templates') + console.log 'Ignored ' + request if options.debug + return class fake + else if /node_modules[\\\/]aether[\\\/]/.test parent.id + null # Let it through + else if '/' in request and not (request[0] is '.') or request is 'application' + #console.log 'making path', path + '/app/' + request, 'from', path, request, 'with parent', parent + request = path + '/app/' + request + else if request is 'underscore' + request = 'lodash' + console.log 'loading ' + request if options.debug + originalLoader request, parent, isMain + +unhook = () -> + m._load = originalLoader +hook = () -> + m._load = hookedLoader + +GLOBAL.$ = GLOBAL.jQuery = require headlessClientPath + 'jQlone' +$._debug = options.debug +$._server = server + +do (setupLodash = this) -> + GLOBAL._ = require 'lodash' + _.str = require 'underscore.string' + _.string = _.str + _.mixin _.str.exports() + +# load Backbone. Needs hooked loader to reroute underscore to lodash. +hook() +GLOBAL.Backbone = require bowerComponentsPath + 'backbone/backbone' +# Use original loader for theese +unhook() +Backbone.$ = $ +require bowerComponentsPath + 'validated-backbone-mediator/backbone-mediator' +Backbone.Mediator.setValidationEnabled false +GLOBAL.Aether = require 'aether' +eval require('fs').readFileSync('./vendor/scripts/Box2dWeb-2.1.a.3.js', 'utf8') +GLOBAL.Box2D = Box2D +# Set up new loader. Again. +hook() + + +SuperModel = require 'models/SuperModel' +VerifierTest = require('views/editor/verifier/VerifierTest') + +supermodel = new SuperModel() + +oldGetQueryVariable = require('core/utils').getQueryVariable +require('core/utils').getQueryVariable = (args...) -> + return useEsper if args[0] is 'esper' + oldGetQueryVariable args... + +list = process.argv.slice(2); +async = require 'async' + + + +async.eachSeries list, (item, next) -> + async.eachSeries ['python','javascript'], (lang, lnext) -> + test = new VerifierTest item, (e) -> + return if e.state is 'running' + obj = + error: test.error + state: e.state + level: item, + language: lang + observed: + goals: _.mapValues(test.goals, 'status') + frameCount: test.frames + lastHash: test.lastFrameHash + solution: + test.solution + process.send?(obj) + console.log(obj) + lnext() if e.state in ['error','complete'] + , supermodel, lang + , () -> next() From a524256b5b4694e1d97396dba90e050986a992a8 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Fri, 8 Apr 2016 12:59:10 -0700 Subject: [PATCH 06/13] Add sessionless play for Teachers --- app/lib/LevelLoader.coffee | 2 +- app/locale/ar.coffee | 2 +- app/locale/bg.coffee | 2 +- app/locale/ca.coffee | 2 +- app/locale/cs.coffee | 2 +- app/locale/da.coffee | 2 +- app/locale/de-AT.coffee | 2 +- app/locale/de-CH.coffee | 2 +- app/locale/de-DE.coffee | 2 +- app/locale/el.coffee | 2 +- app/locale/en-GB.coffee | 2 +- app/locale/en-US.coffee | 2 +- app/locale/en.coffee | 6 ++- app/locale/eo.coffee | 2 +- app/locale/es-419.coffee | 2 +- app/locale/es-ES.coffee | 2 +- app/locale/et.coffee | 2 +- app/locale/fa.coffee | 2 +- app/locale/fi.coffee | 2 +- app/locale/fr.coffee | 2 +- app/locale/gl.coffee | 2 +- app/locale/he.coffee | 2 +- app/locale/hi.coffee | 2 +- app/locale/hu.coffee | 2 +- app/locale/id.coffee | 2 +- app/locale/it.coffee | 2 +- app/locale/ja.coffee | 2 +- app/locale/ko.coffee | 2 +- app/locale/lt.coffee | 2 +- app/locale/mk-MK.coffee | 2 +- app/locale/ms.coffee | 2 +- app/locale/my.coffee | 2 +- app/locale/nb.coffee | 2 +- app/locale/nl-BE.coffee | 2 +- app/locale/nl-NL.coffee | 2 +- app/locale/nn.coffee | 2 +- app/locale/pl.coffee | 2 +- app/locale/pt-BR.coffee | 2 +- app/locale/pt-PT.coffee | 2 +- app/locale/ro.coffee | 2 +- app/locale/ru.coffee | 2 +- app/locale/sl.coffee | 2 +- app/locale/sr.coffee | 2 +- app/locale/sv.coffee | 2 +- app/locale/th.coffee | 2 +- app/locale/uk.coffee | 2 +- app/locale/ur.coffee | 2 +- app/locale/uz.coffee | 2 +- app/locale/vi.coffee | 2 +- app/locale/zh-HANS.coffee | 2 +- app/locale/zh-WUU-HANS.coffee | 2 +- app/locale/zh-WUU-HANT.coffee | 2 +- app/models/SuperModel.coffee | 4 +- .../play/level/modal/progress-view.sass | 7 +++- .../courses/teacher-courses-view.jade | 30 ++++++++------ app/templates/play/level/control_bar.jade | 2 +- .../play/level/modal/progress-view.jade | 37 +++++++++++------- app/views/core/ContactModal.coffee | 4 +- app/views/courses/TeacherCoursesView.coffee | 13 +++++++ app/views/play/level/ControlBarView.coffee | 5 ++- app/views/play/level/PlayLevelView.coffee | 7 +++- .../level/modal/CourseVictoryModal.coffee | 39 ++++++++++++------- 62 files changed, 153 insertions(+), 103 deletions(-) diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index e016ce770..14ca152fa 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -377,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 diff --git a/app/locale/ar.coffee b/app/locale/ar.coffee index 9629a47f4..3398d1b5c 100644 --- a/app/locale/ar.coffee +++ b/app/locale/ar.coffee @@ -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. diff --git a/app/locale/bg.coffee b/app/locale/bg.coffee index 628e51618..9cf7f7825 100644 --- a/app/locale/bg.coffee +++ b/app/locale/bg.coffee @@ -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. diff --git a/app/locale/ca.coffee b/app/locale/ca.coffee index 99e873312..89046f71d 100644 --- a/app/locale/ca.coffee +++ b/app/locale/ca.coffee @@ -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. diff --git a/app/locale/cs.coffee b/app/locale/cs.coffee index 0ec946c42..d309c3559 100644 --- a/app/locale/cs.coffee +++ b/app/locale/cs.coffee @@ -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. diff --git a/app/locale/da.coffee b/app/locale/da.coffee index 71f814269..c2c182f7d 100644 --- a/app/locale/da.coffee +++ b/app/locale/da.coffee @@ -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. diff --git a/app/locale/de-AT.coffee b/app/locale/de-AT.coffee index 3141113d7..7fc4933f6 100644 --- a/app/locale/de-AT.coffee +++ b/app/locale/de-AT.coffee @@ -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. diff --git a/app/locale/de-CH.coffee b/app/locale/de-CH.coffee index 28eca5192..522aa05e7 100644 --- a/app/locale/de-CH.coffee +++ b/app/locale/de-CH.coffee @@ -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. diff --git a/app/locale/de-DE.coffee b/app/locale/de-DE.coffee index 844d06c64..a2c9fd680 100644 --- a/app/locale/de-DE.coffee +++ b/app/locale/de-DE.coffee @@ -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. diff --git a/app/locale/el.coffee b/app/locale/el.coffee index cc7fa37c2..b326a40eb 100644 --- a/app/locale/el.coffee +++ b/app/locale/el.coffee @@ -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. diff --git a/app/locale/en-GB.coffee b/app/locale/en-GB.coffee index 5302c40c9..5008d3143 100644 --- a/app/locale/en-GB.coffee +++ b/app/locale/en-GB.coffee @@ -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. diff --git a/app/locale/en-US.coffee b/app/locale/en-US.coffee index 2bad2bf0c..1a830dec5 100644 --- a/app/locale/en-US.coffee +++ b/app/locale/en-US.coffee @@ -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. diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 635e66823..4e0e0934f 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -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" \ No newline at end of file + oreilly: "ebook of your choice" diff --git a/app/locale/eo.coffee b/app/locale/eo.coffee index b131a6201..14572b85f 100644 --- a/app/locale/eo.coffee +++ b/app/locale/eo.coffee @@ -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. diff --git a/app/locale/es-419.coffee b/app/locale/es-419.coffee index 230f9efaf..fe36e2e6a 100644 --- a/app/locale/es-419.coffee +++ b/app/locale/es-419.coffee @@ -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. diff --git a/app/locale/es-ES.coffee b/app/locale/es-ES.coffee index 736c0cfdc..9b55664fa 100644 --- a/app/locale/es-ES.coffee +++ b/app/locale/es-ES.coffee @@ -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. diff --git a/app/locale/et.coffee b/app/locale/et.coffee index b525ee676..dc2520211 100644 --- a/app/locale/et.coffee +++ b/app/locale/et.coffee @@ -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. diff --git a/app/locale/fa.coffee b/app/locale/fa.coffee index 4d4daefa8..7619f85a1 100644 --- a/app/locale/fa.coffee +++ b/app/locale/fa.coffee @@ -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. diff --git a/app/locale/fi.coffee b/app/locale/fi.coffee index 1fe2bd0ef..b2f1874de 100644 --- a/app/locale/fi.coffee +++ b/app/locale/fi.coffee @@ -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. diff --git a/app/locale/fr.coffee b/app/locale/fr.coffee index d6d2a9b04..f935e295f 100644 --- a/app/locale/fr.coffee +++ b/app/locale/fr.coffee @@ -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. diff --git a/app/locale/gl.coffee b/app/locale/gl.coffee index 1d2162c0c..9eb0cfb34 100644 --- a/app/locale/gl.coffee +++ b/app/locale/gl.coffee @@ -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. diff --git a/app/locale/he.coffee b/app/locale/he.coffee index 0a9804ab9..c7c96ca94 100644 --- a/app/locale/he.coffee +++ b/app/locale/he.coffee @@ -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. diff --git a/app/locale/hi.coffee b/app/locale/hi.coffee index 914518e6e..460939840 100644 --- a/app/locale/hi.coffee +++ b/app/locale/hi.coffee @@ -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. diff --git a/app/locale/hu.coffee b/app/locale/hu.coffee index b1e239cac..92f95f47c 100644 --- a/app/locale/hu.coffee +++ b/app/locale/hu.coffee @@ -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. diff --git a/app/locale/id.coffee b/app/locale/id.coffee index e5d979fef..2b98b99fe 100644 --- a/app/locale/id.coffee +++ b/app/locale/id.coffee @@ -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. diff --git a/app/locale/it.coffee b/app/locale/it.coffee index d51a35db0..1fba91ceb 100644 --- a/app/locale/it.coffee +++ b/app/locale/it.coffee @@ -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. diff --git a/app/locale/ja.coffee b/app/locale/ja.coffee index eea40884a..a46f36ab6 100644 --- a/app/locale/ja.coffee +++ b/app/locale/ja.coffee @@ -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. diff --git a/app/locale/ko.coffee b/app/locale/ko.coffee index 538742301..2e9f80f89 100644 --- a/app/locale/ko.coffee +++ b/app/locale/ko.coffee @@ -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. diff --git a/app/locale/lt.coffee b/app/locale/lt.coffee index 53419c607..2b73f4885 100644 --- a/app/locale/lt.coffee +++ b/app/locale/lt.coffee @@ -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. diff --git a/app/locale/mk-MK.coffee b/app/locale/mk-MK.coffee index cad0797fa..d7e8ba35d 100644 --- a/app/locale/mk-MK.coffee +++ b/app/locale/mk-MK.coffee @@ -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. diff --git a/app/locale/ms.coffee b/app/locale/ms.coffee index 43e68e637..3b9c684b4 100644 --- a/app/locale/ms.coffee +++ b/app/locale/ms.coffee @@ -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. diff --git a/app/locale/my.coffee b/app/locale/my.coffee index d120037f4..233f7322c 100644 --- a/app/locale/my.coffee +++ b/app/locale/my.coffee @@ -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. diff --git a/app/locale/nb.coffee b/app/locale/nb.coffee index 36b6544af..8e3a68a15 100644 --- a/app/locale/nb.coffee +++ b/app/locale/nb.coffee @@ -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. diff --git a/app/locale/nl-BE.coffee b/app/locale/nl-BE.coffee index 5387f989a..8d76fef18 100644 --- a/app/locale/nl-BE.coffee +++ b/app/locale/nl-BE.coffee @@ -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. diff --git a/app/locale/nl-NL.coffee b/app/locale/nl-NL.coffee index f9ce19dae..cb0d1379b 100644 --- a/app/locale/nl-NL.coffee +++ b/app/locale/nl-NL.coffee @@ -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. diff --git a/app/locale/nn.coffee b/app/locale/nn.coffee index 73f4e5267..20873b42e 100644 --- a/app/locale/nn.coffee +++ b/app/locale/nn.coffee @@ -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. diff --git a/app/locale/pl.coffee b/app/locale/pl.coffee index b9438ffc2..bd43d1567 100644 --- a/app/locale/pl.coffee +++ b/app/locale/pl.coffee @@ -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. diff --git a/app/locale/pt-BR.coffee b/app/locale/pt-BR.coffee index dcd339332..3e1ed0ada 100644 --- a/app/locale/pt-BR.coffee +++ b/app/locale/pt-BR.coffee @@ -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. diff --git a/app/locale/pt-PT.coffee b/app/locale/pt-PT.coffee index f33c0ee69..720c8abfe 100644 --- a/app/locale/pt-PT.coffee +++ b/app/locale/pt-PT.coffee @@ -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. diff --git a/app/locale/ro.coffee b/app/locale/ro.coffee index 87096bcdd..8f3f79d45 100644 --- a/app/locale/ro.coffee +++ b/app/locale/ro.coffee @@ -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. diff --git a/app/locale/ru.coffee b/app/locale/ru.coffee index a832aa50f..761d50f59 100644 --- a/app/locale/ru.coffee +++ b/app/locale/ru.coffee @@ -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. diff --git a/app/locale/sl.coffee b/app/locale/sl.coffee index abcfdecbf..8f5ed155f 100644 --- a/app/locale/sl.coffee +++ b/app/locale/sl.coffee @@ -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. diff --git a/app/locale/sr.coffee b/app/locale/sr.coffee index 1f078273f..75a0c6a15 100644 --- a/app/locale/sr.coffee +++ b/app/locale/sr.coffee @@ -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. diff --git a/app/locale/sv.coffee b/app/locale/sv.coffee index 340ca5a19..bdf35d30b 100644 --- a/app/locale/sv.coffee +++ b/app/locale/sv.coffee @@ -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. diff --git a/app/locale/th.coffee b/app/locale/th.coffee index 035a4cfb0..04e4a5955 100644 --- a/app/locale/th.coffee +++ b/app/locale/th.coffee @@ -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. diff --git a/app/locale/uk.coffee b/app/locale/uk.coffee index 5f0ca7e8e..7f0b05083 100644 --- a/app/locale/uk.coffee +++ b/app/locale/uk.coffee @@ -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. diff --git a/app/locale/ur.coffee b/app/locale/ur.coffee index 95e4330ef..2ef99581d 100644 --- a/app/locale/ur.coffee +++ b/app/locale/ur.coffee @@ -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. diff --git a/app/locale/uz.coffee b/app/locale/uz.coffee index 25dd90e23..a891b3a75 100644 --- a/app/locale/uz.coffee +++ b/app/locale/uz.coffee @@ -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. diff --git a/app/locale/vi.coffee b/app/locale/vi.coffee index fa6f7e2cd..4b3b06950 100644 --- a/app/locale/vi.coffee +++ b/app/locale/vi.coffee @@ -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. diff --git a/app/locale/zh-HANS.coffee b/app/locale/zh-HANS.coffee index 1d458150d..9dec32aad 100644 --- a/app/locale/zh-HANS.coffee +++ b/app/locale/zh-HANS.coffee @@ -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. diff --git a/app/locale/zh-WUU-HANS.coffee b/app/locale/zh-WUU-HANS.coffee index dcc26bb87..413084b38 100644 --- a/app/locale/zh-WUU-HANS.coffee +++ b/app/locale/zh-WUU-HANS.coffee @@ -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. diff --git a/app/locale/zh-WUU-HANT.coffee b/app/locale/zh-WUU-HANT.coffee index 469a14dcf..3ad12f378 100644 --- a/app/locale/zh-WUU-HANT.coffee +++ b/app/locale/zh-WUU-HANT.coffee @@ -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. diff --git a/app/models/SuperModel.coffee b/app/models/SuperModel.coffee index 1a18e88fd..3a2c28767 100644 --- a/app/models/SuperModel.coffee +++ b/app/models/SuperModel.coffee @@ -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 diff --git a/app/styles/play/level/modal/progress-view.sass b/app/styles/play/level/modal/progress-view.sass index 700933bc8..61d9f89c3 100644 --- a/app/styles/play/level/modal/progress-view.sass +++ b/app/styles/play/level/modal/progress-view.sass @@ -5,4 +5,9 @@ margin-bottom: 5px p - margin-top: 30px \ No newline at end of file + margin-top: 30px + + .course-title + white-space: nowrap + text-overflow: ellipsis + overflow: hidden diff --git a/app/templates/courses/teacher-courses-view.jade b/app/templates/courses/teacher-courses-view.jade index 3a08fbb8c..7d33eb6bd 100644 --- a/app/templates/courses/teacher-courses-view.jade +++ b/app/templates/courses/teacher-courses-view.jade @@ -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 diff --git a/app/templates/play/level/control_bar.jade b/app/templates/play/level/control_bar.jade index f60bf73ef..bc8f5a5d7 100644 --- a/app/templates/play/level/control_bar.jade +++ b/app/templates/play/level/control_bar.jade @@ -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 diff --git a/app/templates/play/level/modal/progress-view.jade b/app/templates/play/level/modal/progress-view.jade index a1a787018..02fdd0c7f 100644 --- a/app/templates/play/level/modal/progress-view.jade +++ b/app/templates/play/level/modal/progress-view.jade @@ -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 \ No newline at end of file + button#done-btn.btn.btn-illustrated.btn-primary.btn-block.btn-lg.text-uppercase(data-i18n='play_level.done') diff --git a/app/views/core/ContactModal.coffee b/app/views/core/ContactModal.coffee index 8c9acf951..5d304a564 100644 --- a/app/views/core/ContactModal.coffee +++ b/app/views/core/ContactModal.coffee @@ -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/", "/")) diff --git a/app/views/courses/TeacherCoursesView.coffee b/app/views/courses/TeacherCoursesView.coffee index ef1a38e87..4e0517687 100644 --- a/app/views/courses/TeacherCoursesView.coffee +++ b/app/views/courses/TeacherCoursesView.coffee @@ -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() diff --git a/app/views/play/level/ControlBarView.coffee b/app/views/play/level/ControlBarView.coffee index 4a5b61c4d..be90a0d79 100644 --- a/app/views/play/level/ControlBarView.coffee +++ b/app/views/play/level/ControlBarView.coffee @@ -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' diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index c2ed0f687..e47bd9c56 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -136,7 +136,10 @@ module.exports = class PlayLevelView extends RootView load: -> @loadStartTime = new Date() @god = new God() - @levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @opponentSessionID, team: @getQueryVariable('team'), observing: @observing, courseID: @courseID + 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: -> @@ -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) diff --git a/app/views/play/level/modal/CourseVictoryModal.coffee b/app/views/play/level/modal/CourseVictoryModal.coffee index 4a8ef83fb..cbc52ccff 100644 --- a/app/views/play/level/modal/CourseVictoryModal.coffee +++ b/app/views/play/level/modal/CourseVictoryModal.coffee @@ -100,20 +100,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 +163,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=#{me.get('aceConfig').language}" + 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}) From 0dc913ba78d92a2973b4f6e63ca8c0f95ceff1a1 Mon Sep 17 00:00:00 2001 From: cundamic Date: Wed, 13 Apr 2016 17:59:24 +0200 Subject: [PATCH 07/13] Update sk.coffee translated 1-447 --- app/locale/sk.coffee | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app/locale/sk.coffee b/app/locale/sk.coffee index d323da4ee..88cbfa692 100644 --- a/app/locale/sk.coffee +++ b/app/locale/sk.coffee @@ -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 pre študentov, kde sa naučia programovať hrou. " - our_courses: "Naše kurzy boli špeciálne testované 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é v reálnych triedach 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,10 +442,10 @@ 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_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: "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" From 5a6993689975fcd53a397c07014936bc59a5c820 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Wed, 13 Apr 2016 09:43:05 -0700 Subject: [PATCH 08/13] Update init-school-roles --- .../2016-03-18-init-school-roles.js | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/scripts/mongodb/migrations/2016-03-18-init-school-roles.js b/scripts/mongodb/migrations/2016-03-18-init-school-roles.js index c57c54545..a08395f20 100644 --- a/scripts/mongodb/migrations/2016-03-18-init-school-roles.js +++ b/scripts/mongodb/migrations/2016-03-18-init-school-roles.js @@ -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'}})); - } -}); \ No newline at end of file + } +}); From bd6a266f600b41aad07602f1176be377cc3cd8e0 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Wed, 13 Apr 2016 11:39:17 -0700 Subject: [PATCH 09/13] Refactor POST /db/classroom and PUT /db/campaign/:handle and their tests for #3469 --- server/handlers/campaign_handler.coffee | 20 +----- server/handlers/classroom_handler.coffee | 9 +-- server/middleware/campaigns.coffee | 16 +++++ server/middleware/classrooms.coffee | 11 +++ server/models/Campaign.coffee | 14 ++++ server/models/Classroom.coffee | 2 +- server/routes/index.coffee | 6 +- server/slack.coffee | 7 +- .../functional/campaign_handler.spec.coffee | 41 +++++++++++ spec/server/functional/classrooms.spec.coffee | 70 +++++++++---------- 10 files changed, 129 insertions(+), 67 deletions(-) diff --git a/server/handlers/campaign_handler.coffee b/server/handlers/campaign_handler.coffee index e3cb82a44..24e292b93 100644 --- a/server/handlers/campaign_handler.coffee +++ b/server/handlers/campaign_handler.coffee @@ -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() diff --git a/server/handlers/classroom_handler.coffee b/server/handlers/classroom_handler.coffee index 69008e018..9fbe6d9d2 100644 --- a/server/handlers/classroom_handler.coffee +++ b/server/handlers/classroom_handler.coffee @@ -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' diff --git a/server/middleware/campaigns.coffee b/server/middleware/campaigns.coffee index c5d003ca5..754966576 100644 --- a/server/middleware/campaigns.coffee +++ b/server/middleware/campaigns.coffee @@ -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 diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index 1464eed6a..13b5e357f 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -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})) \ No newline at end of file diff --git a/server/models/Campaign.coffee b/server/models/Campaign.coffee index 28c0d87b3..0fc68d2bd 100644 --- a/server/models/Campaign.coffee +++ b/server/models/Campaign.coffee @@ -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) diff --git a/server/models/Classroom.coffee b/server/models/Classroom.coffee index 05e835d8f..c49533273 100644 --- a/server/models/Classroom.coffee +++ b/server/models/Classroom.coffee @@ -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 diff --git a/server/routes/index.coffee b/server/routes/index.coffee index 7802cad58..abea5d999 100644 --- a/server/routes/index.coffee +++ b/server/routes/index.coffee @@ -40,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? @@ -50,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) diff --git a/server/slack.coffee b/server/slack.coffee index 990356451..2f069d31e 100644 --- a/server/slack.coffee +++ b/server/slack.coffee @@ -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 diff --git a/spec/server/functional/campaign_handler.spec.coffee b/spec/server/functional/campaign_handler.spec.coffee index 8949eec07..940cd5f86 100644 --- a/spec/server/functional/campaign_handler.spec.coffee +++ b/spec/server/functional/campaign_handler.spec.coffee @@ -22,6 +22,7 @@ achievement = { campaign = { name: 'Campaign' levels: {} + i18n: {} } levelURL = getURL('/db/level') @@ -34,6 +35,46 @@ Campaign = require '../../../server/models/Campaign' Level = require '../../../server/models/Level' User = require '../../../server/models/User' request = require '../request' +utils = require '../utils' +slack = require '../../../server/slack' + +describe 'PUT /db/campaign', -> + beforeEach utils.wrap (done) -> + yield utils.clearModels [Achievement, Campaign, Level, User] + admin = yield utils.initAdmin() + yield utils.loginUser(admin) + [res, body] = yield request.postAsync { uri: campaignURL, json: campaign } + @campaign = yield Campaign.findById(body._id) + done() + + it 'saves changes to campaigns', utils.wrap (done) -> + [res, body] = yield request.putAsync { uri: campaignURL+'/'+@campaign.id, json: { name: 'A new name' } } + expect(body.name).toBe('A new name') + c = yield Campaign.findById(body._id) + expect(c.get('name')).toBe('A new name') + done() + + it 'does not allow normal users to make changes', utils.wrap (done) -> + user = yield utils.initUser() + yield utils.loginUser(user) + [res, body] = yield request.putAsync { uri: campaignURL+'/'+@campaign.id, json: { name: 'A new name' } } + expect(res.statusCode).toBe(403) + done() + + it 'allows normal users to put translation changes', utils.wrap (done) -> + user = yield utils.initUser() + yield utils.loginUser(user) + json = _.clone @campaign.toObject() + json.i18n = { de: { name: 'A new name' } } + [res, body] = yield request.putAsync { uri: campaignURL+'/'+@campaign.id, json: json } + expect(res.statusCode).toBe(200) + done() + + it 'sends a slack message', utils.wrap (done) -> + spyOn(slack, 'sendSlackMessage') + [res, body] = yield request.putAsync { uri: campaignURL+'/'+@campaign.id, json: { name: 'A new name' } } + expect(slack.sendSlackMessage).toHaveBeenCalled() + done() describe '/db/campaign', -> it 'prepares the db first', (done) -> diff --git a/spec/server/functional/classrooms.spec.coffee b/spec/server/functional/classrooms.spec.coffee index 117b83c78..e8f10dc31 100644 --- a/spec/server/functional/classrooms.spec.coffee +++ b/spec/server/functional/classrooms.spec.coffee @@ -50,7 +50,7 @@ describe 'GET /db/classroom/:id', -> user1.save (err) -> data = { name: 'Classroom 1' } request.post {uri: classroomsURL, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(200) + expect(res.statusCode).toBe(201) classroomID = body._id request.get {uri: classroomsURL + '/' + body._id }, (err, res, body) -> expect(res.statusCode).toBe(200) @@ -59,36 +59,34 @@ describe 'GET /db/classroom/:id', -> describe 'POST /db/classroom', -> - it 'clears database users and classrooms', (done) -> - clearModels [User, Classroom], (err) -> - throw err if err - done() - - it 'creates a new classroom for the given user', (done) -> - loginNewUser (user1) -> - user1.set('role', 'teacher') - user1.save (err) -> - data = { name: 'Classroom 1' } - request.post {uri: classroomsURL, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(200) - expect(body.name).toBe('Classroom 1') - expect(body.members.length).toBe(0) - expect(body.ownerID).toBe(user1.id) - done() + beforeEach utils.wrap (done) -> + yield utils.clearModels [User, Classroom] + done() + + it 'creates a new classroom for the given user with teacher role', utils.wrap (done) -> + teacher = yield utils.initUser({role: 'teacher'}) + yield utils.loginUser(teacher) + data = { name: 'Classroom 1' } + [res, body] = yield request.postAsync {uri: classroomsURL, json: data } + expect(res.statusCode).toBe(201) + expect(res.body.name).toBe('Classroom 1') + expect(res.body.members.length).toBe(0) + expect(res.body.ownerID).toBe(teacher.id) + done() - it 'does not work for anonymous users', (done) -> - logoutUser -> - data = { name: 'Classroom 2' } - request.post {uri: classroomsURL, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(401) - done() + it 'returns 401 for anonymous users', utils.wrap (done) -> + data = { name: 'Classroom 2' } + [res, body] = yield request.postAsync {uri: classroomsURL, json: data } + expect(res.statusCode).toBe(401) + done() - it 'does not work for non-teacher users', (done) -> - loginNewUser (user1) -> - data = { name: 'Classroom 1' } - request.post {uri: classroomsURL, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(403) - done() + it 'does not work for non-teacher users', utils.wrap (done) -> + user = yield utils.initUser() + yield utils.loginUser(user) + data = { name: 'Classroom 1' } + [res, body] = yield request.postAsync {uri: classroomsURL, json: data } + expect(res.statusCode).toBe(403) + done() describe 'PUT /db/classroom', -> @@ -104,7 +102,7 @@ describe 'PUT /db/classroom', -> user1.save (err) -> data = { name: 'Classroom 2' } request.post {uri: classroomsURL, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(200) + expect(res.statusCode).toBe(201) data = { name: 'Classroom 3', description: 'New Description' } url = classroomsURL + '/' + body._id request.put { uri: url, json: data }, (err, res, body) -> @@ -118,7 +116,7 @@ describe 'PUT /db/classroom', -> user1.save (err) -> data = { name: 'Classroom 4' } request.post {uri: classroomsURL, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(200) + expect(res.statusCode).toBe(201) classroomCode = body.code loginNewUser (user2) -> url = getURL("/db/classroom/~/members") @@ -145,7 +143,7 @@ describe 'POST /db/classroom/~/members', -> request.post {uri: classroomsURL, json: data }, (err, res, body) -> classroomCode = body.code classroomID = body._id - expect(res.statusCode).toBe(200) + expect(res.statusCode).toBe(201) loginNewUser (user2) -> url = getURL("/db/classroom/~/members") data = { code: classroomCode } @@ -166,7 +164,7 @@ describe 'POST /db/classroom/~/members', -> request.post {uri: classroomsURL, json: data }, (err, res, body) -> classroomCode = body.code classroomID = body._id - expect(res.statusCode).toBe(200) + expect(res.statusCode).toBe(201) loginNewUser (user2) -> user2.set('role', 'teacher') user2.save (err, user2) -> @@ -183,7 +181,7 @@ describe 'POST /db/classroom/~/members', -> teacher = yield utils.initUser({role: 'teacher'}) yield utils.loginUser(teacher) [res, body] = yield request.postAsync {uri: classroomsURL, json: { name: 'Classroom' } } - expect(res.statusCode).toBe(200) + expect(res.statusCode).toBe(201) classroomCode = body.code yield utils.becomeAnonymous() [res, body] = yield request.postAsync { uri: getURL("/db/classroom/~/members"), json: { code: classroomCode } } @@ -206,7 +204,7 @@ describe 'DELETE /db/classroom/:id/members', -> request.post {uri: classroomsURL, json: data }, (err, res, body) -> classroomCode = body.code classroomID = body._id - expect(res.statusCode).toBe(200) + expect(res.statusCode).toBe(201) loginNewUser (user2) -> url = getURL("/db/classroom/~/members") data = { code: classroomCode } @@ -231,7 +229,7 @@ describe 'POST /db/classroom/:id/invite-members', -> user1.save (err) -> data = { name: 'Classroom 6' } request.post {uri: classroomsURL, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(200) + expect(res.statusCode).toBe(201) url = classroomsURL + '/' + body._id + '/invite-members' data = { emails: ['test@test.com'] } request.post { uri: url, json: data }, (err, res, body) -> From e635396b8a500e5f84f07247cdfb38275d61e775 Mon Sep 17 00:00:00 2001 From: Rob Date: Mon, 4 Apr 2016 19:52:10 -0700 Subject: [PATCH 10/13] Use host header to let any server serve any region --- server_config.coffee | 6 ++---- server_setup.coffee | 15 ++++++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/server_config.coffee b/server_config.coffee index 0d936c74e..1c1d19eb3 100644 --- a/server_config.coffee +++ b/server_config.coffee @@ -2,10 +2,8 @@ config = {} config.unittest = global.testing -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 = diff --git a/server_setup.coffee b/server_setup.coffee index 9d2a4164d..45f25df7a 100644 --- a/server_setup.coffee +++ b/server_setup.coffee @@ -105,10 +105,15 @@ setupPassportMiddleware = (app) -> else app.use(authentication.session()) -setupCountryRedirectMiddleware = (app, country="china", countryCode="CN", languageCode="zh", serverID="tokyo") -> +setupCountryRedirectMiddleware = (app, country="china", countryCode="CN", languageCode="zh", host="cn.codecombat.com") -> shouldRedirectToCountryServer = (req) -> speaksLanguage = _.any req.acceptedLanguages, (language) -> language.indexOf languageCode isnt -1 - unless config[serverID] + + #Work around express 3.0 + reqHost = req.hostname + reqHost ?= req.host + + unless reqHost.toLowerCase() is host ip = req.headers['x-forwarded-for'] or req.connection.remoteAddress ip = ip?.split(/,? /)[0] # If there are two IP addresses, say because of CloudFlare, we just take the first. geo = geoip.lookup(ip) @@ -122,7 +127,7 @@ setupCountryRedirectMiddleware = (app, country="china", countryCode="CN", langua app.use (req, res, next) -> if shouldRedirectToCountryServer req - res.writeHead 302, "Location": config[country + 'Domain'] + req.url + res.writeHead 302, "Location": 'http://' + host + req.url res.end() else next() @@ -159,8 +164,8 @@ setupPerfMonMiddleware = (app) -> exports.setupMiddleware = (app) -> setupPerfMonMiddleware app - setupCountryRedirectMiddleware app, "china", "CN", "zh", "tokyo" - setupCountryRedirectMiddleware app, "brazil", "BR", "pt-BR", "saoPaulo" + setupCountryRedirectMiddleware app, "china", "CN", "zh", config.chinaDomain + setupCountryRedirectMiddleware app, "brazil", "BR", "pt-BR", config.brazilDomain setupMiddlewareToSendOldBrowserWarningWhenPlayersViewLevelDirectly app setupExpressMiddleware app setupPassportMiddleware app From 0416528de04d6db80335c6d794ff9c598a984621 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Wed, 13 Apr 2016 16:32:39 -0700 Subject: [PATCH 11/13] Fix language-getting for next level URL on teacher courses play --- app/views/play/level/modal/CourseVictoryModal.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/play/level/modal/CourseVictoryModal.coffee b/app/views/play/level/modal/CourseVictoryModal.coffee index cbc52ccff..cbbd0270d 100644 --- a/app/views/play/level/modal/CourseVictoryModal.coffee +++ b/app/views/play/level/modal/CourseVictoryModal.coffee @@ -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' @@ -164,7 +165,7 @@ module.exports = class CourseVictoryModal extends ModalView onNextLevel: -> if me.isTeacher() - link = "/play/level/#{@nextLevel.get('slug')}?course=#{@courseID}&codeLanguage=#{me.get('aceConfig').language}" + 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}) From ceb64ce9cae0b16cc5d6b833971f66aa7e912170 Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 12 Apr 2016 12:17:05 -0700 Subject: [PATCH 12/13] Improve verifier. --- app/lib/God.coffee | 6 ++- .../editor/verifier/verifier-view.jade | 50 +++++++++++++------ app/views/editor/verifier/VerifierTest.coffee | 9 ++++ verifier.coffee | 4 +- 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/app/lib/God.coffee b/app/lib/God.coffee index 717b62225..ce8013a1c 100644 --- a/app/lib/God.coffee +++ b/app/lib/God.coffee @@ -90,7 +90,7 @@ module.exports = class God extends CocoClass return if hadPreloader @angelsShare.workQueue = [] - @angelsShare.workQueue.push + work = userCodeMap: userCodeMap level: @level levelSessionIDs: @levelSessionIDs @@ -103,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 diff --git a/app/templates/editor/verifier/verifier-view.jade b/app/templates/editor/verifier/verifier-view.jade index 7d491c19c..c3d82403b 100644 --- a/app/templates/editor/verifier/verifier-view.jade +++ b/app/templates/editor/verifier/verifier-view.jade @@ -2,24 +2,42 @@ extends /templates/base-flat block content .container - each test in view.tests + each test, id in view.tests if test.level - h2= test.level.get('name') - small= ' in ' + test.language + '' - div.well(style='width: 300px; float: right') - if test.goals - each v,k in test.goals || [] - case v.status - when 'success': div(style='color: green') ✓ #{k} - when 'incomplete': div(style='color: orange') ✘ #{k} - when 'failure': div(style='color: red') ✘ #{k} - default: div(style='color: blue') #{k} - else - h3 Running.... - if test.solution - pre(style='margin-right: 350px') #{test.solution.source} + + 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 - h4 Solution not found... + 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... diff --git a/app/views/editor/verifier/VerifierTest.coffee b/app/views/editor/verifier/VerifierTest.coffee index ee1c97276..b71301242 100644 --- a/app/views/editor/verifier/VerifierTest.coffee +++ b/app/views/editor/verifier/VerifierTest.coffee @@ -78,12 +78,21 @@ module.exports = class VerifierTest extends CocoClass @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' diff --git a/verifier.coffee b/verifier.coffee index f6ccd5fa7..72e963794 100644 --- a/verifier.coffee +++ b/verifier.coffee @@ -1,7 +1,7 @@ -useEsper = false +useEsper = true bowerComponentsPath = './bower_components/' headlessClientPath = './headless_client/' - +require 'aether' # SETTINGS options = workerCode: require headlessClientPath + 'worker_world' From 6885de36fcbfa99482e9d9a52834980e3b174048 Mon Sep 17 00:00:00 2001 From: cundamic Date: Thu, 14 Apr 2016 06:51:20 +0200 Subject: [PATCH 13/13] Update sk.coffee subscription --- app/locale/sk.coffee | 76 ++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/app/locale/sk.coffee b/app/locale/sk.coffee index 88cbfa692..ebc7ff2a4 100644 --- a/app/locale/sk.coffee +++ b/app/locale/sk.coffee @@ -446,9 +446,9 @@ module.exports = nativeDescription: "slovenčina", englishDescription: "Slovak", 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: "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_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: "{{gems}} bonusových diamantov každý mesiac !" feature5: "Video tutoriály" feature6: "Prémiová emailová podpora" -# feature7: "Private Clans" -# feature8: "No ads!" + feature7: "Súkromnéklany" + feature8: "Žiadne reklamy!" 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"