From f7a6d354afc2bb76e12a82fa862a5c858b7daea4 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Fri, 6 May 2016 13:28:19 -0700 Subject: [PATCH 001/167] Add school and teacher names to invite email context --- app/models/Classroom.coffee | 7 ++++ .../courses/InviteToClassroomModal.coffee | 11 ++----- server/handlers/classroom_handler.coffee | 20 ------------ server/middleware/classrooms.coffee | 32 +++++++++++++++++++ server/routes/index.coffee | 1 + server/sendwithus.coffee | 2 +- spec/server/functional/classrooms.spec.coffee | 28 +++++++++------- spec/server/utils.coffee | 30 ++++++++++++++++- 8 files changed, 89 insertions(+), 42 deletions(-) diff --git a/app/models/Classroom.coffee b/app/models/Classroom.coffee index d75d0ab47..fb4434461 100644 --- a/app/models/Classroom.coffee +++ b/app/models/Classroom.coffee @@ -91,3 +91,10 @@ module.exports = class Classroom extends CocoModel url: _.result(courseInstance, 'url') + '/classroom' }) @fetch(options) + + inviteMembers: (emails, options={}) -> + options.data ?= {} + options.data.email = emails + options.url = @url() + '/invite-members' + options.type = 'POST' + @fetch(options) diff --git a/app/views/courses/InviteToClassroomModal.coffee b/app/views/courses/InviteToClassroomModal.coffee index e9db74611..a59286870 100644 --- a/app/views/courses/InviteToClassroomModal.coffee +++ b/app/views/courses/InviteToClassroomModal.coffee @@ -20,17 +20,12 @@ module.exports = class InviteToClassroomModal extends ModalView emails = _.filter((_.string.trim(email) for email in emails)) if not emails.length return - url = @classroom.url() + '/invite-members' + @$('#send-invites-btn, #invite-emails-textarea').addClass('hide') @$('#invite-emails-sending-alert').removeClass('hide') application.tracker?.trackEvent 'Classroom invite via email', category: 'Courses', classroomID: @classroom.id, emails: emails - - $.ajax({ - url: url - data: {emails: emails} - method: 'POST' - context: @ - success: -> + @classroom.inviteMembers(emails, { + success: => @$('#invite-emails-sending-alert').addClass('hide') @$('#invite-emails-success-alert').removeClass('hide') }) diff --git a/server/handlers/classroom_handler.coffee b/server/handlers/classroom_handler.coffee index 9fbe6d9d2..708127b24 100644 --- a/server/handlers/classroom_handler.coffee +++ b/server/handlers/classroom_handler.coffee @@ -94,26 +94,6 @@ ClassroomHandler = class ClassroomHandler extends Handler return doc.toObject() return _.omit(doc.toObject(), 'code', 'codeCamel') - inviteStudents: (req, res, classroomID) -> - if not req.body.emails - return @sendBadInputError(res, 'Emails not included') - - Classroom.findById classroomID, (err, classroom) => - return @sendDatabaseError(res, err) if err - return @sendNotFoundError(res) unless classroom - return @sendForbiddenError(res) unless classroom.get('ownerID').equals(req.user.get('_id')) - - for email in req.body.emails - context = - email_id: sendwithus.templates.course_invite_email - recipient: - address: email - email_data: - class_name: classroom.get('name') - join_link: "https://codecombat.com/courses?_cc=" + (classroom.get('codeCamel') or classroom.get('code')) - sendwithus.api.send context, _.noop - return @sendSuccess(res, {}) - get: (req, res) -> if ownerID = req.query.ownerID return @sendForbiddenError(res) unless req.user and (req.user.isAdmin() or ownerID is req.user.id) diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index 3e8980863..f72e62314 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -12,6 +12,8 @@ Level = require '../models/Level' parse = require '../commons/parse' LevelSession = require '../models/LevelSession' User = require '../models/User' +TrialRequest = require '../models/TrialRequest' +sendwithus = require '../sendwithus' module.exports = getByOwner: wrap (req, res, next) -> @@ -141,3 +143,33 @@ module.exports = database.validateDoc(classroom) classroom = yield classroom.save() res.status(201).send(classroom.toObject({req: req})) + + inviteMembers: wrap (req, res) -> + if not req.body.emails + throw new errors.UnprocessableEntity('Emails not included') + + classroom = yield database.getDocFromHandle(req, Classroom) + if not classroom + throw new errors.NotFound('Classroom not found.') + + unless classroom.get('ownerID').equals(req.user?._id) + throw new errors.Forbidden('Must be owner of classroom to send invites.') + + user = req.user + teacherName = user.get('name') + teacherName ?= _.filter([user.get('firstName'), user.get('lastName')]).join(' ') + trialRequest = yield TrialRequest.findOne({applicant: user._id}) + schoolName = trialRequest?.get('properties')?.organization + + for email in req.body.emails + context = + email_id: sendwithus.templates.course_invite_email + recipient: + address: email + email_data: + class_name: classroom.get('name') + teacher_name: teacherName + school_name: schoolName + join_link: "https://codecombat.com/courses?_cc=" + (classroom.get('codeCamel') or classroom.get('code')) + sendwithus.api.send context, _.noop + res.status(200).send({}) diff --git a/server/routes/index.coffee b/server/routes/index.coffee index 68dcc4734..9f3a30d4d 100644 --- a/server/routes/index.coffee +++ b/server/routes/index.coffee @@ -58,6 +58,7 @@ module.exports.setup = (app) -> app.get('/db/classroom', mw.classrooms.getByOwner) app.get('/db/classroom/:handle/levels', mw.classrooms.fetchAllLevels) app.get('/db/classroom/:handle/courses/:courseID/levels', mw.classrooms.fetchLevelsForCourse) + app.post('/db/classroom/:handle/invite-members', mw.classrooms.inviteMembers) app.get('/db/classroom/:handle/member-sessions', mw.classrooms.fetchMemberSessions) app.get('/db/classroom/:handle/members', mw.classrooms.fetchMembers) # TODO: Use mw.auth? app.get('/db/classroom/:handle', mw.auth.checkLoggedIn()) # TODO: Finish migrating route, adding now so 401 is returned diff --git a/server/sendwithus.coffee b/server/sendwithus.coffee index 1cf6fea98..565e4323f 100644 --- a/server/sendwithus.coffee +++ b/server/sendwithus.coffee @@ -9,7 +9,7 @@ module.exports.setupRoutes = (app) -> debug = not config.isProduction module.exports.api = send: (context, cb) -> - log.debug('Tried to send email with context: ', JSON.stringify(context, null, '\t')) + log.debug('Tried to send email with context: ', JSON.stringify(context, null, ' ')) setTimeout(cb, 10) if swuAPIKey diff --git a/spec/server/functional/classrooms.spec.coffee b/spec/server/functional/classrooms.spec.coffee index b047fbbb3..6ccb9cf03 100644 --- a/spec/server/functional/classrooms.spec.coffee +++ b/spec/server/functional/classrooms.spec.coffee @@ -355,18 +355,22 @@ describe 'DELETE /db/classroom/:id/members', -> describe 'POST /db/classroom/:id/invite-members', -> - it 'takes a list of emails and sends invites', (done) -> - loginNewUser (user1) -> - user1.set('role', 'teacher') - user1.save (err) -> - data = { name: 'Classroom 6' } - request.post {uri: classroomsURL, json: data }, (err, res, body) -> - 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) -> - expect(res.statusCode).toBe(200) - done() + it 'takes a list of emails and sends invites', utils.wrap (done) -> + user = yield utils.initUser({role: 'teacher', name: 'Mr Professerson'}) + yield utils.loginUser(user) + yield utils.makeTrialRequest({ properties: { organization: 'Greendale' } }) + classroom = yield utils.makeClassroom() + url = classroomsURL + "/#{classroom.id}/invite-members" + data = { emails: ['test@test.com'] } + sendwithus = require '../../../server/sendwithus' + spyOn(sendwithus.api, 'send').and.callFake (context, cb) -> + expect(context.email_id).toBe(sendwithus.templates.course_invite_email) + expect(context.recipient.address).toBe('test@test.com') + expect(context.email_data.teacher_name).toBe('Mr Professerson') + expect(context.email_data.school_name).toBe('Greendale') + done() + [res, body] = yield request.postAsync { uri: url, json: data } + expect(res.statusCode).toBe(200) describe 'GET /db/classroom/:handle/member-sessions', -> diff --git a/spec/server/utils.coffee b/spec/server/utils.coffee index e17e0f6cc..2ef142431 100644 --- a/spec/server/utils.coffee +++ b/spec/server/utils.coffee @@ -6,6 +6,8 @@ User = require '../../server/models/User' Level = require '../../server/models/Level' Achievement = require '../../server/models/Achievement' Campaign = require '../../server/models/Campaign' +Classroom = require '../../server/models/Classroom' +TrialRequest = require '../../server/models/TrialRequest' campaignSchema = require '../../app/schemas/models/campaign.schema' campaignLevelProperties = _.keys(campaignSchema.properties.levels.additionalProperties.properties) campaignAdjacentCampaignProperties = _.keys(campaignSchema.properties.adjacentCampaigns.additionalProperties.properties) @@ -124,4 +126,30 @@ module.exports = mw = request.post { uri: getURL('/db/campaign'), json: data }, (err, res) -> return done(err) if err - Campaign.findById(res.body._id).exec done \ No newline at end of file + Campaign.findById(res.body._id).exec done + + makeClassroom: Promise.promisify (data, sources, done) -> + args = Array.from(arguments) + [done, [data, sources]] = [args.pop(), args] + + data = _.extend({}, { + name: _.uniqueId('Classroom ') + }, data) + + request.post { uri: getURL('/db/classroom'), json: data }, (err, res) -> + return done(err) if err + Classroom.findById(res.body._id).exec done + + makeTrialRequest: Promise.promisify (data, sources, done) -> + args = Array.from(arguments) + [done, [data, sources]] = [args.pop(), args] + + data = _.extend({}, { + type: 'course' + properties: {} + }, data) + + request.post { uri: getURL('/db/trial.request'), json: data }, (err, res) -> + return done(err) if err + expect(res.statusCode).toBe(201) + TrialRequest.findById(res.body._id).exec done From 7ff302ef48f352496b80d4ad6fa5248d685a1b93 Mon Sep 17 00:00:00 2001 From: Olha Horobets Date: Sun, 15 May 2016 20:35:23 +0300 Subject: [PATCH 002/167] Add translations --- app/locale/uk.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/locale/uk.coffee b/app/locale/uk.coffee index c3cac375d..d3a0341be 100644 --- a/app/locale/uk.coffee +++ b/app/locale/uk.coffee @@ -38,10 +38,10 @@ module.exports = nativeDescription: "Українська", englishDescription: # accessible_to: "Accessible to" # everyone: "everyone" # democratizing: "Democratizing the process of learning coding is at the core of our philosophy. Everyone should be able to learn to code." -# forgot_learning: "I think they actually forgot that they were actually learning something." -# wanted_to_do: " Coding is something I've always wanted to do, and I never thought I would be able to learn it in school." -# why_games: "Why is learning through games important?" -# games_reward: "Games reward the productive struggle." + forgot_learning: "Таке враження, що, граючи, вони забули, що це навчання." # I think they actually forgot that they were actually learning something. + wanted_to_do: " Програмування — те, чим я завжди хотів займатись, ніколи не думав, що буду вчити це в школі." # Coding is something I've always wanted to do, and I never thought I would be able to learn it in school. + why_games: "Чому навчання через гру важливе?" # Why is learning through games important? + games_reward: "Гра — винагорода за зусилля." # Games reward the productive struggle. # encourage: "Gaming is a medium that encourages interaction, discovery, and trial-and-error. A good game challenges the player to master skills over time, which is the same critical process students go through as they learn." # excel: "Games excel at rewarding" # struggle: "productive struggle" From feeca7a586a4a65319f2d2be36f16b60c04a83a8 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Thu, 26 May 2016 13:34:36 -0700 Subject: [PATCH 003/167] Sending HTML emails now so that Front will eat them --- server/routes/contact.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/routes/contact.coffee b/server/routes/contact.coffee index 9265fff40..8d4454e7a 100644 --- a/server/routes/contact.coffee +++ b/server/routes/contact.coffee @@ -55,6 +55,7 @@ createMailContext = (req, done) -> email_data: subject: "[CodeCombat] #{subject ? ('Feedback - ' + fromAddress)}" content: content + contentHTML: content.replace /\n/g, '\n
' if recipientID is 'schools@codecombat.com' or teacher req.user.update({$set: { enrollmentRequestSent: true }}).exec(_.noop) if recipientID is 'schools@codecombat.com' closeIO.getSalesContactEmail fromAddress, (err, salesContactEmail) -> @@ -78,7 +79,7 @@ createMailContext = (req, done) -> ], (err, results) -> console.error "Error getting contact message context for #{sender}: #{err}" if err if req.body.screenshotURL - context.email_data.content += "\n" + context.email_data.contentHTML += "\n
" done context fetchRecentSessions = (user, context, sentFromLevel, callback) -> @@ -98,5 +99,5 @@ fetchRecentSessions = (user, context, sentFromLevel, callback) -> if sentFromLevel?.levelID is s.levelID and sentFromLevel?.courseID url += "&course=#{sentFromLevel.courseID}&course-instance=#{sentFromLevel.courseInstanceID}" urlName += ' (course)' - context.email_data.content += "\n#{urlName}#{sessionStatus}" + context.email_data.contentHTML += "\n
#{urlName}#{sessionStatus}" callback null From b255b0285423560efe88ed20ce94b188833e68a0 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 26 May 2016 14:46:48 -0700 Subject: [PATCH 004/167] Add POST /db/level/:handle test It's erroring in my dev console, complaining of duplicate keys, and not sure why. Adding a test to make sure creating a new level version doesn't break. --- spec/server/functional/level.spec.coffee | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/server/functional/level.spec.coffee b/spec/server/functional/level.spec.coffee index 5a0e9cdb7..2126f0b44 100644 --- a/spec/server/functional/level.spec.coffee +++ b/spec/server/functional/level.spec.coffee @@ -39,6 +39,21 @@ describe 'Level', -> body = JSON.parse(body) expect(body.type).toBeDefined() done() + + +describe 'POST /db/level/:handle', -> + it 'creates a new version', utils.wrap (done) -> + yield utils.clearModels([Campaign, Course, CourseInstance, Level, User]) + admin = yield utils.initAdmin() + yield utils.loginUser(admin) + @level = yield utils.makeLevel() + levelJSON = @level.toObject() + levelJSON.name = 'New name' + + url = getURL("/db/level/#{@level.id}") + [res, body] = yield request.postAsync({url: url, json: levelJSON}) + expect(res.statusCode).toBe(200) + done() describe 'GET /db/level/:handle/session', -> From 8f7e4e22785002a3efc2503908a213994beb7f58 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Wed, 25 May 2016 15:29:57 -0700 Subject: [PATCH 005/167] Add hints to level schema, I18N editor --- app/schemas/models/level.coffee | 9 ++++++++- app/views/i18n/I18NEditLevelView.coffee | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/schemas/models/level.coffee b/app/schemas/models/level.coffee index 0d7148026..70930526b 100644 --- a/app/schemas/models/level.coffee +++ b/app/schemas/models/level.coffee @@ -276,9 +276,16 @@ c.extendNamedProperties LevelSchema # let's have the name be the first property _.extend LevelSchema.properties, description: {title: 'Description', description: 'A short explanation of what this level is about.', type: 'string', maxLength: 65536, format: 'markdown'} loadingTip: { type: 'string', title: 'Loading Tip', description: 'What to show for this level while it\'s loading.' } - documentation: c.object {title: 'Documentation', description: 'Documentation articles relating to this level.', required: ['specificArticles', 'generalArticles'], 'default': {specificArticles: [], generalArticles: []}}, + documentation: c.object {title: 'Documentation', description: 'Documentation articles relating to this level.', 'default': {specificArticles: [], generalArticles: []}}, specificArticles: c.array {title: 'Specific Articles', description: 'Specific documentation articles that live only in this level.', uniqueItems: true }, SpecificArticleSchema generalArticles: c.array {title: 'General Articles', description: 'General documentation articles that can be linked from multiple levels.', uniqueItems: true}, GeneralArticleSchema + hints: c.array {title: 'Hints', description: 'Hints that will be gradually revealed to the player.', uniqueItems: true }, { + type: 'object' + properties: { + body: {type: 'string', title: 'Content', description: 'The body content of the article, in Markdown.', format: 'markdown'} + i18n: {type: 'object', format: 'i18n', props: ['body'], description: 'Help translate this hint'} + } + } background: c.objectId({format: 'hidden'}) nextLevel: { type: 'object', diff --git a/app/views/i18n/I18NEditLevelView.coffee b/app/views/i18n/I18NEditLevelView.coffee index bb797ca73..6a333ab09 100644 --- a/app/views/i18n/I18NEditLevelView.coffee +++ b/app/views/i18n/I18NEditLevelView.coffee @@ -29,6 +29,12 @@ module.exports = class I18NEditLevelView extends I18NEditModelView @wrapRow 'Guide article name', ['name'], doc.name, i18n[lang]?.name, ['documentation', 'specificArticles', index] @wrapRow "'#{doc.name}' body", ['body'], doc.body, i18n[lang]?.body, ['documentation', 'specificArticles', index], 'markdown' + # hints + for hint, index in @model.get('documentation')?.hints ? [] + if i18n = hint.i18n + name = "Hint #{index+1}" + @wrapRow "'#{name}' body", ['body'], hint.body, i18n[lang]?.body, ['documentation', 'hints', index], 'markdown' + # sprite dialogues for script, scriptIndex in @model.get('scripts') ? [] for noteGroup, noteGroupIndex in script.noteChain ? [] From dfcbbb7c9cef4b072ea6eeca2f76123440751d92 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Thu, 26 May 2016 12:35:46 -0700 Subject: [PATCH 006/167] Log classroom forbidden errors for debugging --- server/handlers/classroom_handler.coffee | 13 ++++++++++--- server/middleware/classrooms.coffee | 18 ++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/server/handlers/classroom_handler.coffee b/server/handlers/classroom_handler.coffee index 40dc82a59..a40ca08f1 100644 --- a/server/handlers/classroom_handler.coffee +++ b/server/handlers/classroom_handler.coffee @@ -5,6 +5,7 @@ Classroom = require './../models/Classroom' User = require '../models/User' sendwithus = require '../sendwithus' utils = require '../lib/utils' +log = require 'winston' UserHandler = require './user_handler' ClassroomHandler = class ClassroomHandler extends Handler @@ -74,7 +75,9 @@ ClassroomHandler = class ClassroomHandler extends Handler Classroom.findById classroomID, (err, classroom) => return @sendDatabaseError(res, err) if err return @sendNotFoundError(res) unless classroom - return @sendForbiddenError(res) unless classroom.get('ownerID').equals(req.user.get('_id')) + unless classroom.get('ownerID').equals(req.user.get('_id')) + log.debug "classroom_handler.inviteStudents: Can't invite to classroom (#{classroom.id}) you (#{req.user.get('_id')}) don't own" + return @sendForbiddenError(res) for email in req.body.emails joinCode = (classroom.get('codeCamel') or classroom.get('code')) @@ -91,13 +94,17 @@ ClassroomHandler = class ClassroomHandler extends Handler get: (req, res) -> if ownerID = req.query.ownerID - return @sendForbiddenError(res) unless req.user and (req.user.isAdmin() or ownerID is req.user.id) + unless req.user and (req.user.isAdmin() or ownerID is req.user.id) + log.debug "classroom_handler.get: ownerID (#{ownerID}) must be yourself (#{req.user.id})" + return @sendForbiddenError(res) return @sendBadInputError(res, 'Bad ownerID') unless utils.isID ownerID Classroom.find {ownerID: mongoose.Types.ObjectId(ownerID)}, (err, classrooms) => return @sendDatabaseError(res, err) if err return @sendSuccess(res, (@formatEntity(req, classroom) for classroom in classrooms)) else if memberID = req.query.memberID - return @sendForbiddenError(res) unless req.user and (req.user.isAdmin() or memberID is req.user.id) + unless req.user and (req.user.isAdmin() or memberID is req.user.id) + log.debug "classroom_handler.get: memberID (#{memberID}) must be yourself (#{req.user.id})" + return @sendForbiddenError(res) return @sendBadInputError(res, 'Bad memberID') unless utils.isID memberID Classroom.find {members: mongoose.Types.ObjectId(memberID)}, (err, classrooms) => return @sendDatabaseError(res, err) if err diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index ece79cf6c..c03523a3e 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -3,6 +3,7 @@ utils = require '../lib/utils' errors = require '../commons/errors' schemas = require '../../app/schemas/schemas' wrap = require 'co-express' +log = require 'winston' Promise = require 'bluebird' database = require '../commons/database' mongoose = require 'mongoose' @@ -21,6 +22,7 @@ module.exports = return next() unless code classroom = yield Classroom.findOne({ code: code.toLowerCase() }).select('name ownerID aceConfig') if not classroom + log.debug("classrooms.fetchByCode: Couldn't find Classroom with code: #{code}") throw new errors.NotFound('Classroom not found.') classroom = classroom.toObject() # Tack on the teacher's name for display to the user @@ -33,7 +35,9 @@ module.exports = return next() unless ownerID throw new errors.UnprocessableEntity('Bad ownerID') unless utils.isID ownerID throw new errors.Unauthorized() unless req.user - throw new errors.Forbidden('"ownerID" must be yourself') unless req.user.isAdmin() or ownerID is req.user.id + unless req.user.isAdmin() or ownerID is req.user.id + log.debug("classrooms.getByOwner: Can't fetch classroom you don't own. User: #{req.user.id} Owner: #{ownerID}") + throw new errors.Forbidden('"ownerID" must be yourself') sanitizedOptions = {} unless _.isUndefined(options.archived) # Handles when .archived is true, vs false-or-null @@ -114,6 +118,7 @@ module.exports = isOwner = classroom.get('ownerID').equals(req.user._id) isMember = req.user.id in (m.toString() for m in classroom.get('members')) unless req.user.isAdmin() or isOwner or isMember + log.debug "classrooms.fetchMembers: Can't fetch members for class (#{classroom.id}) you (#{req.user.id}) don't own and aren't a member of." throw new errors.Forbidden('You do not own this classroom.') memberIDs = classroom.get('members') or [] memberIDs = memberIDs.slice(memberSkip, memberSkip + memberLimit) @@ -126,7 +131,9 @@ module.exports = post: wrap (req, res) -> throw new errors.Unauthorized() unless req.user and not req.user.isAnonymous() - throw new errors.Forbidden() unless req.user?.isTeacher() + unless req.user?.isTeacher() + console.log "classrooms.post: Can't create classroom if you (#{req.user?.id}) aren't a teacher." + throw new errors.Forbidden() classroom = database.initDoc(req, Classroom) classroom.set 'ownerID', req.user._id classroom.set 'members', [] @@ -159,11 +166,13 @@ module.exports = unless req.body?.code throw new errors.UnprocessableEntity('Need a code') if req.user.isTeacher() + log.debug("classrooms.join: Cannot join a classroom as a teacher: #{req.user.id}") throw new errors.Forbidden('Cannot join a classroom as a teacher') code = req.body.code.toLowerCase() classroom = yield Classroom.findOne({code: code}) if not classroom - throw new errors.NotFound('Classroom not found.') + log.debug("classrooms.join: Classroom not found with code #{code}") + throw new errors.NotFound("Classroom not found with code #{code}") members = _.clone(classroom.get('members')) if _.any(members, (memberID) -> memberID.equals(req.user._id)) return res.send(classroom.toObject({req: req})) @@ -199,7 +208,8 @@ module.exports = return next() unless memberID in ownedStudentIDs student = yield User.findById(memberID) if student.get('emailVerified') - return next new errors.Forbidden("Can't reset password for a student that has verified their email address.") + log.debug "classrooms.setStudentPassword: Can't reset password for a student (#{memberID}) that has verified their email address." + throw new errors.Forbidden("Can't reset password for a student that has verified their email address.") { valid, error } = tv4.validateResult(newPassword, schemas.passwordString) unless valid throw new errors.UnprocessableEntity(error.message) From 7d1d1500d9f29b6aa1852fce2a2a8f313c4b02c8 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 26 May 2016 15:01:16 -0700 Subject: [PATCH 007/167] Fix course progress tab select --- app/views/courses/TeacherClassView.coffee | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee index 723d50562..659016298 100644 --- a/app/views/courses/TeacherClassView.coffee +++ b/app/views/courses/TeacherClassView.coffee @@ -41,6 +41,7 @@ module.exports = class TeacherClassView extends RootView 'click .select-all': 'onClickSelectAll' 'click .student-checkbox': 'onClickStudentCheckbox' 'keyup #student-search': 'onKeyPressStudentSearch' + 'change .course-select, .bulk-course-select': 'onChangeCourseSelect' getInitialState: -> { @@ -149,6 +150,8 @@ module.exports = class TeacherClassView extends RootView @listenTo @students, 'sort', -> @state.set students: @students @render() + @listenTo @, 'course-select:change', ({ selectedCourse }) -> + @state.set selectedCourse: selectedCourse setCourseMembers: => for course in @courses.models @@ -273,6 +276,10 @@ module.exports = class TeacherClassView extends RootView onKeyPressStudentSearch: (e) -> @state.set('searchTerm', $(e.target).val()) + onChangeCourseSelect: (e) -> + console.log '??' + @trigger 'course-select:change', { selectedCourse: @courses.get($(e.currentTarget).val()) } + getSelectedStudentIDs: -> @$('.student-row .checkbox-flat input:checked').map (index, checkbox) -> $(checkbox).data('student-id') From 6b3e94d60a4d201b526610008f7e22b30c3364f4 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 26 May 2016 15:12:10 -0700 Subject: [PATCH 008/167] Remove log --- app/views/courses/TeacherClassView.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee index 659016298..eecfb3550 100644 --- a/app/views/courses/TeacherClassView.coffee +++ b/app/views/courses/TeacherClassView.coffee @@ -277,7 +277,6 @@ module.exports = class TeacherClassView extends RootView @state.set('searchTerm', $(e.target).val()) onChangeCourseSelect: (e) -> - console.log '??' @trigger 'course-select:change', { selectedCourse: @courses.get($(e.currentTarget).val()) } getSelectedStudentIDs: -> From 77ba873da9aea7d872c3587c53aa436ea4fd2535 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Thu, 26 May 2016 15:49:33 -0700 Subject: [PATCH 009/167] Add tests for teacher password reset endpoint --- server/middleware/classrooms.coffee | 3 +- spec/server/functional/classrooms.spec.coffee | 70 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index c03523a3e..f44198ea7 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -205,7 +205,8 @@ module.exports = ownedStudentIDs = _.flatten ownedClassrooms.map (c) -> c.get('members').map (id) -> id.toString() - return next() unless memberID in ownedStudentIDs + unless memberID in ownedStudentIDs + throw new errors.Forbidden("Can't reset the password of a student that's not in one of your classrooms.") student = yield User.findById(memberID) if student.get('emailVerified') log.debug "classrooms.setStudentPassword: Can't reset password for a student (#{memberID}) that has verified their email address." diff --git a/spec/server/functional/classrooms.spec.coffee b/spec/server/functional/classrooms.spec.coffee index 4d7e814a3..94ad0f9df 100644 --- a/spec/server/functional/classrooms.spec.coffee +++ b/spec/server/functional/classrooms.spec.coffee @@ -447,3 +447,73 @@ describe 'GET /db/classroom/:handle/members', -> expect(user.email).toBeDefined() expect(user.passwordHash).toBeUndefined() done() + +describe 'POST /db/classroom/:classroomID/members/:memberID/reset-password', -> + it 'changes the password', utils.wrap (done) -> + yield utils.clearModels([User, Classroom]) + teacher = yield utils.initUser() + yield utils.loginUser(teacher) + student = yield utils.initUser({ name: "Firstname Lastname" }) + newPassword = "this is a new password" + classroom = yield new Classroom({name: 'Classroom', ownerID: teacher._id, members: [student._id] }).save() + expect(student.get('passwordHash')).not.toEqual(User.hashPassword(newPassword)) + [res, body] = yield request.postAsync({ + uri: getURL("/db/classroom/#{classroom.id}/members/#{student.id}/reset-password") + json: { password: newPassword } + }) + expect(res.statusCode).toBe(200) + changedStudent = yield User.findById(student.id) + expect(changedStudent.get('passwordHash')).toEqual(User.hashPassword(newPassword)) + done() + + it "doesn't change the password if you're not their teacher", utils.wrap (done) -> + yield utils.clearModels([User, Classroom]) + teacher = yield utils.initUser() + yield utils.loginUser(teacher) + student = yield utils.initUser({ name: "Firstname Lastname" }) + student2 = yield utils.initUser({ name: "Firstname Lastname 2" }) + newPassword = "this is a new password" + classroom = yield new Classroom({name: 'Classroom', ownerID: teacher._id, members: [student2._id] }).save() + expect(student.get('passwordHash')).not.toEqual(User.hashPassword(newPassword)) + [res, body] = yield request.postAsync({ + uri: getURL("/db/classroom/#{classroom.id}/members/#{student.id}/reset-password") + json: { password: newPassword } + }) + expect(res.statusCode).toBe(403) + changedStudent = yield User.findById(student.id) + expect(changedStudent.get('passwordHash')).toEqual(student.get('passwordHash')) + done() + + it "doesn't change the password if their email is verified", utils.wrap (done) -> + yield utils.clearModels([User, Classroom]) + teacher = yield utils.initUser() + yield utils.loginUser(teacher) + student = yield utils.initUser({ name: "Firstname Lastname", emailVerified: true }) + newPassword = "this is a new password" + classroom = yield new Classroom({name: 'Classroom', ownerID: teacher._id, members: [student._id] }).save() + expect(student.get('passwordHash')).not.toEqual(User.hashPassword(newPassword)) + [res, body] = yield request.postAsync({ + uri: getURL("/db/classroom/#{classroom.id}/members/#{student.id}/reset-password") + json: { password: newPassword } + }) + expect(res.statusCode).toBe(403) + changedStudent = yield User.findById(student.id) + expect(changedStudent.get('passwordHash')).toEqual(student.get('passwordHash')) + done() + + it "doesn't let you set a 1-character password", utils.wrap (done) -> + yield utils.clearModels([User, Classroom]) + teacher = yield utils.initUser() + yield utils.loginUser(teacher) + student = yield utils.initUser({ name: "Firstname Lastname" }) + newPassword = "e" + classroom = yield new Classroom({name: 'Classroom', ownerID: teacher._id, members: [student._id] }).save() + expect(student.get('passwordHash')).not.toEqual(User.hashPassword(newPassword)) + [res, body] = yield request.postAsync({ + uri: getURL("/db/classroom/#{classroom.id}/members/#{student.id}/reset-password") + json: { password: newPassword } + }) + expect(res.statusCode).toBe(422) + changedStudent = yield User.findById(student.id) + expect(changedStudent.get('passwordHash')).toEqual(student.get('passwordHash')) + done() From c9ece55d499cc8a8fc070e36f4928471ac873e93 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Thu, 26 May 2016 16:15:09 -0700 Subject: [PATCH 010/167] Fix console.log to log.debug --- server/middleware/classrooms.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index f44198ea7..87c8e992c 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -132,7 +132,7 @@ module.exports = post: wrap (req, res) -> throw new errors.Unauthorized() unless req.user and not req.user.isAnonymous() unless req.user?.isTeacher() - console.log "classrooms.post: Can't create classroom if you (#{req.user?.id}) aren't a teacher." + log.debug "classrooms.post: Can't create classroom if you (#{req.user?.id}) aren't a teacher." throw new errors.Forbidden() classroom = database.initDoc(req, Classroom) classroom.set 'ownerID', req.user._id From 9bc35db7a6c79637a3196e6404534d76c6e89d33 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Wed, 25 May 2016 12:53:52 -0700 Subject: [PATCH 011/167] Include teacher name in course invite email data In preparation for changing the sendwithus template. --- server/handlers/classroom_handler.coffee | 2 ++ server/handlers/course_instance_handler.coffee | 1 + 2 files changed, 3 insertions(+) diff --git a/server/handlers/classroom_handler.coffee b/server/handlers/classroom_handler.coffee index a40ca08f1..0fe8b8387 100644 --- a/server/handlers/classroom_handler.coffee +++ b/server/handlers/classroom_handler.coffee @@ -69,6 +69,7 @@ ClassroomHandler = class ClassroomHandler extends Handler return _.omit(doc.toObject(), 'code', 'codeCamel') inviteStudents: (req, res, classroomID) -> + return @sendUnauthorizedError(res) if not req.user? if not req.body.emails return @sendBadInputError(res, 'Emails not included') @@ -86,6 +87,7 @@ ClassroomHandler = class ClassroomHandler extends Handler recipient: address: email email_data: + teacher_name: req.user.broadName() class_name: classroom.get('name') join_link: "https://codecombat.com/courses?_cc=" + joinCode join_code: joinCode diff --git a/server/handlers/course_instance_handler.coffee b/server/handlers/course_instance_handler.coffee index 0aa959124..bf26df540 100644 --- a/server/handlers/course_instance_handler.coffee +++ b/server/handlers/course_instance_handler.coffee @@ -192,6 +192,7 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler address: email subject: course.get('name') email_data: + teacher_name: req.user.broadName() class_name: course.get('name') join_link: "https://codecombat.com/courses/students?_ppc=" + prepaid.get('code') sendwithus.api.send context, _.noop From 4bb3ac1f0aa953235b477b67d2b6f78bf1d6e16a Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 26 May 2016 16:46:03 -0700 Subject: [PATCH 012/167] Replace Anoner with Anonymous Closes #3686 --- app/models/User.coffee | 2 +- app/schemas/models/user.coffee | 2 +- app/templates/clans/clan-details.jade | 4 ++-- app/templates/clans/clans.jade | 4 ++-- app/templates/editor/level/level-feedback-view.jade | 2 +- app/templates/play/level/duel-stats-view.jade | 2 +- app/templates/user/main-user-view.jade | 2 +- app/views/admin/MainAdminView.coffee | 2 +- app/views/clans/ClanDetailsView.coffee | 8 ++++---- app/views/ladder/LadderPlayModal.coffee | 2 +- app/views/play/SpectateView.coffee | 2 +- app/views/play/level/PlayLevelView.coffee | 2 +- scripts/mail.coffee | 2 +- server/models/User.coffee | 2 +- server/routes/mail.coffee | 8 ++++---- 15 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/models/User.coffee b/app/models/User.coffee index 3f8720f16..495d006fa 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -25,7 +25,7 @@ module.exports = class User extends CocoModel return name if name [emailName, emailDomain] = @get('email')?.split('@') or [] return emailName if emailName - return 'Anoner' + return 'Anonymous' getPhotoURL: (size=80, useJobProfilePhoto=false, useEmployerPageAvatar=false) -> photoURL = if useJobProfilePhoto then @get('jobProfile')?.photoURL else null diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index 0f7936205..3b1799786 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -6,7 +6,7 @@ UserSchema = c.object default: visa: 'Authorized to work in the US' music: true - name: 'Anoner' + name: 'Anonymous' autocastDelay: 5000 emails: {} permissions: [] diff --git a/app/templates/clans/clan-details.jade b/app/templates/clans/clan-details.jade index 387fdba53..b1d16daec 100644 --- a/app/templates/clans/clan-details.jade +++ b/app/templates/clans/clan-details.jade @@ -123,7 +123,7 @@ block content if memberLanguageMap && memberLanguageMap[member.id] span.code-language-cell(style="background-image: url(/images/common/code_languages/#{memberLanguageMap[member.id]}_small.png)", title=memberLanguageMap[member.id]) div - a(href="/user/#{member.id}")= member.get('name') || 'Anoner' + a(href="/user/#{member.id}")= member.get('name') || 'Anonymous' div Level #{member.level()} if isOwner && member.id !== clan.get('ownerID') button.btn.btn-xs.btn-warning.remove-member-btn(data-id="#{member.id}", data-i18n="clans.rem_hero") Remove Hero @@ -220,7 +220,7 @@ block content if memberLanguageMap && memberLanguageMap[member.id] span.code-language-cell(style="background-image: url(/images/common/code_languages/#{memberLanguageMap[member.id]}_small.png)", title=memberLanguageMap[member.id]) td.name-cell - a(href="/user/#{member.id}")= member.get('name') || 'Anoner' + a(href="/user/#{member.id}")= member.get('name') || 'Anonymous' td.level-cell= member.level() td.achievements-cell if memberAchievementsMap && memberAchievementsMap[member.id] diff --git a/app/templates/clans/clans.jade b/app/templates/clans/clans.jade index 6cccf104d..797fe586a 100644 --- a/app/templates/clans/clans.jade +++ b/app/templates/clans/clans.jade @@ -45,7 +45,7 @@ block content if view.idNameMap && view.idNameMap[clan.get('ownerID')] a(href="/user/#{clan.get('ownerID')}")= view.idNameMap[clan.get('ownerID')] else - a(href="/user/#{clan.get('ownerID')}") Anoner + a(href="/user/#{clan.get('ownerID')}") Anonymous td if view.myClanIDs.indexOf(clan.id) < 0 button.btn.btn-success.join-clan-btn(data-id="#{clan.id}", data-i18n="clans.join_clan") Join Clan @@ -75,7 +75,7 @@ block content if view.idNameMap && view.idNameMap[clan.get('ownerID')] a(href="/user/#{clan.get('ownerID')}")= view.idNameMap[clan.get('ownerID')] else - a(href="/user/#{clan.get('ownerID')}") Anoner + a(href="/user/#{clan.get('ownerID')}") Anonymous td= clan.get('type') td if clan.get('ownerID') !== me.id diff --git a/app/templates/editor/level/level-feedback-view.jade b/app/templates/editor/level/level-feedback-view.jade index be266b474..97170f3a6 100644 --- a/app/templates/editor/level/level-feedback-view.jade +++ b/app/templates/editor/level/level-feedback-view.jade @@ -11,7 +11,7 @@ ul.user-feedback-list.list-group em= moment(new Date(feedback.created)).fromNow() span.spl.spr - a(href="/user/#{feedback.creator}") - strong= feedback.creatorName || 'Anoner' + strong= feedback.creatorName || 'Anonymous' if feedback.review span.spr : span= feedback.review diff --git a/app/templates/play/level/duel-stats-view.jade b/app/templates/play/level/duel-stats-view.jade index c37aa0eb9..9dbe6d07d 100644 --- a/app/templates/play/level/duel-stats-view.jade +++ b/app/templates/play/level/duel-stats-view.jade @@ -12,7 +12,7 @@ for player in view.players .player-gold .gold-icon .gold-value - .player-name= player.name || 'Anoner' + .player-name= player.name || 'Anonymous' .player-health .health-icon .health-bar-container diff --git a/app/templates/user/main-user-view.jade b/app/templates/user/main-user-view.jade index e08acb0eb..494a23c73 100644 --- a/app/templates/user/main-user-view.jade +++ b/app/templates/user/main-user-view.jade @@ -81,7 +81,7 @@ block append content if idNameMap && idNameMap[clan.get('ownerID')] a(href="/user/#{clan.get('ownerID')}")= idNameMap[clan.get('ownerID')] else - a(href="/user/#{clan.get('ownerID')}") Anoner + a(href="/user/#{clan.get('ownerID')}") Anonymous td= clan.get('members').length else .panel-body diff --git a/app/views/admin/MainAdminView.coffee b/app/views/admin/MainAdminView.coffee index 6a79f155c..cd720d5b0 100644 --- a/app/views/admin/MainAdminView.coffee +++ b/app/views/admin/MainAdminView.coffee @@ -65,7 +65,7 @@ module.exports = class MainAdminView extends RootView forms.enableSubmit(@$('#user-search-button')) result = '' if users.length - result = ("#{user._id}#{_.escape(user.name or 'Anoner')}#{_.escape(user.email)}" for user in users) + result = ("#{user._id}#{_.escape(user.name or 'Anonymous')}#{_.escape(user.email)}" for user in users) result = "#{result.join('\n')}
" @$el.find('#user-search-result').html(result) diff --git a/app/views/clans/ClanDetailsView.coffee b/app/views/clans/ClanDetailsView.coffee index 2348843d8..8e8d7bc12 100644 --- a/app/views/clans/ClanDetailsView.coffee +++ b/app/views/clans/ClanDetailsView.coffee @@ -138,7 +138,7 @@ module.exports = class ClanDetailsView extends RootView return unless @members? and @memberSort? switch @memberSort when "nameDesc" - @members.comparator = (a, b) -> return (b.get('name') or 'Anoner').localeCompare(a.get('name') or 'Anoner') + @members.comparator = (a, b) -> return (b.get('name') or 'Anonymous').localeCompare(a.get('name') or 'Anonymous') when "progressAsc" @members.comparator = (a, b) -> aComplete = (concept for concept, state of userConceptsMap[a.id] when state is 'complete') @@ -151,7 +151,7 @@ module.exports = class ClanDetailsView extends RootView else if aStarted > bStarted then return 1 if highestUserLevelCountMap[a.id] < highestUserLevelCountMap[b.id] then return -1 else if highestUserLevelCountMap[a.id] > highestUserLevelCountMap[b.id] then return 1 - (a.get('name') or 'Anoner').localeCompare(b.get('name') or 'Anoner') + (a.get('name') or 'Anonymous').localeCompare(b.get('name') or 'Anonymous') when "progressDesc" @members.comparator = (a, b) -> aComplete = (concept for concept, state of userConceptsMap[a.id] when state is 'complete') @@ -164,9 +164,9 @@ module.exports = class ClanDetailsView extends RootView else if aStarted < bStarted then return 1 if highestUserLevelCountMap[a.id] > highestUserLevelCountMap[b.id] then return -1 else if highestUserLevelCountMap[a.id] < highestUserLevelCountMap[b.id] then return 1 - (b.get('name') or 'Anoner').localeCompare(a.get('name') or 'Anoner') + (b.get('name') or 'Anonymous').localeCompare(a.get('name') or 'Anonymous') else - @members.comparator = (a, b) -> return (a.get('name') or 'Anoner').localeCompare(b.get('name') or 'Anoner') + @members.comparator = (a, b) -> return (a.get('name') or 'Anonymous').localeCompare(b.get('name') or 'Anonymous') @members.sort() updateHeroIcons: -> diff --git a/app/views/ladder/LadderPlayModal.coffee b/app/views/ladder/LadderPlayModal.coffee index aba8cff87..ee3e79c20 100644 --- a/app/views/ladder/LadderPlayModal.coffee +++ b/app/views/ladder/LadderPlayModal.coffee @@ -61,7 +61,7 @@ module.exports = class LadderPlayModal extends ModalView success = (@nameMap) => for challenger in _.values(@challengers) - challenger.opponentName = @nameMap[challenger.opponentID]?.name or 'Anoner' + challenger.opponentName = @nameMap[challenger.opponentID]?.name or 'Anonymous' challenger.opponentWizard = @nameMap[challenger.opponentID]?.wizard or {} @checkWizardLoaded() diff --git a/app/views/play/SpectateView.coffee b/app/views/play/SpectateView.coffee index 00d391098..769f75324 100644 --- a/app/views/play/SpectateView.coffee +++ b/app/views/play/SpectateView.coffee @@ -207,7 +207,7 @@ module.exports = class SpectateLevelView extends RootView findPlayerNames: -> playerNames = {} for session in [@session, @otherSession] when session?.get('team') - playerNames[session.get('team')] = session.get('creatorName') or 'Anoner' + playerNames[session.get('team')] = session.get('creatorName') or 'Anonymous' playerNames initGoalManager: -> diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index a75ac807c..9be13aa1f 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -353,7 +353,7 @@ module.exports = class PlayLevelView extends RootView return {} unless @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder'] playerNames = {} for session in [@session, @otherSession] when session?.get('team') - playerNames[session.get('team')] = session.get('creatorName') or 'Anoner' + playerNames[session.get('team')] = session.get('creatorName') or 'Anonymous' playerNames # Once Surface is Loaded #################################################### diff --git a/scripts/mail.coffee b/scripts/mail.coffee index 952d4b2a1..c73d8ee41 100644 --- a/scripts/mail.coffee +++ b/scripts/mail.coffee @@ -87,7 +87,7 @@ emailUserInitialRecruiting = (user, callback) -> return callback null, false if DEBUGGING and (totalEmailsSent > 1 or Math.random() > 0.05) ++totalEmailsSent name = if user.firstName and user.lastName then "#{user.firstName}" else user.name - name = 'Wizard' if not name or name is 'Anoner' + name = 'Wizard' if not name or name in ['Anoner', 'Anonymous'] team = user.session.levelInfo.team team = team.substr(0, team.length - 1) context = diff --git a/server/models/User.coffee b/server/models/User.coffee index 26c429517..d35a65b12 100644 --- a/server/models/User.coffee +++ b/server/models/User.coffee @@ -49,7 +49,7 @@ UserSchema.methods.broadName = -> return name if name [emailName, emailDomain] = @get('email').split('@') return emailName if emailName - return 'Anoner' + return 'Anonymous' UserSchema.methods.isInGodMode = -> p = @get('permissions') diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index 20abb99b9..b888f261e 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -578,7 +578,7 @@ sendLadderUpdateEmail = (session, now, daysAgo) -> #log.info "Not sending email to #{user.get('email')} #{user.get('name')} because the session had levelName #{session.levelName} or team #{session.team} in it." return name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name') - name = 'Wizard' if not name or name is 'Anoner' + name = 'Wizard' if not name or name is 'Anonymous' # Fetch the most recent defeat and victory, if there are any. # (We could look at strongest/weakest, but we'd have to fetch everyone, or denormalize more.) @@ -622,13 +622,13 @@ sendLadderUpdateEmail = (session, now, daysAgo) -> if err log.error "Couldn't find defeateded opponent: #{err}" defeatedOpponent = null - victoryContext = {opponent_name: defeatedOpponent?.name ? 'Anoner', url: urlForMatch(victory)} if victory + victoryContext = {opponent_name: defeatedOpponent?.name ? 'Anonymous', url: urlForMatch(victory)} if victory onFetchedVictoriousOpponent = (err, victoriousOpponent) -> if err log.error "Couldn't find victorious opponent: #{err}" victoriousOpponent = null - defeatContext = {opponent_name: victoriousOpponent?.name ? 'Anoner', url: urlForMatch(defeat)} if defeat + defeatContext = {opponent_name: victoriousOpponent?.name ? 'Anonymous', url: urlForMatch(defeat)} if defeat Level.find({original: session.level.original, created: {$gt: session.submitDate}}).select('created commitMessage version').sort('-created').lean().exec (err, levelVersions) -> sendEmail defeatContext, victoryContext, (if levelVersions.length then levelVersions else null) @@ -706,7 +706,7 @@ sendNextStepsEmail = (user, now, daysAgo) -> do (err, nextLevel) -> return log.error "Couldn't find next level for #{user.get('email')}: #{err}" if err name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name') - name = 'hero' if not name or name is 'Anoner' + name = 'Hero' if not name or name in ['Anoner', 'Anonymous'] #secretLevel = switch user.get('testGroupNumber') % 8 # when 0, 1, 2, 3 then name: 'Forgetful Gemsmith', slug: 'forgetful-gemsmith' # when 4, 5, 6, 7 then name: 'Signs and Portents', slug: 'signs-and-portents' From 72b8674237f44bdf46550f136e00177d60081d7a Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 26 May 2016 17:02:58 -0700 Subject: [PATCH 013/167] Add error message for closeIO.getSalesContactEmail --- server/lib/closeIO.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/server/lib/closeIO.coffee b/server/lib/closeIO.coffee index 23fbb3306..be254091f 100644 --- a/server/lib/closeIO.coffee +++ b/server/lib/closeIO.coffee @@ -68,4 +68,5 @@ module.exports = return done(null, activity.sender) if /@codecombat\.com/ig.test(activity.sender) return done(null, config.mail.supportSchools) catch error + log.error("closeIO.getSalesContactEmail Error for #{email}: #{JSON.stringify(error)}") return done(error, config.mail.supportSchools) From d0d3d838edec56494edba42c3d6ca413ab78da47 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Fri, 27 May 2016 09:40:46 -0700 Subject: [PATCH 014/167] Better page titles Also ditching non-production constructor page titles --- app/views/AboutView.coffee | 2 ++ app/views/admin/MainAdminView.coffee | 4 +++- app/views/core/RootView.coffee | 6 ++---- app/views/courses/CoursesView.coffee | 2 ++ app/views/courses/EnrollmentsView.coffee | 2 ++ app/views/courses/TeacherClassView.coffee | 4 +++- app/views/courses/TeacherClassesView.coffee | 2 ++ app/views/courses/TeacherCoursesView.coffee | 2 ++ 8 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app/views/AboutView.coffee b/app/views/AboutView.coffee index f0705979e..7de91ecae 100644 --- a/app/views/AboutView.coffee +++ b/app/views/AboutView.coffee @@ -23,6 +23,8 @@ module.exports = class AboutView extends RootView 'left': 'onLeftPressed' 'esc': 'onEscapePressed' + getTitle: -> return $.i18n.t('nav.about') + afterRender: -> super(arguments...) @$('#fixed-nav').affix({ diff --git a/app/views/admin/MainAdminView.coffee b/app/views/admin/MainAdminView.coffee index cd720d5b0..550fa4f13 100644 --- a/app/views/admin/MainAdminView.coffee +++ b/app/views/admin/MainAdminView.coffee @@ -19,7 +19,9 @@ module.exports = class MainAdminView extends RootView 'click #user-search-result': 'onClickUserSearchResult' 'click #create-free-sub-btn': 'onClickFreeSubLink' 'click #terminal-create': 'onClickTerminalSubLink' - + + getTitle: -> return $.i18n.t('account_settings.admin') + initialize: -> if window.amActually @amActually = new User({_id: window.amActually}) diff --git a/app/views/core/RootView.coffee b/app/views/core/RootView.coffee index 83cc25bcb..75ed205a3 100644 --- a/app/views/core/RootView.coffee +++ b/app/views/core/RootView.coffee @@ -110,10 +110,8 @@ module.exports = class RootView extends CocoView @buildLanguages() $('body').removeClass('is-playing') - if application.isProduction() - title = 'CodeCombat - ' + (@getTitle() or 'Learn how to code by playing a game') - else - title = @getTitle() or @constructor.name + if title = @getTitle() then title += ' | CodeCombat' + else title = 'CodeCombat - Learn how to code by playing a game' $('title').text(title) diff --git a/app/views/courses/CoursesView.coffee b/app/views/courses/CoursesView.coffee index 749318cf7..d255beb9d 100644 --- a/app/views/courses/CoursesView.coffee +++ b/app/views/courses/CoursesView.coffee @@ -28,6 +28,8 @@ module.exports = class CoursesView extends RootView 'submit #join-class-form': 'onSubmitJoinClassForm' 'click #change-language-link': 'onClickChangeLanguageLink' + getTitle: -> return $.i18n.t('teacher.students') + initialize: -> @courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance}) @courseInstances.comparator = (ci) -> return ci.get('classroomID') + ci.get('courseID') diff --git a/app/views/courses/EnrollmentsView.coffee b/app/views/courses/EnrollmentsView.coffee index b79d51886..db77c870c 100644 --- a/app/views/courses/EnrollmentsView.coffee +++ b/app/views/courses/EnrollmentsView.coffee @@ -19,6 +19,8 @@ module.exports = class EnrollmentsView extends RootView 'click #how-to-enroll-link': 'onClickHowToEnrollLink' 'click #contact-us-btn': 'onClickContactUsButton' + getTitle: -> return $.i18n.t('teacher.enrollments') + initialize: -> @state = new State({ totalEnrolled: 0 diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee index eecfb3550..bfb907bbb 100644 --- a/app/views/courses/TeacherClassView.coffee +++ b/app/views/courses/TeacherClassView.coffee @@ -63,6 +63,8 @@ module.exports = class TeacherClassView extends RootView enrolledUsers: "" } + getTitle: -> return @classroom?.get('name') + initialize: (options, classroomID) -> super(options) @singleStudentCourseProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-course' @@ -116,7 +118,7 @@ module.exports = class TeacherClassView extends RootView @supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts'}}) @attachMediatorEvents() - + attachMediatorEvents: () -> @listenTo @state, 'sync change', -> if _.isEmpty(_.omit(@state.changed, 'searchTerm')) diff --git a/app/views/courses/TeacherClassesView.coffee b/app/views/courses/TeacherClassesView.coffee index 5a5f4e904..63f196e14 100644 --- a/app/views/courses/TeacherClassesView.coffee +++ b/app/views/courses/TeacherClassesView.coffee @@ -25,6 +25,8 @@ module.exports = class TeacherClassesView extends RootView 'click .add-students-btn': 'onClickAddStudentsButton' 'click .create-classroom-btn': 'onClickCreateClassroomButton' + getTitle: -> return $.i18n.t('teacher.my_classes') + initialize: (options) -> super(options) @classrooms = new Classrooms() diff --git a/app/views/courses/TeacherCoursesView.coffee b/app/views/courses/TeacherCoursesView.coffee index 482f58ec6..14ca396fe 100644 --- a/app/views/courses/TeacherCoursesView.coffee +++ b/app/views/courses/TeacherCoursesView.coffee @@ -39,6 +39,8 @@ module.exports = class TeacherCoursesView extends RootView "569ed916efa72b0ced971447": null } + getTitle: -> return $.i18n.t('teacher.courses') + constructor: (options) -> super(options) @ownedClassrooms = new Classrooms() From 9ce4ac51f079455e95c037d26a356aaee7ffc9ac Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 26 May 2016 14:25:34 -0700 Subject: [PATCH 015/167] Role-based hard feature blocks --- app/core/Router.coffee | 18 ++++++++-------- app/locale/en.coffee | 3 ++- .../courses/restricted-to-students-view.jade | 21 +++++++++++++------ 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/app/core/Router.coffee b/app/core/Router.coffee index a6c42752b..984619ee6 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -69,15 +69,15 @@ module.exports = class CocoRouter extends Backbone.Router 'contribute/diplomat': go('contribute/DiplomatView') 'contribute/scribe': go('contribute/ScribeView') - 'courses': go('courses/CoursesView') # , { studentsOnly: true }) # TODO: Enforce after session-less play for teachers - 'Courses': go('courses/CoursesView') # , { studentsOnly: true }) + 'courses': go('courses/CoursesView', { studentsOnly: true }) + 'Courses': go('courses/CoursesView', { studentsOnly: true }) 'courses/students': redirect('/courses') 'courses/teachers': redirect('/teachers/classes') 'courses/purchase': redirect('/teachers/licenses') 'courses/enroll(/:courseID)': redirect('/teachers/licenses') 'courses/update-account': go('courses/CoursesUpdateAccountView') - 'courses/:classroomID': go('courses/ClassroomView') #, { studentsOnly: true }) - 'courses/:courseID/:courseInstanceID': go('courses/CourseDetailsView') + 'courses/:classroomID': go('courses/ClassroomView', { studentsOnly: true }) + 'courses/:courseID/:courseInstanceID': go('courses/CourseDetailsView', { studentsOnly: true }) 'db/*path': 'routeToServer' 'demo(/*subpath)': go('DemoView') @@ -142,14 +142,14 @@ module.exports = class CocoRouter extends Backbone.Router 'SEEN': go('NewHomeView') 'teachers': redirect('/teachers/classes') - 'teachers/classes': go('courses/TeacherClassesView') #, { teachersOnly: true }) - 'teachers/classes/:classroomID': go('courses/TeacherClassView') #, { teachersOnly: true }) + 'teachers/classes': go('courses/TeacherClassesView', { teachersOnly: true }) + 'teachers/classes/:classroomID': go('courses/TeacherClassView', { teachersOnly: true }) 'teachers/courses': go('courses/TeacherCoursesView') 'teachers/demo': go('teachers/RequestQuoteView') 'teachers/enrollments': redirect('/teachers/licenses') - 'teachers/licenses': go('courses/EnrollmentsView') #, { teachersOnly: true }) + 'teachers/licenses': go('courses/EnrollmentsView', { teachersOnly: true }) 'teachers/freetrial': go('teachers/RequestQuoteView') - 'teachers/quote': go('teachers/RequestQuoteView') + 'teachers/quote': redirect('/teachers/demo') 'teachers/signup': -> return @routeDirectly('teachers/CreateTeacherAccountView', []) if me.isAnonymous() @navigate('/teachers/update-account', {trigger: true, replace: true}) @@ -174,7 +174,7 @@ module.exports = class CocoRouter extends Backbone.Router routeDirectly: (path, args=[], options={}) -> if options.teachersOnly and not me.isTeacher() return @routeDirectly('teachers/RestrictedToTeachersView') - if options.studentsOnly and me.isTeacher() + if options.studentsOnly and not me.isStudent() return @routeDirectly('courses/RestrictedToStudentsView') leavingMessage = _.result(window.currentView, 'onLeaveMessage') if leavingMessage diff --git a/app/locale/en.coffee b/app/locale/en.coffee index d546acda9..f7ebe2c6e 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -1279,7 +1279,8 @@ student_age_range_to: "to" create_class: "Create Class" class_name: "Class Name" - teacher_account_restricted: "Your account is a teacher account, and so cannot access student content." + teacher_account_restricted: "Your account is a teacher account and cannot access student content." # {change} + account_restricted: "A student account is required to access this page." update_account_login_title: "Log in to update your account" update_account_title: "Your account needs attention!" update_account_blurb: "Before you can access your classes, choose how you want to use this account." diff --git a/app/templates/courses/restricted-to-students-view.jade b/app/templates/courses/restricted-to-students-view.jade index d31f421c8..ebea62f7c 100644 --- a/app/templates/courses/restricted-to-students-view.jade +++ b/app/templates/courses/restricted-to-students-view.jade @@ -3,10 +3,19 @@ extends /templates/base-flat block content .access-restricted.container.text-center.m-y-3 h5(data-i18n='teacher.access_restricted') - p(data-i18n='courses.teacher_account_restricted') - a.btn.btn-lg.btn-primary(href="/teachers/classes" data-i18n="new_home.goto_classes") - button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out") + if me.isTeacher() + p(data-i18n='courses.teacher_account_restricted') + a.btn.btn-lg.btn-primary(href="/teachers/classes" data-i18n="new_home.goto_classes") + button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out") + else + p(data-i18n='courses.account_restricted') + if me.isAnonymous() + .login-button.btn.btn-lg.btn-primary(data-i18n='login.log_in') + else + a.btn.btn-lg.btn-primary(href="/courses/update-account" data-i18n="courses.update_account_update_student") + button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out") - .teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3 - h5(data-i18n='teacher.what_is_a_teacher_account') - p(data-i18n='teacher.teacher_account_explanation') \ No newline at end of file + if me.isTeacher() + .teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3 + h5(data-i18n='teacher.what_is_a_teacher_account') + p(data-i18n='teacher.teacher_account_explanation') \ No newline at end of file From e3c2947e2d7c8ad65aefcb3a375d062527583566 Mon Sep 17 00:00:00 2001 From: duybkict Date: Thu, 19 May 2016 17:05:18 +0700 Subject: [PATCH 016/167] upgrade brunch related packages to >=2.0.0 --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index fc326df34..cd1c9d497 100644 --- a/package.json +++ b/package.json @@ -93,21 +93,21 @@ "devDependencies": { "after-brunch": "0.0.5", "assetsmanager-brunch": "^1.8.1", - "auto-reload-brunch": "^1.8.1", + "auto-reload-brunch": ">=2.0.0", "bower": "~1.6.4", - "brunch": "^1.8.5", - "coffee-script-brunch": "^1.8.3", - "coffeelint-brunch": "^1.7.1", + "brunch": ">=2.0.0", + "coffee-script-brunch": ">=2.0.0", + "coffeelint-brunch": ">=2.0.0", "commonjs-require-definition": "0.2.0", "compressible": "~1.0.1", "country-data": "0.0.24", "country-list": "0.0.3", - "css-brunch": "^1.7.0", + "css-brunch": ">=2.0.0", "fs-extra": "^0.26.2", "http-proxy": "^1.13.2", "jade-brunch": "1.7.5", "jasmine": "^2.4.1", - "javascript-brunch": "> 1.0 < 1.8", + "javascript-brunch": ">=2.0.0", "karma": "~0.13", "karma-chrome-launcher": "~0.1.2", "karma-coffee-preprocessor": "~0.1.2", @@ -123,7 +123,7 @@ "nodemon": "1.6.1", "parse-domain": "^0.2.1", "requirejs": "~2.1.10", - "sass-brunch": "https://github.com/basicer/sass-brunch-bleeding/archive/1.9.1-bleeding.tar.gz", + "sass-brunch": ">=2.0.0", "telepath-brunch": "https://github.com/nwinter/telepath-brunch/tarball/master", "uglify-js": "^2.5.0" }, From 877bea35e7e1533092199ce2434600e89dec0735 Mon Sep 17 00:00:00 2001 From: duybkict Date: Thu, 26 May 2016 15:44:35 +0700 Subject: [PATCH 017/167] add new vagrant box "brunchv2", revert npm package upgrades in package.json --- Vagrantfile | 4 ++++ package.json | 12 ++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 22228c451..c4ac08792 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -19,6 +19,10 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.define "default" do |default| default.vm.provision "shell", path: "scripts/vagrant/core/provision.sh", privileged: false +<<<<<<< HEAD +======= + +>>>>>>> f148776... add new vagrant box "brunchv2", revert npm package upgrades in package.json end config.vm.define "brunchv2", autostart: false do |brunchv2| diff --git a/package.json b/package.json index cd1c9d497..fa69ffaee 100644 --- a/package.json +++ b/package.json @@ -93,11 +93,11 @@ "devDependencies": { "after-brunch": "0.0.5", "assetsmanager-brunch": "^1.8.1", - "auto-reload-brunch": ">=2.0.0", + "auto-reload-brunch": "^1.8.1", "bower": "~1.6.4", - "brunch": ">=2.0.0", - "coffee-script-brunch": ">=2.0.0", - "coffeelint-brunch": ">=2.0.0", + "brunch": "^1.8.5", + "coffee-script-brunch": "^1.8.3", + "coffeelint-brunch": "^1.7.1", "commonjs-require-definition": "0.2.0", "compressible": "~1.0.1", "country-data": "0.0.24", @@ -107,7 +107,7 @@ "http-proxy": "^1.13.2", "jade-brunch": "1.7.5", "jasmine": "^2.4.1", - "javascript-brunch": ">=2.0.0", + "javascript-brunch": "> 1.0 < 1.8", "karma": "~0.13", "karma-chrome-launcher": "~0.1.2", "karma-coffee-preprocessor": "~0.1.2", @@ -123,7 +123,7 @@ "nodemon": "1.6.1", "parse-domain": "^0.2.1", "requirejs": "~2.1.10", - "sass-brunch": ">=2.0.0", + "sass-brunch": "https://github.com/basicer/sass-brunch-bleeding/archive/1.9.1-bleeding.tar.gz", "telepath-brunch": "https://github.com/nwinter/telepath-brunch/tarball/master", "uglify-js": "^2.5.0" }, From ebab0dedec20420bdd2631d72eb9a28fdf509835 Mon Sep 17 00:00:00 2001 From: duybkict Date: Thu, 26 May 2016 16:52:57 +0700 Subject: [PATCH 018/167] minor fixes --- Vagrantfile | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index c4ac08792..22228c451 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -19,10 +19,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.define "default" do |default| default.vm.provision "shell", path: "scripts/vagrant/core/provision.sh", privileged: false -<<<<<<< HEAD -======= - ->>>>>>> f148776... add new vagrant box "brunchv2", revert npm package upgrades in package.json end config.vm.define "brunchv2", autostart: false do |brunchv2| From 5fcc3669af85c7ea007e66de386e4c23e904a998 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Fri, 27 May 2016 11:19:50 -0700 Subject: [PATCH 019/167] :bug:Revert to css-brunch ^1.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fa69ffaee..fc326df34 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "compressible": "~1.0.1", "country-data": "0.0.24", "country-list": "0.0.3", - "css-brunch": ">=2.0.0", + "css-brunch": "^1.7.0", "fs-extra": "^0.26.2", "http-proxy": "^1.13.2", "jade-brunch": "1.7.5", From 438e8e426ce1329ccf0130a61fb5a3a756203b12 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Fri, 27 May 2016 11:22:33 -0700 Subject: [PATCH 020/167] Fix potential undefined error in logging --- server/handlers/classroom_handler.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/handlers/classroom_handler.coffee b/server/handlers/classroom_handler.coffee index 0fe8b8387..40b8055f6 100644 --- a/server/handlers/classroom_handler.coffee +++ b/server/handlers/classroom_handler.coffee @@ -105,7 +105,7 @@ ClassroomHandler = class ClassroomHandler extends Handler return @sendSuccess(res, (@formatEntity(req, classroom) for classroom in classrooms)) else if memberID = req.query.memberID unless req.user and (req.user.isAdmin() or memberID is req.user.id) - log.debug "classroom_handler.get: memberID (#{memberID}) must be yourself (#{req.user.id})" + log.debug "classroom_handler.get: memberID (#{memberID}) must be yourself (#{req.user?.id})" return @sendForbiddenError(res) return @sendBadInputError(res, 'Bad memberID') unless utils.isID memberID Classroom.find {members: mongoose.Types.ObjectId(memberID)}, (err, classrooms) => From 8bb52517972647a277e01e5045cc90c250012916 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Fri, 27 May 2016 11:27:04 -0700 Subject: [PATCH 021/167] Fix potential undefined error in logging --- server/handlers/classroom_handler.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/handlers/classroom_handler.coffee b/server/handlers/classroom_handler.coffee index 40b8055f6..db7809a27 100644 --- a/server/handlers/classroom_handler.coffee +++ b/server/handlers/classroom_handler.coffee @@ -97,7 +97,7 @@ ClassroomHandler = class ClassroomHandler extends Handler get: (req, res) -> if ownerID = req.query.ownerID unless req.user and (req.user.isAdmin() or ownerID is req.user.id) - log.debug "classroom_handler.get: ownerID (#{ownerID}) must be yourself (#{req.user.id})" + log.debug "classroom_handler.get: ownerID (#{ownerID}) must be yourself (#{req.user?.id})" return @sendForbiddenError(res) return @sendBadInputError(res, 'Bad ownerID') unless utils.isID ownerID Classroom.find {ownerID: mongoose.Types.ObjectId(ownerID)}, (err, classrooms) => From 72f95843ee4a2bc231ea32f52aebe0ed6965114f Mon Sep 17 00:00:00 2001 From: duybkict Date: Sat, 28 May 2016 12:44:38 +0700 Subject: [PATCH 022/167] update vagrant script --- scripts/vagrant/core/provision.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/vagrant/core/provision.sh b/scripts/vagrant/core/provision.sh index 18bd6a9a5..ef21af488 100644 --- a/scripts/vagrant/core/provision.sh +++ b/scripts/vagrant/core/provision.sh @@ -48,6 +48,7 @@ sudo mount --bind /node_modules /vagrant/node_modules # prepare find /vagrant/app -type f -exec dos2unix {} \; find /vagrant/vendor -type f -exec dos2unix {} \; +find /vagrant/scripts/vagrant -type f -exec dos2unix {} \; sudo chown -R vagrant:vagrant /home/vagrant # install npm modules From 30424620803f4a80ac5eb267d0df9641130c671a Mon Sep 17 00:00:00 2001 From: Ana Date: Mon, 30 May 2016 18:28:33 +0200 Subject: [PATCH 023/167] Update sr.coffee (#3673) began translation of skill docs and clans --- app/locale/sr.coffee | 130 +++++++++++++++++++++---------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/app/locale/sr.coffee b/app/locale/sr.coffee index 1dd626186..1c608a1ee 100644 --- a/app/locale/sr.coffee +++ b/app/locale/sr.coffee @@ -297,7 +297,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian save: "Сачувај" publish: "Објави" create: "Направи" -# fork: "Fork" + fork: "Форкуј" play: "Играј" # When used as an action verb, like "Play next level" retry: "Покушај поново" actions: "Радње" @@ -676,16 +676,16 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian restricted_to_certain_heroes: "Само одређени хероји могу да играју овај ниво." # skill_docs: -# function: "function" # skill types -# method: "method" -# snippet: "snippet" -# number: "number" -# array: "array" -# object: "object" -# string: "string" + function: "функција" # skill types + method: "метод" + snippet: "исечак" + number: "број" + array: "низ" + object: "објекат" + string: "стринг" # writable: "writable" # Hover over "attack" in Your Skills while playing a level to see most of this # read_only: "read-only" -# action: "Action" + action: "Aкција" # spell: "Spell" # action_name: "name" # action_cooldown: "Takes" @@ -939,14 +939,14 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian password_repeat: "Понови своју шифру." # keyboard_shortcuts: -# keyboard_shortcuts: "Keyboard Shortcuts" -# space: "Space" -# enter: "Enter" -# press_enter: "press enter" -# escape: "Escape" -# shift: "Shift" -# run_code: "Run current code." -# run_real_time: "Run in real time." + keyboard_shortcuts: "Пречице за тастатуру" + space: "Space" + enter: "Enter" + press_enter: "притисни enter" + escape: "Escape" + shift: "Shift" + run_code: "Покрени тренутни код." + run_real_time: "Покрени у реалном времену." # continue_script: "Continue past current script." # skip_scripts: "Skip past all skippable scripts." # toggle_playback: "Toggle play/pause." @@ -960,7 +960,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # maximize_editor: "Maximize/minimize code editor." # community: -# main_title: "CodeCombat Community" + main_title: "CodeCombat Заједница" # introduction: "Check out the ways you can get involved below and decide what sounds the most fun. We look forward to working with you!" # level_editor_prefix: "Use the CodeCombat" # level_editor_suffix: "to create and edit levels. Users have created levels for their classes, friends, hackathons, students, and siblings. If create a new level sounds intimidating you can start by forking one of ours!" @@ -968,53 +968,53 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # thang_editor_suffix: "to modify the CodeCombat source artwork. Allow units to throw projectiles, alter the direction of an animation, change a unit's hit points, or upload your own vector sprites." # article_editor_prefix: "See a mistake in some of our docs? Want to make some instructions for your own creations? Check out the" # article_editor_suffix: "and help CodeCombat players get the most out of their playtime." -# find_us: "Find us on these sites" -# social_github: "Check out all our code on GitHub" -# social_blog: "Read the CodeCombat blog on Sett" -# social_discource: "Join the discussion on our Discourse forum" -# social_facebook: "Like CodeCombat on Facebook" -# social_twitter: "Follow CodeCombat on Twitter" -# social_gplus: "Join CodeCombat on Google+" -# social_slack: "Chat with us in the public CodeCombat Slack channel" -# contribute_to_the_project: "Contribute to the project" + find_us: "Нађи нас на овим сајтовима" + social_github: "Погледај цео наш код на GitHub-у" + social_blog: "Читај CodeCombat блог на Sett-у" + social_discource: "Придружи се дискусији на нашем Discourse форуму" + social_facebook: "Лајкуј CodeCombat на Фејсбуку" + social_twitter: "Запрати CodeCombat на Твитеру" + social_gplus: "Придружи се CodeCombat-у на Гугл+" + social_slack: "Ћаскај са нама на јавном CodeCombat Slack каналу" + contribute_to_the_project: "Допринеси пројекту" -# clans: -# clan: "Clan" -# clans: "Clans" -# new_name: "New clan name" -# new_description: "New clan description" -# make_private: "Make clan private" -# subs_only: "subscribers only" -# create_clan: "Create New Clan" -# private_preview: "Preview" -# private_clans: "Private Clans" -# public_clans: "Public Clans" -# my_clans: "My Clans" -# clan_name: "Clan Name" -# name: "Name" + clans: + clan: "Клан" + clans: "Кланови" + new_name: "Име новог клана" + new_description: "Опис новог клана" + make_private: "Направи клан приватним" + subs_only: "само за претплатнике" + create_clan: "Направи нови клан" + private_preview: "Preview" + private_clans: "Приватни кланови" + public_clans: "Јавни кланови" + my_clans: "Моји кланови" + clan_name: "Име клана" + name: "Име" # chieftain: "Chieftain" # type: "Type" -# edit_clan_name: "Edit Clan Name" -# edit_clan_description: "Edit Clan Description" -# edit_name: "edit name" -# edit_description: "edit description" -# private: "(private)" + edit_clan_name: "Измени име клана" + edit_clan_description: "Измени опис клана" + edit_name: "измени име" + edit_description: "измени опис" + private: "(приватан)" # summary: "Summary" # average_level: "Average Level" # average_achievements: "Average Achievements" -# delete_clan: "Delete Clan" -# leave_clan: "Leave Clan" -# join_clan: "Join Clan" -# invite_1: "Invite:" + delete_clan: "Избриши клан" + leave_clan: "Напусти клан" + join_clan: "Придружи се клану" + invite_1: "Позови:" # invite_2: "*Invite players to this Clan by sending them this link." -# members: "Members" -# progress: "Progress" + members: "Чланови" + progress: "Напредак" # not_started_1: "not started" # started_1: "started" # complete_1: "complete" # exp_levels: "Expand levels" # rem_hero: "Remove Hero" -# status: "Status" + status: "Статус" # complete_2: "Complete" # started_2: "Started" # not_started_2: "Not Started" @@ -1873,15 +1873,15 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian year: "Година" day: "Дан" month: "Месец" - january: "Јануар" - february: "Фебруар" - march: "Март" - april: "Април" - may: "Мај" - june: "Јун" - july: "Јул" - august: "Август" - september: "Септембар" - october: "Октобар" - november: "Новембар" - december: "Децембар" + january: "јануар" + february: "фебруар" + march: "март" + april: "април" + may: "мај" + june: "јун" + july: "јул" + august: "август" + september: "септембар" + october: "октобар" + november: "новембар" + december: "децембар" From 956e91c2f5ace71796fd51bd683c155029ea120e Mon Sep 17 00:00:00 2001 From: JurianLock Date: Mon, 30 May 2016 18:28:55 +0200 Subject: [PATCH 024/167] Update nl-NL.coffee (#3674) Did some user-flow optimizations and some spelling checks. --- app/locale/nl-NL.coffee | 76 ++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/app/locale/nl-NL.coffee b/app/locale/nl-NL.coffee index 5e22c614e..e466d1c4d 100644 --- a/app/locale/nl-NL.coffee +++ b/app/locale/nl-NL.coffee @@ -818,51 +818,51 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription more_info_3: "Het is een goede plek om contact te leggen met andere leerkrachten, die CodeCombat gebruiken." teachers_quote: - name: "Offerte Formulier" # {change} - title: "Offerte aanvragen" # {change} - subtitle: "Haal CodeCombat jouw klaslokaal, club, school of wijk in!" + name: "Demo Formulier" + title: "Demo aanvragen" + subtitle: "Haal CodeCombat jouw klaslokaal of club!" email_exists: "Er bestaat al een gebruiker met dit email adres." phone_number: "Telefoonnummer" - phone_number_help: "Waar kunnen we je bereiken tijdens de werkdag?" - primary_role_label: "Uw Primaire Rol" - role_default: "Selecteer Rol" - primary_role_default: "Selecteer Primaire Rol" - purchaser_role_default: "Selecteer Aanschaf Rol" + phone_number_help: "Waarop kunnen we je bereiken tijdens kantooruren?" + primary_role_label: "Uw primaire rol" + role_default: "Selecteer primaire rol" + primary_role_default: "Selecteer rol" + purchaser_role_default: "Selecteer rol in besluitvorming" tech_coordinator: "IT coördinator" advisor: "Adviseur" principal: "Schoolhoofd" superintendent: "Leidinggevende" parent: "Ouder/verzorger" - purchaser_role_label: "Uw aanschaf Rol" + purchaser_role_label: "Uw rol in besluitvorming" influence_advocate: "Promoten" evaluate_recommend: "Evalueren/Aanbevelen" approve_funds: "Budget toewijzen" - no_purchaser_role: "Geen rol in aanschaf keuze" + no_purchaser_role: "Geen rol in besluitvorming" organization_label: "Schoolnaam" city: "Stad" state: "Provincie" country: "Land" - num_students_help: "Hoeveel leerlingen verwacht u in te schrijven voor CodeCombat?" + num_students_help: "Voor hoeveel leerlingen zou je CodeCombat willen gebruiken?" num_students_default: "Selecteer bereik" - education_level_label: "Opleidingsniveau van de leerlingen" - education_level_help: "Kies hoeveel van toepassing is." + education_level_label: "Opleidingsniveau van leerlingen" + education_level_help: "Selecteer toepasbare niveau(s)." elementary_school: "Basisschool" - high_school: "Middelbare school" - please_explain: "(licht a.u.b. toe)" - middle_school: "Brugklas" + high_school: "Middelbare school (bovenbouw)" + please_explain: "(licht toe a.u.b.)" + middle_school: "Middelbare school (onderbouw)" college_plus: "Hoger Onderwijs" anything_else: "Is er verder nog iets dat we moeten weten?" - thanks_header: "Bedankt voor het aanvragen van een citaat!" # {change} - thanks_sub_header: "Bedankt voor je interesse in CodeCombat voor jouw school." + thanks_header: "Bedankt voor je demo-aanvraag!" + thanks_sub_header: "Bedankt voor je interesse in CodeCombat." thanks_p: "Wij zullen spoedig contact met u opnemen. Vragen? Mail ons:" # {change} -# back_to_classes: "Back to Classes" - finish_signup: "Voltooi aanmaken van jouw docenten-account:" + back_to_classes: "Terug naar Klassen" + finish_signup: "Voltooi je docenten-account:" finish_signup_p: "Maak een account om een klas op te zetten, voeg leerlingen toe en bekijk hun voortgang terwijl ze leren programmeren" signup_with: "Schrijf je in met:" connect_with: "Koppel met:" - conversion_warning: "WAARSCHUWING: Je huidige account is een Leerling Account. Wanneer je dit formulier indient zal dit account gewijzigt worden naar een Docenten Account." - learn_more_modal: "Docenten Accounts op CodeCombat hebben de mogelijkheid om de voortgang van studenten te bekijken, inschrijvingen regelen en klaslokalen beheren. Docenten Accounts kunnen niet in een klaslokaal ingeschreven staan - als je ingeschreven staat in een klas met dit account zul je niet langer toegang hebben tot die klas wanneer je jouw account veranderd in een Docenten Account." - create_account: "Maak een docenten-account" + conversion_warning: "WAARSCHUWING: Je huidige account is een Leerling Account. Wanneer je dit formulier indient zal dit account worden omgezet naar een Docenten Account." + learn_more_modal: "Docenten Accounts kunnen de voortgang van leerlingen te zien, inschrijvingen regelen en klaslokalen beheren. Docenten Accounts kunnen niet in een klaslokaal ingeschreven staan - als je ingeschreven staat in een klas met dit account zul je niet langer als leerling toegang hebben tot die klas." + create_account: "Maak een docenten-account aan" create_account_subtitle: "Krijg toegang tot exclusieve hulpmiddelen voor docenten door CodeCombat in de klas te gebruiken. Maak een klas aan, voeg leerlingen toe, en bekijk hun vooruitgang!" convert_account_title: "Verander in een docenten-account" not: "niet" @@ -871,7 +871,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription versions: save_version_title: "Nieuwe versie opslaan" new_major_version: "Nieuwe hoofdversie" - submitting_patch: "Aanpassing wordt Ingediend..." + submitting_patch: "Aanpassing wordt ingediend..." cla_prefix: "Om bewerkingen op te slaan, moet je eerst akkoord gaan met onze" cla_url: "CLA" cla_suffix: "." @@ -1069,10 +1069,10 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription completed: "voltooid:" invite_students: "Nodig leerlingen uit voor deze klas." invite_link_header: "Link om aan cursus deel te nemen" - invite_link_p_1: "Geef deze link aan leerlingen waarvan u wilt dat zij de cursus volgen." + invite_link_p_1: "Geef deze link aan leerlingen waarvan je wil dat zij de cursus volgen." invite_link_p_2: "Of laat ons hun direct een e-mail sturen:" - capacity_used: "Cursus stoelen gebruikt:" - enter_emails: "Voer e-mailadressen van leerlingen in om uit te nodigen, één per regel" + capacity_used: "Cursusplekken gebruikt:" + enter_emails: "Voer e-mailadressen van leerlingen in om hen uit te nodigen, één per regel" send_invites: "Verstuur uitnodigingen" creating_class: "Aanmaken klas..." purchasing_course: "Aanschaffen cursus..." @@ -1080,7 +1080,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription buy_course1: "Koop deze cursus" select_all_courses: "Selecteer 'Alle cursussen' voor 50% korting!" all_courses: "Alle cursussen" - number_programming_students: "Aantal informatica leerlingen" + number_programming_students: "Aantal doelgroepleerlingen" number_total_students: "Totaal aan leerlingen op school" enter_number_students: "Voer het aantal leerlingen in dat je voor de klas nodig hebt." name_class: "Geef je klas een naam" @@ -1089,25 +1089,25 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription purchasing_for: "U koopt een licentie voor" creating_for: "U maakt een klas voor" for: "voor" # Like in 'for 30 students' - receive_code: "Naderhand ontvangt u een ontgrendelcode om uw leerlingen, waarmee zij zich kunnen inschrijven in uw klas." + receive_code: "Naderhand ontvangt u een ontgrendelcode vooor uw leerlingen, waarmee zij zich kunnen inschrijven in uw klas." free_trial: "Gratis uitproberen voor leerkrachten!" get_access: "Individueel toegang krijgen voor evaluatiedoeleinden." questions: "Vragen?" teachers_click: "Leerkrachten klik hier" students_click: "Leerlingen klik hier" courses_on_coco: "Cursussen van CodeCombat" - designed_to: "Cursussen zijn ontwikkeld ter introductie van ICT-concepten met CodeCombats leuke en leerzame omgevingen. CodeCombat-levels zijn gesturctureerd rond belangrijke concepten om vooruitstrevend leren te bemoedigen, gedurende 5 uur." + designed_to: "Cursussen zijn ontwikkeld ter introductie van ICT-concepten met CodeCombat's leuke en leerzame omgevingen. CodeCombat-levels zijn gesturctureerd rond belangrijke concepten om vooruitstrevend leren te stimuleren, gedurende 5 uur." more_in_less: "Leer meer in minder tijd" no_experience: "Geen programmeerervaring nodig" easy_monitor: "voortgang leerlingen gemakkelijk te bekijken" - purchase_for_class: "Koop een cursus voor de hele klas. Het is makkelijk je leerlingen in te schrijven!" + purchase_for_class: "Koop een cursus voor de hele klas. Schrijf je leerlingen eenvoudig in!" see_the: "Bekijk de" more_info: "voor meer informatie." choose_course: "Kies uw cursus:" enter_code: "voer een ontgrendelcode in" # {change} enter_code1: "Voer ontgrendelcode in" enroll: "Inschrijven" - pick_from_classes: "Kies uit je huidige klas" + pick_from_classes: "Kies uit je huidige klassen" enter: "Treed toe" or: "Of" topics: "Onderwerpen" @@ -1233,10 +1233,10 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription getting_started_3: "Je ziet dan de voortgang van de leerlingen, wanneer ze zich inschrijven en aanmelden voor uw klas." additional_resources: "Extra bronnen" additional_resources_1_pref: "Download/print onze" - additional_resources_1_mid: "Cursus 1 Leerkrachtengids" # {change} + additional_resources_1_mid: "Cursus 1 docentenhandleiding" additional_resources_1_mid2: "en" additional_resources_1_mid3: "Cursus 2" - additional_resources_1_suff: "uitleg en oplossingen voor elke level." # {change} + additional_resources_1_suff: "uitleg en oplossingen voor elk level." additional_resources_2_pref: "Maak onze af" additional_resources_2_suff: "Om twee gratis inschrijvingen te krijgen voor onze overige betaalde cursussen." additional_resources_3_pref: "Bezoek onze" @@ -1601,7 +1601,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription tutorial_skip: "Sla Tutorial over" tutorial_not_sure: "Niet zeker wat er aan de hand is?" tutorial_play_first: "Speel eerst de Tutorial." - simple_ai: "Simpele AI" # {change} + simple_ai: "Simpele K.I." warmup: "Opwarming" friends_playing: "Spelende Vrienden" log_in_for_friends: "Log in om met je vrienden te spelen!" @@ -1623,8 +1623,8 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription rules: "Regels" winners: "Winnaars" league: "Competitie" - red_ai: "KI rood" # "Red AI Wins", at end of multiplayer match playback - blue_ai: "KI blauw" # {change} + red_ai: "K.I. rood" # "Red AI Wins", at end of multiplayer match playback + blue_ai: "K.I. blauw" wins: "Wint" # At end of multiplayer match playback humans: "Rood" # Ladder page display team name ogres: "Blauw" @@ -1698,7 +1698,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription months: "Maanden" purchase_total: "Totaal" purchase_button: "Verzend betaling" - your_codes: "Je codes:" # {change} + your_codes: "Jouw codes:" redeem_codes: "Prepaidcode inwisselen" prepaid_code: "Prepaidcode" lookup_code: "Prepaidcode opzoeken" From 59eeef75d6f663630276fdd7d77e1348aedcbba7 Mon Sep 17 00:00:00 2001 From: GrecK0 Date: Mon, 30 May 2016 11:29:31 -0500 Subject: [PATCH 025/167] Update es-ES.coffee (#3677) Hi, Several sentences translated to it's spanish version. Regards!!! --- app/locale/es-ES.coffee | 106 ++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/app/locale/es-ES.coffee b/app/locale/es-ES.coffee index 0d88fb435..60ac50d36 100644 --- a/app/locale/es-ES.coffee +++ b/app/locale/es-ES.coffee @@ -15,8 +15,8 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis or_ipad: "O descargalo para iPad" new_home: -# slogan: "The most engaging game for learning programming." -# classroom_edition: "Classroom Edition:" + slogan: "El juego más atractivo para aprender a programar." + classroom_edition: "Edición para aulas:" learn_to_code: "Aprende a programar:" teacher: "Profesor" student: "Alumno" @@ -31,24 +31,24 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis designed_with: "Diseñado pensando en los profesores" real_code: "Real, escribe código" from_the_first_level: "desde el primer nivel" -# getting_students: "Getting students to typed code as quickly as possible is critical to learning programming syntax and proper structure." -# educator_resources: "Educator resources" -# course_guides: "and course guides" -# teaching_computer_science: "Teaching computer science does not require a costly degree, because we provide tools to support educators of all backgrounds." + getting_students: "Involucrar a los alumnos en la programación por sentencias tan rápido como sea posible es fundamental para aprender la sintaxis de la programación con una estructura apropiada." + educator_resources: "Recursos para educadores" + course_guides: "y guías de cursos" + teaching_computer_science: "Enseñar ciencias computación no requiere de un costoso título, porque nosotros proveemos las herramientas para apoyar educadores con cualquier nivel de conocimientos." accessible_to: "Accesible para" everyone: "todo el mundo" -# democratizing: "Democratizing the process of learning coding is at the core of our philosophy. Everyone should be able to learn to code." -# forgot_learning: "I think they actually forgot that they were actually learning something." -# wanted_to_do: " Coding is something I've always wanted to do, and I never thought I would be able to learn it in school." -# why_games: "Why is learning through games important?" -# games_reward: "Games reward the productive struggle." -# encourage: "Gaming is a medium that encourages interaction, discovery, and trial-and-error. A good game challenges the player to master skills over time, which is the same critical process students go through as they learn." -# excel: "Games excel at rewarding" -# struggle: "productive struggle" -# kind_of_struggle: "the kind of struggle that results in learning that’s engaging and" -# motivating: "motivating" -# not_tedious: "not tedious." -# gaming_is_good: "Studies suggest gaming is good for children’s brains. (it’s true!)" + democratizing: "La democratización del proceso de aprendizaje es el nucleo de nuestra filosofía. Todo mundo debe ser capaz de aprender a programar." + forgot_learning: "En realidad creo que que ellos olvidaron que en realidad están aprendiendo algo." + wanted_to_do: " Programar es algo que siempre he querido hacer, nunca pensé que sería capáz de aprenderlo en la escuela." + why_games: "¿Por qué aprender usando juegos es importante?" + games_reward: "Los juegos premian el esfuerzo productivo" + encourage: "Jugar es un excelente medio que promueve la interacción, descubrimiento, y aprendizaje a través de prueba-error. Un buen juego reta al jugador a dominar habilidades al pasar el tiempo, éste el el mismo proceso critico por el que pasan los estudiantes al aprender." + excel: "Los juegos son excelentes premiando" + struggle: "el esfuerzo productivo" + kind_of_struggle: "el tipo de esfuerzo que resulta en aprendizaje learning que es atractivo y" + motivating: "motivante" + not_tedious: "no tedioso." + gaming_is_good: "Estudios sugieren que el jugar es bueno para el cerebro de los niños. (¡Es verdad!)" # game_based: "When game-based learning systems are" # compared: "compared" # conventional: "against conventional assessment methods, the difference is clear: games are better at helping students retain knowledge, concentrate and" @@ -61,11 +61,11 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis # request_demo_subtitle: "Request a demo and get your students started in less than an hour." # get_started_title: "Set up your class today" # get_started_subtitle: "Set up a class, add your students, and monitor their progress as they learn computer science." -# request_demo: "Request a Demo" + request_demo: "Pide una demostración" # setup_a_class: "Set Up a Class" -# have_an_account: "Have an account?" + have_an_account: "¿Tienes una cuenta?" # logged_in_as: "You are currently logged in as" -# view_my_classes: "View my classes" + view_my_classes: "Ver mis clases" # computer_science: "Computer science courses for all ages" # show_me_lesson_time: "Show me lesson time estimates for:" # curriculum: "Total curriculum hours:" @@ -905,17 +905,17 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis wrong_password: "Contraseña incorrecta" upload_picture: "Sube una imagen" delete_this_account: "Eliminar esta cuenta de forma permanente" -# reset_progress_tab: "Reset All Progress" -# reset_your_progress: "Clear all your progress and start over" + reset_progress_tab: "Reestablecer progreso" + reset_your_progress: "Elimina tu progreso y empieza de nuevo" god_mode: "Modo Dios" password_tab: "Contraseña" emails_tab: "Correos electrónicos" admin: "Admin" -# manage_subscription: "Click here to manage your subscription." + manage_subscription: "Clic aquí para modificar tu suscripción." new_password: "Nueva contraseña" new_password_verify: "Verificar" - type_in_email: "Introduce tu email para confirmar el borrado de esta cuenta" -# type_in_email_progress: "Type in your email to confirm deleting your progress." + type_in_email: "Introduce tu correo para confirmar el borrado de esta cuenta" + type_in_email_progress: "TypeIntroduce tu correo para confirmar el borrado de tu progreso." type_in_password: "Además, introduce tu contraseña." email_subscriptions: "Suscripciones de correo electrónico" email_subscriptions_none: "Sin suscripciones de correo electrónico." @@ -1398,7 +1398,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis ambassador_title: "Embajador" ambassador_title_description: "(Soporte)" ambassador_summary: "Amansa a los usuarios de nuestro foro y guía a aquellos que tengan preguntas. Nuestros Embajadores representan a CodeCombat frente al mundo." -# teacher_title: "Teacher" + teacher_title: "Profesor" editor: main_title: "Editores de CodeCombat" @@ -1406,7 +1406,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis thang_title: "Editor de Objetos" level_title: "Editor de Niveles" achievement_title: "Editor de Logros" -# poll_title: "Poll Editor" + poll_title: "Editor de Encuestas" back: "Volver" revert: "Revertir" revert_models: "Revertir Modelos" @@ -1471,7 +1471,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis new_achievement_title: "Crea un nuevo Logro" new_achievement_title_login: "Inicia sesión para Crear un Nuevo Logro" new_poll_title: "Crear una nueva Encuesta" -# new_poll_title_login: "Log In to Create a New Poll" + new_poll_title_login: "Inicia sesión para Crear una Nueva Encuesta" article_search_title: "Buscar artículos aquí" thang_search_title: "Busca tipos de objetos aquí" level_search_title: "Buscar niveles aquí" @@ -1511,7 +1511,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis join_desc_3: ", o encuéntranos en nuestro " join_desc_4: "¡y partiremos desde ese punto!" join_url_email: "Escríbenos un correo electrónico" -# join_url_slack: "public Slack channel" + join_url_slack: "canal público de Slack" archmage_subscribe_desc: "Recibe correos sobre nuevos anuncios y oportunidades de codificar." artisan_introduction_pref: "¡Debemos construir niveles adicionales! La gente clama por más contenido y solo podemos crear unos cuantos. Ahora mismo tu estación de trabajo es el nivel uno; nuestro editor de niveles es apenas usable por sus creadores, así que ten cuidado. Si tienes visiones de campañas que alcanzan el infinito" artisan_introduction_suf: ", entonces esta Clase es ideal para ti." @@ -1554,8 +1554,8 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis ambassador_join_desc: "cuéntanos más sobre ti, que has hecho y qué estarías interesado en hacer. ¡Y continuaremos a partir de ahí!" ambassador_join_note_strong: "Nota" ambassador_join_note_desc: "Una de nuestras principales prioridades es construir un modo multijugador donde los jugadores con mayores dificultades a la hora de resolver un nivel, puedan invocar a los magos más avanzados para que les ayuden. Será una buena manera de que los Embajadores puedan hacer su trabajo. ¡Te mantendremos informado!" - ambassador_subscribe_desc: "Recibe correos sobre actualizaciones de soporte y desarrollo del multijugador." -# teacher_subscribe_desc: "Get emails on updates and announcements for teachers." + ambassador_subscribe_desc: "Recibe correos acerca de actualizaciones de soporte y desarrollo del multijugador." + teacher_subscribe_desc: "Recibe acerca de actualizaciones y anuncios para profesores." changes_auto_save: "Los cambios son guardados automáticamente cuando marcas las casillas de verificación." diligent_scribes: "Nuestros diligentes Escribas:" powerful_archmages: "Nuestros poderosos Archimagos:" @@ -1610,7 +1610,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis fight: "¡Pelea!" watch_victory: "Ver tu victoria" defeat_the: "Vence a" -# watch_battle: "Watch the battle" + watch_battle: "Observa la batalla" tournament_started: ", iniciado" tournament_ends: "El torneo termina" tournament_ended: "El torneo ha terminado" @@ -1659,7 +1659,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis account: payments: "Pagos" -# prepaid_codes: "Prepaid Codes" + prepaid_codes: "Código prepagados" purchased: "Adquirido" subscription: "Suscripción" invoices: "Facturas" @@ -1712,9 +1712,9 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis # you_can3: "that can be applied to your own account or given to others." # coppa_deny: -# text1: "Can’t wait to learn programming?" -# text2: "Ask your parents to create an account for you!" -# close: "Close Window" + text1: "¿No puedes esperar para empezar a programar?" + text2: "¡Pide a tus padres que hagan una cuenta para ti!" + close: "Cerrar ventana" loading_error: could_not_load: "Error al cargar desde el servidor." @@ -1860,7 +1860,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis prizes: "Premios" total_value: "Valor Total" in_cash: "en dinero" - custom_wizard: "Personaliza tu Mago de CodeCombat" + custom_wizard: "Personaliza tu Hechicero de CodeCombat" custom_avatar: "Personaliza tu avatar de CoceCombat" heap: "Por seis meses de acceso \"Startup\"" credits: "creditos" @@ -1870,18 +1870,18 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis oreilly: "ebook de tu eleccion" # calendar: -# year: "Year" -# day: "Day" -# month: "Month" -# january: "January" -# february: "February" -# march: "March" -# april: "April" -# may: "May" -# june: "June" -# july: "July" -# august: "August" -# september: "September" -# october: "October" -# november: "November" -# december: "December" + year: "Año" + day: "Día" + month: "Mes" + january: "Enero" + february: "Febrero" + march: "Marzo" + april: "Abril" + may: "Mayo" + june: "Junio" + july: "Julio" + august: "Agosto" + september: "Septiembre" + october: "Octubre" + november: "Noviembre" + december: "Diciembre" From d6bb496d1982b4ead7894d8ee46d70df7ca81878 Mon Sep 17 00:00:00 2001 From: Kevin Avignon Date: Mon, 30 May 2016 12:30:51 -0400 Subject: [PATCH 026/167] French translation (#3687) * Initial work on navigation context * navigation section completed * started work in the code section * code section completed * started work in the sign up section * completed work in the sign up section * common section completed * general section completed * work started in play_level section * work done in play_level section * work in calendar section started * work done in calendar section * work started in account_prepaid * work in progress in account_prepaid --- app/locale/fr.coffee | 236 +++++++++++++++++++++---------------------- 1 file changed, 118 insertions(+), 118 deletions(-) diff --git a/app/locale/fr.coffee b/app/locale/fr.coffee index 9ff9cc0f4..9230b62cb 100644 --- a/app/locale/fr.coffee +++ b/app/locale/fr.coffee @@ -102,7 +102,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t blog: "Blog" forum: "Forum" account: "Compte" -# my_account: "My Account" + my_account: "Mon compte" profile: "Profil" stats: "Stats" code: "Code" @@ -112,24 +112,24 @@ module.exports = nativeDescription: "français", englishDescription: "French", t about: "À propos" contact: "Contact" twitter_follow: "Suivre" -# students: "Students" + students: "Étudiants" teachers: "Enseignants" careers: "Emplois" -# facebook: "Facebook" -# twitter: "Twitter" -# create_a_class: "Create a Class" -# other: "Other" -# learn_to_code: "Learn to Code!" -# toggle_nav: "Toggle navigation" -# jobs: "Jobs" -# schools: "Schools" -# educator_wiki: "Educator Wiki" -# get_involved: "Get Involved" -# open_source: "Open source (GitHub)" -# support: "Support" -# faqs: "FAQs" -# help_pref: "Need help? Email" -# help_suff: "and we'll get in touch!" + facebook: "Facebook" + twitter: "Twitter" + create_a_class: "Créer une classe" + other: "Autre" + learn_to_code: "Apprenez à programmer!" + toggle_nav: "Basculer la navigation" + jobs: "Emplois" + schools: "Écoles" + educator_wiki: "Wiki pour les éducateurs" + get_involved: "Devenez impliqué" + open_source: "Code libre (GitHub)" + support: "Support" + faqs: "FAQs" + help_pref: "Besoin d'aide? Email" + help_suff: "et nous entrerons en contact!" modal: close: "Fermer" @@ -182,55 +182,55 @@ module.exports = nativeDescription: "français", englishDescription: "French", t campaign_old_multiplayer: "(Obsolète) Ancienne arènes multijoueurs" campaign_old_multiplayer_description: "Reliques d'un âge plus civilisé. Il n'y a aucune simulation pour ces vielles arènes multijoueur vides de tout héros." -# code: + code: # if: "if" # Keywords--these translations show up on hover, so please translate them all, even if it's kind of long. (In the code editor, they will still be in English.) -# else: "else" -# elif: "else if" -# while: "while" -# loop: "loop" -# for: "for" -# break: "break" -# continue: "continue" -# pass: "pass" -# return: "return" -# then: "then" -# do: "do" -# end: "end" -# function: "function" -# def: "define" -# var: "variable" -# self: "self" -# hero: "hero" -# this: "this" -# or: "or" -# "||": "or" -# and: "and" -# "&&": "and" -# not: "not" -# "!": "not" -# "=": "assign" -# "==": "equals" -# "===": "strictly equals" -# "!=": "does not equal" -# "!==": "does not strictly equal" -# ">": "is greater than" -# ">=": "is greater than or equal" -# "<": "is less than" -# "<=": "is less than or equal" -# "*": "multiplied by" -# "/": "divided by" -# "+": "plus" -# "-": "minus" -# "+=": "add and assign" -# "-=": "subtract and assign" -# True: "True" -# true: "true" -# False: "False" -# false: "false" -# undefined: "undefined" -# null: "null" -# nil: "nil" -# None: "None" + else: "Sinon" + elif: "et sinon" + while: "tant que" + loop: "boucle" + for: "pour" + break: "break" + continue: "continue" + pass: "passe" + return: "retourne" + then: "alors" + do: "fait" + end: "fin" + function: "fonction" + def: "definit" + var: "variable" + self: "soi-même" + hero: "héros" + this: "ceci" + or: "or" + "||": "ou" + and: "et" + "&&": "et" + not: "n'est pas" + "!": "n'est pas" + "=": "assigner" + "==": "équivalent à" + "===": "strictement équivalent à" + "!=": "n'est pas équivalent à" + "!==": "n'est pas strictement équivalent à" + ">": "est plus grand que" + ">=": "est plus grand que ou équivalent à" + "<": "is less than" + "<=": "est plus petit ou équivalent à" + "*": "multiplié par" + "/": "divisé par" + "+": "plus" + "-": "moins" + "+=": "ajoute et assigne" + "-=": "soustrait et assigne" + True: "Vrai" + true: "vrai" + False: "Faux" + false: "faux" + undefined: "non défini" + null: "nul" + nil: "nil" + None: "Aucune valeur" share_progress_modal: blurb: "Vous faites beaucoup de progrès ! Partagez ce que vous avez appris grâce à CodeCombat avec vos amis !" # {change} @@ -263,15 +263,15 @@ module.exports = nativeDescription: "français", englishDescription: "French", t school_name: "Nom et ville de votre école" optional: "facultatif" school_name_placeholder: "Exemple : Lycée Champollion, Figeac" -# or_sign_up_with: "or sign up with" -# connected_gplus_header: "You've successfully connected with Google+!" -# connected_gplus_p: "Finish signing up so you can log in with your Google+ account." -# gplus_exists: "You already have an account associated with Google+!" -# connected_facebook_header: "You've successfully connected with Facebook!" -# connected_facebook_p: "Finish signing up so you can log in with your Facebook account." -# facebook_exists: "You already have an account associated with Facebook!" -# hey_students: "Students, enter the class code from your teacher." -# birthday: "Birthday" + or_sign_up_with: "ou abonnez-vous avec" + connected_gplus_header: "Vous avez réussi à vous connecter avec votre compte Google+!" + connected_gplus_p: "Finissez de vous abonnez pour pouvoir vous connecter avec votre compte Google+." + gplus_exists: "Vous avez déjà un compte associé à Google+!" + connected_facebook_header: "Vous avez réussi à vous connecter avec votre compte Facebook!" + connected_facebook_p: "Finissez de vous abonnez pour pouvoir vous connecter avec votre compte Facebook." + facebook_exists: "Vous avez réussi à vous connecter avec votre compte Facebook!" + hey_students: "Étudiants, entrez le code de cours de votre enseignant." + birthday: "Anniversaire" recover: recover_account_title: "Récupérer son compte" @@ -308,7 +308,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t submit_patch: "Soumettre un correctif" submit_changes: "Soumettre des Changements" save_changes: "Sauvegarder les modifications" -# required_field: "Required field" + required_field: "Champ requis" general: and: "et" @@ -341,7 +341,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t subject: "Sujet" email: "E-mail" password: "Mot de passe" -# confirm_password: "Confirm Password" + confirm_password: "Confirmez votre mot de passe" message: "Message" code: "Code" ladder: "Companion" @@ -360,9 +360,9 @@ module.exports = nativeDescription: "français", englishDescription: "French", t warrior: "Guerrier" ranger: "Ranger" wizard: "Magicien" -# first_name: "First Name" -# last_name: "Last Name" -# username: "Username" + first_name: "Prénom" + last_name: "Nom de famille" + username: "Nom d'utilisateur" units: second: "seconde" @@ -381,7 +381,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t years: "années" play_level: -# level_complete: "Level Complete" + level_complete: "Niveau complété" completed_level: "Niveau terminé:" course: "Cours:" done: "Fait" @@ -425,7 +425,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t victory_new_item: "Nouvel item" victory_viking_code_school: "Par la barbe d'Odin, c'est un niveau difficile que tu viens de compléter! Si tu n'est pas un développeur de logiciel, tu devrais l'être ! Ceci vient de te propulser dans l'école de Code Vikings où tu pourras amener tes habilités au prochain niveau et devenir un développer web profesionnel en deux semaines." victory_become_a_viking: "Devenez un viking" -# victory_no_progress_for_teachers: "Progress is not saved for teachers. But, you can add a student account to your classroom for yourself." + victory_no_progress_for_teachers: "La progression n'est pas sauvegardé pour les enseignants. Mais vous pouvez ajouter un compte étudiant à votre salle de cours pour vous." guide_title: "Guide" tome_cast_button_run: "Exécuter" tome_cast_button_running: "En cours d'exécution" @@ -505,15 +505,15 @@ module.exports = nativeDescription: "français", englishDescription: "French", t tip_strong_opponents: "Même les ennemis les plus puissant ont une faiblesse. - Itachi Uchiha" tip_paper_and_pen: "Avant de commencer à coder, vous pouvez plannifier les choses sur papier." tip_solve_then_write: "D'abord, résolvez le problème. Ensuite, écrivez le code. - John Johnson" -# tip_compiler_ignores_comments: "Sometimes I think that the compiler ignores my comments." -# tip_understand_recursion: "The only way to understand recursion is to understand recursion." -# tip_life_and_polymorphism: "Open Source is like a totally polymorphic heterogeneous structure: All types are welcome." -# tip_mistakes_proof_of_trying: "Mistakes in your code are just proof that you are trying." -# tip_adding_orgres: "Rounding up ogres." -# tip_sharpening_swords: "Sharpening the swords." -# tip_ratatouille: "You must not let anyone define your limits because of where you come from. Your only limit is your soul. - Gusteau, Ratatouille" -# tip_nemo: "When life gets you down, want to know what you've gotta do? Just keep swimming, just keep swimming. - 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_compiler_ignores_comments: "Des fois, je pense que le compilateur ignore mes commentaires." + tip_understand_recursion: "La seule manière de comprendre la récursion est de comprendre la récursion." + tip_life_and_polymorphism: "Le code libre est une structure hétérogène polymorphique. Tous les types sont les bienvenues." + tip_mistakes_proof_of_trying: "Les erreurs dans votre code sont la preuve que vous essayez." + tip_adding_orgres: "Rassembler les ogres." + tip_sharpening_swords: "Aiguiser les épées." + tip_ratatouille: "Tu ne dois laisser personne définir où se trouvent tes limites. La seule vraie limite est c'est ton âme. - Gusteau, Ratatouille" + tip_nemo: "Quand la vie vous rabaisse, vous savez ce que vous devenez faire ? Juste continuer de nager, juste continuer de nager. - 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" @@ -1689,27 +1689,27 @@ module.exports = nativeDescription: "français", englishDescription: "French", t retrying: "Erreur interne, réessayez" success: "Paiement accepté, merci !" -# account_prepaid: -# purchase_code: "Purchase a Subscription Code" -# purchase_code1: "Subscription Codes can be redeemed to add premium subscription time to one or more CodeCombat accounts." + account_prepaid: + purchase_code: "Acheter un code d'abonnement" + purchase_code1: "Les codes d'abonnement peuvent seulement être racheter pour ajouter des comptes prenium à un ou plusieurs comptes de CodeCombat" # purchase_code2: "Each CodeCombat account can only redeem a particular Subscription Code once." # purchase_code3: "Subscription Code months will be added to the end of any existing subscription on the account." -# users: "Users" -# months: "Months" -# purchase_total: "Total" + users: "Utilisateurs" + months: "Mois" + purchase_total: "Total" # purchase_button: "Submit Purchase" -# your_codes: "Your Codes" + your_codes: "Vos codes" # redeem_codes: "Redeem a Subscription Code" # prepaid_code: "Prepaid Code" # lookup_code: "Lookup prepaid code" # apply_account: "Apply to your account" # copy_link: "You can copy the code's link and send it to someone." -# quantity: "Quantity" + quantity: "Quantité" # redeemed: "Redeemed" -# no_codes: "No codes yet!" -# you_can1: "You can" -# you_can2: "purchase a prepaid code" -# you_can3: "that can be applied to your own account or given to others." + no_codes: "Pas de code soumis" + you_can1: "Vous pouvez" + you_can2: "achetez un code pré-payé" + you_can3: "qui pourra être appliqué à votre compte ou donné à un autre." # coppa_deny: # text1: "Can’t wait to learn programming?" @@ -1869,19 +1869,19 @@ module.exports = nativeDescription: "français", englishDescription: "French", t license: "Licence" oreilly: "ebook de votre choix" -# calendar: -# year: "Year" -# day: "Day" -# month: "Month" -# january: "January" -# february: "February" -# march: "March" -# april: "April" -# may: "May" -# june: "June" -# july: "July" -# august: "August" -# september: "September" -# october: "October" -# november: "November" -# december: "December" + calendar: + year: "Année" + day: "Jour" + month: "Mois" + january: "Janvier" + february: "Février" + march: "Mars" + april: "Avril" + may: "Mai" + june: "Juin" + july: "Juillet" + august: "Août" + september: "Septembre" + october: "Octobre" + november: "Novembre" + december: "Décembre" From 25daaead127005fe870060139ae038221193f36a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dong=E6=AE=BA=E5=B8=83=E6=B2=99?= Date: Tue, 31 May 2016 00:32:36 +0800 Subject: [PATCH 027/167] Zh han traditional update (#3690) * Update zh-HANT.coffee * Update zh-HANT.coffee * Update zh-HANT.coffee * Update zh-HANT.coffee --- app/locale/zh-HANT.coffee | 76 +++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/app/locale/zh-HANT.coffee b/app/locale/zh-HANT.coffee index 62d408037..2b2e80bfc 100644 --- a/app/locale/zh-HANT.coffee +++ b/app/locale/zh-HANT.coffee @@ -279,8 +279,8 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese recovery_sent: "密碼重置的信件已寄出" items: - primary: "主要武器" - secondary: "次要武器" + primary: "主手裝備" + secondary: "副手裝備" armor: "裝甲" accessories: "飾品" misc: "輔助" @@ -344,10 +344,10 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese confirm_password: "確認密碼" message: "訊息" code: "程式碼" - ladder: "升級比賽" - when: "當" + ladder: "天梯" + when: "何時" opponent: "對手" - rank: "階級" + rank: "排名" score: "分數" win: "獲勝" loss: "失敗" @@ -472,7 +472,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese tip_reticulating: "網格狀鋸齒(指卡頓現象)" tip_harry: "巫師," tip_great_responsibility: "隨著擁有強大的編程技巧,除錯的責任將越大。" - tip_munchkin: "如果您不吃掉您的蔬菜,那小矮人將在您沉睡時找到您。" + tip_munchkin: "如果您不吃掉您的蔬菜,那些矮人將在您睡覺時找到您。" tip_binary: "只有2種人在這世上:那些懂2進位的,和那些不懂的。" tip_commitment_yoda: "一個程式員必須擁有強烈的責任感和一顆認真的心。 ~ Yoda《星際大戰》" tip_no_try: "做,或者不做。這邊不存在嘗試的選項。 - Yoda《星際大戰》" @@ -510,7 +510,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese tip_life_and_polymorphism: "開放源碼就像是一個完全多樣異質的結構:海納百川。"#"Open Source is like a totally polymorphic heterogeneous structure: All types are welcome." tip_mistakes_proof_of_trying: "程式碼中的錯誤證明了你曾經嘗試過。"#"Mistakes in your code are just proof that you are trying." tip_adding_orgres: "包圍敵人!"#"Rounding up ogres." - tip_sharpening_swords: "磨煉造就寶劍。"#"Sharpening the swords." + tip_sharpening_swords: "磨利劍鋒。"#"Sharpening the swords." tip_ratatouille: "你不需要讓任何人以你的出身決定你的極限,你唯一的限制就是你的心靈。廚神Gusteau《料理鼠王》 "#"You must not let anyone define your limits because of where you come from. Your only limit is your soul. - Gusteau, Ratatouille" tip_nemo: "想知道當生活使你消沉時,你必須做甚麼嗎?一直游,一直游就對了。多莉《海底總動員》"#"When life gets you down, want to know what you've gotta do? Just keep swimming, just keep swimming. - Dory, Finding Nemo" tip_internet_weather: "搬來網路世界吧,這裡十分不錯。我們住的地方天氣總是很好。 - John Green"#"Just move to the internet, it's great here. We get to live inside where the weather is always awesome. - John Green" @@ -725,37 +725,37 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese editor_config_behaviors_description: "自動填入小括號、大括號以及引號。" about: -# main_title: "If you want to learn to program, you need to write (a lot of) code." -# main_description: "At CodeCombat, our job is to make sure you're doing that with a smile on your face." -# mission_link: "Mission" -# team_link: "Team" -# story_link: "Story" -# press_link: "Press" -# mission_title: "Our mission: make programming accessible to every student on Earth." -# mission_description_1: "Programming is magic. It's the ability to create things from pure imagination. We started CodeCombat to give learners the feeling of wizardly power at their fingertips by using typed code." -# mission_description_2: "As it turns out, that enables them to learn faster too. WAY faster. It's like having a conversation instead of reading a manual. We want to bring that conversation to every school and to every student, because everyone should have the chance to learn the magic of programming." -# team_title: "Meet the CodeCombat team" -# team_values: "We value open and respectful dialog, where the best idea wins. Our decisions are grounded in customer research and our process is focused on delivering tangible results for them. Everyone is hands-on, from our CEO to our Github contributors, because we value growth and learning in our team." + main_title: "如果您想要學編程,你需要寫(一大堆)程式碼。"#"If you want to learn to program, you need to write (a lot of) code." + main_description: "在CodeCombat中,我們的職責是確保您笑著玩遊戲。" #"At CodeCombat, our job is to make sure you're doing that with a smile on your face." + mission_link: "任務"#"Mission" + team_link: "隊伍"#"Team" + story_link: "故事"#"Story" + press_link: "按"#"Press" + mission_title: "我們的任務為:讓世上每個學生都有機會能學會編程。" #"Our mission: make programming accessible to every student on Earth." + mission_description_1: "編程是一種魔法。它是一種從純粹的想像中創造東西的能力。我們營運CodeCombat來讓學習者可以透過鍵入程式碼在指尖感覺到巫師般的力量。" #"Programming is magic. It's the ability to create things from pure imagination. We started CodeCombat to give learners the feeling of wizardly power at their fingertips by using typed code." + mission_description_2: "它的出現也讓使用者學得更快,就像是與它對話而不是閱讀手冊一般。我們想將這種對話帶給每一個學校以及每一位學生,因為每個人都應該要有機會體會編程的魔力。 " #"As it turns out, that enables them to learn faster too. WAY faster. It's like having a conversation instead of reading a manual. We want to bring that conversation to every school and to every student, because everyone should have the chance to learn the magic of programming." + team_title: "認識CodeCombat團隊。"#"Meet the CodeCombat team" + team_values: "我們重視開放且互相尊重的對話,好的想法才會勝出。我們的抉擇建立於客戶調查上,且過程注重於將具體的結果交給客戶。從我們的CEO到我們的Github貢獻者,每一個人都會動手參與,因為我們傾向在團隊中學習成長。" #" We value open and respectful dialog, where the best idea wins. Our decisions are grounded in customer research and our process is focused on delivering tangible results for them. Everyone is hands-on, from our CEO to our Github contributors, because we value growth and learning in our team." nick_title: "程式員" # {change} - nick_blurb: "亢奮的Guru" + nick_blurb: "亢奮的咕嚕" matt_title: "共同創辦人" # {change} cat_title: "首席開卡設計師" # {change} cat_blurb: "氣宗" scott_title: "共同創辦人" # {change} scott_blurb: "理性至上" -# maka_title: "Customer Advocate" + maka_title: "客戶代言人" #"Customer Advocate" maka_blurb: "說書人" rob_title: "編譯工程師" # {change} rob_blurb: "編寫一些的程式碼" josh_c_title: "遊戲設計師" josh_c_blurb: "設計遊戲" -# robin_title: "UX Design & Research" -# robin_blurb: "Scaffolding" + robin_title: "使用者體驗研究與設計"#"UX Design & Research" + robin_blurb: "鷹架"#"Scaffolding" josh_title: "遊戲設計師" josh_blurb: "地面是熔岩" -# phoenix_title: "Software Engineer" -# nolan_title: "Territory Manager" -# elliot_title: "Partnership Manager" + phoenix_title: "軟體工程師"#"Software Engineer" + nolan_title: "區域經理"#"Territory Manager" + elliot_title: "合作經裡"#"Partnership Manager" retrostyle_title: "插畫師" retrostyle_blurb: "復古風格的遊戲" jose_title: "音樂" @@ -1566,41 +1566,41 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese ladder: please_login: "在參與對戰前請先登入。" - my_matches: "我的對手" + my_matches: "我的對戰" simulate: "模擬" - simulation_explanation: "通過模擬遊戲,您可以使您的遊戲更快得到評分!" + simulation_explanation: "通過模擬遊戲,您可以使您的遊戲更快得到評定排名!" simulation_explanation_leagues: "你主要會為你的部落或者課程的同伴幫忙模擬遊戲。" simulate_games: "模擬遊戲!" games_simulated_by: "您模擬過的次數:" games_simulated_for: "替您模擬的次數:" games_in_queue: "遊戲正在列隊中:" - games_simulated: "遊戲已模擬" + games_simulated: "遊戲模擬次數" games_played: "玩過的遊戲" - ratio: "通過率" + ratio: "比率" leaderboard: "排行榜" battle_as: "我要扮演 " summary_your: "您的 " summary_matches: "對手 - " summary_wins: " 勝利," summary_losses: " 失敗" - rank_no_code: "沒有新程式碼可評分" - rank_my_game: "對我的遊戲評分!" + rank_no_code: "沒有新程式碼可評定排名" + rank_my_game: "對我的遊戲評定排名!" rank_submitting: "上傳中…" - rank_submitted: "已上傳以求評分" - rank_failed: "評分失敗" - rank_being_ranked: "已評分" + rank_submitted: "已上傳以求評定排名" + rank_failed: "評定失敗" + rank_being_ranked: "已評定" rank_last_submitted: "已上傳 " help_simulate: "幫我們模擬遊戲?" - code_being_simulated: "您的新程式碼正在被其他人模擬評分中。分數將隨每次新的配對而更新。" + code_being_simulated: "您的新程式碼正在被其他人模擬評定中。分數將隨每次新的配對而更新。" no_ranked_matches_pre: "對這個隊伍尚未有評分過的配對!" - no_ranked_matches_post: " 在別人的戰場上扮演對手並且回到這使您的程式碼接受評分。" + no_ranked_matches_post: " 在別人的戰場上扮演對手並且回到這使您的程式碼接受評定排名。" choose_opponent: "選擇對手" select_your_language: "選擇您的語言!" tutorial_play: "教學" tutorial_recommended: "如果您尚未玩過,建議先嘗試教學" tutorial_skip: "略過教學" tutorial_not_sure: "不確定發生啥事?" - tutorial_play_first: "先嘗試教學" + tutorial_play_first: "先試試教學" simple_ai: "簡單人工智慧" # {change} warmup: "熱身" friends_playing: "與朋友連線" @@ -1609,7 +1609,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese invite_friends_to_battle: "邀請您的朋友加入此戰鬥!" fight: "戰鬥!" watch_victory: "觀看您的勝利" - defeat_the: "擊敗" + defeat_the: "敗於" watch_battle: "觀看戰役" tournament_started: ",錦標賽已開始" tournament_ends: "錦標賽結束" From 06df1b9485789727430ba17504fc56eb44a9fc5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabr=C3=ADcio=20Matt=C3=A9?= Date: Mon, 30 May 2016 13:33:42 -0300 Subject: [PATCH 028/167] i18n pt-BR: add translations for code keywords (#3694) --- app/locale/pt-BR.coffee | 98 ++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/app/locale/pt-BR.coffee b/app/locale/pt-BR.coffee index f97047076..421065e9a 100644 --- a/app/locale/pt-BR.coffee +++ b/app/locale/pt-BR.coffee @@ -182,55 +182,55 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: campaign_old_multiplayer: "(Depreciado) Velhas Arenas Multijogador" campaign_old_multiplayer_description: "Reliquias de uma era mais civilizada.Nenhuma simulação roda nessa antiguidade ,arenas multijogador sem heróis." -# code: -# if: "if" # Keywords--these translations show up on hover, so please translate them all, even if it's kind of long. (In the code editor, they will still be in English.) -# else: "else" -# elif: "else if" -# while: "while" -# loop: "loop" -# for: "for" -# break: "break" -# continue: "continue" -# pass: "pass" -# return: "return" -# then: "then" -# do: "do" -# end: "end" -# function: "function" -# def: "define" -# var: "variable" -# self: "self" -# hero: "hero" -# this: "this" -# or: "or" -# "||": "or" -# and: "and" -# "&&": "and" -# not: "not" -# "!": "not" -# "=": "assign" -# "==": "equals" -# "===": "strictly equals" -# "!=": "does not equal" -# "!==": "does not strictly equal" -# ">": "is greater than" -# ">=": "is greater than or equal" -# "<": "is less than" -# "<=": "is less than or equal" -# "*": "multiplied by" -# "/": "divided by" -# "+": "plus" -# "-": "minus" -# "+=": "add and assign" -# "-=": "subtract and assign" -# True: "True" -# true: "true" -# False: "False" -# false: "false" -# undefined: "undefined" -# null: "null" -# nil: "nil" -# None: "None" + code: + if: "se" # Keywords--these translations show up on hover, so please translate them all, even if it's kind of long. (In the code editor, they will still be in English.) + else: "senão" + elif: "senão se" + while: "enquanto" + loop: "repetir" + for: "para" + break: "parar" + continue: "continuar" + pass: "passar" + return: "devolver" + then: "então" + do: "fazer" + end: "fim" + function: "função" + def: "definir" + var: "variável" + self: "próprio" + hero: "herói" + this: "isto" + or: "ou" + "||": "ou" + and: "e" + "&&": "e" + not: "não" + "!": "não" + "=": "atribuir" + "==": "é igual a" + "===": "é estritamente igual a" + "!=": "não é igual a" + "!==": "não é estritamente igual a" + ">": "é maior do que" + ">=": "é maior do que ou igual a" + "<": "é menor do que" + "<=": "é menor do que ou igual a" + "*": "multiplicado por" + "/": "dividido por" + "+": "mais" + "-": "menos" + "+=": "adicionar e atribuir" + "-=": "subtrair e atribuir" + True: "Verdadeiro" + true: "verdadeiro" + False: "Falso" + false: "falso" + undefined: "não definido" + null: "nulo" + nil: "nada" + None: "Nenhum" share_progress_modal: blurb: "Você está fazendo bastante progresso! Compartilhe com alguém o quanto você já aprendeu com o CodeCombat!" # {change} From fde005b2c88e0298080fa244f87b2b37af474bbf Mon Sep 17 00:00:00 2001 From: Louis Robinson Date: Mon, 30 May 2016 12:39:38 -0400 Subject: [PATCH 029/167] Added more names for #53 (#3692) --- app/lib/world/names.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/lib/world/names.coffee b/app/lib/world/names.coffee index ee446841d..2e0671c37 100644 --- a/app/lib/world/names.coffee +++ b/app/lib/world/names.coffee @@ -3,6 +3,7 @@ module.exports.thangNames = thangNames = # Female 'Alali' 'Anabel' + 'Delma' 'Dosha' 'Gurzunn' 'Hoot' @@ -22,6 +23,7 @@ module.exports.thangNames = thangNames = # Male 'Blob' 'Brack' + 'Cragg' 'Dobo' 'Draff' 'Eugen' @@ -72,6 +74,7 @@ module.exports.thangNames = thangNames = 'Pinakin' 'Rakash' 'Rasha' + 'Savatha' 'Vujii' 'Wuda' 'Yetu' @@ -108,6 +111,7 @@ module.exports.thangNames = thangNames = ] 'Ogre Warlock': [ # Male + 'Gronak' 'Sorgoth' 'Vax' 'Vyrryx' @@ -148,6 +152,7 @@ module.exports.thangNames = thangNames = 'Raven Pet': [ # Animal 'Nevermore' + 'Poe' ] 'Cougar Pet': [ # Animal @@ -606,6 +611,7 @@ module.exports.thangNames = thangNames = 'Hugo' 'Ieyasu' 'Jack' + 'Jackson' 'James' 'Jason' 'Jax' From 252b9a04487b8a4a550f03f2797144030168408a Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Mon, 30 May 2016 10:02:51 -0700 Subject: [PATCH 030/167] Pets should only have one name --- app/lib/world/names.coffee | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/app/lib/world/names.coffee b/app/lib/world/names.coffee index 2e0671c37..62332cd5c 100644 --- a/app/lib/world/names.coffee +++ b/app/lib/world/names.coffee @@ -149,32 +149,14 @@ module.exports.thangNames = thangNames = 'Ofgar' 'Randall' ] - 'Raven Pet': [ - # Animal - 'Nevermore' - 'Poe' - ] - 'Cougar Pet': [ - # Animal - 'Kitty' - ] - 'Frog Pet': [ - # Animal - 'Bighead' - 'Bufo' - 'Bunda' - 'Dan\'l Webster' - 'Freddy' - 'Frogger' - 'Froggy' - 'Hypnotoad' - 'Mr. Toad' - 'Slippy' - 'Toada' - 'Trevor' - 'Wart' - 'Wei Qi' - ] + # Pets only have one name + 'Raven Pet': ['Nevermore'] + 'Cougar Pet': ['Kitty'] + 'Frog Pet': ['Hypnotoad'] + 'Griffin Pet': [''] + 'Pugicorn Pet': [''] + 'Polar Bear Pet': [''] + 'Wolf Pet': [''] 'Horse': [ # Animal 'Abby' From c0e1f5fc1c97751515c761d2965aad3c049d49d9 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Mon, 30 May 2016 10:23:17 -0700 Subject: [PATCH 031/167] Fix arrows zooming in from (0, 0) in real-time playback mode --- app/lib/world/thang_state.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/world/thang_state.coffee b/app/lib/world/thang_state.coffee index 67830eded..a27adf3ac 100644 --- a/app/lib/world/thang_state.coffee +++ b/app/lib/world/thang_state.coffee @@ -108,7 +108,7 @@ module.exports = class ThangState storage = @trackedPropertyValues[propIndex] value = @getStoredProp propIndex, type, storage if prop is 'pos' - if @thang.teleport and @thang.pos.distanceSquared(value) > 900 + if (@thang.teleport and @thang.pos.distanceSquared(value) > 900) or (@thang.pos.x is 0 and @thang.pos.y is 0) # Don't interpolate; it was probably a teleport. https://github.com/codecombat/codecombat/issues/738 @thang.pos = value else From 92e2eaa4cde68f29772cd9f918ec486d9044c37f Mon Sep 17 00:00:00 2001 From: Imperadeiro98 Date: Mon, 30 May 2016 20:24:24 +0100 Subject: [PATCH 032/167] Fix some i18n files (#3696) Uncomment headers and change translation --- app/locale/es-ES.coffee | 4 ++-- app/locale/pt-PT.coffee | 2 +- app/locale/sr.coffee | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/locale/es-ES.coffee b/app/locale/es-ES.coffee index 60ac50d36..e8c44ba79 100644 --- a/app/locale/es-ES.coffee +++ b/app/locale/es-ES.coffee @@ -1711,7 +1711,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis # you_can2: "purchase a prepaid code" # you_can3: "that can be applied to your own account or given to others." -# coppa_deny: + coppa_deny: text1: "¿No puedes esperar para empezar a programar?" text2: "¡Pide a tus padres que hagan una cuenta para ti!" close: "Cerrar ventana" @@ -1869,7 +1869,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis license: "licencia" oreilly: "ebook de tu eleccion" -# calendar: + calendar: year: "Año" day: "Día" month: "Mes" diff --git a/app/locale/pt-PT.coffee b/app/locale/pt-PT.coffee index 7b92c77a3..f87134184 100644 --- a/app/locale/pt-PT.coffee +++ b/app/locale/pt-PT.coffee @@ -187,7 +187,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: else: "senão" elif: "senão se" while: "enquanto" - loop: "circular" + loop: "repetir" for: "para" break: "parar" continue: "continuar" diff --git a/app/locale/sr.coffee b/app/locale/sr.coffee index 1c608a1ee..b7ec6630e 100644 --- a/app/locale/sr.coffee +++ b/app/locale/sr.coffee @@ -675,7 +675,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian level_to_unlock: "Ниво који треба откључати:" # Label for which level you have to beat to unlock a particular hero (click a locked hero in the store to see) restricted_to_certain_heroes: "Само одређени хероји могу да играју овај ниво." -# skill_docs: + skill_docs: function: "функција" # skill types method: "метод" snippet: "исечак" @@ -938,7 +938,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian password_mismatch: "Шифре се не слажу." password_repeat: "Понови своју шифру." -# keyboard_shortcuts: + keyboard_shortcuts: keyboard_shortcuts: "Пречице за тастатуру" space: "Space" enter: "Enter" @@ -959,7 +959,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # beautify: "Beautify your code by standardizing its formatting." # maximize_editor: "Maximize/minimize code editor." -# community: + community: main_title: "CodeCombat Заједница" # introduction: "Check out the ways you can get involved below and decide what sounds the most fun. We look forward to working with you!" # level_editor_prefix: "Use the CodeCombat" From 5f90866efd67d77e2648cb2fb98af5733d2488b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?RUSLAN=20L=C3=93PEZ=20CARRO?= Date: Mon, 30 May 2016 21:56:33 +0200 Subject: [PATCH 033/167] Update es-419.coffee (#3695) --- app/locale/es-419.coffee | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/app/locale/es-419.coffee b/app/locale/es-419.coffee index 9db2d07b9..214d73177 100644 --- a/app/locale/es-419.coffee +++ b/app/locale/es-419.coffee @@ -82,7 +82,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip # get_started: "Get Started" students: "Estudiantes:" join_class: "Unirse a una clase" -# role: "Your role:" + role: "Su rol:" student_count: "Número de estudiantes:" start_playing_for_free: "Empieza a jugar gratis!" students_and_players: "Estudiantes y Jugadores" @@ -102,7 +102,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip blog: "Blog" forum: "Foro" account: "Cuenta" -# my_account: "My Account" + my_account: "Mi Cuenta" profile: "Perfil" stats: "Estadísticas" code: "Código" @@ -112,7 +112,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip about: "Acerca" contact: "Contacto" twitter_follow: "Seguir" -# students: "Students" + students: "Estudiantes" teachers: "Maestros" careers: "Carreras" facebook: "Facebook" @@ -125,8 +125,8 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip schools: "Escuelas" # educator_wiki: "Educator Wiki" get_involved: "Participa" -# open_source: "Open source (GitHub)" -# support: "Support" + open_source: "Código Libre (GitHub)" + support: "Soporte" # faqs: "FAQs" help_pref: "¿Necesitas ayuda? Envianos un correó electronico!" help_suff: "y te contactaremos pronto" @@ -271,7 +271,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip # connected_facebook_p: "Finish signing up so you can log in with your Facebook account." # facebook_exists: "You already have an account associated with Facebook!" # hey_students: "Students, enter the class code from your teacher." -# birthday: "Birthday" + birthday: "Compleaños" recover: recover_account_title: "recuperar cuenta" @@ -341,7 +341,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip subject: "Asunto" email: "Email" password: "Contraseña" -# confirm_password: "Confirm Password" + confirm_password: "Confirme Contraseña" message: "Mensaje" code: "Código" ladder: "Escalera" @@ -425,7 +425,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip victory_new_item: "Objeto Nuevo" victory_viking_code_school: "¡Changos macacos!, el nivel que acabas de pasar era dificil! Si todavía no eres un desarrollador de software, deberías serlo. Acabas de conseguir una aceptación por vía rápida con la Escuela Vikinga de Có, donde tú puedes llevar tus habilidades al siguiente nivel y convertirteen un desarrollador web profesional en 14 semanas." victory_become_a_viking: "Conviértete en un Vikingo" -# victory_no_progress_for_teachers: "Progress is not saved for teachers. But, you can add a student account to your classroom for yourself." +# victory_no_progress_for_teachers: "El progreso no es guardado para maestros. But, you can add a student account to your classroom for yourself." guide_title: "Guía" tome_cast_button_run: "Ejecutar" tome_cast_button_running: "Ejecutando" @@ -585,7 +585,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip feature5: "Video tutoriales" feature6: "Soporte Premium vía email" feature7: "Clan Privado" -# feature8: "No ads!" + feature8: "¡Sin publicidad!" free: "Gratis" month: "mes" must_be_logged: "Debes iniciar sesión primero. Por favor crea una cuenta o inicia sesión desde el menú superior." @@ -753,7 +753,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip robin_blurb: "Scaffolding" josh_title: "Diseñador de Juegos" josh_blurb: "El piso es Lava" -# phoenix_title: "Software Engineer" + phoenix_title: "Ingeniero de Software" # nolan_title: "Territory Manager" # elliot_title: "Partnership Manager" retrostyle_title: "Ilustración" @@ -825,7 +825,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip phone_number: "Teléfono" phone_number_help: "¿Dónde podemos contactarte durante el horario de trabajo ?" # primary_role_label: "Your Primary Role" -# role_default: "Select Role" + role_default: "Seleccione Rol" # primary_role_default: "Select Primary Role" # purchaser_role_default: "Select Purchaser Role" tech_coordinator: "Coordinador de tecnología" @@ -865,7 +865,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip # create_account: "Create a Teacher Account" # create_account_subtitle: "Get access to teacher-only tools for using CodeCombat in the classroom. Set up a class, add your students, and monitor their progress!" # convert_account_title: "Update to Teacher Account" -# not: "Not" + not: "No" # setup_a_class: "Set Up a Class" versions: @@ -1368,8 +1368,8 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip # total_enrolled_students: "Total Enrolled Students" # unenrolled_students: "Unenrolled Students" # add_enrollment_credits: "Add Enrollment Credits" -# purchasing: "Purchasing..." -# purchased: "Purchased!" + purchasing: "Adquiriendo..." + purchased: "Adquirido!" # purchase_now: "Purchase Now" # how_to_enroll: "How to Enroll Students" # how_to_enroll_blurb_1: "If a student is not enrolled yet, there will be an \"Enroll\" button next to their course progress in your class." @@ -1398,7 +1398,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip ambassador_title: "Embajador" ambassador_title_description: "(Soporte)" ambassador_summary: "Ayuda a responder las preguntas de los usuarios del foro. Nuestros Embajadores representan CodeCombat en todo el mundo." -# teacher_title: "Teacher" + teacher_title: "Maestro" editor: main_title: "Editor de CodeCombat" @@ -1714,7 +1714,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip # coppa_deny: # text1: "Can’t wait to learn programming?" # text2: "Ask your parents to create an account for you!" -# close: "Close Window" +# close: "Cerrar ventana" loading_error: could_not_load: "Error cargando del servidor" @@ -1870,9 +1870,9 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip oreilly: "ebook de su elección" # calendar: -# year: "Year" -# day: "Day" -# month: "Month" + year: "Año" + day: "Día" + month: "Mes" # january: "January" # february: "February" # march: "March" From ac3ef36a212c607e198c4b7c2943771c17e8316a Mon Sep 17 00:00:00 2001 From: Imperadeiro98 Date: Mon, 30 May 2016 20:57:48 +0100 Subject: [PATCH 034/167] Fix es-419.coffee --- app/locale/es-419.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/locale/es-419.coffee b/app/locale/es-419.coffee index 214d73177..a85bab647 100644 --- a/app/locale/es-419.coffee +++ b/app/locale/es-419.coffee @@ -1294,7 +1294,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip # class_name: "Class Name" # teacher_account_restricted: "Your account is a teacher account, and so cannot access student content." -# teacher: + teacher: # teacher_dashboard: "Teacher Dashboard" # Navbar # my_classes: "My Classes" # courses: "Courses" @@ -1711,7 +1711,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip you_can2: "comprar un código prepagado" you_can3: "que puede ser aplicado a tu propia cuenta o regalado a otros." -# coppa_deny: + coppa_deny: # text1: "Can’t wait to learn programming?" # text2: "Ask your parents to create an account for you!" # close: "Cerrar ventana" @@ -1869,7 +1869,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip license: "licencia" oreilly: "ebook de su elección" -# calendar: + calendar: year: "Año" day: "Día" month: "Mes" From f1ace99dad1e83f4fcd6d4d126117e40ef6ee287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Duy=20Tr=E1=BA=A7n?= Date: Tue, 31 May 2016 02:58:11 +0700 Subject: [PATCH 035/167] Vi translation (#3691) * Translate into Vietnamese * Translate and edit some Vietnamese strings * update Vietnamese translate * try to fix some error * fix bugs with tab character * update vi.coffee: play_level * Update vi.coffee * update vi.coffee * update text on Home page * Vietnamese: update legal * vi.coffee: edit community page * vi.coffee: update user's page * vi.coffee: about page * update vi.coffee * update vi.coffee * vi.coffee: invoices * vi.coffee: account_prepaid * update vi.coffee * vi.coffee: error strings * Update vi.coffee * update vi.coffee * vi.coffee: update contact form * vi.coffee: minor update * vi.coffee: update about page, modify some strings * update vi.coffee * vi.coffee: minor update * no message * Update vi strings, fix minor bug in page /community * vi.coffee: update Adventure and Artisan * vi.coffee: update Adventurer * vi.coffee: reverse changes * vi.coffee: reverse changes * vi.coffee: update some strings * vi.cofffe: update Diplomat * vi.coffee: update Diplomat * vi.coffee: update descriptions of classes * vi.coffee: update Ambassador * remove .cleafix div, will fix in another branch * refactor ClansView #3138 #3488 * use initialize() in ClansView #3138 #3488 * update vi.coffee * update vi.coffee * exclude changes on ClansView * vi.coffee: update play screen * vi.coffee: update some strings * vi.coffee: update createClass form * vi.coffee: modify some strings on home page and teachers page * minor fix --- app/locale/vi.coffee | 48 ++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/app/locale/vi.coffee b/app/locale/vi.coffee index fb407b1b3..cebf8dcc0 100644 --- a/app/locale/vi.coffee +++ b/app/locale/vi.coffee @@ -15,7 +15,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn or_ipad: "Hoặc tải phiên bản dành cho iPad" new_home: - slogan: "Tựa game hấp dẫn nhất để học lập trình." + slogan: "Trò chơi hấp dẫn nhất để học lập trình." classroom_edition: "Phiên bản Lớp học:" learn_to_code: "Học lập trình:" teacher: "Giáo viên" @@ -24,28 +24,28 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn im_a_teacher: "Tôi là Giáo viên" im_a_student: "Tôi là Học viên" learn_more: "Tìm hiểu thêm" - classroom_in_a_box: "Một lớp học đặc biệt cho bộ môn công nghệ thông tin." - codecombat_is: "CodeCombat là nền tảng cho học sinh tiếp cận với bộ môn công nghệ thông tin bằng cách chơi game." - our_courses: "Các khóa học của chúng tôi đã được thử nghiệm để triển khai trong lớp học, thậm chí được yêu thích bởi các giáo viên có ít hoặc không có kinh nghiệm lập trình." - top_screenshots_hint: "Học viên viết code và có thể cập nhật kết quả ngay lập tức" + classroom_in_a_box: "Một lớp học đặc biệt dành cho bộ môn công nghệ thông tin." + codecombat_is: "CodeCombat là nền tảng giúp cho học sinh tiếp cận với bộ môn công nghệ thông tin bằng cách chơi game." + our_courses: "Các khóa học của chúng tôi đã được thử nghiệm để triển khai trong các lớp học, thậm chí được sử dụng bởi các giáo viên có ít hoặc không có kinh nghiệm lập trình." + top_screenshots_hint: "Học viên viết code và cập nhật kết quả ngay lập tức" designed_with: "Được thiết kế để dạy học" real_code: "Thực tế, tự viết code" from_the_first_level: "ngay từ trình độ đầu tiên" getting_students: "Cho phép học viên viết code ngay từ đầu là vô cùng quan trọng trong việc làm quen cú pháp và học cấu trúc lập trình." educator_resources: "Xây dựng bài giảng" course_guides: "và các khóa học" - teaching_computer_science: "Việc dạy bộ môn công nghệ thông tin không nhất thiết yêu cầu một tấm bằng đắt tiền, bởi vì chúng tôi cung cấp sẵn công cụ dành cho người giảng dạy dù cho bạn là ai." + teaching_computer_science: "Việc giảng dạy bộ môn công nghệ thông tin không nhất thiết yêu cầu một tấm bằng đắt tiền, bởi vì chúng tôi cung cấp sẵn công cụ dành cho các giảng viên." accessible_to: "Tiếp cận dễ dàng" everyone: "cho mọi người" democratizing: "Xã hội hóa quá trình học lập trình là tôn chỉ của chúng tôi. Mọi người đều có thể học code." forgot_learning: "Tôi nghĩ học viên không để ý rằng họ thực sự đang học." - wanted_to_do: "Lập trình là điều mà em luôn muốn làm, và em không thể nghĩ rằng em có thể học nó ngay ở trường." + wanted_to_do: "Lập trình là điều mà em luôn muốn làm, em không thể nghĩ rằng em có thể học nó ngay tại trường." why_games: "Tại sao học thông qua chơi game lại quan trọng?" games_reward: "Game cổ vũ một cách tích cực." - encourage: "Game là môi trường khuyến khích sự tương tác, khám phá, và tinh thần không sợ thất bại. Một tựa game hay sẽ thử thách người chơi giúp họ dần dần hoàn thiện kĩ năng, điều này cũng giống như quá trình học tập của một học viên trong lớp học vậy." + encourage: "Game là môi trường khuyến khích sự tương tác, khám phá, và tinh thần không sợ thất bại. Một tựa game hay sẽ thử thách người chơi giúp họ dần dần hoàn thiện kĩ năng, điều này cũng giống như quá trình học tập của một học sinh trong lớp học vậy." excel: "Game cổ vũ" struggle: "những cố gắng tích cục" - kind_of_struggle: "những sự cố gắng trong học tập mà thực sự dem lại sự cuốn hút" + kind_of_struggle: "những sự cố gắng trong học tập mà thực sự dem lại sự cuốn hút, " motivating: "tạo động lực" not_tedious: "mà không hề buồn chán." gaming_is_good: "Nghiên cứu chứng minh chơi game tốt cho não bộ của trẻ. (đúng vậy đấy!)" @@ -56,7 +56,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn feedback: "Việc chơi game cũng giúp học sinh tương tác để có thể phát triển tư duy một cách toàn diện hơn, thay vì chỉ biết đến đáp án “đúng” hoặc “sai”." real_game: "Game thật, code thật." great_game: "Một tựa game hay không chỉ cần những huy chương hay thành tích hào nhoáng - quan trọng hơn hết là trải nghiệm của người chơi, những thử thách tinh tế, đồng thời là khả năng đối đầu và vượt qua thử thách." - agency: "CodeCombat là game đưa ra cho người chơi những thử thách buộc họ phải vượt qua bằng cách viết code, điều này giúp cho cả những người mới bắt đầu và những học viên trình độ nâng cao luyện tập khả năng lập trình thành thạo." + agency: "CodeCombat là trò chơi đưa ra cho người chơi những thử thách buộc họ phải vượt qua bằng cách viết code, điều này giúp cho cả những người mới bắt đầu và những học viên trình độ nâng cao luyện tập khả năng lập trình thành thạo." request_demo_title: "Bắt đầu giảng dạy ngay hôm nay!" request_demo_subtitle: "Yêu cầu demo và chuẩn bị sẵn sàng cho học viên của bạn chỉ trong vòng một tiếng đồng hồ." get_started_title: "Xây dựng lớp học của bạn ngay hôm nay" @@ -73,7 +73,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn lesson_time: "Thời gian học:" coming_soon: "Sắp ra mắt!" courses_available_in: "Các khóa học giảng dạy JavaScript, Python, và Java (sắp ra mắt!)" - boast: "Đưa ra những câu đố đủ khó để hấp dẫn cả game thủ và coder." + boast: "Đưa ra những câu đố đủ khó để hấp dẫn cả các game thủ và coder." winning: "Một sự kết hợp hoàn hảo giữa game RPG và bài tập lập trình, giúp việc giáo dục trẻ thú vị hơn bao giờ hết." run_class: "Mọi thứ bạn cần để tổ chức một lớp học công nghệ thông tin ngay hôm nay, không yêu cầu kinh nghiệm lập trình." teachers: "Giáo viên!" @@ -84,7 +84,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn join_class: "Tham gia Lớp học" role: "Vai trò của bạn:" student_count: "Số học viên:" - start_playing_for_free: "Bắt đầu Chơi Miễn phí!" + start_playing_for_free: "Bắt Đầu Chơi Miễn Phí!" students_and_players: "Học viên và Người chơi" goto_classes: "Tới Lớp học của tôi" # {change} view_profile: "Xem hồ sơ của tôi" @@ -259,7 +259,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn sign_up: "Đăng Ký" log_in: "đăng nhập với mật khẩu" required: "Bạn cần phải đăng nhập trước khi muốn tiếp tục." - login_switch: "Bạn đã có tài khoản rồi?" + login_switch: "Bạn đã có tài khoản?" school_name: "Tên Trường và Thành phố" optional: "không bắt buộc" school_name_placeholder: "Ví dụ tên Trường, Thành phố, (Tiểu bang nếu đang ở Mỹ)" @@ -1261,16 +1261,16 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn # students_enrolled: "students enrolled" # students_assigned: "students assigned" # length: "Length:" - title: "Tiêu đề" # Flat style redesign -# subtitle: "Review course guidelines, solutions, and levels" -# changelog: "View latest changes to course levels." -# select_language: "Select language" -# select_level: "Select level" -# play_level: "Play Level" -# concepts_covered: "Concepts covered" -# print_guide: "Print Guide (PDF)" -# view_guide_online: "View Guide Online (PDF)" -# last_updated: "Last updated:" + title: "Các khóa học" # Flat style redesign + subtitle: "Xem giáo án của các khóa học, lời giải và các màn chơi" + changelog: "Xem những thay đổi mới nhất về các khóa học." + select_language: "Chọn ngôn ngữ" + select_level: "Chọn màn chơi" + play_level: "Chơi Màn Chơi" + concepts_covered: "Bao gồm các khái niệm" + print_guide: "In Hướng Dẫn (PDF)" + view_guide_online: "Xem Hướng Dẫn Online (PDF)" + last_updated: "Cập nhật lần cuối:" # grants_lifetime_access: "Grants access to all Courses." # enrollment_credits_available: "Enrollment Credits Available:" description: "Mô tả" # ClassroomSettingsModal @@ -1292,7 +1292,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn student_age_range_to: "tới" create_class: "Tạo Lớp" class_name: "Tên lớp" -# teacher_account_restricted: "Your account is a teacher account, and so cannot access student content." + teacher_account_restricted: "Tài khoản của bạn là tài khoản Giáo Viên, vì thế bạn không thể truy cập nội dung dành cho học viên." teacher: teacher_dashboard: "Bảng Điều Khiển Giáo Viên" # Navbar From 1deddc6fd2dc5c49286c9ff75c98571fdbc68bc2 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Mon, 30 May 2016 13:51:09 -0700 Subject: [PATCH 036/167] Verifier now runs all solutions and lets you choose which campaigns and languages to skip. --- app/models/Level.coffee | 11 ++ .../editor/verifier/verifier-view.jade | 137 ++++++++++-------- app/views/editor/verifier/VerifierTest.coffee | 9 +- app/views/editor/verifier/VerifierView.coffee | 112 +++++++++----- 4 files changed, 165 insertions(+), 104 deletions(-) diff --git a/app/models/Level.coffee b/app/models/Level.coffee index 4c67779cb..b594e43ba 100644 --- a/app/models/Level.coffee +++ b/app/models/Level.coffee @@ -258,3 +258,14 @@ module.exports = class Level extends CocoModel else options.url = "/db/course/#{courseID}/levels/#{levelOriginalID}/next" @fetch(options) + + getSolutions: -> + return [] unless hero = _.find (@get("thangs") ? []), id: 'Hero Placeholder' + return [] unless config = _.find(hero.components ? [], (x) -> x.config?.programmableMethods?.plan)?.config + solutions = _.cloneDeep config.programmableMethods.plan.solutions ? [] + for solution in solutions + try + solution.source = _.template(solution.source)(config?.programmableMethods?.plan.context) + catch e + console.error "Problem with template and solution comments for", @get('slug'), e + solutions diff --git a/app/templates/editor/verifier/verifier-view.jade b/app/templates/editor/verifier/verifier-view.jade index a3a58c0eb..ff9c014e5 100644 --- a/app/templates/editor/verifier/verifier-view.jade +++ b/app/templates/editor/verifier/verifier-view.jade @@ -2,80 +2,101 @@ extends /templates/base-flat block content .container - div.row(style="margin-top: 20px") + div.row.verifier-row div.col-sm-3 - p.alert.alert-success(style="padding: 5px") + p.alert.alert-success | Passed: #{view.passed} div.col-sm-3 - p.alert.alert-warning(style="padding: 5px") + p.alert.alert-warning | Test Problem: #{view.problem} div.col-sm-3 - p.alert.alert-danger(style="padding: 5px") + p.alert.alert-danger | Failed: #{view.failed} div.col-sm-3 - p.alert.alert-info(style="padding: 5px") + p.alert.alert-info | To Run: #{view.testCount - view.passed - view.problem - view.failed} - if view.levelIDs + if view.levelsByCampaign + .form.form-inline + .row + each campaignInfo, campaign in view.levelsByCampaign + .form-group.campaign-mix + - var campaignID = "campaign-" + campaign + "-checkbox"; + input(id=campaignID, type="checkbox", checked=campaignInfo.checked, disabled=!!view.tests) + label(for=campaignID)= campaign + ': ' + campaignInfo.levels.length + .row + each codeLanguage in view.codeLanguages + .form-group.code-language-mix + - var codeLanguageID = "code-language-" + codeLanguage.id + "-checkbox"; + input(id=codeLanguageID, type="checkbox", checked=codeLanguage.checked, disabled=!!view.tests) + label(for=codeLanguageID)= codeLanguage.id + .pull-right + button.btn.btn-primary#go-button(disabled=!!view.tests) Start Tests + + if view.levelsToLoad && !view.tests .progress - .progress-bar.progress-bar-success(role="progressbar" style="width: #{100*view.passed/view.testCount}%") - .progress-bar.progress-bar-warning(role="progressbar" style="width: #{100*view.problem/view.testCount}%") - .progress-bar.progress-bar-danger(role="progressbar" style="width: #{100*view.failed/view.testCount}%") + .progress-bar.progress-bar-success(role="progressbar" style="width: #{100*(1 - view.levelsToLoad/view.initialLevelsToLoad)}%") + if view.tests + if view.levelIDs + .progress + .progress-bar.progress-bar-success(role="progressbar" style="width: #{100*view.passed/view.testCount}%") + .progress-bar.progress-bar-warning(role="progressbar" style="width: #{100*view.problem/view.testCount}%") + .progress-bar.progress-bar-danger(role="progressbar" style="width: #{100*view.failed/view.testCount}%") - each test, id in view.tests - - if (test.state == 'no-solution') - - continue; - if test.level - .pull-right - - var last = test.level.get('slug') + view.linksQueryString - a.btn.btn-primary(href="/editor/verifier/" + last) Focus - a.btn.btn-success(href="/play/level/" + last) Play - a.btn.btn-warning(href="/editor/level/" + last) Edit - a.btn.btn-default(data-target='#verifier-test-' + id, data-toggle="collapse") Toggle + each test, id in view.tests + - if (test.state == 'no-solution') + - continue; + if test.level + .pull-right + - var last = test.level.get('slug') + view.linksQueryString + a.btn.btn-primary(href="/editor/verifier/" + last) Focus + a.btn.btn-success(href="/play/level/" + last) Play + a.btn.btn-warning(href="/editor/level/" + last) Edit + a.btn.btn-default(data-target='#verifier-test-' + id, data-toggle="collapse") Toggle - if !test.goals - h2(style='color: orange')= test.level.get('name') - small= ' in ' + test.language + '' - else if test.isSuccessful() - h2(style='color: green')= test.level.get('name') - small= ' in ' + test.language + '' - else - h2(style='color: red')= test.level.get('name') - small= ' in ' + test.language + '' + if !test.goals + h2.test-running= test.level.get('name') + small= ' in ' + test.language + '' + else if test.isSuccessful() + h2.test-success= test.level.get('name') + small= ' in ' + test.language + '' + else + h2.test-failed= test.level.get('name') + small= ' in ' + test.language + '' - div.row(class=(test.isSuccessful() && id > 1 ? 'collapse' : 'collapse in'), id='verifier-test-' + id) - div.col-xs-8 - if test.solution - pre #{test.solution.source} - else - h4 Error Loading Test - pre #{test.error} - div.col-xs-4.well - if test.goals - if test.frames == test.solution.frameCount - div(style='color: green') ✓ Frames: #{test.frames} + div.row(class=(test.isSuccessful() && id > 1 ? 'collapse' : 'collapse in'), id='verifier-test-' + id) + div.col-xs-8 + if test.solution + pre #{test.solution.source} 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}) + h4 Error Loading Test + pre #{test.error} + div.col-xs-4.well + if test.goals + if test.frames == test.solution.frameCount + div.test-success ✓ Frames: #{test.frames} else - div(style='color: red') ✘ #{k} (#{v.status} vs #{test.solution.goals[k]}) - else - h3 Pending.... + div.test-failed ✘ Frames: #{test.frames} vs #{test.solution.frameCount} - if test.error - pre(style="color: red") #{test.error} - - if test.userCodeProblems.length - h4(style="color: red") User Code Problems - pre(style="color: red") #{JSON.stringify(test.userCodeProblems, null, 2)} + each v,k in test.goals || [] + if !test.solution.goals + div.test-running ? #{k} (#{v.status}) + else if v.status == test.solution.goals[k] + div.test-success ✓ #{k} (#{v.status}) + else + div.test-failed ✘ #{k} (#{v.status} vs #{test.solution.goals[k]}) + else + h3 Pending.... - else - h1 Loading Level... + if test.error + pre.test-faile #{test.error} - // TODO: show last frame hash + if test.userCodeProblems.length + h4.test-failed User Code Problems + pre.test-failed #{JSON.stringify(test.userCodeProblems, null, 2)} + + else + h1 Loading Level... + + // TODO: show last frame hash diff --git a/app/views/editor/verifier/VerifierTest.coffee b/app/views/editor/verifier/VerifierTest.coffee index 024c4f3ba..0826cad0c 100644 --- a/app/views/editor/verifier/VerifierTest.coffee +++ b/app/views/editor/verifier/VerifierTest.coffee @@ -42,15 +42,8 @@ module.exports = class VerifierTest extends CocoClass @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" - config = _.find(hero.components, (x) -> x.config?.programmableMethods?.plan).config - programmable = config.programmableMethods.plan - solution = _.find (programmable.solutions ? []), language: session.get('codeLanguage') - solution.source = _.template(solution.source)(config?.programmableMethods?.plan.context) - session.solution = solution - + session.solution = _.find level.getSolutions(), language: session.get('codeLanguage') session.set 'heroConfig', session.solution.heroConfig session.set 'code', {'hero-placeholder': plan: session.solution.source} state = session.get 'state' diff --git a/app/views/editor/verifier/VerifierView.coffee b/app/views/editor/verifier/VerifierView.coffee index d3a618c7a..80dbfb019 100644 --- a/app/views/editor/verifier/VerifierView.coffee +++ b/app/views/editor/verifier/VerifierView.coffee @@ -4,66 +4,102 @@ RootView = require 'views/core/RootView' template = require 'templates/editor/verifier/verifier-view' VerifierTest = require './VerifierTest' SuperModel = require 'models/SuperModel' +Campaigns = require 'collections/Campaigns' +Level = require 'models/Level' module.exports = class VerifierView extends RootView className: 'style-flat' template: template id: 'verifier-view' + events: + 'click #go-button': 'onClickGoButton' + constructor: (options, @levelID) -> super options # TODO: sort tests by unexpected result first @passed = 0 @failed = 0 @problem = 0 + @testCount = 0 - 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', - 'breakout', 'attack-wisely', 'kithgard-mastery', 'kithgard-apprentice', 'robot-ragnarok', - 'defense-of-plainswood', 'peasant-protection', 'forest-fire-dancing', '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', 'munchkin-swarm', - 'stillness-in-motion', 'the-agrippa-defense', 'backwoods-bombardier', 'coinucopia', 'copper-meadows', - 'drop-the-flag', 'mind-the-trap', 'signal-corpse', 'rich-forager', + if @levelID + @levelIDs = [@levelID] + @testLanguages = ['python', 'javascript', 'java', 'lua', 'coffeescript'] + @startTestingLevels() + else + @campaigns = new Campaigns() + @supermodel.trackRequest @campaigns.fetch(data: {project: 'slug,type,levels'}) + @campaigns.comparator = (m) -> + ['intro', 'course-2', 'course-3', 'course-4', 'course-5', 'course-6', 'course-8', + 'dungeon', 'forest', 'desert', 'mountain', 'glacier', 'volcano'].indexOf(m.get('slug')) - 'the-mighty-sand-yak', 'oasis', 'sarven-road', 'sarven-gaps', 'thunderhooves', 'minesweeper', - 'medical-attention', 'sarven-sentry', 'keeping-time', 'hoarding-gold', 'decoy-drill', 'continuous-alchemy', - 'dust', 'desert-combat', 'sarven-savior', 'lurkers', 'preferential-treatment', 'sarven-shepherd', - 'shine-getter', + onLoaded: -> + super() + return if @levelID + @filterCampaigns() + @filterCodeLanguages() + @render() - 'a-fine-mint', 'borrowed-sword', 'cloudrip-commander', 'crag-tag', - 'hunters-and-prey', 'hunting-party', - 'leave-it-to-cleaver', 'library-tactician', 'mad-maxer', 'mad-maxer-strikes-back', - 'mirage-maker', 'mixed-unit-tactics', 'mountain-mercenaries', - 'noble-sacrifice', 'odd-sandstorm', 'ogre-gorge-gouger', 'reaping-fire', - 'return-to-thornbush-farm', 'ring-bearer', 'sand-snakes', - 'slalom', 'steelclaw-gap', 'the-geometry-of-flowers', - 'the-two-flowers', 'timber-guard', 'toil-and-trouble', 'village-rover', - 'vital-powers', 'zoo-keeper', - ] + filterCampaigns: -> + @levelsByCampaign = {} + for campaign in @campaigns.models when campaign.get('type') in ['course', 'hero'] and campaign.get('slug') isnt 'picoctf' + @levelsByCampaign[campaign.get('slug')] ?= {levels: [], checked: true} + campaignInfo = @levelsByCampaign[campaign.get('slug')] + for levelID, level of campaign.get('levels') when level.type not in ['hero-ladder', 'course-ladder', 'game-dev'] + campaignInfo.levels.push level.slug + filterCodeLanguages: -> + defaultLanguages = utils.getQueryVariable('languages', 'python,javascript').split(/, ?/) + @codeLanguages ?= ({id: c, checked: c in defaultLanguages} for c in ['python', 'javascript', 'java', 'lua', 'coffeescript']) + + onClickGoButton: (e) -> + @filterCampaigns() + @levelIDs = [] + for campaign, campaignInfo of @levelsByCampaign + if @$("#campaign-#{campaign}-checkbox").is(':checked') + for level in campaignInfo.levels + @levelIDs.push level unless level in @levelIDs + else + campaignInfo.checked = false + @testLanguages = [] + for codeLanguage in @codeLanguages + if @$("#code-language-#{codeLanguage.id}-checkbox").is(':checked') + codeLanguage.checked = true + @testLanguages.push codeLanguage.id + else + codeLanguage.checked = false + @startTestingLevels() + + startTestingLevels: -> + @levelsToLoad = @initialLevelsToLoad = @levelIDs.length + for levelID in @levelIDs + level = @supermodel.getModel(Level, levelID) or new Level _id: levelID + if level.loaded + @onLevelLoaded() + else + @listenToOnce @supermodel.loadModel(level).model, 'sync', @onLevelLoaded + + onLevelLoaded: (level) -> + if --@levelsToLoad is 0 + @onTestLevelsLoaded() + else + @render() + + onTestLevelsLoaded: -> defaultCores = 2 cores = Math.max(window.navigator.hardwareConcurrency, defaultCores) - #testLevels = testLevels.slice 0, 15 @linksQueryString = window.location.search - @levelIDs = if @levelID then [@levelID] else testLevels - languages = utils.getQueryVariable 'languages', 'python,javascript' #supermodel = if @levelID then @supermodel else undefined @tests = [] - @taskList = [] - @tasksList = _.flatten _.map @levelIDs, (v) -> - # TODO: offer good interface for choosing which languages, better performance for skipping missing solutions - #_.map ['python', 'javascript', 'coffeescript', 'lua'], (l) -> - _.map languages.split(','), (l) -> - #_.map ['javascript'], (l) -> - level: v, language: l + @tasksList = [] + for levelID in @levelIDs + level = @supermodel.getModel(Level, levelID) + solutions = level?.getSolutions() + for codeLanguage in @testLanguages + if not solutions or _.find(solutions, language: codeLanguage) + @tasksList.push level: levelID, language: codeLanguage @testCount = @tasksList.length chunks = _.groupBy @tasksList, (v,i) -> i%cores From fccc33da5953b4e97810d0da23806f72dd90894f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?RUSLAN=20L=C3=93PEZ=20CARRO?= Date: Mon, 30 May 2016 16:42:53 -0500 Subject: [PATCH 037/167] Update es-419.coffee --- app/locale/es-419.coffee | 78 ++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/app/locale/es-419.coffee b/app/locale/es-419.coffee index a85bab647..ca9855ad7 100644 --- a/app/locale/es-419.coffee +++ b/app/locale/es-419.coffee @@ -27,7 +27,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip classroom_in_a_box: "Un aula prediseñada para enseñar informática." codecombat_is: "CodeCombat es una plataforma para que los estudiantes aprendan informática mientras están jugando un juego de verdad." # {change} our_courses: "Nuestros cursos fueron probados especificamente para destacarse en el aula, incluso con maestros con poca o ninguna experiencia previa en programación." # {change} -# top_screenshots_hint: "Students write code and see their changes update in real-time" +# top_screenshots_hint: "Los estudiantes write code and see their changes update in real-time" designed_with: "Diseñado teniendo en cuenta a los maestros" real_code: "Escritura real de código" from_the_first_level: "desde el primer nivel" @@ -57,12 +57,12 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip real_game: "Un juego de verdad, jugado con código auténtico." great_game: "Un gran juego es más que solo medallas y logros - se trata sobre una travesía del jugador, acertijos bien diseñados, y la habilidad de abordar desafios con voluntad y confianza." agency: "CodeCombat es un juego que ofrece a los jugadores esa voluntad y confianza mediante nuestro motor de código robusto, que ayuda tanto a principiantes como a estudiantes avanzados por igual a escribir código correcto y válido." -# request_demo_title: "Get your students started today!" +# request_demo_title: "Get your students started hoy!" # request_demo_subtitle: "Request a demo and get your students started in less than an hour." -# get_started_title: "Set up your class today" +# get_started_title: "Set up your clase hoy" # get_started_subtitle: "Set up a class, add your students, and monitor their progress as they learn computer science." request_demo: "Solicitar un Demo" -# setup_a_class: "Set Up a Class" +# setup_a_class: "Set Up una Clase" have_an_account: "¿Ya tenes una cuenta?" # {change} logged_in_as: "Estás actualmente conectado como" view_my_classes: "Ver mis clases" @@ -79,7 +79,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip teachers: "Maestros!" teachers_and_educators: "Maestros y Educadores" # class_in_box: "Learn how our classroom-in-a-box platform fits into your curriculum." -# get_started: "Get Started" + get_started: "Iniciarse" students: "Estudiantes:" join_class: "Unirse a una clase" role: "Su rol:" @@ -265,12 +265,12 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip school_name_placeholder: "Ejemplo: High School, Springfield, IL" or_sign_up_with: "o conectate a travez de:" connected_gplus_header: "Te haz conectado exitosamente a travez de Google+!" -# connected_gplus_p: "Finish signing up so you can log in with your Google+ account." -# gplus_exists: "You already have an account associated with Google+!" +# connected_gplus_p: "Finish signing up so you can log in con your Google+ cuenta." +# gplus_exists: "You already have an cuenta associated con Google+!" connected_facebook_header: "Te haz conectado exitosamente a travez de Facebook!" -# connected_facebook_p: "Finish signing up so you can log in with your Facebook account." -# facebook_exists: "You already have an account associated with Facebook!" -# hey_students: "Students, enter the class code from your teacher." +# connected_facebook_p: "Finish signing up so you can log in con your Facebook cuenta." +# facebook_exists: "You already have an cuenta associated con Facebook!" +# hey_students: "Estudiantes, enter the class code from your maestro." birthday: "Compleaños" recover: @@ -509,17 +509,17 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip tip_understand_recursion: "La única manera de entender la recursividad es entender la recursividad." tip_life_and_polymorphism: "El Open Source es como una estructura heterogénea totalmente polimórfica: Todos los tipos son bienvenidos." tip_mistakes_proof_of_trying: "Errores en tu código son solo evidencia de que estas intentando." -# tip_adding_orgres: "Rounding up ogres." -# tip_sharpening_swords: "Sharpening the swords." +# tip_adding_orgres: "Rounding up ogros." + tip_sharpening_swords: "Afilando las espadas." # tip_ratatouille: "You must not let anyone define your limits because of where you come from. Your only limit is your soul. - Gusteau, Ratatouille" -# tip_nemo: "When life gets you down, want to know what you've gotta do? Just keep swimming, just keep swimming. - Dory, Finding Nemo" +# tip_nemo: "Cuando la vida gets you down, want to know what you've gotta do? Just keep swimming, just keep swimming. - 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_luna_lovegood: "No te preocupes, 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_programming_not_about_computers: "La ciencia cpomputacional is no more about computers than astronomy is about telescopes. - Edsger Dijkstra" + tip_mulan: "Cree que puedes, y entonces lo harás. - Mulan" game_menu: inventory_tab: "Inventario" @@ -676,13 +676,13 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip restricted_to_certain_heroes: "Sólo ciertos héroes pueden jugar este nivel." skill_docs: -# function: "function" # skill types -# method: "method" + function: "función" # skill types + method: "método" # snippet: "snippet" -# number: "number" -# array: "array" -# object: "object" -# string: "string" + number: "número" + array: "arreglo" + object: "objeto" + string: "cadena" writable: "escribible" # Hover over "attack" in Your Skills while playing a level to see most of this read_only: "Sólo Lectura" action: "Acción" @@ -754,8 +754,8 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip josh_title: "Diseñador de Juegos" josh_blurb: "El piso es Lava" phoenix_title: "Ingeniero de Software" -# nolan_title: "Territory Manager" -# elliot_title: "Partnership Manager" + nolan_title: "Administrador de territorio" +# elliot_title: "Administrador de Partnership" retrostyle_title: "Ilustración" retrostyle_blurb: "Juegos con estilo Retro" jose_title: "Música" @@ -785,7 +785,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip jobs_benefit_4: "Días personales/enfermo ilimitados" # {change} jobs_benefit_5: "Desarrollo profesional y soporte de formación continua" # {change} jobs_benefit_6: "Seguro médico/dental/vista" # {change} -# jobs_benefit_7: "Sit-stand desks for all" +# jobs_benefit_7: "Sit-stand desks para todos" # jobs_benefit_9: "10-year option exercise window" # jobs_benefit_10: "Maternity leave: 10 weeks paid, next 6 @ 55% salary" # jobs_benefit_11: "Paternity leave: 10 weeks paid" @@ -824,10 +824,10 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip email_exists: "Existe un usuario con este email." phone_number: "Teléfono" phone_number_help: "¿Dónde podemos contactarte durante el horario de trabajo ?" -# primary_role_label: "Your Primary Role" + primary_role_label: "Tu Rol Primario" role_default: "Seleccione Rol" -# primary_role_default: "Select Primary Role" -# purchaser_role_default: "Select Purchaser Role" + primary_role_default: "Seleccione Rol Primario" +# purchaser_role_default: "Seleccione Purchaser Role" tech_coordinator: "Coordinador de tecnología" advisor: "Tutor" principal: "Director" @@ -835,15 +835,15 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip parent: "Padre" # purchaser_role_label: "Your Purchaser Role" # influence_advocate: "Influence/Advocate" -# evaluate_recommend: "Evaluate/Recommend" -# approve_funds: "Approve Funds" + evaluate_recommend: "Evaluar/Recomendar" + approve_funds: "Aprobar Fondos" # no_purchaser_role: "No role in purchase decisions" organization_label: "Escuela / Distrito" city: "Ciudad" state: "Estado" country: "País" num_students_help: "¿Cuántos prevees inscribir en CodeCombat?" -# num_students_default: "Select Range" + num_students_default: "Seleccionar Rango" education_level_label: "Nivel Educativo de los Estudiantes" education_level_help: "Elegí todas las que correspondan." elementary_school: "Escuela Primaria" @@ -853,16 +853,16 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip college_plus: "Universidad o superior" anything_else: "¿Algo más que deberíamos saber?" thanks_header: "¡Gracias por solicitar un presupuesto!" # {change} -# thanks_sub_header: "Thanks for expressing interest in CodeCombat for your school." +# thanks_sub_header: "Gracias por expressing interest in CodeCombat for your school." thanks_p: "Estaremos en contacto pronto. ¿Preguntas? Escríbenos:" # {change} # back_to_classes: "Back to Classes" # finish_signup: "Finish creating your teacher account:" # finish_signup_p: "Create an account to set up a class, add your students, and monitor their progress as they learn computer science." -# signup_with: "Sign up with:" -# connect_with: "Connect with:" +# signup_with: "Sign up con:" + connect_with: "Conectar con:" # conversion_warning: "WARNING: Your current account is a Student Account. Once you submit this form, your account will be updated to a Teacher Account." # learn_more_modal: "Teacher accounts on CodeCombat have the ability to monitor student progress, assign enrollments and manage classrooms. Teacher accounts cannot be a part of a classroom - if you are currently enrolled in a class using this account, you will no longer be able to access it once you update to a Teacher Account." -# create_account: "Create a Teacher Account" + create_account: "Crear una Cuenta de maestro" # create_account_subtitle: "Get access to teacher-only tools for using CodeCombat in the classroom. Set up a class, add your students, and monitor their progress!" # convert_account_title: "Update to Teacher Account" not: "No" @@ -1511,7 +1511,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip join_desc_3: ", o encuéntranos en " join_desc_4: "y ahí empezaremos!" join_url_email: "Escríbenos" -# join_url_slack: "public Slack channel" +# join_url_slack: "public Slack canal" archmage_subscribe_desc: "Obten correos de nuevas oportunidades y anuncios." artisan_introduction_pref: "¡Debemos construir niveles adicionales! La gente ruega por más contenido, y podemos hacer tanto por nosotros mismos. De momento tu estación de trabajo es nivel 1 ; Nuestro editor de niveles es apenas útil incluso para sus creadores, así que sea cauteloso. Si tuviera visiones de campañas apareciendo para ciclos for" artisan_introduction_suf: ", entonces esta lase es ideal para ti." @@ -1712,9 +1712,9 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip you_can3: "que puede ser aplicado a tu propia cuenta o regalado a otros." coppa_deny: -# text1: "Can’t wait to learn programming?" + text1: "¿No puedes esperar para aprender a programar?" # text2: "Ask your parents to create an account for you!" -# close: "Cerrar ventana" + close: "Cerrar ventana" loading_error: could_not_load: "Error cargando del servidor" @@ -1884,4 +1884,4 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip # september: "September" # october: "October" # november: "November" -# december: "December" + december: "Diciembre" From 85e38ba8512bdecfd00daa787d401fd20bc46c78 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Mon, 30 May 2016 16:03:00 -0700 Subject: [PATCH 038/167] Add the stylesheet for the previous commit --- app/styles/editor/verifier/verifier-view.sass | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 app/styles/editor/verifier/verifier-view.sass diff --git a/app/styles/editor/verifier/verifier-view.sass b/app/styles/editor/verifier/verifier-view.sass new file mode 100644 index 000000000..4eb18abd9 --- /dev/null +++ b/app/styles/editor/verifier/verifier-view.sass @@ -0,0 +1,21 @@ +#verifier-view + .verifier-row + margin-top: 20px + + .alert + padding: 5px + + .campaign-mix, .code-language-mix + padding: 5px 20px 5px 5px + + input + margin-right: 8px + + .test-running + color: orange + + .test-success + color: green + + .test-failed + color: red From 904d58a8ef7f863a6bea1ca72640a53828131fc3 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Mon, 30 May 2016 16:09:55 -0700 Subject: [PATCH 039/167] Fix player-code-immutable vector methods with new Esper --- app/lib/world/vector.coffee | 55 ++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/app/lib/world/vector.coffee b/app/lib/world/vector.coffee index b5f7ffc12..c8b8cbda0 100644 --- a/app/lib/world/vector.coffee +++ b/app/lib/world/vector.coffee @@ -5,14 +5,14 @@ class Vector for name in ['add', 'subtract', 'multiply', 'divide', 'limit', 'normalize', 'rotate'] do (name) -> Vector[name] = (a, b, useZ) -> - a.copy()["#{name}Self"](b, useZ) + a.copy()[name](b, useZ) for name in ['magnitude', 'heading', 'distance', 'dot', 'equals', 'copy', 'distanceSquared'] do (name) -> Vector[name] = (a, b, useZ) -> a[name](b, useZ) isVector: true - apiProperties: ['x', 'y', 'z', 'magnitude', 'heading', 'distance', 'dot', 'equals', 'copy', 'distanceSquared', 'rotate', 'add', 'subtract', 'multiply', 'divide', 'limit', 'normalize', 'rotate'] + apiProperties: ['x', 'y', 'z', 'magnitude', 'heading', 'distance', 'dot', 'equals', 'copy', 'distanceSquared', 'add', 'subtract', 'multiply', 'divide', 'limit', 'normalize', 'rotate'] constructor: (x=0, y=0, z=0) -> return new Vector x, y, z unless @ instanceof Vector @@ -24,68 +24,67 @@ class Vector # Mutating methods: - normalizeSelf: (useZ) -> + normalize: (useZ) -> m = @magnitude useZ - @divideSelf m, useZ if m > 0 + @divide m, useZ if m > 0 @ - normalize: (useZ) -> - # Hack to detect when we are in player code so we can avoid mutation - (if @__aetherAPIValue? then @copy() else @).normalizeSelf(useZ) + esper_normalize: (useZ) -> + @copy().normalize(useZ) - limitSelf: (max) -> + limit: (max) -> if @magnitude() > max - @normalizeSelf() - @multiplySelf(max) + @normalize() + @multiply(max) else @ - limit: (useZ) -> - (if @__aetherAPIValue? then @copy() else @).limitSelf(useZ) + esper_limit: (max) -> + @copy().limit(max) - subtractSelf: (other, useZ) -> + subtract: (other, useZ) -> @x -= other.x @y -= other.y @z -= other.z if useZ @ - subtract: (other, useZ) -> - (if @__aetherAPIValue? then @copy() else @).subtractSelf(other, useZ) + esper_subtract: (other, useZ) -> + @copy().subtract(other, useZ) - addSelf: (other, useZ) -> + add: (other, useZ) -> @x += other.x @y += other.y @z += other.z if useZ @ - add: (other, useZ) -> - (if @__aetherAPIValue? then @copy() else @).addSelf(other, useZ) + esper_add: (other, useZ) -> + @copy().add(other, useZ) - divideSelf: (n, useZ) -> + divide: (n, useZ) -> [@x, @y] = [@x / n, @y / n] @z = @z / n if useZ @ - divide: (n, useZ) -> - (if @__aetherAPIValue? then @copy() else @).divideSelf(n, useZ) + esper_divide: (n, useZ) -> + @copy().divide(n, useZ) - multiplySelf: (n, useZ) -> + multiply: (n, useZ) -> [@x, @y] = [@x * n, @y * n] @z = @z * n if useZ @ - multiply: (n, useZ) -> - (if @__aetherAPIValue? then @copy() else @).multiplySelf(n, useZ) + esper_multiply: (n, useZ) -> + @copy().multiply(n, useZ) # Rotate it around the origin # If we ever want to make this also use z: https://en.wikipedia.org/wiki/Axes_conventions - rotateSelf: (theta) -> + rotate: (theta) -> return @ unless theta [@x, @y] = [Math.cos(theta) * @x - Math.sin(theta) * @y, Math.sin(theta) * @x + Math.cos(theta) * @y] @ - rotate: (theta) -> - (if @__aetherAPIValue? then @copy() else @).rotateSelf(theta) + esper_rotate: (theta) -> + @copy().rotate(theta) # Non-mutating methods: @@ -127,7 +126,7 @@ class Vector # Not the strict projection, the other isn't converted to a unit vector first. projectOnto: (other, useZ) -> - other.copy().multiplySelf(@dot(other, useZ), useZ) + other.copy().multiply(@dot(other, useZ), useZ) isZero: (useZ) -> result = @x is 0 and @y is 0 From 065069cfdde2798edbf081598788ae6c2ae36bb9 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Mon, 30 May 2016 16:26:55 -0700 Subject: [PATCH 040/167] Make CourseVictoryModal respect Markdown in next level description --- app/templates/play/level/modal/progress-view.jade | 2 +- app/views/play/level/modal/ProgressView.coffee | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/templates/play/level/modal/progress-view.jade b/app/templates/play/level/modal/progress-view.jade index c5f56051c..5dd7c00ca 100644 --- a/app/templates/play/level/modal/progress-view.jade +++ b/app/templates/play/level/modal/progress-view.jade @@ -38,7 +38,7 @@ span : h2.text-uppercase= i18n(view.nextLevel.attributes, 'name') - p= i18n(view.nextLevel.attributes, 'description') + div!= view.nextLevelDescription .row .col-sm-5.col-sm-offset-2 diff --git a/app/views/play/level/modal/ProgressView.coffee b/app/views/play/level/modal/ProgressView.coffee index 14afaf48f..7653e850f 100644 --- a/app/views/play/level/modal/ProgressView.coffee +++ b/app/views/play/level/modal/ProgressView.coffee @@ -1,7 +1,8 @@ CocoView = require 'views/core/CocoView' +utils = require 'core/utils' module.exports = class ProgressView extends CocoView - + id: 'progress-view' className: 'modal-content' template: require 'templates/play/level/modal/progress-view' @@ -16,9 +17,12 @@ module.exports = class ProgressView extends CocoView @classroom = options.classroom @nextLevel = options.nextLevel @levelSessions = options.levelSessions + # Translate and Markdownify level description, but take out any images (we don't have room for arena banners, etc.). + # Images in Markdown are like ![description](url) + @nextLevelDescription = marked(utils.i18n(@nextLevel.attributes, 'description').replace(/!\[.*?\]\(.*?\)\n*/g, '')) onClickDoneButton: -> @trigger 'done' onClickNextLevelButton: -> - @trigger 'next-level' \ No newline at end of file + @trigger 'next-level' From c00f436ab54e212e2debe3b56033d31567cc2823 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Mon, 30 May 2016 16:33:00 -0700 Subject: [PATCH 041/167] Show to-be-deleted student info in RemoveStudentModal --- app/templates/courses/remove-student-modal.jade | 1 + 1 file changed, 1 insertion(+) diff --git a/app/templates/courses/remove-student-modal.jade b/app/templates/courses/remove-student-modal.jade index 2b96a05e5..c831df2f7 100644 --- a/app/templates/courses/remove-student-modal.jade +++ b/app/templates/courses/remove-student-modal.jade @@ -4,6 +4,7 @@ block modal-header-content .text-center h1.modal-title(data-i18n="courses.remove_student1") span.glyphicon.glyphicon-warning-sign.text-danger + p= view.user.get('name', true) + ' - ' + view.user.get('email') h2(data-i18n="courses.are_you_sure") block modal-body-content From 6f63de5ec8dd4ac8d1d9474ad3a3b7e21e43a742 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Mon, 30 May 2016 16:45:33 -0700 Subject: [PATCH 042/167] Improve contrast of name labels against their backgrounds --- app/lib/surface/Label.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/lib/surface/Label.coffee b/app/lib/surface/Label.coffee index e4b6f1f2d..1e07d4f76 100644 --- a/app/lib/surface/Label.coffee +++ b/app/lib/surface/Label.coffee @@ -74,16 +74,16 @@ module.exports = class Label extends CocoClass o.marginY = {D: 6, S: 4, N: 3}[st] o.fontWeight = {D: 'bold', S: 'bold', N: 'bold'}[st] o.shadow = {D: false, S: true, N: true}[st] - o.shadowColor = {D: '#FFF', S: '#000', N: '#FFF'}[st] + o.shadowColor = {D: '#FFF', S: '#000', N: '#000'}[st] o.fontSize = {D: 25, S: 12, N: 24}[st] fontFamily = {D: 'Arial', S: 'Arial', N: 'Arial'}[st] o.fontDescriptor = "#{o.fontWeight} #{o.fontSize}px #{fontFamily}" - o.fontColor = {D: '#000', S: '#FFF', N: '#0a0'}[st] + o.fontColor = {D: '#000', S: '#FFF', N: '#6c6'}[st] if @style is 'name' and @sprite?.thang?.team is 'humans' - o.fontColor = '#a00' + o.fontColor = '#c66' else if @style is 'name' and @sprite?.thang?.team is 'ogres' - o.fontColor = '#00a' - o.backgroundFillColor = {D: 'white', S: 'rgba(0,0,0,0.4)', N: 'rgba(255,255,255,0.5)'}[st] + o.fontColor = '#66c' + o.backgroundFillColor = {D: 'white', S: 'rgba(0,0,0,0.4)', N: 'rgba(0,0,0,0.5)'}[st] o.backgroundStrokeColor = {D: 'black', S: 'rgba(0,0,0,0.6)', N: 'rgba(0,0,0,0)'}[st] o.backgroundStrokeStyle = {D: 2, S: 1, N: 1}[st] o.backgroundBorderRadius = {D: 10, S: 3, N: 3}[st] From 88a07b5c090f1fb4b2801ec9b5e1a2e9f124dd31 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Mon, 30 May 2016 17:08:11 -0700 Subject: [PATCH 043/167] Destroy vim mode option --- app/locale/en.coffee | 3 --- app/schemas/models/user.coffee | 4 ++-- app/templates/play/menu/options-view.jade | 8 -------- app/views/play/menu/OptionsView.coffee | 5 ++--- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/app/locale/en.coffee b/app/locale/en.coffee index f7ebe2c6e..fd844fb3c 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -711,9 +711,6 @@ music_label: "Music" music_description: "Turn background music on/off." editor_config_title: "Editor Configuration" - editor_config_keybindings_label: "Key Bindings" - editor_config_keybindings_default: "Default (Ace)" - editor_config_keybindings_description: "Adds additional shortcuts known from the common editors." editor_config_livecompletion_label: "Live Autocompletion" editor_config_livecompletion_description: "Displays autocomplete suggestions while typing." editor_config_invisibles_label: "Show Invisibles" diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index 3b1799786..625d4b2f0 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -121,7 +121,7 @@ _.extend UserSchema.properties, aceConfig: c.object { default: { language: 'python', keyBindings: 'default', invisibles: false, indentGuides: false, behaviors: false, liveCompletion: true }}, language: {type: 'string', 'enum': ['python', 'javascript', 'coffeescript', 'clojure', 'lua', 'java', 'io']} - keyBindings: {type: 'string', 'enum': ['default', 'vim', 'emacs']} + keyBindings: {type: 'string', 'enum': ['default', 'vim', 'emacs']} # Deprecated 2016-05-30; now we just always give them 'default'. invisibles: {type: 'boolean' } indentGuides: {type: 'boolean' } behaviors: {type: 'boolean' } @@ -337,7 +337,7 @@ _.extend UserSchema.properties, } } enrollmentRequestSent: { type: 'boolean' } - + schoolName: {type: 'string'} role: {type: 'string', enum: ["God", "advisor", "parent", "principal", "student", "superintendent", "teacher", "technology coordinator"]} birthday: c.stringDate({title: "Birthday"}) diff --git a/app/templates/play/menu/options-view.jade b/app/templates/play/menu/options-view.jade index 451d960df..ad4126e08 100644 --- a/app/templates/play/menu/options-view.jade +++ b/app/templates/play/menu/options-view.jade @@ -26,14 +26,6 @@ h3(data-i18n="options.editor_config_title") Editor Configuration - .form-group.select-group - label.control-label(for="option-key-bindings", data-i18n="options.editor_config_keybindings_label") Key Bindings - select#option-key-bindings.form-control(name="keyBindings") - option(value="default", selected=(aceConfig.keyBindings === "default"), data-i18n="options.editor_config_keybindings_default") Default (Ace) - option(value="vim", selected=(aceConfig.keyBindings === "vim")) Vim - option(value="emacs", selected=(aceConfig.keyBindings === "emacs")) Emacs - span.help-block(data-i18n="options.editor_config_keybindings_description") Adds additional shortcuts known from the common editors. - .form-group.checkbox label(for="option-live-completion") input#option-live-completion(name="liveCompletion", type="checkbox", checked=aceConfig.liveCompletion) diff --git a/app/views/play/menu/OptionsView.coffee b/app/views/play/menu/OptionsView.coffee index 205057065..5bd6285e4 100644 --- a/app/views/play/menu/OptionsView.coffee +++ b/app/views/play/menu/OptionsView.coffee @@ -20,8 +20,7 @@ module.exports = class OptionsView extends CocoView events: 'change #option-music': 'updateMusic' - 'change #option-key-bindings': 'updateInvisibles' - 'change #option-key-bindings': 'updateKeyBindings' + 'change #option-invisibles': 'updateInvisibles' 'change #option-indent-guides': 'updateIndentGuides' 'change #option-behaviors': 'updateBehaviors' 'change #option-live-completion': 'updateLiveCompletion' @@ -67,7 +66,7 @@ module.exports = class OptionsView extends CocoView if @playerName and @playerName isnt me.get('name') me.set 'name', @playerName @aceConfig.invisibles = @$el.find('#option-invisibles').prop('checked') - @aceConfig.keyBindings = @$el.find('#option-key-bindings').val() + @aceConfig.keyBindings = 'default' # We used to give them the option, but we took it away. @aceConfig.indentGuides = @$el.find('#option-indent-guides').prop('checked') @aceConfig.behaviors = @$el.find('#option-behaviors').prop('checked') @aceConfig.liveCompletion = @$el.find('#option-live-completion').prop('checked') From 96e6e0de9e362d2778ef43633968889aa8330783 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Mon, 30 May 2016 17:33:15 -0700 Subject: [PATCH 044/167] Fix #2889: show incremental gem and xp award for replayable levels, not total --- app/views/play/level/modal/HeroVictoryModal.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee index 594953f7c..b01808aa3 100644 --- a/app/views/play/level/modal/HeroVictoryModal.coffee +++ b/app/views/play/level/modal/HeroVictoryModal.coffee @@ -292,7 +292,7 @@ module.exports = class HeroVictoryModal extends ModalView duration = 1000 ratio = @getEaseRatio (new Date() - @sequentialAnimationStart), duration if panel.unit is 'xp' - newXP = Math.floor(panel.previousNumber + ratio * (panel.number - panel.previousNumber)) + newXP = Math.floor(ratio * (panel.number - panel.previousNumber)) totalXP = @totalXPAnimated + newXP if totalXP isnt @lastTotalXP panel.textEl.text('+' + newXP) @@ -304,7 +304,7 @@ module.exports = class HeroVictoryModal extends ModalView @XPEl.addClass 'five-digits' if totalXP >= 10000 and @lastTotalXP < 10000 @lastTotalXP = totalXP else if panel.unit is 'gem' - newGems = Math.floor(panel.previousNumber + ratio * (panel.number - panel.previousNumber)) + newGems = Math.floor(ratio * (panel.number - panel.previousNumber)) totalGems = @totalGemsAnimated + newGems if totalGems isnt @lastTotalGems panel.textEl.text('+' + newGems) @@ -326,9 +326,9 @@ module.exports = class HeroVictoryModal extends ModalView panel.rootEl.removeClass('animating').find('.reward-image-container img').removeClass('pulse') @sequentialAnimationStart = new Date() if panel.unit is 'xp' - @totalXPAnimated += panel.number + @totalXPAnimated += panel.number - panel.previousNumber else if panel.unit is 'gem' - @totalGemsAnimated += panel.number + @totalGemsAnimated += panel.number - panel.previousNumber @sequentialAnimatedPanels.shift() return panel.rootEl.addClass('animating').find('.reward-image-container').removeClass('pending-reward-image').find('img').addClass('pulse') From 4160058505654aa56c6129ffc39295fb302d1f29 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 31 May 2016 08:50:17 -0700 Subject: [PATCH 045/167] Make skipping real-time playback jump to final frame. Fix #2827. --- app/lib/surface/CountdownScreen.coffee | 6 +++--- app/views/play/level/PlayLevelView.coffee | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/lib/surface/CountdownScreen.coffee b/app/lib/surface/CountdownScreen.coffee index 47a52c7d6..c24abddb2 100644 --- a/app/lib/surface/CountdownScreen.coffee +++ b/app/lib/surface/CountdownScreen.coffee @@ -57,11 +57,11 @@ module.exports = class CountdownScreen extends CocoClass else @endCountdown() - hide: -> + hide: (duration=500) -> return unless @showing @showing = false createjs.Tween.removeTweens @dimLayer - createjs.Tween.get(@dimLayer).to({alpha: 0}, 500).call => @layer.removeChild @dimLayer unless @destroyed + createjs.Tween.get(@dimLayer).to({alpha: 0}, duration).call => @layer.removeChild @dimLayer unless @destroyed decrementCountdown: => return if @destroyed @@ -85,4 +85,4 @@ module.exports = class CountdownScreen extends CocoClass onRealTimePlaybackEnded: (e) -> clearInterval @countdownInterval if @countdownInterval @countdownInterval = null - @hide() + @hide Math.max(500, 1000 * (@secondsRemaining or 0)) diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index 9be13aa1f..7a100c50a 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -641,7 +641,7 @@ module.exports = class PlayLevelView extends RootView return unless @$el.hasClass 'real-time' @$el.removeClass 'real-time' @onWindowResize() - if @world.frames.length is @world.totalFrames + if @world.frames.length is @world.totalFrames and not @surface.countdownScreen?.showing _.delay @onSubmissionComplete, 750 # Wait for transition to end. else @waitingForSubmissionComplete = true @@ -649,6 +649,7 @@ module.exports = class PlayLevelView extends RootView onSubmissionComplete: => return if @destroyed + Backbone.Mediator.publish 'level:set-time', ratio: 1 return if @level.hasLocalChanges() # Don't award achievements when beating level changed in level editor # TODO: Show a victory dialog specific to hero-ladder level if @goalManager.checkOverallStatus() is 'success' and not @options.realTimeMultiplayerSessionID? From 2cbbeaaddf60b13fa2dd5dcbac76051360a322ba Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 31 May 2016 09:30:07 -0700 Subject: [PATCH 046/167] Fix #2720: serve default wizard over https --- server/handlers/user_handler.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/handlers/user_handler.coffee b/server/handlers/user_handler.coffee index 431e6dc93..455aa90bb 100644 --- a/server/handlers/user_handler.coffee +++ b/server/handlers/user_handler.coffee @@ -686,8 +686,8 @@ UserHandler = class UserHandler extends Handler buildGravatarURL: (user, size, fallback) -> emailHash = @buildEmailHash user - fallback ?= 'http://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png' - fallback = "http://codecombat.com#{fallback}" unless /^http/.test fallback + fallback ?= 'https://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png' + fallback = "https://codecombat.com#{fallback}" unless /^http/.test fallback "https://www.gravatar.com/avatar/#{emailHash}?s=#{size}&default=#{fallback}" buildEmailHash: (user) -> From e8f18f08f50e35871a94a96fe22d5a615bbe87cd Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 31 May 2016 09:44:38 -0700 Subject: [PATCH 047/167] Fix #3636: make CourseVictoryModal close button look clickable --- app/models/ThangType.coffee | 4 ++-- app/templates/play/level/modal/progress-view.jade | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/ThangType.coffee b/app/models/ThangType.coffee index 3aae60261..fe9ec9b1d 100644 --- a/app/models/ThangType.coffee +++ b/app/models/ThangType.coffee @@ -16,12 +16,12 @@ module.exports = class ThangType extends CocoModel samurai: '53e12be0d042f23505c3023b' raider: '55527eb0b8abf4ba1fe9a107' goliath: '55e1a6e876cb0948c96af9f8' - guardian: '' + guardian: '566a058620de41290036a745' ninja: '52fc0ed77e01835453bd8f6c' 'forest-archer': '5466d4f2417c8b48a9811e87' trapper: '5466d449417c8b48a9811e83' pixie: '' - assassin: '' + assassin: '566a2202e132c81f00f38c81' librarian: '52fbf74b7e01835453bd8d8e' 'potion-master': '52e9adf7427172ae56002172' sorcerer: '52fd1524c7e6cf99160e7bc9' diff --git a/app/templates/play/level/modal/progress-view.jade b/app/templates/play/level/modal/progress-view.jade index 5dd7c00ca..e6f2ff42e 100644 --- a/app/templates/play/level/modal/progress-view.jade +++ b/app/templates/play/level/modal/progress-view.jade @@ -1,5 +1,5 @@ .modal-header - #close-modal.well.well-sm.well-parchment(data-dismiss="modal") + #close-modal.btn.well.well-sm.well-parchment(data-dismiss="modal") span.glyphicon.glyphicon-remove .well.well-sm.well-parchment h1(data-i18n='play_level.level_complete') From 4f369a1e2f32ce9a84274f3da6478513bc30f215 Mon Sep 17 00:00:00 2001 From: duybkict Date: Thu, 26 May 2016 17:19:02 +0700 Subject: [PATCH 048/167] #3138 #3488 refactor UserView, MainUserView --- app/templates/common/user.jade | 8 +-- app/templates/user/main-user-view.jade | 49 ++++++++-------- app/views/common/UserView.coffee | 13 +---- app/views/user/MainUserView.coffee | 79 +++++++++++++------------- 4 files changed, 70 insertions(+), 79 deletions(-) diff --git a/app/templates/common/user.jade b/app/templates/common/user.jade index 1bcd19c1c..08b73409f 100644 --- a/app/templates/common/user.jade +++ b/app/templates/common/user.jade @@ -3,11 +3,11 @@ extends /templates/base // User pages might have some user page specific header, if not remove this block content .clearfix - if user && viewName + if view.userData && view.viewName ol.breadcrumb li - a(href="/user/#{user.getSlugOrID()}") #{user.displayName()} + a(href="/user/#{view.userData.getSlugOrID()}") #{view.userData.displayName()} li.active - | #{viewName} - if !user || user.loading + | #{view.viewName} + if !view.userData || view.userData.loading | LOADING diff --git a/app/templates/user/main-user-view.jade b/app/templates/user/main-user-view.jade index 494a23c73..fcd5f39bb 100644 --- a/app/templates/user/main-user-view.jade +++ b/app/templates/user/main-user-view.jade @@ -9,24 +9,25 @@ block append content a(href="/account", data-i18n="nav.account") li.active(data-i18n="nav.profile") - if user + if view.user + - var playerLevel = view.user.level() + - var emails = view.user.getEnabledEmails() .vertical-buffer .row .left-column .profile-wrapper - img.picture(src="#{user.getPhotoURL(150)}" alt="") + img.picture(src="#{view.user.getPhotoURL(150)}" alt="") div.profile-info - h3.name= user.get('name') - if favoriteLanguage + h3.name= view.user.get('name') + if view.favoriteLanguage div.extra-info span(data-i18n="user.favorite_prefix") Favorite language is - strong.favorite-language= favoriteLanguage - span(data-i18n="user.favorite_postfix") . + strong.favorite-language= view.favoriteLanguage + span(data-i18n="user.favorite_postfix") . if playerLevel div.extra-info span.spr(data-i18n="general.player_level") Level - strong= playerLevel - - var emails = user.getEnabledEmails() + strong= playerLevel // TODO: fix this, use some other method for finding contributor classes other than email settings, since they're private... Maybe achievements? if emails ul.contributor-categories @@ -64,22 +65,22 @@ block append content .panel.panel-default .panel-heading h3.panel-title(data-i18n="clans.clans") Clans - if (!clans) + if (!view.clanModels) .panel-body p(data-i18n="common.loading") - else if (clans.length) + else if (view.clanModels.length) table.table tr th.col-xs-4(data-i18n="clans.name") Name th.col-xs-4(data-i18n="clans.chieftain") Chieftain th.col-xs-4(data-i18n="play.heroes") Heroes - each clan in clans + each clan in view.clanModels tr td a(href="/clans/#{clan.id}")= clan.get('name') td - if idNameMap && idNameMap[clan.get('ownerID')] - a(href="/user/#{clan.get('ownerID')}")= idNameMap[clan.get('ownerID')] + if view.idNameMap && view.idNameMap[clan.get('ownerID')] + a(href="/user/#{clan.get('ownerID')}")= view.idNameMap[clan.get('ownerID')] else a(href="/user/#{clan.get('ownerID')}") Anonymous td= clan.get('members').length @@ -90,17 +91,17 @@ block append content .panel.panel-default .panel-heading h3.panel-title(data-i18n="user.singleplayer_title") Singleplayer Levels - if (!singlePlayerSessions) + if (!view.singlePlayerSessions) .panel-body p(data-i18n="common.loading") Loading... - else if (singlePlayerSessions.length) + else if (view.singlePlayerSessions.length) table.table tr th.col-xs-4(data-i18n="resources.level") Level th.col-xs-4(data-i18n="user.last_played") Last Played th.col-xs-4(data-i18n="user.status") Status - var count = 0 - each session, index in singlePlayerSessions + each session, index in view.singlePlayerSessions if session.get('levelName') tr(class=count > 4 ? 'hide' : '') - count++; @@ -120,16 +121,16 @@ block append content .panel.panel-default .panel-heading h3.panel-title(data-i18n="user.multiplayer_title") Multiplayer Levels - if (!multiPlayerSessions) + if (!view.multiPlayerSessions) .panel-body p(data-i18n="common.loading") Loading... - else if (multiPlayerSessions.length) + else if (view.multiPlayerSessions.length) table.table tr th.col-xs-4(data-i18n="resources.level") Level th.col-xs-4(data-i18n="user.last_played") Last Played th.col-xs-4(data-i18n="general.score") Score - each session, index in multiPlayerSessions + each session, index in view.multiPlayerSessions tr(class=index > 4 ? 'hide' : '') td - var posturl = '' @@ -140,7 +141,7 @@ block append content td= parseInt(session.get('totalScore') * 100) else td(data-i18n="user.status_unfinished") Unfinished - if multiPlayerSessions.length > 4 + if view.multiPlayerSessions.length > 4 .panel-footer button.btn.btn-info.btn-sm.more-button(data-i18n="editor.more") else @@ -149,10 +150,10 @@ block append content .panel.panel-default .panel-heading h3.panel-title(data-i18n="user.achievements_title") Achievements - if ! earnedAchievements + if ! view.earnedAchievements .panel-body p(data-i18n="common.loading") Loading... - else if ! earnedAchievements.length + else if ! view.earnedAchievements.length .panel-body p(data-i18n="user.no_achievements") No achievements earned so far. else @@ -161,7 +162,7 @@ block append content th.col-xs-4(data-i18n="achievements.achievement") Achievement th.col-xs-4(data-i18n="achievements.last_earned") Last Earned th.col-xs-4(data-i18n="achievements.amount_achieved") Amount - each achievement, index in earnedAchievements.models + each achievement, index in view.earnedAchievements.models tr(class=index > 4 ? 'hide' : '') td= achievement.get('achievementName') td= moment(achievement.get('changed')).format("MMMM Do YYYY") @@ -169,6 +170,6 @@ block append content td= achievement.get('achievedAmount') else td - if earnedAchievements.length > 4 + if view.earnedAchievements.length > 4 .panel-footer button.btn.btn-info.btn-sm.more-button(data-i18n="editor.more") diff --git a/app/views/common/UserView.coffee b/app/views/common/UserView.coffee index a9001af32..173acdf93 100644 --- a/app/views/common/UserView.coffee +++ b/app/views/common/UserView.coffee @@ -19,21 +19,12 @@ module.exports = class UserView extends RootView @user = new User _id: @userID @supermodel.loadModel @user, cache: false - getRenderData: -> - context = super() - context.viewName = @viewName - context.user = @user unless @user?.isAnonymous() - context - isMe: -> @userID in [me.id, me.get('slug')] onLoaded: -> - @onUserLoaded @user if @user.loaded and not @userLoaded - super() - - onUserLoaded: -> + @userData = @user unless @user?.isAnonymous() @userID = @user.id - @userLoaded = true + super() ifUserNotFound: -> console.warn 'user not found' diff --git a/app/views/user/MainUserView.coffee b/app/views/user/MainUserView.coffee index 37b343e2d..cd4bda48c 100644 --- a/app/views/user/MainUserView.coffee +++ b/app/views/user/MainUserView.coffee @@ -26,59 +26,39 @@ module.exports = class MainUserView extends UserView destroy: -> @stopListening?() - getRenderData: -> - context = super() - if @levelSessions and @levelSessions.loaded - singlePlayerSessions = [] - multiPlayerSessions = [] - languageCounts = {} - for levelSession in @levelSessions.models - if levelSession.isMultiplayer() - multiPlayerSessions.push levelSession - else - singlePlayerSessions.push levelSession - language = levelSession.get('codeLanguage') or levelSession.get('submittedCodeLanguage') - if language - languageCounts[language] = (languageCounts[language] or 0) + 1 - mostUsedCount = 0 - favoriteLanguage = null - for language, count of languageCounts - if count > mostUsedCount - mostUsedCount = count - favoriteLanguage = language - context.singlePlayerSessions = singlePlayerSessions - context.multiPlayerSessions = multiPlayerSessions - context.favoriteLanguage = favoriteLanguage - context.playerLevel = @user.level() - if @earnedAchievements and @earnedAchievements.loaded - context.earnedAchievements = @earnedAchievements - if @clans and @clans.loaded - context.clans = @clans.models - context.idNameMap = @idNameMap - context - onLoaded: -> - if @user.loaded and not (@earnedAchievements or @levelSessions) - @supermodel.resetProgress() - @levelSessions = new LevelSessionsCollection @user.getSlugOrID() - @earnedAchievements = new EarnedAchievementCollection @user.getSlugOrID() - @supermodel.loadCollection @levelSessions, 'levelSessions', {cache: false} - @supermodel.loadCollection @earnedAchievements, 'earnedAchievements', {cache: false} + if @user.loaded + if !@levelSessions + @levelSessions = new LevelSessionsCollection @user.getSlugOrID() + @listenTo @levelSessions, 'sync', => + @onSyncLevelSessions @levelSessions?.models + @render() + @supermodel.loadCollection @levelSessions, 'levelSessions', {cache: false} + + if !@earnedAchievements + @earnedAchievements = new EarnedAchievementCollection @user.getSlugOrID() + @listenTo @earnedAchievements, 'sync', => + @render() + @supermodel.loadCollection @earnedAchievements, 'earnedAchievements', {cache: false} + sortClanList = (a, b) -> if a.get('members').length isnt b.get('members').length if a.get('members').length < b.get('members').length then 1 else -1 else b.id.localeCompare(a.id) - @idNameMap = {} + @clans = new CocoCollection([], { url: "/db/user/#{@userID}/clans", model: Clan, comparator: sortClanList }) @listenTo @clans, 'sync', => - @refreshNameMap @clans?.models + @onSyncClans @clans?.models @render?() @supermodel.loadCollection(@clans, 'clans', {cache: false}) + super() - refreshNameMap: (clans) -> + onSyncClans: (clans) -> return unless clans? + @idNameMap = [] + @clanModels = clans options = url: '/db/user/-/names' method: 'POST' @@ -88,6 +68,25 @@ module.exports = class MainUserView extends UserView @render?() @supermodel.addRequestResource('user_names', options, 0).load() + onSyncLevelSessions: (levelSessions) -> + return unless levelSessions? + @multiPlayerSessions = [] + @singlePlayerSessions = [] + languageCounts = [] + mostUsedCount = 0 + for levelSession in levelSessions + if levelSession.isMultiplayer() + @multiPlayerSessions.push levelSession + else + @singlePlayerSessions.push levelSession + language = levelSession.get('codeLanguage') or levelSession.get('submittedCodeLanguage') + if language + languageCounts[language] = (languageCounts[language] or 0) + 1 + for language, count of languageCounts + if count > mostUsedCount + mostUsedCount = count + @favoriteLanguage = language + onClickMoreButton: (e) -> panel = $(e.target).closest('.panel') panel.find('tr.hide').removeClass('hide') From ffcd89d32774cce53aaf4b56c35a71b77a490f30 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Tue, 31 May 2016 10:12:20 -0700 Subject: [PATCH 049/167] Turn off video tutorials for course levels --- app/locale/es-419.coffee | 2 +- app/views/play/menu/GuideView.coffee | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/locale/es-419.coffee b/app/locale/es-419.coffee index a85bab647..239527f23 100644 --- a/app/locale/es-419.coffee +++ b/app/locale/es-419.coffee @@ -1711,7 +1711,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip you_can2: "comprar un código prepagado" you_can3: "que puede ser aplicado a tu propia cuenta o regalado a otros." - coppa_deny: +# coppa_deny: # text1: "Can’t wait to learn programming?" # text2: "Ask your parents to create an account for you!" # close: "Cerrar ventana" diff --git a/app/views/play/menu/GuideView.coffee b/app/views/play/menu/GuideView.coffee index 1d735349f..506e843ef 100644 --- a/app/views/play/menu/GuideView.coffee +++ b/app/views/play/menu/GuideView.coffee @@ -19,12 +19,13 @@ module.exports = class LevelGuideView extends CocoView @levelSlug = options.level.get('slug') @sessionID = options.session.get('_id') @requiresSubscription = not me.isPremium() - @helpVideos = options.level.get('helpVideos') ? [] + @isCourseLevel = options.level.get('type', true) in ['course', 'course-ladder'] + @helpVideos = if @isCourseLevel then [] else options.level.get('helpVideos') ? [] @trackedHelpVideoStart = @trackedHelpVideoFinish = false # A/B Testing video tutorial styles @helpVideosIndex = me.getVideoTutorialStylesIndex(@helpVideos.length) - @helpVideo = @helpVideos[@helpVideosIndex] if @helpVideos.length > 0 - @videoLocked = not (@helpVideo?.free or options.level.get('type', true) in ['course', 'course-ladder']) and @requiresSubscription + @helpVideo = @helpVideos[@helpVideosIndex] if @helpVideos.length > 0 and not @isCourseLevel + @videoLocked = not (@helpVideo?.free or @isCourseLevel) and @requiresSubscription @firstOnly = options.firstOnly @docs = options?.docs ? options.level.get('documentation') ? {} @@ -57,7 +58,7 @@ module.exports = class LevelGuideView extends CocoView getRenderData: -> c = super() c.docs = @docs - c.showVideo = @helpVideos.length > 0 + c.showVideo = @helpVideos.length > 0 unless @isCourseLevel c.videoLocked = @videoLocked c From fd8f65cadce26cc30ca5745e62efb27487b63422 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Tue, 31 May 2016 10:37:30 -0700 Subject: [PATCH 050/167] Help button should open "Overview" (Guide) by default --- app/views/play/menu/GuideView.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/play/menu/GuideView.coffee b/app/views/play/menu/GuideView.coffee index 1d735349f..ca7020a02 100644 --- a/app/views/play/menu/GuideView.coffee +++ b/app/views/play/menu/GuideView.coffee @@ -68,7 +68,7 @@ module.exports = class LevelGuideView extends CocoView if @helpVideos.length startingTab = 0 else - startingTab = _.findIndex @docs, slug: 'intro' + startingTab = _.findIndex @docs, slug: 'overview' startingTab = 0 if startingTab is -1 # incredible hackiness. Getting bootstrap tabs to work shouldn't be this complex @$el.find(".nav-tabs li:nth(#{startingTab})").addClass('active') From 5ff1c4b700c0043351668b0d618f3f69d9196df6 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 31 May 2016 10:48:16 -0700 Subject: [PATCH 051/167] Fix #3427: allow language-specific images in Markdown articles --- app/core/utils.coffee | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/core/utils.coffee b/app/core/utils.coffee index 72b007db3..afef39637 100644 --- a/app/core/utils.coffee +++ b/app/core/utils.coffee @@ -259,8 +259,11 @@ module.exports.filterMarkdownCodeLanguages = (text, language) -> return '' unless text currentLanguage = language or me.get('aceConfig')?.language or 'python' excludedLanguages = _.without ['javascript', 'python', 'coffeescript', 'clojure', 'lua', 'java', 'io'], currentLanguage - exclusionRegex = new RegExp "```(#{excludedLanguages.join('|')})\n[^`]+```\n?", 'gm' - text.replace exclusionRegex, '' + # Exclude language-specific code blocks like ```python (... code ...)``` for each non-target language. + codeBlockExclusionRegex = new RegExp "```(#{excludedLanguages.join('|')})\n[^`]+```\n?", 'gm' + # Exclude language-specific images like ![python - image description](image url) for each non-target language. + imageExclusionRegex = new RegExp "!\\[(#{excludedLanguages.join('|')}) - .+?\\]\\(.+?\\)\n?", 'gm' + return text.replace(codeBlockExclusionRegex, '').replace(imageExclusionRegex, '') module.exports.aceEditModes = aceEditModes = 'javascript': 'ace/mode/javascript' From da90ab980e04c0fe3ee2123499921b52a1945bd2 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 31 May 2016 10:59:06 -0700 Subject: [PATCH 052/167] Fix some failing tests --- app/views/play/level/modal/ProgressView.coffee | 3 ++- test/app/lib/world/vector.spec.coffee | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/play/level/modal/ProgressView.coffee b/app/views/play/level/modal/ProgressView.coffee index 7653e850f..552d28fe0 100644 --- a/app/views/play/level/modal/ProgressView.coffee +++ b/app/views/play/level/modal/ProgressView.coffee @@ -19,7 +19,8 @@ module.exports = class ProgressView extends CocoView @levelSessions = options.levelSessions # Translate and Markdownify level description, but take out any images (we don't have room for arena banners, etc.). # Images in Markdown are like ![description](url) - @nextLevelDescription = marked(utils.i18n(@nextLevel.attributes, 'description').replace(/!\[.*?\]\(.*?\)\n*/g, '')) + @nextLevel.get('description', true) # Make sure the defaults are available + @nextLevelDescription = marked(utils.i18n(@nextLevel.attributesWithDefaults, 'description').replace(/!\[.*?\]\(.*?\)\n*/g, '')) onClickDoneButton: -> @trigger 'done' diff --git a/test/app/lib/world/vector.spec.coffee b/test/app/lib/world/vector.spec.coffee index b12c3f0e9..2d0994648 100644 --- a/test/app/lib/world/vector.spec.coffee +++ b/test/app/lib/world/vector.spec.coffee @@ -50,11 +50,10 @@ describe 'Vector', -> expectEquivalentMethods 'equals', new Vector 7, 7 expectEquivalentMethods 'copy' - it "doesn't mutate when in player code", -> + xit "doesn't mutate when in player code", -> + # We can't run these tests easily because it depends on being in interpreter mode now expectNoMutation = (fn) -> v = new Vector 5, 5 - # player code detection hack depends on this property being != null - v.__aetherAPIValue = {} v2 = fn v expect(v.x).toEqual 5 expect(v).not.toBe v2 From f067337635414b0ca5be33972459c222324f018c Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 31 May 2016 11:21:55 -0700 Subject: [PATCH 053/167] Fix classroom student count --- app/views/courses/TeacherClassView.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee index bfb907bbb..ce4c99c76 100644 --- a/app/views/courses/TeacherClassView.coffee +++ b/app/views/courses/TeacherClassView.coffee @@ -127,7 +127,6 @@ module.exports = class TeacherClassView extends RootView @render() # Model/Collection events @listenTo @classroom, 'sync change update', -> - @removeDeletedStudents() classCode = @classroom.get('codeCamel') or @classroom.get('code') @state.set { classCode: classCode @@ -144,7 +143,6 @@ module.exports = class TeacherClassView extends RootView @listenTo @students, 'sync change update add remove reset', -> # Set state/props of things that depend on students? # Set specific parts of state based on the models, rather than just dumping the collection there? - @removeDeletedStudents() @calculateProgressAndLevels() classStats = @calculateClassStats() @state.set classStats: classStats if classStats From fd43e09abc7922155ebb9df0180c1083948b43ad Mon Sep 17 00:00:00 2001 From: Josh Callebaut Date: Tue, 31 May 2016 11:23:28 -0700 Subject: [PATCH 054/167] Fix #3350 --- app/styles/play/level/tome/spell.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/play/level/tome/spell.sass b/app/styles/play/level/tome/spell.sass index df05d8dab..e0e9bb894 100644 --- a/app/styles/play/level/tome/spell.sass +++ b/app/styles/play/level/tome/spell.sass @@ -29,7 +29,7 @@ span.code-background border-width: 124px 76px 64px 40px - border-image: url(/images/level/code_editor_background_border.png) 124 76 64 40 fill round + border-image: url(/images/level/code_editor_background_border.png) 124 76 64 40 fill stretch img.code-background display: none From 9f4861bbaa24d0974139706371b08ae3b44e6812 Mon Sep 17 00:00:00 2001 From: Imperadeiro98 Date: Tue, 31 May 2016 19:29:55 +0100 Subject: [PATCH 055/167] Fix note in #3636 --- app/templates/play/level/modal/new-item-view.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/play/level/modal/new-item-view.jade b/app/templates/play/level/modal/new-item-view.jade index 36fb7f772..0d4df5394 100644 --- a/app/templates/play/level/modal/new-item-view.jade +++ b/app/templates/play/level/modal/new-item-view.jade @@ -1,5 +1,5 @@ .modal-header - #close-modal.well.well-sm.well-parchment(data-dismiss="modal") + #close-modal.btn.well.well-sm.well-parchment(data-dismiss="modal") span.glyphicon.glyphicon-remove .well.well-sm.well-parchment h1(data-i18n="play_level.victory_new_item") From abc42298c66b78e3bafca6b37b347934a0d14994 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Tue, 31 May 2016 12:30:36 -0700 Subject: [PATCH 056/167] Delay MailChimp subscribing after account creation by 30min --- server/models/User.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/models/User.coffee b/server/models/User.coffee index d35a65b12..fa43f8c32 100644 --- a/server/models/User.coffee +++ b/server/models/User.coffee @@ -162,6 +162,9 @@ UserSchema.statics.updateServiceSettings = (doc, callback) -> return callback?() unless isProduction or GLOBAL.testing return callback?() if doc.updatedMailChimp return callback?() unless doc.get('email') + return callback?() unless doc.get('dateCreated') + accountAgeMinutes = (new Date().getTime() - doc.get('dateCreated').getTime?() ? 0) / 1000 / 60 + return callback?() unless accountAgeMinutes > 30 or GLOBAL.testing existingProps = doc.get('mailChimp') emailChanged = (not existingProps) or existingProps?.email isnt doc.get('email') From 249255e4dc11b21dafd30915ec1b997cbe8f55d8 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Tue, 31 May 2016 12:39:30 -0700 Subject: [PATCH 057/167] AuthModal and CreateAccountModal load gplus and facebook on init On Windows/Chrome, the first click of the g+ button shows a popup blocked warning, because the action is deferred while loading the gplus library. Have the modals load the library first, and enable the buttons when they're ready. --- app/templates/core/auth.jade | 4 +- app/templates/core/create-account-modal.jade | 4 +- app/views/core/AuthModal.coffee | 60 ++++++-------- app/views/core/CreateAccountModal.coffee | 80 +++++++++---------- .../views/core/CreateAccountModal.spec.coffee | 18 +++-- 5 files changed, 76 insertions(+), 90 deletions(-) diff --git a/app/templates/core/auth.jade b/app/templates/core/auth.jade index 88e06edd2..1b65b9e09 100644 --- a/app/templates/core/auth.jade +++ b/app/templates/core/auth.jade @@ -39,10 +39,10 @@ // btn.btn.btn-sm.github-login-button#github-login-button // img(src="/images/pages/modal/auth/github_icon.png") // | GitHub - #facebook-login-btn.btn.btn-primary.btn-lg.btn-illustrated.network-login + button#facebook-login-btn.btn.btn-primary.btn-lg.btn-illustrated.network-login(disabled=true) img.network-logo(src="/images/pages/community/logo_facebook.png", draggable="false") span.sign-in-blurb(data-i18n="login.sign_in_with_facebook") - #gplus-login-btn.btn.btn-danger.btn-lg.btn-illustrated.network-login + button#gplus-login-btn.btn.btn-danger.btn-lg.btn-illustrated.network-login(disabled=true) img.network-logo(src="/images/pages/community/logo_g+.png", draggable="false") span.sign-in-blurb(data-i18n="login.sign_in_with_gplus") .gplus-login-wrapper diff --git a/app/templates/core/create-account-modal.jade b/app/templates/core/create-account-modal.jade index be94165f3..c56c01282 100644 --- a/app/templates/core/create-account-modal.jade +++ b/app/templates/core/create-account-modal.jade @@ -33,10 +33,10 @@ .col-md-6 .auth-network-logins.text-center strong(data-i18n="signup.or_sign_up_with") - #facebook-signup-btn.btn.btn-primary.btn-lg.btn-illustrated.network-login + button#facebook-signup-btn.btn.btn-primary.btn-lg.btn-illustrated.network-login(disabled=true) img.network-logo(src="/images/pages/community/logo_facebook.png", draggable="false") span.sign-in-blurb(data-i18n="login.sign_in_with_facebook") - #gplus-signup-btn.btn.btn-danger.btn-lg.btn-illustrated.network-login + button#gplus-signup-btn.btn.btn-danger.btn-lg.btn-illustrated.network-login(disabled=true) img.network-logo(src="/images/pages/community/logo_g+.png", draggable="false") span.sign-in-blurb(data-i18n="login.sign_in_with_gplus") .gplus-login-wrapper diff --git a/app/views/core/AuthModal.coffee b/app/views/core/AuthModal.coffee index fc6a78cde..0ed1b3116 100644 --- a/app/views/core/AuthModal.coffee +++ b/app/views/core/AuthModal.coffee @@ -28,6 +28,10 @@ module.exports = class AuthModal extends ModalView initialize: (options={}) -> @previousFormInputs = options.initialValues or {} + # TODO: Switch to promises and state, rather than using defer to hackily enable buttons after render + application.gplusHandler.loadAPI({ success: => _.defer => @$('#gplus-login-btn').attr('disabled', false) }) + application.facebookHandler.loadAPI({ success: => _.defer => @$('#facebook-login-btn').attr('disabled', false) }) + getRenderData: -> c = super() c.showRequiredError = @options.showRequiredError @@ -68,28 +72,22 @@ module.exports = class AuthModal extends ModalView onClickGPlusLoginButton: -> btn = @$('#gplus-login-btn') - btn.attr('disabled', true) - application.gplusHandler.loadAPI({ + application.gplusHandler.connect({ context: @ success: -> - btn.attr('disabled', false) - application.gplusHandler.connect({ + btn.find('.sign-in-blurb').text($.i18n.t('login.logging_in')) + btn.attr('disabled', true) + application.gplusHandler.loadPerson({ context: @ - success: -> - btn.find('.sign-in-blurb').text($.i18n.t('login.logging_in')) - btn.attr('disabled', true) - application.gplusHandler.loadPerson({ - context: @ - success: (gplusAttrs) -> - existingUser = new User() - existingUser.fetchGPlusUser(gplusAttrs.gplusID, { - success: => - me.loginGPlusUser(gplusAttrs.gplusID, { - success: -> window.location.reload() - error: @onGPlusLoginError - }) + success: (gplusAttrs) -> + existingUser = new User() + existingUser.fetchGPlusUser(gplusAttrs.gplusID, { + success: => + me.loginGPlusUser(gplusAttrs.gplusID, { + success: -> window.location.reload() error: @onGPlusLoginError }) + error: @onGPlusLoginError }) }) }) @@ -105,28 +103,22 @@ module.exports = class AuthModal extends ModalView onClickFacebookLoginButton: -> btn = @$('#facebook-login-btn') - btn.attr('disabled', true) - application.facebookHandler.loadAPI({ + application.facebookHandler.connect({ context: @ success: -> - btn.attr('disabled', false) - application.facebookHandler.connect({ + btn.find('.sign-in-blurb').text($.i18n.t('login.logging_in')) + btn.attr('disabled', true) + application.facebookHandler.loadPerson({ context: @ - success: -> - btn.find('.sign-in-blurb').text($.i18n.t('login.logging_in')) - btn.attr('disabled', true) - application.facebookHandler.loadPerson({ - context: @ - success: (facebookAttrs) -> - existingUser = new User() - existingUser.fetchFacebookUser(facebookAttrs.facebookID, { - success: => - me.loginFacebookUser(facebookAttrs.facebookID, { - success: -> window.location.reload() - error: @onFacebookLoginError - }) + success: (facebookAttrs) -> + existingUser = new User() + existingUser.fetchFacebookUser(facebookAttrs.facebookID, { + success: => + me.loginFacebookUser(facebookAttrs.facebookID, { + success: -> window.location.reload() error: @onFacebookLoginError }) + error: @onFacebookLoginError }) }) }) diff --git a/app/views/core/CreateAccountModal.coffee b/app/views/core/CreateAccountModal.coffee index 1fc210d16..a87e220b5 100644 --- a/app/views/core/CreateAccountModal.coffee +++ b/app/views/core/CreateAccountModal.coffee @@ -29,6 +29,10 @@ module.exports = class CreateAccountModal extends ModalView initialize: (options={}) -> @onNameChange = _.debounce(_.bind(@checkNameExists, @), 500) @previousFormInputs = options.initialValues or {} + + # TODO: Switch to promises and state, rather than using defer to hackily enable buttons after render + application.gplusHandler.loadAPI({ success: => _.defer => @$('#gplus-signup-btn').attr('disabled', false) }) + application.facebookHandler.loadAPI({ success: => _.defer => @$('#facebook-signup-btn').attr('disabled', false) }) afterRender: -> super() @@ -155,32 +159,26 @@ module.exports = class CreateAccountModal extends ModalView onClickGPlusSignupButton: -> btn = @$('#gplus-signup-btn') - btn.attr('disabled', true) - application.gplusHandler.loadAPI({ + application.gplusHandler.connect({ context: @ success: -> - btn.attr('disabled', false) - application.gplusHandler.connect({ + btn.find('.sign-in-blurb').text($.i18n.t('signup.creating')) + btn.attr('disabled', true) + application.gplusHandler.loadPerson({ context: @ - success: -> - btn.find('.sign-in-blurb').text($.i18n.t('signup.creating')) - btn.attr('disabled', true) - application.gplusHandler.loadPerson({ + success: (@gplusAttrs) -> + existingUser = new User() + existingUser.fetchGPlusUser(@gplusAttrs.gplusID, { context: @ - success: (@gplusAttrs) -> - existingUser = new User() - existingUser.fetchGPlusUser(@gplusAttrs.gplusID, { - context: @ - complete: -> - @$('#email-password-row').remove() - success: => - @$('#gplus-account-exists-row').removeClass('hide') - error: (user, jqxhr) => - if jqxhr.status is 404 - @$('#gplus-logged-in-row').toggleClass('hide') - else - errors.showNotyNetworkError(jqxhr) - }) + complete: -> + @$('#email-password-row').remove() + success: => + @$('#gplus-account-exists-row').removeClass('hide') + error: (user, jqxhr) => + if jqxhr.status is 404 + @$('#gplus-logged-in-row').toggleClass('hide') + else + errors.showNotyNetworkError(jqxhr) }) }) }) @@ -201,32 +199,26 @@ module.exports = class CreateAccountModal extends ModalView onClickFacebookSignupButton: -> btn = @$('#facebook-signup-btn') - btn.attr('disabled', true) - application.facebookHandler.loadAPI({ + application.facebookHandler.connect({ context: @ success: -> - btn.attr('disabled', false) - application.facebookHandler.connect({ + btn.find('.sign-in-blurb').text($.i18n.t('signup.creating')) + btn.attr('disabled', true) + application.facebookHandler.loadPerson({ context: @ - success: -> - btn.find('.sign-in-blurb').text($.i18n.t('signup.creating')) - btn.attr('disabled', true) - application.facebookHandler.loadPerson({ + success: (@facebookAttrs) -> + existingUser = new User() + existingUser.fetchFacebookUser(@facebookAttrs.facebookID, { context: @ - success: (@facebookAttrs) -> - existingUser = new User() - existingUser.fetchFacebookUser(@facebookAttrs.facebookID, { - context: @ - complete: -> - @$('#email-password-row').remove() - success: => - @$('#facebook-account-exists-row').removeClass('hide') - error: (user, jqxhr) => - if jqxhr.status is 404 - @$('#facebook-logged-in-row').toggleClass('hide') - else - errors.showNotyNetworkError(jqxhr) - }) + complete: -> + @$('#email-password-row').remove() + success: => + @$('#facebook-account-exists-row').removeClass('hide') + error: (user, jqxhr) => + if jqxhr.status is 404 + @$('#facebook-logged-in-row').toggleClass('hide') + else + errors.showNotyNetworkError(jqxhr) }) }) }) diff --git a/test/app/views/core/CreateAccountModal.spec.coffee b/test/app/views/core/CreateAccountModal.spec.coffee index 5fe9d82ab..1dc457854 100644 --- a/test/app/views/core/CreateAccountModal.spec.coffee +++ b/test/app/views/core/CreateAccountModal.spec.coffee @@ -6,31 +6,31 @@ describe 'CreateAccountModal', -> modal = null - initModal = (options) -> + initModal = (options) -> (done) -> application.facebookHandler.fakeAPI() application.gplusHandler.fakeAPI() modal = new CreateAccountModal(options) modal.render() modal.render = _.noop jasmine.demoModal(modal) + _.defer done afterEach -> modal.stopListening() describe 'constructed with showRequiredError is true', -> + beforeEach initModal({showRequiredError: true}) it 'shows a modal explaining to login first', -> - initModal({showRequiredError: true}) expect(modal.$('#required-error-alert').length).toBe(1) describe 'constructed with showSignupRationale is true', -> + beforeEach initModal({showSignupRationale: true}) it 'shows a modal explaining signup rationale', -> - initModal({showSignupRationale: true}) expect(modal.$('#signup-rationale-alert').length).toBe(1) describe 'clicking the save button', -> - beforeEach -> - initModal() + beforeEach initModal() it 'fails if nothing is in the form, showing errors for email, birthday, and password', -> modal.$('form').each (i, el) -> el.reset() @@ -110,8 +110,9 @@ describe 'CreateAccountModal', -> signupButton = null + beforeEach initModal() + beforeEach -> - initModal() forms.objectToForm(modal.$el, { birthdayDay: 24, birthdayMonth: 7, birthdayYear: 1988 }) signupButton = modal.$('#gplus-signup-btn') expect(signupButton.attr('disabled')).toBeFalsy() @@ -176,8 +177,9 @@ describe 'CreateAccountModal', -> signupButton = null + beforeEach initModal() + beforeEach -> - initModal() forms.objectToForm(modal.$el, { birthdayDay: 24, birthdayMonth: 7, birthdayYear: 1988 }) signupButton = modal.$('#facebook-signup-btn') expect(signupButton.attr('disabled')).toBeFalsy() @@ -235,4 +237,4 @@ describe 'CreateAccountModal', -> expect(request.method).toBe('PUT') expect(_.string.startsWith(request.url, '/db/user')).toBe(true) expect(modal.$('#signup-button').is(':disabled')).toBe(true) - expect(request.url).toBe('/db/user?facebookID=abcd&facebookAccessToken=1234') \ No newline at end of file + expect(request.url).toBe('/db/user?facebookID=abcd&facebookAccessToken=1234') From 41d8346c1ecd0e8480ab614fc629b8827a13a64e Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 31 May 2016 13:56:02 -0700 Subject: [PATCH 058/167] Don't show teacher warning to students who own classrooms --- app/templates/courses/classroom-view.jade | 2 +- app/templates/courses/course-details.jade | 2 +- app/templates/courses/courses-view.jade | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/templates/courses/classroom-view.jade b/app/templates/courses/classroom-view.jade index c57237de5..e7cc3607d 100644 --- a/app/templates/courses/classroom-view.jade +++ b/app/templates/courses/classroom-view.jade @@ -2,7 +2,7 @@ extends /templates/base block content - if !me.isAnonymous() && (me.isTeacher() || view.ownedClassrooms.size()) + if !me.isAnonymous() && me.isTeacher() .alert.alert-danger.text-center // DNT: Temporary h3 ATTENTION TEACHERS: diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade index 333e0fc12..e47e37282 100644 --- a/app/templates/courses/course-details.jade +++ b/app/templates/courses/course-details.jade @@ -2,7 +2,7 @@ extends /templates/base block content - if me.isTeacher() || view.ownedClassrooms.size() + if me.isTeacher() .alert.alert-danger.text-center // DNT: Temporary h3 ATTENTION TEACHERS: diff --git a/app/templates/courses/courses-view.jade b/app/templates/courses/courses-view.jade index 2bdf2b1a0..0200ae6b6 100644 --- a/app/templates/courses/courses-view.jade +++ b/app/templates/courses/courses-view.jade @@ -4,7 +4,7 @@ block content .container .row.m-y-3 .col-xs-12 - if me.isTeacher() || view.ownedClassrooms.size() + if me.isTeacher() .alert.alert-danger.text-center // DNT: Temporary h3 ATTENTION TEACHERS: From bf6f3f53af8154681b482e1109103efd35d217d7 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Tue, 31 May 2016 12:30:36 -0700 Subject: [PATCH 059/167] Delay MailChimp subscribing after account creation by 30min --- server/models/User.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/models/User.coffee b/server/models/User.coffee index d35a65b12..fa43f8c32 100644 --- a/server/models/User.coffee +++ b/server/models/User.coffee @@ -162,6 +162,9 @@ UserSchema.statics.updateServiceSettings = (doc, callback) -> return callback?() unless isProduction or GLOBAL.testing return callback?() if doc.updatedMailChimp return callback?() unless doc.get('email') + return callback?() unless doc.get('dateCreated') + accountAgeMinutes = (new Date().getTime() - doc.get('dateCreated').getTime?() ? 0) / 1000 / 60 + return callback?() unless accountAgeMinutes > 30 or GLOBAL.testing existingProps = doc.get('mailChimp') emailChanged = (not existingProps) or existingProps?.email isnt doc.get('email') From 794f4aadc9405b6d8e03a76f8fa6294535e3eded Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Tue, 31 May 2016 14:33:49 -0700 Subject: [PATCH 060/167] :bug:Contact Us emails must be sent from approved email --- server/routes/contact.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/server/routes/contact.coffee b/server/routes/contact.coffee index 8d4454e7a..cce6e96e3 100644 --- a/server/routes/contact.coffee +++ b/server/routes/contact.coffee @@ -61,7 +61,6 @@ createMailContext = (req, done) -> closeIO.getSalesContactEmail fromAddress, (err, salesContactEmail) -> console.error "Error getting sales contact for #{sender}: #{err}" if err context.recipient.address = salesContactEmail ? config.mail.supportSchools - context.sender.address = fromAddress done context else if recipientID and (user.isAdmin() or ('employer' in (user.get('permissions') ? []))) User.findById(recipientID, 'email').exec (err, document) -> From d0251d20bf77124862006f2f5754d37f056c8294 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 31 May 2016 14:48:47 -0700 Subject: [PATCH 061/167] Trim 'Course: ' from course level names --- app/templates/courses/teacher-courses-view.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/courses/teacher-courses-view.jade b/app/templates/courses/teacher-courses-view.jade index d8ff7c88e..25038c973 100644 --- a/app/templates/courses/teacher-courses-view.jade +++ b/app/templates/courses/teacher-courses-view.jade @@ -63,7 +63,7 @@ block content span.spr | . span - = level.get('name') + = level.get('name').replace('Course: ', '') a.play-level-button.btn.btn-lg.btn-primary span(data-i18n="courses.play_level") .clearfix From 59608a5bc22a87815d2e7027b306e4b47dd1c720 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Tue, 31 May 2016 14:54:35 -0700 Subject: [PATCH 062/167] :bug:Fix clan page loads for a really long time Fixes #3336 --- server/handlers/clan_handler.coffee | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server/handlers/clan_handler.coffee b/server/handlers/clan_handler.coffee index b76bdc72c..24ae07959 100644 --- a/server/handlers/clan_handler.coffee +++ b/server/handlers/clan_handler.coffee @@ -95,7 +95,6 @@ ClanHandler = class ClanHandler extends Handler AnalyticsLogEvent.logEvent req.user, 'Clan left', clanID: clanID, type: clan.get('type') getMemberAchievements: (req, res, clanID) -> - # TODO: add tests Clan.findById clanID, (err, clan) => return @sendDatabaseError(res, err) if err return @sendNotFoundError(res) unless clan @@ -111,18 +110,16 @@ ClanHandler = class ClanHandler extends Handler @sendSuccess(res, cleandocs) getMembers: (req, res, clanID) -> - # TODO: add tests Clan.findById clanID, (err, clan) => return @sendDatabaseError(res, err) if err return @sendNotFoundError(res) unless clan memberIDs = _.map clan.get('members') ? [], (memberID) -> memberID.toHexString?() or memberID - User.find {_id: {$in: memberIDs}}, 'name nameLower points heroConfig.thangType', {}, (err, users) => + User.find {_id: {$in: memberIDs}}, 'name nameLower points heroConfig.thangType', {limit: memberLimit}, (err, users) => return @sendDatabaseError(res, err) if err cleandocs = (UserHandler.formatEntity(req, doc) for doc in users) @sendSuccess(res, cleandocs) getMemberSessions: (req, res, clanID) -> - # TODO: add tests # TODO: restrict information returned based on clan type Clan.findById clanID, (err, clan) => return @sendDatabaseError(res, err) if err From 826c9c9de04c96462de05e69f35f4ec423200f5b Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 31 May 2016 15:01:32 -0700 Subject: [PATCH 063/167] Trim 'Course: ' from course level names (pt 2) --- app/templates/play/level/modal/progress-view.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/play/level/modal/progress-view.jade b/app/templates/play/level/modal/progress-view.jade index e6f2ff42e..8c754fb33 100644 --- a/app/templates/play/level/modal/progress-view.jade +++ b/app/templates/play/level/modal/progress-view.jade @@ -36,7 +36,7 @@ h3.text-uppercase span(data-i18n='play_level.next_level') span : - h2.text-uppercase= i18n(view.nextLevel.attributes, 'name') + h2.text-uppercase= i18n(view.nextLevel.attributes, 'name').replace('Course: ', '') div!= view.nextLevelDescription From d36908f8c15adb8e20e5bd47586d1809dfc96a08 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Tue, 31 May 2016 11:15:37 -0700 Subject: [PATCH 064/167] Grant admins access to teacher and student only pages --- app/core/Router.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/core/Router.coffee b/app/core/Router.coffee index 984619ee6..3ddbbf206 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -172,9 +172,9 @@ module.exports = class CocoRouter extends Backbone.Router @navigate e, {trigger: true} routeDirectly: (path, args=[], options={}) -> - if options.teachersOnly and not me.isTeacher() + if options.teachersOnly and not (me.isTeacher() or me.isAdmin()) return @routeDirectly('teachers/RestrictedToTeachersView') - if options.studentsOnly and not me.isStudent() + if options.studentsOnly and not (me.isStudent() or me.isAdmin()) return @routeDirectly('courses/RestrictedToStudentsView') leavingMessage = _.result(window.currentView, 'onLeaveMessage') if leavingMessage From d4c5d418ff37b7711bc11af2b3fb11c028b82c19 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Tue, 31 May 2016 15:08:09 -0700 Subject: [PATCH 065/167] Improve CampaignEditor saving, particularly level order * Do not cache campaign * Instead have an on leave message if there are unsaved changes * Propagate campaignIndex values to campaign and levels so that they signal a change in order The save modal seems to have a lot of issues with identifying what models have changed. I tried using different methods, but none were consistent. Moving on to other bugs. --- app/models/Campaign.coffee | 1 - .../editor/campaign/CampaignEditorView.coffee | 22 +++++++++++++++++++ app/views/play/CampaignView.coffee | 1 - server/middleware/campaigns.coffee | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/models/Campaign.coffee b/app/models/Campaign.coffee index 495ca7ac5..2e5cb9db1 100644 --- a/app/models/Campaign.coffee +++ b/app/models/Campaign.coffee @@ -8,7 +8,6 @@ module.exports = class Campaign extends CocoModel @className: 'Campaign' @schema: schema urlRoot: '/db/campaign' - saveBackups: true @denormalizedLevelProperties: _.keys(_.omit(schema.properties.levels.additionalProperties.properties, ['unlocks', 'position', 'rewards'])) @denormalizedCampaignProperties: ['name', 'i18n', 'slug'] diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index 245d330b9..7a0920023 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -60,6 +60,14 @@ module.exports = class CampaignEditorView extends RootView @listenToOnce @levels, 'sync', @onFundamentalLoaded @listenToOnce @achievements, 'sync', @onFundamentalLoaded + onLeaveMessage: -> + @propagateCampaignIndexes() + for model in @toSave.models + diff = model.getDelta() + if _.size(diff) + console.log 'model, diff', model, diff + return 'You have changes!' + loadThangTypeNames: -> # Load the names of the ThangTypes that this level's Treema nodes might want to display. originals = [] @@ -143,6 +151,19 @@ module.exports = class CampaignEditorView extends RootView @updateRewardsForLevel model, level.rewards super() + + propagateCampaignIndexes: -> + campaignLevels = $.extend({}, @campaign.get('levels')) + + index = 0 + for levelOriginal, campaignLevel of campaignLevels + level = @levels.findWhere({original: levelOriginal}) + if level.get('campaignIndex') isnt index + level.set('campaignIndex', index) + campaignLevel.campaignIndex = index + index += 1 + + @campaign.set('levels', campaignLevels) onClickPatches: (e) -> @patchesView = @insertSubView(new PatchesView(@campaign), @$el.find('.patches-view')) @@ -160,6 +181,7 @@ module.exports = class CampaignEditorView extends RootView break onClickSaveButton: -> + @propagateCampaignIndexes() @toSave.set @toSave.filter (m) -> m.hasLocalChanges() @openModalView new SaveCampaignModal({}, @toSave) diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index 8c87b2dd8..5852514fd 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -80,7 +80,6 @@ module.exports = class CampaignView extends RootView return @campaign = new Campaign({_id:@terrain}) - @campaign.saveBackups = @editorMode @campaign = @supermodel.loadModel(@campaign).model # Temporary attempt to make sure all earned rewards are accounted for. Figure out a better solution... diff --git a/server/middleware/campaigns.coffee b/server/middleware/campaigns.coffee index 2be34e9db..66c1cbba7 100644 --- a/server/middleware/campaigns.coffee +++ b/server/middleware/campaigns.coffee @@ -79,4 +79,4 @@ module.exports = for original, level of campaign.levels campaign.levels[original] = _.pick level, ['locked', 'disabled', 'original', 'rewards', 'slug'] return campaign - res.status(200).send((formatCampaign(campaign) for campaign in campaigns)) \ No newline at end of file + res.status(200).send((formatCampaign(campaign) for campaign in campaigns)) From cf76434943ab4b6267f11a6347c3c30be3e84237 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Tue, 31 May 2016 15:09:12 -0700 Subject: [PATCH 066/167] Add script for removing anonymous users from classrooms --- ...016-05-31-remove-anon-classroom-members.js | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 scripts/mongodb/migrations/2016-05-31-remove-anon-classroom-members.js diff --git a/scripts/mongodb/migrations/2016-05-31-remove-anon-classroom-members.js b/scripts/mongodb/migrations/2016-05-31-remove-anon-classroom-members.js new file mode 100644 index 000000000..f42b8e318 --- /dev/null +++ b/scripts/mongodb/migrations/2016-05-31-remove-anon-classroom-members.js @@ -0,0 +1,43 @@ +// Usage: Copy and paste into mongo shell + +function removeAnonymousMembers(classroom) { + if(!classroom.members) { + return; + } + + print('checking classroom', + classroom._id, + '\n\t', + classroom._id.getTimestamp(), + classroom.members.length, + 'owner', classroom.ownerID); + + classroom.members.forEach(function(userID) { + var user = db.users.findOne({_id: userID}, {anonymous:1}); + if (!user) { + return; + } + if(user.anonymous) { + print('\tRemove user', JSON.stringify(user)); + + print('\t\tRemoving from course instances', + db.course.instances.update( + {classroomID: classroom._id}, + {$pull: {members: userID}}) + ); + + print('\t\tRemoving from classroom', + db.classrooms.update( + {_id: classroom._id}, + {$pull: {members: userID}}) + ); + } + }); +} + +var startID = ObjectId('566838b00fb44a2e00000000'); +while (true) { + var classroom = db.classrooms.findOne({_id: {$gt: startID}}); + removeAnonymousMembers(classroom); + startID = classroom._id; +} From ebc94e6e4b10a7505f185f2815650ab277658feb Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 31 May 2016 15:12:08 -0700 Subject: [PATCH 067/167] Show something reasonable when class has no students --- app/locale/en.coffee | 1 + app/templates/courses/teacher-class-view.jade | 50 +++++++++++-------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/app/locale/en.coffee b/app/locale/en.coffee index fd844fb3c..a99949080 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -1339,6 +1339,7 @@ unarchive_class: "unarchive class" unarchive_this_class: "Unarchive this class" no_students_yet: "This class has no students yet." + try_refreshing: "(You may need to refresh the page)" add_students: "Add Students" create_new_class: "Create a New Class" class_overview: "Class Overview" # View Class page diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade index 11c689eea..5b12984c6 100644 --- a/app/templates/courses/teacher-class-view.jade +++ b/app/templates/courses/teacher-class-view.jade @@ -108,28 +108,36 @@ block content +copyCodes +addStudentsButton - ul.nav.nav-tabs.m-t-5(role='tablist') - - var activeTab = state.get('activeTab'); - li(class=(activeTab === "#students-tab" ? 'active' : '')) - a.students-tab-btn(href='#students-tab') - .small-details.text-center(data-i18n='teacher.students') - .tab-spacer - li(class=(activeTab === "#course-progress-tab" ? 'active' : '')) - a.course-progress-tab-btn(href='#course-progress-tab') - .small-details.text-center(data-i18n='teacher.course_progress') - .tab-spacer - li(class=(activeTab === "#enrollment-status-tab" ? 'active' : '')) - a.course-progress-tab-btn(href='#enrollment-status-tab') - .small-details.text-center(data-i18n='teacher.enrollment_status') - .tab-filler + if view.students.length > 0 + ul.nav.nav-tabs.m-t-5(role='tablist') + - var activeTab = state.get('activeTab'); + li(class=(activeTab === "#students-tab" ? 'active' : '')) + a.students-tab-btn(href='#students-tab') + .small-details.text-center(data-i18n='teacher.students') + .tab-spacer + li(class=(activeTab === "#course-progress-tab" ? 'active' : '')) + a.course-progress-tab-btn(href='#course-progress-tab') + .small-details.text-center(data-i18n='teacher.course_progress') + .tab-spacer + li(class=(activeTab === "#enrollment-status-tab" ? 'active' : '')) + a.course-progress-tab-btn(href='#enrollment-status-tab') + .small-details.text-center(data-i18n='teacher.enrollment_status') + .tab-filler - .tab-content - if activeTab === '#students-tab' - +studentsTab - else if activeTab === '#course-progress-tab' - +courseProgressTab - else - +enrollmentStatusTab + .tab-content + if activeTab === '#students-tab' + +studentsTab + else if activeTab === '#course-progress-tab' + +courseProgressTab + else + +enrollmentStatusTab + + else + .text-center.m-t-5.m-b-5 + .text-h2 + span(data-i18n="teacher.no_students_yet") + .text-h4 + span(data-i18n="teacher.try_refreshing") mixin breadcrumbs .breadcrumbs From e7f79ab721397377316690dbc6e6190c41113d4a Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 31 May 2016 15:32:32 -0700 Subject: [PATCH 068/167] Several fixes for level editor update speed--should be faster for making changes on complex levels --- app/models/Level.coffee | 35 +++++++++++-------- .../editor/level/thangs/ThangsTabView.coffee | 2 ++ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/app/models/Level.coffee b/app/models/Level.coffee index b594e43ba..08179a5cb 100644 --- a/app/models/Level.coffee +++ b/app/models/Level.coffee @@ -59,11 +59,13 @@ module.exports = class Level extends CocoModel denormalize: (supermodel, session, otherSession) -> o = $.extend true, {}, @attributes if o.thangs and @get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] + thangTypesWithComponents = (tt for tt in supermodel.getModels(ThangType) when tt.get('components')?) + thangTypesByOriginal = _.indexBy thangTypesWithComponents, (tt) -> tt.get('original') # Optimization for levelThang in o.thangs - @denormalizeThang(levelThang, supermodel, session, otherSession) + @denormalizeThang(levelThang, supermodel, session, otherSession, thangTypesByOriginal) o - denormalizeThang: (levelThang, supermodel, session, otherSession) -> + denormalizeThang: (levelThang, supermodel, session, otherSession, thangTypesByOriginal) -> levelThang.components ?= [] isHero = /Hero Placeholder/.test(levelThang.id) and @get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] if isHero and otherSession @@ -79,7 +81,7 @@ module.exports = class Level extends CocoModel if isHero placeholders = {} placeholdersUsed = {} - placeholderThangType = supermodel.getModelByOriginal(ThangType, levelThang.thangType) + placeholderThangType = thangTypesByOriginal[levelThang.thangType] unless placeholderThangType console.error "Couldn't find placeholder ThangType for the hero!" isHero = false @@ -92,7 +94,7 @@ module.exports = class Level extends CocoModel heroThangType = session?.get('heroConfig')?.thangType levelThang.thangType = heroThangType if heroThangType - thangType = supermodel.getModelByOriginal(ThangType, levelThang.thangType, (m) -> m.get('components')?) + thangType = thangTypesByOriginal[levelThang.thangType] configs = {} for thangComponent in levelThang.components @@ -168,11 +170,16 @@ module.exports = class Level extends CocoModel # Decision? Just special case the sort logic in here until we have more examples than these two and decide how best to handle most of the cases then, since we don't really know the whole of the problem yet. # TODO: anything that depends on Programmable will break right now. + originalsToComponents = _.indexBy levelComponents, 'original' # Optimization for speed + alliedComponent = _.find levelComponents, name: 'Allied' + actsComponent = _.find levelComponents, name: 'Acts' + for thang in thangs ? [] + originalsToThangComponents = _.indexBy thang.components, 'original' sorted = [] visit = (c, namesToIgnore) -> return if c in sorted - lc = _.find levelComponents, {original: c.original} + lc = originalsToComponents[c.original] console.error thang.id or thang.name, 'couldn\'t find lc for', c, 'of', levelComponents unless lc return unless lc return if namesToIgnore and lc.name in namesToIgnore @@ -184,20 +191,18 @@ module.exports = class Level extends CocoModel visit c2, [lc.name] for c2 in thang.components else for d in lc.dependencies or [] - c2 = _.find thang.components, {original: d.original} + c2 = originalsToThangComponents[d.original] unless c2 - dependent = _.find levelComponents, {original: d.original} + dependent = originalsToComponents[d.original] dependent = dependent?.name or d.original console.error parentType, thang.id or thang.name, 'does not have dependent Component', dependent, 'from', lc.name visit c2 if c2 - if lc.name is 'Collides' - if allied = _.find levelComponents, {name: 'Allied'} - allied = _.find(thang.components, {original: allied.original}) - visit allied if allied - if lc.name is 'Moves' - if acts = _.find levelComponents, {name: 'Acts'} - acts = _.find(thang.components, {original: acts.original}) - visit acts if acts + if lc.name is 'Collides' and alliedComponent + if allied = originalsToThangComponents[alliedComponent.original] + visit allied + if lc.name is 'Moves' and actsComponent + if acts = originalsToThangComponents[actsComponent.original] + visit acts #console.log thang.id, 'sorted comps adding', lc.name sorted.push c for comp in thang.components diff --git a/app/views/editor/level/thangs/ThangsTabView.coffee b/app/views/editor/level/thangs/ThangsTabView.coffee index 8ce64029d..351da5032 100644 --- a/app/views/editor/level/thangs/ThangsTabView.coffee +++ b/app/views/editor/level/thangs/ThangsTabView.coffee @@ -90,6 +90,8 @@ module.exports = class ThangsTabView extends CocoView getRenderData: (context={}) -> context = super(context) return context unless @supermodel.finished() + for thangType in @thangTypes.models + thangType.notInLevel = true thangTypes = (thangType.attributes for thangType in @supermodel.getModels(ThangType)) thangTypes = _.uniq thangTypes, false, 'original' thangTypes = _.reject thangTypes, (tt) -> tt.kind in ['Mark', undefined] From 634cc1fb152d8c527616fffc78ad53794a094e5b Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Tue, 31 May 2016 15:37:22 -0700 Subject: [PATCH 069/167] Fix anchor links on /about --- app/views/AboutView.coffee | 43 +++++++++++++------------------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/app/views/AboutView.coffee b/app/views/AboutView.coffee index 7de91ecae..3e5dd2d83 100644 --- a/app/views/AboutView.coffee +++ b/app/views/AboutView.coffee @@ -8,12 +8,7 @@ module.exports = class AboutView extends RootView logoutRedirectURL: false events: - 'click #mission-link': 'onClickMissionLink' - 'click #team-link': 'onClickTeamLink' - 'click #community-link': 'onClickCommunityLink' - 'click #story-link': 'onClickStoryLink' - 'click #jobs-link': 'onClickJobsLink' - 'click #contact-link': 'onClickContactLink' + 'click #fixed-nav a': 'onClickFixedNavLink' 'click .screen-thumbnail': 'onClickScreenThumbnail' 'click #carousel-left': 'onLeftPressed' 'click #carousel-right': 'onRightPressed' @@ -44,29 +39,21 @@ module.exports = class AboutView extends RootView keyboard: false }) - onClickMissionLink: (event) -> - event.preventDefault() - @scrollToLink('#mission') + afterInsert: -> + # scroll to the current hash, once everything in the browser is set up + f = => + return if @destroyed + link = $(document.location.hash) + if link.length + @scrollToLink(document.location.hash, 0) + _.delay(f, 100) - onClickTeamLink: (event) -> - event.preventDefault() - @scrollToLink('#team') - - onClickCommunityLink: (event) -> - event.preventDefault() - @scrollToLink('#community') - - onClickStoryLink: (event) -> - event.preventDefault() - @scrollToLink('#story') - - onClickJobsLink: (event) -> - event.preventDefault() - @scrollToLink('#jobs') - - onClickContactLink: (event) -> - event.preventDefault() - @scrollToLink('#contact') + onClickFixedNavLink: (event) -> + event.preventDefault() # prevent default page scroll + link = $(event.target).closest('a') + target = link.attr('href') + history.replaceState(null, null, "about#{target}") # update hash without triggering page scroll + @scrollToLink(target) onRightPressed: (event) -> # Special handling, otherwise after you click the control, keyboard presses move the slide twice From f684708a9446331d10129c20ab9b4b431d5fe854 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Tue, 31 May 2016 15:47:31 -0700 Subject: [PATCH 070/167] Fix Level indentation --- app/models/Level.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/Level.coffee b/app/models/Level.coffee index 08179a5cb..975e34309 100644 --- a/app/models/Level.coffee +++ b/app/models/Level.coffee @@ -198,8 +198,8 @@ module.exports = class Level extends CocoModel console.error parentType, thang.id or thang.name, 'does not have dependent Component', dependent, 'from', lc.name visit c2 if c2 if lc.name is 'Collides' and alliedComponent - if allied = originalsToThangComponents[alliedComponent.original] - visit allied + if allied = originalsToThangComponents[alliedComponent.original] + visit allied if lc.name is 'Moves' and actsComponent if acts = originalsToThangComponents[actsComponent.original] visit acts From dfd460255323adc84a6539fa95247146ce41fefa Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Tue, 31 May 2016 15:54:28 -0700 Subject: [PATCH 071/167] AuthModal handles empty fields, fix #3587 --- app/views/core/AuthModal.coffee | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/views/core/AuthModal.coffee b/app/views/core/AuthModal.coffee index 0ed1b3116..0791d396c 100644 --- a/app/views/core/AuthModal.coffee +++ b/app/views/core/AuthModal.coffee @@ -59,7 +59,7 @@ module.exports = class AuthModal extends ModalView e.preventDefault() forms.clearFormAlerts(@$el) userObject = forms.formToObject @$el - res = tv4.validateMultiple userObject, User.schema + res = tv4.validateMultiple userObject, formSchema return forms.applyErrorsToForm(@$el, res.errors) unless res.valid @enableModalInProgress(@$el) # TODO: part of forms loginUser userObject, null, window.nextURL @@ -133,3 +133,9 @@ module.exports = class AuthModal extends ModalView onHidden: -> super() @playSound 'game-menu-close' + +formSchema = { + type: 'object' + properties: _.pick(User.schema.properties, 'email', 'password') + required: ['email', 'password'] +} From 9bea73b1994358d19b2c0eacdeffb9ff2a129242 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 31 May 2016 16:09:52 -0700 Subject: [PATCH 072/167] Use course invite template v3 --- server/sendwithus.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/sendwithus.coffee b/server/sendwithus.coffee index 6e781beda..d65c193b4 100644 --- a/server/sendwithus.coffee +++ b/server/sendwithus.coffee @@ -29,7 +29,7 @@ module.exports.templates = generic_email: 'tem_JhRnQ4pvTS4KdQjYoZdbei' plain_text_email: 'tem_85UvKDCCNPXsFckERTig6Y' next_steps_email: 'tem_RDHhTG5inXQi8pthyqWr5D' - course_invite_email: 'tem_f5K7BXX5vQ9a7kwYTACbJa' + course_invite_email: 'tem_ic2ZhPkpj8GBADFuyAp4bj' teacher_free_trial: 'tem_R7d9Hpoba9SceQNiYSXBak' teacher_free_trial_hoc: 'tem_4ZSY9wsA9Qwn4wBFmZgPdc' teacher_request_demo: 'tem_cwG3HZjEyb6QE493hZuUra' From 9f9590a22395a652f3f1ebc330e604194f20f357 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 31 May 2016 16:42:25 -0700 Subject: [PATCH 073/167] Fix Select All text --- app/locale/en.coffee | 1 + app/styles/courses/teacher-class-view.sass | 6 ++++++ app/templates/courses/teacher-class-view.jade | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/locale/en.coffee b/app/locale/en.coffee index a99949080..15b00dba5 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -1422,6 +1422,7 @@ status_expired: "Expired on {{date}}" status_not_enrolled: "Not Enrolled" status_enrolled: "Expires on {{date}}" + select_all: "Select All" classes: archmage_title: "Archmage" diff --git a/app/styles/courses/teacher-class-view.sass b/app/styles/courses/teacher-class-view.sass index 70ba4d654..c5d848b38 100644 --- a/app/styles/courses/teacher-class-view.sass +++ b/app/styles/courses/teacher-class-view.sass @@ -104,6 +104,12 @@ vertical-align: bottom td height: 66px + + .select-all + padding-top: 5px + + .checkbox-flat + margin-top: 3px .enroll-student-button margin-left: 33% diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade index 5b12984c6..e0e6713da 100644 --- a/app/templates/courses/teacher-class-view.jade +++ b/app/templates/courses/teacher-class-view.jade @@ -179,8 +179,8 @@ mixin studentsTab +bulkAssignControls table.students-table thead - th.checkbox-col.select-all - span Select All + th.checkbox-col.select-all.small.text-center + span(data-i18n="teacher.select_all") .checkbox-flat input(type='checkbox' id='checkbox-all-students') label.checkmark(for='checkbox-all-students') From 13b69d15f2c98b6fa4e6af114026b8fc236afec7 Mon Sep 17 00:00:00 2001 From: Josh Callebaut Date: Tue, 31 May 2016 16:50:22 -0700 Subject: [PATCH 074/167] Display auto-complete box even after adding a new line --- app/views/play/level/tome/SpellView.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index ad9749986..743b51a41 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -692,6 +692,8 @@ module.exports = class SpellView extends CocoView @aceDoc.insertNewLine row: lineCount, column: 0 #lastLine.length @ace.navigateLeft(1) if wasAtEnd ++lineCount + # Force the popup back + @ace?.completer?.showPopup(@ace) screenLineCount = @aceSession.getScreenLength() if screenLineCount isnt @lastScreenLineCount @lastScreenLineCount = screenLineCount From 81b550aeb368007f2b9de4cca70706034b3f2d76 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 31 May 2016 17:03:51 -0700 Subject: [PATCH 075/167] Fix extra scrolling space on right of page --- app/templates/base-flat.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/base-flat.jade b/app/templates/base-flat.jade index a5740bad6..c93b52906 100644 --- a/app/templates/base-flat.jade +++ b/app/templates/base-flat.jade @@ -18,7 +18,7 @@ mixin accountLinks .style-flat block header nav#main-nav.navbar.navbar-default - .container-fluid + .container-fluid.container .row .col-lg-12 .navbar-header From 790c0375f18d7bada3a20b56005aa474726fd91d Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 31 May 2016 17:22:37 -0700 Subject: [PATCH 076/167] Increase name label background opacity as per Robin's suggestion --- app/lib/surface/Label.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/surface/Label.coffee b/app/lib/surface/Label.coffee index 1e07d4f76..07697cc93 100644 --- a/app/lib/surface/Label.coffee +++ b/app/lib/surface/Label.coffee @@ -83,7 +83,7 @@ module.exports = class Label extends CocoClass o.fontColor = '#c66' else if @style is 'name' and @sprite?.thang?.team is 'ogres' o.fontColor = '#66c' - o.backgroundFillColor = {D: 'white', S: 'rgba(0,0,0,0.4)', N: 'rgba(0,0,0,0.5)'}[st] + o.backgroundFillColor = {D: 'white', S: 'rgba(0,0,0,0.4)', N: 'rgba(0,0,0,0.7)'}[st] o.backgroundStrokeColor = {D: 'black', S: 'rgba(0,0,0,0.6)', N: 'rgba(0,0,0,0)'}[st] o.backgroundStrokeStyle = {D: 2, S: 1, N: 1}[st] o.backgroundBorderRadius = {D: 10, S: 3, N: 3}[st] From e053ad5c61503bcfac3f618c2a4816cc428f8e6e Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 31 May 2016 17:45:54 -0700 Subject: [PATCH 077/167] Only show progress dot for assigned courses (TeacherClassesView) --- app/templates/courses/teacher-classes-view.jade | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/templates/courses/teacher-classes-view.jade b/app/templates/courses/teacher-classes-view.jade index c861328f8..ae7e134e6 100644 --- a/app/templates/courses/teacher-classes-view.jade +++ b/app/templates/courses/teacher-classes-view.jade @@ -77,7 +77,8 @@ mixin classRow(classroom) else each trimCourse, index in classroom.get('courses') || [] - var course = view.courses.get(trimCourse._id); - +progressDot(classroom, course, index) + if view.courseInstances.findWhere({ classroomID: classroom.id, courseID: course.id }) + +progressDot(classroom, course, index) .view-class-arrow.col-xs-1 a.view-class-arrow-inner.glyphicon.glyphicon-chevron-right(data-classroom-id=classroom.id, href=('/teachers/classes/' + classroom.id)) From c08d426c6bd3417523182b08c0836f687c42a22b Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 31 May 2016 19:56:37 -0700 Subject: [PATCH 078/167] Use cloudflare for analytics so we get the correct SSL cert. --- app/core/Tracker.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/Tracker.coffee b/app/core/Tracker.coffee index 0b779c6da..e1a4ae7cc 100644 --- a/app/core/Tracker.coffee +++ b/app/core/Tracker.coffee @@ -169,7 +169,7 @@ module.exports = class Tracker extends CocoClass eventObject["user"] = me.id dataToSend = JSON.stringify eventObject # console.log dataToSend if debugAnalytics - $.post("#{window.location.protocol or 'http:'}//analytics.codecombat.com/analytics", dataToSend).fail -> + $.post("#{window.location.protocol or 'http:'}//analytics-cf.codecombat.com/analytics", dataToSend).fail -> console.error "Analytics post failed!" else request = @supermodel.addRequestResource { From ee0195ab214509d8d00797049c495b4b9a9a039a Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Wed, 1 Jun 2016 09:53:16 -0700 Subject: [PATCH 079/167] Require req.user in all /db routes --- server/handlers/subscription_handler.coffee | 6 +++--- server/routes/index.coffee | 10 ++++++---- spec/helpers/helper.js | 12 ++++++++++++ spec/server/functional/article.spec.coffee | 10 +++++----- spec/server/functional/clan.spec.coffee | 8 ++++---- spec/server/functional/courses.spec.coffee | 2 ++ spec/server/functional/prepaid.spec.coffee | 2 +- spec/server/functional/user.spec.coffee | 2 +- 8 files changed, 34 insertions(+), 18 deletions(-) diff --git a/server/handlers/subscription_handler.coffee b/server/handlers/subscription_handler.coffee index a37f600f1..a01421404 100644 --- a/server/handlers/subscription_handler.coffee +++ b/server/handlers/subscription_handler.coffee @@ -433,9 +433,9 @@ class SubscriptionHandler extends Handler productName = "#{user.get('country')}_basic_subscription" Product.findOne({name: productName}).exec (err, product) => - return @sendDatabaseError(res, err) if err - return @sendNotFoundError(res, 'basic_subscription product not found') if not product - + return done({res: 'Database error.', code: 500}) if err + return done({res: 'basic_subscription product not found.', code: 404}) if not product + if increment purchased = _.clone(user.get('purchased')) purchased ?= {} diff --git a/server/routes/index.coffee b/server/routes/index.coffee index 08a8552cb..5e9bff30f 100644 --- a/server/routes/index.coffee +++ b/server/routes/index.coffee @@ -1,7 +1,7 @@ mw = require '../middleware' module.exports.setup = (app) -> - + passport = require('passport') app.post('/auth/login', passport.authenticate('local'), mw.auth.afterLogin) app.post('/auth/login-facebook', mw.auth.loginByFacebook, mw.auth.afterLogin) @@ -14,6 +14,8 @@ module.exports.setup = (app) -> app.get('/auth/unsubscribe', mw.auth.unsubscribe) app.get('/auth/whoami', mw.auth.whoAmI) + app.all('/db/*', mw.auth.checkHasUser()) + Achievement = require '../models/Achievement' app.get('/db/achievement', mw.achievements.fetchByRelated, mw.rest.get(Achievement)) app.post('/db/achievement', mw.auth.checkHasPermission(['admin', 'artisan']), mw.rest.post(Achievement)) @@ -28,7 +30,7 @@ module.exports.setup = (app) -> Article = require '../models/Article' app.get('/db/article', mw.rest.get(Article)) - app.post('/db/article', mw.auth.checkHasPermission(['admin', 'artisan']), mw.rest.post(Article)) + app.post('/db/article', mw.auth.checkLoggedIn(), mw.auth.checkHasPermission(['admin', 'artisan']), mw.rest.post(Article)) app.get('/db/article/names', mw.named.names(Article)) app.post('/db/article/names', mw.named.names(Article)) app.get('/db/article/:handle', mw.rest.getByHandle(Article)) @@ -65,7 +67,7 @@ module.exports.setup = (app) -> app.get('/db/classroom/:handle', mw.auth.checkLoggedIn()) # TODO: Finish migrating route, adding now so 401 is returned CodeLog = require ('../models/CodeLog') - app.post('/db/codelogs', mw.auth.checkHasUser(), mw.codelogs.post) + app.post('/db/codelogs', mw.codelogs.post) app.get('/db/codelogs', mw.auth.checkHasPermission(['admin']), mw.rest.get(CodeLog)) Course = require '../models/Course' @@ -86,7 +88,7 @@ module.exports.setup = (app) -> app.post('/db/user/:userID/request-verify-email', mw.users.sendVerificationEmail) app.post('/db/user/:userID/verify/:verificationCode', mw.users.verifyEmailAddress) # TODO: Finalize URL scheme - app.get('/db/level/:handle/session', mw.auth.checkHasUser(), mw.levels.upsertSession) + app.get('/db/level/:handle/session', mw.levels.upsertSession) app.get('/db/prepaid', mw.auth.checkLoggedIn(), mw.prepaids.fetchByCreator) app.post('/db/prepaid', mw.auth.checkHasPermission(['admin']), mw.prepaids.post) diff --git a/spec/helpers/helper.js b/spec/helpers/helper.js index a0135dafb..09be41fa3 100644 --- a/spec/helpers/helper.js +++ b/spec/helpers/helper.js @@ -76,6 +76,18 @@ beforeEach(function(done) { cb(err); }); }, + function(cb) { + // Initialize products + var utils = require('../server/utils'); + request = require('../server/request'); + utils.initUser() + .then(function (user) { + return utils.loginUser(user, {request: request}) + }) + .then(function () { + cb() + }); + }, function(cb) { // Initialize products request = require('../server/request'); diff --git a/spec/server/functional/article.spec.coffee b/spec/server/functional/article.spec.coffee index 3d634dedc..5a5fb1c4a 100644 --- a/spec/server/functional/article.spec.coffee +++ b/spec/server/functional/article.spec.coffee @@ -17,7 +17,7 @@ describe 'GET /db/article', -> yield utils.loginUser(@admin) yield request.postAsync(getURL('/db/article'), { json: articleData1 }) yield request.postAsync(getURL('/db/article'), { json: articleData2 }) - yield utils.logout() + yield utils.becomeAnonymous() done() @@ -194,7 +194,7 @@ describe 'POST /db/article', -> it 'does not allow anonymous users to create Articles', utils.wrap (done) -> yield utils.clearModels([Article]) - yield utils.logout() + yield utils.becomeAnonymous() [res, body] = yield request.postAsync({uri: getURL('/db/article'), json: articleData }) expect(res.statusCode).toBe(401) done() @@ -451,7 +451,7 @@ describe 'POST /db/article/:handle/new-version', -> it 'does not work for anonymous users', utils.wrap (done) -> - yield utils.logout() + yield utils.becomeAnonymous() yield postNewVersion({ name: 'Article name', body: 'New body' }, 401) articles = yield Article.find() expect(articles.length).toBe(1) @@ -580,7 +580,7 @@ describe 'GET and POST /db/article/:handle/names', -> yield utils.loginUser(admin) [res, article1] = yield request.postAsync(getURL('/db/article'), { json: articleData1 }) [res, article2] = yield request.postAsync(getURL('/db/article'), { json: articleData2 }) - yield utils.logout() + yield utils.becomeAnonymous() [res, body] = yield request.getAsync { uri: getURL('/db/article/names?ids='+[article1._id, article2._id].join(',')), json: true } expect(body.length).toBe(2) expect(body[0].name).toBe('Article 1') @@ -679,4 +679,4 @@ describe 'DELETE /db/article/:handle/watchers', -> article = yield Article.findById(article._id) ids = (id.toString() for id in article.get('watchers')) expect(_.contains(ids, user.id)).toBe(false) - done() \ No newline at end of file + done() diff --git a/spec/server/functional/clan.spec.coffee b/spec/server/functional/clan.spec.coffee index d4c4149b2..5a1f06c11 100644 --- a/spec/server/functional/clan.spec.coffee +++ b/spec/server/functional/clan.spec.coffee @@ -1,9 +1,9 @@ config = require '../../../server_config' require '../common' -utils = require '../../../app/core/utils' # Must come after require /common Clan = require '../../../server/models/Clan' User = require '../../../server/models/User' request = require '../request' +utils = require '../utils' describe 'Clans', -> clanURL = getURL('/db/clan') @@ -53,7 +53,7 @@ describe 'Clans', -> done() it 'Anonymous create clan 401', (done) -> - logoutUser -> + utils.logout().then -> requestBody = type: 'public' name: createClanName 'myclan' @@ -152,7 +152,7 @@ describe 'Clans', -> loginNewUser (user1) -> createClan user1, 'public', null, (clan1) -> createClan user1, 'public', null, (clan2) -> - logoutUser -> + utils.becomeAnonymous().then -> request.get {uri: "#{clanURL}/-/public" }, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(200) @@ -498,7 +498,7 @@ describe 'Clans', -> user1.save (err) -> expect(err).toBeNull() createClan user1, 'private', 'my private clan', (clan1) -> - logoutUser -> + utils.becomeAnonymous().then -> request.get {uri: "#{clanURL}/#{clan1.id}" }, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(200) diff --git a/spec/server/functional/courses.spec.coffee b/spec/server/functional/courses.spec.coffee index ee963be6c..f56264334 100644 --- a/spec/server/functional/courses.spec.coffee +++ b/spec/server/functional/courses.spec.coffee @@ -23,6 +23,7 @@ describe 'GET /db/course', -> yield utils.clearModels([Course, User]) yield new Course({ name: 'Course 1' }).save() yield new Course({ name: 'Course 2' }).save() + yield utils.becomeAnonymous() done() @@ -36,6 +37,7 @@ describe 'GET /db/course/:handle', -> beforeEach utils.wrap (done) -> yield utils.clearModels([Course, User]) @course = yield new Course({ name: 'Some Name' }).save() + yield utils.becomeAnonymous() done() diff --git a/spec/server/functional/prepaid.spec.coffee b/spec/server/functional/prepaid.spec.coffee index 4baacae41..9aaf1fc6d 100644 --- a/spec/server/functional/prepaid.spec.coffee +++ b/spec/server/functional/prepaid.spec.coffee @@ -544,7 +544,7 @@ describe '/db/prepaid', -> logoutUser () -> fetchPrepaid joeCode, (err, res) -> expect(err).toBeNull() - expect(res.statusCode).toEqual(403) + expect(res.statusCode).toEqual(401) done() it 'User can fetch a prepaid code', (done) -> diff --git a/spec/server/functional/user.spec.coffee b/spec/server/functional/user.spec.coffee index ea3bf026e..9926251f1 100644 --- a/spec/server/functional/user.spec.coffee +++ b/spec/server/functional/user.spec.coffee @@ -39,7 +39,7 @@ describe 'POST /db/user', -> it 'serves the user through /db/user/id', (done) -> unittest.getNormalJoe (user) -> - request.post getURL('/auth/logout'), -> + utils.becomeAnonymous().then -> url = getURL(urlUser+'/'+user._id) request.get url, (err, res, body) -> expect(res.statusCode).toBe(200) From b143d552cec92e04764eb83a175ae3f4f6b40d37 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Wed, 1 Jun 2016 10:01:05 -0700 Subject: [PATCH 080/167] Write test, verifying removeFromClassrooms handles sessionless requests --- spec/server/functional/user.spec.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/server/functional/user.spec.coffee b/spec/server/functional/user.spec.coffee index 9926251f1..c58dd59d3 100644 --- a/spec/server/functional/user.spec.coffee +++ b/spec/server/functional/user.spec.coffee @@ -567,6 +567,12 @@ describe 'DELETE /db/user', -> expect(classroom.get('members')[0].toString()).toEqual(user2.id) expect(classroom.get('deletedMembers')[0].toString()).toEqual(user.id) done() + + fit 'returns 401 if no cookie session', utils.wrap (done) -> + yield utils.logout() + [res, body] = yield request.delAsync {uri: "#{getURL(urlUser)}/1234"} + expect(res.statusCode).toBe(401) + done() describe 'Statistics', -> LevelSession = require '../../../server/models/LevelSession' From 1f3eed9a4e86fa6f3d46579a276c5196ffc34222 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Wed, 1 Jun 2016 10:01:41 -0700 Subject: [PATCH 081/167] Remove focus --- spec/server/functional/user.spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/server/functional/user.spec.coffee b/spec/server/functional/user.spec.coffee index c58dd59d3..69e22e1c8 100644 --- a/spec/server/functional/user.spec.coffee +++ b/spec/server/functional/user.spec.coffee @@ -568,7 +568,7 @@ describe 'DELETE /db/user', -> expect(classroom.get('deletedMembers')[0].toString()).toEqual(user.id) done() - fit 'returns 401 if no cookie session', utils.wrap (done) -> + it 'returns 401 if no cookie session', utils.wrap (done) -> yield utils.logout() [res, body] = yield request.delAsync {uri: "#{getURL(urlUser)}/1234"} expect(res.statusCode).toBe(401) From bd57121f63d7cc8d7dedeaf9f382af3be415ce17 Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 1 Jun 2016 15:11:08 -0700 Subject: [PATCH 082/167] Fix algolia-autocomplete.js version --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 5bba7ea9f..064a9dbc2 100644 --- a/bower.json +++ b/bower.json @@ -51,7 +51,7 @@ "lscache": "~1.0.5", "esper.js": "http://files.codecombat.com/esper.tar.gz", "algoliasearch": "^3.13.1", - "algolia-autocomplete.js": "^1.17.0", + "algolia-autocomplete.js": "^0.17.0", "algolia-autocomplete-no-conflict": "1.0.0" }, "overrides": { From 215ff1faaf11cbb96b4dc197cbe4421dbe24beb2 Mon Sep 17 00:00:00 2001 From: duybkict Date: Thu, 2 Jun 2016 14:54:00 +0700 Subject: [PATCH 083/167] refactor MainLadderView --- app/views/ladder/MainLadderView.coffee | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/views/ladder/MainLadderView.coffee b/app/views/ladder/MainLadderView.coffee index ebd2eca0c..b4108ee42 100644 --- a/app/views/ladder/MainLadderView.coffee +++ b/app/views/ladder/MainLadderView.coffee @@ -15,15 +15,16 @@ module.exports = class MainLadderView extends RootView id: 'main-ladder-view' template: template - constructor: (options) -> - super options - @levelStatusMap = {} - @levelPlayCountMap = {} - @sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', {cache: false}, 0).model - @listenToOnce @sessions, 'sync', @onSessionsLoaded - @getLevelPlayCounts() + initialize: -> + @levelStatusMap = [] + @levelPlayCountMap = [] @campaigns = campaigns + @sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', {cache: false}, 0).model + @listenToOnce @sessions, 'sync', @onSessionsLoaded + + @getLevelPlayCounts() + onSessionsLoaded: (e) -> for session in @sessions.models @levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started' From a9d9a67383d1947eff9b011d3a379ea7308e1e4c Mon Sep 17 00:00:00 2001 From: duybkict Date: Thu, 2 Jun 2016 15:44:19 +0700 Subject: [PATCH 084/167] refactor LadderView --- app/templates/play/ladder/ladder.jade | 106 +++++++++++++------------- app/views/ladder/LadderView.coffee | 29 +++---- 2 files changed, 63 insertions(+), 72 deletions(-) diff --git a/app/templates/play/ladder/ladder.jade b/app/templates/play/ladder/ladder.jade index 31e1fceb4..b4f1075c5 100644 --- a/app/templates/play/ladder/ladder.jade +++ b/app/templates/play/ladder/ladder.jade @@ -4,10 +4,10 @@ block content div#ladder-top - if leagueType == 'course' && view.course.id + if view.leagueType === 'course' && view.course #course-header #course-details-link - a(href="/courses/"+view.course.id+"/"+view.league.id) + a(href="/courses/{#view.course.id}/{#view.league.id}") span.glyphicon.glyphicon-arrow-left span.spl Levels @@ -15,22 +15,21 @@ block content span#course-name span= view.course.get('name') span.spl - Arena - div#level-column - if levelDescription - div!= levelDescription + if view.levelDescription + div!= view.levelDescription - if leagueType === 'clan' + if view.leagueType === 'clan' h1.league-header - a(href="/clans/#{league.id}")= league.get('name') + a(href="/clans/#{view.league.id}")= view.league.get('name') span.spl(data-i18n="ladder.league") League - if level.get('name') == 'Greed' + if view.level.get('name') == 'Greed' .tournament-blurb h2 span(data-i18n="ladder.tournament_ended") Tournament ended - | #{tournamentTimeLeft} + | #{view.tournamentTimeLeft} p span(data-i18n="ladder.tournament_blurb") Write code, collect gold, build armies, crush foes, win prizes, and upgrade your career in our $40,000 Greed tournament! Check out the details | @@ -63,11 +62,11 @@ block content a(href="http://aws.amazon.com/") img(src=base + "aws.png") - if level.get('name') == 'Criss-Cross' + if view.level.get('name') == 'Criss-Cross' .tournament-blurb h2 span(data-i18n="ladder.tournament_ended") Tournament ended - | #{tournamentTimeLeft} + | #{view.tournamentTimeLeft} p span(data-i18n="ladder.tournament_blurb_criss_cross") Win bids, construct paths, outwit opponents, grab gems, and upgrade your career in our Criss-Cross tournament! Check out the details | @@ -86,13 +85,13 @@ block content a(href="http://discourse.codecombat.com/") the forum | and discuss your strategies, your triumphs, and your turmoils. - if level.get('name') == 'Zero Sum' && !league + if view.level.get('name') == 'Zero Sum' && !view.league .tournament-blurb h2 span(data-i18n="ladder.tournament_ended") Tournament ended - | #{tournamentTimeLeft} + | #{view.tournamentTimeLeft} //span(data-i18n="ladder.tournament_started") , started - //| #{tournamentTimeElapsed} + //| #{view.tournamentTimeElapsed} p span(data-i18n="ladder.tournament_blurb_zero_sum") Unleash your coding creativity in both gold gathering and battle tactics in this alpine mirror match between red sorcerer and blue sorcerer. The tournament began on Friday, March 27 and will run until Monday, April 6 at 5PM PDT. Compete for fun and glory! Check out the details | @@ -109,14 +108,14 @@ block content a(href="http://discourse.codecombat.com/") the forum | and discuss your strategies, your triumphs, and your turmoils. - if level.get('name') == 'Ace of Coders' + if view.level.get('name') == 'Ace of Coders' .tournament-blurb h2 //span(data-i18n="ladder.tournament_ends") Tournament ends span(data-i18n="ladder.tournament_ended") Tournament ended - | #{tournamentTimeLeft} + | #{view.tournamentTimeLeft} span(data-i18n="ladder.tournament_started") , started - | #{tournamentTimeElapsed} + | #{view.tournamentTimeElapsed} p span(data-i18n="ladder.tournament_blurb_ace_of_coders") Battle it out in the frozen glacier in this domination-style mirror match! The tournament began on Wednesday, September 16 and will run until Wednesday, October 14 at 5PM PDT. Check out the details | @@ -135,17 +134,18 @@ block content div#columns.row div.column.col-md-2 - for team in teams - div.column.col-md-4 - a(class="play-button btn btn-illustrated btn-block btn-lg " + (team.id == 'ogres' ? 'btn-primary' : 'btn-danger'), data-team=team.id) - span(data-i18n="play.play_as") Play As - | - span= team.displayName + if view.teams + for team in view.teams + div.column.col-md-4 + a(class="play-button btn btn-illustrated btn-block btn-lg " + (team.id == 'ogres' ? 'btn-primary' : 'btn-danger'), data-team=team.id) + span(data-i18n="play.play_as") Play As + | + span= team.displayName div.column.col-md-2 - if leagueType !== 'course' + if view.leagueType !== 'course' .spectate-button-container - a(href="/play/spectate/#{level.get('slug')}" + (league ? "?league=" + league.id : "")).spectate-button.btn.btn-illustrated.btn-info.center + a(href="/play/spectate/#{view.level.get('slug')}" + (view.league ? "?league=" + view.league.id : "")).spectate-button.btn.btn-illustrated.btn-info.center span(data-i18n="play.spectate") Spectate ul.nav.nav-pills @@ -154,16 +154,16 @@ block content if !me.get('anonymous') li a(href="#my-matches", data-toggle="tab", data-i18n="ladder.my_matches") My Matches - if leagueType !== 'course' + if view.leagueType !== 'course' li a(href="#simulate", data-toggle="tab", data-i18n="ladder.simulate") Simulate - if level.get('name') == 'Greed' + if view.level.get('name') == 'Greed' li a(href="#prizes", data-toggle="tab", data-i18n="ladder_prizes.prizes") Prizes - if level.get('name') == 'Greed' + if view.level.get('name') == 'Greed' li a(href="#rules", data-toggle="tab", data-i18n="ladder.rules") Rules - if level.get('name') == 'Greed' || level.get('name') == 'Criss-Cross' || level.get('name') == 'Zero Sum' || level.get('name') == 'Ace of Coders' + if view.level.get('name') == 'Greed' || view.level.get('name') == 'Criss-Cross' || view.level.get('name') == 'Zero Sum' || view.level.get('name') == 'Ace of Coders' li a(href="#winners", data-toggle="tab", data-i18n="ladder.winners") Winners @@ -174,7 +174,7 @@ block content #my-matches-tab-view .tab-pane.well#simulate #simulate-tab-view - if level.get('name') == 'Greed' + if view.level.get('name') == 'Greed' .tab-pane.well#prizes h1(data-i18n="ladder_prizes.title") Tournament Prizes p @@ -740,7 +740,7 @@ block content | - $50 td $50 - if level.get('name') == 'Greed' + if view.level.get('name') == 'Greed' .tab-pane.well#rules h1(data-i18n="ladder.tournament_rules") Tournament Rules h2 General @@ -802,7 +802,7 @@ block content a(href="http://discourse.codecombat.com/") Discourse forum | . - if level.get('name') == 'Greed' || level.get('name') == 'Criss-Cross' || level.get('name') == 'Zero Sum' + if view.level.get('name') == 'Greed' || view.level.get('name') == 'Criss-Cross' || view.level.get('name') == 'Zero Sum' .tab-pane.well#winners h1(data-i18n="ladder.winners") Winners @@ -810,71 +810,71 @@ block content thead tr th(data-i18n="ladder_prizes.rank") Rank - if level.get('name') == 'Criss-Cross' || level.get('name') == 'Zero Sum' + if view.level.get('name') == 'Criss-Cross' || view.level.get('name') == 'Zero Sum' th th Human - if level.get('name') == 'Greed' || level.get('name') == 'Zero Sum' + if view.level.get('name') == 'Greed' || view.level.get('name') == 'Zero Sum' th Human wins/losses/ties else th Human score - if level.get('name') == 'Zero Sum' + if view.level.get('name') == 'Zero Sum' th - if level.get('name') == 'Criss-Cross' || level.get('name') == 'Zero Sum' + if view.level.get('name') == 'Criss-Cross' || view.level.get('name') == 'Zero Sum' th th Ogre - if level.get('name') == 'Greed' || level.get('name') == 'Zero Sum' + if view.level.get('name') == 'Greed' || view.level.get('name') == 'Zero Sum' th Ogre wins/losses/ties else th Ogre score th(data-i18n="play.spectate") Spectate tbody - each human, index in winners.humans - - var ogre = winners.ogres[index] + each human, index in view.winners.humans + - var ogre = view.winners.ogres[index] tr td= human.rank - if level.get('name') == 'Criss-Cross' || level.get('name') == 'Zero Sum' + if view.level.get('name') == 'Criss-Cross' || view.level.get('name') == 'Zero Sum' td.code-language-cell(style="background-image: url(/images/common/code_languages/" + human.codeLanguage + "_icon.png)" title=_.string.capitalize(human.codeLanguage)) td= human.name - if level.get('name') == 'Greed' || level.get('name') == 'Zero Sum' + if view.level.get('name') == 'Greed' || view.level.get('name') == 'Zero Sum' td span.win= human.wins | - span.loss= human.losses | - - if level.get('name') == 'Greed' + if view.level.get('name') == 'Greed' span.tie= 377 - human.wins - human.losses - else if level.get('name') == 'Zero Sum' + else if view.level.get('name') == 'Zero Sum' span.tie= 108 - human.wins - human.losses else td span= Math.round(100 * human.score) if ogre - if level.get('name') == 'Zero Sum' + if view.level.get('name') == 'Zero Sum' td= ogre.rank - if level.get('name') == 'Criss-Cross' || level.get('name') == 'Zero Sum' + if view.level.get('name') == 'Criss-Cross' || view.level.get('name') == 'Zero Sum' td.code-language-cell(style="background-image: url(/images/common/code_languages/" + ogre.codeLanguage + "_icon.png)" title=_.string.capitalize(ogre.codeLanguage)) td= ogre.name - if level.get('name') == 'Greed' || level.get('name') == 'Zero Sum' + if view.level.get('name') == 'Greed' || view.level.get('name') == 'Zero Sum' td span.win= ogre.wins | - span.loss= ogre.losses | - - if level.get('name') == 'Greed' + if view.level.get('name') == 'Greed' span.tie= 407 - ogre.wins - ogre.losses - else if level.get('name') == 'Zero Sum' + else if view.level.get('name') == 'Zero Sum' span.tie= Math.max(0, 163 - ogre.wins - ogre.losses) else td span= Math.round(100 * ogre.score) td - a(href="/play/spectate/" + level.get('slug') + "?session-one=" + human.sessionID + "&session-two=" + ogre.sessionID, data-i18n="ladder.watch_battle") Watch the battle + a(href="/play/spectate/" + view.level.get('slug') + "?session-one=" + human.sessionID + "&session-two=" + ogre.sessionID, data-i18n="ladder.watch_battle") Watch the battle else td td td - if level.get('name') == 'Ace of Coders' + if view.level.get('name') == 'Ace of Coders' .tab-pane.well#winners h1(data-i18n="ladder.winners") Winners @@ -888,7 +888,7 @@ block content th Losses th(data-i18n="play.spectate") Spectate tbody - each player in winners.humans + each player in view.winners.humans tr td= player.rank td.code-language-cell(style="background-image: url(/images/common/code_languages/" + player.codeLanguage + "_icon.png)" title=_.string.capitalize(player.codeLanguage)) @@ -899,6 +899,6 @@ block content span.loss= player.losses td if player.team == "ogres" - a(href="/play/spectate/" + level.get('slug') + "?session-one=55df8c9207d920b7e4262f33" + "&session-two=" + player.sessionID, data-i18n="ladder.watch_battle") Watch the battle + a(href="/play/spectate/" + view.level.get('slug') + "?session-one=55df8c9207d920b7e4262f33" + "&session-two=" + player.sessionID, data-i18n="ladder.watch_battle") Watch the battle else - a(href="/play/spectate/" + level.get('slug') + "?session-one=" + player.sessionID + "&session-two=55e1d23686c019bc47b640fe", data-i18n="ladder.watch_battle") Watch the battle + a(href="/play/spectate/" + view.level.get('slug') + "?session-one=" + player.sessionID + "&session-two=55e1d23686c019bc47b640fe", data-i18n="ladder.watch_battle") Watch the battle diff --git a/app/views/ladder/LadderView.coffee b/app/views/ladder/LadderView.coffee index 8e5d2240e..5a2e5d69b 100644 --- a/app/views/ladder/LadderView.coffee +++ b/app/views/ladder/LadderView.coffee @@ -41,11 +41,19 @@ module.exports = class LadderView extends RootView constructor: (options, @levelID, @leagueType, @leagueID) -> super(options) + + initialize: -> @level = @supermodel.loadModel(new Level(_id: @levelID)).model + @levelDescription = marked(@level.get('description')) if @level.get('description') @sessions = @supermodel.loadCollection(new LevelSessionsCollection(@levelID), 'your_sessions', {cache: false}).model - @teams = [] + @winners = require('./tournament_results')[@levelID] + + if tournamentEndDate = {greed: 1402444800000, 'criss-cross': 1410912000000, 'zero-sum': 1428364800000, 'ace-of-coders': 1444867200000}[@levelID] + @tournamentTimeLeft = moment(new Date(tournamentEndDate)).fromNow() + if tournamentStartDate = {'zero-sum': 1427472000000, 'ace-of-coders': 1442417400000}[@levelID] + @tournamentTimeElapsed = moment(new Date(tournamentStartDate)).fromNow() + @loadLeague() - @course = new Course() loadLeague: -> @leagueID = @leagueType = null unless @leagueType in ['clan', 'course'] @@ -68,23 +76,6 @@ module.exports = class LadderView extends RootView @teams = teamDataFromLevel @level super() - getRenderData: -> - ctx = super() - ctx.level = @level - ctx.link = "/play/level/#{@level.get('name')}" - ctx.teams = @teams - ctx.levelID = @levelID - ctx.levelDescription = marked(@level.get('description')) if @level.get('description') - ctx.leagueType = @leagueType - ctx.league = @league - ctx._ = _ - if tournamentEndDate = {greed: 1402444800000, 'criss-cross': 1410912000000, 'zero-sum': 1428364800000, 'ace-of-coders': 1444867200000}[@levelID] - ctx.tournamentTimeLeft = moment(new Date(tournamentEndDate)).fromNow() - if tournamentStartDate = {'zero-sum': 1427472000000, 'ace-of-coders': 1442417400000}[@levelID] - ctx.tournamentTimeElapsed = moment(new Date(tournamentStartDate)).fromNow() - ctx.winners = require('./tournament_results')[@levelID] - ctx - afterRender: -> super() return unless @supermodel.finished() From 5e8b402c3dce4865fd4dc7ed6d34c5cc67f1b52f Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 2 Jun 2016 10:27:20 -0700 Subject: [PATCH 085/167] Switch POST /db/classroom/:id/invite-members to refactored version --- server/handlers/classroom_handler.coffee | 28 ------------------- server/middleware/classrooms.coffee | 15 ++++------ server/routes/index.coffee | 2 +- spec/server/functional/classrooms.spec.coffee | 2 -- 4 files changed, 7 insertions(+), 40 deletions(-) diff --git a/server/handlers/classroom_handler.coffee b/server/handlers/classroom_handler.coffee index db7809a27..97daaac3d 100644 --- a/server/handlers/classroom_handler.coffee +++ b/server/handlers/classroom_handler.coffee @@ -28,8 +28,6 @@ ClassroomHandler = class ClassroomHandler extends Handler false getByRelationship: (req, res, args...) -> - method = req.method.toLowerCase() - return @inviteStudents(req, res, args[0]) if args[1] is 'invite-members' return @removeMember(req, res, args[0]) if req.method is 'DELETE' and args[1] is 'members' return @getMembersAPI(req, res, args[0]) if args[1] is 'members' super(arguments...) @@ -68,32 +66,6 @@ ClassroomHandler = class ClassroomHandler extends Handler return doc.toObject() return _.omit(doc.toObject(), 'code', 'codeCamel') - inviteStudents: (req, res, classroomID) -> - return @sendUnauthorizedError(res) if not req.user? - if not req.body.emails - return @sendBadInputError(res, 'Emails not included') - - Classroom.findById classroomID, (err, classroom) => - return @sendDatabaseError(res, err) if err - return @sendNotFoundError(res) unless classroom - unless classroom.get('ownerID').equals(req.user.get('_id')) - log.debug "classroom_handler.inviteStudents: Can't invite to classroom (#{classroom.id}) you (#{req.user.get('_id')}) don't own" - return @sendForbiddenError(res) - - for email in req.body.emails - joinCode = (classroom.get('codeCamel') or classroom.get('code')) - context = - email_id: sendwithus.templates.course_invite_email - recipient: - address: email - email_data: - teacher_name: req.user.broadName() - class_name: classroom.get('name') - join_link: "https://codecombat.com/courses?_cc=" + joinCode - join_code: joinCode - sendwithus.api.send context, _.noop - return @sendSuccess(res, {}) - get: (req, res) -> if ownerID = req.query.ownerID unless req.user and (req.user.isAdmin() or ownerID is req.user.id) diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index 5697107b6..cb811859d 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -228,23 +228,20 @@ module.exports = throw new errors.NotFound('Classroom not found.') unless classroom.get('ownerID').equals(req.user?._id) + log.debug "classroom_handler.inviteStudents: Can't invite to classroom (#{classroom.id}) you (#{req.user.get('_id')}) don't own" throw new errors.Forbidden('Must be owner of classroom to send invites.') - - user = req.user - teacherName = user.get('name') - teacherName ?= _.filter([user.get('firstName'), user.get('lastName')]).join(' ') - trialRequest = yield TrialRequest.findOne({applicant: user._id}) - schoolName = trialRequest?.get('properties')?.organization for email in req.body.emails + joinCode = (classroom.get('codeCamel') or classroom.get('code')) context = email_id: sendwithus.templates.course_invite_email recipient: address: email email_data: + teacher_name: req.user.broadName() class_name: classroom.get('name') - teacher_name: teacherName - school_name: schoolName - join_link: "https://codecombat.com/courses?_cc=" + (classroom.get('codeCamel') or classroom.get('code')) + join_link: "https://codecombat.com/courses?_cc=" + joinCode + join_code: joinCode sendwithus.api.send context, _.noop + res.status(200).send({}) diff --git a/server/routes/index.coffee b/server/routes/index.coffee index ceeefb6ac..c523fcc3d 100644 --- a/server/routes/index.coffee +++ b/server/routes/index.coffee @@ -60,7 +60,7 @@ module.exports.setup = (app) -> app.get('/db/classroom', mw.classrooms.fetchByCode, mw.classrooms.getByOwner) app.get('/db/classroom/:handle/levels', mw.classrooms.fetchAllLevels) app.get('/db/classroom/:handle/courses/:courseID/levels', mw.classrooms.fetchLevelsForCourse) -# app.post('/db/classroom/:handle/invite-members', mw.classrooms.inviteMembers) + app.post('/db/classroom/:handle/invite-members', mw.classrooms.inviteMembers) app.get('/db/classroom/:handle/member-sessions', mw.classrooms.fetchMemberSessions) app.get('/db/classroom/:handle/members', mw.classrooms.fetchMembers) # TODO: Use mw.auth? app.post('/db/classroom/:classroomID/members/:memberID/reset-password', mw.classrooms.setStudentPassword) diff --git a/spec/server/functional/classrooms.spec.coffee b/spec/server/functional/classrooms.spec.coffee index caecf5b86..81c9f2964 100644 --- a/spec/server/functional/classrooms.spec.coffee +++ b/spec/server/functional/classrooms.spec.coffee @@ -346,7 +346,6 @@ describe 'POST /db/classroom/:id/invite-members', -> it 'takes a list of emails and sends invites', utils.wrap (done) -> user = yield utils.initUser({role: 'teacher', name: 'Mr Professerson'}) yield utils.loginUser(user) - yield utils.makeTrialRequest({ properties: { organization: 'Greendale' } }) classroom = yield utils.makeClassroom() url = classroomsURL + "/#{classroom.id}/invite-members" data = { emails: ['test@test.com'] } @@ -355,7 +354,6 @@ describe 'POST /db/classroom/:id/invite-members', -> expect(context.email_id).toBe(sendwithus.templates.course_invite_email) expect(context.recipient.address).toBe('test@test.com') expect(context.email_data.teacher_name).toBe('Mr Professerson') - expect(context.email_data.school_name).toBe('Greendale') done() [res, body] = yield request.postAsync { uri: url, json: data } expect(res.statusCode).toBe(200) From 7dc490bbcb7de8483b5f9c87cdcdf834d1b09a1c Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Thu, 2 Jun 2016 10:23:30 -0700 Subject: [PATCH 086/167] Add Create Account button to student-only pages --- app/templates/courses/restricted-to-students-view.jade | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/templates/courses/restricted-to-students-view.jade b/app/templates/courses/restricted-to-students-view.jade index ebea62f7c..9ffcb3909 100644 --- a/app/templates/courses/restricted-to-students-view.jade +++ b/app/templates/courses/restricted-to-students-view.jade @@ -11,6 +11,7 @@ block content p(data-i18n='courses.account_restricted') if me.isAnonymous() .login-button.btn.btn-lg.btn-primary(data-i18n='login.log_in') + .signup-button.btn.btn-lg.btn-primary-alt(data-i18n="login.sign_up") else a.btn.btn-lg.btn-primary(href="/courses/update-account" data-i18n="courses.update_account_update_student") button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out") @@ -18,4 +19,4 @@ block content if me.isTeacher() .teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3 h5(data-i18n='teacher.what_is_a_teacher_account') - p(data-i18n='teacher.teacher_account_explanation') \ No newline at end of file + p(data-i18n='teacher.teacher_account_explanation') From 18138ec6ae91b79b41bbda9712297d998666447e Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Thu, 2 Jun 2016 10:55:48 -0700 Subject: [PATCH 087/167] Fix typo --- test/app/views/core/CreateAccountModal.spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/app/views/core/CreateAccountModal.spec.coffee b/test/app/views/core/CreateAccountModal.spec.coffee index 1dc457854..9778e5d9c 100644 --- a/test/app/views/core/CreateAccountModal.spec.coffee +++ b/test/app/views/core/CreateAccountModal.spec.coffee @@ -45,7 +45,7 @@ describe 'CreateAccountModal', -> expect(jasmine.Ajax.requests.all().length).toBe(0) expect(modal.$('.has-error').length).toBeTruthy() - it 'fails if birthay is missing', -> + it 'fails if birthday is missing', -> modal.$('form').each (i, el) -> el.reset() forms.objectToForm(modal.$el, { email: 'some@email.com', password: 'xyzzy' }) modal.$('form').submit() From 3c2e3e37d4894d750938591e62c85266399af386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Duy=20Tr=E1=BA=A7n?= Date: Fri, 3 Jun 2016 00:58:29 +0700 Subject: [PATCH 088/167] Vi translation (#3703) * Translate into Vietnamese * Translate and edit some Vietnamese strings * update Vietnamese translate * try to fix some error * fix bugs with tab character * update vi.coffee: play_level * Update vi.coffee * update vi.coffee * update text on Home page * Vietnamese: update legal * vi.coffee: edit community page * vi.coffee: update user's page * vi.coffee: about page * update vi.coffee * update vi.coffee * vi.coffee: invoices * vi.coffee: account_prepaid * update vi.coffee * vi.coffee: error strings * Update vi.coffee * update vi.coffee * vi.coffee: update contact form * vi.coffee: minor update * vi.coffee: update about page, modify some strings * update vi.coffee * vi.coffee: minor update * no message * Update vi strings, fix minor bug in page /community * vi.coffee: update Adventure and Artisan * vi.coffee: update Adventurer * vi.coffee: reverse changes * vi.coffee: reverse changes * vi.coffee: update some strings * vi.cofffe: update Diplomat * vi.coffee: update Diplomat * vi.coffee: update descriptions of classes * vi.coffee: update Ambassador * remove .cleafix div, will fix in another branch * refactor ClansView #3138 #3488 * use initialize() in ClansView #3138 #3488 * update vi.coffee * update vi.coffee * exclude changes on ClansView * vi.coffee: update play screen * vi.coffee: update some strings * vi.coffee: update createClass form * vi.coffee: modify some strings on home page and teachers page * minor fix * vi.coffee: update some strings --- app/locale/vi.coffee | 86 ++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/app/locale/vi.coffee b/app/locale/vi.coffee index cebf8dcc0..84b8c457e 100644 --- a/app/locale/vi.coffee +++ b/app/locale/vi.coffee @@ -735,7 +735,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn mission_description_1: "Lập trình thật kì diệu. Bạn có thể tạo ra một thứ gì đó chỉ từ trí tưởng tượng. Chúng tôi bắt đầu CodeCombat để đem tới cho học viên những trải nghiệm nhiệm màu khi viết code thực tế." mission_description_2: "Trên thực tế, việc này giúp cho bạn học nhanh hơn. Nhanh hơn RẤT NHIỀU. Bạn được thực hành thay vì chỉ đọc lý thuyết. Chúng tôi muốn đưa môi trường thực hành này đến với trường học và đến tay mọi học sinh, bởi vì mọi người đều cần có cơ hội biết đến sự nhiệm màu của lập trình." team_title: "Đội ngũ của CodeCombat" -# team_values: "We value open and respectful dialog, where the best idea wins. Our decisions are grounded in customer research and our process is focused on delivering tangible results for them. Everyone is hands-on, from our CEO to our Github contributors, because we value growth and learning in our team." + team_values: "Chúng tôi chân trọng những cuộc đối thoại mở và có sự tôn trọng lẫn nhau, nơi mà những ý tưởng tốt nhất giành chiến thắng. Những quyết định của chúng tôi được đưa ra hoàn toàn dựa trên những báo cáo nghiên cứu ý kiến khách hàng và quy trình của chúng tôi chú trọng vào mục tiêu đưa đến những giá trị hữu hình cho cho khách hàng. Mọi người đều có vai trò của mình, từ CEO của chúng tôi cho đến những người tham gia công đồng trên Github, bởi vì chúng tôi chân trọng sự phát triển và học hỏi của từng thành viên trong nhóm." nick_title: "Đồng Sáng Lập, CEO" # {change} nick_blurb: "Người truyền cảm hứng" matt_title: "Đồng Sáng Lập, CTO" # {change} @@ -853,10 +853,10 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn college_plus: "Cao đẳng/Đại học hoặc cao hơn" anything_else: "Còn điều gì chúng tôi nên biết thêm nữa không?" thanks_header: "Đã nhận yêu cầu!" -# thanks_sub_header: "Thanks for expressing interest in CodeCombat for your school." + thanks_sub_header: "Cảm ơn vì đã bày tỏ sự quan tâm của trường bạn với CodeCombat." thanks_p: "Chúng tôi sẽ sớm trả lời lại! Nếu bạn cần liên hệ, hãy liên lạc với chúng tôi tại:" -# back_to_classes: "Back to Classes" -# finish_signup: "Finish creating your teacher account:" + back_to_classes: "Quay lại Lớp học" + finish_signup: "Hoàn thiện tài khoản giáo viên:" # finish_signup_p: "Create an account to set up a class, add your students, and monitor their progress as they learn computer science." signup_with: "Đăng ký bằng:" connect_with: "Kết nối với:" @@ -866,7 +866,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn # create_account_subtitle: "Get access to teacher-only tools for using CodeCombat in the classroom. Set up a class, add your students, and monitor their progress!" convert_account_title: "Năng cấp lên tài khoản Giáo Viên" not: "Không phải" -# setup_a_class: "Set Up a Class" + setup_a_class: "Thiết lập một Lớp học" versions: save_version_title: "Lưu phiên bản mới" @@ -922,10 +922,10 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn email_announcements: "Thông báo" email_announcements_description: "Nhận email về tin tức mới nhất và sự phát triển của Codecombat." email_notifications: "Thông báo" -# email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity." -# email_any_notes: "Any Notifications" -# email_any_notes_description: "Disable to stop all activity notification emails." -# email_news: "News" + email_notifications_summary: "Kiểm soát các thông báo cá nhân tự động liên quan đến các hoạt động của CodeCombat." + email_any_notes: "Bất kỳ thông báo nào" + email_any_notes_description: "Vô hiệu hóa để ngừng mọi thông báo qua email." + email_news: "Tin tức" email_recruit_notes: "Cơ hội việc làm" email_recruit_notes_description: "Nếu bạn chơi trò này rất giỏi, chúng tôi có thể sẽ liên lạc với bạn về cơ hội nghề nghiệp." contributor_emails: "Email tham gia đóng góp" @@ -957,7 +957,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn # toggle_grid: "Toggle grid overlay." # toggle_pathfinding: "Toggle pathfinding overlay." # beautify: "Beautify your code by standardizing its formatting." -# maximize_editor: "Maximize/minimize code editor." + maximize_editor: "Phong to/thu nhỏ trình soạn thảo code." community: main_title: "Cộng đồng CodeCombat" @@ -1000,8 +1000,8 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn edit_description: "sửa mô tả" private: "(kín)" summary: "Tóm tắt" - average_level: "Cấp độ trng bình" -# average_achievements: "Average Achievements" + average_level: "Cấp độ trung bình" + average_achievements: "Thành tựu trung bình" delete_clan: "Xóa Clan" leave_clan: "Rời Clan" join_clan: "Tham gia Clan" @@ -1013,29 +1013,29 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn started_1: "đã bắt đầu" complete_1: "hoàn thành" # exp_levels: "Expand levels" -# rem_hero: "Remove Hero" -# status: "Status" + rem_hero: "Xóa Tướng" + status: "Trạng thái" complete_2: "Hoàn thành" started_2: "Đã bắt đầu" not_started_2: "Chưa bắt đầu" view_solution: "Click để xem lời giải." -# view_attempt: "Click to view attempt." -# latest_achievement: "Latest Achievement" + view_attempt: "Click để xem thử." + latest_achievement: "Thành tựu mới nhất" playtime: "Thời gian chơi" last_played: "Lần chơi cuối" # leagues_explanation: "Play in a league against other clan members in these multiplayer arena instances." # track_concepts1: "Track concepts" # track_concepts2a: "learned by each student" # track_concepts2b: "learned by each member" -# track_concepts3a: "Track levels completed for each student" -# track_concepts3b: "Track levels completed for each member" + track_concepts3a: "Theo dõi các màn chơi được hoàn thành bởi mỗi học viên" + track_concepts3b: "Theo dõi các màn chơi được hoàn thành bởi mỗi thành viên" track_concepts4a: "Xem các học viên của bạn'" track_concepts4b: "Xem các thành viên của bạn'" track_concepts5: "lời giải" track_concepts6a: "Sắp xếp học viên theo tên hoặc tiến trình" track_concepts6b: "Sắp xếp thành viên theo tên hoặc tiến trình" -# track_concepts7: "Requires invitation" -# track_concepts8: "to join" + track_concepts7: "Yêu cầu lời mời" + track_concepts8: "để tham gia" private_require_sub: "Các Clan kín cần mua subscription để tạo hoặc tham gia." courses: @@ -1047,7 +1047,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn visit_suf: "để tham gia." select_class: "Chọn một trong các lớp học của bạn" unnamed: "*unnamed*" -# select: "Select" + select: "Lựa chọn" unnamed_class: "Lớp học chưa đặt tên" edit_settings: "thay đổi tùy chỉnh lớp học" edit_settings1: "Thay đổi tùy chỉnh lớp học" @@ -1055,24 +1055,24 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn add_students: "Thêm học sinh" stats: "Thống kê" total_students: "Tổng số học sinh:" -# average_time: "Average level play time:" + average_time: "Thời gian chơi trung bình:" total_time: "Tổng thời gian chơi:" average_levels: "Lượng cấp độ trung bình đã hoàn thành:" total_levels: "Tổng số cấp độ đã hoàn thành" -# furthest_level: "Furthest level completed:" -# students: "Students" -# students1: "students" -# concepts: "Concepts" -# levels: "levels" -# played: "Played" -# play_time: "Play time:" -# completed: "Completed:" -# invite_students: "Invite students to join this class." -# invite_link_header: "Link to join course" + furthest_level: "Màn chơi xa nhất đã hoàn thành:" + students: "Học viên" + students1: "học viên" + concepts: "Các khái niệm" + levels: "màn chơi" + played: "Đã chơi" + play_time: "Thời gian chơi:" + completed: "Đã hoàn thành:" + invite_students: "Mời các học viên tham gia lớp học này." + invite_link_header: "Đường link để tham gia khóa học" # invite_link_p_1: "Give this link to students you would like to have join the course." # invite_link_p_2: "Or have us email them directly:" -# capacity_used: "Course slots used:" -# enter_emails: "Enter student emails to invite, one per line" + capacity_used: "Số chỗ đã đăng ký:" + enter_emails: "Nhập email học viên để gửi lời mời, mỗi email một dòng" send_invites: "Gửi lời mời" creating_class: "Đang tạo lớp..." purchasing_course: "Đang mua khóa học..." @@ -1086,15 +1086,15 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn name_class: "Đặt tên lớp của bạn" # displayed_course_page: "This will be displayed on the course page for you and your students. It can be changed later." buy: "Mua" -# purchasing_for: "You are purchasing a license for" -# creating_for: "You are creating a class for" -# for: "for" # Like in 'for 30 students' + purchasing_for: "Bạn đang muc giấy phép cho" + creating_for: "Bạn đang tạo một lớp cho" + for: "dành cho" # Like in 'for 30 students' # receive_code: "Afterwards you will receive an unlock code to distribute to your students, which they can use to enroll in your class." # free_trial: "Free trial for teachers!" # get_access: "to get individual access to all courses for evalutaion purposes." -# questions: "Questions?" -# teachers_click: "Teachers Click Here" -# students_click: "Students Click Here" + questions: "Có câu hỏi?" + teachers_click: "Giáo viên click vào đây" + students_click: "Học viên click vào đây" courses_on_coco: "Những khóa học trên CodeCombat" # designed_to: "Courses are designed to introduce computer science concepts using CodeCombat's fun and engaging environment. CodeCombat levels are organized around key topics to encourage progressive learning, over the course of 5 hours." # more_in_less: "Learn more in less time" @@ -1406,7 +1406,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn # level_title: "Level Editor" # achievement_title: "Achievement Editor" # poll_title: "Poll Editor" -# back: "Back" + back: "Quay lại" # revert: "Revert" # revert_models: "Revert Models" pick_a_terrain: "Chọn Địa Hình" @@ -1418,8 +1418,8 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn glacier: "Núi băng" small: "Bé" large: "Lớn" -# fork_title: "Fork New Version" -# fork_creating: "Creating Fork..." + fork_title: "Fork phiên bản mới" + fork_creating: "Đang tạo Fork..." # generate_terrain: "Generate Terrain" more: "Thêm" wiki: "Wiki" From 13ca4a6174ab47ac698883a4c8cad2aa81de74e3 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 2 Jun 2016 10:59:33 -0700 Subject: [PATCH 089/167] Fix intermittent CreateAccountModal error AuthModal spec was loading the G+ API during tests, which caused G+ to get unhappy when CreateAccountModal mocked it. --- test/app/views/core/AuthModal.spec.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/app/views/core/AuthModal.spec.coffee b/test/app/views/core/AuthModal.spec.coffee index 2bc461f0a..2fe5207f5 100644 --- a/test/app/views/core/AuthModal.spec.coffee +++ b/test/app/views/core/AuthModal.spec.coffee @@ -6,6 +6,8 @@ describe 'AuthModal', -> modal = null beforeEach -> + application.facebookHandler.fakeAPI() + application.gplusHandler.fakeAPI() modal = new AuthModal() modal.render() @@ -20,4 +22,4 @@ describe 'AuthModal', -> expect(args[0] instanceof RecoverModal).toBeTruthy() it '(demo)', -> - jasmine.demoModal(modal) \ No newline at end of file + jasmine.demoModal(modal) From 50ee20d3aef5303163e2866068b9053f52d09277 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 2 Jun 2016 11:13:47 -0700 Subject: [PATCH 090/167] Send teacher contact us form emails through Close.io --- .../teachers/TeachersContactModal.coffee | 2 +- server/lib/closeIO.coffee | 33 +++++++++-- server/routes/contact.coffee | 58 +++++++++++-------- 3 files changed, 64 insertions(+), 29 deletions(-) diff --git a/app/views/teachers/TeachersContactModal.coffee b/app/views/teachers/TeachersContactModal.coffee index 57ee0bc58..ec19bd824 100644 --- a/app/views/teachers/TeachersContactModal.coffee +++ b/app/views/teachers/TeachersContactModal.coffee @@ -55,7 +55,7 @@ module.exports = class TeachersContactModal extends ModalView return unless _.isEmpty(formErrors) @state.set('sendingState', 'sending') - data = _.extend({ country: me.get('country'), recipientID: 'schools@codecombat.com' }, formValues) + data = _.extend({ country: me.get('country'), recipientID: 'schools@codecombat.com', enrollmentsNeeded: @enrollmentsNeeded }, formValues) contact.send({ data context: @ diff --git a/server/lib/closeIO.coffee b/server/lib/closeIO.coffee index be254091f..5e15b92a5 100644 --- a/server/lib/closeIO.coffee +++ b/server/lib/closeIO.coffee @@ -57,7 +57,7 @@ module.exports = return done(error) if error leads = JSON.parse(body) return done("Unexpected leads format: " + body) unless leads.data? - return done(null, config.mail.supportSchools) unless leads.data?.length > 0 + return done("No existing Close.IO lead found for #{email}") unless leads.data?.length > 0 lead = leads.data[0] uri = "https://#{apiKey}:X@app.close.io/api/v1/activity/?lead_id=#{lead.id}" request.get uri, (error, response, body) => @@ -65,8 +65,33 @@ module.exports = activities = JSON.parse(body) return done("Unexpected activities format: " + body) unless activities.data? for activity in activities.data when activity._type is 'Email' - return done(null, activity.sender) if /@codecombat\.com/ig.test(activity.sender) - return done(null, config.mail.supportSchools) + if /@codecombat\.com/ig.test(activity.sender) and not activity.sender?.indexOf(config.mail.username) >= 0 + return done(null, activity.sender, lead.id) + return done(null, config.mail.supportSchools, lead.id) catch error log.error("closeIO.getSalesContactEmail Error for #{email}: #{JSON.stringify(error)}") - return done(error, config.mail.supportSchools) + return done(error) + + sendMail: (fromAddress, subject, content, done) -> + # log.info("DEBUG: closeIO.sendMail #{fromAddress} #{subject} #{content}") + @getSalesContactEmail fromAddress, (err, salesContactEmail, leadID) -> + return done("Error getting sales contact for #{fromAddress}: #{err}") if err + matches = salesContactEmail.match(/^[a-zA-Z_]+ <(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3})>$|(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3})/i) + salesContactEmail = matches?[1] ? matches?[2] ? config.mail.supportSchools + postData = + to: [salesContactEmail] + sender: config.mail.username + subject: subject + body_text: content + lead_id: leadID + status: 'outbox' + options = + uri: "https://#{apiKey}:X@app.close.io/api/v1/activity/email/" + body: JSON.stringify(postData) + request.post options, (error, response, body) => + return done(error) if error + result = JSON.parse(body); + if result.errors or result['field-errors'] + errorMessage = "Close.io Send email POST error for #{fromAddress} #{JSON.stringify(result.errors)} #{JSON.stringify(result['field-errors'])}"; + return done(errorMessage) + return done() diff --git a/server/routes/contact.coffee b/server/routes/contact.coffee index cce6e96e3..651639bcb 100644 --- a/server/routes/contact.coffee +++ b/server/routes/contact.coffee @@ -11,21 +11,27 @@ module.exports.setup = (app) -> app.post '/contact', (req, res) -> return res.end() unless req.user # log.info "Sending mail from #{req.body.email} saying #{req.body.message}" - createMailContext req, (context) -> - sendwithus.api.send context, (err, result) -> - if err - log.error "Error sending contact form email: #{err.message or err}" + fromAddress = req.body.sender or req.body.email or req.user.get('email') + createMailContent req, fromAddress, (subject, content) -> + if req.body.recipientID is 'schools@codecombat.com' or req.user.isTeacher() + req.user.update({$set: { enrollmentRequestSent: true }}).exec(_.noop) if req.body.recipientID is 'schools@codecombat.com' + closeIO.sendMail fromAddress, subject, content, (err) -> + log.error "Error sending contact form email via Close.io: #{err.message or err}" if err + else + createSendWithUsContext req, fromAddress, subject, content, (context) -> + sendwithus.api.send context, (err, result) -> + log.error "Error sending contact form email via sendwithus: #{err.message or err}" if err return res.end() -createMailContext = (req, done) -> - sender = req.body.sender or req.body.email +createMailContent = (req, fromAddress, done) -> + country = req.body.country + enrollmentsNeeded = req.body.enrollmentsNeeded message = req.body.message user = req.user - recipientID = req.body.recipientID - subject = req.body.subject - country = req.body.country - sentFromLevel = levelID: req.body.levelID, courseID: req.body.courseID, courseInstanceID: req.body.courseInstanceID - + subject = switch + when enrollmentsNeeded then "#{enrollmentsNeeded} Licenses needed for #{fromAddress}" + when req.body.subject then req.body.subject + else "Contact Us Form: #{fromAddress}" level = if user?.get('points') > 0 then Math.floor(5 * Math.log((1 / 100) * (user.get('points') + 100))) + 1 else 0 premium = user?.isPremium() teacher = user?.isTeacher() @@ -34,15 +40,25 @@ createMailContext = (req, done) -> -- http://codecombat.com/user/#{user.get('slug') or user.get('_id')} - #{user.get('name') or 'Anonymous'} - Level #{level}#{if teacher then ' - Teacher' else ''}#{if premium then ' - Subscriber' else ''}#{if country then ' - ' + country else ''} + #{fromAddress} - #{user.get('name') or 'Anonymous'} - Level #{level}#{if teacher then ' - Teacher' else ''}#{if premium then ' - Subscriber' else ''}#{if country then ' - ' + country else ''} """ if req.body.browser content += "\n#{req.body.browser} - #{req.body.screenSize}" + done(subject, content) + +createSendWithUsContext = (req, fromAddress, subject, content, done) -> + user = req.user + recipientID = req.body.recipientID + sentFromLevel = levelID: req.body.levelID, courseID: req.body.courseID, courseInstanceID: req.body.courseInstanceID + premium = user?.isPremium() + teacher = user?.isTeacher() + + if recipientID is 'schools@codecombat.com' or teacher + return done("Tried to send a teacher contact us email via sendwithus #{fromAddress} #{subject}") toAddress = switch when premium then config.mail.supportPremium else config.mail.supportPrimary - fromAddress = sender or user.get('email') context = email_id: sendwithus.templates.plain_text_email @@ -53,30 +69,24 @@ createMailContext = (req, done) -> reply_to: fromAddress name: user.get('name') email_data: - subject: "[CodeCombat] #{subject ? ('Feedback - ' + fromAddress)}" + subject: subject content: content contentHTML: content.replace /\n/g, '\n
' - if recipientID is 'schools@codecombat.com' or teacher - req.user.update({$set: { enrollmentRequestSent: true }}).exec(_.noop) if recipientID is 'schools@codecombat.com' - closeIO.getSalesContactEmail fromAddress, (err, salesContactEmail) -> - console.error "Error getting sales contact for #{sender}: #{err}" if err - context.recipient.address = salesContactEmail ? config.mail.supportSchools - done context - else if recipientID and (user.isAdmin() or ('employer' in (user.get('permissions') ? []))) + if recipientID and (user.isAdmin() or ('employer' in (user.get('permissions') ? []))) User.findById(recipientID, 'email').exec (err, document) -> if err log.error "Error looking up recipient to email from #{recipientID}: #{err}" if err else - context.recipient.bcc = [context.recipient.address, sender] + context.recipient.bcc = [context.recipient.address, fromAddress] context.recipient.address = document.get('email') - context.email_data.content = message + context.email_data.content = content done context else async.waterfall [ fetchRecentSessions.bind undefined, user, context, sentFromLevel # Can add other data-grabbing stuff here if we want. ], (err, results) -> - console.error "Error getting contact message context for #{sender}: #{err}" if err + console.error "Error getting contact message context for #{fromAddress}: #{err}" if err if req.body.screenshotURL context.email_data.contentHTML += "\n
" done context From ae3b8057f7de840f2afd157f32ee54706278bfd9 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Thu, 2 Jun 2016 12:51:05 -0700 Subject: [PATCH 091/167] Detect URL classCode in createAccountModal --- app/views/core/CreateAccountModal.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/core/CreateAccountModal.coffee b/app/views/core/CreateAccountModal.coffee index a87e220b5..c7150695c 100644 --- a/app/views/core/CreateAccountModal.coffee +++ b/app/views/core/CreateAccountModal.coffee @@ -7,6 +7,7 @@ application = require 'core/application' Classroom = require 'models/Classroom' errors = require 'core/errors' COPPADenyModal = require 'views/core/COPPADenyModal' +utils = require 'core/utils' module.exports = class CreateAccountModal extends ModalView @@ -28,6 +29,8 @@ module.exports = class CreateAccountModal extends ModalView initialize: (options={}) -> @onNameChange = _.debounce(_.bind(@checkNameExists, @), 500) + options.initialValues ?= {} + options.initialValues?.classCode ?= utils.getQueryVariable('_cc', "") @previousFormInputs = options.initialValues or {} # TODO: Switch to promises and state, rather than using defer to hackily enable buttons after render From 51a37d9edc863d3163512b43fd063b641ed03c90 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 2 Jun 2016 15:32:54 -0700 Subject: [PATCH 092/167] Update licenses contact us form email routing --- server/lib/closeIO.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/server/lib/closeIO.coffee b/server/lib/closeIO.coffee index 5e15b92a5..cb4825109 100644 --- a/server/lib/closeIO.coffee +++ b/server/lib/closeIO.coffee @@ -78,6 +78,7 @@ module.exports = return done("Error getting sales contact for #{fromAddress}: #{err}") if err matches = salesContactEmail.match(/^[a-zA-Z_]+ <(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3})>$|(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3})/i) salesContactEmail = matches?[1] ? matches?[2] ? config.mail.supportSchools + salesContactEmail = config.mail.supportSchools if salesContactEmail?.indexOf('brian@codecombat.com') >= 0 postData = to: [salesContactEmail] sender: config.mail.username From 3485abd41363eaf272691aa766be9da35a5b1c06 Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 2 Jun 2016 18:46:47 -0700 Subject: [PATCH 093/167] Don't show indent guides for incorrect python indention. --- app/views/play/level/tome/SpellView.coffee | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index 743b51a41..8de3a5bc4 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -314,13 +314,20 @@ module.exports = class SpellView extends CocoView if /^\s+$/.test lines[docRange.end.row+1] docRange.end.row += 1 + xstart = startOfRow(row) + if language is 'python' + requiredIndent = new RegExp '^' + new Array(xstart / 4 + 2).join '( |\t)' + '(\\S|\\s*$)' + console.log requiredIndent + for crow in [docRange.start.row+1..docRange.end.row] + console.log("CROW", xstart, crow, lines[crow]) + unless requiredIndent.test lines[crow] + docRange.end.row = crow - 1 + break + rstart = @aceSession.documentToScreenPosition docRange.start.row, docRange.start.column rend = @aceSession.documentToScreenPosition docRange.end.row, docRange.end.column range = new Range rstart.row, rstart.column, rend.row, rend.column - - xstart = startOfRow(row) level = Math.floor(xstart / 4) - indent = startOfRow(row + 1) color = colors[level % colors.length] bw = 3 to = markerLayer.$getTop(range.start.row, config) From e7f221813e247b28df3e6da48341d4e42f042c1e Mon Sep 17 00:00:00 2001 From: duybkict Date: Fri, 3 Jun 2016 09:23:35 +0700 Subject: [PATCH 094/167] remove constructor, handle load async process --- app/views/ladder/LadderView.coffee | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/views/ladder/LadderView.coffee b/app/views/ladder/LadderView.coffee index 5a2e5d69b..28076bc4a 100644 --- a/app/views/ladder/LadderView.coffee +++ b/app/views/ladder/LadderView.coffee @@ -39,12 +39,10 @@ module.exports = class LadderView extends RootView 'click a:not([data-toggle])': 'onClickedLink' 'click .spectate-button': 'onClickSpectateButton' - constructor: (options, @levelID, @leagueType, @leagueID) -> - super(options) - - initialize: -> + initialize: (options, @levelID, @leagueType, @leagueID) -> @level = @supermodel.loadModel(new Level(_id: @levelID)).model - @levelDescription = marked(@level.get('description')) if @level.get('description') + @level.once 'sync', => + @levelDescription = marked(@level.get('description')) if @level.get('description') @sessions = @supermodel.loadCollection(new LevelSessionsCollection(@levelID), 'your_sessions', {cache: false}).model @winners = require('./tournament_results')[@levelID] From 8d2df0baff90cce5efcae3e8a5d8847e445889d0 Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 2 Jun 2016 20:17:52 -0700 Subject: [PATCH 095/167] Kill some console logs @poojawins --- app/views/play/level/tome/SpellView.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index 8de3a5bc4..2745e4777 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -317,9 +317,7 @@ module.exports = class SpellView extends CocoView xstart = startOfRow(row) if language is 'python' requiredIndent = new RegExp '^' + new Array(xstart / 4 + 2).join '( |\t)' + '(\\S|\\s*$)' - console.log requiredIndent for crow in [docRange.start.row+1..docRange.end.row] - console.log("CROW", xstart, crow, lines[crow]) unless requiredIndent.test lines[crow] docRange.end.row = crow - 1 break From a12c9402f40dea44198b6f40e42999d19ac3bb30 Mon Sep 17 00:00:00 2001 From: Oliver Harald Jessner Date: Fri, 3 Jun 2016 15:23:46 +0200 Subject: [PATCH 096/167] Adding some German words (#3699) In some strings the contexts are missing, so its hard to translate. --- app/locale/de-AT.coffee | 110 ++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/app/locale/de-AT.coffee b/app/locale/de-AT.coffee index a9eb7afc1..d779b5509 100644 --- a/app/locale/de-AT.coffee +++ b/app/locale/de-AT.coffee @@ -4,7 +4,7 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: no_ie: "CodeCombat läuft nicht im IE8 oder älteren Browsern. Tut uns Leid!" # Warning that only shows up in IE8 and older no_mobile: "CodeCombat ist nicht für Mobilgeräte optimiert und funktioniert möglicherweise nicht." # Warning that shows up on mobile devices play: "Spielen" # The big play button that opens up the campaign view. -# play_campaign_version: "Play Campaign Version" # Shows up under big play button if you only play /courses + play_campaign_version: "Spiele Kampagnen Version" # Shows up under big play button if you only play /courses old_browser: "Oh! Dein Browser ist zu alt für CodeCombat. Sorry!" # Warning that shows up on really old Firefox/Chrome/Safari old_browser_suffix: "Du kannst es trotzdem versuchen, aber es wird wahrscheinlich nicht funktionieren." ipad_browser: "Schlechte Nachricht: CodeCombat funktioniert im iPad-Browser nicht. Gute Nachricht: Unsere iPad App wartet auf das OK von Apple." @@ -102,7 +102,7 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: blog: "Blog" forum: "Forum" account: "Account" -# my_account: "My Account" + my_account: "Mein Account" profile: "Profil" stats: "Statistiken" code: "Code" @@ -112,24 +112,24 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: about: "Über" contact: "Kontakt" twitter_follow: "Twitter" -# students: "Students" + students: "Studenten" teachers: "Lehrer" # careers: "Careers" -# facebook: "Facebook" -# twitter: "Twitter" -# create_a_class: "Create a Class" -# other: "Other" -# learn_to_code: "Learn to Code!" -# toggle_nav: "Toggle navigation" -# jobs: "Jobs" -# schools: "Schools" -# educator_wiki: "Educator Wiki" -# get_involved: "Get Involved" -# open_source: "Open source (GitHub)" -# support: "Support" -# faqs: "FAQs" -# help_pref: "Need help? Email" -# help_suff: "and we'll get in touch!" + facebook: "Facebook" + twitter: "Twitter" + create_a_class: "Erstelle eine Klasse" + other: "Andere" + learn_to_code: "Lerne zu programmieren!" + toggle_nav: "Navigation umschalten" + jobs: "Jobs" + schools: "Schule" + educator_wiki: "Educator Wiki" + get_involved: "Werde eingebunden" + open_source: "Open source (GitHub)" + support: "Support" + faqs: "FAQs" + help_pref: "Brauchst du Hilfe? Email" + help_suff: "und wir treten in Kontakt!" modal: close: "Schließen" @@ -148,18 +148,18 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: play: play_as: "Spiele als " # Ladder page -# compete: "Compete!" # Course details page + compete: "Abgeschlossen!" # Course details page spectate: "Zuschauen" # Ladder page players: "Spieler" # Hover over a level on /play hours_played: "Stunden gespielt" # Hover over a level on /play items: "Gegenstände" # Tooltip on item shop button from /play -# unlock: "Unlock" # For purchasing items and heroes + unlock: "Kaufen" # For purchasing items and heroes confirm: "Bestätigen" -# owned: "Owned" # For items you own + owned: "bereits im Besitz" # For items you own locked: "Gesperrt" -# purchasable: "Purchasable" # For a hero you unlocked but haven't purchased + purchasable: "Erhältlich" # For a hero you unlocked but haven't purchased available: "Verfügbar" -# skills_granted: "Skills Granted" # Property documentation details + skills_granted: "Freischaltbare Fähigkeiten" # Property documentation details heroes: "Helden" # Tooltip on hero shop button from /play achievements: "Erfolge" # Tooltip on achievement list button from /play account: "Account" # Tooltip on account button from /play @@ -168,15 +168,15 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: next: "Weiter" # Go from choose hero to choose inventory before playing a level change_hero: "Held wechseln" # Go back from choose inventory to choose hero buy_gems: "Juwelen kaufen" -# subscription_required: "Subscription Required" + subscription_required: "Abonnement benötigt" anonymous: "Anonymer Spieler" level_difficulty: "Schwierigkeit: " -# play_classroom_version: "Play Classroom Version" # Choose a level in campaign version that you also can play in one of your courses + play_classroom_version: "Spiele Kurs Version" # Choose a level in campaign version that you also can play in one of your courses campaign_beginner: "Anfängerkampagne" -# awaiting_levels_adventurer_prefix: "We release new levels every week." -# awaiting_levels_adventurer: "Sign up as an Adventurer" + awaiting_levels_adventurer_prefix: "Wir veröffentlichen wöchentlichen neu Levels." + awaiting_levels_adventurer: "Melde dich als Abenteuerer an" # awaiting_levels_adventurer_suffix: "to be the first to play new levels." -# adjust_volume: "Adjust volume" + adjust_volume: "Lautstärke anpassen" campaign_multiplayer: "Multiplayerarena" campaign_multiplayer_description: "... in der Du Kopf-an-Kopf gegen andere Spieler programmierst." # campaign_old_multiplayer: "(Deprecated) Old Multiplayer Arenas" @@ -260,18 +260,18 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: log_in: "mit Passwort einloggen" required: "Du musst dich vorher einloggen um dort hin zu gehen." login_switch: "Du hast bereits einen Account?" -# school_name: "School Name and City" -# optional: "optional" -# school_name_placeholder: "Example High School, Springfield, IL" -# or_sign_up_with: "or sign up with" -# connected_gplus_header: "You've successfully connected with Google+!" -# connected_gplus_p: "Finish signing up so you can log in with your Google+ account." -# gplus_exists: "You already have an account associated with Google+!" -# connected_facebook_header: "You've successfully connected with Facebook!" -# connected_facebook_p: "Finish signing up so you can log in with your Facebook account." -# facebook_exists: "You already have an account associated with Facebook!" -# hey_students: "Students, enter the class code from your teacher." -# birthday: "Birthday" + school_name: "Schulname und Stadt" + optional: "Optional" + school_name_placeholder: "Beispiel Schule, Springfield, IL" + or_sign_up_with: "oder melde dich an mit" + connected_gplus_header: "Du hast dich erfolgreich über Google+ verbunden!" + connected_gplus_p: "Vervollständige die Anmeldung um dich mit deinem Google+ Account anzumelden." + gplus_exists: "Du hast bereits einen verknüpften Google+ Account!" + connected_facebook_header: "Du hast dich erfolgreich über Facebook verbunden!" + connected_facebook_p: "Vervollständige die Anmeldung um dich mit deinem Facebook Account anzumelden." + facebook_exists: "Du hast bereits einen verknüpften Facebook Account!" + hey_students: "Schüler, gebt den Klassencode eures Lehrers ein." + birthday: "Geburstag" recover: recover_account_title: "Account Wiederherstellung" @@ -1870,18 +1870,18 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: oreilly: "Ebook deiner Wahl" # calendar: -# year: "Year" -# day: "Day" -# month: "Month" -# january: "January" -# february: "February" -# march: "March" -# april: "April" -# may: "May" -# june: "June" -# july: "July" -# august: "August" -# september: "September" -# october: "October" -# november: "November" -# december: "December" + year: "Jahr" + day: "Tag" + month: "Monat" + january: "Januar" + february: "Februar" + march: "März" + april: "April" + may: "Mai" + june: "Juni" + july: "Juli" + august: "August" + september: "September" + october: "Oktober" + november: "November" + december: "Dezember" From 5a6fe46b22a86f1d8c5ba94fd3b16b684cd60f10 Mon Sep 17 00:00:00 2001 From: Imperadeiro98 Date: Fri, 3 Jun 2016 14:25:20 +0100 Subject: [PATCH 097/167] Uncomment an header in de-AT.coffee --- app/locale/de-AT.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/locale/de-AT.coffee b/app/locale/de-AT.coffee index d779b5509..d74c7be3b 100644 --- a/app/locale/de-AT.coffee +++ b/app/locale/de-AT.coffee @@ -1869,7 +1869,7 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: license: "Lizenz" oreilly: "Ebook deiner Wahl" -# calendar: + calendar: year: "Jahr" day: "Tag" month: "Monat" From 6829d60b80d965e3c58f2a40faf8ceab3ff1b34b Mon Sep 17 00:00:00 2001 From: Oliver Harald Jessner Date: Fri, 3 Jun 2016 21:02:26 +0200 Subject: [PATCH 098/167] Did more translations (#3711) - Finished the code section - Finished general section - nearly finished the play section - Still don't understand which/what kind of `"Poll" # Tooltip on poll button from /play` is meant. --- app/locale/de-AT.coffee | 144 ++++++++++++++++++++-------------------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/app/locale/de-AT.coffee b/app/locale/de-AT.coffee index d74c7be3b..8e1ae0845 100644 --- a/app/locale/de-AT.coffee +++ b/app/locale/de-AT.coffee @@ -97,7 +97,7 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: nav: play: "Spielen" # The top nav bar entry where players choose which levels to play community: "Community" -# courses: "Courses" + courses: "Kurse" editor: "Editor" blog: "Blog" forum: "Forum" @@ -114,7 +114,7 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: twitter_follow: "Twitter" students: "Studenten" teachers: "Lehrer" -# careers: "Careers" + careers: "Berufe" facebook: "Facebook" twitter: "Twitter" create_a_class: "Erstelle eine Klasse" @@ -175,62 +175,62 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: campaign_beginner: "Anfängerkampagne" awaiting_levels_adventurer_prefix: "Wir veröffentlichen wöchentlichen neu Levels." awaiting_levels_adventurer: "Melde dich als Abenteuerer an" -# awaiting_levels_adventurer_suffix: "to be the first to play new levels." + awaiting_levels_adventurer_suffix: "um der erste zu sein der neue Levels spielt." adjust_volume: "Lautstärke anpassen" campaign_multiplayer: "Multiplayerarena" campaign_multiplayer_description: "... in der Du Kopf-an-Kopf gegen andere Spieler programmierst." -# campaign_old_multiplayer: "(Deprecated) Old Multiplayer Arenas" -# campaign_old_multiplayer_description: "Relics of a more civilized age. No simulations are run for these older, hero-less multiplayer arenas." + campaign_old_multiplayer: "(Veraltet) Alte Multiplayer Arenen" + campaign_old_multiplayer_description: "Relikte eines zivilisierteren Zeitalters. Es werden keine Simulationen für diese älteren, heldenlosen Mehrspieler Arenen durchgeführt." # code: -# if: "if" # Keywords--these translations show up on hover, so please translate them all, even if it's kind of long. (In the code editor, they will still be in English.) -# else: "else" -# elif: "else if" -# while: "while" -# loop: "loop" -# for: "for" -# break: "break" -# continue: "continue" -# pass: "pass" -# return: "return" -# then: "then" -# do: "do" -# end: "end" -# function: "function" -# def: "define" -# var: "variable" -# self: "self" -# hero: "hero" -# this: "this" -# or: "or" -# "||": "or" -# and: "and" -# "&&": "and" -# not: "not" -# "!": "not" -# "=": "assign" -# "==": "equals" -# "===": "strictly equals" -# "!=": "does not equal" -# "!==": "does not strictly equal" -# ">": "is greater than" -# ">=": "is greater than or equal" -# "<": "is less than" -# "<=": "is less than or equal" -# "*": "multiplied by" -# "/": "divided by" -# "+": "plus" -# "-": "minus" -# "+=": "add and assign" -# "-=": "subtract and assign" -# True: "True" -# true: "true" -# False: "False" -# false: "false" -# undefined: "undefined" -# null: "null" -# nil: "nil" -# None: "None" + if: "wenn" # Keywords--these translations show up on hover, so please translate them all, even if it's kind of long. (In the code editor, they will still be in English.) + else: "sonst" + elif: "sonst wenn" + while: "so lange" + loop: "schleife" + for: "für" + break: "pause" + continue: "fortsetzen" + pass: "bestehen" + return: "zurückgeben" + then: "dann" + do: "mache" + end: "ende" + function: "funktion" + def: "definiere" + var: "variable" + self: "selbst" + hero: "held" + this: "dies" + or: "oder" + "||": "oder" + and: "und" + "&&": "und" + not: "nicht" + "!": "nicht" + "=": "zuweisen" + "==": "entspricht" + "===": "entspricht strikt" + "!=": "entspricht nicht" + "!==": "entspricht strikt nicht" + ">": "ist größer als" + ">=": "ist größer als oder gleich" + "<": "ist kleiner als" + "<=": "ist kleiner als oder gleich" + "*": "multipliziert um" + "/": "dividiert durch" + "+": "plus" + "-": "minus" + "+=": "addieren und zuweisen" + "-=": "subtrahieren und zuweisen" + True: "Wahr" + true: "wahr" + False: "Falsch" + false: "falsch" + undefined: "undefiniert" + null: "nichts" + nil: "nichts" + None: "Kein" # share_progress_modal: # blurb: "You’re making great progress! Tell your parent how much you've learned with CodeCombat." @@ -316,23 +316,23 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: date: "Datum" body: "Inhalt" version: "Version" -# pending: "Pending" -# accepted: "Accepted" -# rejected: "Rejected" -# withdrawn: "Withdrawn" -# accept: "Accept" -# reject: "Reject" -# withdraw: "Withdraw" + pending: "Ausstehend" + accepted: "Akzeptiert" + rejected: "Abgelehnt" + withdrawn: "Zurückgezogen" + accept: "Akzeptieren" + reject: "Abgelehnen" + withdraw: "Zurückziehen" submitter: "Veröffentlicher" submitted: "Veröffentlicht" commit_msg: "Nachricht absenden" version_history: "Versionshistorie" version_history_for: "Versionsgeschichte für: " -# select_changes: "Select two changes below to see the difference." -# undo_prefix: "Undo" -# undo_shortcut: "(Ctrl+Z)" -# redo_prefix: "Redo" -# redo_shortcut: "(Ctrl+Shift+Z)" + select_changes: "Wählen zwei Änderungen unterhalb um den Unterschied zu sehen." + undo_prefix: "Rückgängig" + undo_shortcut: "(Strg+Z)" + redo_prefix: "Wiederherstellen" + redo_shortcut: "(Strg+Umschalt+Z)" play_preview: "Vorschau vom aktuellen Level spielen" result: "Ergebnis" results: "Ergebnisse" @@ -341,7 +341,7 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: subject: "Betreff" email: "Email" password: "Passwort" -# confirm_password: "Confirm Password" + confirm_password: "Passwort bestätigen" message: "Nachricht" code: "Code" ladder: "Rangliste" @@ -357,12 +357,12 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: hard: "Schwer" player: "Spieler" player_level: "Spielerlevel" # Like player level 5, not like level: Dungeons of Kithgard -# warrior: "Warrior" -# ranger: "Ranger" -# wizard: "Wizard" -# first_name: "First Name" -# last_name: "Last Name" -# username: "Username" + warrior: "Krieger" + ranger: "Jäger" + wizard: "Hexer" + first_name: "Vorname" + last_name: "Nachname" + username: "Benutzername" units: second: "Sekunde" From e8896e4d496bb5b1c2e09ed8a74d903cdc8982ed Mon Sep 17 00:00:00 2001 From: Imperadeiro98 Date: Fri, 3 Jun 2016 20:03:09 +0100 Subject: [PATCH 099/167] Uncomment header in de-AT.coffee --- app/locale/de-AT.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/locale/de-AT.coffee b/app/locale/de-AT.coffee index 8e1ae0845..5314735d0 100644 --- a/app/locale/de-AT.coffee +++ b/app/locale/de-AT.coffee @@ -182,7 +182,7 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: campaign_old_multiplayer: "(Veraltet) Alte Multiplayer Arenen" campaign_old_multiplayer_description: "Relikte eines zivilisierteren Zeitalters. Es werden keine Simulationen für diese älteren, heldenlosen Mehrspieler Arenen durchgeführt." -# code: + code: if: "wenn" # Keywords--these translations show up on hover, so please translate them all, even if it's kind of long. (In the code editor, they will still be in English.) else: "sonst" elif: "sonst wenn" From 5e1942c0d3701611b3f9fef4809def7b0cd21743 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Fri, 3 Jun 2016 13:30:52 -0700 Subject: [PATCH 100/167] Fix adding levels to campaigns --- app/views/editor/campaign/CampaignEditorView.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index 7a0920023..17b182574 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -158,7 +158,7 @@ module.exports = class CampaignEditorView extends RootView index = 0 for levelOriginal, campaignLevel of campaignLevels level = @levels.findWhere({original: levelOriginal}) - if level.get('campaignIndex') isnt index + if level and level.get('campaignIndex') isnt index level.set('campaignIndex', index) campaignLevel.campaignIndex = index index += 1 From 870ae9a8a1b40139ba30c952fe8e9c75476a8a86 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Wed, 25 May 2016 15:24:51 -0700 Subject: [PATCH 101/167] Add hero selector for courses mode Use selected hero in Course mode play Show selected hero on Courses (in progress) Add hero select modal Use short names, only show warriors Use box-shadow instead of borders Add tests for HeroSelectModal Refactor modal opening test Address code review feedback --- app/collections/ThangTypes.coffee | 7 +++- app/lib/LevelLoader.coffee | 7 ++++ app/locale/en.coffee | 3 ++ app/models/Level.coffee | 5 +++ app/models/ThangType.coffee | 19 +++++++++ app/styles/courses/courses-view.sass | 19 +++++++++ app/styles/courses/hero-select-modal.sass | 40 ++++++++++++++++++ app/templates/courses/courses-view.jade | 12 ++++++ app/templates/courses/hero-select-modal.jade | 27 ++++++++++++ app/views/courses/CoursesView.coffee | 25 ++++++++++- app/views/courses/HeroSelectModal.coffee | 42 +++++++++++++++++++ .../app/views/courses/CoursesView.spec.coffee | 32 ++++++++++++++ .../views/courses/HeroSelectModal.spec.coffee | 38 +++++++++++++++++ 13 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 app/styles/courses/hero-select-modal.sass create mode 100644 app/templates/courses/hero-select-modal.jade create mode 100644 app/views/courses/HeroSelectModal.coffee create mode 100644 test/app/views/courses/CoursesView.spec.coffee create mode 100644 test/app/views/courses/HeroSelectModal.spec.coffee diff --git a/app/collections/ThangTypes.coffee b/app/collections/ThangTypes.coffee index 8dd041307..0b030bf66 100644 --- a/app/collections/ThangTypes.coffee +++ b/app/collections/ThangTypes.coffee @@ -3,4 +3,9 @@ ThangType = require 'models/ThangType' module.exports = class ThangTypeCollection extends CocoCollection url: '/db/thang.type' - model: ThangType \ No newline at end of file + model: ThangType + + fetchHeroes: -> + @fetch { + url: '/db/thang.type?view=heroes' + } diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index 29a201235..34548f6dc 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -168,6 +168,13 @@ module.exports = class LevelLoader extends CocoClass @consolidateFlagHistory() if @opponentSession?.loaded else if session is @opponentSession @consolidateFlagHistory() if @session.loaded + if @level.get('type', true) in ['course'] # course-ladder is hard to handle because there's 2 sessions + heroConfig = me.get('heroConfig') + return if not heroConfig + url = "/db/thang.type/#{heroConfig.thangType}/version" + if heroResource = @maybeLoadURL(url, ThangType, 'thang') + @worldNecessities.push heroResource + return return unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] heroConfig = session.get('heroConfig') heroConfig ?= me.get('heroConfig') if session is @session and not @headless diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 15b00dba5..3e409016e 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -1314,6 +1314,9 @@ sent_verification: "We've sent a verification email to:" you_can_edit: "You can edit your email address in " account_settings: "Account Settings" + select_your_hero: "Select Your Hero" + select_your_hero_description: "You can always change your hero by going to your Courses page and clicking \"Select Hero\"" + select_this_hero: "Select this Hero" teacher: teacher_dashboard: "Teacher Dashboard" # Navbar diff --git a/app/models/Level.coffee b/app/models/Level.coffee index 975e34309..166b7b11d 100644 --- a/app/models/Level.coffee +++ b/app/models/Level.coffee @@ -145,6 +145,11 @@ module.exports = class Level extends CocoModel for original, placeholderComponent of placeholders when not placeholdersUsed[original] levelThang.components.push placeholderComponent + # Load the user's chosen hero AFTER getting stats from default char + if /Hero Placeholder/.test(levelThang.id) and @get('type', true) in ['course', 'course-ladder'] + heroThangType = me.get('heroConfig')?.thangType + levelThang.thangType = heroThangType if heroThangType + sortSystems: (levelSystems, systemModels) -> [sorted, originalsSeen] = [[], {}] visit = (system) -> diff --git a/app/models/ThangType.coffee b/app/models/ThangType.coffee index fe9ec9b1d..5e1dd3a6f 100644 --- a/app/models/ThangType.coffee +++ b/app/models/ThangType.coffee @@ -239,6 +239,25 @@ module.exports = class ThangType extends CocoModel portraitOnly = !!options.portraitOnly "#{@get('name')} - #{options.resolutionFactor} - #{colorConfigs} - #{portraitOnly}" + getHeroShortName: -> + map = { + "Assassin": "Ritic" + "Captain": "Anya" + "Forest Archer": "Naria" + "Goliath": "Okar" + "Guardian": "Illia" + "Knight": "Tharin" + "Librarian": "Hushbaum" + "Necromancer": "Nalfar" + "Ninja": "Amara" + "Potion Master": "Omarn" + "Raider": "Arryn" + "Samurai": "Hattori" + "Sorcerer": "Pender" + "Trapper": "Senick" + } + map[@get('name')] + getPortraitImage: (spriteOptionsOrKey, size=100) -> src = @getPortraitSource(spriteOptionsOrKey, size) return null unless src diff --git a/app/styles/courses/courses-view.sass b/app/styles/courses/courses-view.sass index ce74620d9..0189b033d 100644 --- a/app/styles/courses/courses-view.sass +++ b/app/styles/courses/courses-view.sass @@ -41,3 +41,22 @@ #join-class-form .alert, .progress margin-top: 20px + + // Hero display + .current-hero-container + display: flex + justify-content: center + + .current-hero-text + font-size: 16pt + + .hero-avatar + background-color: #f8f8f8 + box-shadow: 0 0 0 1px gray + margin-right: 25px + + .current-hero-right-col + display: flex + flex-direction: column + justify-content: space-between + align-items: flex-start diff --git a/app/styles/courses/hero-select-modal.sass b/app/styles/courses/hero-select-modal.sass new file mode 100644 index 000000000..de1e84f09 --- /dev/null +++ b/app/styles/courses/hero-select-modal.sass @@ -0,0 +1,40 @@ +@import "app/styles/style-flat-variables" + +#hero-select-modal + .modal-dialog + width: auto + max-width: 900px + + .modal-header, .modal-body:not(.secret), .modal-footer + display: flex + flex-direction: column + align-items: center + + .modal-footer + margin: 30px + + h4 + max-width: 500px + + .hero-list + display: flex + flex-wrap: wrap + justify-content: center + margin-bottom: -50px + + .hero-option + display: flex + flex-direction: column + align-items: center + margin: 0 50px 50px + + .hero-avatar + margin: 6px + background-color: #f8f8f8 + box-shadow: 0 0 0 1px gray + + .current .hero-avatar + box-shadow: 0 0 0 6px gray + + .selected .hero-avatar + box-shadow: 0 0 0 6px $gold diff --git a/app/templates/courses/courses-view.jade b/app/templates/courses/courses-view.jade index 0200ae6b6..11a03df1e 100644 --- a/app/templates/courses/courses-view.jade +++ b/app/templates/courses/courses-view.jade @@ -39,6 +39,18 @@ block content .text-center h1(data-i18n="courses.welcome_to_page") Welcome to your Courses page! + + .current-hero-container.text-center.row + .hero-avatar + img(src=view.hero.getPortraitURL()) + .current-hero-right-col + .semibold.current-hero-text + span.spr(data-i18n="TODO") + | Current Hero: + span.current-hero-name= view.hero.getHeroShortName() + button.change-hero-btn.btn.btn-lg.btn-forest + span(data-i18n="TODO") + | Change Hero if view.classrooms.size() h3.text-uppercase(data-i18n="courses.my_classes") diff --git a/app/templates/courses/hero-select-modal.jade b/app/templates/courses/hero-select-modal.jade new file mode 100644 index 000000000..9d918bb41 --- /dev/null +++ b/app/templates/courses/hero-select-modal.jade @@ -0,0 +1,27 @@ +extends /templates/core/modal-base-flat + +block modal-header-content + .text-center + h3(data-i18n="courses.select_your_hero") + h4(data-i18n="courses.select_your_hero_description") + +block modal-body-content + .hero-list + if view.heroes.loaded + each hero in view.heroes.models + if hero.get('heroClass') === 'Warrior' + +heroOption(hero) + +mixin heroOption(hero) + - var heroID = hero.id + - var selectedState = (state.get('selectedHeroID') === heroID ? 'selected' : (state.get('currentHeroID') === heroID ? 'current' : '')) + .hero-option(data-hero-id=heroID class=selectedState) + .hero-avatar + img(src=hero.getPortraitURL()) + .text-h5.hero-name + span= hero.getHeroShortName() + +block modal-footer-content + .select-hero-btn.btn.btn-lg.btn-forest + span(data-i18n="courses.select_this_hero") + diff --git a/app/views/courses/CoursesView.coffee b/app/views/courses/CoursesView.coffee index d255beb9d..0cf3d2d90 100644 --- a/app/views/courses/CoursesView.coffee +++ b/app/views/courses/CoursesView.coffee @@ -4,6 +4,7 @@ template = require 'templates/courses/courses-view' AuthModal = require 'views/core/AuthModal' CreateAccountModal = require 'views/core/CreateAccountModal' ChangeCourseLanguageModal = require 'views/courses/ChangeCourseLanguageModal' +HeroSelectModal = require 'views/courses/HeroSelectModal' ChooseLanguageModal = require 'views/courses/ChooseLanguageModal' JoinClassModal = require 'views/courses/JoinClassModal' CourseInstance = require 'models/CourseInstance' @@ -13,6 +14,7 @@ Classroom = require 'models/Classroom' Classrooms = require 'collections/Classrooms' LevelSession = require 'models/LevelSession' Campaign = require 'models/Campaign' +ThangType = require 'models/ThangType' utils = require 'core/utils' # TODO: Test everything @@ -24,6 +26,7 @@ module.exports = class CoursesView extends RootView events: 'click #log-in-btn': 'onClickLogInButton' 'click #start-new-game-btn': 'openSignUpModal' + 'click .change-hero-btn': 'onClickChangeHeroButton' 'click #join-class-btn': 'onClickJoinClassButton' 'submit #join-class-form': 'onSubmitJoinClassForm' 'click #change-language-link': 'onClickChangeLanguageLink' @@ -43,6 +46,16 @@ module.exports = class CoursesView extends RootView @courses = new CocoCollection([], { url: "/db/course", model: Course}) @supermodel.loadCollection(@courses) + # TODO: Trim this section for only what's necessary + @hero = new ThangType + defaultHeroOriginal = ThangType.heroes.captain + heroOriginal = me.get('heroConfig')?.thangType or defaultHeroOriginal + @hero.url = "/db/thang.type/#{heroOriginal}/version" + # @hero.setProjection ['name','slug','soundTriggers','featureImages','gems','heroClass','description','components','extendedName','unlockLevelName','i18n'] + @supermodel.loadModel(@hero, 'hero') + @listenTo @hero, 'all', -> + @render() + onCourseInstancesLoaded: -> map = {} for courseInstance in @courseInstances.models @@ -76,6 +89,16 @@ module.exports = class CoursesView extends RootView @openModalView(modal) application.tracker?.trackEvent 'Started Student Signup', category: 'Courses' + onClickChangeHeroButton: -> + modal = new HeroSelectModal({ currentHeroID: @hero.id }) + @openModalView(modal) + @listenTo modal, 'hero-select:success', (newHero) => + # @hero.url = "/db/thang.type/#{me.get('heroConfig').thangType}/version" + # @hero.fetch() + @hero.set(newHero.attributes) + @listenTo modal, 'hide', -> + @stopListening modal + onSubmitJoinClassForm: (e) -> e.preventDefault() @joinClass() @@ -136,7 +159,7 @@ module.exports = class CoursesView extends RootView classroomCourseInstances.fetch({ data: {classroomID: newClassroom.id} }) @listenToOnce classroomCourseInstances, 'sync', -> # TODO: Smoother system for joining a classroom and course instances, without requiring page reload, - # and showing which class was just joined. + # and showing which class was just joined. document.location.search = '' # Using document.location.reload() causes an infinite loop of reloading onClickChangeLanguageLink: -> diff --git a/app/views/courses/HeroSelectModal.coffee b/app/views/courses/HeroSelectModal.coffee new file mode 100644 index 000000000..c658b6941 --- /dev/null +++ b/app/views/courses/HeroSelectModal.coffee @@ -0,0 +1,42 @@ +ModalView = require 'views/core/ModalView' +template = require 'templates/courses/hero-select-modal' +Classroom = require 'models/Classroom' +ThangTypes = require 'collections/ThangTypes' +State = require 'models/State' +ThangType = require 'models/ThangType' +User = require 'models/User' + +module.exports = class HeroSelectModal extends ModalView + id: 'hero-select-modal' + template: template + + events: + 'click .select-hero-btn': 'onClickSelectHeroButton' + 'click .hero-option': 'onClickHeroOption' + + initialize: ({ currentHeroID }) -> + @debouncedRender = _.debounce @render, 0 + + @state = new State({ + currentHeroID + selectedHeroID: currentHeroID + }) + + @heroes = new ThangTypes({}, { project: ['original', 'name', 'heroClass'] }) + @supermodel.trackRequest @heroes.fetchHeroes() + + @listenTo @state, 'all', -> @debouncedRender() + @listenTo @heroes, 'all', -> @debouncedRender() + + onClickHeroOption: (e) -> + heroID = $(e.currentTarget).data('hero-id') + @state.set selectedHeroID: heroID + hero = @heroes.get(heroID) + me.set(heroConfig: {}) unless me.get('heroConfig') + heroConfig = _.assign me.get('heroConfig'), { thangType: hero.get('original') } + me.set({ heroConfig }) + me.save().then => + @trigger 'hero-select:success', hero + + onClickSelectHeroButton: () -> + @hide() diff --git a/test/app/views/courses/CoursesView.spec.coffee b/test/app/views/courses/CoursesView.spec.coffee new file mode 100644 index 000000000..195111232 --- /dev/null +++ b/test/app/views/courses/CoursesView.spec.coffee @@ -0,0 +1,32 @@ +CoursesView = require 'views/courses/CoursesView' +HeroSelectModal = require 'views/courses/HeroSelectModal' +Classrooms = require 'collections/Classrooms' +CourseInstances = require 'collections/CourseInstances' +Courses = require 'collections/Courses' +auth = require 'core/auth' +factories = require 'test/app/factories' + +describe 'CoursesView', -> + + modal = null + view = null + + describe 'Change Hero button', -> + beforeEach (done) -> + view = new CoursesView() + classrooms = new Classrooms([factories.makeClassroom()]) + courseInstances = new CourseInstances([factories.makeCourseInstance()]) + courses = new Courses([factories.makeCourse()]) + view.classrooms.fakeRequests[0].respondWith({ status: 200, responseText: classrooms.stringify() }) + view.ownedClassrooms.fakeRequests[0].respondWith({ status: 200, responseText: classrooms.stringify() }) + view.courseInstances.fakeRequests[0].respondWith({ status: 200, responseText: courseInstances.stringify() }) + view.render() + jasmine.demoEl(view.$el) + done() + + it 'opens the modal when you click Change Hero', -> + spyOn(view, 'openModalView') + view.$('.change-hero-btn').click() + expect(view.openModalView).toHaveBeenCalled() + args = view.openModalView.calls.argsFor(0) + expect(args[0] instanceof HeroSelectModal).toBe(true) diff --git a/test/app/views/courses/HeroSelectModal.spec.coffee b/test/app/views/courses/HeroSelectModal.spec.coffee new file mode 100644 index 000000000..b38861f4e --- /dev/null +++ b/test/app/views/courses/HeroSelectModal.spec.coffee @@ -0,0 +1,38 @@ +HeroSelectModal = require 'views/courses/HeroSelectModal' +auth = require 'core/auth' +factories = require 'test/app/factories' + +describe 'HeroSelectModal', -> + + modal = null + coursesView = null + user = null + + hero1 = factories.makeThangType({ original: "hero1original", _id: "hero1id", heroClass: "Warrior", name: "Hero 1" }) + hero2 = factories.makeThangType({ original: "hero2original", _id: "hero2id", heroClass: "Warrior", name: "Hero 2" }) + heroesResponse = JSON.stringify([hero1, hero2]) + + beforeEach (done) -> + window.me = user = factories.makeUser({ heroConfig: { thangType: hero1.get('original') } }) + auth.loginUser(user.attributes) + modal = new HeroSelectModal({ currentHeroID: hero1.id }) + modal.heroes.fakeRequests[0].respondWith({ status: 200, responseText: heroesResponse }) + jasmine.demoModal(modal) + _.defer -> + modal.render() + done() + + afterEach -> + modal.stopListening() + + it 'highlights the current hero', -> + expect(modal.$(".hero-option[data-hero-id='#{hero1.id}']")[0].className.split(" ")).toContain('selected') + + it 'saves when you change heroes', (done) -> + modal.$(".hero-option[data-hero-id='#{hero2.id}']").click() + _.defer -> + expect(user.fakeRequests.length).toBe(1) + request = user.fakeRequests[0] + expect(request.method).toBe("PUT") + expect(JSON.parse(request.params).heroConfig?.thangType).toBe(hero2.get('original')) + done() From 705463615be9854e2a4637c0f1d7a7a71fda4152 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 2 Jun 2016 15:56:14 -0700 Subject: [PATCH 102/167] Fix some intermittent client erroring Some tests are triggering achievement polling. Prevent that from happening. --- app/models/CocoModel.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 8c8510857..4a5c84fb5 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -368,6 +368,7 @@ class CocoModel extends Backbone.Model return if _.isString @url then @url else @url() @pollAchievements: -> + return if application.testing CocoCollection = require 'collections/CocoCollection' EarnedAchievement = require 'models/EarnedAchievement' From b3663196d759ce2fa5323fe9f46746ef0823895b Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 2 Jun 2016 16:08:35 -0700 Subject: [PATCH 103/167] Fix CourseVictoryModal.spec --- .../views/play/level/modal/CourseVictoryModal.spec.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee b/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee index 09e5e21f2..3ca11cad2 100644 --- a/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee +++ b/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee @@ -40,6 +40,12 @@ describe 'CourseVictoryModal', -> modal.classroom.fakeRequests[0].respondWith({ status: 200, responseText: factories.makeClassroom().stringify() }) + if me.fakeRequests + lastRequest = _.last(me.fakeRequests) + if not lastRequest.response + lastRequest.respondWith({ + status: 200, responseText: factories.makeUser().stringify() + }) nextLevelRequest = modal.nextLevel.fakeRequests[0] describe 'given a course level with a next level and no item or hero rewards', -> From 2ef10f58b34b53ea5d77d121ad6f24ce33a93bfb Mon Sep 17 00:00:00 2001 From: Rob Date: Fri, 3 Jun 2016 14:01:37 -0700 Subject: [PATCH 104/167] Fix bug where visual indents couldn't nest. --- 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 2745e4777..dbddb0de3 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -316,7 +316,7 @@ module.exports = class SpellView extends CocoView xstart = startOfRow(row) if language is 'python' - requiredIndent = new RegExp '^' + new Array(xstart / 4 + 2).join '( |\t)' + '(\\S|\\s*$)' + requiredIndent = new RegExp '^' + new Array(xstart / 4 + 1).join('( |\t)') + '( |\t)+(\\S|\\s*$)' for crow in [docRange.start.row+1..docRange.end.row] unless requiredIndent.test lines[crow] docRange.end.row = crow - 1 From 8e64a3b244484792f36213c54c227829ed013c48 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Fri, 3 Jun 2016 14:41:48 -0700 Subject: [PATCH 105/167] Add Promise polyfill To allow using Promises while still supporting IE11 --- bower.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 064a9dbc2..0f69fe4a9 100644 --- a/bower.json +++ b/bower.json @@ -52,7 +52,8 @@ "esper.js": "http://files.codecombat.com/esper.tar.gz", "algoliasearch": "^3.13.1", "algolia-autocomplete.js": "^0.17.0", - "algolia-autocomplete-no-conflict": "1.0.0" + "algolia-autocomplete-no-conflict": "1.0.0", + "promise-polyfill": "^5.2.1" }, "overrides": { "algolia-autocomplete.js": { From 3f8272afe9b0ebea838ea65c6fed0a8101cdc97f Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Fri, 3 Jun 2016 15:57:30 -0700 Subject: [PATCH 106/167] Switch Promise polyfill to bluebird --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 0f69fe4a9..50b1b9e16 100644 --- a/bower.json +++ b/bower.json @@ -53,7 +53,7 @@ "algoliasearch": "^3.13.1", "algolia-autocomplete.js": "^0.17.0", "algolia-autocomplete-no-conflict": "1.0.0", - "promise-polyfill": "^5.2.1" + "bluebird": "^3.4.0" }, "overrides": { "algolia-autocomplete.js": { From 7a0fb967f01a22e405851e13b2e052028ecff44e Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Fri, 3 Jun 2016 16:26:03 -0700 Subject: [PATCH 107/167] Add clearer reports to client TestView --- app/styles/{test.sass => test-view.sass} | 4 ++ app/templates/test-view.jade | 14 ++++++ app/views/TestView.coffee | 60 +++++++++++++++++------- 3 files changed, 60 insertions(+), 18 deletions(-) rename app/styles/{test.sass => test-view.sass} (78%) diff --git a/app/styles/test.sass b/app/styles/test-view.sass similarity index 78% rename from app/styles/test.sass rename to app/styles/test-view.sass index f32c27af2..4beda86e2 100644 --- a/app/styles/test.sass +++ b/app/styles/test-view.sass @@ -7,3 +7,7 @@ font-family: Arial, Geneva, sans-serif padding: 20px font-weight: bold + + .alert-report + font-size: 20px + diff --git a/app/templates/test-view.jade b/app/templates/test-view.jade index bb520438f..97e155141 100644 --- a/app/templates/test-view.jade +++ b/app/templates/test-view.jade @@ -11,6 +11,20 @@ ol.breadcrumb .container-fluid .row .col-md-8 + #failure-reports + for report in view.failureReports + .alert.alert-danger.alert-report + ul.suite-list + for description in report.suiteDescriptions + li= description + li + strong ... #{report.testDescription} + hr + ol.error-list + for message in report.failMessages + li + strong= message + #test-wrapper.well #testing-area diff --git a/app/views/TestView.coffee b/app/views/TestView.coffee index 631a3374f..5475b6791 100644 --- a/app/views/TestView.coffee +++ b/app/views/TestView.coffee @@ -13,7 +13,7 @@ module.exports = TestView = class TestView extends RootView id: 'test-view' template: template reloadOnClose: true - loadedFileIDs: [] + className: 'style-flat' events: 'click #show-demos-btn': 'onClickShowDemosButton' @@ -24,11 +24,13 @@ module.exports = TestView = class TestView extends RootView initialize: (options, @subPath='') -> @subPath = @subPath[1..] if @subPath[0] is '/' @demosOn = storage.load('demos-on') + @failureReports = [] + @loadedFileIDs = [] afterInsert: -> @initSpecFiles() @render() - TestView.runTests(@specFiles, @demosOn) + TestView.runTests(@specFiles, @demosOn, @) window.runJasmine() # EVENTS @@ -59,7 +61,30 @@ module.exports = TestView = class TestView extends RootView prefix = TEST_REQUIRE_PREFIX + @subPath @specFiles = (f for f in @specFiles when _.string.startsWith f, prefix) - @runTests: (specFiles, demosOn=false) -> + @runTests: (specFiles, demosOn=false, view) -> + + jasmine.getEnv().addReporter({ + suiteStack: [] + + specDone: (result) -> + if result.status is 'failed' + console.log 'result', result + report = { + suiteDescriptions: _.clone(@suiteStack) + failMessages: (fe.message for fe in result.failedExpectations) + testDescription: result.description + } + view.failureReports.push(report) + view.renderSelectors('#failure-reports') + + suiteStarted: (result) -> + @suiteStack.push(result.description) + + suiteDone: (result) -> + @suiteStack.pop() + + }) + application.testing = true specFiles ?= @getAllSpecFiles() if demosOn @@ -71,23 +96,22 @@ module.exports = TestView = class TestView extends RootView jasmine.demoEl = _.noop jasmine.demoModal = _.noop - describe 'CodeCombat Client', => - jasmine.Ajax.install() - beforeEach -> - jasmine.Ajax.requests.reset() - Backbone.Mediator.init() - Backbone.Mediator.setValidationEnabled false - spyOn(application.tracker, 'trackEvent') - # TODO Stubbify more things - # * document.location - # * firebase - # * all the services that load in main.html + jasmine.Ajax.install() + beforeEach -> + jasmine.Ajax.requests.reset() + Backbone.Mediator.init() + Backbone.Mediator.setValidationEnabled false + spyOn(application.tracker, 'trackEvent') + # TODO Stubbify more things + # * document.location + # * firebase + # * all the services that load in main.html - afterEach -> - # TODO Clean up more things - # * Events + afterEach -> + # TODO Clean up more things + # * Events - require f for f in specFiles # runs the tests + require f for f in specFiles # runs the tests @getAllSpecFiles = -> allFiles = window.require.list() From 0d5ad789e54166a85d9104ab5aaab5b649e88c0e Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Fri, 3 Jun 2016 10:18:41 -0700 Subject: [PATCH 108/167] Add time played to level progress tooltips --- app/lib/coursesHelper.coffee | 2 ++ app/templates/courses/teacher-class-view.jade | 4 ++-- .../hovers/progress-dot-single-student-level.jade | 9 +++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/lib/coursesHelper.coffee b/app/lib/coursesHelper.coffee index 0866d95e7..693234fa2 100644 --- a/app/lib/coursesHelper.coffee +++ b/app/lib/coursesHelper.coffee @@ -156,6 +156,8 @@ module.exports = courseProgress[levelID][userID] = { completed: true, started: false } # These don't matter, will always be set session = _.find classroom.sessions.models, (session) -> session.get('creator') is userID and session.get('level').original is levelID + + courseProgress[levelID][userID].session = session if not session # haven't gotten to this level yet, but might have completed others before courseProgress.started ||= false #no-op diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade index e0e6713da..3f135c235 100644 --- a/app/templates/courses/teacher-class-view.jade +++ b/app/templates/courses/teacher-class-view.jade @@ -321,7 +321,7 @@ mixin studentLevelsRow(student) - var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models each level, index in levels - var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level, user: student }) - +studentLevelProgressDot(progress, level, index+1) + +studentLevelProgressDot(progress, level, index+1, session) mixin studentCourseProgressDot(progress, levelsTotal, level, label) //- TODO: Refactor with TeacherClassesView jade @@ -342,7 +342,7 @@ mixin studentLevelProgressDot(progress, level, levelNumber) //- TODO: Refactor with TeacherClassesView jade - dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : ''); - levelName = level.get('name') - - context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber }) + - context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber, moment: moment }) .progress-dot.level-progress-dot(class=dotClass, data-html='true', data-title=view.singleStudentLevelProgressDotTemplate(context)) +progressDotLabel(levelNumber) diff --git a/app/templates/teachers/hovers/progress-dot-single-student-level.jade b/app/templates/teachers/hovers/progress-dot-single-student-level.jade index 3922be2ed..0a8433a48 100644 --- a/app/templates/teachers/hovers/progress-dot-single-student-level.jade +++ b/app/templates/teachers/hovers/progress-dot-single-student-level.jade @@ -1,3 +1,10 @@ +mixin timePlayed() + if session.get('playtime') > 0 + .small-details.nowrap + span.spr(data-i18n='teacher.time_played') + | Played for + span= moment.duration({ seconds: session.get('playtime') }).humanize() + if completed .small-details.nowrap span= levelNumber @@ -7,6 +14,7 @@ if completed span.spr(data-i18n='teacher.completed') | Completed span= new Date(dateFirstCompleted).toLocaleString() + +timePlayed //- .small-details //- i(data-i18n='teacher.click_to_view_solution') //- | click to view solution @@ -19,6 +27,7 @@ else if started span.spr(data-i18n='teacher.last_played') | Last played span= new Date(lastPlayed).toLocaleString() + +timePlayed //- .small-details //- i(data-i18n='teacher.click_to_view_progress') //- | click to view progress From a6bb706cf295e2680fe77ff13e91d3da59487b53 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Fri, 3 Jun 2016 20:20:16 -0700 Subject: [PATCH 109/167] Update licenses needed form email contacts to include NL --- server/lib/closeIO.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/closeIO.coffee b/server/lib/closeIO.coffee index cb4825109..ed9fd9aa2 100644 --- a/server/lib/closeIO.coffee +++ b/server/lib/closeIO.coffee @@ -65,7 +65,7 @@ module.exports = activities = JSON.parse(body) return done("Unexpected activities format: " + body) unless activities.data? for activity in activities.data when activity._type is 'Email' - if /@codecombat\.com/ig.test(activity.sender) and not activity.sender?.indexOf(config.mail.username) >= 0 + if /@codecombat\.(?:com)|(?:nl)$/ig.test(activity.sender) and not activity.sender?.indexOf(config.mail.username) >= 0 return done(null, activity.sender, lead.id) return done(null, config.mail.supportSchools, lead.id) catch error From 189f9fa7aff324b2596b64a53f57c282a3d160a4 Mon Sep 17 00:00:00 2001 From: JurianLock Date: Sun, 5 Jun 2016 15:46:19 +0200 Subject: [PATCH 110/167] Update nl-NL.coffee (#3713) Some UX updates and some minor spelling checks. --- app/locale/nl-NL.coffee | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/app/locale/nl-NL.coffee b/app/locale/nl-NL.coffee index e466d1c4d..6ae868591 100644 --- a/app/locale/nl-NL.coffee +++ b/app/locale/nl-NL.coffee @@ -5,8 +5,8 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription no_mobile: "CodeCombat is niet gemaakt voor mobiele apparaten en werkt misschien niet!" # Warning that shows up on mobile devices play: "Speel" # The big play button that opens up the campaign view. play_campaign_version: "Speel de Verhaallijn" # Shows up under big play button if you only play /courses - old_browser: "Oh oh, jouw browser is te oud om CodeCombat te kunnen spelen, Sorry!" # Warning that shows up on really old Firefox/Chrome/Safari - old_browser_suffix: "Je kan toch proberen, maar het zal waarschijnlijk niet werken!" + old_browser: "uh-oh, jouw browser is te oud om CodeCombat te kunnen spelen, Sorry!" # Warning that shows up on really old Firefox/Chrome/Safari + old_browser_suffix: "Je kan alsnog proberen, maar het zal waarschijnlijk niet werken!" ipad_browser: "Slecht nieuws: CodeCombat draait niet in je browser op de iPad. Goed nieuws: onze iPad-app wordt op het moment beoordeeld door Apple." campaign: "Verhaallijn" for_beginners: "Voor Beginners" @@ -24,25 +24,25 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription im_a_teacher: "Ik ben een leraar" im_a_student: "Ik ben een leerling" learn_more: "Lees verder" - classroom_in_a_box: "Een kant-en-klare digitale klas voor programmeerlessen." + classroom_in_a_box: "Kant-en-klare programmeerlessen." codecombat_is: "CodeCombat is een platform waarmee leerlingen leren programmeren door het spelen van een spel." # {change} our_courses: "Onze lessen zijn specifiek ontwikkeld voor een klasomgeving, zelfs voor leraren zonder programmeerervaring." # {change} - top_screenshots_hint: "Leerlingen schrijven code en zien direct het resultaat van de verandering." + top_screenshots_hint: "Leerlingen schrijven code en zien direct resultaat." designed_with: "Gemaakt voor leraren" real_code: "Echte, getypte code" from_the_first_level: "vanaf het eerste level" - getting_students: "Leerlingen zo snel mogelijk echte code laten schrijven is noodzakelijk voor het leren van programmeer syntax en correcte structuur." + getting_students: "Doordat leerlingen code schrijven in 'echte programmeertaal', leren ze niet alleen hoe computers denken, maar kunnen ze het ook echt toepassen." educator_resources: "Lesbrieven voor docenten" course_guides: "en ondersteuningsmateriaal" teaching_computer_science: "Je hebt geen informatica diploma nodig om te kunnen programmeren, wij verschaffen de materialen waarmee elke docent programmeerles kan geven." - accessible_to: "Bereikbaar voor" + accessible_to: "Toegankelijk voor" everyone: "iedereen" democratizing: "Programmeerles toegankelijk maken is onze filosofie. Iedereen moet de kans krijgen om te leren programmeren." forgot_learning: "Volgens mij hadden ze niet meer door dat ze eigenlijk bezig waren met leren." wanted_to_do: " Ik wilde altijd al leren programmeren, maar op school was hier nooit aandacht voor." why_games: "Waarom is spelenderwijs leren belangrijk?" games_reward: "Games vergroten de productiviteit." - encourage: "Gaming is een middel dat interactie, nieuwschierigheid, en trial-and-error aanmoedigt. Een goed spel daagt de speler uit zijn vaardigheden te perfectioneren, wat hetzelfde noodzakelijke proces is waar leerlingen doorheen gaan wanneer zij iets leren." + encourage: "Iedereen wordt geboren als klein onderzoekertje dat de wereld ontdekt door vallen en opstaan. Deze intrinsieke motivatie om te leren ziet men ook terug in een spelomgeving. Voor de speler wordt 'leren' een middel om het spel te winnen, in plaats van een doel op zich." excel: "Games helpen bij de" struggle: "productiviteit-strijd" kind_of_struggle: "het soort worsteling dat uitmondt in een leerproces dat uitdagend is en " @@ -58,7 +58,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription great_game: "Een goed spel is meer dan alleen medailles en prestaties - het gaat om een reis, nauwkeurig ontworpen puzzels, en de mogelijkheid om uitdagingen vol zelfvertrouwen aan te pakken." agency: "CodeCombat is een game die de mogelijkheid en het zelfvertrouwen geeft om met echte code te werken, wat zowel beginners als gevorderde helpt bij het schrijven van goede, valide code." request_demo_title: "Laat je leerlingen vandaag nog starten!" - request_demo_subtitle: "Vraag een demo aan en start binnen een uur met programmeerlessen." + request_demo_subtitle: "Vraag een demo aan en start met programmeerlessen." get_started_title: "Maak vandaag nog een klas aan!" get_started_subtitle: "Maak een klas aan, voeg je leerlingen toe, en monitor hun vooruitgang." request_demo: "Vraag een demo aan" @@ -74,7 +74,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription coming_soon: "Binnenkort beschikbaar!" courses_available_in: "Lessen zijn beschikbaar in JavaScript, Python, en Java (Java is binnenkort beschikbaar!)" boast: "Uitdagende raadsels die zowel gamers als fanatieke programmeurs weten te prikkelen." - winning: "Een gouden combinatie van spel-elementen en programmeerhuiswerk, dat samen zorgt voor kind-vriendelijk en oprecht aangenaam onderwijs." + winning: "Een gouden combinatie van spel-elementen en programmeerhuiswerk, dat samen zorgt voor kindvriendelijk en oprecht aangenaam onderwijs." run_class: "Alles wat je nodig hebt om vandaag nog programmeerlessen in jouw klas te geven, geen voorkennis vereist." teachers: "Docenten!" teachers_and_educators: "Docenten & Mentoren" @@ -92,7 +92,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription check_out_wiki: "Bekijk onze nieuwe leraren Wiki" want_coco: "Wil je CodeCombat op jouw school?" form_select_role: "Selecteer je rol" - form_select_range: "Selecteer klassengrootte" + form_select_range: "Selecteer klasomvang" nav: play: "Levels" # The top nav bar entry where players choose which levels to play @@ -221,19 +221,19 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription "/": "gedeeld door" "+": "plus" "-": "min" -# "+=": "add and assign" -# "-=": "subtract and assign" + "+=": "tel op en wijs toe" + "-=": "trek af en wijs toe" True: "Waar" true: "waar" False: "onwaar" false: "onwaar" undefined: "ongedefinieerd" -# null: "null" -# nil: "nil" + null: "nul" + nil: "nihil" None: "Geen" share_progress_modal: - blurb: "Je gaat snel vooruit! Vertel aan je ouders hoeveel je geleerd hebt van CodeCombat." + blurb: "Je gaat snel vooruit! Vertel je ouders hoeveel je geleerd hebt van CodeCombat." email_invalid: "E-mailadres klopt niet." form_blurb: "Vul het e-mailadres van je ouders hieronder in en we zullen het ze laten zien!" form_label: "E-mailadres" @@ -472,19 +472,19 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription tip_reticulating: "Paden aan het verknopen." tip_harry: "Je bent een tovenaar, " tip_great_responsibility: "Met een groots talent voor programmeren komt een grootse debug verantwoordelijkheid." - tip_munchkin: "Als je je groentjes niet opeet zal een munchkin je ontvoeren terwijl je slaapt." + tip_munchkin: "Als je je groenten niet opeet zal een munchkin je ontvoeren terwijl je slaapt." tip_binary: "Er zijn 10 soorten mensen in de wereld: Mensen die binair kunnen tellen en mensen die dat niet kunnen." tip_commitment_yoda: "Een programmeur moet de grootste inzet hebben, een meest serieuze geest. ~ Yoda" tip_no_try: "Doe het. Of doe het niet. Je kunt niet proberen. - Yoda" tip_patience: "Geduld moet je hebben, jonge Padawan. - Yoda" tip_documented_bug: "Een gedocumenteerde fout is geen fout; het is deel van het programma." - tip_impossible: "Het lijkt altijd onmogelijk tot het gedaan wordt. - Nelson Mandela" + tip_impossible: "Het lijkt altijd onmogelijk totdat iemand het doet. - Nelson Mandela" tip_talk_is_cheap: "Je kunt het goed uitleggen, maar toon me de code. - Linus Torvalds" tip_first_language: "Het ergste dat je kan leren is je eerste programmeertaal. - Alan Kay" tip_hardware_problem: "Q: Hoeveel programmeurs heb je nodig om een lampje te vervangen? A: Nul, het is een a hardware probleem." tip_hofstadters_law: "De Wet van Hofstadter: Het duurt altijd langer dan je verwacht, zelfs wanneer je rekening houdt met de Wet van Hofstadter." tip_premature_optimization: "vroegtijdig optimaliseren is de wortel van al het kwaad. - Donald Knuth" - tip_brute_force: "Wanneer je twijfelt, gebruik brute force. - Ken Thompson" + tip_brute_force: "Wanneer je twijfelt, gebruik dan brute force. - Ken Thompson" tip_extrapolation: "Er zijn twee soorten mensen: Zij die iets kunnen afleiden van onvolledige gegevens..." tip_superpower: "Van alle dingen komt programmeren het dichtst in de buurt van een superkracht." tip_control_destiny: "In echte open source, hebt je het recht om je eigen toekomst te bepalen. - Linus Torvalds" @@ -820,8 +820,8 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription teachers_quote: name: "Demo Formulier" title: "Demo aanvragen" - subtitle: "Haal CodeCombat jouw klaslokaal of club!" - email_exists: "Er bestaat al een gebruiker met dit email adres." + subtitle: "Gebruik CodeCombat voor jouw klas of programmeerclub!" + email_exists: "Er bestaat al een gebruiker met dit emailadres." phone_number: "Telefoonnummer" phone_number_help: "Waarop kunnen we je bereiken tijdens kantooruren?" primary_role_label: "Uw primaire rol" From 5da85621c6f90162395afba94fc26616546519a9 Mon Sep 17 00:00:00 2001 From: Ana Date: Sun, 5 Jun 2016 15:47:27 +0200 Subject: [PATCH 111/167] Update sr.coffee (#3714) - translation of contact, acc settings, community and clans sections + some other stuff --- app/locale/sr.coffee | 196 +++++++++++++++++++++---------------------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/app/locale/sr.coffee b/app/locale/sr.coffee index b7ec6630e..5e317d20e 100644 --- a/app/locale/sr.coffee +++ b/app/locale/sr.coffee @@ -686,21 +686,21 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # writable: "writable" # Hover over "attack" in Your Skills while playing a level to see most of this # read_only: "read-only" action: "Aкција" -# spell: "Spell" -# action_name: "name" -# action_cooldown: "Takes" -# action_specific_cooldown: "Cooldown" -# action_damage: "Damage" -# action_range: "Range" -# action_radius: "Radius" -# action_duration: "Duration" -# example: "Example" -# ex: "ex" # Abbreviation of "example" -# current_value: "Current Value" -# default_value: "Default value" -# parameters: "Parameters" -# returns: "Returns" -# granted_by: "Granted by" + spell: "Магија" + action_name: "име" + action_cooldown: "Потребно" + action_specific_cooldown: "Хлађење" + action_damage: "Штета" + action_range: "Домет" + action_radius: "Опсег" + action_duration: "Трајање" + example: "Пример" + ex: "нпр." # Abbreviation of "example" + current_value: "Тренутна вредност" + default_value: "Подразумевана вредност" + parameters: "Параметри" + returns: "Враћа" + granted_by: "Додељено од" save_load: granularity_saved_games: "Сачувано" @@ -860,12 +860,12 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian finish_signup_p: "Направи налог да оснујеш разред, додаш своје ученике и пратиш њихов напредак док уче компјутерске науке." signup_with: "Пријави се са:" connect_with: "Повежи се са:" -# conversion_warning: "WARNING: Your current account is a Student Account. Once you submit this form, your account will be updated to a Teacher Account." -# learn_more_modal: "Teacher accounts on CodeCombat have the ability to monitor student progress, assign enrollments and manage classrooms. Teacher accounts cannot be a part of a classroom - if you are currently enrolled in a class using this account, you will no longer be able to access it once you update to a Teacher Account." + conversion_warning: "УПОЗОРЕЊЕ: Твој тренутни налог је Студентски Налог. Након што пошаљеш овај формулар, твој налог ће бити надограђен у Учитељски Налог." + learn_more_modal: "Учитељски налози на CodeCombat-у имају могућност посматрања напретка ученика, додељивања уписа и управљања учионицама. Учитељски налози не могу бити део учионице - ако си тренутно уписан у разред преко овог налога, нећеш више моћи да му приступиш кад ажурираш у Учитељски Налог." create_account: "Направи учитељски налог" -# create_account_subtitle: "Get access to teacher-only tools for using CodeCombat in the classroom. Set up a class, add your students, and monitor their progress!" -# convert_account_title: "Update to Teacher Account" -# not: "Not" + create_account_subtitle: "Добиј приступ алатима само за учитеље за коришћење CodeCombat-а у учионици. Подеси разред, додај своје ученике, и посматрај њихов напредак!" + convert_account_title: "Ажурирај у Учитељски Налог" + not: "Није" setup_a_class: "Подеси разред" versions: @@ -880,18 +880,18 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian contact: contact_us: "Контактирај CodeCombat" - welcome: "Драго нам је што нас контактираш! Искористи ову форму да нам пошаљеш мејл. " + welcome: "Драго нам је што нас контактираш! Искористи овај формулар да нам пошаљеш мејл. " forum_prefix: "За било шта јавно, посети " - forum_page: "наш форум." -# forum_suffix: " instead." -# faq_prefix: "There's also a" -# faq: "FAQ" -# subscribe_prefix: "If you need help figuring out a level, please" -# subscribe: "buy a CodeCombat subscription" -# subscribe_suffix: "and we'll be happy to help you with your code." -# subscriber_support: "Since you're a CodeCombat subscriber, your email will get our priority support." -# screenshot_included: "Screenshot included." -# where_reply: "Where should we reply?" + forum_page: "наш форум" + forum_suffix: " уместо тога." + faq_prefix: "Такође, ту је" + faq: "FAQ" + subscribe_prefix: "Ако ти треба помоћ да разумеш ниво, молимо да" + subscribe: "купиш CodeCombat претплату" + subscribe_suffix: "и радо ћемо ти помоћи у твом коду." + subscriber_support: "Пошто си CodeCombat претплатник, твој мејл ће имати приоритет у нашој подршци." + screenshot_included: "Снимак екрана укључен." + where_reply: "Где треба да одговоримо?" send: "Пошаљи повратну информацију" account_settings: @@ -910,24 +910,24 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # god_mode: "God Mode" password_tab: "Шифра" emails_tab: "Мејлови" -# admin: "Admin" + admin: "Администратор" manage_subscription: "Кликни овде да би управљао својом претплатом." new_password: "Нова Шифра" new_password_verify: "Потврди" -# type_in_email: "Type in your email to confirm account deletion." -# type_in_email_progress: "Type in your email to confirm deleting your progress." -# type_in_password: "Also, type in your password." + type_in_email: "Упиши свој мејл да потврдиш брисање налога." + type_in_email_progress: "Упиши свој мејл да потврдиш брисање свог напретка." + type_in_password: "Такође, упиши своју шифру." email_subscriptions: "Мејл претплате" -# email_subscriptions_none: "No Email Subscriptions." + email_subscriptions_none: "Без мејл претплата." email_announcements: "Обавештења" email_announcements_description: "Прими мејл за најновије вести и достигнућа на CodeCombat-у" -# email_notifications: "Notifications" -# email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity." -# email_any_notes: "Any Notifications" -# email_any_notes_description: "Disable to stop all activity notification emails." + email_notifications: "Обавештења" + email_notifications_summary: "Контроле за персонализована, аутоматска мејл обавештења вазана за твоју CodeCombat активност." + email_any_notes: "Сва обавештења" + email_any_notes_description: "Онемогући да би прекинуо сва мејл обавештења о активности." email_news: "Вести" email_recruit_notes: "Пословне могућности" -# email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job." + email_recruit_notes_description: "Ако играш јако добро, можда ћемо те контактирати о томе да добијеш (бољи) посао." contributor_emails: "Мејлови реда сарадника" contribute_prefix: "Тражимо људе који би нам се придружили! Погледај " contribute_page: "страницу за сарадњу" @@ -961,13 +961,13 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian community: main_title: "CodeCombat Заједница" -# introduction: "Check out the ways you can get involved below and decide what sounds the most fun. We look forward to working with you!" -# level_editor_prefix: "Use the CodeCombat" -# level_editor_suffix: "to create and edit levels. Users have created levels for their classes, friends, hackathons, students, and siblings. If create a new level sounds intimidating you can start by forking one of ours!" -# thang_editor_prefix: "We call units within the game 'thangs'. Use the" -# thang_editor_suffix: "to modify the CodeCombat source artwork. Allow units to throw projectiles, alter the direction of an animation, change a unit's hit points, or upload your own vector sprites." -# article_editor_prefix: "See a mistake in some of our docs? Want to make some instructions for your own creations? Check out the" -# article_editor_suffix: "and help CodeCombat players get the most out of their playtime." + introduction: "Погледај испод како можеш да се укључиш и одлучи шта звучи најзанимљивије. Радујемо се прилици да радимо са тобом!" + level_editor_prefix: "Користи CodeCombat" + level_editor_suffix: "да правиш и уређујеш нивое. Корисници су направили нивое за њихове разреде, пријатеље, хакатоне, ученике и браћу и сестре. Ако прављење новог нивоа звучи застрашујуће, можеш да почнеш форковањем једног од наших!" + thang_editor_prefix: "Ми зовемо јединице у игри 'thangs'. Користи" + thang_editor_suffix: "да модификујеш CodeCombat изворне илустрације. Дозволи јединицама да бацају пројектиле, измени дирекцију анимације, промени хит поене јединице или отпреми сопствене векторске спрајтове." + article_editor_prefix: "Видиш грешку у неком од наших докумената? Желиш да направиш инструкције за сопствене креације? Погледај" + article_editor_suffix: "и помози CodeCombat играчима да добију највише од свог играња." find_us: "Нађи нас на овим сајтовима" social_github: "Погледај цео наш код на GitHub-у" social_blog: "Читај CodeCombat блог на Sett-у" @@ -986,57 +986,57 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian make_private: "Направи клан приватним" subs_only: "само за претплатнике" create_clan: "Направи нови клан" - private_preview: "Preview" + private_preview: "Приказ" private_clans: "Приватни кланови" public_clans: "Јавни кланови" my_clans: "Моји кланови" clan_name: "Име клана" name: "Име" -# chieftain: "Chieftain" -# type: "Type" + chieftain: "Поглавица" + type: "Врста" edit_clan_name: "Измени име клана" edit_clan_description: "Измени опис клана" edit_name: "измени име" edit_description: "измени опис" private: "(приватан)" -# summary: "Summary" -# average_level: "Average Level" -# average_achievements: "Average Achievements" + summary: "Преглед" + average_level: "Просечни ниво" + average_achievements: "Просечна достигнућа" delete_clan: "Избриши клан" leave_clan: "Напусти клан" join_clan: "Придружи се клану" invite_1: "Позови:" -# invite_2: "*Invite players to this Clan by sending them this link." + invite_2: "*Позови играче у овај Клан тако што ћеш им послати овај линк." members: "Чланови" progress: "Напредак" -# not_started_1: "not started" -# started_1: "started" -# complete_1: "complete" -# exp_levels: "Expand levels" -# rem_hero: "Remove Hero" + not_started_1: "није започето" + started_1: "започето" + complete_1: "заврши" + exp_levels: "Прошири нивое" + rem_hero: "Уклони Хероја" status: "Статус" -# complete_2: "Complete" -# started_2: "Started" -# not_started_2: "Not Started" -# view_solution: "Click to view solution." -# view_attempt: "Click to view attempt." -# latest_achievement: "Latest Achievement" -# playtime: "Playtime" -# last_played: "Last played" -# leagues_explanation: "Play in a league against other clan members in these multiplayer arena instances." -# track_concepts1: "Track concepts" -# track_concepts2a: "learned by each student" -# track_concepts2b: "learned by each member" -# track_concepts3a: "Track levels completed for each student" -# track_concepts3b: "Track levels completed for each member" -# track_concepts4a: "See your students'" -# track_concepts4b: "See your members'" -# track_concepts5: "solutions" -# track_concepts6a: "Sort students by name or progress" -# track_concepts6b: "Sort members by name or progress" -# track_concepts7: "Requires invitation" -# track_concepts8: "to join" -# private_require_sub: "Private clans require a subscription to create or join." + complete_2: "Заврши" + started_2: "Започето" + not_started_2: "Није започето" + view_solution: "Кликни да видиш решење." + view_attempt: "Кликни да видиш покушај." + latest_achievement: "Последње достигнуће" + playtime: "Време игања" + last_played: "Последњи пут играно" + leagues_explanation: "Играј у лиги против других чланова клана у овим мултиплејер инстанцама арене." + track_concepts1: "Прати концепте" + track_concepts2a: "научене од сваког ученика" + track_concepts2b: "научене од сваког члана" + track_concepts3a: "Прати завршене нивое за сваког ученика" + track_concepts3b: "Прати завршене нивое за сваког члана" + track_concepts4a: "Види од својих ученика" + track_concepts4b: "Види од својих чланова" + track_concepts5: "решења" + track_concepts6a: "Сортирај ученике према имену или напретку" + track_concepts6b: "Сортирај чланове према имену или напретку" + track_concepts7: "Захтева позив" + track_concepts8: "за придруживање" + private_require_sub: "Приватни кланови захтевају претплату да би могао да их направиш или да им се придружиш." # courses: # course: "Course" @@ -1488,12 +1488,12 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # add_system_title: "Add Systems to Level" # done_adding: "Done Adding" -# article: -# edit_btn_preview: "Preview" -# edit_article_title: "Edit Article" + article: + edit_btn_preview: "Приказ" + edit_article_title: "Измени Чланак" -# polls: -# priority: "Priority" + polls: + priority: "Приоритет" # contribute: # page_title: "Contributing" @@ -1645,17 +1645,17 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # favorite_postfix: "." # not_member_of_clans: "Not a member of any clans yet." -# achievements: -# last_earned: "Last Earned" -# amount_achieved: "Amount" -# achievement: "Achievement" -# current_xp_prefix: "" -# current_xp_postfix: " in total" -# new_xp_prefix: "" -# new_xp_postfix: " earned" -# left_xp_prefix: "" -# left_xp_infix: " until level " -# left_xp_postfix: "" + achievements: + last_earned: "Последње стечено" + amount_achieved: "Количина" + achievement: "Достигнуће" + current_xp_prefix: "" + current_xp_postfix: " укупно" + new_xp_prefix: "" + new_xp_postfix: " стечено" + left_xp_prefix: "" + left_xp_infix: " до нивоа " + left_xp_postfix: "" account: payments: "Уплате" From 0d4a88a957643f7b9493977f2d7db5f2efe2720c Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Fri, 27 May 2016 16:03:58 -0700 Subject: [PATCH 112/167] Strip spaces in classCode on fetch and join --- server/handlers/classroom_handler.coffee | 2 +- server/middleware/classrooms.coffee | 4 ++-- spec/server/functional/classrooms.spec.coffee | 24 +++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/server/handlers/classroom_handler.coffee b/server/handlers/classroom_handler.coffee index 97daaac3d..d404183b5 100644 --- a/server/handlers/classroom_handler.coffee +++ b/server/handlers/classroom_handler.coffee @@ -84,7 +84,7 @@ ClassroomHandler = class ClassroomHandler extends Handler return @sendDatabaseError(res, err) if err return @sendSuccess(res, (@formatEntity(req, classroom) for classroom in classrooms)) else if code = req.query.code - code = code.toLowerCase() + code = code.toLowerCase().replace(/ /g, '') Classroom.findOne {code: code}, (err, classroom) => return @sendDatabaseError(res, err) if err return @sendNotFoundError(res) unless classroom diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index cb811859d..5cada7b30 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -22,7 +22,7 @@ module.exports = fetchByCode: wrap (req, res, next) -> code = req.query.code return next() unless code - classroom = yield Classroom.findOne({ code: code.toLowerCase() }).select('name ownerID aceConfig') + classroom = yield Classroom.findOne({ code: code.toLowerCase().replace(/ /g, '') }).select('name ownerID aceConfig') if not classroom log.debug("classrooms.fetchByCode: Couldn't find Classroom with code: #{code}") throw new errors.NotFound('Classroom not found.') @@ -170,7 +170,7 @@ module.exports = if req.user.isTeacher() log.debug("classrooms.join: Cannot join a classroom as a teacher: #{req.user.id}") throw new errors.Forbidden('Cannot join a classroom as a teacher') - code = req.body.code.toLowerCase() + code = req.body.code.toLowerCase().replace(/ /g, '') classroom = yield Classroom.findOne({code: code}) if not classroom log.debug("classrooms.join: Classroom not found with code #{code}") diff --git a/spec/server/functional/classrooms.spec.coffee b/spec/server/functional/classrooms.spec.coffee index 81c9f2964..b34cb3152 100644 --- a/spec/server/functional/classrooms.spec.coffee +++ b/spec/server/functional/classrooms.spec.coffee @@ -60,6 +60,18 @@ describe 'GET /db/classroom/:id', -> expect(body._id).toBe(classroomID = body._id) done() +describe 'GET /db/classroom by classCode', -> + it 'Returns the class if you include spaces', utils.wrap (done) -> + user = yield utils.initUser() + yield utils.loginUser(user) + teacher = yield utils.initUser() + classroom = new Classroom({ name: "some class", ownerID: teacher.id, camelCode: "FooBarBaz", code: "foobarbaz" }) + yield classroom.save() + [res, body] = yield request.getAsync(getURL('/db/classroom?code=foo bar baz'), { json: true }) + expect(res.statusCode).toBe(200) + expect(res.body.data?.name).toBe(classroom.get('name')) + done() + describe 'POST /db/classroom', -> beforeEach utils.wrap (done) -> @@ -295,6 +307,18 @@ describe 'POST /db/classroom/-/members', -> fail('student should be added to the free course instance.') done() + it 'joins the class even with spaces in the classcode', utils.wrap (done) -> + yield utils.loginUser(@student) + url = getURL("/db/classroom/anything-here/members") + code = @classroom.get('code') + codeWithSpaces = code.split("").join(" ") + [res, body] = yield request.postAsync { uri: url, json: { code: codeWithSpaces } } + expect(res.statusCode).toBe(200) + classroom = yield Classroom.findById(@classroom.id) + if classroom.get('members').length isnt 1 + fail 'expected classCode with spaces to work too' + done() + it 'returns 403 if the user is a teacher', utils.wrap (done) -> yield utils.loginUser(@teacher) url = getURL("/db/classroom/~/members") From d9959db856f40c1829358d63b2392c24909c53bf Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Mon, 6 Jun 2016 13:43:41 -0700 Subject: [PATCH 113/167] Fix sending invitation emails --- app/models/Classroom.coffee | 2 +- server/middleware/classrooms.coffee | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/Classroom.coffee b/app/models/Classroom.coffee index bbabc8959..d3564fab5 100644 --- a/app/models/Classroom.coffee +++ b/app/models/Classroom.coffee @@ -124,7 +124,7 @@ module.exports = class Classroom extends CocoModel inviteMembers: (emails, options={}) -> options.data ?= {} - options.data.email = emails + options.data.emails = emails options.url = @url() + '/invite-members' options.type = 'POST' @fetch(options) diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index 5cada7b30..1c2ec0f78 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -221,14 +221,15 @@ module.exports = inviteMembers: wrap (req, res) -> if not req.body.emails + log.debug "classrooms.inviteMembers: No emails included in request: #{JSON.stringify(req.body)}" throw new errors.UnprocessableEntity('Emails not included') classroom = yield database.getDocFromHandle(req, Classroom) if not classroom throw new errors.NotFound('Classroom not found.') - + unless classroom.get('ownerID').equals(req.user?._id) - log.debug "classroom_handler.inviteStudents: Can't invite to classroom (#{classroom.id}) you (#{req.user.get('_id')}) don't own" + log.debug "classroom_handler.inviteMembers: Can't invite to classroom (#{classroom.id}) you (#{req.user.get('_id')}) don't own" throw new errors.Forbidden('Must be owner of classroom to send invites.') for email in req.body.emails From 33b614911c1e4f2a8823e9090511bfb65e843a7e Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Mon, 6 Jun 2016 13:41:49 -0700 Subject: [PATCH 114/167] Support alternate NL spelling in lead import automation --- scripts/updateCloseIoLeads.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/updateCloseIoLeads.js b/scripts/updateCloseIoLeads.js index 6d9cfc029..8d1edce15 100644 --- a/scripts/updateCloseIoLeads.js +++ b/scripts/updateCloseIoLeads.js @@ -107,6 +107,7 @@ function upsertLeads(done) { function getCountryCode(country, emails) { // console.log(`DEBUG: getCountryCode ${country} ${emails.length}`); if (country) { + if (country.indexOf('Nederland') >= 0) return 'NL'; let countryCode = countryList.getCode(country); if (countryCode) return countryCode; } @@ -728,7 +729,7 @@ function createUpdateLeadFn(lead, existingLeads) { if (data.total_results === 0) { if (existingLeads[lead.name.toLowerCase()]) { if (existingLeads[lead.name.toLowerCase()].length === 1) { - console.log(`DEBUG: Using lead from email lookup: ${lead.name}`); + // console.log(`DEBUG: Using lead from email lookup: ${lead.name}`); return updateExistingLead(lead, existingLeads[lead.name.toLowerCase()][0], done); } console.error(`ERROR: ${existingLeads[lead.name.toLowerCase()].length} email leads found for ${lead.name}`); From 891d0fe12f0f9e45acac9a2caa2c1fd13a4f068d Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Mon, 6 Jun 2016 13:55:31 -0700 Subject: [PATCH 115/167] Fix TeacherClassView sometimes not loading This was a race condition where the view would trigger a render before courses loaded, and the template required them when it had some of its other resources. --- app/templates/courses/teacher-class-view.jade | 2 ++ app/views/courses/TeacherClassView.coffee | 24 +++++++++++++++---- .../teachers/TeacherClassView.spec.coffee | 15 ++++++++---- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade index 3f135c235..4ef3d947a 100644 --- a/app/templates/courses/teacher-class-view.jade +++ b/app/templates/courses/teacher-class-view.jade @@ -368,6 +368,7 @@ mixin copyCodes mixin bulkAssignControls .bulk-assign-controls.form-inline + - console.log('state errors', state.get('errors')) .no-students-selected.small-details(class=state.get('errors').assigningToNobody ? 'visible' : '') span(data-i18n='teacher.no_students_selected') .cant-assign-to-unenrolled.small-details(class=state.get('errors').assigningToUnenrolled ? 'visible' : '') @@ -378,6 +379,7 @@ mixin bulkAssignControls select.bulk-course-select.form-control each trimCourse in _.rest(view.classroom.get('courses')) - var course = view.courses.get(trimCourse._id) + - console.log('???', course) option(value=course.id selected=(course===state.get('selectedCourse'))) = course.get('name') button.btn.btn-primary-alt.assign-to-selected-students diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee index ce4c99c76..6957cc96a 100644 --- a/app/views/courses/TeacherClassView.coffee +++ b/app/views/courses/TeacherClassView.coffee @@ -71,6 +71,10 @@ module.exports = class TeacherClassView extends RootView @singleStudentLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-level' @allStudentsLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-all-students-single-level' + @debouncedRender = _.debounce -> + console.log 'we debounced', @ + @render() + @state = new State(@getInitialState()) @updateHash @state.get('activeTab') # TODO: Don't push to URL history (maybe don't use url fragment for default tab) @@ -121,10 +125,13 @@ module.exports = class TeacherClassView extends RootView attachMediatorEvents: () -> @listenTo @state, 'sync change', -> + console.log '...' if _.isEmpty(_.omit(@state.changed, 'searchTerm')) - @renderSelectors('#enrollment-status-table') + console.log 'render selectors...' +# @renderSelectors('#enrollment-status-table') else - @render() + console.log 'render...' +# @render() # Model/Collection events @listenTo @classroom, 'sync change update', -> classCode = @classroom.get('codeCamel') or @classroom.get('code') @@ -137,7 +144,6 @@ module.exports = class TeacherClassView extends RootView @state.set selectedCourse: @courses.first() unless @state.get('selectedCourse') @listenTo @courseInstances, 'sync change update', -> @setCourseMembers() - @render() # TODO: use state @listenTo @courseInstances, 'add-members', -> noty text: $.i18n.t('teacher.assigned'), layout: 'center', type: 'information', killer: true, timeout: 5000 @listenTo @students, 'sync change update add remove reset', -> @@ -149,7 +155,6 @@ module.exports = class TeacherClassView extends RootView @state.set students: @students @listenTo @students, 'sort', -> @state.set students: @students - @render() @listenTo @, 'course-select:change', ({ selectedCourse }) -> @state.set selectedCourse: selectedCourse @@ -162,6 +167,17 @@ module.exports = class TeacherClassView extends RootView onLoaded: -> @removeDeletedStudents() # TODO: Move this to mediator listeners? For both classroom and students? @calculateProgressAndLevels() + + # render callback setup + @listenTo @courseInstances, 'sync change update', @debouncedRender + console.log 'attaching' + @listenTo @state, 'sync change', -> + console.log 'we good' + if _.isEmpty(_.omit(@state.changed, 'searchTerm')) + @renderSelectors('#enrollment-status-table') + else + @debouncedRender() + @listenTo @students, 'sort', @debouncedRender super() afterRender: -> diff --git a/test/app/views/teachers/TeacherClassView.spec.coffee b/test/app/views/teachers/TeacherClassView.spec.coffee index 57a5af175..065c128ee 100644 --- a/test/app/views/teachers/TeacherClassView.spec.coffee +++ b/test/app/views/teachers/TeacherClassView.spec.coffee @@ -75,24 +75,29 @@ describe 'TeacherClassView', -> # it "shows the classroom's join code" describe 'the Students tab', -> - beforeEach -> + beforeEach (done) -> @view.state.set('activeTab', '#students-tab') + _.defer(done) # it 'shows all of the students' # it 'sorts correctly by Name' # it 'sorts correctly by Progress' describe 'bulk-assign controls', -> - it 'shows alert when assigning course 2 to unenrolled students', -> + it 'shows alert when assigning course 2 to unenrolled students', (done) -> expect(@view.$('.cant-assign-to-unenrolled').hasClass('visible')).toBe(false) @view.$('.student-row .checkbox-flat').click() @view.$('.assign-to-selected-students').click() - expect(@view.$('.cant-assign-to-unenrolled').hasClass('visible')).toBe(true) + _.defer => + expect(@view.$('.cant-assign-to-unenrolled').hasClass('visible')).toBe(true) + done() - it 'shows alert when assigning but no students are selected', -> + it 'shows alert when assigning but no students are selected', (done) -> expect(@view.$('.no-students-selected').hasClass('visible')).toBe(false) @view.$('.assign-to-selected-students').click() - expect(@view.$('.no-students-selected').hasClass('visible')).toBe(true) + _.defer => + expect(@view.$('.no-students-selected').hasClass('visible')).toBe(true) + done() # describe 'the Course Progress tab', -> # it 'shows the correct Course Overview progress' From 53a7510c46c6c49f17b1c88a87792cdb2c56a5a4 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Mon, 6 Jun 2016 14:30:58 -0700 Subject: [PATCH 116/167] Remove logs --- app/templates/courses/teacher-class-view.jade | 2 -- app/views/courses/TeacherClassView.coffee | 14 +------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade index 4ef3d947a..3f135c235 100644 --- a/app/templates/courses/teacher-class-view.jade +++ b/app/templates/courses/teacher-class-view.jade @@ -368,7 +368,6 @@ mixin copyCodes mixin bulkAssignControls .bulk-assign-controls.form-inline - - console.log('state errors', state.get('errors')) .no-students-selected.small-details(class=state.get('errors').assigningToNobody ? 'visible' : '') span(data-i18n='teacher.no_students_selected') .cant-assign-to-unenrolled.small-details(class=state.get('errors').assigningToUnenrolled ? 'visible' : '') @@ -379,7 +378,6 @@ mixin bulkAssignControls select.bulk-course-select.form-control each trimCourse in _.rest(view.classroom.get('courses')) - var course = view.courses.get(trimCourse._id) - - console.log('???', course) option(value=course.id selected=(course===state.get('selectedCourse'))) = course.get('name') button.btn.btn-primary-alt.assign-to-selected-students diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee index 6957cc96a..906337f42 100644 --- a/app/views/courses/TeacherClassView.coffee +++ b/app/views/courses/TeacherClassView.coffee @@ -71,9 +71,7 @@ module.exports = class TeacherClassView extends RootView @singleStudentLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-level' @allStudentsLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-all-students-single-level' - @debouncedRender = _.debounce -> - console.log 'we debounced', @ - @render() + @debouncedRender = _.debounce @render @state = new State(@getInitialState()) @updateHash @state.get('activeTab') # TODO: Don't push to URL history (maybe don't use url fragment for default tab) @@ -124,14 +122,6 @@ module.exports = class TeacherClassView extends RootView @attachMediatorEvents() attachMediatorEvents: () -> - @listenTo @state, 'sync change', -> - console.log '...' - if _.isEmpty(_.omit(@state.changed, 'searchTerm')) - console.log 'render selectors...' -# @renderSelectors('#enrollment-status-table') - else - console.log 'render...' -# @render() # Model/Collection events @listenTo @classroom, 'sync change update', -> classCode = @classroom.get('codeCamel') or @classroom.get('code') @@ -170,9 +160,7 @@ module.exports = class TeacherClassView extends RootView # render callback setup @listenTo @courseInstances, 'sync change update', @debouncedRender - console.log 'attaching' @listenTo @state, 'sync change', -> - console.log 'we good' if _.isEmpty(_.omit(@state.changed, 'searchTerm')) @renderSelectors('#enrollment-status-table') else From be78f4049c4a0b7e0a3ec29329e59d9d5fd940f2 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Mon, 6 Jun 2016 15:35:10 -0700 Subject: [PATCH 117/167] Fix adding inventory items to hero in level editor Was only happening when HeroPlaceholder had HasPet component. Presumably because the collection was loaded by HasPet, but the thang node for the inventory node needed to populate its autocomplete array. --- app/views/editor/component/ThangComponentConfigView.coffee | 2 +- app/views/editor/level/treema_nodes.coffee | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/editor/component/ThangComponentConfigView.coffee b/app/views/editor/component/ThangComponentConfigView.coffee index 878795081..ba95de63c 100644 --- a/app/views/editor/component/ThangComponentConfigView.coffee +++ b/app/views/editor/component/ThangComponentConfigView.coffee @@ -119,4 +119,4 @@ class SolutionsNode extends TreemaArrayNode succeeds: true }) - @set('/', solutions) \ No newline at end of file + @set('/', solutions) diff --git a/app/views/editor/level/treema_nodes.coffee b/app/views/editor/level/treema_nodes.coffee index 2cc00b7c3..ffa798b58 100644 --- a/app/views/editor/level/treema_nodes.coffee +++ b/app/views/editor/level/treema_nodes.coffee @@ -260,7 +260,10 @@ module.exports.ThangTypeNode = ThangTypeNode = class ThangTypeNode extends Treem thangType?.name or '...' getThangTypes: -> - return if ThangTypeNode.thangTypesCollection + if ThangTypeNode.thangTypesCollection + if not @constructor.thangTypes + @processThangTypes(ThangTypeNode.thangTypesCollection) + return ThangTypeNode.thangTypesCollection = new CocoCollection([], { url: '/db/thang.type' project:['name', 'components', 'original'] From 81d9e192213fe4d3747b63d0157e911ba0264eef Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Mon, 6 Jun 2016 13:22:53 -0700 Subject: [PATCH 118/167] Use SendWithUs versioning --- scripts/mail.coffee | 6 ++- server/commons/Handler.coffee | 3 +- .../handlers/course_instance_handler.coffee | 3 +- server/handlers/patch_handler.coffee | 3 +- server/middleware/auth.coffee | 5 ++- server/middleware/classrooms.coffee | 3 +- server/middleware/users.coffee | 3 +- server/middleware/versions.coffee | 5 ++- server/routes/contact.coffee | 5 ++- server/routes/mail.coffee | 6 ++- server/sendwithus.coffee | 44 ++++++++++--------- 11 files changed, 51 insertions(+), 35 deletions(-) diff --git a/scripts/mail.coffee b/scripts/mail.coffee index c73d8ee41..c9ff590c4 100644 --- a/scripts/mail.coffee +++ b/scripts/mail.coffee @@ -91,7 +91,8 @@ emailUserInitialRecruiting = (user, callback) -> team = user.session.levelInfo.team team = team.substr(0, team.length - 1) context = - email_id: sendwithus.templates.recruiting_email + email_id: sendwithus.templates.recruiting_email.id + version_name: sendwithus.templates.recruiting_email.version recipient: address: if DEBUGGING then 'nick@codecombat.com' else user.email name: name @@ -134,7 +135,8 @@ emailUserTournamentResults = (winner, callback) -> name = winner.name team = winner.team.substr(0, winner.team.length - 1) context = - email_id: sendwithus.templates.greed_tournament_rank + email_id: sendwithus.templates.greed_tournament_rank.id + version_name: sendwithus.templates.greed_tournament_rank.version recipient: address: if DEBUGGING then 'nick@codecombat.com' else winner.email name: name diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index aceff9437..c61941074 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -468,7 +468,8 @@ module.exports = class Handler notifyWatcherOfChange: (editor, watcher, changedDocument, editPath) -> context = - email_id: sendwithus.templates.change_made_notify_watcher + email_id: sendwithus.templates.change_made_notify_watcher.id + version_name: sendwithus.templates.change_made_notify_watcher.version recipient: address: watcher.get('email') name: watcher.get('name') diff --git a/server/handlers/course_instance_handler.coffee b/server/handlers/course_instance_handler.coffee index bf26df540..d7353f1ef 100644 --- a/server/handlers/course_instance_handler.coffee +++ b/server/handlers/course_instance_handler.coffee @@ -187,7 +187,8 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler return @sendForbiddenError(res) unless prepaid.get('maxRedeemers') > prepaid.get('redeemers').length for email in req.body.emails context = - email_id: sendwithus.templates.course_invite_email + email_id: sendwithus.templates.course_invite_email.id + version: sendwithus.templates.course_invite_email.version recipient: address: email subject: course.get('name') diff --git a/server/handlers/patch_handler.coffee b/server/handlers/patch_handler.coffee index b88f63a86..f47c88831 100644 --- a/server/handlers/patch_handler.coffee +++ b/server/handlers/patch_handler.coffee @@ -100,7 +100,8 @@ PatchHandler = class PatchHandler extends Handler sendPatchCreatedEmail: (patchCreator, watcher, patch, target, docLink) -> # return if watcher._id is patchCreator._id context = - email_id: sendwithus.templates.patch_created + email_id: sendwithus.templates.patch_created.id + version_name: sendwithus.templates.patch_created.version recipient: address: watcher.get('email') name: watcher.get('name') diff --git a/server/middleware/auth.coffee b/server/middleware/auth.coffee index 9f3c2f756..d60013ad1 100644 --- a/server/middleware/auth.coffee +++ b/server/middleware/auth.coffee @@ -52,7 +52,7 @@ module.exports = yield req.logInAsync(user) if req.query.callback - res.jsonp(req.user.toObject({req, publicOnly: true})) + res.jsonp(req.user.toObject({req, publicOnly: true})) else res.send(req.user.toObject({req, publicOnly: false})) res.end() @@ -132,7 +132,8 @@ module.exports = user.set('passwordReset', utils.getCodeCamel()) yield user.save() context = - email_id: sendwithus.templates.password_reset + email_id: sendwithus.templates.password_reset.id + version_name: sendwithus.templates.password_reset.version recipient: address: req.body.email email_data: diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index 1c2ec0f78..783e375e4 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -235,7 +235,8 @@ module.exports = for email in req.body.emails joinCode = (classroom.get('codeCamel') or classroom.get('code')) context = - email_id: sendwithus.templates.course_invite_email + email_id: sendwithus.templates.course_invite_email.id + version_name: sendwithus.templates.course_invite_email.version recipient: address: email email_data: diff --git a/server/middleware/users.coffee b/server/middleware/users.coffee index b334ca8f2..aff914d5b 100644 --- a/server/middleware/users.coffee +++ b/server/middleware/users.coffee @@ -85,7 +85,8 @@ module.exports = if not user throw new errors.NotFound('User not found') context = - email_id: sendwithus.templates.verify_email + email_id: sendwithus.templates.verify_email.id + version_name: sendwithus.templates.verify_email.version recipient: address: user.get('email') name: user.broadName() diff --git a/server/middleware/versions.coffee b/server/middleware/versions.coffee index aafebcb08..21cd9af90 100644 --- a/server/middleware/versions.coffee +++ b/server/middleware/versions.coffee @@ -107,7 +107,8 @@ module.exports = User.find({_id:{$in:watchers}}).select({email:1, name:1}).exec (err, watchers) -> for watcher in watchers context = - email_id: sendwithus.templates.change_made_notify_watcher + email_id: sendwithus.templates.change_made_notify_watcher.id + version_name: sendwithus.templates.change_made_notify_watcher.version recipient: address: watcher.get('email') name: watcher.get('name') @@ -127,7 +128,7 @@ module.exports = original = req.params.handle version = req.params.version if not database.isID(original) - throw new errors.UnprocessableEntity('Invalid MongoDB id: '+original) + throw new errors.UnprocessableEntity('Invalid MongoDB id: '+original) query = { 'original': mongoose.Types.ObjectId(original) } if version? diff --git a/server/routes/contact.coffee b/server/routes/contact.coffee index 651639bcb..fea0b2ff7 100644 --- a/server/routes/contact.coffee +++ b/server/routes/contact.coffee @@ -17,7 +17,7 @@ module.exports.setup = (app) -> req.user.update({$set: { enrollmentRequestSent: true }}).exec(_.noop) if req.body.recipientID is 'schools@codecombat.com' closeIO.sendMail fromAddress, subject, content, (err) -> log.error "Error sending contact form email via Close.io: #{err.message or err}" if err - else + else createSendWithUsContext req, fromAddress, subject, content, (context) -> sendwithus.api.send context, (err, result) -> log.error "Error sending contact form email via sendwithus: #{err.message or err}" if err @@ -61,7 +61,8 @@ createSendWithUsContext = (req, fromAddress, subject, content, done) -> else config.mail.supportPrimary context = - email_id: sendwithus.templates.plain_text_email + email_id: sendwithus.templates.plain_text_email.id + version_name: sendwithus.templates.plain_text_email.version recipient: address: toAddress sender: diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index b888f261e..d823d296c 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -592,7 +592,8 @@ sendLadderUpdateEmail = (session, now, daysAgo) -> sendEmail = (defeatContext, victoryContext, levelVersionsContext) -> # TODO: do something with the preferredLanguage? context = - email_id: sendwithus.templates.ladder_update_email + email_id: sendwithus.templates.ladder_update_email.id + version_name: sendwithus.templates.ladder_update_email.version recipient: address: if DEBUGGING then 'nick@codecombat.com' else user.get('email') name: name @@ -721,7 +722,8 @@ sendNextStepsEmail = (user, now, daysAgo) -> # Used to use these categories to customize the email; not doing it right now. TODO: customize it again in Sendwithus. # TODO: do something with the preferredLanguage? context = - email_id: sendwithus.templates.next_steps_email + email_id: sendwithus.templates.next_steps_email.id + version_name: sendwithus.templates.next_steps_email.version recipient: address: if DEBUGGING then 'nick@codecombat.com' else user.get('email') name: name diff --git a/server/sendwithus.coffee b/server/sendwithus.coffee index 23f834438..df6ba88c3 100644 --- a/server/sendwithus.coffee +++ b/server/sendwithus.coffee @@ -11,26 +11,30 @@ module.exports.api = send: (context, cb) -> log.debug('Tried to send email with context: ', JSON.stringify(context, null, ' ')) setTimeout(cb, 10) - + if swuAPIKey module.exports.api = new sendwithusAPI swuAPIKey, debug - + +# Version name can be supplied to tie a specific version to a deploy. +# That is most useful for testing templates with new data fields on staging. +# If it doesn't need to be synchronized to a deploy, you can just "publish" +# the new template version on SendWithUs (and leave this version blank) module.exports.templates = - parent_subscribe_email: 'tem_2APERafogvwKhmcnouigud' - share_progress_email: 'tem_VHE3ihhGmVa3727qds9zY8' - welcome_email_user: 'tem_z7Xvj3mtWYk6ec6aW7RwFk' - welcome_email_student: 'tem_4WYPZNLzs5wawMF9qUJXUH' - verify_email: 'tem_zJee6uRsRmzqzktzneCkCn' - ladder_update_email: 'JzaZxf39A4cKMxpPZUfWy4' - patch_created: 'tem_xhxuNosLALsizTNojBjNcL' - change_made_notify_watcher: 'tem_7KVkfmv9SZETb25dtHbUtG' - recruiting_email: 'tem_mdFMgtcczHKYu94Jmq68j8' - greed_tournament_rank: 'tem_c4KYnk2TriEkkZx5NqqGLG' - generic_email: 'tem_JhRnQ4pvTS4KdQjYoZdbei' - plain_text_email: 'tem_85UvKDCCNPXsFckERTig6Y' - next_steps_email: 'tem_RDHhTG5inXQi8pthyqWr5D' - course_invite_email: 'tem_ic2ZhPkpj8GBADFuyAp4bj' - teacher_free_trial: 'tem_R7d9Hpoba9SceQNiYSXBak' - teacher_free_trial_hoc: 'tem_4ZSY9wsA9Qwn4wBFmZgPdc' - teacher_request_demo: 'tem_cwG3HZjEyb6QE493hZuUra' - password_reset: 'tem_wbQUMRtLY9xhec8BSCykLA' + parent_subscribe_email: { id: 'tem_2APERafogvwKhmcnouigud' } + share_progress_email: { id: 'tem_VHE3ihhGmVa3727qds9zY8' } + welcome_email_user: { id: 'tem_z7Xvj3mtWYk6ec6aW7RwFk' } + welcome_email_student: { id: 'tem_4WYPZNLzs5wawMF9qUJXUH' } + verify_email: { id: 'tem_zJee6uRsRmzqzktzneCkCn' } + ladder_update_email: { id: 'JzaZxf39A4cKMxpPZUfWy4' } + patch_created: { id: 'tem_xhxuNosLALsizTNojBjNcL' } + change_made_notify_watcher: { id: 'tem_7KVkfmv9SZETb25dtHbUtG' } + recruiting_email: { id: 'tem_mdFMgtcczHKYu94Jmq68j8' } + greed_tournament_rank: { id: 'tem_c4KYnk2TriEkkZx5NqqGLG' } + generic_email: { id: 'tem_JhRnQ4pvTS4KdQjYoZdbei' } + plain_text_email: { id: 'tem_85UvKDCCNPXsFckERTig6Y' } + next_steps_email: { id: 'tem_RDHhTG5inXQi8pthyqWr5D' } + course_invite_email: { id: 'tem_u6D2EFWYC5Ptk38bSykjsU', version: 'v3' } + teacher_free_trial: { id: 'tem_R7d9Hpoba9SceQNiYSXBak' } + teacher_free_trial_hoc: { id: 'tem_4ZSY9wsA9Qwn4wBFmZgPdc' } + teacher_request_demo: { id: 'tem_cwG3HZjEyb6QE493hZuUra' } + password_reset: { id: 'tem_wbQUMRtLY9xhec8BSCykLA' } From fcb2ce8504daa5b2a8eeea1c561a16b7da9a0467 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Mon, 6 Jun 2016 16:00:46 -0700 Subject: [PATCH 119/167] Fix email tests --- spec/server/functional/article.spec.coffee | 8 ++++---- spec/server/functional/classrooms.spec.coffee | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/server/functional/article.spec.coffee b/spec/server/functional/article.spec.coffee index 5a5fb1c4a..d42e8f55e 100644 --- a/spec/server/functional/article.spec.coffee +++ b/spec/server/functional/article.spec.coffee @@ -127,7 +127,7 @@ describe 'POST /db/article', -> @admin = yield utils.initAdmin({}) yield utils.loginUser(@admin) [@res, @body] = yield request.postAsync { - uri: getURL('/db/article'), json: articleData + uri: getURL('/db/article'), json: articleData } done() @@ -163,8 +163,8 @@ describe 'POST /db/article', -> it 'returns 422 when properties do not pass validation', utils.wrap (done) -> - [res, body] = yield request.postAsync { - uri: getURL('/db/article'), json: { i18nCoverage: 9001 } + [res, body] = yield request.postAsync { + uri: getURL('/db/article'), json: { i18nCoverage: 9001 } } expect(res.statusCode).toBe(422) expect(body.validationErrors).toBeDefined() @@ -461,7 +461,7 @@ describe 'POST /db/article/:handle/new-version', -> it 'notifies watchers of changes', utils.wrap (done) -> sendwithus = require '../../../server/sendwithus' spyOn(sendwithus.api, 'send').and.callFake (context, cb) -> - expect(context.email_id).toBe(sendwithus.templates.change_made_notify_watcher) + expect(context.email_id).toBe(sendwithus.templates.change_made_notify_watcher.id) expect(context.recipient.address).toBe('test@gmail.com') done() user = yield User({email: 'test@gmail.com', name: 'a user'}).save() diff --git a/spec/server/functional/classrooms.spec.coffee b/spec/server/functional/classrooms.spec.coffee index b34cb3152..4ab2efd29 100644 --- a/spec/server/functional/classrooms.spec.coffee +++ b/spec/server/functional/classrooms.spec.coffee @@ -375,7 +375,7 @@ describe 'POST /db/classroom/:id/invite-members', -> data = { emails: ['test@test.com'] } sendwithus = require '../../../server/sendwithus' spyOn(sendwithus.api, 'send').and.callFake (context, cb) -> - expect(context.email_id).toBe(sendwithus.templates.course_invite_email) + expect(context.email_id).toBe(sendwithus.templates.course_invite_email.id) expect(context.recipient.address).toBe('test@test.com') expect(context.email_data.teacher_name).toBe('Mr Professerson') done() From 08bc32e0057020a9ae40cb94001032f9241f6a4c Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Mon, 6 Jun 2016 16:38:20 -0700 Subject: [PATCH 120/167] Fix some missed sendwithus changes --- server/handlers/user_handler.coffee | 4 ++-- server/models/User.coffee | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/handlers/user_handler.coffee b/server/handlers/user_handler.coffee index 455aa90bb..50779ca79 100644 --- a/server/handlers/user_handler.coffee +++ b/server/handlers/user_handler.coffee @@ -457,9 +457,9 @@ UserHandler = class UserHandler extends Handler # Type-specific email data if type is 'subscribe modal parent' - emailParams['email_id'] = sendwithus.templates.parent_subscribe_email + emailParams['email_id'] = sendwithus.templates.parent_subscribe_email.id else if type is 'share progress modal parent' - emailParams['email_id'] = sendwithus.templates.share_progress_email + emailParams['email_id'] = sendwithus.templates.share_progress_email.id sendMail emailParams diff --git a/server/models/User.coffee b/server/models/User.coffee index fa43f8c32..59e16143d 100644 --- a/server/models/User.coffee +++ b/server/models/User.coffee @@ -272,7 +272,7 @@ UserSchema.methods.register = (done) -> { welcome_email_student, welcome_email_user } = sendwithus.templates timestamp = (new Date).getTime() data = - email_id: if @isStudent() then welcome_email_student else welcome_email_user + email_id: if @isStudent() then welcome_email_student.id else welcome_email_user.id recipient: address: @get('email') name: @broadName() From 9d4215d99df8c3f0e85f705cea8a05b7fded4c39 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Mon, 6 Jun 2016 16:48:21 -0700 Subject: [PATCH 121/167] Fix more sendwithus things --- server/handlers/user_handler.coffee | 2 ++ server/models/User.coffee | 1 + 2 files changed, 3 insertions(+) diff --git a/server/handlers/user_handler.coffee b/server/handlers/user_handler.coffee index 50779ca79..2d97e95d1 100644 --- a/server/handlers/user_handler.coffee +++ b/server/handlers/user_handler.coffee @@ -458,8 +458,10 @@ UserHandler = class UserHandler extends Handler # Type-specific email data if type is 'subscribe modal parent' emailParams['email_id'] = sendwithus.templates.parent_subscribe_email.id + emailParams['version_name'] = sendwithus.templates.parent_subscribe_email.version else if type is 'share progress modal parent' emailParams['email_id'] = sendwithus.templates.share_progress_email.id + emailParams['version_name'] = sendwithus.templates.share_progress_email.version sendMail emailParams diff --git a/server/models/User.coffee b/server/models/User.coffee index 59e16143d..e177ae7ee 100644 --- a/server/models/User.coffee +++ b/server/models/User.coffee @@ -273,6 +273,7 @@ UserSchema.methods.register = (done) -> timestamp = (new Date).getTime() data = email_id: if @isStudent() then welcome_email_student.id else welcome_email_user.id + version_name: if @isStudent() then welcome_email_student.version else welcome_email_user.version recipient: address: @get('email') name: @broadName() From c191f63c156672e3eda070c32eaa6a3cfc6dd62a Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Mon, 6 Jun 2016 16:53:05 -0700 Subject: [PATCH 122/167] Revert all of my sendwithus changes Revert "Fix more sendwithus things" This reverts commit 9d4215d99df8c3f0e85f705cea8a05b7fded4c39. Revert "Fix some missed sendwithus changes" This reverts commit 08bc32e0057020a9ae40cb94001032f9241f6a4c. Revert "Fix email tests" This reverts commit fcb2ce8504daa5b2a8eeea1c561a16b7da9a0467. Revert "Use SendWithUs versioning" This reverts commit 81d9e192213fe4d3747b63d0157e911ba0264eef. --- scripts/mail.coffee | 6 +-- server/commons/Handler.coffee | 3 +- .../handlers/course_instance_handler.coffee | 3 +- server/handlers/patch_handler.coffee | 3 +- server/handlers/user_handler.coffee | 6 +-- server/middleware/auth.coffee | 5 +-- server/middleware/classrooms.coffee | 3 +- server/middleware/users.coffee | 3 +- server/middleware/versions.coffee | 5 +-- server/models/User.coffee | 3 +- server/routes/contact.coffee | 5 +-- server/routes/mail.coffee | 6 +-- server/sendwithus.coffee | 44 +++++++++---------- spec/server/functional/article.spec.coffee | 8 ++-- spec/server/functional/classrooms.spec.coffee | 2 +- 15 files changed, 43 insertions(+), 62 deletions(-) diff --git a/scripts/mail.coffee b/scripts/mail.coffee index c9ff590c4..c73d8ee41 100644 --- a/scripts/mail.coffee +++ b/scripts/mail.coffee @@ -91,8 +91,7 @@ emailUserInitialRecruiting = (user, callback) -> team = user.session.levelInfo.team team = team.substr(0, team.length - 1) context = - email_id: sendwithus.templates.recruiting_email.id - version_name: sendwithus.templates.recruiting_email.version + email_id: sendwithus.templates.recruiting_email recipient: address: if DEBUGGING then 'nick@codecombat.com' else user.email name: name @@ -135,8 +134,7 @@ emailUserTournamentResults = (winner, callback) -> name = winner.name team = winner.team.substr(0, winner.team.length - 1) context = - email_id: sendwithus.templates.greed_tournament_rank.id - version_name: sendwithus.templates.greed_tournament_rank.version + email_id: sendwithus.templates.greed_tournament_rank recipient: address: if DEBUGGING then 'nick@codecombat.com' else winner.email name: name diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee index c61941074..aceff9437 100644 --- a/server/commons/Handler.coffee +++ b/server/commons/Handler.coffee @@ -468,8 +468,7 @@ module.exports = class Handler notifyWatcherOfChange: (editor, watcher, changedDocument, editPath) -> context = - email_id: sendwithus.templates.change_made_notify_watcher.id - version_name: sendwithus.templates.change_made_notify_watcher.version + email_id: sendwithus.templates.change_made_notify_watcher recipient: address: watcher.get('email') name: watcher.get('name') diff --git a/server/handlers/course_instance_handler.coffee b/server/handlers/course_instance_handler.coffee index d7353f1ef..bf26df540 100644 --- a/server/handlers/course_instance_handler.coffee +++ b/server/handlers/course_instance_handler.coffee @@ -187,8 +187,7 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler return @sendForbiddenError(res) unless prepaid.get('maxRedeemers') > prepaid.get('redeemers').length for email in req.body.emails context = - email_id: sendwithus.templates.course_invite_email.id - version: sendwithus.templates.course_invite_email.version + email_id: sendwithus.templates.course_invite_email recipient: address: email subject: course.get('name') diff --git a/server/handlers/patch_handler.coffee b/server/handlers/patch_handler.coffee index f47c88831..b88f63a86 100644 --- a/server/handlers/patch_handler.coffee +++ b/server/handlers/patch_handler.coffee @@ -100,8 +100,7 @@ PatchHandler = class PatchHandler extends Handler sendPatchCreatedEmail: (patchCreator, watcher, patch, target, docLink) -> # return if watcher._id is patchCreator._id context = - email_id: sendwithus.templates.patch_created.id - version_name: sendwithus.templates.patch_created.version + email_id: sendwithus.templates.patch_created recipient: address: watcher.get('email') name: watcher.get('name') diff --git a/server/handlers/user_handler.coffee b/server/handlers/user_handler.coffee index 2d97e95d1..455aa90bb 100644 --- a/server/handlers/user_handler.coffee +++ b/server/handlers/user_handler.coffee @@ -457,11 +457,9 @@ UserHandler = class UserHandler extends Handler # Type-specific email data if type is 'subscribe modal parent' - emailParams['email_id'] = sendwithus.templates.parent_subscribe_email.id - emailParams['version_name'] = sendwithus.templates.parent_subscribe_email.version + emailParams['email_id'] = sendwithus.templates.parent_subscribe_email else if type is 'share progress modal parent' - emailParams['email_id'] = sendwithus.templates.share_progress_email.id - emailParams['version_name'] = sendwithus.templates.share_progress_email.version + emailParams['email_id'] = sendwithus.templates.share_progress_email sendMail emailParams diff --git a/server/middleware/auth.coffee b/server/middleware/auth.coffee index d60013ad1..9f3c2f756 100644 --- a/server/middleware/auth.coffee +++ b/server/middleware/auth.coffee @@ -52,7 +52,7 @@ module.exports = yield req.logInAsync(user) if req.query.callback - res.jsonp(req.user.toObject({req, publicOnly: true})) + res.jsonp(req.user.toObject({req, publicOnly: true})) else res.send(req.user.toObject({req, publicOnly: false})) res.end() @@ -132,8 +132,7 @@ module.exports = user.set('passwordReset', utils.getCodeCamel()) yield user.save() context = - email_id: sendwithus.templates.password_reset.id - version_name: sendwithus.templates.password_reset.version + email_id: sendwithus.templates.password_reset recipient: address: req.body.email email_data: diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index 783e375e4..1c2ec0f78 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -235,8 +235,7 @@ module.exports = for email in req.body.emails joinCode = (classroom.get('codeCamel') or classroom.get('code')) context = - email_id: sendwithus.templates.course_invite_email.id - version_name: sendwithus.templates.course_invite_email.version + email_id: sendwithus.templates.course_invite_email recipient: address: email email_data: diff --git a/server/middleware/users.coffee b/server/middleware/users.coffee index aff914d5b..b334ca8f2 100644 --- a/server/middleware/users.coffee +++ b/server/middleware/users.coffee @@ -85,8 +85,7 @@ module.exports = if not user throw new errors.NotFound('User not found') context = - email_id: sendwithus.templates.verify_email.id - version_name: sendwithus.templates.verify_email.version + email_id: sendwithus.templates.verify_email recipient: address: user.get('email') name: user.broadName() diff --git a/server/middleware/versions.coffee b/server/middleware/versions.coffee index 21cd9af90..aafebcb08 100644 --- a/server/middleware/versions.coffee +++ b/server/middleware/versions.coffee @@ -107,8 +107,7 @@ module.exports = User.find({_id:{$in:watchers}}).select({email:1, name:1}).exec (err, watchers) -> for watcher in watchers context = - email_id: sendwithus.templates.change_made_notify_watcher.id - version_name: sendwithus.templates.change_made_notify_watcher.version + email_id: sendwithus.templates.change_made_notify_watcher recipient: address: watcher.get('email') name: watcher.get('name') @@ -128,7 +127,7 @@ module.exports = original = req.params.handle version = req.params.version if not database.isID(original) - throw new errors.UnprocessableEntity('Invalid MongoDB id: '+original) + throw new errors.UnprocessableEntity('Invalid MongoDB id: '+original) query = { 'original': mongoose.Types.ObjectId(original) } if version? diff --git a/server/models/User.coffee b/server/models/User.coffee index e177ae7ee..fa43f8c32 100644 --- a/server/models/User.coffee +++ b/server/models/User.coffee @@ -272,8 +272,7 @@ UserSchema.methods.register = (done) -> { welcome_email_student, welcome_email_user } = sendwithus.templates timestamp = (new Date).getTime() data = - email_id: if @isStudent() then welcome_email_student.id else welcome_email_user.id - version_name: if @isStudent() then welcome_email_student.version else welcome_email_user.version + email_id: if @isStudent() then welcome_email_student else welcome_email_user recipient: address: @get('email') name: @broadName() diff --git a/server/routes/contact.coffee b/server/routes/contact.coffee index fea0b2ff7..651639bcb 100644 --- a/server/routes/contact.coffee +++ b/server/routes/contact.coffee @@ -17,7 +17,7 @@ module.exports.setup = (app) -> req.user.update({$set: { enrollmentRequestSent: true }}).exec(_.noop) if req.body.recipientID is 'schools@codecombat.com' closeIO.sendMail fromAddress, subject, content, (err) -> log.error "Error sending contact form email via Close.io: #{err.message or err}" if err - else + else createSendWithUsContext req, fromAddress, subject, content, (context) -> sendwithus.api.send context, (err, result) -> log.error "Error sending contact form email via sendwithus: #{err.message or err}" if err @@ -61,8 +61,7 @@ createSendWithUsContext = (req, fromAddress, subject, content, done) -> else config.mail.supportPrimary context = - email_id: sendwithus.templates.plain_text_email.id - version_name: sendwithus.templates.plain_text_email.version + email_id: sendwithus.templates.plain_text_email recipient: address: toAddress sender: diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index d823d296c..b888f261e 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -592,8 +592,7 @@ sendLadderUpdateEmail = (session, now, daysAgo) -> sendEmail = (defeatContext, victoryContext, levelVersionsContext) -> # TODO: do something with the preferredLanguage? context = - email_id: sendwithus.templates.ladder_update_email.id - version_name: sendwithus.templates.ladder_update_email.version + email_id: sendwithus.templates.ladder_update_email recipient: address: if DEBUGGING then 'nick@codecombat.com' else user.get('email') name: name @@ -722,8 +721,7 @@ sendNextStepsEmail = (user, now, daysAgo) -> # Used to use these categories to customize the email; not doing it right now. TODO: customize it again in Sendwithus. # TODO: do something with the preferredLanguage? context = - email_id: sendwithus.templates.next_steps_email.id - version_name: sendwithus.templates.next_steps_email.version + email_id: sendwithus.templates.next_steps_email recipient: address: if DEBUGGING then 'nick@codecombat.com' else user.get('email') name: name diff --git a/server/sendwithus.coffee b/server/sendwithus.coffee index df6ba88c3..23f834438 100644 --- a/server/sendwithus.coffee +++ b/server/sendwithus.coffee @@ -11,30 +11,26 @@ module.exports.api = send: (context, cb) -> log.debug('Tried to send email with context: ', JSON.stringify(context, null, ' ')) setTimeout(cb, 10) - + if swuAPIKey module.exports.api = new sendwithusAPI swuAPIKey, debug - -# Version name can be supplied to tie a specific version to a deploy. -# That is most useful for testing templates with new data fields on staging. -# If it doesn't need to be synchronized to a deploy, you can just "publish" -# the new template version on SendWithUs (and leave this version blank) + module.exports.templates = - parent_subscribe_email: { id: 'tem_2APERafogvwKhmcnouigud' } - share_progress_email: { id: 'tem_VHE3ihhGmVa3727qds9zY8' } - welcome_email_user: { id: 'tem_z7Xvj3mtWYk6ec6aW7RwFk' } - welcome_email_student: { id: 'tem_4WYPZNLzs5wawMF9qUJXUH' } - verify_email: { id: 'tem_zJee6uRsRmzqzktzneCkCn' } - ladder_update_email: { id: 'JzaZxf39A4cKMxpPZUfWy4' } - patch_created: { id: 'tem_xhxuNosLALsizTNojBjNcL' } - change_made_notify_watcher: { id: 'tem_7KVkfmv9SZETb25dtHbUtG' } - recruiting_email: { id: 'tem_mdFMgtcczHKYu94Jmq68j8' } - greed_tournament_rank: { id: 'tem_c4KYnk2TriEkkZx5NqqGLG' } - generic_email: { id: 'tem_JhRnQ4pvTS4KdQjYoZdbei' } - plain_text_email: { id: 'tem_85UvKDCCNPXsFckERTig6Y' } - next_steps_email: { id: 'tem_RDHhTG5inXQi8pthyqWr5D' } - course_invite_email: { id: 'tem_u6D2EFWYC5Ptk38bSykjsU', version: 'v3' } - teacher_free_trial: { id: 'tem_R7d9Hpoba9SceQNiYSXBak' } - teacher_free_trial_hoc: { id: 'tem_4ZSY9wsA9Qwn4wBFmZgPdc' } - teacher_request_demo: { id: 'tem_cwG3HZjEyb6QE493hZuUra' } - password_reset: { id: 'tem_wbQUMRtLY9xhec8BSCykLA' } + parent_subscribe_email: 'tem_2APERafogvwKhmcnouigud' + share_progress_email: 'tem_VHE3ihhGmVa3727qds9zY8' + welcome_email_user: 'tem_z7Xvj3mtWYk6ec6aW7RwFk' + welcome_email_student: 'tem_4WYPZNLzs5wawMF9qUJXUH' + verify_email: 'tem_zJee6uRsRmzqzktzneCkCn' + ladder_update_email: 'JzaZxf39A4cKMxpPZUfWy4' + patch_created: 'tem_xhxuNosLALsizTNojBjNcL' + change_made_notify_watcher: 'tem_7KVkfmv9SZETb25dtHbUtG' + recruiting_email: 'tem_mdFMgtcczHKYu94Jmq68j8' + greed_tournament_rank: 'tem_c4KYnk2TriEkkZx5NqqGLG' + generic_email: 'tem_JhRnQ4pvTS4KdQjYoZdbei' + plain_text_email: 'tem_85UvKDCCNPXsFckERTig6Y' + next_steps_email: 'tem_RDHhTG5inXQi8pthyqWr5D' + course_invite_email: 'tem_ic2ZhPkpj8GBADFuyAp4bj' + teacher_free_trial: 'tem_R7d9Hpoba9SceQNiYSXBak' + teacher_free_trial_hoc: 'tem_4ZSY9wsA9Qwn4wBFmZgPdc' + teacher_request_demo: 'tem_cwG3HZjEyb6QE493hZuUra' + password_reset: 'tem_wbQUMRtLY9xhec8BSCykLA' diff --git a/spec/server/functional/article.spec.coffee b/spec/server/functional/article.spec.coffee index d42e8f55e..5a5fb1c4a 100644 --- a/spec/server/functional/article.spec.coffee +++ b/spec/server/functional/article.spec.coffee @@ -127,7 +127,7 @@ describe 'POST /db/article', -> @admin = yield utils.initAdmin({}) yield utils.loginUser(@admin) [@res, @body] = yield request.postAsync { - uri: getURL('/db/article'), json: articleData + uri: getURL('/db/article'), json: articleData } done() @@ -163,8 +163,8 @@ describe 'POST /db/article', -> it 'returns 422 when properties do not pass validation', utils.wrap (done) -> - [res, body] = yield request.postAsync { - uri: getURL('/db/article'), json: { i18nCoverage: 9001 } + [res, body] = yield request.postAsync { + uri: getURL('/db/article'), json: { i18nCoverage: 9001 } } expect(res.statusCode).toBe(422) expect(body.validationErrors).toBeDefined() @@ -461,7 +461,7 @@ describe 'POST /db/article/:handle/new-version', -> it 'notifies watchers of changes', utils.wrap (done) -> sendwithus = require '../../../server/sendwithus' spyOn(sendwithus.api, 'send').and.callFake (context, cb) -> - expect(context.email_id).toBe(sendwithus.templates.change_made_notify_watcher.id) + expect(context.email_id).toBe(sendwithus.templates.change_made_notify_watcher) expect(context.recipient.address).toBe('test@gmail.com') done() user = yield User({email: 'test@gmail.com', name: 'a user'}).save() diff --git a/spec/server/functional/classrooms.spec.coffee b/spec/server/functional/classrooms.spec.coffee index 4ab2efd29..b34cb3152 100644 --- a/spec/server/functional/classrooms.spec.coffee +++ b/spec/server/functional/classrooms.spec.coffee @@ -375,7 +375,7 @@ describe 'POST /db/classroom/:id/invite-members', -> data = { emails: ['test@test.com'] } sendwithus = require '../../../server/sendwithus' spyOn(sendwithus.api, 'send').and.callFake (context, cb) -> - expect(context.email_id).toBe(sendwithus.templates.course_invite_email.id) + expect(context.email_id).toBe(sendwithus.templates.course_invite_email) expect(context.recipient.address).toBe('test@test.com') expect(context.email_data.teacher_name).toBe('Mr Professerson') done() From f439ffddc790cfff87b09157e18e26cea8066249 Mon Sep 17 00:00:00 2001 From: duybkict Date: Tue, 7 Jun 2016 09:42:57 +0700 Subject: [PATCH 123/167] fix issue #3678 --- app/lib/LevelLoader.coffee | 4 ++++ app/styles/play/level/loading.sass | 2 +- app/templates/play/level/level_loading.jade | 3 +++ app/views/play/level/LevelLoadingView.coffee | 4 ++++ app/views/play/level/PlayLevelView.coffee | 4 ++++ 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index 34548f6dc..a3a2725f6 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -40,6 +40,7 @@ module.exports = class LevelLoader extends CocoClass @worldNecessities = [] @listenTo @supermodel, 'resource-loaded', @onWorldNecessityLoaded + @listenTo @supermodel, 'failed', @onWorldNecessityLoadFailed @loadLevel() @loadAudio() @playJingle() @@ -329,6 +330,9 @@ module.exports = class LevelLoader extends CocoClass @worldNecessities = (r for r in @worldNecessities when r?) @onWorldNecessitiesLoaded() if @checkAllWorldNecessitiesRegisteredAndLoaded() + onWorldNecessityLoadFailed: (resource) -> + @trigger('world-necessity-load-failed', resource: resource) + checkAllWorldNecessitiesRegisteredAndLoaded: -> return false unless _.filter(@worldNecessities).length is 0 return false unless @thangNamesLoaded diff --git a/app/styles/play/level/loading.sass b/app/styles/play/level/loading.sass index 398ef1d7d..d5d3f4537 100644 --- a/app/styles/play/level/loading.sass +++ b/app/styles/play/level/loading.sass @@ -157,7 +157,7 @@ $UNVEIL_TIME: 1.2s font-variant: small-caps text-transform: none - .subscription-required, .course-membership-required + .subscription-required, .course-membership-required, .could-not-load display: none margin-top: -160px color: black diff --git a/app/templates/play/level/level_loading.jade b/app/templates/play/level/level_loading.jade index e207c4103..1e6852a0e 100644 --- a/app/templates/play/level/level_loading.jade +++ b/app/templates/play/level/level_loading.jade @@ -25,6 +25,9 @@ .progress-bar.progress-bar-success .rim + .could-not-load + span(data-i18n="loading_error.could_not_load") Error loading from server + .subscription-required span(data-i18n="subscribe.subscription_required_to_play") You'll need a subscription to play this level. button.start-subscription-button.btn.btn-lg.btn-warning(data-i18n="subscribe.subscribe") Subscribe diff --git a/app/views/play/level/LevelLoadingView.coffee b/app/views/play/level/LevelLoadingView.coffee index cabeca1f5..81c3fa6e1 100644 --- a/app/views/play/level/LevelLoadingView.coffee +++ b/app/views/play/level/LevelLoadingView.coffee @@ -204,6 +204,10 @@ module.exports = class LevelLoadingView extends CocoView @$el.find('.level-loading-goals, .tip, .load-progress').hide() @$el.find('.course-membership-required').show() + onLoadError: (resource) -> + @$el.find('.level-loading-goals, .tip, .load-progress').hide() + @$el.find('.could-not-load').show() + onClickStartSubscription: (e) -> @openModalView new SubscribeModal() levelSlug = @level?.get('slug') or @options.level?.get('slug') diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index 7a100c50a..e5e3507a0 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -141,6 +141,7 @@ module.exports = class PlayLevelView extends RootView levelLoaderOptions.fakeSessionConfig = {} @levelLoader = new LevelLoader levelLoaderOptions @listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded + @listenTo @levelLoader, 'world-necessity-load-failed', @onWorldNecessityLoadFailed trackLevelLoadEnd: -> return if @isEditorPreview @@ -195,6 +196,9 @@ module.exports = class PlayLevelView extends RootView @controlBar.setBus(@bus) @initScriptManager() + onWorldNecessityLoadFailed: (resource) -> + @loadingView.onLoadError(resource) + grabLevelLoaderData: -> @session = @levelLoader.session @world = @levelLoader.world From 7e4573f37cdd1c28fa0c1e6444585d9e10ca40ec Mon Sep 17 00:00:00 2001 From: duybkict Date: Tue, 7 Jun 2016 17:09:00 +0700 Subject: [PATCH 124/167] #3138 refactor contributor views --- .../contribute/contributor_list.jade | 25 ++++++++++--------- app/views/contribute/AdventurerView.coffee | 4 ++- app/views/contribute/AmbassadorView.coffee | 4 ++- app/views/contribute/ArchmageView.coffee | 4 ++- app/views/contribute/ArtisanView.coffee | 4 ++- .../contribute/ContributeClassView.coffee | 9 ------- app/views/contribute/DiplomatView.coffee | 4 ++- .../contribute/MainContributeView.coffee | 1 - app/views/contribute/ScribeView.coffee | 4 ++- 9 files changed, 31 insertions(+), 28 deletions(-) diff --git a/app/templates/contribute/contributor_list.jade b/app/templates/contribute/contributor_list.jade index 32e5ac9e3..3604c4b90 100644 --- a/app/templates/contribute/contributor_list.jade +++ b/app/templates/contribute/contributor_list.jade @@ -1,13 +1,14 @@ .row - for contributor in contributors - .col-xs-6.col-md-3 - .thumbnail - - var src = "/images/pages/contribute/" + contributorClassName + ".png"; - - if(contributor.avatar) - - src = src.replace(contributorClassName, contributorClassName + "/" + contributor.avatar + "_small"); - - if(contributor.id) - - src = "/db/user/" + contributor.id + "/avatar?s=100&fallback=" + src; - a(href=contributor.github ? "https://github.com/codecombat/codecombat/commits?author=" + contributor.github : null, class=contributor.github ? 'has-github' : '') - img.img-responsive(src=src, alt=contributor.name) - .caption - h4= contributor.name + if contributors + for contributor in contributors + .col-xs-6.col-md-3 + .thumbnail + - var src = "/images/pages/contribute/" + contributorClassName + ".png"; + - if(contributor.avatar) + - src = src.replace(contributorClassName, contributorClassName + "/" + contributor.avatar + "_small"); + - if(contributor.id) + - src = "/db/user/" + contributor.id + "/avatar?s=100&fallback=" + src; + a(href=contributor.github ? "https://github.com/codecombat/codecombat/commits?author=" + contributor.github : null, class=contributor.github ? 'has-github' : '') + img.img-responsive(src=src, alt=contributor.name) + .caption + h4= contributor.name diff --git a/app/views/contribute/AdventurerView.coffee b/app/views/contribute/AdventurerView.coffee index f88130f4a..e82c262dc 100644 --- a/app/views/contribute/AdventurerView.coffee +++ b/app/views/contribute/AdventurerView.coffee @@ -5,4 +5,6 @@ template = require 'templates/contribute/adventurer' module.exports = class AdventurerView extends ContributeClassView id: 'adventurer-view' template: template - contributorClassName: 'adventurer' + + initialize: -> + @contributorClassName = 'adventurer' diff --git a/app/views/contribute/AmbassadorView.coffee b/app/views/contribute/AmbassadorView.coffee index cd10501c8..9dfb3b7f6 100644 --- a/app/views/contribute/AmbassadorView.coffee +++ b/app/views/contribute/AmbassadorView.coffee @@ -5,4 +5,6 @@ template = require 'templates/contribute/ambassador' module.exports = class AmbassadorView extends ContributeClassView id: 'ambassador-view' template: template - contributorClassName: 'ambassador' + + initialize: -> + @contributorClassName = 'ambassador' diff --git a/app/views/contribute/ArchmageView.coffee b/app/views/contribute/ArchmageView.coffee index 93cdc9401..5bd9fe553 100644 --- a/app/views/contribute/ArchmageView.coffee +++ b/app/views/contribute/ArchmageView.coffee @@ -4,7 +4,9 @@ template = require 'templates/contribute/archmage' module.exports = class ArchmageView extends ContributeClassView id: 'archmage-view' template: template - contributorClassName: 'archmage' + + initialize: -> + @contributorClassName = 'archmage' contributors: [ {id: '547acbb2af18b03c0563fdb3', name: 'David Liu', github: 'trotod'} diff --git a/app/views/contribute/ArtisanView.coffee b/app/views/contribute/ArtisanView.coffee index fa4c7433b..7d6c350b5 100644 --- a/app/views/contribute/ArtisanView.coffee +++ b/app/views/contribute/ArtisanView.coffee @@ -5,7 +5,9 @@ template = require 'templates/contribute/artisan' module.exports = class ArtisanView extends ContributeClassView id: 'artisan-view' template: template - contributorClassName: 'artisan' + + initialize: -> + @contributorClassName = 'artisan' contributors: [ {id: '5276ad5dcf83207a2801d3b4', name: 'Zach Martin', github: 'zachster01'} diff --git a/app/views/contribute/ContributeClassView.coffee b/app/views/contribute/ContributeClassView.coffee index 34a0b2ef2..dfc931770 100644 --- a/app/views/contribute/ContributeClassView.coffee +++ b/app/views/contribute/ContributeClassView.coffee @@ -6,17 +6,10 @@ contributorSignupTemplate = require 'templates/contribute/contributor_signup' contributorListTemplate = require 'templates/contribute/contributor_list' module.exports = class ContributeClassView extends RootView - navPrefix: '/contribute' events: 'change input[type="checkbox"]': 'onCheckboxChanged' - getRenderData: -> - c = super() - c.navPrefix = @navPrefix - c.contributors = @contributors - c - afterRender: -> super() @$el.find('.contributor-signup-anonymous').replaceWith(contributorSignupAnonymousTemplate(me: me)) @@ -39,5 +32,3 @@ module.exports = class ContributeClassView extends RootView me.patch() @openModalView new CreateAccountModal() if me.get 'anonymous' el.parent().find('.saved-notification').finish().show('fast').delay(3000).fadeOut(2000) - - contributors: [] diff --git a/app/views/contribute/DiplomatView.coffee b/app/views/contribute/DiplomatView.coffee index bbf74cb45..802d0ec7d 100644 --- a/app/views/contribute/DiplomatView.coffee +++ b/app/views/contribute/DiplomatView.coffee @@ -60,7 +60,9 @@ require("locale/et") module.exports = class DiplomatView extends ContributeClassView id: 'diplomat-view' template: template - contributorClassName: 'diplomat' + + initialize: -> + @contributorClassName = 'diplomat' calculateSpokenLanguageStats: -> @locale ?= require 'locale/locale' diff --git a/app/views/contribute/MainContributeView.coffee b/app/views/contribute/MainContributeView.coffee index 930870110..55a382c49 100644 --- a/app/views/contribute/MainContributeView.coffee +++ b/app/views/contribute/MainContributeView.coffee @@ -4,7 +4,6 @@ template = require 'templates/contribute/contribute' module.exports = class MainContributeView extends ContributeClassView id: 'contribute-view' template: template - navPrefix: '' events: 'change input[type="checkbox"]': 'onCheckboxChanged' diff --git a/app/views/contribute/ScribeView.coffee b/app/views/contribute/ScribeView.coffee index 319a4fef1..16a45cd31 100644 --- a/app/views/contribute/ScribeView.coffee +++ b/app/views/contribute/ScribeView.coffee @@ -5,7 +5,9 @@ template = require 'templates/contribute/scribe' module.exports = class ScribeView extends ContributeClassView id: 'scribe-view' template: template - contributorClassName: 'scribe' + + initialize: -> + @contributorClassName = 'scribe' contributors: [ {name: 'Ryan Faidley'} From c1f1b4a0483ff7d42df2788524a93d1c379ca4c9 Mon Sep 17 00:00:00 2001 From: duybkict Date: Tue, 7 Jun 2016 18:05:06 +0700 Subject: [PATCH 125/167] #3138 #3488 refactor LadderPlayModal --- app/templates/play/ladder/play_modal.jade | 82 +++++++++++------------ app/views/ladder/LadderPlayModal.coffee | 70 ++++++++----------- 2 files changed, 68 insertions(+), 84 deletions(-) diff --git a/app/templates/play/ladder/play_modal.jade b/app/templates/play/ladder/play_modal.jade index 84063c62b..10118e10c 100644 --- a/app/templates/play/ladder/play_modal.jade +++ b/app/templates/play/ladder/play_modal.jade @@ -9,81 +9,81 @@ block modal-body-content h4.language-selection(data-i18n="ladder.select_your_language") Select your language! .form-group.select-group select#tome-language(name="language") - for option in languages - option(value=option.id selected=(language === option.id))= option.name + for option in view.languages + option(value=option.id selected=(view.language === option.id))= option.name div#noob-view.secret - a(href="/play/level/#{levelID}-tutorial" + (league ? "?league=" + league.id : "")).btn.btn-success.btn-block.btn-lg + a(href="/play/level/#{view.levelID}-tutorial" + (view.options.league ? "?league=" + view.options.league.id : "")).btn.btn-success.btn-block.btn-lg p strong(data-i18n="ladder.tutorial_play") Play Tutorial span(data-i18n="ladder.tutorial_recommended") Recommended if you've never played before span.btn.btn-primary.btn-block.btn-lg#skip-tutorial-button(data-i18n="ladder.tutorial_skip") Skip Tutorial div#normal-view - if tutorialLevelExists + if view.tutorialLevelExists p.tutorial-suggestion strong(data-i18n="ladder.tutorial_not_sure") Not sure what's going on? | - a(href="/play/level/#{levelID}-tutorial" + (league ? "?league=" + league.id : ""), data-i18n="ladder.tutorial_play_first") Play the tutorial first. - a(href="/play/level/#{levelID}?team=#{teamID}" + (league ? "&league=" + league.id : "")) + a(href="/play/level/#{view.levelID}-tutorial" + (view.options.league ? "?league=" + view.options.league.id : ""), data-i18n="ladder.tutorial_play_first") Play the tutorial first. + a(href="/play/level/#{view.levelID}?team=#{view.team}" + (view.options.league ? "&league=" + view.options.league.id : "")) div.play-option - img(src=myPortrait).my-icon.only-one - img(src="/images/pages/play/ladder/"+teamID+"_ladder_tutorial.png", style="border: 1px solid #{teamColor}; background: #{teamBackgroundColor}").my-team-icon.img-circle.only-one - img(src=genericPortrait).opponent-icon - img(src="/images/pages/play/ladder/"+otherTeamID+"_ladder_tutorial.png", style="border: 1px solid #{opponentTeamColor}; background: #{opponentTeamBackgroundColor}").opponent-team-icon.img-circle + img(src=view.myPortrait).my-icon.only-one + img(src="/images/pages/play/ladder/#{view.team}_ladder_tutorial.png", style="border: 1px solid #{view.teamColor}; background: #{view.teamBackgroundColor}").my-team-icon.img-circle.only-one + img(src=view.genericPortrait).opponent-icon + img(src="/images/pages/play/ladder/#{view.otherTeam}_ladder_tutorial.png", style="border: 1px solid #{view.opponentTeamColor}; background: #{view.opponentTeamBackgroundColor}").opponent-team-icon.img-circle div.my-name.name-label.only-one - span= myName + span= view.myName div.opponent-name.name-label span(data-i18n="ladder.simple_ai") //span.code-language(style="background-image: url(/images/common/code_languages/javascript_small.png)") div.difficulty span(data-i18n="ladder.warmup") Warmup - if challengers.easy - a(href="/play/level/#{levelID}?team=#{teamID}&opponent=#{challengers.easy.sessionID}" + (league ? "&league=" + league.id : "")) + if view.challengers && view.challengers.easy + a(href="/play/level/#{view.levelID}?team=#{view.team}&opponent=#{view.challengers.easy.sessionID}" + (view.options.league ? "&league=" + view.options.league.id : "")) div.play-option.easy-option - img(src=myPortrait).my-icon.only-one - img(src="/images/pages/play/ladder/"+teamID+"_ladder_easy.png", style="border: 1px solid #{teamColor}; background: #{teamBackgroundColor}").my-team-icon.img-circle.only-one - img(src=challengers.easy.opponentImageSource||genericPortrait).opponent-icon - img(src="/images/pages/play/ladder/"+otherTeamID+"_ladder_easy.png", style="border: 1px solid #{opponentTeamColor}; background: #{opponentTeamBackgroundColor}").opponent-team-icon.img-circle + img(src=view.myPortrait).my-icon.only-one + img(src="/images/pages/play/ladder/#{view.team}_ladder_easy.png", style="border: 1px solid #{view.teamColor}; background: #{view.teamBackgroundColor}").my-team-icon.img-circle.only-one + img(src=view.challengers.easy.opponentImageSource||view.genericPortrait).opponent-icon + img(src="/images/pages/play/ladder/#{view.otherTeam}_ladder_easy.png", style="border: 1px solid #{view.opponentTeamColor}; background: #{view.opponentTeamBackgroundColor}").opponent-team-icon.img-circle div.my-name.name-label.only-one - span= myName + span= view.myName div.opponent-name.name-label - span= challengers.easy.opponentName - if challengers.easy.codeLanguage - span.code-language(style="background-image: url(/images/common/code_languages/" + challengers.easy.codeLanguage + "_small.png)") + span= view.challengers.easy.opponentName + if view.challengers.easy.codeLanguage + span.code-language(style="background-image: url(/images/common/code_languages/#{view.challengers.easy.codeLanguage}_small.png)") div.difficulty span(data-i18n="general.easy") Easy - if challengers.medium - a(href="/play/level/#{levelID}?team=#{teamID}&opponent=#{challengers.medium.sessionID}" + (league ? "&league=" + league.id : "")) + if view.challengers && view.challengers.medium + a(href="/play/level/#{view.levelID}?team=#{view.team}&opponent=#{view.challengers.medium.sessionID}" + (view.options.league ? "&league=" + view.options.league.id : "")) div.play-option.medium-option - img(src=myPortrait).my-icon.only-one - img(src="/images/pages/play/ladder/"+teamID+"_ladder_medium.png", style="border: 1px solid #{teamColor}; background: #{teamBackgroundColor}").my-team-icon.img-circle.only-one - img(src=challengers.medium.opponentImageSource||genericPortrait).opponent-icon - img(src="/images/pages/play/ladder/"+otherTeamID+"_ladder_medium.png", style="border: 1px solid #{opponentTeamColor}; background: #{opponentTeamBackgroundColor}").opponent-team-icon.img-circle + img(src=view.myPortrait).my-icon.only-one + img(src="/images/pages/play/ladder/#{view.team}_ladder_medium.png", style="border: 1px solid #{view.teamColor}; background: #{view.teamBackgroundColor}").my-team-icon.img-circle.only-one + img(src=view.challengers.medium.opponentImageSource||view.genericPortrait).opponent-icon + img(src="/images/pages/play/ladder/#{view.otherTeam}_ladder_medium.png", style="border: 1px solid #{view.opponentTeamColor}; background: #{view.opponentTeamBackgroundColor}").opponent-team-icon.img-circle div.my-name.name-label.only-one - span= myName + span= view.myName div.opponent-name.name-label - span= challengers.medium.opponentName - if challengers.medium.codeLanguage - span.code-language(style="background-image: url(/images/common/code_languages/" + challengers.medium.codeLanguage + "_small.png)") + span= view.challengers.medium.opponentName + if view.challengers.medium.codeLanguage + span.code-language(style="background-image: url(/images/common/code_languages/#{view.challengers.medium.codeLanguage}_small.png)") div.difficulty span(data-i18n="general.medium") Medium - if challengers.hard - a(href="/play/level/#{levelID}?team=#{teamID}&opponent=#{challengers.hard.sessionID}" + (league ? "&league=" + league.id : "")) + if view.challengers && view.challengers.hard + a(href="/play/level/#{view.levelID}?team=#{view.team}&opponent=#{view.challengers.hard.sessionID}" + (view.options.league ? "&league=" + view.options.league.id : "")) div.play-option.hard-option - img(src=myPortrait).my-icon.only-one - img(src="/images/pages/play/ladder/"+teamID+"_ladder_hard.png", style="border: 1px solid #{teamColor}; background: #{teamBackgroundColor}").my-team-icon.img-circle.only-one - img(src=challengers.hard.opponentImageSource||genericPortrait).opponent-icon - img(src="/images/pages/play/ladder/"+otherTeamID+"_ladder_hard.png", style="border: 1px solid #{opponentTeamColor}; background: #{opponentTeamBackgroundColor}").opponent-team-icon.img-circle + img(src=view.myPortrait).my-icon.only-one + img(src="/images/pages/play/ladder/#{view.team}_ladder_hard.png", style="border: 1px solid #{view.teamColor}; background: #{view.teamBackgroundColor}").my-team-icon.img-circle.only-one + img(src=view.challengers.hard.opponentImageSource||view.genericPortrait).opponent-icon + img(src="/images/pages/play/ladder/#{view.otherTeam}_ladder_hard.png", style="border: 1px solid #{view.opponentTeamColor}; background: #{view.opponentTeamBackgroundColor}").opponent-team-icon.img-circle div.my-name.name-label.only-one - span= myName + span= view.myName div.opponent-name.name-label - span= challengers.hard.opponentName - if challengers.hard.codeLanguage - span.code-language(style="background-image: url(/images/common/code_languages/" + challengers.hard.codeLanguage + "_small.png)") + span= view.challengers.hard.opponentName + if view.challengers.hard.codeLanguage + span.code-language(style="background-image: url(/images/common/code_languages/#{view.challengers.hard.codeLanguage}_small.png)") div.difficulty span(data-i18n="general.hard") Hard diff --git a/app/views/ladder/LadderPlayModal.coffee b/app/views/ladder/LadderPlayModal.coffee index ee3e79c20..77a87fd2d 100644 --- a/app/views/ladder/LadderPlayModal.coffee +++ b/app/views/ladder/LadderPlayModal.coffee @@ -24,12 +24,27 @@ module.exports = class LadderPlayModal extends ModalView behaviors: false liveCompletion: true - constructor: (options, @level, @session, @team) -> - super(options) - @nameMap = {} + initialize: (options, @level, @session, @team) -> @otherTeam = if @team is 'ogres' then 'humans' else 'ogres' @startLoadingChallengersMaybe() @wizardType = ThangType.loadUniversalWizard() + @levelID = @level.get('slug') or @level.id + @language = @session?.get('codeLanguage') ? me.get('aceConfig')?.language ? 'python' + @languages = [ + {id: 'python', name: 'Python'} + {id: 'javascript', name: 'JavaScript'} + {id: 'coffeescript', name: 'CoffeeScript (Experimental)'} + {id: 'lua', name: 'Lua'} + {id: 'java', name: 'Java'} + ] + @myName = me.get('name') || 'Newcomer' + + teams = [] + teams[t.id] = t for t in teamDataFromLevel @level + @teamColor = teams[@team].primaryColor + @teamBackgroundColor = teams[@team].bgColor + @opponentTeamColor = teams[@otherTeam].primaryColor + @opponentTeamBackgroundColor = teams[@otherTeam].bgColor updateLanguage: -> aceConfig = _.cloneDeep me.get('aceConfig') ? {} @@ -59,6 +74,12 @@ module.exports = class LadderPlayModal extends ModalView @challengers = @getChallengers() ids = (challenger.opponentID for challenger in _.values @challengers) + for challenger in _.values @challengers + continue unless challenger and @wizardType.loaded + if (not challenger.opponentImageSource) and challenger.opponentWizard?.colorConfig + challenger.opponentImageSource = @wizardType.getPortraitSource( + {colorConfig: challenger.opponentWizard.colorConfig}) + success = (@nameMap) => for challenger in _.values(@challengers) challenger.opponentName = @nameMap[challenger.opponentID]?.name or 'Anonymous' @@ -86,46 +107,9 @@ module.exports = class LadderPlayModal extends ModalView @tutorialLevelExists = exists @render() @maybeShowTutorialButtons() - - getRenderData: -> - ctx = super() - ctx.level = @level - ctx.levelID = @level.get('slug') or @level.id - ctx.teamName = _.string.titleize @team - ctx.teamID = @team - ctx.otherTeamID = @otherTeam - ctx.tutorialLevelExists = @tutorialLevelExists - ctx.language = @session?.get('codeLanguage') ? me.get('aceConfig')?.language ? 'python' - ctx.languages = [ - {id: 'python', name: 'Python'} - {id: 'javascript', name: 'JavaScript'} - {id: 'coffeescript', name: 'CoffeeScript (Experimental)'} - {id: 'lua', name: 'Lua'} - {id: 'java', name: 'Java'} - ] - ctx.league = @options.league - teamsList = teamDataFromLevel @level - teams = {} - teams[team.id] = team for team in teamsList - ctx.teamColor = teams[@team].primaryColor - ctx.teamBackgroundColor = teams[@team].bgColor - ctx.opponentTeamColor = teams[@otherTeam].primaryColor - ctx.opponentTeamBackgroundColor = teams[@otherTeam].bgColor - - ctx.challengers = @challengers or {} - for challenger in _.values ctx.challengers - continue unless challenger and @wizardType.loaded - if (not challenger.opponentImageSource) and challenger.opponentWizard?.colorConfig - challenger.opponentImageSource = @wizardType.getPortraitSource( - {colorConfig: challenger.opponentWizard.colorConfig}) - - if @wizardType.loaded - ctx.genericPortrait = @wizardType.getPortraitSource() - myColorConfig = me.get('wizard')?.colorConfig - ctx.myPortrait = if myColorConfig then @wizardType.getPortraitSource({colorConfig: myColorConfig}) else ctx.genericPortrait - - ctx.myName = me.get('name') || 'Newcomer' - ctx + @genericPortrait = @wizardType.getPortraitSource() + myColorConfig = me.get('wizard')?.colorConfig + @myPortrait = if myColorConfig then @wizardType.getPortraitSource({colorConfig: myColorConfig}) else @genericPortrait maybeShowTutorialButtons: -> return if @session or LadderPlayModal.shownTutorialButton or not @tutorialLevelExists From c3f1e6e04cec94f6c2081da3affb9d903c01ac5d Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Tue, 7 Jun 2016 05:36:48 -0700 Subject: [PATCH 126/167] Turn off homepage logged out a/b tests --- app/models/User.coffee | 15 ------ app/templates/new-home-view.jade | 90 ++++---------------------------- app/views/NewHomeView.coffee | 1 - 3 files changed, 11 insertions(+), 95 deletions(-) diff --git a/app/models/User.coffee b/app/models/User.coffee index 495d006fa..678297279 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -165,21 +165,6 @@ module.exports = class User extends CocoModel application.tracker.identify campaignAdsGroup: @campaignAdsGroup unless me.isAdmin() @campaignAdsGroup - getHomepageGroup: -> - # Only testing on en-US so localization issues are not a factor - return 'home-legacy' unless _.string.startsWith(me.get('preferredLanguage', true) or 'en-US', 'en') - return @homepageGroup if @homepageGroup - group = parseInt(util.getQueryVariable('variation')) - group ?= me.get('testGroupNumber') % 5 - @homepageGroup = switch group - when 0 then 'home-legacy' - when 1 then 'home-teachers' - when 2 then 'home-legacy-left' - when 3 then 'home-dropdowns' - when 4 then 'home-play-for-free' - application.tracker.identify homepageGroup: @homepageGroup unless me.isAdmin() - return @homepageGroup - # Signs and Portents was receiving updates after test started, and also had a big bug on March 4, so just look at test from March 5 on. # ... and stopped working well until another update on March 10, so maybe March 11+... # ... and another round, and then basically it just isn't completing well, so we pause the test until we can fix it. diff --git a/app/templates/new-home-view.jade b/app/templates/new-home-view.jade index 866b0af65..59065644a 100644 --- a/app/templates/new-home-view.jade +++ b/app/templates/new-home-view.jade @@ -3,74 +3,14 @@ extends /templates/base-flat mixin box .well.text-center if me.isAnonymous() == true - if ['home-legacy','home-legacy-left'].indexOf(view.variation) != -1 - h6#classroom-edition-header(data-i18n="new_home.classroom_edition") - div - button.teacher-btn.btn.btn-primary.btn-lg.btn-block(data-i18n="new_home.im_a_teacher") - div - a.btn.btn-forest.btn-lg.btn-block(href="/courses", data-i18n="new_home.im_a_student") - h6#learn-to-code-header(data-i18n="new_home.learn_to_code") - a.btn.btn-gold.btn-lg.btn-block.play-btn(href=view.playURL, data-i18n="new_home.play_now") - else if ['home-teachers','home-dropdowns'].indexOf(view.variation) != -1 - h5#educator-header(data-i18n="new_home.teachers") - p.small(data-i18n="new_home.class_in_box") + h6#classroom-edition-header(data-i18n="new_home.classroom_edition") + div + button.teacher-btn.btn.btn-primary.btn-lg.btn-block(data-i18n="new_home.im_a_teacher") + div + a.btn.btn-forest.btn-lg.btn-block(href="/courses", data-i18n="new_home.im_a_student") + h6#learn-to-code-header(data-i18n="new_home.learn_to_code") + a.btn.btn-gold.btn-lg.btn-block.play-btn(href=view.playURL, data-i18n="new_home.play_now") - if view.variation == 'home-dropdowns' - div.form-group - h6.control-label(data-i18n="new_home.role") - select.form-control#request-form-role - option(value="", data-i18n="new_home.form_select_role") - option(data-i18n="courses.teacher", value="Teacher") - option(data-i18n="teachers_quote.tech_coordinator", value="Technology coordinator") - option(data-i18n="teachers_quote.advisor", value="Advisor") - option(data-i18n="teachers_quote.principal", value="Principal") - option(data-i18n="teachers_quote.superintendent", value="Superintendent") - option(data-i18n="teachers_quote.parent", value="Parent") - div.form-group - h6.control-label(data-i18n="new_home.student_count") - select.form-control#request-form-range - option(value="", data-i18n="new_home.form_select_range") - option 1-10 - option 11-50 - option 51-100 - option 101-200 - option 201-500 - option 501-1000 - option 1000+ - div - button.btn.btn-forest.btn-lg.btn-block.request-demo(href="#",data-i18n="new_home.get_started") - else - div - button.teacher-btn.btn.btn-forest.btn-lg.btn-block(data-i18n="new_home.get_started") - - if view.variation == 'home-teachers' - h5#students-header(data-i18n="new_home.students") - div - button.join-class.btn.btn-primary.btn-lg.btn-block(data-i18n="new_home.join_class", href="/courses") - div - a.btn.btn-default.btn-lg.btn-block.play-btn(href=view.playURL, data-i18n="new_home.play_now") - else - h6#students-header(data-i18n="new_home.students_and_players") - div - a.btn.btn-primary.btn-lg.play-btn.btn-block(href=view.playURL, data-i18n="new_home.play_now") - else - h5#educator-header(data-i18n="new_home.start_playing_for_free") - div - a.btn.btn-forest.btn-lg.btn-block.play-btn(href=view.playURL, data-i18n="new_home.play_now") - div - button.join-class.btn.btn-default.btn-lg.btn-block(data-i18n="new_home.join_class", href="/courses") - - - h6#classroom-edition-header(style="padding-top: 20px",data-i18n="new_home.teachers_and_educators") - p.small(data-i18n="new_home.class_in_box") - button.teacher-btn.btn.btn-primary.btn-lg.btn-block(data-i18n="new_home.get_started") - - - if ['home-teachers','home-dropdowns','home-play-for-free'].indexOf(view.variation) != -1 - p.small - span(data-i18n="new_home.have_an_account") - span   - a.login-button(data-i18n="login.log_in", href="#") else h6#classroom-edition-header(data-i18n="new_home.logged_in_as") p.small #{me.get("email")} @@ -113,24 +53,16 @@ block content .container .row.hidden-xs.top-spacer(style="height: 160px") .row.hidden-sm.hidden-xs - case view.variation - when 'home-legacy-left' - .col-lg-3.col-md-4 - +box - .col-lg-7.col-lg-offset-2.col-md-8 - h1(data-i18n="new_home.slogan") - default - .col-lg-7.col-md-8 - h1(data-i18n="new_home.slogan") - .col-lg-3.col-lg-offset-2.col-md-4 - +box + .col-lg-7.col-md-8 + h1(data-i18n="new_home.slogan") + .col-lg-3.col-lg-offset-2.col-md-4 + +box .row.hidden-lg.hidden-md .col-lg-7.col-md-8 h1(data-i18n="new_home.slogan") .col div(style="margin: auto; max-width: 300px") +box - .row#learn-more-row .col-xs-12.text-center a#learn-more-link diff --git a/app/views/NewHomeView.coffee b/app/views/NewHomeView.coffee index 962c7cc05..0651a512b 100644 --- a/app/views/NewHomeView.coffee +++ b/app/views/NewHomeView.coffee @@ -35,7 +35,6 @@ module.exports = class NewHomeView extends RootView initialize: (options) -> @courses = new CocoCollection [], {url: "/db/course", model: Course} @supermodel.loadCollection(@courses, 'courses') - @variation ?= me.getHomepageGroup() window.tracker?.trackEvent 'Homepage Loaded', category: 'Homepage' if me.isTeacher() From aa443d372f1c9d02d0ce7ffc4440502895547c7d Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 7 Jun 2016 11:38:00 -0700 Subject: [PATCH 127/167] Revert using Bluebird instead of Promise polyfill --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 50b1b9e16..0f69fe4a9 100644 --- a/bower.json +++ b/bower.json @@ -53,7 +53,7 @@ "algoliasearch": "^3.13.1", "algolia-autocomplete.js": "^0.17.0", "algolia-autocomplete-no-conflict": "1.0.0", - "bluebird": "^3.4.0" + "promise-polyfill": "^5.2.1" }, "overrides": { "algolia-autocomplete.js": { From 23d45ab9474131b9cc83878e7bbca82980db22f9 Mon Sep 17 00:00:00 2001 From: duybkict Date: Wed, 8 Jun 2016 16:36:01 +0700 Subject: [PATCH 128/167] #3138 #3488 refactor LadderTabView --- .../play/ladder/ladder-tab-view.jade | 32 ++++++++++--------- app/views/ladder/LadderTabView.coffee | 31 +++++++----------- app/views/ladder/LadderView.coffee | 5 +-- 3 files changed, 29 insertions(+), 39 deletions(-) diff --git a/app/templates/play/ladder/ladder-tab-view.jade b/app/templates/play/ladder/ladder-tab-view.jade index 3240c2609..784d296f9 100644 --- a/app/templates/play/ladder/ladder-tab-view.jade +++ b/app/templates/play/ladder/ladder-tab-view.jade @@ -1,16 +1,21 @@ +- var league = view.options.league .row - for team, teamIndex in teams + for team, teamIndex in view.teams div.column.col-md-6 div(id="histogram-display-#{team.name}", class="histogram-display", data-team-name=team.name) table.table.table-bordered.table-condensed.table-hover.ladder-table(data-team=team.id) + - var levelType = view.level.get('type', true) + - var topSessions = team.leaderboard.topPlayers.models; + - var showJustTop = team.leaderboard.inTopSessions() || me.get('anonymous'); + - if(!showJustTop && topSessions.length == 20) topSessions = topSessions.slice(0, 10); thead tr - th(colspan=level.get('type', true) == 'hero-ladder' ? 3 : 2) + th(colspan=levelType == 'hero-ladder' ? 3 : 2) th(colspan=4, style="color: #{team.primaryColor}") span= team.displayName span.spl(data-i18n="ladder.leaderboard") Leaderboard tr - th(colspan=level.get('type', true) == 'hero-ladder' ? 3 : 2) + th(colspan=levelType == 'hero-ladder' ? 3 : 2) th(data-i18n="general.score") Score th(data-i18n="general.name").name-col-cell Name th(data-i18n="general.when") When @@ -18,23 +23,20 @@ th.iconic-cell .glyphicon.glyphicon-eye-open - tbody - - var topSessions = team.leaderboard.topPlayers.models; - - var showJustTop = team.leaderboard.inTopSessions() || me.get('anonymous'); - - if(!showJustTop && topSessions.length == 20) topSessions = topSessions.slice(0, 10); + tbody for session, rank in topSessions - var myRow = session.get('creator') == me.id - var sessionStats = league ? (_.find(session.get('leagues') || [], {leagueID: league.id}) || {}).stats || {} : session.attributes; tr(class=myRow ? "success" : "", data-player-id=session.get('creator'), data-session-id=session.id) - td.code-language-cell(style="background-image: url(/images/common/code_languages/" + session.get('submittedCodeLanguage') + "_icon.png)" title=capitalize(session.get('submittedCodeLanguage'))) - if level.get('type', true) == 'hero-ladder' + td.code-language-cell(style="background-image: url(/images/common/code_languages/" + session.get('submittedCodeLanguage') + "_icon.png)" title=view.capitalize(session.get('submittedCodeLanguage'))) + if levelType == 'hero-ladder' td.hero-portrait-cell(style="background-image: url(/file/db/thang.type/#{(session.get('heroConfig') || {}).thangType || '529ffbf1cf1818f2be000001'}/portrait.png)") td.rank-cell= rank + 1 td.score-cell= Math.round((sessionStats.totalScore || session.get('totalScore') / 2) * 100) td(class='name-col-cell' + ((new RegExp('(Simple|Shaman|Brawler|Chieftain|Thoktar) CPU')).test(session.get('creatorName')) ? ' ai' : ''))= session.get('creatorName') || "Anonymous" td.age-cell= moment(session.get('submitDate')).fromNow().replace('a few ', '') td.fight-cell - a(href="/play/level/#{level.get('slug') || level.id}?team=#{team.otherTeam}&opponent=#{session.id}" + (league ? "&league=" + league.id : "")) + a(href="/play/level/#{view.level.get('slug') || view.level.id}?team=#{team.otherTeam}&opponent=#{session.id}" + (league ? "&league=" + league.id : "")) span(data-i18n="ladder.fight") Fight! td.spectate-cell.iconic-cell .glyphicon.glyphicon-eye-open @@ -47,14 +49,14 @@ - var sessionStats = league ? (_.find(session.get('leagues'), {leagueID: league.id}) || {}).stats || {} : session.attributes; tr(class=myRow ? "success" : "", data-player-id=session.get('creator'), data-session-id=session.id) td.code-language-cell(style="background-image: url(/images/common/code_languages/" + session.get('submittedCodeLanguage') + "_icon.png)") - if level.get('type', true) == 'hero-ladder' + if levelType == 'hero-ladder' td.hero-portrait-cell(style="background-image: url(/file/db/thang.type/#{(session.get('heroConfig') || {}).thangType || '529ffbf1cf1818f2be000001'}/portrait.png)") td.rank-cell= session.rank td.score-cell= Math.round((sessionStats.totalScore || session.get('totalScore') / 2) * 100) td(class='name-col-cell' + ((new RegExp('(Simple|Shaman|Brawler|Chieftain|Thoktar) CPU')).test(session.get('creatorName')) ? ' ai' : ''))= session.get('creatorName') || "Anonymous" td.age-cell= moment(session.get('submitDate')).fromNow().replace('a few ', '') td.fight-cell - a(href="/play/level/#{level.get('slug') || level.id}?team=#{team.otherTeam}&opponent=#{session.id}" + (league ? "&league=" + league.id : "")) + a(href="/play/level/#{view.level.get('slug') || view.level.id}?team=#{team.otherTeam}&opponent=#{session.id}" + (league ? "&league=" + league.id : "")) span(data-i18n="ladder.fight") Fight! td.spectate-cell.iconic-cell .glyphicon.glyphicon-eye-open @@ -77,8 +79,8 @@ if !onGPlus button.btn.btn-sm.connect-google-plus(data-i18n="community.gplus") Google+ - if friends.length - for friend in friends + if view.friends + for friend in view.friends p.friend-entry img(src=friend.imageSource).img-thumbnail span= friend.creatorName + ' (' + friend.name + ')' @@ -87,7 +89,7 @@ span : span= friend.team br - a(href="/play/level/#{level.get('slug') || level.id}/?team=#{friend.otherTeam}&opponent=#{friend._id}" + (league ? "&league=" + league.id : "")) + a(href="/play/level/#{view.level.get('slug') || view.level.id}/?team=#{friend.otherTeam}&opponent=#{friend._id}" + (league ? "&league=" + league.id : "")) span(data-i18n="ladder.fight") Fight! else if onFacebook || onGPlus diff --git a/app/views/ladder/LadderTabView.coffee b/app/views/ladder/LadderTabView.coffee index 82feb23c0..06180c450 100644 --- a/app/views/ladder/LadderTabView.coffee +++ b/app/views/ladder/LadderTabView.coffee @@ -29,11 +29,13 @@ module.exports = class LadderTabView extends CocoView # 'auth:logged-in-with-facebook': 'onConnectedWithFacebook' # 'auth:logged-in-with-gplus': 'onConnectedWithGPlus' - constructor: (options, @level, @sessions) -> - super(options) + initialize: (options, @level, @sessions) -> @teams = teamDataFromLevel @level - @leaderboards = {} + @leaderboards = [] @refreshLadder() + + @capitalize = _.string.capitalize + # Trying not loading the FP/G+ stuff for now to see if anyone complains they were using it so we can have just two columns. #@socialNetworkRes = @supermodel.addSomethingResource('social_network_apis', 0) #@checkFriends() @@ -51,7 +53,8 @@ module.exports = class LadderTabView extends CocoView FB.getLoginStatus (response) => return if @destroyed @facebookStatus = response.status - @loadFacebookFriends() if @facebookStatus is 'connected' + @onFacebook = view.facebookStatus is 'connected' + @loadFacebookFriends() if @onFacebook @fbStatusRes.markLoaded() if application.gplusHandler.loggedIn is undefined @@ -102,6 +105,7 @@ module.exports = class LadderTabView extends CocoView friend.otherTeam = if friend.team is 'humans' then 'ogres' else 'humans' friend.imageSource = "http://graph.facebook.com/#{friend.facebookID}/picture" @facebookFriendSessions = result + @friends = @consolidateFriends() @render() # because the ladder tab renders before waiting for fb to finish # GOOGLE PLUS @@ -115,6 +119,7 @@ module.exports = class LadderTabView extends CocoView gplusSessionStateLoaded: -> if application.gplusHandler.loggedIn + @onGPlus = true #@addSomethingToLoad('gplus_friends') @gpFriendRes = @supermodel.addSomethingResource('gplus_friends', 0) @gpFriendRes.load() @@ -145,6 +150,7 @@ module.exports = class LadderTabView extends CocoView friend.otherTeam = if friend.team is 'humans' then 'ogres' else 'humans' friend.imageSource = friendsMap[friend.gplusID].image.url @gplusFriendSessions = result + @friends = @consolidateFriends() @render() # because the ladder tab renders before waiting for gplus to finish # LADDER LOADING @@ -161,6 +167,7 @@ module.exports = class LadderTabView extends CocoView oldLeaderboard.destroy() teamSession = _.find @sessions.models, (session) -> session.get('team') is team.id @leaderboards[team.id] = new LeaderboardData(@level, team.id, teamSession, @ladderLimit, @options.league) + team.leaderboard = @leaderboards[team.id] @leaderboardRes = @supermodel.addModelResource(@leaderboards[team.id], 'leaderboard', {cache: false}, 3) @leaderboardRes.load() @@ -179,22 +186,6 @@ module.exports = class LadderTabView extends CocoView ).then => @generateHistogram(histogramWrapper, histogramData, team.name.toLowerCase()) unless @destroyed - getRenderData: -> - ctx = super() - ctx.level = @level - ctx.link = "/play/level/#{@level.get('name')}" - ctx.teams = @teams - team.leaderboard = @leaderboards[team.id] for team in @teams - ctx.levelID = @levelID - ctx.friends = @consolidateFriends() - ctx.onFacebook = @facebookStatus is 'connected' - ctx.onGPlus = application.gplusHandler.loggedIn - ctx.capitalize = _.string.capitalize - ctx.league = @options.league - ctx._ = _ - ctx.moment = moment - ctx - generateHistogram: (histogramElement, histogramData, teamName) -> #renders twice, hack fix if $('#' + histogramElement.attr('id')).has('svg').length then return diff --git a/app/views/ladder/LadderView.coffee b/app/views/ladder/LadderView.coffee index 28076bc4a..b2bb6e9c6 100644 --- a/app/views/ladder/LadderView.coffee +++ b/app/views/ladder/LadderView.coffee @@ -43,6 +43,7 @@ module.exports = class LadderView extends RootView @level = @supermodel.loadModel(new Level(_id: @levelID)).model @level.once 'sync', => @levelDescription = marked(@level.get('description')) if @level.get('description') + @teams = teamDataFromLevel @level @sessions = @supermodel.loadCollection(new LevelSessionsCollection(@levelID), 'your_sessions', {cache: false}).model @winners = require('./tournament_results')[@levelID] @@ -70,10 +71,6 @@ module.exports = class LadderView extends RootView @course = @supermodel.loadModel(course).model @listenToOnce @course, 'sync', @render - onLoaded: -> - @teams = teamDataFromLevel @level - super() - afterRender: -> super() return unless @supermodel.finished() From fe8760b122839c300ba8c248f7ea0d2cae53af7b Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Mon, 6 Jun 2016 15:50:31 -0700 Subject: [PATCH 129/167] Add Travis-Slack integration --- .travis.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.travis.yml b/.travis.yml index 3486ab76b..f3c0f484b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,3 +30,14 @@ before_script: script: - "./node_modules/karma/bin/karma start --browsers Firefox --single-run --reporters progress" - "npm run jasmine" + +notifications: + slack: + rooms: + - secure: dv1zozcLo/gGb1i1OeKKLQgjLagW3jqhkEIiZrVbubfRMrrHouZwtlg1HDVc2GOzP1yCaUOYOLBqT3lTE9CKt4pE8k3p0gJ77j1rHxTyeYny2ETq4w5sXak8ju8M3EDnj+Au2NWWrGiD0YkIUnF5vxzKUc1pkS+vDSWR2ORkOrU= + template: + - Tests failed <%{build_url}|#%{build_number}> (<%{compare_url}|%{commit}>) of %{repository}@%{branch} by %{author} %{result} + on_success: never + on_failure: always + on_start: never + on_pull_requests: false From 69f3ee3a5b794b5bdb0fa6947e7fbf854bed1b87 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Wed, 8 Jun 2016 13:45:25 -0700 Subject: [PATCH 130/167] Allow username login, tear out a bunch of related stuff in change * Switch from auth.loginUser to User.loginPasswordUser with Promise * Remove a cascade of unused views that were using auth.loginUser: StudentLogInModal, StudentSignupModal, HourOfCodeView * Also remove auth.createUser --- app/core/auth.coffee | 34 ------ app/locale/en.coffee | 1 + app/models/User.coffee | 7 ++ app/styles/courses/hour-of-code-view.sass | 19 --- app/styles/courses/student-log-in-modal.sass | 7 -- app/styles/courses/student-sign-up-modal.sass | 10 -- app/styles/modal/auth-modal.sass | 2 +- .../core/{auth.jade => auth-modal.jade} | 32 ++++-- app/templates/courses/hour-of-code-view.jade | 74 ------------ .../courses/student-log-in-modal.jade | 30 ----- .../courses/student-sign-up-modal.jade | 46 -------- app/views/core/AuthModal.coffee | 56 +++++---- app/views/core/CreateAccountModal.coffee | 1 - app/views/courses/HourOfCodeView.coffee | 108 ------------------ app/views/courses/StudentLogInModal.coffee | 43 ------- app/views/courses/StudentSignUpModal.coffee | 101 ---------------- server/commons/auth.coffee | 15 +-- server/commons/errors.coffee | 4 + spec/server/functional/auth.spec.coffee | 25 ++-- .../views/courses/HeroSelectModal.spec.coffee | 2 - .../courses/StudentLogInModal.spec.coffee | 22 ---- 21 files changed, 90 insertions(+), 549 deletions(-) delete mode 100644 app/styles/courses/hour-of-code-view.sass delete mode 100644 app/styles/courses/student-log-in-modal.sass delete mode 100644 app/styles/courses/student-sign-up-modal.sass rename app/templates/core/{auth.jade => auth-modal.jade} (64%) delete mode 100644 app/templates/courses/hour-of-code-view.jade delete mode 100644 app/templates/courses/student-log-in-modal.jade delete mode 100644 app/templates/courses/student-sign-up-modal.jade delete mode 100644 app/views/courses/HourOfCodeView.coffee delete mode 100644 app/views/courses/StudentLogInModal.coffee delete mode 100644 app/views/courses/StudentSignUpModal.coffee delete mode 100644 test/app/views/courses/StudentLogInModal.spec.coffee diff --git a/app/core/auth.coffee b/app/core/auth.coffee index 79c9f8ade..0056b91ad 100644 --- a/app/core/auth.coffee +++ b/app/core/auth.coffee @@ -14,40 +14,6 @@ init = -> Backbone.listenTo me, 'sync', -> Backbone.Mediator.publish('auth:me-synced', me: me) -module.exports.createUser = (userObject, failure=backboneFailure, nextURL=null) -> - user = new User(userObject) - user.notyErrors = false - user.save({}, { - error: (model, jqxhr, options) -> - error = parseServerError(jqxhr.responseText) - property = error.property if error.property - if jqxhr.status is 409 and property is 'name' - anonUserObject = _.omit(userObject, 'name') - module.exports.createUser anonUserObject, failure, nextURL - else - genericFailure(jqxhr) - success: -> if nextURL then window.location.href = nextURL else window.location.reload() - }) - -module.exports.createUserWithoutReload = (userObject, failure=backboneFailure) -> - user = new User(userObject) - user.save({}, { - error: failure - success: -> - Backbone.Mediator.publish('created-user-without-reload') - }) - -module.exports.loginUser = (userObject, failure=genericFailure, nextURL=null) -> - console.log 'logging in as', userObject.email - jqxhr = $.post('/auth/login', - { - username: userObject.email, - password: userObject.password - }, - (model) -> if nextURL then window.location.href = nextURL else window.location.reload() - ) - jqxhr.fail(failure) - module.exports.logoutUser = -> # TODO: Refactor to use User.logout FB?.logout?() diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 3e409016e..60842638d 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -243,6 +243,7 @@ login: sign_up: "Create Account" + email_or_username: "Email or username" log_in: "Log In" logging_in: "Logging In" log_out: "Log Out" diff --git a/app/models/User.coffee b/app/models/User.coffee index 678297279..6f7e98c2a 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -275,6 +275,13 @@ module.exports = class User extends CocoModel options.data.facebookAccessToken = application.facebookHandler.token() @fetch(options) + loginPasswordUser: (usernameOrEmail, password, options={}) -> + options.url = '/auth/login' + options.type = 'POST' + options.data ?= {} + _.extend(options.data, { username: usernameOrEmail, password }) + @fetch(options) + makeCoursePrepaid: -> coursePrepaid = @get('coursePrepaid') return null unless coursePrepaid diff --git a/app/styles/courses/hour-of-code-view.sass b/app/styles/courses/hour-of-code-view.sass deleted file mode 100644 index 1f66367e6..000000000 --- a/app/styles/courses/hour-of-code-view.sass +++ /dev/null @@ -1,19 +0,0 @@ -#hour-of-code-view - - hr - border-top: 1px solid grey - margin: 30px 20px - - #site-content-area - padding: 20px 300px - - h1 - margin-bottom: 40px - p - margin: 20px - - h3 - margin-top: 50px - - ul - margin-bottom: 50px \ No newline at end of file diff --git a/app/styles/courses/student-log-in-modal.sass b/app/styles/courses/student-log-in-modal.sass deleted file mode 100644 index 0fd2e0cf0..000000000 --- a/app/styles/courses/student-log-in-modal.sass +++ /dev/null @@ -1,7 +0,0 @@ -#student-log-in-modal - #log-in-btn - min-width: 30% - margin-bottom: 10px - - .form - margin: 0 25% \ No newline at end of file diff --git a/app/styles/courses/student-sign-up-modal.sass b/app/styles/courses/student-sign-up-modal.sass deleted file mode 100644 index 6d06268f8..000000000 --- a/app/styles/courses/student-sign-up-modal.sass +++ /dev/null @@ -1,10 +0,0 @@ -#student-sign-up-modal - #sign-up-btn - min-width: 30% - margin-bottom: 10px - - .form - margin: 0 25% - - .modal-dialog - margin-top: 0 diff --git a/app/styles/modal/auth-modal.sass b/app/styles/modal/auth-modal.sass index a9ec10461..98805ab74 100644 --- a/app/styles/modal/auth-modal.sass +++ b/app/styles/modal/auth-modal.sass @@ -143,7 +143,7 @@ //- Primary auth button - #login-button + #login-btn position: absolute top: 186px height: 70px diff --git a/app/templates/core/auth.jade b/app/templates/core/auth-modal.jade similarity index 64% rename from app/templates/core/auth.jade rename to app/templates/core/auth-modal.jade index 1b65b9e09..8bcc27620 100644 --- a/app/templates/core/auth.jade +++ b/app/templates/core/auth-modal.jade @@ -3,7 +3,7 @@ img(src="/images/pages/modal/auth/login-background.png", draggable="false").auth-modal-background h1(data-i18n="login.log_in") - div#close-modal + #close-modal span.glyphicon.glyphicon-remove .auth-form-content @@ -11,24 +11,40 @@ if showRequiredError .alert.alert-success span(data-i18n="signup.required") + + #unknown-error-alert.alert.alert-danger.hide(data-i18n="loading_error.unknown") form.form .form-group - label.control-label(for="email") - span(data-i18n="general.email") + label.control-label(for="username-or-email-input") + span(data-i18n="login.email_or_username") | : .input-border - input#email.input-large.form-control(name="email", type="email", value=formValues.email) + input#username-or-email-input.input-large.form-control( + name="emailOrUsername" + value=view.previousFormInputs.email + ) .form-group - div#recover-account-wrapper - a(data-toggle="coco-modal", data-target="core/RecoverModal", data-i18n="login.forgot_password")#link-to-recover + #recover-account-wrapper + a#link-to-recover( + data-toggle="coco-modal" + data-target="core/RecoverModal" + data-i18n="login.forgot_password" + ) label.control-label(for="password") span(data-i18n="general.password") | : .input-border - input#password.input-large.form-control(name="password", type="password", value=formValues.password) + input#password-input.input-large.form-control( + name="password" + type="password" + value=view.previousFormInputs.password + ) - input.btn.btn-lg.btn-illustrated.btn-block.btn-success#login-button(value=translate("login.log_in"), type="submit") + input#login-btn.btn.btn-lg.btn-illustrated.btn-block.btn-success( + value=translate("login.log_in") + type="submit" + ) .wait.secret h3(data-i18n="login.logging_in") diff --git a/app/templates/courses/hour-of-code-view.jade b/app/templates/courses/hour-of-code-view.jade deleted file mode 100644 index c37ecdce1..000000000 --- a/app/templates/courses/hour-of-code-view.jade +++ /dev/null @@ -1,74 +0,0 @@ -extends /templates/base - -block content - .pull-right - if me.isAnonymous() - a(href="/teachers") - span(data-i18n="courses.teachers_click") - span ! - else - a(href="/teachers/classes") - span(data-i18n="courses.teachers_click") - span ! - br - - h1.text-center(data-i18n="courses.welcome_to_hoc") - - #main-content - .well.text-center - if !me.isAnonymous() - p - strong.spr(data-i18n="courses.logged_in_as") - strong= me.get('name') || me.get('email') - - p - span.spr(data-i18n="courses.not_you") - a#log-out-link(data-i18n="login.logout") - - hr - - if !view.lastLevel - p - strong(data-i18n="courses.ready_to_play") - - p - button#start-new-game-btn.btn.btn-success.btn-lg(data-i18n="courses.start_new_game") - else - - p - strong(data-i18n="courses.welcome_back") - p - button#continue-playing-btn.btn.btn-success.btn-lg(data-i18n="courses.continue_playing") - p - em.spr - span(data-i18n="clans.last_played") - span.spr : - span= view.lastLevel.get('name').replace('Course :', '') - - if me.isAnonymous() - p - strong(data-i18n="courses.more_options") - p - button#start-new-game-btn.btn.btn-default.btn-lg(data-i18n="courses.start_new_game") - - if me.isAnonymous() - p - span.spr - - span.text-uppercase(data-i18n="general.or") - span.spl - - - p - button#log-in-btn.btn.btn-default.btn-lg(data-i18n="login.log_in") - - #begin-hoc-area.hide - h2.text-center(data-i18n="common.loading") - .progress.progress-striped.active - .progress-bar(style="width: 100%") - - - h3.text-center.text-uppercase(data-i18n="courses.play_now_learn_header") - ul - li(data-i18n="courses.play_now_learn_1") - li(data-i18n="courses.play_now_learn_2") - li(data-i18n="courses.play_now_learn_3") - li(data-i18n="courses.play_now_learn_4") diff --git a/app/templates/courses/student-log-in-modal.jade b/app/templates/courses/student-log-in-modal.jade deleted file mode 100644 index 60ce36877..000000000 --- a/app/templates/courses/student-log-in-modal.jade +++ /dev/null @@ -1,30 +0,0 @@ -extends /templates/core/modal-base - -block modal-header-content - .clearfix - - -block modal-body-content - .text-center - h2.modal-title(data-i18n="login.log_in") - - form.form - .form-group - label.control-label(for="email") - span(data-i18n="general.email") - input#email.input-large.form-control(name="email", type="email") - .form-group - label.control-label(for="password") - span(data-i18n="general.password") - input#password.input-large.form-control(name="password", type="password") - - #errors-alert.alert.alert-danger.hide - - .text-center - input#log-in-btn.btn.btn-default(data-i18n="[value]login.log_in", type="submit") - p - a#create-new-account-link(data-i18n="login.signup_switch") - p - a(data-toggle="coco-modal", data-target="core/RecoverModal", data-i18n="login.forgot_password") - -block modal-footer-content diff --git a/app/templates/courses/student-sign-up-modal.jade b/app/templates/courses/student-sign-up-modal.jade deleted file mode 100644 index 8454e228d..000000000 --- a/app/templates/courses/student-sign-up-modal.jade +++ /dev/null @@ -1,46 +0,0 @@ -extends /templates/core/modal-base - -block modal-header-content - .clearfix - - -block modal-body-content - .text-center - h2.modal-title(data-i18n="signup.sign_up") - - form.form - .form-group - label.control-label(for="email") - span(data-i18n="general.email") - input#email.input-large.form-control(name="email", type="email") - .help-block(data-i18n="courses.use_school_email") - .form-group - label.control-label(for="name") - span(data-i18n="general.name") - if me.get('name') - input#name.input-large.form-control(name="name", type="text", value="#{me.get('name')}") - else - input#name.input-large.form-control(name="name", type="text", value="", placeholder="e.g. Alex W the Skater") - .help-block(data-i18n="courses.unique_name") - .form-group - label.control-label(for="password") - span(data-i18n="general.password") - input#password.input-large.form-control(name="password", type="password") - .help-block(data-i18n="courses.pick_something") - .form-group - label.control-label(for="class-code-input") - span(data-i18n="courses.class_code") - input#class-code-input.input-large.form-control(name="classCode", value=view.classCode) - .help-block(data-i18n="courses.optional_ask") - .form-group - label.control-label(for="school-input") - span(data-i18n="signup.school_name") - input#school-input.input-large.form-control(name="schoolName", data-i18n="[placeholder]signup.school_name_placeholder") - .help-block(data-i18n="courses.optional_school") - - #errors-alert.alert.alert-danger.hide - - .text-center - input#sign-up-btn.btn.btn-default(data-i18n="[value]signup.sign_up", type="submit") - -block modal-footer-content diff --git a/app/views/core/AuthModal.coffee b/app/views/core/AuthModal.coffee index 0791d396c..13609655a 100644 --- a/app/views/core/AuthModal.coffee +++ b/app/views/core/AuthModal.coffee @@ -1,6 +1,5 @@ ModalView = require 'views/core/ModalView' -template = require 'templates/core/auth' -{loginUser, createUser, me} = require 'core/auth' +template = require 'templates/core/auth-modal' forms = require 'core/forms' User = require 'models/User' application = require 'core/application' @@ -12,15 +11,11 @@ module.exports = class AuthModal extends ModalView events: 'click #switch-to-signup-btn': 'onSignupInstead' - 'click #github-login-button': 'onGitHubLoginClicked' 'submit form': 'onSubmitForm' 'keyup #name': 'onNameChange' 'click #gplus-login-btn': 'onClickGPlusLoginButton' 'click #facebook-login-btn': 'onClickFacebookLoginButton' 'click #close-modal': 'hide' - - subscriptions: - 'errors:server-error': 'onServerError' # Initialization @@ -32,15 +27,6 @@ module.exports = class AuthModal extends ModalView application.gplusHandler.loadAPI({ success: => _.defer => @$('#gplus-login-btn').attr('disabled', false) }) application.facebookHandler.loadAPI({ success: => _.defer => @$('#facebook-login-btn').attr('disabled', false) }) - getRenderData: -> - c = super() - c.showRequiredError = @options.showRequiredError - c.showSignupRationale = @options.showSignupRationale - c.mode = @mode - c.formValues = @previousFormInputs or {} - c.me = me - c - afterRender: -> super() @playSound 'game-menu-open' @@ -58,16 +44,30 @@ module.exports = class AuthModal extends ModalView @playSound 'menu-button-click' e.preventDefault() forms.clearFormAlerts(@$el) + @$('#unknown-error-alert').addClass('hide') userObject = forms.formToObject @$el res = tv4.validateMultiple userObject, formSchema return forms.applyErrorsToForm(@$el, res.errors) unless res.valid - @enableModalInProgress(@$el) # TODO: part of forms - loginUser userObject, null, window.nextURL - - onServerError: (e) -> # TODO: work error handling into a separate forms system - @disableModalInProgress(@$el) - - + new Promise(me.loginPasswordUser(userObject.emailOrUsername, userObject.password).then) + .then(-> + if window.nextURL then window.location.href = window.nextURL else window.location.reload() + ) + .catch((jqxhr) => + showingError = false + if jqxhr.status is 401 + errorID = jqxhr.responseJSON.errorID + if errorID is 'not-found' + forms.setErrorToProperty(@$el, 'emailOrUsername', $.i18n.t('loading_error.not_found')) + showingError = true + if errorID is 'wrong-password' + forms.setErrorToProperty(@$el, 'password', $.i18n.t('account_settings.wrong_password')) + showingError = true + + if not showingError + @$('#unknown-error-alert').removeClass('hide') + ) + + # Google Plus onClickGPlusLoginButton: -> @@ -136,6 +136,14 @@ module.exports = class AuthModal extends ModalView formSchema = { type: 'object' - properties: _.pick(User.schema.properties, 'email', 'password') - required: ['email', 'password'] + properties: { + emailOrUsername: { + $or: [ + User.schema.properties.name + User.schema.properties.email + ] + } + password: User.schema.properties.password + } + required: ['emailOrUsername', 'password'] } diff --git a/app/views/core/CreateAccountModal.coffee b/app/views/core/CreateAccountModal.coffee index c7150695c..440fe1a1b 100644 --- a/app/views/core/CreateAccountModal.coffee +++ b/app/views/core/CreateAccountModal.coffee @@ -1,6 +1,5 @@ ModalView = require 'views/core/ModalView' template = require 'templates/core/create-account-modal' -{loginUser, createUser, me} = require 'core/auth' forms = require 'core/forms' User = require 'models/User' application = require 'core/application' diff --git a/app/views/courses/HourOfCodeView.coffee b/app/views/courses/HourOfCodeView.coffee deleted file mode 100644 index 3f1ce8464..000000000 --- a/app/views/courses/HourOfCodeView.coffee +++ /dev/null @@ -1,108 +0,0 @@ -app = require 'core/application' -CocoCollection = require 'collections/CocoCollection' -Course = require 'models/Course' -CourseInstance = require 'models/CourseInstance' -RootView = require 'views/core/RootView' -template = require 'templates/courses/hour-of-code-view' -utils = require 'core/utils' -LevelSession = require 'models/LevelSession' -Level = require 'models/Level' -ChooseLanguageModal = require 'views/courses/ChooseLanguageModal' -StudentLogInModal = require 'views/courses/StudentLogInModal' -StudentSignUpModal = require 'views/courses/StudentSignUpModal' -auth = require 'core/auth' - -module.exports = class HourOfCodeView extends RootView - id: 'hour-of-code-view' - template: template - - events: - 'click #continue-playing-btn': 'onClickContinuePlayingButton' - 'click #start-new-game-btn': 'onClickStartNewGameButton' - 'click #log-in-btn': 'onClickLogInButton' - 'click #log-out-link': 'onClickLogOutLink' - - initialize: -> - @setUpHourOfCode() - @courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance}) - @listenToOnce @courseInstances, 'sync', @onCourseInstancesLoaded - @courseInstances.comparator = (ci) -> return ci.get('classroomID') + ci.get('courseID') - @supermodel.loadCollection(@courseInstances, 'course_instances', { cache: false }) - - onCourseInstancesLoaded: -> - @hourOfCodeCourseInstance = @courseInstances.findWhere({hourOfCode: true}) - if @hourOfCodeCourseInstance - @sessions = new CocoCollection([], { - url: "/db/course_instance/#{@hourOfCodeCourseInstance.id}/level_sessions" - model: LevelSession - }) - @sessions.comparator = 'created' - @listenTo @sessions, 'sync', @onSessionsLoaded - @supermodel.loadCollection(@sessions, 'sessions', { cache: false }) - - onSessionsLoaded: -> - @lastSession = @sessions.last() - if @lastSession - @lastLevel = new Level() - levelData = @lastSession.get('level') - @supermodel.loadModel(@lastLevel, { - url: "/db/level/#{levelData.original}/version/#{levelData.majorVersion}" - data: { - project: 'name,slug' - } - }) - - setUpHourOfCode: -> - # If we haven't tracked this player as an hourOfCode player yet, and it's a new account, we do that now. - elapsed = new Date() - new Date(me.get('dateCreated')) - if not me.get('hourOfCode') and (elapsed < 5 * 60 * 1000 or me.get('anonymous')) - me.set('hourOfCode', true) - me.patch() - $('body').append($('')) - window.tracker?.trackEvent 'Hour of Code Begin' - - onClickContinuePlayingButton: -> - url = @continuePlayingLink() - window.tracker?.trackEvent 'HoC continue playing ', category: 'HoC', label: url - app.router.navigate(url, { trigger: true }) - - afterRender: -> - super() - @onClickStartNewGameButton() if @getQueryVariable('go') and not @lastLevel - - onClickStartNewGameButton: -> - # User without hour of code course instance, creates one, starts playing - modal = new ChooseLanguageModal({ - logoutFirst: @hourOfCodeCourseInstance? - }) - @openModalView(modal) - @listenToOnce modal, 'set-language', @startHourOfCodePlay - window.tracker?.trackEvent 'Start New Game', category: 'HoC', label: 'HoC Start New Game' - - continuePlayingLink: -> - ci = @hourOfCodeCourseInstance - "/play/level/#{@lastLevel.get('slug')}?course=#{ci.get('courseID')}&course-instance=#{ci.id}" - - startHourOfCodePlay: -> - @$('#main-content').hide() - @$('#begin-hoc-area').removeClass('hide') - hocCourseInstance = new CourseInstance() - hocCourseInstance.upsertForHOC({cache: false}) - @listenToOnce hocCourseInstance, 'sync', -> - url = hocCourseInstance.firstLevelURL() - document.location.href = url - - onClickLogInButton: -> - modal = new StudentLogInModal() - @openModalView(modal) - modal.on 'want-to-create-account', @onWantToCreateAccount, @ - window.tracker?.trackEvent 'Started Login', category: 'HoC', label: 'HoC Login' - - onWantToCreateAccount: -> - modal = new StudentSignUpModal() - @openModalView(modal) - window.tracker?.trackEvent 'Started Signup', category: 'HoC', label: 'HoC Sign Up' - - onClickLogOutLink: -> - window.tracker?.trackEvent 'Log Out', category: 'HoC', label: 'HoC Log Out' - auth.logoutUser() diff --git a/app/views/courses/StudentLogInModal.coffee b/app/views/courses/StudentLogInModal.coffee deleted file mode 100644 index 38ceda440..000000000 --- a/app/views/courses/StudentLogInModal.coffee +++ /dev/null @@ -1,43 +0,0 @@ -ModalView = require 'views/core/ModalView' -template = require 'templates/courses/student-log-in-modal' -auth = require 'core/auth' -forms = require 'core/forms' -User = require 'models/User' - -module.exports = class StudentLogInModal extends ModalView - id: 'student-log-in-modal' - template: template - - events: - 'click #log-in-btn': 'onClickLogInButton' - 'submit form': 'onSubmitForm' - 'click #create-new-account-link': 'onClickCreateNewAccountLink' - - onSubmitForm: (e) -> - e.preventDefault() - @login() - - onClickLogInButton: -> - @login() - - login: -> - # TODO: doesn't track failed login - window.tracker?.trackEvent 'Finished Login', category: 'Courses', label: 'Courses Student Login' - data = forms.formToObject @$el - @enableModalInProgress(@$el) - auth.loginUser data, (jqxhr) => - message = jqxhr.responseText - if jqxhr.status is 401 - message = 'Wrong username or password. Try again!' - # TODO: Make the server return better error message - message = _.string.capitalize(message) - @disableModalInProgress(@$el) - @$('#errors-alert').text(message).removeClass('hide') - - onClickCreateNewAccountLink: -> - @trigger 'want-to-create-account' - @hide?() - - afterInsert: -> - super() - _.delay (=> @$('input:visible:first').focus()), 500 diff --git a/app/views/courses/StudentSignUpModal.coffee b/app/views/courses/StudentSignUpModal.coffee deleted file mode 100644 index 76d2307ad..000000000 --- a/app/views/courses/StudentSignUpModal.coffee +++ /dev/null @@ -1,101 +0,0 @@ -ModalView = require 'views/core/ModalView' -template = require 'templates/courses/student-sign-up-modal' -auth = require 'core/auth' -forms = require 'core/forms' -User = require 'models/User' -Classroom = require 'models/Classroom' -utils = require 'core/utils' - -module.exports = class StudentSignUpModal extends ModalView - id: 'student-sign-up-modal' - template: template - - events: - 'submit form': 'onSubmitForm' - 'click #skip-link': 'onClickSkipLink' - - initialize: (options) -> - options ?= {} - @willPlay = options.willPlay - @classCode = utils.getQueryVariable('_cc') or '' - - afterInsert: -> - super() - _.delay (=> @$('input:visible:first').focus()), 500 - - onClickSkipLink: -> - @trigger 'click-skip-link' # defer to view that opened this modal - @hide?() - - onSubmitForm: (e) -> - e.preventDefault() - @signupClassroomPrecheck() - - emailCheck: -> - email = @$('#email').val() - filter = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,63}$/i # https://news.ycombinator.com/item?id=5763990 - unless filter.test(email) - @$('#errors-alert').text($.i18n.t('share_progress_modal.email_invalid')).removeClass('hide') - return false - return true - - signupClassroomPrecheck: -> - if not _.all([@$('#email').val(), @$('#password').val(), @$('#name').val()]) - @$('#errors-alert').text('Enter email, username and password').removeClass('hide') - return - classCode = @$('#class-code-input').val() - if not classCode - return @signup() - classroom = new Classroom() - classroom.fetch({ url: '/db/classroom?code='+classCode }) - classroom.once 'sync', @signup, @ - classroom.once 'error', @onClassroomFetchError, @ - @enableModalInProgress(@$el) - - onClassroomFetchError: -> - @disableModalInProgress(@$el) - @$('#errors-alert').text('Classroom code could not be found').removeClass('hide') - - signup: -> - return unless @emailCheck() - # TODO: consolidate with AuthModal logic, or make user creation process less magical, more RESTful - data = forms.formToObject @$el - delete data.classCode - for key, val of me.attributes when key in ['preferredLanguage', 'testGroupNumber', 'dateCreated', 'wizardColor1', 'name', 'music', 'volume', 'emails', 'schoolName'] - data[key] ?= val - Backbone.Mediator.publish "auth:signed-up", {} - data.emails ?= {} - data.emails.generalNews ?= {} - data.emails.generalNews.enabled = false - # TODO: Doesn't handle failed user creation. Double posts when placed in onCreateUserSuccess. - window.tracker?.trackEvent 'Finished Student Signup', category: 'Courses', label: 'Courses Student Signup' - @enableModalInProgress(@$el) - user = new User(data) - user.notyErrors = false - user.save({}, { - validate: false # make server deal with everything - error: @onCreateUserError - success: @onCreateUserSuccess - }) - - onCreateUserError: (model, jqxhr) => - # really need to make our server errors uniform - if jqxhr.responseJSON - error = jqxhr.responseJSON - error = error[0] if _.isArray(error) - message = _.filter([error.property, error.message]).join(' ') - else - message = jqxhr.responseText - @disableModalInProgress(@$el) - @$('#errors-alert').text(message).removeClass('hide') - - onCreateUserSuccess: => - classCode = @$('#class-code-input').val() - if classCode - url = "/courses?_cc="+classCode - document.location.href = url - # This was a terrible hack to make navigating trigger when just adding query params - # application.router.navigate('/thisisahack') - # application.router.navigate(url, { trigger: true }) - else - window.location.reload() diff --git a/server/commons/auth.coffee b/server/commons/auth.coffee index 323b71d30..18e958c87 100644 --- a/server/commons/auth.coffee +++ b/server/commons/auth.coffee @@ -17,16 +17,17 @@ module.exports.setup = -> 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() } + # TODO: Add special iPad login endpoint. There was some logic here for the old, hacky method, + # but was removed for username login + q = { $or: [ + { emailLower: username.toLowerCase() } + { slug: _.str.slugify(username) } + ]} User.findOne(q).exec((err, user) -> return done(err) if err if not user - return done(new errors.Unauthorized('not found', { property: 'email' })) + return done(new errors.Unauthorized('not found', { errorID: 'not-found' })) passwordReset = (user.get('passwordReset') or '').toLowerCase() if passwordReset and password.toLowerCase() is passwordReset User.update {_id: user.get('_id')}, {$unset: {passwordReset: ''}}, {}, -> @@ -34,7 +35,7 @@ module.exports.setup = -> hash = User.hashPassword(password) unless user.get('passwordHash') is hash - return done(new errors.Unauthorized('is wrong', { property: 'password' })) + return done(new errors.Unauthorized('is wrong', { errorID: 'wrong-password' })) return done(null, user) ) )) diff --git a/server/commons/errors.coffee b/server/commons/errors.coffee index a82eb7b0f..24276645b 100644 --- a/server/commons/errors.coffee +++ b/server/commons/errors.coffee @@ -92,6 +92,10 @@ errorResponseSchema = { type: 'string' description: 'Provided for /auth/name.' # TODO: refactor out } + errorID: { + type: 'string' + description: 'Error id to be used by the client to handle specific errors' + } } } errorProps = _.keys(errorResponseSchema.properties) diff --git a/spec/server/functional/auth.spec.coffee b/spec/server/functional/auth.spec.coffee index ffb173131..886471d37 100644 --- a/spec/server/functional/auth.spec.coffee +++ b/spec/server/functional/auth.spec.coffee @@ -24,18 +24,6 @@ describe 'POST /auth/login', -> yield utils.becomeAnonymous() done() - it 'allows logging in by iosIdentifierForVendor', utils.wrap (done) -> - yield utils.initUser({ - 'iosIdentifierForVendor': '012345678901234567890123456789012345' - 'password': '12345' - }) - [res, body] = yield request.postAsync({uri: urlLogin, json: { - username: '012345678901234567890123456789012345' - password: '12345' - }}) - expect(res.statusCode).toBe(200) - done() - it 'returns 401 when the user does not exist', utils.wrap (done) -> [res, body] = yield request.postAsync({uri: urlLogin, json: { username: 'some@email.com' @@ -55,6 +43,19 @@ describe 'POST /auth/login', -> }}) expect(res.statusCode).toBe(200) done() + + it 'allows login by username', utils.wrap (done) -> + yield utils.initUser({ + name: 'Some name that will be lowercased...' + 'email': 'some@email.com' + 'password': '12345' + }) + [res, body] = yield request.postAsync({uri: urlLogin, json: { + username: 'Some name that will be lowercased...' + password: '12345' + }}) + expect(res.statusCode).toBe(200) + done() it 'rejects wrong passwords', utils.wrap (done) -> yield utils.initUser({ diff --git a/test/app/views/courses/HeroSelectModal.spec.coffee b/test/app/views/courses/HeroSelectModal.spec.coffee index b38861f4e..7a933bf51 100644 --- a/test/app/views/courses/HeroSelectModal.spec.coffee +++ b/test/app/views/courses/HeroSelectModal.spec.coffee @@ -1,5 +1,4 @@ HeroSelectModal = require 'views/courses/HeroSelectModal' -auth = require 'core/auth' factories = require 'test/app/factories' describe 'HeroSelectModal', -> @@ -14,7 +13,6 @@ describe 'HeroSelectModal', -> beforeEach (done) -> window.me = user = factories.makeUser({ heroConfig: { thangType: hero1.get('original') } }) - auth.loginUser(user.attributes) modal = new HeroSelectModal({ currentHeroID: hero1.id }) modal.heroes.fakeRequests[0].respondWith({ status: 200, responseText: heroesResponse }) jasmine.demoModal(modal) diff --git a/test/app/views/courses/StudentLogInModal.spec.coffee b/test/app/views/courses/StudentLogInModal.spec.coffee deleted file mode 100644 index 0161264fe..000000000 --- a/test/app/views/courses/StudentLogInModal.spec.coffee +++ /dev/null @@ -1,22 +0,0 @@ -StudentLoginModal = require 'views/courses/StudentLogInModal' -RecoverModal = require 'views/core/RecoverModal' -auth = require 'core/auth' - -describe 'StudentLoginModal', -> - - modal = null - - beforeEach -> - modal = new StudentLoginModal() - modal.render() - - afterEach -> - modal.stopListening() - - it 'displays an error when you submit an empty login form', -> - spyOn(auth, 'loginUser').and.callFake (data, callback) -> - callback { status: 401, responseText: "Unauthorized" } - modal.$el.find('#log-in-btn').click() - expect(modal.$el.html()).toContain('Wrong username or password. Try again!') - - jasmine.demoModal(modal) From 8f20e9f7d1c54c94a1b8e24b8257a23095c402c3 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Wed, 8 Jun 2016 14:14:39 -0700 Subject: [PATCH 131/167] Fix missing names for Ida/Nando --- app/models/ThangType.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/ThangType.coffee b/app/models/ThangType.coffee index 5e1dd3a6f..bf7f04a9b 100644 --- a/app/models/ThangType.coffee +++ b/app/models/ThangType.coffee @@ -255,6 +255,8 @@ module.exports = class ThangType extends CocoModel "Samurai": "Hattori" "Sorcerer": "Pender" "Trapper": "Senick" + "Champion": "Ida" + "Duelist": "Nando" } map[@get('name')] From 6019beac25e5929f5ca7e58e0fd66e8e29a476cd Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Wed, 8 Jun 2016 14:53:27 -0700 Subject: [PATCH 132/167] :bug:Fix license request email parsing Caused by https://github.com/codecombat/codecombat/commit/a6bb706cf295e2680fe77ff1 3e91d3da59487b53 --- server/lib/closeIO.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/closeIO.coffee b/server/lib/closeIO.coffee index ed9fd9aa2..c72c4b8c4 100644 --- a/server/lib/closeIO.coffee +++ b/server/lib/closeIO.coffee @@ -65,7 +65,7 @@ module.exports = activities = JSON.parse(body) return done("Unexpected activities format: " + body) unless activities.data? for activity in activities.data when activity._type is 'Email' - if /@codecombat\.(?:com)|(?:nl)$/ig.test(activity.sender) and not activity.sender?.indexOf(config.mail.username) >= 0 + if /@codecombat\.(?:com)|(?:nl)/ig.test(activity.sender) and not activity.sender?.indexOf(config.mail.username) >= 0 return done(null, activity.sender, lead.id) return done(null, config.mail.supportSchools, lead.id) catch error From 546598396dcbb820d9006309ecced761bce14833 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Wed, 8 Jun 2016 15:46:21 -0700 Subject: [PATCH 133/167] Add Lisa to /about page --- app/assets/images/pages/about/lisa_small.png | Bin 0 -> 20467 bytes app/templates/about.jade | 7 +++++++ 2 files changed, 7 insertions(+) create mode 100644 app/assets/images/pages/about/lisa_small.png diff --git a/app/assets/images/pages/about/lisa_small.png b/app/assets/images/pages/about/lisa_small.png new file mode 100644 index 0000000000000000000000000000000000000000..08fba0a64f0b5194532c1e056ea725d96fb99d9b GIT binary patch literal 20467 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 literal 0 HcmV?d00001 diff --git a/app/templates/about.jade b/app/templates/about.jade index e6693ee99..619dc241b 100644 --- a/app/templates/about.jade +++ b/app/templates/about.jade @@ -139,6 +139,13 @@ block content small(data-i18n="about.elliot_title") br + li + img(src="/images/pages/about/lisa_small.png").img-thumbnail + .team-bio + h6.label.team-name Lisa Wu + small Marketing Development Rep + br + // Part time / contract li a(href="http://floor.is/lava/" rel="external") From c57fd6f460a0485c8bce342fd44f286f8e7d497c Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Wed, 8 Jun 2016 15:48:22 -0700 Subject: [PATCH 134/167] Do not set campaignIndex for levels except for course campaigns --- app/views/editor/campaign/CampaignEditorView.coffee | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index 17b182574..4cff9c845 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -154,16 +154,15 @@ module.exports = class CampaignEditorView extends RootView propagateCampaignIndexes: -> campaignLevels = $.extend({}, @campaign.get('levels')) - index = 0 for levelOriginal, campaignLevel of campaignLevels - level = @levels.findWhere({original: levelOriginal}) - if level and level.get('campaignIndex') isnt index - level.set('campaignIndex', index) + if @campaign.get('type') is 'course' + level = @levels.findWhere({original: levelOriginal}) + if level and level.get('campaignIndex') isnt index + level.set('campaignIndex', index) campaignLevel.campaignIndex = index index += 1 - - @campaign.set('levels', campaignLevels) + @campaign.set('levels', campaignLevels) onClickPatches: (e) -> @patchesView = @insertSubView(new PatchesView(@campaign), @$el.find('.patches-view')) From b0fcddac6860138522011831f3541a058cb0c8bd Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Wed, 8 Jun 2016 16:57:00 -0700 Subject: [PATCH 135/167] Add game-dev level type (#3725) * Initial pass adding new game-dev level type. * Fix a failing test with updated LevelSystem required properties * Bring back normal Angel worker timeout times * Fix another failing LevelSystem test since removing propertyDocumentation --- app/core/ParticleMan.coffee | 36 +++++++++++++++++++ app/models/Level.coffee | 2 +- app/schemas/models/campaign.schema.coffee | 2 +- app/schemas/models/level.coffee | 2 +- app/schemas/models/level_component.coffee | 2 +- app/schemas/models/level_system.coffee | 20 ++--------- .../component/ThangComponentConfigView.coffee | 2 +- .../level/systems/NewLevelSystemModal.coffee | 2 +- .../level/thangs/LevelThangEditView.coffee | 2 +- .../editor/level/thangs/ThangsTabView.coffee | 4 +-- app/views/play/CampaignView.coffee | 2 +- app/views/play/level/ControlBarView.coffee | 3 +- app/views/play/level/PlayLevelView.coffee | 8 ++--- .../play/level/modal/HeroVictoryModal.coffee | 12 ++++--- app/views/play/level/tome/Spell.coffee | 2 +- .../play/level/tome/SpellListEntryView.coffee | 2 +- .../level/tome/SpellPaletteEntryView.coffee | 2 +- .../play/level/tome/SpellPaletteView.coffee | 4 +-- app/views/play/level/tome/SpellView.coffee | 2 +- app/views/play/level/tome/TomeView.coffee | 4 +-- bower.json | 2 +- package.json | 2 +- .../functional/level_system.spec.coffee | 2 -- 23 files changed, 71 insertions(+), 50 deletions(-) diff --git a/app/core/ParticleMan.coffee b/app/core/ParticleMan.coffee index b63f5d8a4..bcaebd72b 100644 --- a/app/core/ParticleMan.coffee +++ b/app/core/ParticleMan.coffee @@ -239,6 +239,12 @@ particleKinds['level-dungeon-replayable'] = particleKinds['level-dungeon-replaya colorMiddle: hsl 0.17, 0.75, 0.5 colorEnd: hsl 0.17, 0.75, 0.3 +particleKinds['level-dungeon-game-dev'] = particleKinds['level-dungeon-game-dev-premium'] = ext particleKinds['level-dungeon-hero-ladder'], + emitter: + colorStart: hsl 0.7, 0.75, 0.7 + colorMiddle: hsl 0.7, 0.75, 0.5 + colorEnd: hsl 0.7, 0.75, 0.3 + particleKinds['level-dungeon-premium-item'] = ext particleKinds['level-dungeon-gate'], emitter: particleCount: 2000 @@ -288,6 +294,12 @@ particleKinds['level-forest-replayable'] = particleKinds['level-forest-replayabl colorMiddle: hsl 0.17, 0.75, 0.5 colorEnd: hsl 0.17, 0.75, 0.3 +particleKinds['level-forest-game-dev'] = particleKinds['level-forest-game-dev-premium'] = ext particleKinds['level-forest-hero-ladder'], + emitter: + colorStart: hsl 0.7, 0.75, 0.7 + colorMiddle: hsl 0.7, 0.75, 0.5 + colorEnd: hsl 0.7, 0.75, 0.3 + particleKinds['level-forest-premium-item'] = ext particleKinds['level-forest-gate'], emitter: particleCount: 2000 @@ -337,6 +349,12 @@ particleKinds['level-desert-replayable'] = particleKinds['level-desert-replayabl colorMiddle: hsl 0.17, 0.75, 0.5 colorEnd: hsl 0.17, 0.75, 0.3 +particleKinds['level-desert-game-dev'] = particleKinds['level-desert-game-dev-premium'] = ext particleKinds['level-desert-hero-ladder'], + emitter: + colorStart: hsl 0.7, 0.75, 0.7 + colorMiddle: hsl 0.7, 0.75, 0.5 + colorEnd: hsl 0.7, 0.75, 0.3 + particleKinds['level-mountain-premium-hero'] = ext particleKinds['level-mountain-premium'], emitter: particleCount: 200 @@ -371,6 +389,12 @@ particleKinds['level-mountain-replayable'] = particleKinds['level-mountain-repla colorMiddle: hsl 0.17, 0.75, 0.5 colorEnd: hsl 0.17, 0.75, 0.3 +particleKinds['level-mountain-game-dev'] = particleKinds['level-mountain-game-dev-premium'] = ext particleKinds['level-mountain-hero-ladder'], + emitter: + colorStart: hsl 0.7, 0.75, 0.7 + colorMiddle: hsl 0.7, 0.75, 0.5 + colorEnd: hsl 0.7, 0.75, 0.3 + particleKinds['level-glacier-premium-hero'] = ext particleKinds['level-glacier-premium'], emitter: particleCount: 200 @@ -405,6 +429,12 @@ particleKinds['level-glacier-replayable'] = particleKinds['level-glacier-replaya colorMiddle: hsl 0.17, 0.75, 0.5 colorEnd: hsl 0.17, 0.75, 0.3 +particleKinds['level-glacier-game-dev'] = particleKinds['level-glacier-game-dev-premium'] = ext particleKinds['level-glacier-hero-ladder'], + emitter: + colorStart: hsl 0.7, 0.75, 0.7 + colorMiddle: hsl 0.7, 0.75, 0.5 + colorEnd: hsl 0.7, 0.75, 0.3 + particleKinds['level-volcano-premium-hero'] = ext particleKinds['level-volcano-premium'], emitter: particleCount: 200 @@ -438,3 +468,9 @@ particleKinds['level-volcano-replayable'] = particleKinds['level-volcano-replaya colorStart: hsl 0.17, 0.75, 0.7 colorMiddle: hsl 0.17, 0.75, 0.5 colorEnd: hsl 0.17, 0.75, 0.3 + +particleKinds['level-volcano-game-dev'] = particleKinds['level-volcano-game-dev-premium'] = ext particleKinds['level-volcano-hero-ladder'], + emitter: + colorStart: hsl 0.7, 0.75, 0.7 + colorMiddle: hsl 0.7, 0.75, 0.5 + colorEnd: hsl 0.7, 0.75, 0.3 diff --git a/app/models/Level.coffee b/app/models/Level.coffee index 166b7b11d..b284f1dda 100644 --- a/app/models/Level.coffee +++ b/app/models/Level.coffee @@ -58,7 +58,7 @@ module.exports = class Level extends CocoModel denormalize: (supermodel, session, otherSession) -> o = $.extend true, {}, @attributes - if o.thangs and @get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] + if o.thangs and @get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] thangTypesWithComponents = (tt for tt in supermodel.getModels(ThangType) when tt.get('components')?) thangTypesByOriginal = _.indexBy thangTypesWithComponents, (tt) -> tt.get('original') # Optimization for levelThang in o.thangs diff --git a/app/schemas/models/campaign.schema.coffee b/app/schemas/models/campaign.schema.coffee index 770a2f81b..182d62771 100644 --- a/app/schemas/models/campaign.schema.coffee +++ b/app/schemas/models/campaign.schema.coffee @@ -61,7 +61,7 @@ _.extend CampaignSchema.properties, { i18n: { type: 'object', format: 'hidden' } requiresSubscription: { type: 'boolean' } replayable: { type: 'boolean' } - type: {'enum': ['ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']} + type: {'enum': ['ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']} slug: { type: 'string', format: 'hidden' } original: { type: 'string', format: 'hidden' } adventurer: { type: 'boolean' } diff --git a/app/schemas/models/level.coffee b/app/schemas/models/level.coffee index 70930526b..a6f7a42e7 100644 --- a/app/schemas/models/level.coffee +++ b/app/schemas/models/level.coffee @@ -306,7 +306,7 @@ _.extend LevelSchema.properties, icon: {type: 'string', format: 'image-file', title: 'Icon'} banner: {type: 'string', format: 'image-file', title: 'Banner'} goals: c.array {title: 'Goals', description: 'An array of goals which are visible to the player and can trigger scripts.'}, GoalSchema - type: c.shortString(title: 'Type', description: 'What kind of level this is.', 'enum': ['campaign', 'ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']) + type: c.shortString(title: 'Type', description: 'What kind of level this is.', 'enum': ['campaign', 'ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']) terrain: c.terrainString showsGuide: c.shortString(title: 'Shows Guide', description: 'If the guide is shown at the beginning of the level.', 'enum': ['first-time', 'always']) requiresSubscription: {title: 'Requires Subscription', description: 'Whether this level is available to subscribers only.', type: 'boolean'} diff --git a/app/schemas/models/level_component.coffee b/app/schemas/models/level_component.coffee index 086b680bb..bb1e07f48 100644 --- a/app/schemas/models/level_component.coffee +++ b/app/schemas/models/level_component.coffee @@ -10,7 +10,7 @@ class AttacksSelf extends Component systems = [ 'action', 'ai', 'alliance', 'collision', 'combat', 'display', 'event', 'existence', 'hearing', 'inventory', 'movement', 'programming', 'targeting', 'ui', 'vision', 'misc', 'physics', 'effect', - 'magic' + 'magic', 'game' ] PropertyDocumentationSchema = c.object { diff --git a/app/schemas/models/level_system.coffee b/app/schemas/models/level_system.coffee index 218e4a0e0..7a85de552 100644 --- a/app/schemas/models/level_system.coffee +++ b/app/schemas/models/level_system.coffee @@ -18,21 +18,6 @@ class Jitter extends System return hash """ -PropertyDocumentationSchema = c.object { - title: 'Property Documentation' - description: 'Documentation entry for a property this System will add to its Thang which other Systems might want to also use.' - default: - name: 'foo' - type: 'object' - description: 'This System provides a "foo" property to satisfy all one\'s foobar needs. Use it wisely.' - required: ['name', 'type', 'description'] -}, - name: {type: 'string', pattern: c.identifierPattern, title: 'Name', description: 'Name of the property.'} - # not actual JS types, just whatever they describe... - type: c.shortString(title: 'Type', description: 'Intended type of the property.') - description: {type: 'string', description: 'Description of the property.', maxLength: 1000} - args: c.array {title: 'Arguments', description: 'If this property has type "function", then provide documentation for any function arguments.'}, c.FunctionArgumentSchema - DependencySchema = c.object { title: 'System Dependency' description: 'A System upon which this System depends.' @@ -50,14 +35,14 @@ DependencySchema = c.object { LevelSystemSchema = c.object { title: 'System' description: 'A System which can affect Level behavior.' - required: ['name', 'description', 'code', 'dependencies', 'propertyDocumentation', 'codeLanguage'] + required: ['name', 'code'] default: name: 'JitterSystem' description: 'This System makes all idle, movable Thangs jitter around.' code: jitterSystemCode codeLanguage: 'coffeescript' dependencies: [] # TODO: should depend on something by default - propertyDocumentation: [] + configSchema: {} } c.extendNamedProperties LevelSystemSchema # let's have the name be the first property LevelSystemSchema.properties.name.pattern = c.classNamePattern @@ -83,7 +68,6 @@ _.extend LevelSystemSchema.properties, type: 'string' format: 'hidden' dependencies: c.array {title: 'Dependencies', description: 'An array of Systems upon which this System depends.', uniqueItems: true}, DependencySchema - propertyDocumentation: c.array {title: 'Property Documentation', description: 'An array of documentation entries for each notable property this System will add to its Level which other Systems might want to also use.'}, PropertyDocumentationSchema configSchema: _.extend metaschema, {title: 'Configuration Schema', description: 'A schema for validating the arguments that can be passed to this System as configuration.', default: {type: 'object', additionalProperties: false}} official: type: 'boolean' diff --git a/app/views/editor/component/ThangComponentConfigView.coffee b/app/views/editor/component/ThangComponentConfigView.coffee index ba95de63c..dc5498b41 100644 --- a/app/views/editor/component/ThangComponentConfigView.coffee +++ b/app/views/editor/component/ThangComponentConfigView.coffee @@ -46,7 +46,7 @@ module.exports = class ThangComponentConfigView extends CocoView schema.default ?= {} _.merge schema.default, @additionalDefaults if @additionalDefaults - if @level?.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] + if @level?.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] schema.required = [] treemaOptions = supermodel: @supermodel diff --git a/app/views/editor/level/systems/NewLevelSystemModal.coffee b/app/views/editor/level/systems/NewLevelSystemModal.coffee index 193a50c5d..e44996ab5 100644 --- a/app/views/editor/level/systems/NewLevelSystemModal.coffee +++ b/app/views/editor/level/systems/NewLevelSystemModal.coffee @@ -20,7 +20,7 @@ module.exports = class NewLevelSystemModal extends ModalView name = @$el.find('#level-system-name').val() system = new LevelSystem() system.set 'name', name - system.set 'code', system.get('code').replace(/Jitter/g, name) + system.set 'code', system.get('code', true).replace(/Jitter/g, name) system.set 'permissions', [{access: 'owner', target: me.id}] # Private until saved in a published Level res = system.save(null, {type: 'POST'}) # Override PUT so we can trigger postFirstVersion logic return unless res diff --git a/app/views/editor/level/thangs/LevelThangEditView.coffee b/app/views/editor/level/thangs/LevelThangEditView.coffee index bf7d6b162..84429d644 100644 --- a/app/views/editor/level/thangs/LevelThangEditView.coffee +++ b/app/views/editor/level/thangs/LevelThangEditView.coffee @@ -41,7 +41,7 @@ module.exports = class LevelThangEditView extends CocoView level: @level world: @world - if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] then options.thangType = thangType + if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] then options.thangType = thangType @thangComponentEditView = new ThangComponentsEditView options @listenTo @thangComponentEditView, 'components-changed', @onComponentsChanged diff --git a/app/views/editor/level/thangs/ThangsTabView.coffee b/app/views/editor/level/thangs/ThangsTabView.coffee index 351da5032..c5ff8c65f 100644 --- a/app/views/editor/level/thangs/ThangsTabView.coffee +++ b/app/views/editor/level/thangs/ThangsTabView.coffee @@ -585,14 +585,14 @@ module.exports = class ThangsTabView extends CocoView if batchInsert if thangType.get('name') is 'Hero Placeholder' thangID = 'Hero Placeholder' - return if not (@level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']) or @getThangByID(thangID) + return if not (@level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']) or @getThangByID(thangID) else thangID = "Random #{thangType.get('name')} #{@thangsBatch.length}" else thangID = Thang.nextID(thangType.get('name'), @world) until thangID and not @getThangByID(thangID) if @cloneSourceThang components = _.cloneDeep @getThangByID(@cloneSourceThang.id).components - else if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] + else if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] components = [] # Load them all from default ThangType Components else components = _.cloneDeep thangType.get('components') ? [] diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index 5852514fd..a484db9d9 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -404,7 +404,7 @@ module.exports = class CampaignView extends RootView particleKey.push 'hero' if level.unlocksHero and not level.unlockedHero #particleKey.push 'item' if level.slug is 'robot-ragnarok' # TODO: generalize continue if particleKey.length is 2 # Don't show basic levels - continue unless level.hidden or _.intersection(particleKey, ['item', 'hero-ladder', 'replayable']).length + continue unless level.hidden or _.intersection(particleKey, ['item', 'hero-ladder', 'replayable', 'game-dev']).length @particleMan.addEmitter level.position.x / 100, level.position.y / 100, particleKey.join('-') onMouseEnterPortals: (e) -> diff --git a/app/views/play/level/ControlBarView.coffee b/app/views/play/level/ControlBarView.coffee index fd649ef8e..515a20f2b 100644 --- a/app/views/play/level/ControlBarView.coffee +++ b/app/views/play/level/ControlBarView.coffee @@ -61,7 +61,7 @@ module.exports = class ControlBarView extends CocoView getRenderData: (c={}) -> super c c.worldName = @worldName - c.campaignIndex = @level.get('campaignIndex') + 1 if @level.get('type') is 'course' and @level.get('campaignIndex')? + c.campaignIndex = @level.get('campaignIndex') + 1 if @level.get('type') is 'course' and @level.get('campaignIndex')? # TODO: support 'game-dev' levels in courses c.multiplayerEnabled = @session.get('multiplayer') c.ladderGame = @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder'] if c.isMultiplayerLevel = @isMultiplayerLevel @@ -104,6 +104,7 @@ module.exports = class ControlBarView extends CocoView if @courseInstanceID @homeLink += "/#{@courseInstanceID}" @homeViewArgs.push @courseInstanceID + #else if @level.get('type', true) is 'game-dev' # TODO else @homeLink = '/' @homeViewClass = 'views/HomeView' diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index e5e3507a0..8911ba832 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -203,7 +203,7 @@ module.exports = class PlayLevelView extends RootView @session = @levelLoader.session @world = @levelLoader.world @level = @levelLoader.level - @$el.addClass 'hero' if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] + @$el.addClass 'hero' if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] @$el.addClass 'flags' if _.any(@world.thangs, (t) -> (t.programmableProperties and 'findFlags' in t.programmableProperties) or t.inventory?.flag) or @level.get('slug') is 'sky-span' # TODO: Update terminology to always be opponentSession or otherSession # TODO: E.g. if it's always opponent right now, then variable names should be opponentSession until we have coop play @@ -463,7 +463,7 @@ module.exports = class PlayLevelView extends RootView return false if $.browser?.msie or $.browser?.msedge return false if $.browser.linux return false if me.level() < 8 - if levelType is 'course' + if levelType in ['course', 'game-dev'] return false else if levelType is 'hero' and gamesSimulated return false if stillBuggy @@ -536,7 +536,7 @@ module.exports = class PlayLevelView extends RootView onDonePressed: -> @showVictory() onShowVictory: (e) -> - $('#level-done-button').show() unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] + $('#level-done-button').show() unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] @showVictory() if e.showModal return if @victorySeen @victorySeen = true @@ -554,7 +554,7 @@ module.exports = class PlayLevelView extends RootView return if @level.hasLocalChanges() # Don't award achievements when beating level changed in level editor @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 = if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] then HeroVictoryModal else VictoryModal ModalClass = CourseVictoryModal if @isCourseMode() or me.isSessionless() ModalClass = PicoCTFVictoryModal if window.serverConfig.picoCTF victoryModal = new ModalClass(options) diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee index b01808aa3..1f7a46672 100644 --- a/app/views/play/level/modal/HeroVictoryModal.coffee +++ b/app/views/play/level/modal/HeroVictoryModal.coffee @@ -49,7 +49,7 @@ module.exports = class HeroVictoryModal extends ModalView @session = options.session @level = options.level @thangTypes = {} - if @level.get('type', true) in ['hero', 'hero-ladder', 'course', 'course-ladder'] + if @level.get('type', true) in ['hero', 'hero-ladder', 'course', 'course-ladder', 'game-dev'] achievements = new CocoCollection([], { url: "/db/achievement?related=#{@session.get('level').original}" model: Achievement @@ -73,6 +73,7 @@ module.exports = class HeroVictoryModal extends ModalView if @level.get('type', true) in ['course', 'course-ladder'] @saveReviewEventually = _.debounce(@saveReviewEventually, 2000) @loadExistingFeedback() + # TODO: support game-dev destroy: -> clearInterval @sequentialAnimationInterval @@ -153,7 +154,8 @@ module.exports = class HeroVictoryModal extends ModalView getRenderData: -> c = super() c.levelName = utils.i18n @level.attributes, 'name' - if @level.get('type', true) isnt 'hero' + # TODO: support 'game-dev' + if @level.get('type', true) not in ['hero', 'game-dev'] c.victoryText = utils.i18n @level.get('victory') ? {}, 'body' earnedAchievementMap = _.indexBy(@newEarnedAchievements or [], (ea) -> ea.get('achievement')) for achievement in (@achievements?.models or []) @@ -221,7 +223,7 @@ module.exports = class HeroVictoryModal extends ModalView afterRender: -> super() - @$el.toggleClass 'with-achievements', @level.get('type', true) in ['hero', 'hero-ladder'] + @$el.toggleClass 'with-achievements', @level.get('type', true) in ['hero', 'hero-ladder', 'game-dev'] # TODO: support game-dev return unless @supermodel.finished() @playSelectionSound hero, true for original, hero of @thangTypes # Preload them @updateSavingProgressStatus() @@ -231,7 +233,7 @@ module.exports = class HeroVictoryModal extends ModalView @insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view') initializeAnimations: -> - return @endSequentialAnimations() unless @level.get('type', true) in ['hero', 'hero-ladder'] + return @endSequentialAnimations() unless @level.get('type', true) in ['hero', 'hero-ladder', 'game-dev'] # TODO: support game-dev @updateXPBars 0 #playVictorySound = => @playSound 'victory-title-appear' # TODO: actually add this @$el.find('#victory-header').delay(250).queue(-> @@ -262,7 +264,7 @@ module.exports = class HeroVictoryModal extends ModalView beginSequentialAnimations: -> return if @destroyed - return unless @level.get('type', true) in ['hero', 'hero-ladder'] + return unless @level.get('type', true) in ['hero', 'hero-ladder', 'game-dev'] # TODO: support game-dev @sequentialAnimatedPanels = _.map(@animatedPanels.find('.reward-panel'), (panel) -> { number: $(panel).data('number') previousNumber: $(panel).data('previous-number') diff --git a/app/views/play/level/tome/Spell.coffee b/app/views/play/level/tome/Spell.coffee index 0e6642d67..aabfa4c96 100644 --- a/app/views/play/level/tome/Spell.coffee +++ b/app/views/play/level/tome/Spell.coffee @@ -165,7 +165,7 @@ module.exports = class Spell writable = @permissions.readwrite.length > 0 skipProtectAPI = @skipProtectAPI or not writable problemContext = @createProblemContext thang - includeFlow = (@levelType in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']) and not skipProtectAPI + includeFlow = (@levelType in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']) and not skipProtectAPI aetherOptions = createAetherOptions functionName: @name codeLanguage: @language diff --git a/app/views/play/level/tome/SpellListEntryView.coffee b/app/views/play/level/tome/SpellListEntryView.coffee index eb6b93cf6..7dc00f9b6 100644 --- a/app/views/play/level/tome/SpellListEntryView.coffee +++ b/app/views/play/level/tome/SpellListEntryView.coffee @@ -37,7 +37,7 @@ module.exports = class SpellListEntryView extends CocoView context createMethodSignature: -> - return @spell.name if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] + return @spell.name if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] parameters = (@spell.parameters or []).slice() if @spell.language in ['python', 'lua'] parameters.unshift 'self' diff --git a/app/views/play/level/tome/SpellPaletteEntryView.coffee b/app/views/play/level/tome/SpellPaletteEntryView.coffee index 31f75fcaf..cbada6610 100644 --- a/app/views/play/level/tome/SpellPaletteEntryView.coffee +++ b/app/views/play/level/tome/SpellPaletteEntryView.coffee @@ -84,7 +84,7 @@ module.exports = class SpellPaletteEntryView extends CocoView Backbone.Mediator.publish 'tome:palette-pin-toggled', entry: @, pinned: @popoverPinned onClick: (e) => - if true or @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] + if true or @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] # Jiggle instead of pin for hero levels # Actually, do it all the time, because we recently busted the pin CSS. TODO: restore pinning jigglyPopover = $('.spell-palette-popover.popover') diff --git a/app/views/play/level/tome/SpellPaletteView.coffee b/app/views/play/level/tome/SpellPaletteView.coffee index c7ac5174f..be4abdc59 100644 --- a/app/views/play/level/tome/SpellPaletteView.coffee +++ b/app/views/play/level/tome/SpellPaletteView.coffee @@ -163,7 +163,7 @@ module.exports = class SpellPaletteView extends CocoView else propStorage = 'this': ['apiProperties', 'apiMethods'] - if not (@options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']) or not @options.programmable + if not (@options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']) or not @options.programmable @organizePalette propStorage, allDocs, excludedDocs else @organizePaletteHero propStorage, allDocs, excludedDocs @@ -205,7 +205,7 @@ module.exports = class SpellPaletteView extends CocoView if tabbify and _.find @entries, ((entry) -> entry.doc.owner isnt 'this') @entryGroups = _.groupBy @entries, groupForEntry else - i18nKey = if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] then 'play_level.tome_your_skills' else 'play_level.tome_available_spells' + i18nKey = if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] then 'play_level.tome_your_skills' else 'play_level.tome_available_spells' defaultGroup = $.i18n.t i18nKey @entryGroups = {} @entryGroups[defaultGroup] = @entries diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index dbddb0de3..75a800446 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -635,7 +635,7 @@ module.exports = class SpellView extends CocoView @createToolbarView() createDebugView: -> - return if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] # We'll turn this on later, maybe, but not yet. + return if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] # We'll turn this on later, maybe, but not yet. @debugView = new SpellDebugView ace: @ace, thang: @thang, spell:@spell @$el.append @debugView.render().$el.hide() diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee index 3e1df0292..f15233079 100644 --- a/app/views/play/level/tome/TomeView.coffee +++ b/app/views/play/level/tome/TomeView.coffee @@ -60,7 +60,7 @@ module.exports = class TomeView extends CocoView @worker = @createWorker() programmableThangs = _.filter @options.thangs, (t) -> t.isProgrammable and t.programmableMethods @createSpells programmableThangs, programmableThangs[0]?.world # Do before spellList, thangList, and castButton - unless @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] + unless @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] @spellList = @insertSubView new SpellListView spells: @spells, supermodel: @supermodel, level: @options.level @castButton = @insertSubView new CastButtonView spells: @spells, level: @options.level, session: @options.session, god: @options.god @teamSpellMap = @generateTeamSpellMap(@spells) @@ -193,7 +193,7 @@ module.exports = class TomeView extends CocoView @castButton?.$el.hide() onSpriteSelected: (e) -> - return if @spellView and @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] # Never deselect the hero in the Tome. + return if @spellView and @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] # Never deselect the hero in the Tome. thang = e.thang spellName = e.spellName @spellList?.$el.hide() diff --git a/bower.json b/bower.json index 0f69fe4a9..454979b4e 100644 --- a/bower.json +++ b/bower.json @@ -32,7 +32,7 @@ "firepad": "~0.1.2", "marked": "~0.3.0", "moment": "~2.5.0", - "aether": "~0.5.0", + "aether": "~0.5.6", "underscore.string": "~2.3.3", "firebase": "~1.0.2", "d3": "~3.4.4", diff --git a/package.json b/package.json index fc326df34..80047b905 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "dependencies": { "JQDeferred": "~2.1.0", "ace-builds": "https://github.com/ajaxorg/ace-builds/archive/3fb55e8e374ab02ce47c1ae55ffb60a1835f3055.tar.gz", - "aether": "~0.5.0", + "aether": "~0.5.6", "async": "0.2.x", "aws-sdk": "~2.0.0", "bayesian-battle": "0.0.7", diff --git a/spec/server/functional/level_system.spec.coffee b/spec/server/functional/level_system.spec.coffee index cf53c59e4..df5ed836a 100644 --- a/spec/server/functional/level_system.spec.coffee +++ b/spec/server/functional/level_system.spec.coffee @@ -15,7 +15,6 @@ describe 'LevelSystem', -> codeLanguage: 'coffeescript' permissions: simplePermissions dependencies: [] - propertyDocumentation: [] systems = {} @@ -80,7 +79,6 @@ describe 'LevelSystem', -> expect(body.original).toBeDefined() expect(body.created).toBeDefined() expect(body.dependencies).toBeDefined() - expect(body.propertyDocumentation).toBeDefined() expect(body.version.isLatestMajor).toBe(true) expect(body.version.isLatestMinor).toBe(true) expect(body.permissions).toBeDefined() From 9dbcf27e115269556030ed75f114a4041c99f82e Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Wed, 8 Jun 2016 06:24:59 -0700 Subject: [PATCH 136/167] Add classroom Mixpanel logging Closes #3720 --- app/core/Tracker.coffee | 9 +- app/templates/courses/courses-view.jade | 10 +- app/templates/courses/teacher-class-view.jade | 6 +- .../courses/teacher-classes-view.jade | 36 +++---- .../courses/teacher-courses-view.jade | 4 +- app/templates/new-home-view.jade | 28 +++--- app/views/NewHomeView.coffee | 71 +++++++------- .../courses/ClassroomSettingsModal.coffee | 6 +- app/views/courses/ClassroomView.coffee | 2 +- app/views/courses/CourseDetailsView.coffee | 5 + app/views/courses/CoursesView.coffee | 43 ++++++--- app/views/courses/EnrollmentsView.coffee | 22 +++-- .../courses/RestrictedToStudentsView.coffee | 5 +- app/views/courses/TeacherClassView.coffee | 32 ++++--- app/views/courses/TeacherClassesView.coffee | 45 ++++++--- app/views/courses/TeacherCoursesView.coffee | 94 +++---------------- app/views/play/level/ControlBarView.coffee | 6 +- .../level/modal/CourseVictoryModal.coffee | 3 + .../ConvertToTeacherAccountView.coffee | 4 + .../teachers/CreateTeacherAccountView.coffee | 6 +- app/views/teachers/RequestQuoteView.coffee | 12 ++- .../teachers/RestrictedToTeachersView.coffee | 5 +- 22 files changed, 228 insertions(+), 226 deletions(-) diff --git a/app/core/Tracker.coffee b/app/core/Tracker.coffee index e1a4ae7cc..2f2aab758 100644 --- a/app/core/Tracker.coffee +++ b/app/core/Tracker.coffee @@ -95,9 +95,13 @@ module.exports = class Tracker extends CocoClass analytics.identify me.id, traits trackPageView: (includeIntegrations=[]) -> + includeMixpanel = (name) -> + mixpanelIncludes = ['', 'schools', 'play', 'play/level/dungeons-of-kithgard'] + name in mixpanelIncludes or /courses|students|teachers/ig.test(name) + name = Backbone.history.getFragment() url = "/#{name}" - console.log "Would track analytics pageview: #{url}" if debugAnalytics + console.log "Would track analytics pageview: #{url} Mixpanel=#{includeMixpanel(name)}" if debugAnalytics @trackEventInternal 'Pageview', url: name unless me?.isAdmin() and @isProduction return unless @isProduction and not me.isAdmin() @@ -106,8 +110,7 @@ module.exports = class Tracker extends CocoClass ga? 'send', 'pageview', url # Mixpanel - mixpanelIncludes = ['', 'courses', 'courses/purchase', 'courses/teachers', 'courses/students', 'schools', 'teachers', 'teachers/freetrial', 'teachers/quote', 'play', 'play/level/dungeons-of-kithgard'] - mixpanel.track('page viewed', 'page name' : name, url : url) if name in mixpanelIncludes + mixpanel.track('page viewed', 'page name' : name, url : url) if includeMixpanel(name) if me.isTeacher() and @segmentLoaded options = {} diff --git a/app/templates/courses/courses-view.jade b/app/templates/courses/courses-view.jade index 11a03df1e..69e5265b5 100644 --- a/app/templates/courses/courses-view.jade +++ b/app/templates/courses/courses-view.jade @@ -67,7 +67,7 @@ block content h5 span.spr= classroom.get('name') span.spr (#{(classroom.get('aceConfig') || {}).language === 'javascript' ? 'JavaScript' : 'Python'}) - a(href="/courses/"+classroom.id, data-i18n="courses.view_class") + a.view-class-btn(data-classroom-id=classroom.id, data-i18n="courses.view_class") - var courseInstances = view.courseInstances.where({classroomID: classroom.id}); for courseInstance in courseInstances @@ -77,7 +77,7 @@ block content h6 span.spr= course.get('name') small - a(href="/courses/"+courseInstance.get('courseID')+'/'+courseInstance.id, data-i18n="courses.view_levels") + a.view-levels-btn(data-course-id=courseInstance.get('courseID'), data-courseinstance-id=courseInstance.id, data-i18n="courses.view_levels") +course-instance-body(courseInstance, classroom) .clearfix @@ -112,19 +112,19 @@ mixin course-instance-body(courseInstance, classroom) - var arenaLevel = stats.levels.arena; if arenaLevel - var arenaURL = "/play/ladder/"+arenaLevel.get('slug')+"/course/"+courseInstance.id; - a.btn.btn-burgandy.btn-lg.m-b-1(href=arenaURL) + a.play-btn.btn.btn-burgandy.btn-lg.m-b-1(data-href=arenaURL, data-level-slug=arenaLevel.get('slug'), data-event-action="Students Play Arena") span(data-i18n="courses.play_arena") else a.btn.btn-default.btn-lg.m-b-1(disabled=true, data-i18n="courses.course_complete") else if stats.levels.next != stats.levels.first - var next = stats.levels.next; - var levelURL = "/play/level/"+next.get('slug')+"?course="+courseInstance.get('courseID')+"&course-instance="+courseInstance.id; - a.btn.btn-forest.btn-lg.m-b-1(href=levelURL) + a.play-btn.btn.btn-forest.btn-lg.m-b-1(data-href=levelURL, data-level-slug=next.get('slug'), data-event-action="Students Continue Course") span(data-i18n="common.continue") else - var firstLevel = stats.levels.first; - var levelURL = "/play/level/"+firstLevel.get('slug')+"?course="+courseInstance.get('courseID')+"&course-instance="+courseInstance.id; - a.btn.btn-navy.btn-lg.m-b-1(href=levelURL) + a.play-btn.btn.btn-navy.btn-lg.m-b-1(data-href=levelURL, data-level-slug=firstLevel.get('slug'), data-event-action="Students Start Course") span(data-i18n="courses.start") div diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade index 3f135c235..7215a08cf 100644 --- a/app/templates/courses/teacher-class-view.jade +++ b/app/templates/courses/teacher-class-view.jade @@ -244,7 +244,7 @@ mixin studentRow(student) div(data-i18n='teacher.remove') mixin enrollStudentButton(student) - a.enroll-student-button.btn.btn-lg.btn-primary(data-classroom-id=view.classroom.id data-user-id=student.id) + a.enroll-student-button.btn.btn-lg.btn-primary(data-classroom-id=view.classroom.id data-user-id=student.id data-event-action="Teachers Class Students Enroll Student") span(data-i18n='teacher.enroll_student') mixin courseProgressTab @@ -294,7 +294,7 @@ mixin courseProgressTab span(data-i18n='TODO') | Assign Course else - .enroll-student-button.btn.btn-md.btn-navy.pull-right(data-user-id=student.id) + .enroll-student-button.btn.btn-md.btn-navy.pull-right(data-user-id=student.id data-event-action="Teachers Class Course Enroll Student") span(data-i18n='TODO') | Enroll Student @@ -426,4 +426,4 @@ mixin enrollmentStatusTab strong(class= status === 'expired' ? 'text-danger' : '')= view.studentStatusString(student) td.enroll-col if status !== 'enrolled' - button.enroll-student-button.btn.btn-navy(data-i18n="teacher.enroll_student", data-user-id=student.id) + button.enroll-student-button.btn.btn-navy(data-i18n="teacher.enroll_student", data-user-id=student.id, data-event-action="Teachers Class Enrollment Enroll Student") diff --git a/app/templates/courses/teacher-classes-view.jade b/app/templates/courses/teacher-classes-view.jade index ae7e134e6..9946d300c 100644 --- a/app/templates/courses/teacher-classes-view.jade +++ b/app/templates/courses/teacher-classes-view.jade @@ -10,15 +10,15 @@ block content p(data-i18n='teacher.teacher_account_required') if me.isAnonymous() .login-button.btn.btn-lg.btn-primary(data-i18n='login.log_in') - a.btn.btn-lg.btn-primary-alt(href="/teachers/signup" data-i18n='teacher.create_teacher_account') + button.btn.btn-lg.btn-primary-alt.create-teacher-btn(data-event-action="Teachers Classes Create Teacher Account", data-i18n='teacher.create_teacher_account') else - a.btn.btn-lg.btn-primary(href="/teachers/update-account" data-i18n="teachers_quote.convert_account_title") + button.btn.btn-lg.btn-primary.update-teacher-btn(data-event-action="Teachers Classes Convert Teacher Account", data-i18n="teachers_quote.convert_account_title") button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out") - + .teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3 h5(data-i18n='teacher.what_is_a_teacher_account') p(data-i18n='teacher.teacher_account_explanation') - + else if !me.isTeacher() .alert.alert-danger.text-center @@ -27,30 +27,30 @@ block content h3 ATTENTION: Please upgrade your account to a Teacher Account. p | We are transitioning to a new improved classroom management system for instructors. - | Please convert your account to ensure you retain access to your classrooms. - a.btn.btn-primary.btn-lg(href="/teachers/update-account") Upgrade to teacher account - + | Please convert your account to ensure you retain access to your classrooms. + button.btn.btn-primary.btn-lg.update-teacher-btn(data-event-action="Teachers Classes Convert Teacher Account Temp") Upgrade to teacher account + .container h3(data-i18n='teacher.current_classes') - + .classes.container // Loop each class each classroom in view.classrooms.models unless classroom.get('archived') +classRow(classroom) - + +createClassButton - + - var archivedClassrooms = view.classrooms.where({archived: true}); if _.size(archivedClassrooms) .container h3(data-i18n='teacher.archived_classes') p(data-i18n='teacher.archived_classes_blurb') - + .classes.container each classroom in archivedClassrooms +archivedClassRow(classroom) - + mixin classRow(classroom) .class.row .col-xs-6 @@ -67,9 +67,9 @@ mixin classRow(classroom) span = classroom.get('members').length .class-links - a.text-h6(data-i18n='teacher.view_class' href=('/teachers/classes/' + classroom.id)) - a.edit-classroom.text-h6(data-i18n='teacher.edit_class_settings' data-classroom-id=classroom.id) - a.archive-classroom.text-h6(data-i18n='teacher.archive_class' data-classroom-id=classroom.id) + a.view-class-btn.text-h6(data-i18n='teacher.view_class' data-classroom-id=classroom.id data-event-action="Teachers Classes View Class Link") + a.edit-classroom.text-h6(data-i18n='teacher.edit_class_settings' data-classroom-id=classroom.id data-event-action="Teachers Classes Edit Class Started") + a.archive-classroom.text-h6(data-i18n='teacher.archive_class' data-classroom-id=classroom.id data-event-action="Teachers Classes Archive Class") .progress-col.col-xs-5 if classroom.get('members').length == 0 @@ -80,7 +80,7 @@ mixin classRow(classroom) if view.courseInstances.findWhere({ classroomID: classroom.id, courseID: course.id }) +progressDot(classroom, course, index) .view-class-arrow.col-xs-1 - a.view-class-arrow-inner.glyphicon.glyphicon-chevron-right(data-classroom-id=classroom.id, href=('/teachers/classes/' + classroom.id)) + a.view-class-arrow-inner.glyphicon.glyphicon-chevron-right.view-class-btn(data-classroom-id=classroom.id data-event-action="Teachers Classes View Class Chevron") mixin addStudentsButton(classroom) @@ -91,7 +91,7 @@ mixin addStudentsButton(classroom) a.add-students-btn.btn.btn-lg.btn-primary(data-classroom-id=classroom.id ) span(data-i18n='teacher.add_students') | Add Students - + mixin createClassButton .create-class .text-center @@ -110,7 +110,7 @@ mixin progressDot(classroom, course, index) - complete = courseInstance.numCompleted - started = courseInstance.started - dotClass = complete === total ? 'forest' : started ? 'gold' : ''; - - var progressDotContext = {total: total, complete: complete}; + - var progressDotContext = {total: total, complete: complete}; .progress-dot(class=dotClass, data-title=view.progressDotTemplate(progressDotContext)) +progressDotLabel(index) diff --git a/app/templates/courses/teacher-courses-view.jade b/app/templates/courses/teacher-courses-view.jade index 25038c973..9707e0170 100644 --- a/app/templates/courses/teacher-courses-view.jade +++ b/app/templates/courses/teacher-courses-view.jade @@ -86,10 +86,10 @@ mixin course-info(course) if view.guideLinks[course.id] //- a.btn.btn-primary(href=view.guideLinks[course.id] class=(me.isTeacher() ? '': 'disabled')) //- span(data-i18n="courses.print_guide") - a.btn.btn-primary(href=view.guideLinks[course.id].python class=(me.isTeacher() ? '': 'disabled')) + a.guide-btn.btn.btn-primary(href=view.guideLinks[course.id].python data-course-id=course.id data-course-name=course.get('name') data-event-action="Classes Guides Guide Python" class=(me.isTeacher() ? '': 'disabled')) span(data-i18n="courses.view_guide_online") | — Python - a.btn.btn-primary(href=view.guideLinks[course.id].javascript class=(me.isTeacher() ? '': 'disabled')) + a.guide-btn.btn.btn-primary(href=view.guideLinks[course.id].javascript data-course-id=course.id data-course-name=course.get('name') data-event-action="Classes Guides Guide JavaScript" class=(me.isTeacher() ? '': 'disabled')) span(data-i18n="courses.view_guide_online") | — JavaScript else diff --git a/app/templates/new-home-view.jade b/app/templates/new-home-view.jade index 59065644a..f4c88d015 100644 --- a/app/templates/new-home-view.jade +++ b/app/templates/new-home-view.jade @@ -5,36 +5,36 @@ mixin box if me.isAnonymous() == true h6#classroom-edition-header(data-i18n="new_home.classroom_edition") div - button.teacher-btn.btn.btn-primary.btn-lg.btn-block(data-i18n="new_home.im_a_teacher") + a.teacher-btn.btn.btn-primary.btn-lg.btn-block(data-event-action="Homepage Click Teacher Button CTA", data-i18n="new_home.im_a_teacher") div - a.btn.btn-forest.btn-lg.btn-block(href="/courses", data-i18n="new_home.im_a_student") + a.student-btn.btn.btn-forest.btn-lg.btn-block(href="/courses", data-event-action="Homepage Click Student Button CTA", data-i18n="new_home.im_a_student") h6#learn-to-code-header(data-i18n="new_home.learn_to_code") - a.btn.btn-gold.btn-lg.btn-block.play-btn(href=view.playURL, data-i18n="new_home.play_now") + a.btn.btn-gold.btn-lg.btn-block.play-btn(href=view.playURL, data-event-action="Homepage Play Now CTA", data-i18n="new_home.play_now") else h6#classroom-edition-header(data-i18n="new_home.logged_in_as") p.small #{me.get("email")} if me.isTeacher() div - button.teacher-btn.btn.btn-forest.btn-lg.btn-block(data-i18n="new_home.goto_classes") + button.teacher-btn.btn.btn-forest.btn-lg.btn-block(data-event-action="Homepage Click My Classes CTA", data-i18n="new_home.goto_classes") div if view.isTeacherWithDemo h6(data-i18n="new_home.check_out_wiki") - a.btn.btn-primary.btn-lg.btn-block(href="https://sites.google.com/a/codecombat.com/teacher-guides/course-guides", data-i18n="nav.educator_wiki") + button.wiki-btn.btn.btn-primary.btn-lg.btn-block(data-event-action="Homepage Click Educator Wiki CTA", data-i18n="nav.educator_wiki") else h6(data-i18n="new_home.want_coco") - a.btn.btn-primary.btn-lg.btn-block(href=view.demoRequestURL, data-i18n="new_home.get_started") + button.btn.btn-primary.btn-lg.request-demo(data-event-action="Homepage Request Demo CTA", data-i18n="new_home.request_demo") else if me.justPlaysCourses() div - a.btn.btn-forest.btn-lg.btn-block(href=view.playURL, data-i18n="courses.continue_playing") + a.btn.btn-forest.btn-lg.btn-block.play-btn(href=view.playURL, data-event-action="Homepage Classroom Continue Playing CTA", data-i18n="courses.continue_playing") div - a.btn.btn-primary.btn-lg.btn-block.play-btn(href=view.playURL, data-i18n="new_home.view_progress") + a.btn.btn-primary.btn-lg.btn-block.play-btn(href=view.playURL, data-event-action="Homepage View Progress CTA", data-i18n="new_home.view_progress") else div - a.btn.btn-forest.btn-lg.btn-block.play-btn(href=view.playURL, data-i18n="courses.continue_playing") + a.btn.btn-forest.btn-lg.btn-block.play-btn(href=view.playURL, data-event-action="Homepage Campaign Continue Playing CTA", data-i18n="courses.continue_playing") div - a.btn.btn-primary.btn-lg.btn-block(href="/user/#{me.getSlugOrID()}", data-i18n="new_home.view_profile") + a.btn.btn-primary.btn-lg.btn-block.profile-btn(href=view.playURL, data-event-action="Homepage View Profile CTA", data-i18n="new_home.view_profile") p.small @@ -213,11 +213,11 @@ block content if view.isTeacherWithDemo h4(data-i18n="new_home.get_started_subtitle") div - a.btn.btn-primary.btn-lg(href="/teachers/classes", data-i18n="new_home.setup_a_class") + button.btn.btn-primary.btn-lg.setup-class-btn(data-event-action="Homepage Setup Class", data-i18n="new_home.setup_a_class") else h4(data-i18n="new_home.request_demo_subtitle") div - a.btn.btn-primary.btn-lg(href=view.demoRequestURL, data-i18n="new_home.request_demo") + button.btn.btn-primary.btn-lg.request-demo(data-event-action="Homepage Request Demo", data-i18n="new_home.request_demo") if me.isAnonymous() .have-an-account span.spr(data-i18n="new_home.have_an_account") @@ -306,10 +306,10 @@ block content h3(data-i18n="new_home.run_class") if view.isTeacherWithDemo div - a.btn.btn-primary.btn-lg(href="/teachers/classes", data-i18n="new_home.setup_a_class") + button.btn.btn-primary.btn-lg.setup-class-btn(data-event-action="Homepage Setup Class Page Bottom", data-i18n="new_home.setup_a_class") else div - a.btn.btn-primary.btn-lg(href=view.demoRequestURL, data-i18n="new_home.request_demo") + button.btn.btn-primary.btn-lg.request-demo(data-event-action="Homepage Request Demo Page Bottom", data-i18n="new_home.request_demo") if me.isAnonymous() .have-an-account span.spr(data-i18n="new_home.have_an_account") diff --git a/app/views/NewHomeView.coffee b/app/views/NewHomeView.coffee index 0651a512b..3ccade336 100644 --- a/app/views/NewHomeView.coffee +++ b/app/views/NewHomeView.coffee @@ -18,14 +18,17 @@ module.exports = class NewHomeView extends RootView events: 'click .play-btn': 'onClickPlayButton' 'change #school-level-dropdown': 'onChangeSchoolLevelDropdown' + 'click .student-btn': 'onClickStudentButton' 'click .teacher-btn': 'onClickTeacherButton' 'click #learn-more-link': 'onClickLearnMoreLink' 'click .screen-thumbnail': 'onClickScreenThumbnail' 'click #carousel-left': 'onLeftPressed' 'click #carousel-right': 'onRightPressed' 'click .request-demo': 'onClickRequestDemo' - 'click .join-class': 'onClickJoinClass' 'click .logout-btn': 'logoutAccount' + 'click .profile-btn': 'onClickViewProfile' + 'click .setup-class-btn': 'onClickSetupClass' + 'click .wiki-btn': 'onClickWikiButton' shortcuts: 'right': 'onRightPressed' @@ -36,7 +39,6 @@ module.exports = class NewHomeView extends RootView @courses = new CocoCollection [], {url: "/db/course", model: Course} @supermodel.loadCollection(@courses, 'courses') - window.tracker?.trackEvent 'Homepage Loaded', category: 'Homepage' if me.isTeacher() @trialRequests = new TrialRequests() @trialRequests.fetchOwn() @@ -51,43 +53,52 @@ module.exports = class NewHomeView extends RootView else if me.justPlaysCourses() # Save players who might be in a classroom from getting into the campaign @playURL = '/courses' - @alternatePlayURL = '/play' - @alternatePlayText = 'home.play_campaign_version' else @playURL = '/play' onLoaded: -> @trialRequest = @trialRequests.first() if @trialRequests?.size() @isTeacherWithDemo = @trialRequest and @trialRequest.get('status') in ['approved', 'submitted'] - @demoRequestURL = if me.isTeacher() then '/teachers/update-account' else '/teachers/demo' super() + onClickLearnMoreLink: -> + window.tracker?.trackEvent 'Homepage Click Learn More', category: 'Homepage', ['Mixpanel'] + @scrollToLink('#classroom-in-box-container') + onClickPlayButton: (e) -> - @playSound 'menu-button-click' - e.preventDefault() - e.stopImmediatePropagation() - window.tracker?.trackEvent 'Homepage Click Play', category: 'Homepage' - application.router.navigate @playURL, trigger: true - #window.open @playURL, '_blank' + window.tracker?.trackEvent $(e.target).data('event-action'), category: 'Homepage', ['Mixpanel'] onClickRequestDemo: (e) -> @playSound 'menu-button-click' e.preventDefault() e.stopImmediatePropagation() - window.tracker?.trackEvent 'Homepage Submit Jumbo Form', category: 'Homepage' - obj = storage.load('request-quote-form') - obj ?= {} - obj.role = @$('#request-form-role').val() - obj.numStudents = @$('#request-form-range').val() - storage.save('request-quote-form', obj) - application.router.navigate "/teachers/demo", trigger: true + window.tracker?.trackEvent $(e.target).data('event-action'), category: 'Homepage', ['Mixpanel'] + if me.isTeacher() + application.router.navigate '/teachers/update-account', trigger: true + else + application.router.navigate '/teachers/demo', trigger: true - onClickJoinClass: (e) -> - @playSound 'menu-button-click' - e.preventDefault() - e.stopImmediatePropagation() - window.tracker?.trackEvent 'Homepage Click Join Class', category: 'Homepage' - application.router.navigate "/courses", trigger: true + onClickSetupClass: (e) -> + window.tracker?.trackEvent $(e.target).data('event-action'), category: 'Homepage', ['Mixpanel'] + application.router.navigate("/teachers/classes", { trigger: true }) + + onClickStudentButton: (e) -> + window.tracker?.trackEvent $(e.target).data('event-action'), category: 'Homepage', ['Mixpanel'] + + onClickTeacherButton: (e) -> + window.tracker?.trackEvent $(e.target).data('event-action'), category: 'Homepage', ['Mixpanel'] + if me.isTeacher() + application.router.navigate('/teachers', { trigger: true }) + else + @scrollToLink('.request-demo-row', 600) + + onClickViewProfile: (e) -> + window.tracker?.trackEvent $(e.target).data('event-action'), category: 'Homepage', ['Mixpanel'] + application.router.navigate("/user/#{me.getSlugOrID()}", { trigger: true }) + + onClickWikiButton: (e) -> + window.tracker?.trackEvent $(e.target).data('event-action'), category: 'Homepage', ['Mixpanel'] + window.location.href = 'https://sites.google.com/a/codecombat.com/teacher-guides/course-guides' afterRender: -> @onChangeSchoolLevelDropdown() @@ -125,18 +136,6 @@ module.exports = class NewHomeView extends RootView isNewPlayer: -> not me.get('stats')?.gamesCompleted and not me.get('heroConfig') - onClickLearnMoreLink: -> - window.tracker?.trackEvent 'Homepage Click Learn More', category: 'Homepage' - @scrollToLink('#classroom-in-box-container') - - onClickTeacherButton: -> - if me.isTeacher() - window.tracker?.trackEvent 'Homepage Click Teacher Button (logged in)', category: 'Homepage' - application.router.navigate('/teachers', { trigger: true }) - else - window.tracker?.trackEvent 'Homepage Click Teacher Button', category: 'Homepage' - @scrollToLink('.request-demo-row', 600) - onRightPressed: (event) -> # Special handling, otherwise after you click the control, keyboard presses move the slide twice return if event.type is 'keydown' and $(document.activeElement).is('.carousel-control') diff --git a/app/views/courses/ClassroomSettingsModal.coffee b/app/views/courses/ClassroomSettingsModal.coffee index d42a20bd6..e228e16af 100644 --- a/app/views/courses/ClassroomSettingsModal.coffee +++ b/app/views/courses/ClassroomSettingsModal.coffee @@ -14,10 +14,6 @@ module.exports = class ClassroomSettingsModal extends ModalView initialize: (options={}) -> @classroom = options.classroom or new Classroom() - if @classroom.isNew() - application.tracker?.trackEvent 'Create new class', category: 'Courses' - else - application.tracker?.trackEvent 'Classroom started edit settings', category: 'Courses', classroomID: @classroom.id afterRender: -> super() @@ -53,3 +49,5 @@ module.exports = class ClassroomSettingsModal extends ModalView button.text(@oldButtonText).attr('disabled', false) errors.showNotyNetworkError(jqxhr) @listenToOnce @classroom, 'sync', @hide + window.tracker?.trackEvent "Teachers Edit Class Saved", category: 'Teachers', classroomID: @classroom.id, ['Mixpanel'] + diff --git a/app/views/courses/ClassroomView.coffee b/app/views/courses/ClassroomView.coffee index 4ba47d3ce..e7f9f1c51 100644 --- a/app/views/courses/ClassroomView.coffee +++ b/app/views/courses/ClassroomView.coffee @@ -57,8 +57,8 @@ module.exports = class ClassroomView extends RootView @levels = new Levels() @levels.fetchForClassroom(classroomID, {data: {project: 'name,slug,original'}}) @levels.on 'add', (model) -> @_byId[model.get('original')] = model # so you can 'get' them - @supermodel.trackCollection(@levels) + window.tracker?.trackEvent 'Students Class Loaded', category: 'Students', classroomID: classroomID, ['Mixpanel'] onCourseInstancesSync: -> @sessions = new CocoCollection([], { model: LevelSession }) diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee index abb619d2b..61ebbf35c 100644 --- a/app/views/courses/CourseDetailsView.coffee +++ b/app/views/courses/CourseDetailsView.coffee @@ -74,6 +74,10 @@ module.exports = class CourseDetailsView extends RootView )) )) + initialize: (options) -> + window.tracker?.trackEvent 'Students Class Course Loaded', category: 'Students', ['Mixpanel'] + super(options) + buildSessionStats: -> return if @destroyed @@ -125,6 +129,7 @@ module.exports = class CourseDetailsView extends RootView levelSlug = $(e.target).closest('.btn-play-level').data('level-slug') levelID = $(e.target).closest('.btn-play-level').data('level-id') level = @levels.findWhere({original: levelID}) + window.tracker?.trackEvent 'Students Class Course Play Level', category: 'Students', courseID: @courseID, courseInstanceID: @courseInstanceID, levelSlug: levelSlug, ['Mixpanel'] if level.get('type') is 'course-ladder' viewClass = 'views/ladder/LadderView' viewArgs = [{supermodel: @supermodel}, levelSlug] diff --git a/app/views/courses/CoursesView.coffee b/app/views/courses/CoursesView.coffee index 0cf3d2d90..fe70659c4 100644 --- a/app/views/courses/CoursesView.coffee +++ b/app/views/courses/CoursesView.coffee @@ -29,11 +29,14 @@ module.exports = class CoursesView extends RootView 'click .change-hero-btn': 'onClickChangeHeroButton' 'click #join-class-btn': 'onClickJoinClassButton' 'submit #join-class-form': 'onSubmitJoinClassForm' - 'click #change-language-link': 'onClickChangeLanguageLink' + 'click .play-btn': 'onClickPlay' + 'click .view-class-btn': 'onClickViewClass' + 'click .view-levels-btn': 'onClickViewLevels' getTitle: -> return $.i18n.t('teacher.students') initialize: -> + @classCodeQueryVar = utils.getQueryVariable('_cc', false) @courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance}) @courseInstances.comparator = (ci) -> return ci.get('classroomID') + ci.get('courseID') @listenToOnce @courseInstances, 'sync', @onCourseInstancesLoaded @@ -55,6 +58,7 @@ module.exports = class CoursesView extends RootView @supermodel.loadModel(@hero, 'hero') @listenTo @hero, 'all', -> @render() + window.tracker?.trackEvent 'Students Loaded', category: 'Students', ['Mixpanel'] onCourseInstancesLoaded: -> map = {} @@ -76,20 +80,22 @@ module.exports = class CoursesView extends RootView onLoaded: -> super() - if utils.getQueryVariable('_cc', false) and not me.isAnonymous() + if @classCodeQueryVar and not me.isAnonymous() + window.tracker?.trackEvent 'Students Join Class Link', category: 'Students', classCode: @classCodeQueryVar, ['Mixpanel'] @joinClass() onClickLogInButton: -> modal = new AuthModal() @openModalView(modal) - application.tracker?.trackEvent 'Started Student Login', category: 'Courses' + window.tracker?.trackEvent 'Students Login Started', category: 'Students', ['Mixpanel'] openSignUpModal: -> + window.tracker?.trackEvent 'Students Signup Started', category: 'Students', ['Mixpanel'] modal = new CreateAccountModal({ initialValues: { classCode: utils.getQueryVariable('_cc', "") } }) @openModalView(modal) - application.tracker?.trackEvent 'Started Student Signup', category: 'Courses' onClickChangeHeroButton: -> + window.tracker?.trackEvent 'Students Change Hero Started', category: 'Students', ['Mixpanel'] modal = new HeroSelectModal({ currentHeroID: @hero.id }) @openModalView(modal) @listenTo modal, 'hero-select:success', (newHero) => @@ -101,16 +107,20 @@ module.exports = class CoursesView extends RootView onSubmitJoinClassForm: (e) -> e.preventDefault() + classCode = @$('#class-code-input').val() or @classCodeQueryVar + window.tracker?.trackEvent 'Students Join Class With Code', category: 'Students', classCode: classCode, ['Mixpanel'] @joinClass() onClickJoinClassButton: (e) -> + classCode = @$('#class-code-input').val() or @classCodeQueryVar + window.tracker?.trackEvent 'Students Join Class With Code', category: 'Students', classCode: classCode, ['Mixpanel'] @joinClass() joinClass: -> return if @state @state = 'enrolling' @errorMessage = null - @classCode = @$('#class-code-input').val() or utils.getQueryVariable('_cc', false) + @classCode = @$('#class-code-input').val() or @classCodeQueryVar if not @classCode @state = null @errorMessage = 'Please enter a code.' @@ -133,7 +143,6 @@ module.exports = class CoursesView extends RootView onJoinClassroomError: (classroom, jqxhr, options) -> @state = null - application.tracker?.trackEvent 'Failed to join classroom with code', category: 'Courses', status: jqxhr.status if jqxhr.status is 422 @errorMessage = 'Please enter a code.' else if jqxhr.status is 404 @@ -161,9 +170,19 @@ module.exports = class CoursesView extends RootView # TODO: Smoother system for joining a classroom and course instances, without requiring page reload, # and showing which class was just joined. document.location.search = '' # Using document.location.reload() causes an infinite loop of reloading - - onClickChangeLanguageLink: -> - application.tracker?.trackEvent 'Student clicked change language', category: 'Courses' - modal = new ChangeCourseLanguageModal() - @openModalView(modal) - modal.once 'hidden', @render, @ + + onClickPlay: (e) -> + levelSlug = $(e.currentTarget).data('level-slug') + window.tracker?.trackEvent $(e.currentTarget).data('event-action'), category: 'Students', levelSlug: levelSlug, ['Mixpanel'] + application.router.navigate($(e.currentTarget).data('href'), { trigger: true }) + + onClickViewClass: (e) -> + classroomID = $(e.target).data('classroom-id') + window.tracker?.trackEvent 'Students View Class', category: 'Students', classroomID: classroomID, ['Mixpanel'] + application.router.navigate("/courses/#{classroomID}", { trigger: true }) + + onClickViewLevels: (e) -> + courseID = $(e.target).data('course-id') + courseInstanceID = $(e.target).data('courseinstance-id') + window.tracker?.trackEvent 'Students View Levels', category: 'Students', courseID: courseID, courseInstanceID: courseInstanceID, ['Mixpanel'] + application.router.navigate("/courses/#{courseID}/#{courseInstanceID}", { trigger: true }) diff --git a/app/views/courses/EnrollmentsView.coffee b/app/views/courses/EnrollmentsView.coffee index db77c870c..bcc3b3596 100644 --- a/app/views/courses/EnrollmentsView.coffee +++ b/app/views/courses/EnrollmentsView.coffee @@ -21,7 +21,7 @@ module.exports = class EnrollmentsView extends RootView getTitle: -> return $.i18n.t('teacher.enrollments') - initialize: -> + initialize: (options) -> @state = new State({ totalEnrolled: 0 totalNotEnrolled: 0 @@ -34,6 +34,8 @@ module.exports = class EnrollmentsView extends RootView 'pending': [] } }) + window.tracker?.trackEvent 'Classes Licenses Loaded', category: 'Teachers', ['Mixpanel'] + super(options) @courses = new Courses() @supermodel.trackRequest @courses.fetch({data: { project: 'free' }}) @@ -58,31 +60,31 @@ module.exports = class EnrollmentsView extends RootView @calculateEnrollmentStats() @state.set('totalCourses', @courses.size()) super() - + updatePrepaidGroups: -> @state.set('prepaidGroups', @prepaids.groupBy((p) -> p.status())) calculateEnrollmentStats: -> @removeDeletedStudents() - + # sort users into enrolled, not enrolled groups = @members.groupBy (m) -> m.isEnrolled() enrolledUsers = new Users(groups.true) @notEnrolledUsers = new Users(groups.false) - map = {} - + map = {} + for classroom in @classrooms.models map[classroom.id] = _.countBy(classroom.get('members'), (userID) -> enrolledUsers.get(userID)?).false - + @state.set({ totalEnrolled: enrolledUsers.size() totalNotEnrolled: @notEnrolledUsers.size() classroomNotEnrolledMap: map }) - + true - + removeDeletedStudents: (e) -> for classroom in @classrooms.models _.remove(classroom.get('members'), (memberID) => @@ -94,8 +96,9 @@ module.exports = class EnrollmentsView extends RootView @openModalView(new HowToEnrollModal()) onClickContactUsButton: -> + window.tracker?.trackEvent 'Classes Licenses Contact Us', category: 'Teachers', enrollmentsNeeded: @state.get('numberOfStudents'), ['Mixpanel'] @openModalView(new TeachersContactModal({ enrollmentsNeeded: @state.get('numberOfStudents') })) - + onInputStudentsInput: -> input = @$('#students-input').val() if input isnt "" and (parseFloat(input) isnt parseInt(input) or _.isNaN parseInt(input)) @@ -106,6 +109,7 @@ module.exports = class EnrollmentsView extends RootView numberOfStudentsIsValid: -> 0 < @get('numberOfStudents') < 100000 onClickEnrollStudentsButton: -> + window.tracker?.trackEvent 'Classes Licenses Enroll Students', category: 'Teachers', ['Mixpanel'] modal = new ActivateLicensesModal({ selectedUsers: @notEnrolledUsers, users: @members }) @openModalView(modal) modal.once 'hidden', => diff --git a/app/views/courses/RestrictedToStudentsView.coffee b/app/views/courses/RestrictedToStudentsView.coffee index d607f538a..276b39d32 100644 --- a/app/views/courses/RestrictedToStudentsView.coffee +++ b/app/views/courses/RestrictedToStudentsView.coffee @@ -2,4 +2,7 @@ RootView = require 'views/core/RootView' module.exports = class RestrictedToStudentsView extends RootView id: 'restricted-to-students-view' - template: require 'templates/courses/restricted-to-students-view' \ No newline at end of file + template: require 'templates/courses/restricted-to-students-view' + + initialize: -> + window.tracker?.trackEvent 'Restricted To Students Loaded', category: 'Students', ['Mixpanel'] diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee index 906337f42..f5978af3f 100644 --- a/app/views/courses/TeacherClassView.coffee +++ b/app/views/courses/TeacherClassView.coffee @@ -120,6 +120,7 @@ module.exports = class TeacherClassView extends RootView @supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts'}}) @attachMediatorEvents() + window.tracker?.trackEvent 'Teachers Class Loaded', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel'] attachMediatorEvents: () -> # Model/Collection events @@ -211,31 +212,35 @@ module.exports = class TeacherClassView extends RootView window.location.hash = hash onClickCopyCodeButton: -> + window.tracker?.trackEvent 'Teachers Class Copy Class Code', category: 'Teachers', classroomID: @classroom.id, classCode: @state.get('classCode'), ['Mixpanel'] @$('#join-code-input').val(@state.get('classCode')).select() @tryCopy() onClickCopyURLButton: -> + window.tracker?.trackEvent 'Teachers Class Copy Class URL', category: 'Teachers', classroomID: @classroom.id, url: @state.get('joinURL'), ['Mixpanel'] @$('#join-url-input').val(@state.get('joinURL')).select() @tryCopy() tryCopy: -> try document.execCommand('copy') - application.tracker?.trackEvent 'Classroom copy URL', category: 'Courses', classroomID: @classroom.id, url: @state.joinURL catch err message = 'Oops, unable to copy' noty text: message, layout: 'topCenter', type: 'error', killer: false onClickUnarchive: -> + window.tracker?.trackEvent 'Teachers Class Unarchive', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel'] @classroom.save { archived: false } onClickEditClassroom: (e) -> + window.tracker?.trackEvent 'Teachers Class Edit Class Started', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel'] classroom = @classroom modal = new ClassroomSettingsModal({ classroom: classroom }) @openModalView(modal) @listenToOnce modal, 'hide', @render onClickEditStudentLink: (e) -> + window.tracker?.trackEvent 'Teachers Class Students Edit', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel'] user = @students.get($(e.currentTarget).data('student-id')) modal = new EditStudentModal({ user, @classroom }) @openModalView(modal) @@ -252,9 +257,10 @@ module.exports = class TeacherClassView extends RootView onStudentRemoved: (e) -> @students.remove(e.user) - application.tracker?.trackEvent 'Classroom removed student', category: 'Courses', classroomID: @classroom.id, userID: e.user.id + window.tracker?.trackEvent 'Teachers Class Students Removed', category: 'Teachers', classroomID: @classroom.id, userID: e.user.id, ['Mixpanel'] onClickAddStudents: (e) => + window.tracker?.trackEvent 'Teachers Class Add Students', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel'] modal = new InviteToClassroomModal({ classroom: @classroom }) @openModalView(modal) @listenToOnce modal, 'hide', @render @@ -294,14 +300,14 @@ module.exports = class TeacherClassView extends RootView user = @students.get(userID) selectedUsers = new Users([user]) @enrollStudents(selectedUsers) - + window.tracker?.trackEvent $(e.currentTarget).data('event-action'), category: 'Teachers', classroomID: @classroom.id, userID: userID, ['Mixpanel'] + onClickBulkEnroll: -> - courseID = @$('.bulk-course-select').val() - courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id }) userIDs = @getSelectedStudentIDs().toArray() selectedUsers = new Users(@students.get(userID) for userID in userIDs) @enrollStudents(selectedUsers) - + window.tracker?.trackEvent 'Teachers Class Students Enroll Selected', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel'] + enrollStudents: (selectedUsers) -> modal = new ActivateLicensesModal { @classroom, selectedUsers, users: @students } @openModalView(modal) @@ -311,10 +317,10 @@ module.exports = class TeacherClassView extends RootView if user user.set(newUser.attributes) null - application.tracker?.trackEvent 'Classroom started enroll students', category: 'Courses' onClickExportStudentProgress: -> # TODO: Does not yield .csv download on Safari, and instead opens a new tab with the .csv contents + window.tracker?.trackEvent 'Teachers Class Export CSV', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel'] csvContent = "data:text/csv;charset=utf-8,Username, Email, Playtime, Concepts\n" for student in @students.models concepts = [] @@ -336,15 +342,14 @@ module.exports = class TeacherClassView extends RootView encodedUri = encodeURI(csvContent) window.open(encodedUri) - onClickAssignStudentButton: (e) -> userID = $(e.currentTarget).data('user-id') user = @students.get(userID) members = [userID] courseID = $(e.currentTarget).data('course-id') - @assignCourse courseID, members - + window.tracker?.trackEvent 'Teachers Class Students Assign Selected', category: 'Teachers', classroomID: @classroom.id, courseID: courseID, userID: userID, ['Mixpanel'] + onClickBulkAssign: -> courseID = @$('.bulk-course-select').val() selectedIDs = @getSelectedStudentIDs() @@ -352,16 +357,13 @@ module.exports = class TeacherClassView extends RootView user = @students.get(userID) user.isEnrolled() ).toArray() - assigningToUnenrolled = _.any selectedIDs, (userID) => not @students.get(userID).isEnrolled() - assigningToNobody = selectedIDs.length is 0 - @state.set errors: { assigningToNobody, assigningToUnenrolled } - @assignCourse courseID, members - + window.tracker?.trackEvent 'Teachers Class Students Assign Selected', category: 'Teachers', classroomID: @classroom.id, courseID: courseID, ['Mixpanel'] + # TODO: Move this to the model. Use promises/callbacks? assignCourse: (courseID, members) -> courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id }) diff --git a/app/views/courses/TeacherClassesView.coffee b/app/views/courses/TeacherClassesView.coffee index 63f196e14..b281a2ce0 100644 --- a/app/views/courses/TeacherClassesView.coffee +++ b/app/views/courses/TeacherClassesView.coffee @@ -17,13 +17,16 @@ helper = require 'lib/coursesHelper' module.exports = class TeacherClassesView extends RootView id: 'teacher-classes-view' template: template - + events: 'click .edit-classroom': 'onClickEditClassroom' 'click .archive-classroom': 'onClickArchiveClassroom' 'click .unarchive-classroom': 'onClickUnarchiveClassroom' 'click .add-students-btn': 'onClickAddStudentsButton' 'click .create-classroom-btn': 'onClickCreateClassroomButton' + 'click .create-teacher-btn': 'onClickCreateTeacherButton' + 'click .update-teacher-btn': 'onClickUpdateTeacherButton' + 'click .view-class-btn': 'onClickViewClassButton' getTitle: -> return $.i18n.t('teacher.my_classes') @@ -38,18 +41,19 @@ module.exports = class TeacherClassesView extends RootView jqxhrs = classroom.sessions.fetchForAllClassroomMembers(classroom) if jqxhrs.length > 0 @supermodel.trackCollection(classroom.sessions) - + window.tracker?.trackEvent 'Teachers Classes Loaded', category: 'Teachers', ['Mixpanel'] + @courses = new Courses() @courses.fetch() @supermodel.trackCollection(@courses) - + @courseInstances = new CourseInstances() @courseInstances.fetchByOwner(me.id) @supermodel.trackCollection(@courseInstances) @progressDotTemplate = require 'templates/teachers/hovers/progress-dot-whole-course' - + # Level Sessions loaded after onLoaded to prevent race condition in calculateDots - + afterRender: -> super() $('.progress-dot').each (i, el) -> @@ -58,52 +62,71 @@ module.exports = class TeacherClassesView extends RootView html: true container: dot }) - + onLoaded: -> helper.calculateDots(@classrooms, @courses, @courseInstances) super() - + onClickEditClassroom: (e) -> classroomID = $(e.target).data('classroom-id') + window.tracker?.trackEvent $(e.target).data('event-action'), category: 'Teachers', classroomID: classroomID, ['Mixpanel'] classroom = @classrooms.get(classroomID) modal = new ClassroomSettingsModal({ classroom: classroom }) @openModalView(modal) @listenToOnce modal, 'hide', @render onClickCreateClassroomButton: (e) -> + window.tracker?.trackEvent 'Teachers Classes Create New Class Started', category: 'Teachers', ['Mixpanel'] classroom = new Classroom({ ownerID: me.id }) modal = new ClassroomSettingsModal({ classroom: classroom }) @openModalView(modal) @listenToOnce modal.classroom, 'sync', -> + window.tracker?.trackEvent 'Teachers Classes Create New Class Finished', category: 'Teachers', ['Mixpanel'] @classrooms.add(modal.classroom) @addFreeCourseInstances() @render() - + + onClickCreateTeacherButton: (e) -> + window.tracker?.trackEvent $(e.target).data('event-action'), category: 'Teachers', ['Mixpanel'] + application.router.navigate("/teachers/signup", { trigger: true }) + + onClickUpdateTeacherButton: (e) -> + window.tracker?.trackEvent $(e.target).data('event-action'), category: 'Teachers', ['Mixpanel'] + application.router.navigate("/teachers/update-account", { trigger: true }) + onClickAddStudentsButton: (e) -> + window.tracker?.trackEvent 'Teachers Classes Add Students Started', category: 'Teachers', ['Mixpanel'] classroomID = $(e.currentTarget).data('classroom-id') classroom = @classrooms.get(classroomID) modal = new InviteToClassroomModal({ classroom: classroom }) @openModalView(modal) @listenToOnce modal, 'hide', @render - + onClickArchiveClassroom: (e) -> classroomID = $(e.currentTarget).data('classroom-id') classroom = @classrooms.get(classroomID) classroom.set('archived', true) classroom.save {}, { success: => + window.tracker?.trackEvent 'Teachers Classes Archived Class', category: 'Teachers', ['Mixpanel'] @render() } - + onClickUnarchiveClassroom: (e) -> classroomID = $(e.currentTarget).data('classroom-id') classroom = @classrooms.get(classroomID) classroom.set('archived', false) classroom.save {}, { success: => + window.tracker?.trackEvent 'Teachers Classes Unarchived Class', category: 'Teachers', ['Mixpanel'] @render() } - + + onClickViewClassButton: (e) -> + classroomID = $(e.target).data('classroom-id') + window.tracker?.trackEvent $(e.target).data('event-action'), category: 'Teachers', classroomID: classroomID, ['Mixpanel'] + application.router.navigate("/teachers/classes/#{classroomID}", { trigger: true }) + addFreeCourseInstances: -> # so that when students join the classroom, they can automatically get free courses # non-free courses are generated when the teacher first adds a student to them diff --git a/app/views/courses/TeacherCoursesView.coffee b/app/views/courses/TeacherCoursesView.coffee index 14ca396fe..018326881 100644 --- a/app/views/courses/TeacherCoursesView.coffee +++ b/app/views/courses/TeacherCoursesView.coffee @@ -1,4 +1,3 @@ -ActivateLicensesModal = require 'views/courses/ActivateLicensesModal' app = require 'core/application' CocoCollection = require 'collections/CocoCollection' CocoModel = require 'models/CocoModel' @@ -6,24 +5,19 @@ Course = require 'models/Course' Campaigns = require 'collections/Campaigns' Classroom = require 'models/Classroom' Classrooms = require 'collections/Classrooms' -InviteToClassroomModal = require 'views/courses/InviteToClassroomModal' User = require 'models/User' CourseInstance = require 'models/CourseInstance' RootView = require 'views/core/RootView' template = require 'templates/courses/teacher-courses-view' -ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal' module.exports = class TeacherCoursesView extends RootView id: 'teacher-courses-view' template: template events: - 'click #activate-licenses-btn': 'onClickActivateLicensesButton' - 'click .btn-add-students': 'onClickAddStudents' - 'click .create-new-class': 'onClickCreateNewClassButton' - 'click .edit-classroom-small': 'onClickEditClassroomSmall' + 'click .guide-btn': 'onClickGuideButton' 'click .play-level-button': 'onClickPlayLevel' - + guideLinks: { "560f1a9f22961295f9427742": @@ -48,91 +42,25 @@ module.exports = class TeacherCoursesView extends RootView @supermodel.trackCollection(@ownedClassrooms) @courses = new CocoCollection([], { url: "/db/course", model: Course}) @supermodel.loadCollection(@courses, 'courses') - @classrooms = new CocoCollection([], { url: "/db/classroom", model: Classroom }) - @classrooms.comparator = '_id' - @listenToOnce @classrooms, 'sync', @onceClassroomsSync - @supermodel.loadCollection(@classrooms, 'classrooms', {data: {ownerID: me.id}}) @campaigns = new Campaigns() @supermodel.trackRequest @campaigns.fetchByType('course', { data: { project: 'levels,levelsUpdated' } }) - @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') - @supermodel.loadCollection(@courseInstances, 'course_instances', {data: {ownerID: me.id}}) - @members = new CocoCollection([], { model: User }) - @listenTo @members, 'sync', @render @ - onceClassroomsSync: -> - for classroom in @classrooms.models - @members.fetch({ - remove: false - url: "/db/classroom/#{classroom.id}/members" - }) + initialize: (options) -> + window.tracker?.trackEvent 'Classes Guides Loaded', category: 'Teachers', ['Mixpanel'] + super(options) - onClickActivateLicensesButton: -> - modal = new ActivateLicensesModal({ - users: @members - }) - @openModalView(modal) - modal.once 'redeem-users', -> document.location.reload() - application.tracker?.trackEvent 'Courses teachers started enroll students', category: 'Courses' + onClickGuideButton: (e) -> + courseID = $(e.currentTarget).data('course-id') + courseName = $(e.currentTarget).data('course-name') + eventAction = $(e.currentTarget).data('event-action') + window.tracker?.trackEvent eventAction, category: 'Teachers', courseID: courseID, courseName: courseName, ['Mixpanel'] - onClickAddStudents: (e) -> - classroomID = $(e.target).data('classroom-id') - classroom = @classrooms.get(classroomID) - unless classroom - console.error 'No classroom ID found.' - return - modal = new InviteToClassroomModal({classroom: classroom}) - @openModalView(modal) - application.tracker?.trackEvent 'Classroom started add students', category: 'Courses', classroomID: classroom.id - - onClickCreateNewClassButton: -> - return application.router.navigate('/teachers/signup', {trigger: true}) if me.get('anonymous') - modal = new ClassroomSettingsModal({}) - @openModalView(modal) - @listenToOnce modal, 'hide', => - # TODO: how to get new classroom from modal? - @classrooms.add(modal.classroom) - # TODO: will this definitely fire after modal saves new classroom? - @listenToOnce modal.classroom, 'sync', -> - @addFreeCourseInstances() - @render() - - onClickEditClassroomSmall: (e) -> - classroomID = $(e.target).data('classroom-id') - classroom = @classrooms.get(classroomID) - 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() + window.tracker?.trackEvent 'Classes Guides Play Level', category: 'Teachers', courseID: courseID, language: language, levelSlug: levelSlug, ['Mixpanel'] url = "/play/level/#{levelSlug}?course=#{courseID}&codeLanguage=#{language}" application.router.navigate(url, { trigger: true }) - - onLoaded: -> - super() - @addFreeCourseInstances() - - addFreeCourseInstances: -> - # so that when students join the classroom, they can automatically get free courses - # non-free courses are generated when the teacher first adds a student to them - for classroom in @classrooms.models - for course in @courses.models - continue if not course.get('free') - courseInstance = @courseInstances.findWhere({classroomID: classroom.id, courseID: course.id}) - if not courseInstance - courseInstance = new CourseInstance({ - classroomID: classroom.id - courseID: course.id - }) - # TODO: figure out a better way to get around triggering validation errors for properties - # that the server will end up filling in, like an empty members array, ownerID - courseInstance.save(null, {validate: false}) - @courseInstances.add(courseInstance) - @listenToOnce courseInstance, 'sync', @addFreeCourseInstances - return diff --git a/app/views/play/level/ControlBarView.coffee b/app/views/play/level/ControlBarView.coffee index 515a20f2b..32d3b7e2f 100644 --- a/app/views/play/level/ControlBarView.coffee +++ b/app/views/play/level/ControlBarView.coffee @@ -34,7 +34,8 @@ module.exports = class ControlBarView extends CocoView @worldName = options.worldName @session = options.session @level = options.level - @levelID = @level.get('slug') or @level.id + @levelSlug = @level.get('slug') + @levelID = @levelSlug or @level.id @spectateGame = options.spectateGame ? false @observing = options.session.get('creator') isnt me.id super options @@ -121,6 +122,9 @@ module.exports = class ControlBarView extends CocoView @setupManager.open() onClickHome: (e) -> + if @level.get('type', true) in ['course'] + category = if me.isTeacher() then 'Teachers' else 'Students' + window.tracker?.trackEvent 'Play Level Back To Levels', category: category, levelSlug: @levelSlug, ['Mixpanel'] e.preventDefault() e.stopImmediatePropagation() Backbone.Mediator.publish 'router:navigate', route: @homeLink, viewClass: @homeViewClass, viewArgs: @homeViewArgs diff --git a/app/views/play/level/modal/CourseVictoryModal.coffee b/app/views/play/level/modal/CourseVictoryModal.coffee index 6694718f8..47ec3eb84 100644 --- a/app/views/play/level/modal/CourseVictoryModal.coffee +++ b/app/views/play/level/modal/CourseVictoryModal.coffee @@ -56,6 +56,7 @@ module.exports = class CourseVictoryModal extends ModalView @levelSessions = @supermodel.loadCollection(@levelSessions, 'sessions', { data: { project: 'state.complete level.original playtime changed' } }).model + window.tracker?.trackEvent 'Play Level Victory Modal Loaded', category: 'Students', levelSlug: @level.get('slug'), ['Mixpanel'] onResourceLoadFailed: (e) -> if e.resource.jqxhr is @nextLevelRequest @@ -158,6 +159,7 @@ module.exports = class CourseVictoryModal extends ModalView @showView(@views[index+1]) onNextLevel: -> + window.tracker?.trackEvent 'Play Level Victory Modal Next Level', category: 'Students', levelSlug: @level.get('slug'), nextLevelSlug: @nextLevel.get('slug'), ['Mixpanel'] if me.isSessionless() link = "/play/level/#{@nextLevel.get('slug')}?course=#{@courseID}&codeLanguage=#{utils.getQueryVariable('codeLanguage', 'python')}" else @@ -165,6 +167,7 @@ module.exports = class CourseVictoryModal extends ModalView application.router.navigate(link, {trigger: true}) onDone: -> + window.tracker?.trackEvent 'Play Level Victory Modal Done', category: 'Students', levelSlug: @level.get('slug'), ['Mixpanel'] if me.isSessionless() link = "/teachers/courses" else diff --git a/app/views/teachers/ConvertToTeacherAccountView.coffee b/app/views/teachers/ConvertToTeacherAccountView.coffee index 096ac7607..ad3e3b7f5 100644 --- a/app/views/teachers/ConvertToTeacherAccountView.coffee +++ b/app/views/teachers/ConvertToTeacherAccountView.coffee @@ -33,6 +33,7 @@ module.exports = class ConvertToTeacherAccountView extends RootView @trialRequests = new TrialRequests() @trialRequests.fetchOwn() @supermodel.trackCollection(@trialRequests) + window.tracker?.trackEvent 'Teachers Convert Account Loaded', category: 'Teachers', ['Mixpanel'] onLeaveMessage: -> if @formChanged @@ -87,6 +88,8 @@ module.exports = class ConvertToTeacherAccountView extends RootView @onChangeForm() onChangeForm: -> + unless @formChanged + window.tracker?.trackEvent 'Teachers Convert Account Form Started', category: 'Teachers', ['Mixpanel'] @formChanged = true onSubmitForm: (e) -> @@ -141,6 +144,7 @@ module.exports = class ConvertToTeacherAccountView extends RootView errors.showNotyNetworkError(arguments...) onTrialRequestSubmit: -> + window.tracker?.trackEvent 'Teachers Convert Account Submitted', category: 'Teachers', ['Mixpanel'] @formChanged = false me.setRole @trialRequest.get('properties').role.toLowerCase(), true application.router.navigate('/teachers/classes', {trigger: true}) diff --git a/app/views/teachers/CreateTeacherAccountView.coffee b/app/views/teachers/CreateTeacherAccountView.coffee index 59be15e5a..b0107e08c 100644 --- a/app/views/teachers/CreateTeacherAccountView.coffee +++ b/app/views/teachers/CreateTeacherAccountView.coffee @@ -31,6 +31,7 @@ module.exports = class CreateTeacherAccountView extends RootView @trialRequests = new TrialRequests() @trialRequests.fetchOwn() @supermodel.trackCollection(@trialRequests) + window.tracker?.trackEvent 'Teachers Create Account Loaded', category: 'Teachers', ['Mixpanel'] onLeaveMessage: -> if @formChanged @@ -39,8 +40,6 @@ module.exports = class CreateTeacherAccountView extends RootView onLoaded: -> if @trialRequests.size() @trialRequest = @trialRequests.first() - if @trialRequest and @trialRequest.get('status') isnt 'submitted' and @trialRequest.get('status') isnt 'approved' - window.tracker?.trackEvent 'View Trial Request', category: 'Teachers', label: 'View Trial Request', ['Mixpanel'] super() invalidateNCES: -> @@ -90,6 +89,8 @@ module.exports = class CreateTeacherAccountView extends RootView @openModalView(modal) onChangeForm: -> + unless @formChanged + window.tracker?.trackEvent 'Teachers Create Account Form Started', category: 'Teachers', ['Mixpanel'] @formChanged = true onSubmitForm: (e) -> @@ -158,6 +159,7 @@ module.exports = class CreateTeacherAccountView extends RootView @openModalView(modal) onTrialRequestSubmit: -> + window.tracker?.trackEvent 'Teachers Create Account Submitted', category: 'Teachers', ['Mixpanel'] @formChanged = false attrs = _.pick(forms.formToObject(@$('form')), 'name', 'email', 'role') attrs.role = attrs.role.toLowerCase() diff --git a/app/views/teachers/RequestQuoteView.coffee b/app/views/teachers/RequestQuoteView.coffee index bb18ce945..b48a9d355 100644 --- a/app/views/teachers/RequestQuoteView.coffee +++ b/app/views/teachers/RequestQuoteView.coffee @@ -34,6 +34,7 @@ module.exports = class RequestQuoteView extends RootView @trialRequests.fetchOwn() @supermodel.trackCollection(@trialRequests) @formChanged = false + window.tracker?.trackEvent 'Teachers Request Demo Loaded', category: 'Teachers', ['Mixpanel'] onLeaveMessage: -> if @formChanged @@ -42,8 +43,6 @@ module.exports = class RequestQuoteView extends RootView onLoaded: -> if @trialRequests.size() @trialRequest = @trialRequests.first() - if @trialRequest and @trialRequest.get('status') isnt 'submitted' and @trialRequest.get('status') isnt 'approved' - window.tracker?.trackEvent 'View Trial Request', category: 'Teachers', label: 'View Trial Request', ['Mixpanel'] super() invalidateNCES: -> @@ -89,6 +88,8 @@ module.exports = class RequestQuoteView extends RootView @onChangeRequestForm() onChangeRequestForm: -> + unless @formChanged + window.tracker?.trackEvent 'Teachers Request Demo Form Started', category: 'Teachers', ['Mixpanel'] @formChanged = true onSubmitRequestForm: (e) -> @@ -161,6 +162,7 @@ module.exports = class RequestQuoteView extends RootView @openModalView(modal) onTrialRequestSubmit: -> + window.tracker?.trackEvent 'Teachers Request Demo Form Submitted', category: 'Teachers', ['Mixpanel'] @formChanged = false me.setRole @trialRequest.get('properties').role.toLowerCase(), true defaultName = [@trialRequest.get('firstName'), @trialRequest.get('lastName')].join(' ') @@ -168,7 +170,6 @@ module.exports = class RequestQuoteView extends RootView @$('#request-form, #form-submit-success').toggleClass('hide') @scrollToTop(0) $('#flying-focus').css({top: 0, left: 0}) # Hack copied from Router.coffee#187. Ideally we'd swap out the view and have view-swapping logic handle this - window.tracker?.trackEvent 'Submit Trial Request', category: 'Teachers', label: 'Trial Request', ['Mixpanel'] onClickGPlusSignupButton: -> btn = @$('#gplus-signup-btn') @@ -190,6 +191,7 @@ module.exports = class RequestQuoteView extends RootView url: "/db/user?gplusID=#{gplusAttrs.gplusID}&gplusAccessToken=#{application.gplusHandler.token()}" type: 'PUT' success: -> + window.tracker?.trackEvent 'Teachers Request Demo Create Account Google', category: 'Teachers', ['Mixpanel'] application.router.navigate(SIGNUP_REDIRECT) window.location.reload() error: errors.showNotyNetworkError @@ -218,6 +220,7 @@ module.exports = class RequestQuoteView extends RootView url: "/db/user?facebookID=#{facebookAttrs.facebookID}&facebookAccessToken=#{application.facebookHandler.token()}" type: 'PUT' success: -> + window.tracker?.trackEvent 'Teachers Request Demo Create Account Facebook', category: 'Teachers', ['Mixpanel'] application.router.navigate(SIGNUP_REDIRECT) window.location.reload() error: errors.showNotyNetworkError @@ -250,13 +253,12 @@ module.exports = class RequestQuoteView extends RootView }) me.save(null, { success: -> + window.tracker?.trackEvent 'Teachers Request Demo Create Account', category: 'Teachers', ['Mixpanel'] application.router.navigate(SIGNUP_REDIRECT) window.location.reload() error: errors.showNotyNetworkError }) - - requestFormSchemaAnonymous = { type: 'object' required: [ diff --git a/app/views/teachers/RestrictedToTeachersView.coffee b/app/views/teachers/RestrictedToTeachersView.coffee index add4f4796..9b4a6014e 100644 --- a/app/views/teachers/RestrictedToTeachersView.coffee +++ b/app/views/teachers/RestrictedToTeachersView.coffee @@ -2,4 +2,7 @@ RootView = require 'views/core/RootView' module.exports = class RestrictedToTeachersView extends RootView id: 'restricted-to-teachers-view' - template: require 'templates/teachers/restricted-to-teachers-view' \ No newline at end of file + template: require 'templates/teachers/restricted-to-teachers-view' + + initialize: -> + window.tracker?.trackEvent 'Restricted To Teachers Loaded', category: 'Students', ['Mixpanel'] From d5cbe1e11a9aec1bbb76adf58e1e8c9e33a1613d Mon Sep 17 00:00:00 2001 From: dacha76 Date: Thu, 9 Jun 2016 15:07:13 -0400 Subject: [PATCH 137/167] translated "new_home" section to french. (#3728) translated "new_home" section to french. --- app/locale/fr.coffee | 157 +++++++++++++++++++++---------------------- 1 file changed, 78 insertions(+), 79 deletions(-) diff --git a/app/locale/fr.coffee b/app/locale/fr.coffee index 9230b62cb..f4c581368 100644 --- a/app/locale/fr.coffee +++ b/app/locale/fr.coffee @@ -14,85 +14,84 @@ module.exports = nativeDescription: "français", englishDescription: "French", t for_developers: "Pour développeurs" # Not currently shown on home page. or_ipad: "Ou télécharger pour iPad" -# new_home: -# slogan: "The most engaging game for learning programming." -# classroom_edition: "Classroom Edition:" -# learn_to_code: "Learn to code:" -# teacher: "Teacher" -# student: "Student" -# play_now: "Play Now" -# im_a_teacher: "I'm a Teacher" -# im_a_student: "I'm a Student" -# learn_more: "Learn more" -# classroom_in_a_box: "A classroom in-a-box for teaching computer science." -# codecombat_is: "CodeCombat is a platform for students to learn computer science while playing through a real game." -# our_courses: "Our courses have been specifically playtested to excel in the classroom, even by teachers with little to no prior programming experience." -# top_screenshots_hint: "Students write code and see their changes update in real-time" -# designed_with: "Designed with teachers in mind" -# real_code: "Real, typed code" -# from_the_first_level: "from the first level" -# getting_students: "Getting students to typed code as quickly as possible is critical to learning programming syntax and proper structure." -# educator_resources: "Educator resources" -# course_guides: "and course guides" -# teaching_computer_science: "Teaching computer science does not require a costly degree, because we provide tools to support educators of all backgrounds." -# accessible_to: "Accessible to" -# everyone: "everyone" -# democratizing: "Democratizing the process of learning coding is at the core of our philosophy. Everyone should be able to learn to code." -# forgot_learning: "I think they actually forgot that they were actually learning something." -# wanted_to_do: " Coding is something I've always wanted to do, and I never thought I would be able to learn it in school." -# why_games: "Why is learning through games important?" -# games_reward: "Games reward the productive struggle." -# encourage: "Gaming is a medium that encourages interaction, discovery, and trial-and-error. A good game challenges the player to master skills over time, which is the same critical process students go through as they learn." -# excel: "Games excel at rewarding" -# struggle: "productive struggle" -# kind_of_struggle: "the kind of struggle that results in learning that’s engaging and" -# motivating: "motivating" -# not_tedious: "not tedious." -# gaming_is_good: "Studies suggest gaming is good for children’s brains. (it’s true!)" -# game_based: "When game-based learning systems are" -# compared: "compared" -# conventional: "against conventional assessment methods, the difference is clear: games are better at helping students retain knowledge, concentrate and" -# perform_at_higher_level: "perform at a higher level of achievement" -# feedback: "Games also provide real-time feedback that allows students to adjust their solution path and understand concepts more holistically, instead of being limited to just “correct” or “incorrect” answers." -# real_game: "A real game, played with real coding." -# great_game: "A great game is more than just badges and achievements - it’s about a player’s journey, well-designed puzzles, and the ability to tackle challenges with agency and confidence." -# agency: "CodeCombat is a game that gives players that agency and confidence with our robust typed code engine, which helps beginner and advanced students alike write proper, valid code." -# request_demo_title: "Get your students started today!" -# request_demo_subtitle: "Request a demo and get your students started in less than an hour." -# get_started_title: "Set up your class today" -# get_started_subtitle: "Set up a class, add your students, and monitor their progress as they learn computer science." -# request_demo: "Request a Demo" -# setup_a_class: "Set Up a Class" -# have_an_account: "Have an account?" -# logged_in_as: "You are currently logged in as" -# view_my_classes: "View my classes" -# computer_science: "Computer science courses for all ages" -# show_me_lesson_time: "Show me lesson time estimates for:" -# curriculum: "Total curriculum hours:" -# ffa: "Free for all students" -# lesson_time: "Lesson time:" -# coming_soon: "Coming soon!" -# courses_available_in: "Courses are available in JavaScript, Python, and Java (coming soon!)" -# boast: "Boasts riddles that are complex enough to fascinate gamers and coders alike." -# winning: "A winning combination of RPG gameplay and programming homework that pulls off making kid-friendly education legitimately enjoyable." -# run_class: "Everything you need to run a computer science class in your school today, no CS background required." -# teachers: "Teachers!" -# teachers_and_educators: "Teachers & Educators" -# class_in_box: "Learn how our classroom-in-a-box platform fits into your curriculum." -# get_started: "Get Started" -# students: "Students:" -# join_class: "Join Class" -# role: "Your role:" -# student_count: "Number of students:" -# start_playing_for_free: "Start Playing for Free!" -# students_and_players: "Students & Players" -# goto_classes: "Go to My Classes" -# view_profile: "View My Profile" -# view_progress: "View Progress" -# check_out_wiki: "Check out our new Educator Wiki" -# want_coco: "Want CodeCombat at your school?" -# form_select_role: "Select primary role" -# form_select_range: "Select class size" + new_home: + slogan: "Le jeu le plus engageant pour apprendre la programmation." + classroom_edition: "Édition Classe:" + learn_to_code: "Apprend à programmer:" + teacher: "Enseignant" + student: "Étudiant" + play_now: "Jouez Maintenant" + im_a_teacher: "Je suis un enseignant" + im_a_student: "Je suis un étudiant" + learn_more: "Pour en savoir plus" + classroom_in_a_box: "Une classe tout-inclus pour enseigner l'informatique." + codecombat_is: "CodeCombat est une plateforme pour les étudiants afin d'apprendre l'informatique tout en jouant." + our_courses: "Nos cours ont été testés spécifiquement pour excellé en classe, même si les enseignants ont peu ou pas d'expérience en programmation." + top_screenshots_hint: "Les étudiants écrivent du code et voient leurs changements s'appliqués en temps-réel" + designed_with: "Concu en pensant aux enseignants" + real_code: "Vrai code entré au clavier" + from_the_first_level: "à partir du premier niveau" + getting_students: "Encourager les étudiants à écrire du code aussi rapidement que possible est primordial afin qu'ils apprennent comment bien structurer et écrire un programme." + educator_resources: "Resources pour les enseignants" + course_guides: "et les guides de cours" + teaching_computer_science: "Enseigner l'informatique ne nécessite pas un diplôme dispendieux, parce que nous fournissons des outils afin d'aider les enseignants, peu importe leurs curriculums." + accessible_to: "Accessible à" + everyone: "tout le monde" + democratizing: "Démocratiser le processus d'apprendre à programmer est au coeur de notre philosophie. Tout le monde devrait être capable d'apprendre la programmation." + forgot_learning: "Je crois qu'ils ont oublié qu'ils étaient en train d'apprendre quelquechose." + wanted_to_do: " Programmer est quelquechose que j'ai toujours voulu faire, et je n'ai jamais pensé que je pourrais l'apprendre à l'école." + why_games: "Pourquoi est-ce qu'apprendre en jouant est important?" + games_reward: "Les jeux récompensent l'effort productif." + encourage: "Le jeu est une plateforme qui encourage l'interaction, la découverte et l'essaie et erreur. Un bon jeu défit le joeur d'apprendre à maîtriser de nouvelles habiletés au fil du temps, ce qui correspond au même processus critique que l'on retrouve au niveau de l'éducation. + excel: "Les jeux sont excellents afin de récompenser" + struggle: "l'effort productif" + kind_of_struggle: "le type d'effort qui permet d'apprendre, qui nous engage et" + motivating: "qui nous motive" + not_tedious: "non pas fastidieux." + gaming_is_good: "Des études suggèrent que jouer est bon pour le cerveau des enfants. (C'est vrai!)" + game_based: "Quand les systèmes d'apprentissage basés sur le jeu sont" + compared: "comparés" + conventional: "aux méthodes d'évaluation conventionnelles, la différence est claire: les jeux sont meilleurs afin d'aider les enfants à retenir ce qu'ils apprennent, à se concentrer et" + perform_at_higher_level: "à performer à un niveau supérieur d'accomplissement" + feedback: "Les jeux procurent aussi des résultats en temps réel qui permettent aux étudiants d'ajuster leur solution et de comprendre des concepts de façon plus holistique, au lieu d'être limité aux simples réponses “correct” ou “incorrect”." + real_game: "Un vrai jeu, joué en programmant pour de vrai." + great_game: "Un bon jeu c'est plus qu'une série d'insignes et d'accomplissements - c'est à propos de l'aventure d'un joueur, des énigmes bien conçues et la capacité d'entreprendre des défis avec confiance et détermination." + agency: "CodeCombat est un jeu qui donne aux joueurs cette confiance et détermination à l'aide de notre interpretteur de lignes de code, qui aide les étudiants débutants ainsi qu'avancés à concevoir des programmes bien écrits et valides." + request_demo_title: "Débutez avec vos étudiants aujourd'hui!" + request_demo_subtitle: "Demandez une démonstration et permettez à vos étudiants de démarrer en moins d'une heure." + get_started_title: "Mettez en place votre classe aujourd'hui" + get_started_subtitle: "Mettez en place une classe, ajoutez vos étudiants et surveillez leurs progrès au fur et à mesure qu'ils apprennent l'informatique." + request_demo: "Demandez une Démonstration" + setup_a_class: "Mettre en place une classe" + have_an_account: "Avez-vous un compte?" + logged_in_as: "Vous êtes présentement enregistré en tant que" + view_my_classes: "Voir Mes Classes" + computer_science: "Des cours d'informatique pour tous âges" + show_me_lesson_time: "Montrez-moi des estimés de temps de cours pour:" + curriculum: "Total des heures du curriculum:" + ffa: "Gratuis pour tous les étudiants" + lesson_time: "Temps de cours:" + coming_soon: "Bientôt disponible!" + courses_available_in: "Les cours sont disponibles en JavaScript, Python et Java (bientôt disponible!)" + boast: "Comprend des énigmes assez complexes pour fasciner autant les joueurs que les programmeurs." + winning: "Une combinaison gagnante d'élément de jeux de rôles et programmation qui permet aux enfants d'apprendre de façon légitimement plaisante." + run_class: "Tous ce dont vous avez besoin pour enseigner l'informaique dans votre classe aujourd'hui, sans expérience préalable en informatique." + teachers: "Enseignants!" + teachers_and_educators: "Enseignants et éducateurs" + class_in_box: "Apprenez comment notre plateforme classe-tout-inclus s'adapte à votre curriculum." + students: "Étudiants:" + join_class: "Joindre une classe" + role: "Votre rôle:" + student_count: "Nombre d'étudiants:" + start_playing_for_free: "Commencez à jouer gratuitement!" + students_and_players: "Étudiants et joueurs" + goto_classes: "Aller à Mes Classes" + view_profile: "Voir Mon Profil" + view_progress: "Voir Progrès" + check_out_wiki: "Visionner notre nouveau Wiki pour Éducateurs" + want_coco: "Voulez-vous CodeCombat à votre école?" + form_select_role: "Sélectionnez votre rôle principale" + form_select_range: "Sélectionnez la taille de votre classe" nav: play: "Jouer" # The top nav bar entry where players choose which levels to play From ad37fd34609aee68ea94b0d80aaf8b9cd0e563eb Mon Sep 17 00:00:00 2001 From: Imperadeiro98 Date: Thu, 9 Jun 2016 20:10:14 +0100 Subject: [PATCH 138/167] Fix fr.coffee --- app/locale/fr.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/locale/fr.coffee b/app/locale/fr.coffee index f4c581368..a4d27068c 100644 --- a/app/locale/fr.coffee +++ b/app/locale/fr.coffee @@ -42,7 +42,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t wanted_to_do: " Programmer est quelquechose que j'ai toujours voulu faire, et je n'ai jamais pensé que je pourrais l'apprendre à l'école." why_games: "Pourquoi est-ce qu'apprendre en jouant est important?" games_reward: "Les jeux récompensent l'effort productif." - encourage: "Le jeu est une plateforme qui encourage l'interaction, la découverte et l'essaie et erreur. Un bon jeu défit le joeur d'apprendre à maîtriser de nouvelles habiletés au fil du temps, ce qui correspond au même processus critique que l'on retrouve au niveau de l'éducation. + encourage: "Le jeu est une plateforme qui encourage l'interaction, la découverte et l'essaie et erreur. Un bon jeu défit le joeur d'apprendre à maîtriser de nouvelles habiletés au fil du temps, ce qui correspond au même processus critique que l'on retrouve au niveau de l'éducation." excel: "Les jeux sont excellents afin de récompenser" struggle: "l'effort productif" kind_of_struggle: "le type d'effort qui permet d'apprendre, qui nous engage et" @@ -79,6 +79,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t teachers: "Enseignants!" teachers_and_educators: "Enseignants et éducateurs" class_in_box: "Apprenez comment notre plateforme classe-tout-inclus s'adapte à votre curriculum." +# get_started: "Get Started" students: "Étudiants:" join_class: "Joindre une classe" role: "Votre rôle:" From aa7f2af700692ad0b4e0281ea37f3a735e32e3e7 Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 9 Jun 2016 12:13:17 -0700 Subject: [PATCH 139/167] No ads, act two. --- app/views/play/CampaignView.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index a484db9d9..931a5244c 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -265,6 +265,7 @@ module.exports = class CampaignView extends RootView @openModalView authModal showAds: -> + return false # No ads for now. if application.isProduction() && !me.isPremium() && !me.isTeacher() && !window.serverConfig.picoCTF return me.getCampaignAdsGroup() is 'leaderboard-ads' false From 8203727d609c45d62362d4337d209194b0471514 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Thu, 9 Jun 2016 14:35:19 -0700 Subject: [PATCH 140/167] Fix #3457 --- app/styles/play/campaign-view.sass | 1 + 1 file changed, 1 insertion(+) diff --git a/app/styles/play/campaign-view.sass b/app/styles/play/campaign-view.sass index fe6f38fbe..19d4bad14 100644 --- a/app/styles/play/campaign-view.sass +++ b/app/styles/play/campaign-view.sass @@ -204,6 +204,7 @@ $gameControlMargin: 30px .tooltip z-index: 2 + pointer-events: none .tooltip-arrow display: none From 55bd49fde0d844653053dd047fa9f86833769501 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 9 Jun 2016 14:44:43 -0700 Subject: [PATCH 141/167] Add course playtimes to student progress CSV --- app/views/courses/TeacherClassView.coffee | 52 ++++++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee index f5978af3f..d1e8c6f24 100644 --- a/app/views/courses/TeacherClassView.coffee +++ b/app/views/courses/TeacherClassView.coffee @@ -8,6 +8,7 @@ ActivateLicensesModal = require 'views/courses/ActivateLicensesModal' EditStudentModal = require 'views/teachers/EditStudentModal' RemoveStudentModal = require 'views/courses/RemoveStudentModal' +Campaigns = require 'collections/Campaigns' Classroom = require 'models/Classroom' Classrooms = require 'collections/Classrooms' Levels = require 'collections/Levels' @@ -110,12 +111,15 @@ module.exports = class TeacherClassView extends RootView return dir * diff if diff return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir) + @campaigns = new Campaigns() + @supermodel.trackRequest @campaigns.fetchByType('course', { data: { project: 'levels,slug,type' } }) + @courses = new Courses() @supermodel.trackRequest @courses.fetch() @courseInstances = new CourseInstances() @supermodel.trackRequest @courseInstances.fetchForClassroom(classroomID) - + @levels = new Levels() @supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts'}}) @@ -321,23 +325,59 @@ module.exports = class TeacherClassView extends RootView onClickExportStudentProgress: -> # TODO: Does not yield .csv download on Safari, and instead opens a new tab with the .csv contents window.tracker?.trackEvent 'Teachers Class Export CSV', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel'] - csvContent = "data:text/csv;charset=utf-8,Username, Email, Playtime, Concepts\n" + courseLabels = "" + courseOrder = [] + for course, index in @classroom.get('courses') + courseLabels += "CS#{index + 1} Playtime," + courseOrder.push(course._id) + csvContent = "data:text/csv;charset=utf-8,Username,Email,Total Playtime,#{courseLabels}Concepts\n" + campaignCourseMap = {} + courseMap = {} + for course in @courses.models + campaignCourseMap[course.get('campaignID')] = course + courseMap[course.id] = course + levelCourseMap = {} + for campaign in @campaigns.models + continue unless campaignCourseMap[campaign.id] + for levelID, level of campaign.get('levels') + levelCourseMap[levelID] = campaignCourseMap[campaign.id] for student in @students.models concepts = [] - for course, index in @courses.models + for course in @courses.models instance = @courseInstances.findWhere({ courseID: course.id, classroomID: @classroom.id }) if instance and instance.hasMember(student) # TODO: @levels collection is for the classroom, and not per-course - for level, index in @levels.models + for level in @levels.models progress = @state.get('progressData').get({ classroom: @classroom, course: course, level: level, user: student }) concepts.push(level.get('concepts') ? []) if progress?.completed concepts = _.union(_.flatten(concepts)) conceptsString = _.map(concepts, (c) -> $.i18n.t("concepts." + c)).join(', ') + coursePlaytimeMap = {} playtime = 0 for session in @classroom.sessions.models when session.get('creator') is student.id playtime += session.get('playtime') or 0 - playtimeString = moment.duration(playtime, 'seconds').humanize() - csvContent += "#{student.get('name')},#{student.get('email')},#{playtimeString},\"#{conceptsString}\"\n" + if courseID = levelCourseMap[session.get('level')?.original]?.id + coursePlaytimeMap[courseID] ?= 0 + coursePlaytimeMap[courseID] += session.get('playtime') or 0 + playtimeString = if playtime is 0 then "0" else moment.duration(playtime, 'seconds').humanize() + for course in @courses.models + coursePlaytimeMap[course.id] ?= 0 + coursePlaytimes = [] + for courseID, playtime of coursePlaytimeMap + coursePlaytimes.push + courseID: courseID + playtime: playtime + coursePlaytimes.sort (a, b) -> + return -1 if courseOrder.indexOf(a.courseID) < courseOrder.indexOf(b.courseID) + return 0 if courseOrder.indexOf(a.courseID) is courseOrder.indexOf(b.courseID) + return 1 + coursePlaytimesString = "" + for coursePlaytime, index in coursePlaytimes + if coursePlaytime.playtime is 0 + coursePlaytimesString += "0," + else + coursePlaytimesString += "#{moment.duration(coursePlaytime.playtime, 'seconds').humanize()}," + csvContent += "#{student.get('name')},#{student.get('email')},#{playtimeString},#{coursePlaytimesString}\"#{conceptsString}\"\n" csvContent = csvContent.substring(0, csvContent.length - 1) encodedUri = encodeURI(csvContent) window.open(encodedUri) From bd14e497078d2c71261472de5d0952f31a4343a4 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Thu, 9 Jun 2016 16:48:18 -0700 Subject: [PATCH 142/167] Fix #3701: load data properly for simulation again. --- app/lib/LevelLoader.coffee | 2 +- app/models/Level.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index a3a2725f6..d4f225a38 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -123,7 +123,7 @@ module.exports = class LevelLoader extends CocoClass url += "?course=#{@courseID}" if @courseID session = new LevelSession().setURL url - session.project = ['creator', 'team', 'heroConfig', 'codeLanguage', 'submittedCodeLanguage', 'state'] if @headless + session.project = ['creator', 'team', 'heroConfig', 'codeLanguage', 'submittedCodeLanguage', 'state', 'submittedCode'] if @headless @sessionResource = @supermodel.loadModel(session, 'level_session', {cache: false}) @session = @sessionResource.model if @opponentSessionID diff --git a/app/models/Level.coffee b/app/models/Level.coffee index b284f1dda..32b238df0 100644 --- a/app/models/Level.coffee +++ b/app/models/Level.coffee @@ -146,7 +146,7 @@ module.exports = class Level extends CocoModel levelThang.components.push placeholderComponent # Load the user's chosen hero AFTER getting stats from default char - if /Hero Placeholder/.test(levelThang.id) and @get('type', true) in ['course', 'course-ladder'] + if /Hero Placeholder/.test(levelThang.id) and @get('type', true) in ['course'] heroThangType = me.get('heroConfig')?.thangType levelThang.thangType = heroThangType if heroThangType From 3a9f0adba8a04ec1a49c733b21d8fa0772e29e19 Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 9 Jun 2016 15:02:01 -0700 Subject: [PATCH 143/167] Switch to using actual node server to receive log events --- app/core/Tracker.coffee | 23 ++++++------------- server/models/AnalyticsLogEvent.coffee | 31 +++++--------------------- server_config.coffee | 3 +++ 3 files changed, 16 insertions(+), 41 deletions(-) diff --git a/app/core/Tracker.coffee b/app/core/Tracker.coffee index 2f2aab758..8c0f2eefc 100644 --- a/app/core/Tracker.coffee +++ b/app/core/Tracker.coffee @@ -165,22 +165,13 @@ module.exports = class Tracker extends CocoClass properties[key] = value for key, value of @explicitTraits if @explicitTraits? console.log 'Tracking internal analytics event:', event, properties if debugAnalytics - if @isProduction - eventObject = {} - eventObject["event"] = event - eventObject["properties"] = properties unless _.isEmpty properties - eventObject["user"] = me.id - dataToSend = JSON.stringify eventObject - # console.log dataToSend if debugAnalytics - $.post("#{window.location.protocol or 'http:'}//analytics-cf.codecombat.com/analytics", dataToSend).fail -> - console.error "Analytics post failed!" - else - request = @supermodel.addRequestResource { - url: '/db/analytics.log.event/-/log_event' - data: {event: event, properties: properties} - method: 'POST' - }, 0 - request.load() + + request = @supermodel.addRequestResource { + url: '/db/analytics.log.event/-/log_event' + data: {event: event, properties: properties} + method: 'POST' + }, 0 + request.load() trackTiming: (duration, category, variable, label) -> # https://developers.google.com/analytics/devguides/collection/analyticsjs/user-timings diff --git a/server/models/AnalyticsLogEvent.coffee b/server/models/AnalyticsLogEvent.coffee index 076f98be3..b1e998702 100644 --- a/server/models/AnalyticsLogEvent.coffee +++ b/server/models/AnalyticsLogEvent.coffee @@ -6,15 +6,10 @@ http = require 'http' config = require '../../server_config' AnalyticsLogEventSchema = new mongoose.Schema({ - u: mongoose.Schema.Types.ObjectId - e: Number # event analytics.string ID - p: mongoose.Schema.Types.Mixed - - # TODO: Remove these legacy properties after we stop querying for them (probably 30 days, ~2/16/15) - user: mongoose.Schema.Types.ObjectId + user: String #Actually a `mongoose.Schema.Types.ObjectId` but ... event: String properties: mongoose.Schema.Types.Mixed -}, {strict: false}) +}, {strict: false, versionKey: false}) AnalyticsLogEventSchema.index({event: 1, _id: -1}) AnalyticsLogEventSchema.index({event: 1, 'properties.level': 1}) @@ -30,23 +25,9 @@ AnalyticsLogEventSchema.statics.logEvent = (user, event, properties={}) -> user: user event: event properties: properties - if config.isProduction and not config.unittest - docString = JSON.stringify doc - headers = - "Content-Type":'application/json' - "Content-Length": docString.length - options = - host: 'analytics.codecombat.com' - port: 80 - path: '/analytics' - method: 'POST' - headers: headers - req = http.request options, (res) -> - req.on 'error', (e) -> log.warn e - req.write(docString) - req.end() - else - doc.save() + doc.save() -module.exports = AnalyticsLogEvent = mongoose.model('analytics.log.event', AnalyticsLogEventSchema) +analyticsMongoose = mongoose.createConnection "mongodb://#{config.mongo.analytics_host}:#{config.mongo.analytics_port}/#{config.mongo.analytics_db}" + +module.exports = AnalyticsLogEvent = analyticsMongoose.model(config.mongo.analytics_collection, AnalyticsLogEventSchema) diff --git a/server_config.coffee b/server_config.coffee index 6f6eaf7ee..d76451789 100644 --- a/server_config.coffee +++ b/server_config.coffee @@ -21,10 +21,13 @@ config.mongo = analytics_port: process.env.COCO_MONGO_ANALYTICS_PORT or 27017 analytics_host: process.env.COCO_MONGO_ANALYTICS_HOST or 'localhost' analytics_db: process.env.COCO_MONGO_ANALYTICS_DATABASE_NAME or 'analytics' + analytics_collection: process.env.COCO_MONGO_ANALYTICS_COLLECTION or 'analytics.log.event' mongoose_replica_string: process.env.COCO_MONGO_MONGOOSE_REPLICA_STRING or '' mongoose_tokyo_replica_string: process.env.COCO_MONGO_MONGOOSE_TOKYO_REPLICA_STRING or '' mongoose_saoPaulo_replica_string : process.env.COCO_MONGO_MONGOOSE_SAOPAULO_REPLICA_STRING or '' + + if config.tokyo or config.saoPaulo config.mongo.readpref = 'nearest' else From 41f062fce83b1df68f465639eb258d2aec0c1a18 Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 9 Jun 2016 17:38:36 -0700 Subject: [PATCH 144/167] Don't make log collection plural. --- server/models/AnalyticsLogEvent.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/models/AnalyticsLogEvent.coffee b/server/models/AnalyticsLogEvent.coffee index b1e998702..db974f4a9 100644 --- a/server/models/AnalyticsLogEvent.coffee +++ b/server/models/AnalyticsLogEvent.coffee @@ -30,4 +30,4 @@ AnalyticsLogEventSchema.statics.logEvent = (user, event, properties={}) -> analyticsMongoose = mongoose.createConnection "mongodb://#{config.mongo.analytics_host}:#{config.mongo.analytics_port}/#{config.mongo.analytics_db}" -module.exports = AnalyticsLogEvent = analyticsMongoose.model(config.mongo.analytics_collection, AnalyticsLogEventSchema) +module.exports = AnalyticsLogEvent = analyticsMongoose.model('analytics.log.event', AnalyticsLogEventSchema, config.mongo.analytics_collection) From d2634e63ed801e2b1f54fb33531f45a1ac49d91a Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Thu, 9 Jun 2016 17:59:19 -0700 Subject: [PATCH 145/167] Fix #3642: AIs using aiSource now don't need API protection (with new Aether) --- app/views/play/level/tome/Spell.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/play/level/tome/Spell.coffee b/app/views/play/level/tome/Spell.coffee index aabfa4c96..dd0cf0936 100644 --- a/app/views/play/level/tome/Spell.coffee +++ b/app/views/play/level/tome/Spell.coffee @@ -46,6 +46,7 @@ module.exports = class Spell @source = sessionSource if p.aiSource and not @otherSession and not @canWrite() @source = @originalSource = p.aiSource + @isAISource = true @thangs = {} if @canRead() # We can avoid creating these views if we'll never use them. @view = new SpellView {spell: @, level: options.level, session: @session, otherSession: @otherSession, worker: @worker, god: options.god, @supermodel} @@ -162,7 +163,7 @@ module.exports = class Spell cb(aether.hasChangedSignificantly((newSource ? @originalSource), (currentSource ? @source), true, true)) createAether: (thang) -> - writable = @permissions.readwrite.length > 0 + writable = @permissions.readwrite.length > 0 and not @isAISource skipProtectAPI = @skipProtectAPI or not writable problemContext = @createProblemContext thang includeFlow = (@levelType in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']) and not skipProtectAPI From c1f0e2d5d037b987f7de94449725113eb371232b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?RUSLAN=20L=C3=93PEZ=20CARRO?= Date: Fri, 10 Jun 2016 12:48:50 +0200 Subject: [PATCH 146/167] Update es-419.coffee (#3732) --- app/locale/es-419.coffee | 80 ++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/app/locale/es-419.coffee b/app/locale/es-419.coffee index ca9855ad7..e53af41de 100644 --- a/app/locale/es-419.coffee +++ b/app/locale/es-419.coffee @@ -27,7 +27,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip classroom_in_a_box: "Un aula prediseñada para enseñar informática." codecombat_is: "CodeCombat es una plataforma para que los estudiantes aprendan informática mientras están jugando un juego de verdad." # {change} our_courses: "Nuestros cursos fueron probados especificamente para destacarse en el aula, incluso con maestros con poca o ninguna experiencia previa en programación." # {change} -# top_screenshots_hint: "Los estudiantes write code and see their changes update in real-time" + top_screenshots_hint: "Los estudiantes escriben código y ven sus cambios actualizarse en tiempo real" designed_with: "Diseñado teniendo en cuenta a los maestros" real_code: "Escritura real de código" from_the_first_level: "desde el primer nivel" @@ -59,10 +59,10 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip agency: "CodeCombat es un juego que ofrece a los jugadores esa voluntad y confianza mediante nuestro motor de código robusto, que ayuda tanto a principiantes como a estudiantes avanzados por igual a escribir código correcto y válido." # request_demo_title: "Get your students started hoy!" # request_demo_subtitle: "Request a demo and get your students started in less than an hour." -# get_started_title: "Set up your clase hoy" -# get_started_subtitle: "Set up a class, add your students, and monitor their progress as they learn computer science." + get_started_title: "Configure su clase hoy" +# get_started_subtitle: "Configure a class, add your students, and monitor their progress as they learn computer science." request_demo: "Solicitar un Demo" -# setup_a_class: "Set Up una Clase" + setup_a_class: "Configurar una Clase" have_an_account: "¿Ya tenes una cuenta?" # {change} logged_in_as: "Estás actualmente conectado como" view_my_classes: "Ver mis clases" @@ -90,7 +90,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip view_profile: "Ver mi perfíl" view_progress: "Ver mi progreso" check_out_wiki: "Check out our new educator Wiki" -# want_coco: "Want CodeCombat at your school?" + want_coco: "¿Quisiera CodeCombat en su escuela?" # form_select_role: "Select primary role" form_select_range: "Seleccione el tamaño de la clase" @@ -127,7 +127,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip get_involved: "Participa" open_source: "Código Libre (GitHub)" support: "Soporte" -# faqs: "FAQs" + faqs: "FAQs" help_pref: "¿Necesitas ayuda? Envianos un correó electronico!" help_suff: "y te contactaremos pronto" @@ -204,14 +204,14 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip # this: "this" # or: "or" # "||": "or" -# and: "and" -# "&&": "and" -# not: "not" -# "!": "not" + and: "y" + "&&": "y" + not: "no" + "!": "no" # "=": "assign" -# "==": "equals" -# "===": "strictly equals" -# "!=": "does not equal" + "==": "iguala" + "===": "iguala estrictamente" + "!=": "no iguala" # "!==": "does not strictly equal" # ">": "is greater than" # ">=": "is greater than or equal" @@ -219,15 +219,15 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip # "<=": "is less than or equal" # "*": "multiplied by" # "/": "divided by" -# "+": "plus" -# "-": "minus" + "+": "mas" + "-": "menos" # "+=": "add and assign" # "-=": "subtract and assign" -# True: "True" -# true: "true" -# False: "False" -# false: "false" -# undefined: "undefined" + True: "Verdadero" + true: "verdadero" + False: "Falso" + false: "falso" + undefined: "undefinido" # null: "null" # nil: "nil" # None: "None" @@ -785,10 +785,10 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip jobs_benefit_4: "Días personales/enfermo ilimitados" # {change} jobs_benefit_5: "Desarrollo profesional y soporte de formación continua" # {change} jobs_benefit_6: "Seguro médico/dental/vista" # {change} -# jobs_benefit_7: "Sit-stand desks para todos" +# jobs_benefit_7: "Sit-stand escritorios para todos" # jobs_benefit_9: "10-year option exercise window" -# jobs_benefit_10: "Maternity leave: 10 weeks paid, next 6 @ 55% salary" -# jobs_benefit_11: "Paternity leave: 10 weeks paid" +# jobs_benefit_10: "Maternity leave: 10 semanas pagadas, next 6 @ 55% salary" +# jobs_benefit_11: "Paternity leave: 10 semanas pagadas" learn_more: "Aprender Más" jobs_custom_title: "Crear Tu Propio" jobs_custom_description: "¿Te apasiona CodeCombat pero nos ves un puesto de trabajo que coincida con tus cualificaciones? Escríbenos y muestrenos como cree que puede contribuir a nuestro equipo. ¡Nos encantaría saber de vos!" @@ -855,8 +855,8 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip thanks_header: "¡Gracias por solicitar un presupuesto!" # {change} # thanks_sub_header: "Gracias por expressing interest in CodeCombat for your school." thanks_p: "Estaremos en contacto pronto. ¿Preguntas? Escríbenos:" # {change} -# back_to_classes: "Back to Classes" -# finish_signup: "Finish creating your teacher account:" +# back_to_classes: "Back to Clases" + finish_signup: "Termine la creación de su cuenta de maestro:" # finish_signup_p: "Create an account to set up a class, add your students, and monitor their progress as they learn computer science." # signup_with: "Sign up con:" connect_with: "Conectar con:" @@ -975,7 +975,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip social_facebook: "Me Gusta CodeCombat en Facebook" social_twitter: "Sigue a CodeCombat en Twitter" social_gplus: "Únete a CodeCombat con Google+" -# social_slack: "Chat with us in the public CodeCombat Slack channel" +# social_slack: "Chat con nosotros en el public CodeCombat Slack canal" contribute_to_the_project: "Contribuir al proyecto" clans: @@ -1080,8 +1080,8 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip buy_course1: "Comprar este curso" select_all_courses: "Seleccione 'Todos los cursos' para un 50% de descuento!" all_courses: "Todos los Cursos" -# number_programming_students: "Number of Programming Students" -# number_total_students: "Total Students in School/District" +# number_programming_students: "Número de Programming Students" +# number_total_students: "Total Students en School/District" enter_number_students: "Introducir el número de estudiantes que necesita para este grupo." name_class: "Nombre de su grupo" displayed_course_page: "Se mostrará en la página de curso para usted y sus estudiantes. Puede cambiarse después." @@ -1234,8 +1234,8 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip additional_resources: "Recursos Adicionales" additional_resources_1_pref: "Descargar/imprimir nuestra" additional_resources_1_mid: "Guía para maestros: Curso 1" -# additional_resources_1_mid2: "and" -# additional_resources_1_mid3: "Course 2" + additional_resources_1_mid2: "y" + additional_resources_1_mid3: "Curso 2" additional_resources_1_suff: "explicaciones y soluciones para cada nivel." # {change} additional_resources_2_pref: "Complete nuestro" additional_resources_2_suff: "para obtener dos matrículas gratuitas para el resto de nuestros cursos pagos." @@ -1264,9 +1264,9 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip title: "Título" # Flat style redesign # subtitle: "Review course guidelines, solutions, and levels" # changelog: "View latest changes to course levels." -# select_language: "Select language" -# select_level: "Select level" -# play_level: "Play Level" + select_language: "Seleccione lenguaje" + select_level: "Seleccione nivel" +# play_level: "Play Nivel" concepts_covered: "Conceptos Cubiertos" # print_guide: "Print Guide (PDF)" # view_guide_online: "View Guide Online (PDF)" @@ -1300,9 +1300,9 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip # courses: "Courses" # enrollments: "Enrollments" # resources: "Resources" -# help: "Help" + help: "Ayuda" # students: "Students" # Shared -# language: "Language" + language: "Lenguaje" # edit_class_settings: "edit class settings" # complete: "Complete" # access_restricted: "Account Update Required" @@ -1555,7 +1555,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip ambassador_join_note_strong: "Nota" ambassador_join_note_desc: "Una de nuestras principales prioridades es construir un modo multijugador donde los jugadores con mayores dificultades a la hora de resolver un nivel, puedan invocar a los magos más avanzados para que les ayuden. Será una buena manera de que los Embajadores puedan hacer su trabajo. ¡Te mantendremos informado!" ambassador_subscribe_desc: "Recibe correos sobre actualizaciones de soporte y desarrollo del multijugador." -# teacher_subscribe_desc: "Get emails on updates and announcements for teachers." +# teacher_subscribe_desc: "Get emails on updates and announcements para maestros." changes_auto_save: "Los cambios son guardados automáticamente cuando marcas las casillas de verificación." diligent_scribes: "Nuestros diligentes Escribas:" powerful_archmages: "Nuestros poderosos Archimagos:" @@ -1713,14 +1713,14 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip coppa_deny: text1: "¿No puedes esperar para aprender a programar?" -# text2: "Ask your parents to create an account for you!" +# text2: "Ask your parents que creen una cuenta for you!" close: "Cerrar ventana" loading_error: could_not_load: "Error cargando del servidor" connection_failure: "Fallo de conexión." # connection_failure_desc: "It doesn’t look like you’re connected to the internet! Check your network connection and then reload this page." -# login_required: "Login Required" + login_required: "Login Requerido" # login_required_desc: "You need to be logged in to access this page." unauthorized: "Necesitas acceder. ¿Tienes desabilitadas las cookies?" forbidden: "No tienes los permisos." @@ -1878,8 +1878,8 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip # march: "March" # april: "April" # may: "May" -# june: "June" -# july: "July" + june: "Junio" + july: "Julio" # august: "August" # september: "September" # october: "October" From ccd9bb3eb6e8fea199b8f26fb9450ea4f7551113 Mon Sep 17 00:00:00 2001 From: Imperadeiro98 Date: Fri, 10 Jun 2016 11:49:32 +0100 Subject: [PATCH 147/167] Uncomment and header in es-419.coffee --- app/locale/es-419.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/locale/es-419.coffee b/app/locale/es-419.coffee index e53af41de..e765d040e 100644 --- a/app/locale/es-419.coffee +++ b/app/locale/es-419.coffee @@ -182,7 +182,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip campaign_old_multiplayer: "(Obsoleto) Antiguas Arenas Multijugador" campaign_old_multiplayer_description: "Reliquias de una era más civilizada. Ninguna simulación es ejecutada para estas arenas multijugador antiguas y sin héroes." -# code: + code: # if: "if" # Keywords--these translations show up on hover, so please translate them all, even if it's kind of long. (In the code editor, they will still be in English.) # else: "else" # elif: "else if" From 9bb1e3946ff1a198e53e2765add7fb43949ba5d0 Mon Sep 17 00:00:00 2001 From: Ana Date: Fri, 10 Jun 2016 21:45:50 +0200 Subject: [PATCH 148/167] Update sr.coffee (#3733) - fixed some typos and language flow issues - translated more sections and started courses section --- app/locale/sr.coffee | 218 +++++++++++++++++++++---------------------- 1 file changed, 109 insertions(+), 109 deletions(-) diff --git a/app/locale/sr.coffee b/app/locale/sr.coffee index 5e317d20e..8491ee568 100644 --- a/app/locale/sr.coffee +++ b/app/locale/sr.coffee @@ -37,9 +37,9 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian teaching_computer_science: "Подучавање компјутерских наука не захтева скупу димплому, јер ми пружамо алатке да подржимо едукаторе из свих позадина." accessible_to: "Доступно" everyone: "свима" - democratizing: "Демократизација процеса учења кодирања је у средишту наше филозофије. Свако би требало да има прилику да научи кодирање" + democratizing: "Демократизација процеса учења кодирања је у средишту наше филозофије. Свако би требало да има прилику да научи кодирање." forgot_learning: "Мислим да су већ заборавили да су заправо учили нешто." - wanted_to_do: " Кодирање је нешто што сам одувек желео да радим, и никад нисам мислио да ћу моћи да га научим у школи." + wanted_to_do: " Кодирање је нешто што сам одувек желео да радим и никад нисам мислио да ћу моћи то да научим у школи." why_games: "Зашто је учење кроз игре важно?" games_reward: "Игре награђују продуктивну борбу." encourage: "Гејминг је медиј који подстиче интеракцију, откривање и учење путем покушаја и грешке. Добра игра изазива играча да савлада вештине током времена, што је исти есенцијални процес кроз који ученици пролазе док уче." @@ -695,7 +695,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian action_radius: "Опсег" action_duration: "Трајање" example: "Пример" - ex: "нпр." # Abbreviation of "example" + ex: "нпр" # Abbreviation of "example" current_value: "Тренутна вредност" default_value: "Подразумевана вредност" parameters: "Параметри" @@ -732,7 +732,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian story_link: "Прича" press_link: "Прес" mission_title: "Наша мисија: да учинимо програмирање доступним сваком ученику на планети." - mission_description_1: "Програмирање је магично. То је могућност да створиш ствари из чисте имагинације. Ми смо покренули CodeCombat да бисмо дали ученицима осећај чаробњачке моћи на дохват руке користећикуцани код." + mission_description_1: "Програмирање је магично. То је могућност да створиш ствари из чисте имагинације. Ми смо покренули CodeCombat да бисмо дали ученицима осећај чаробњачке моћи на дохват руке користећи куцани код." mission_description_2: "Како се испоставља, то им омогућава да такође уче брже. МНОГО брже. То је као конверзација уместо коришћења упутства. Желимо да донесему ту конверзацију у сваку школу и сваком ученику, јер би свако требало да добије шансу да научи магију програмирања." team_title: "Упознај CodeCombat тим" team_values: "Ми ценимо отворен дијалог пун поштовања, где најбоља идеја побеђује. Наше одлуке су засноване на истраживању потрошача и наш процес је фокусиран на достављање опипљивих резултата за њих. Свако је практичан, од нашег генералног директора до наших Github сарадника, јер ми ценимо раст и учење у нашем тиму." @@ -768,7 +768,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian number_contributors: "Преко 450 сарадника је дало своју подршку и време овом пројекту." story_title: "Наша прича до сад" story_subtitle: "Од 2013, CodeCombat је израстао из обичних скупова скица у праву успешну игру." - story_statistic_1a: "Више од 5 000 000" + story_statistic_1a: "Више од 5.000.000" story_statistic_1b: "играча укупно" story_statistic_1c: "је започело њихово програмерско путовање кроз CodeCombat" story_statistic_2a: "Преведени смо на преко 50 језика — наши играчи долазе из" @@ -784,7 +784,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian jobs_benefits: "Бенефиције за запослене" jobs_benefit_4: "Неограничен одмор" jobs_benefit_5: "Професионални развој и континуирана подршка образовања - бесплатне књиге и игре!" -# jobs_benefit_6: "Медицинско (gold), зубно, очно" + jobs_benefit_6: "Медицинско (gold), дентално, очно осигурање" jobs_benefit_7: "Sit-stand радни столови за свакога" # jobs_benefit_9: "10-year option exercise window" jobs_benefit_10: "Породиљско одсуство: 10 плаћених недеља, наредних 6 недеља 55% плате" @@ -1038,63 +1038,63 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian track_concepts8: "за придруживање" private_require_sub: "Приватни кланови захтевају претплату да би могао да их направиш или да им се придружиш." -# courses: -# course: "Course" -# courses: "courses" -# create_new_class: "Create New Class" -# not_enrolled: "You are not enrolled in this course." -# visit_pref: "Please visit the" -# visit_suf: "page to enroll." -# select_class: "Select one of your classes" -# unnamed: "*unnamed*" -# select: "Select" -# unnamed_class: "Unnamed Class" -# edit_settings: "edit class settings" -# edit_settings1: "Edit Class Settings" -# progress: "Class Progress" -# add_students: "Add Students" -# stats: "Statistics" -# total_students: "Total students:" -# average_time: "Average level play time:" -# total_time: "Total play time:" -# average_levels: "Average levels completed:" -# total_levels: "Total levels completed:" -# furthest_level: "Furthest level completed:" -# students: "Students" -# students1: "students" -# concepts: "Concepts" -# levels: "levels" -# played: "Played" -# play_time: "Play time:" -# completed: "Completed:" -# invite_students: "Invite students to join this class." -# invite_link_header: "Link to join course" -# invite_link_p_1: "Give this link to students you would like to have join the course." -# invite_link_p_2: "Or have us email them directly:" + courses: + course: "Курс" + courses: "курсеви" + create_new_class: "Направи нови разред" + not_enrolled: "Ниси уписан на овај курс." + visit_pref: "Молимо да посетиш" + visit_suf: "страницу да би се уписао." + select_class: "Изабери један од својих разреда" + unnamed: "*неименован*" + select: "Изабери" + unnamed_class: "Неименован разред" + edit_settings: "измени подешавања разреда" + edit_settings1: "Измени подешавања разреда" + progress: "Напредак разреда" + add_students: "Додај ученике" + stats: "Статистика" + total_students: "Укупно ученика:" + average_time: "Просечно време играња нивоа:" + total_time: "Укупно време играња:" + average_levels: "Просечан број завршених нивоа:" + total_levels: "Укупно завршених нивоа:" + furthest_level: "Најдаљи завршен ниво:" + students: "Ученици" + students1: "ученици" + concepts: "Концепти" + levels: "нивои" + played: "Одиграно" + play_time: "Време играња:" + completed: "Завршено:" + invite_students: "Позови ученике да се придруже овом разреду." + invite_link_header: "Линк за придруживање курсу" + invite_link_p_1: "Дај овај линк ученицима за које желиш да се придруже курсу." + invite_link_p_2: "Или дај нама да им директно пошаљемо мејл:" # capacity_used: "Course slots used:" -# enter_emails: "Enter student emails to invite, one per line" -# send_invites: "Send Invites" -# creating_class: "Creating class..." -# purchasing_course: "Purchasing course..." -# buy_course: "Buy Course" -# buy_course1: "Buy this course" -# select_all_courses: "Select 'All Courses' for a 50% discount!" -# all_courses: "All Courses" -# number_programming_students: "Number of Programming Students" -# number_total_students: "Total Students in School/District" -# enter_number_students: "Enter the number of students you need for this class." -# name_class: "Name your class" + enter_emails: "Упиши мејлове ученика, један по линији" + send_invites: "Пошаљи позиве" + creating_class: "Креирање разреда..." + purchasing_course: "Куповина курса..." + buy_course: "Купи курс" + buy_course1: "Купи овај курс" + select_all_courses: "Изабери 'Сви курсеви' за 50% попуста!" + all_courses: "Сви курсеви" + number_programming_students: "Број ученика који програмирају" + number_total_students: "Укупан број ученика у школи/округу" + enter_number_students: "Унеси број ученика који ти треба за овај разред." + name_class: "Именуј свој разред" # displayed_course_page: "This will be displayed on the course page for you and your students. It can be changed later." -# buy: "Buy" -# purchasing_for: "You are purchasing a license for" -# creating_for: "You are creating a class for" -# for: "for" # Like in 'for 30 students' + buy: "Купи" + purchasing_for: "Купујеш лиценцу за" + creating_for: "Правиш разред за" + for: "за" # Like in 'for 30 students' # receive_code: "Afterwards you will receive an unlock code to distribute to your students, which they can use to enroll in your class." -# free_trial: "Free trial for teachers!" + free_trial: "Бесплатна пробна верзија за учитеље!" # get_access: "to get individual access to all courses for evalutaion purposes." -# questions: "Questions?" -# teachers_click: "Teachers Click Here" -# students_click: "Students Click Here" + questions: "Питања?" + teachers_click: "Учитељи кликните овде" + students_click: "Ученици кликните овде" # courses_on_coco: "Courses on CodeCombat" # designed_to: "Courses are designed to introduce computer science concepts using CodeCombat's fun and engaging environment. CodeCombat levels are organized around key topics to encourage progressive learning, over the course of 5 hours." # more_in_less: "Learn more in less time" @@ -1629,21 +1629,21 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # humans: "Red" # Ladder page display team name # ogres: "Blue" -# user: -# stats: "Stats" -# singleplayer_title: "Singleplayer Levels" -# multiplayer_title: "Multiplayer Levels" -# achievements_title: "Achievements" -# last_played: "Last Played" -# status: "Status" -# status_completed: "Completed" -# status_unfinished: "Unfinished" + user: + stats: "Статистика" + singleplayer_title: "Нивои за једног играча" + multiplayer_title: "Нивои за више играча" + achievements_title: "Достигнућа" + last_played: "Последњи игран" + status: "Статус" + status_completed: "Завршен" + status_unfinished: "Незавршен" # no_singleplayer: "No Singleplayer games played yet." # no_multiplayer: "No Multiplayer games played yet." -# no_achievements: "No Achievements earned yet." -# favorite_prefix: "Favorite language is " -# favorite_postfix: "." -# not_member_of_clans: "Not a member of any clans yet." + no_achievements: "Још увек нема стечених достигнућа." + favorite_prefix: "Омиљени језик је " + favorite_postfix: "." + not_member_of_clans: "Ниси члан ниједног клана још увек." achievements: last_earned: "Последње стечено" @@ -1711,10 +1711,10 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # you_can2: "purchase a prepaid code" # you_can3: "that can be applied to your own account or given to others." -# coppa_deny: -# text1: "Can’t wait to learn programming?" -# text2: "Ask your parents to create an account for you!" -# close: "Close Window" + coppa_deny: + text1: "Не можеш да дочекаш да научиш програмирање?" + text2: "Питај своје родитеље да направе налог за тебе!" + close: "Затвори прозор" # loading_error: # could_not_load: "Error loading from server" @@ -1747,50 +1747,50 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian hero: "Херој" campaigns: "Кампање" -# concepts: -# advanced_strings: "Advanced Strings" -# algorithms: "Algorithms" -# arguments: "Arguments" -# arithmetic: "Arithmetic" -# arrays: "Arrays" -# basic_syntax: "Basic Syntax" -# boolean_logic: "Boolean Logic" -# break_statements: "Break Statements" -# classes: "Classes" -# continue_statements: "Continue Statements" -# for_loops: "For Loops" -# functions: "Functions" -# graphics: "Graphics" -# if_statements: "If Statements" -# input_handling: "Input Handling" -# math_operations: "Math Operations" -# object_literals: "Object Literals" -# parameters: "Parameters" -# strings: "Strings" -# variables: "Variables" -# vectors: "Vectors" -# while_loops: "While Loops" -# recursion: "Recursion" + concepts: + advanced_strings: "Напредни стрингови" + algorithms: "Алгоритми" + arguments: "Аргументи" + arithmetic: "Аритметика" + arrays: "Низови" + basic_syntax: "Основна синтакса" + boolean_logic: "Булова логика" + break_statements: "Break искази" + classes: "Класе" + continue_statements: "Continue искази" + for_loops: "For петље" + functions: "Функције" + graphics: "Графика" + if_statements: "If искази" + input_handling: "Руковање инпутом" + math_operations: "Математичке операције" + object_literals: "Објекат литерали" + parameters: "Параметри" + strings: "Стрингови" + variables: "Променљиве" + vectors: "Вектори" + while_loops: "While петље" + recursion: "Рекурзије" -# delta: -# added: "Added" -# modified: "Modified" -# not_modified: "Not Modified" -# deleted: "Deleted" -# moved_index: "Moved Index" + delta: + added: "Додато" + modified: "Измењено" + not_modified: "Није измењено" + deleted: "Избрисано" + moved_index: "Померен индекс" # text_diff: "Text Diff" # merge_conflict_with: "MERGE CONFLICT WITH" # no_changes: "No Changes" multiplayer: multiplayer_title: "Подешавање мода за више играча" # We'll be changing this around significantly soon. Until then, it's not important to translate. -# multiplayer_toggle: "Enable multiplayer" -# multiplayer_toggle_description: "Allow others to join your game." + multiplayer_toggle: "Омогући мултиплејер" + multiplayer_toggle_description: "Дозволи другима да се придруже твојој игри." multiplayer_link_description: "Дај овај линк онима које желиш да ти се придруже." multiplayer_hint_label: "Мала помоћ" multiplayer_hint: " Кликни на линк да обележиш све, затим притисни Apple-C или Ctrl-C да копираш линк." multiplayer_coming_soon: "Стиже још нових карактеристика!" -# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." + multiplayer_sign_in_leaderboard: "Пријави се или направи налог и стави своје решење на ранг листу." # legal: # page_title: "Legal" From e7d076be6588e2533066991f19828bbbd301af25 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Sat, 11 Jun 2016 21:33:15 -0700 Subject: [PATCH 149/167] Update logo --- app/assets/images/pages/base/logo.png | Bin 32624 -> 60440 bytes app/templates/base-flat.jade | 2 -- 2 files changed, 2 deletions(-) diff --git a/app/assets/images/pages/base/logo.png b/app/assets/images/pages/base/logo.png index 0640f41474a4f5e394a76b4b4d112dc3daff13f5..a33d4f6eab23ef6a89e3c9270b58756d96220fb8 100644 GIT binary patch literal 60440 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 Zus81T-2-3C=wi6{r6{W^QzK;-`ahkt#|;1g literal 32624 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%0AM_eQ*zMfOq^MvKFs;^XhD9Omy{@T!%VjBVL) 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 Date: Thu, 9 Jun 2016 10:24:47 -0700 Subject: [PATCH 150/167] Test client for invite-by-email --- .../InviteToClassroomModal.spec.coffee | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/app/views/teachers/InviteToClassroomModal.spec.coffee diff --git a/test/app/views/teachers/InviteToClassroomModal.spec.coffee b/test/app/views/teachers/InviteToClassroomModal.spec.coffee new file mode 100644 index 000000000..29a59093f --- /dev/null +++ b/test/app/views/teachers/InviteToClassroomModal.spec.coffee @@ -0,0 +1,29 @@ +InviteToClassroomModal = require 'views/courses/InviteToClassroomModal' +User = require 'models/User' +factories = require 'test/app/factories' + +describe 'InviteToClassroomModal', -> + + modal = null + + beforeEach (done) -> + window.me = @teacher = factories.makeUser() + @classroom = factories.makeClassroom({ code: "wordsouphere", codeCamel: "WordSoupHere", ownerID: @teacher.id }) + modal = new InviteToClassroomModal({ @classroom }) + jasmine.demoModal(modal) + modal.render() + _.defer done + + describe 'Invite by email', -> + beforeEach (done) -> + @emails = ['test@example.com', 'test2@example.com'] + modal.$('#invite-emails-textarea').val(@emails.join('\n')) + modal.$('#send-invites-btn').click() + _.defer done + + it 'sends the request', (done) -> + request = jasmine.Ajax.requests.mostRecent() + expect(request.url).toBe("/db/classroom/#{@classroom.id}/invite-members") + expect(request.method).toBe("POST") + expect(request.data()['emails[]']).toEqual(@emails) + _.defer done From dccb4f317210c8356d5fdad6735ba5a56fcce05e Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Mon, 13 Jun 2016 12:11:22 -0700 Subject: [PATCH 151/167] Fix Alejandro's name --- app/models/ThangType.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/ThangType.coffee b/app/models/ThangType.coffee index bf7f04a9b..d8cc7c7ed 100644 --- a/app/models/ThangType.coffee +++ b/app/models/ThangType.coffee @@ -256,7 +256,7 @@ module.exports = class ThangType extends CocoModel "Sorcerer": "Pender" "Trapper": "Senick" "Champion": "Ida" - "Duelist": "Nando" + "Duelist": "Alejandro" } map[@get('name')] From 98bfcb4c9e391b00ff7dd4490257bb2b96730482 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Mon, 13 Jun 2016 14:45:24 -0700 Subject: [PATCH 152/167] Fix footer margin on TeacherClassView --- app/styles/style-flat.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/styles/style-flat.sass b/app/styles/style-flat.sass index 939de0aa4..480372890 100644 --- a/app/styles/style-flat.sass +++ b/app/styles/style-flat.sass @@ -444,7 +444,7 @@ body[lang='ru'], body[lang='uk'], body[lang='bg'], body[lang^='mk'], body[lang=' #footer background-image: url("/images/pages/home/footer_background.png") height: 229px - margin: -22px auto 0 + margin: 65px auto 0 color: white @media (max-width: $screen-sm-min) From ccf5b449e3b41d14e9a7fe74e5d25dad3ccbee37 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Mon, 13 Jun 2016 14:30:56 -0700 Subject: [PATCH 153/167] Use classroom.courses.levels instead of campaign.levels --- app/views/courses/TeacherClassView.coffee | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee index d1e8c6f24..fc821f0aa 100644 --- a/app/views/courses/TeacherClassView.coffee +++ b/app/views/courses/TeacherClassView.coffee @@ -111,9 +111,6 @@ module.exports = class TeacherClassView extends RootView return dir * diff if diff return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir) - @campaigns = new Campaigns() - @supermodel.trackRequest @campaigns.fetchByType('course', { data: { project: 'levels,slug,type' } }) - @courses = new Courses() @supermodel.trackRequest @courses.fetch() @@ -331,23 +328,18 @@ module.exports = class TeacherClassView extends RootView courseLabels += "CS#{index + 1} Playtime," courseOrder.push(course._id) csvContent = "data:text/csv;charset=utf-8,Username,Email,Total Playtime,#{courseLabels}Concepts\n" - campaignCourseMap = {} - courseMap = {} - for course in @courses.models - campaignCourseMap[course.get('campaignID')] = course - courseMap[course.id] = course levelCourseMap = {} - for campaign in @campaigns.models - continue unless campaignCourseMap[campaign.id] - for levelID, level of campaign.get('levels') - levelCourseMap[levelID] = campaignCourseMap[campaign.id] + for trimCourse in @classroom.get('courses') + for trimLevel in trimCourse.levels + levelCourseMap[trimLevel.original] = @courses.get(trimCourse._id) for student in @students.models concepts = [] - for course in @courses.models + for trimCourse in @classroom.get('courses') + course = @courses.get(trimCourse._id) instance = @courseInstances.findWhere({ courseID: course.id, classroomID: @classroom.id }) if instance and instance.hasMember(student) - # TODO: @levels collection is for the classroom, and not per-course - for level in @levels.models + for trimLevel in trimCourse.levels + level = @levels.findWhere({ original: trimLevel.original }) progress = @state.get('progressData').get({ classroom: @classroom, course: course, level: level, user: student }) concepts.push(level.get('concepts') ? []) if progress?.completed concepts = _.union(_.flatten(concepts)) From a96d5df78e2ee88e69fddd6fb1f3c20e2c62d62c Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Mon, 13 Jun 2016 16:21:22 -0700 Subject: [PATCH 154/167] Don't protect APIs in game-dev levels --- app/views/play/level/tome/Spell.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/play/level/tome/Spell.coffee b/app/views/play/level/tome/Spell.coffee index dd0cf0936..358f2f6df 100644 --- a/app/views/play/level/tome/Spell.coffee +++ b/app/views/play/level/tome/Spell.coffee @@ -164,7 +164,7 @@ module.exports = class Spell createAether: (thang) -> writable = @permissions.readwrite.length > 0 and not @isAISource - skipProtectAPI = @skipProtectAPI or not writable + skipProtectAPI = @skipProtectAPI or not writable or @levelType in ['game-dev'] problemContext = @createProblemContext thang includeFlow = (@levelType in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']) and not skipProtectAPI aetherOptions = createAetherOptions From 6cd1bfac7155c3d4eb332dcbdf2cbdd6967d9b9b Mon Sep 17 00:00:00 2001 From: Diego Toala Date: Tue, 14 Jun 2016 05:29:25 -0500 Subject: [PATCH 155/167] Update es-419.coffee (#3734) I want to contribute translating to Spanish (Latam). This is a test to know I am doing right. --- app/locale/es-419.coffee | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/locale/es-419.coffee b/app/locale/es-419.coffee index e765d040e..443f55a6e 100644 --- a/app/locale/es-419.coffee +++ b/app/locale/es-419.coffee @@ -265,13 +265,13 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip school_name_placeholder: "Ejemplo: High School, Springfield, IL" or_sign_up_with: "o conectate a travez de:" connected_gplus_header: "Te haz conectado exitosamente a travez de Google+!" -# connected_gplus_p: "Finish signing up so you can log in con your Google+ cuenta." -# gplus_exists: "You already have an cuenta associated con Google+!" + connected_gplus_p: "Termina el registro para que inicies sesión con tu cuenta de Google+." + gplus_exists: "Ya tienes asociada una cuenta con Google+!" connected_facebook_header: "Te haz conectado exitosamente a travez de Facebook!" -# connected_facebook_p: "Finish signing up so you can log in con your Facebook cuenta." -# facebook_exists: "You already have an cuenta associated con Facebook!" -# hey_students: "Estudiantes, enter the class code from your maestro." - birthday: "Compleaños" + connected_facebook_p: "Termina el registro para que inicies sesión con tu cuenta de Facebook." + facebook_exists: "Ya tienes asociada una cuenta Facebook!" + hey_students: "Estudiante, ingresa el coódigo de clase de tu maestro." + birthday: "Cumpleaños" recover: recover_account_title: "recuperar cuenta" @@ -853,12 +853,12 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip college_plus: "Universidad o superior" anything_else: "¿Algo más que deberíamos saber?" thanks_header: "¡Gracias por solicitar un presupuesto!" # {change} -# thanks_sub_header: "Gracias por expressing interest in CodeCombat for your school." + thanks_sub_header: "Gracias por el interés de su institución en CodeCombat" #"Gracias por expressing interest in CodeCombat for your school." thanks_p: "Estaremos en contacto pronto. ¿Preguntas? Escríbenos:" # {change} -# back_to_classes: "Back to Clases" + back_to_classes: "Volver a las clases"#"Back to Clases" finish_signup: "Termine la creación de su cuenta de maestro:" -# finish_signup_p: "Create an account to set up a class, add your students, and monitor their progress as they learn computer science." -# signup_with: "Sign up con:" + finish_signup_p: "Crear una cuenta para configurar la clase, agregar estudiante y monitorear su progreso a medida que aprenden programacioón"#"Create an account to set up a class, add your students, and monitor their progress as they learn computer science." + signup_with: "Registrarse con:" connect_with: "Conectar con:" # conversion_warning: "WARNING: Your current account is a Student Account. Once you submit this form, your account will be updated to a Teacher Account." # learn_more_modal: "Teacher accounts on CodeCombat have the ability to monitor student progress, assign enrollments and manage classrooms. Teacher accounts cannot be a part of a classroom - if you are currently enrolled in a class using this account, you will no longer be able to access it once you update to a Teacher Account." From ecb5169ca7e2fef8a3ddec6cb1172dc86a2138a5 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Tue, 14 Jun 2016 10:29:30 -0700 Subject: [PATCH 156/167] Add hintsB to level schema --- app/schemas/models/level.coffee | 9 ++++++++- app/views/i18n/I18NEditLevelView.coffee | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/schemas/models/level.coffee b/app/schemas/models/level.coffee index a6f7a42e7..caca6a8b8 100644 --- a/app/schemas/models/level.coffee +++ b/app/schemas/models/level.coffee @@ -279,7 +279,14 @@ _.extend LevelSchema.properties, documentation: c.object {title: 'Documentation', description: 'Documentation articles relating to this level.', 'default': {specificArticles: [], generalArticles: []}}, specificArticles: c.array {title: 'Specific Articles', description: 'Specific documentation articles that live only in this level.', uniqueItems: true }, SpecificArticleSchema generalArticles: c.array {title: 'General Articles', description: 'General documentation articles that can be linked from multiple levels.', uniqueItems: true}, GeneralArticleSchema - hints: c.array {title: 'Hints', description: 'Hints that will be gradually revealed to the player.', uniqueItems: true }, { + hints: c.array {title: 'Hints', description: 'Tips and tricks to help unstick a player for the level.', uniqueItems: true }, { + type: 'object' + properties: { + body: {type: 'string', title: 'Content', description: 'The body content of the article, in Markdown.', format: 'markdown'} + i18n: {type: 'object', format: 'i18n', props: ['body'], description: 'Help translate this hint'} + } + } + hintsB: c.array {title: 'HintsB', description: '2nd style of hints for a/b testing significant variations', uniqueItems: true }, { type: 'object' properties: { body: {type: 'string', title: 'Content', description: 'The body content of the article, in Markdown.', format: 'markdown'} diff --git a/app/views/i18n/I18NEditLevelView.coffee b/app/views/i18n/I18NEditLevelView.coffee index 6a333ab09..375d67c3a 100644 --- a/app/views/i18n/I18NEditLevelView.coffee +++ b/app/views/i18n/I18NEditLevelView.coffee @@ -34,6 +34,10 @@ module.exports = class I18NEditLevelView extends I18NEditModelView if i18n = hint.i18n name = "Hint #{index+1}" @wrapRow "'#{name}' body", ['body'], hint.body, i18n[lang]?.body, ['documentation', 'hints', index], 'markdown' + for hint, index in @model.get('documentation')?.hintsB ? [] + if i18n = hint.i18n + name = "Hint #{index+1}" + @wrapRow "'#{name}' body", ['body'], hint.body, i18n[lang]?.body, ['documentation', 'hints', index], 'markdown' # sprite dialogues for script, scriptIndex in @model.get('scripts') ? [] From 2dc66f5947c8b870149f0cf8686fed53930a1ff1 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 14 Jun 2016 11:54:29 -0700 Subject: [PATCH 157/167] Log some stuff for debugging --- app/lib/LevelLoader.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index d4f225a38..b607ad144 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -147,6 +147,7 @@ module.exports = class LevelLoader extends CocoClass @listenToOnce @opponentSession, 'sync', @loadDependenciesForSession loadDependenciesForSession: (session) -> + console.log "Loading dependencies for session: ", session if me.id isnt session.get 'creator' session.patch = session.save = -> console.error "Not saving session, since we didn't create it." else if codeLanguage = utils.getQueryVariable 'codeLanguage' @@ -171,9 +172,11 @@ module.exports = class LevelLoader extends CocoClass @consolidateFlagHistory() if @session.loaded if @level.get('type', true) in ['course'] # course-ladder is hard to handle because there's 2 sessions heroConfig = me.get('heroConfig') + console.log "Course mode, loading custom hero: ", heroConfig return if not heroConfig url = "/db/thang.type/#{heroConfig.thangType}/version" if heroResource = @maybeLoadURL(url, ThangType, 'thang') + console.log "Pushing resource: ", heroResource @worldNecessities.push heroResource return return unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] @@ -341,6 +344,7 @@ module.exports = class LevelLoader extends CocoClass true onWorldNecessitiesLoaded: -> + console.log "World necessities loaded." @initWorld() @supermodel.clearMaxProgress() @trigger 'world-necessities-loaded' From b3f8b79897fdfcbea1c13b62ae0c3e1e62ab9ace Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 14 Jun 2016 12:01:12 -0700 Subject: [PATCH 158/167] Register dependencies for course mode too --- app/lib/LevelLoader.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index b607ad144..2dbd1e6a4 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -111,7 +111,7 @@ module.exports = class LevelLoader extends CocoClass @loadDependenciesForSession @session loadSession: -> - if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] + if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course'] @sessionDependenciesRegistered = {} if @sessionID @@ -178,6 +178,7 @@ module.exports = class LevelLoader extends CocoClass if heroResource = @maybeLoadURL(url, ThangType, 'thang') console.log "Pushing resource: ", heroResource @worldNecessities.push heroResource + @sessionDependenciesRegistered[session.id] = true return return unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] heroConfig = session.get('heroConfig') From e73a31abe6562d8cca0b92a86b48cf9300e40582 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 14 Jun 2016 13:03:42 -0700 Subject: [PATCH 159/167] Fix loading hero thangs more --- app/lib/LevelLoader.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index 2dbd1e6a4..b4b03df8c 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -178,7 +178,7 @@ module.exports = class LevelLoader extends CocoClass if heroResource = @maybeLoadURL(url, ThangType, 'thang') console.log "Pushing resource: ", heroResource @worldNecessities.push heroResource - @sessionDependenciesRegistered[session.id] = true + @sessionDependenciesRegistered[session.id] = true return return unless @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] heroConfig = session.get('heroConfig') From 749601c9d8a75e20abd4cb75928342f14bd5699c Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 14 Jun 2016 14:27:57 -0700 Subject: [PATCH 160/167] Don't cache classrooms (fetched by memberID) --- app/views/courses/CoursesView.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/courses/CoursesView.coffee b/app/views/courses/CoursesView.coffee index fe70659c4..e4caa6662 100644 --- a/app/views/courses/CoursesView.coffee +++ b/app/views/courses/CoursesView.coffee @@ -42,7 +42,7 @@ module.exports = class CoursesView extends RootView @listenToOnce @courseInstances, 'sync', @onCourseInstancesLoaded @supermodel.loadCollection(@courseInstances) @classrooms = new CocoCollection([], { url: "/db/classroom", model: Classroom }) - @supermodel.loadCollection(@classrooms, { data: {memberID: me.id} }) + @supermodel.loadCollection(@classrooms, { data: {memberID: me.id}, cache: false }) @ownedClassrooms = new Classrooms() @ownedClassrooms.fetchMine({data: {project: '_id'}}) @supermodel.trackCollection(@ownedClassrooms) From 409b1708916c9da5ce21a715c9b58c8b5e20e397 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Tue, 14 Jun 2016 16:44:51 -0700 Subject: [PATCH 161/167] Update inbound lead assignment splits --- scripts/updateCloseIoLeads.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/scripts/updateCloseIoLeads.js b/scripts/updateCloseIoLeads.js index 8d1edce15..7753fa320 100644 --- a/scripts/updateCloseIoLeads.js +++ b/scripts/updateCloseIoLeads.js @@ -57,7 +57,20 @@ const emailDelayMinutes = 27; const scriptStartTime = new Date(); const closeIoApiKey = process.argv[2]; // Automatic mails sent as API owners, first key assumed to be primary and gets 50% of the leads -const closeIoMailApiKeys = [process.argv[3], process.argv[3], process.argv[4], process.argv[5]]; +const closeIoMailApiKeys = [ + { + apiKey: process.argv[3], + weight: .7 + }, + { + apiKey: process.argv[4], + weight: .25 + }, + { + apiKey: process.argv[5], + weight: .05 + }, +]; const closeIoEuMailApiKey = process.argv[6]; const intercomAppIdApiKey = process.argv[7]; const intercomAppId = intercomAppIdApiKey.split(':')[0]; @@ -238,7 +251,14 @@ function isUSSchoolStatus(status) { function getEmailApiKey(leadStatus) { if (leadStatus === defaultEuLeadStatus) return closeIoEuMailApiKey; if (closeIoMailApiKeys.length < 0) return; - return closeIoMailApiKeys[Math.floor(Math.random() * closeIoMailApiKeys.length)]; + const weightedList = []; + for (let closeIoMailApiKey of closeIoMailApiKeys) { + const multiples = closeIoMailApiKey.weight * 100; + for (let i = 0; i < multiples; i++) { + weightedList.push(closeIoMailApiKey.apiKey); + } + } + return weightedList[Math.floor(Math.random() * weightedList.length)]; } function getRandomEmailTemplate(templates) { From 109de551857cc6f266f6204170ab241676aa38a6 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 14 Jun 2016 18:02:08 -0700 Subject: [PATCH 162/167] Stop unprocessing flag events when serializing the flag history so that flag history is properly deterministic again --- app/lib/world/world.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee index d62fe4bc2..070e9b143 100644 --- a/app/lib/world/world.coffee +++ b/app/lib/world/world.coffee @@ -364,8 +364,8 @@ module.exports = class World endFrame = @frames.length #console.log "... world serializing frames from", startFrame, "to", endFrame, "of", @totalFrames [transferableObjects, nontransferableObjects] = [0, 0] - delete flag.processed for flag in @flagHistory - o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}, flagHistory: @flagHistory, difficulty: @difficulty, scores: @getScores(), randomSeed: @randomSeed, picoCTFFlag: @picoCTFFlag} + serializedFlagHistory = (_.omit(_.clone(flag), 'processed') for flag in @flagHistory) + o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}, flagHistory: serializedFlagHistory, difficulty: @difficulty, scores: @getScores(), randomSeed: @randomSeed, picoCTFFlag: @picoCTFFlag} o.trackedProperties[prop] = @[prop] for prop in @trackedProperties or [] for thangID, methods of @userCodeMap From 9239d2a81bb4d75dca30c46b6bd14c9aa99a1a61 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Wed, 15 Jun 2016 13:09:43 -0700 Subject: [PATCH 163/167] Fix text overflowing on teacher dashboard --- app/templates/courses/enrollments-view.jade | 26 ++++++++++--------- .../courses/restricted-to-students-view.jade | 7 ++--- .../courses/teacher-classes-view.jade | 7 ++--- .../teachers/restricted-to-teachers-view.jade | 7 ++--- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/app/templates/courses/enrollments-view.jade b/app/templates/courses/enrollments-view.jade index 15e77b330..babd88a79 100644 --- a/app/templates/courses/enrollments-view.jade +++ b/app/templates/courses/enrollments-view.jade @@ -5,19 +5,21 @@ block page_nav block content if me.isAnonymous() || (!me.isTeacher() && !view.classrooms.size()) - .access-restricted.container.text-center.m-y-3 - h5(data-i18n='teacher.access_restricted') - p(data-i18n='teacher.teacher_account_required') - if me.isAnonymous() - .login-button.btn.btn-lg.btn-primary(data-i18n='login.log_in') - a.btn.btn-lg.btn-primary-alt(href="/teachers/signup" data-i18n='teacher.create_teacher_account') - else - a.btn.btn-lg.btn-primary(href="/teachers/update-account" data-i18n="teachers_quote.convert_account_title") - button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out") + .container + .access-restricted.container.text-center.m-y-3 + h5(data-i18n='teacher.access_restricted') + p(data-i18n='teacher.teacher_account_required') + if me.isAnonymous() + .login-button.btn.btn-lg.btn-primary(data-i18n='login.log_in') + a.btn.btn-lg.btn-primary-alt(href="/teachers/signup" data-i18n='teacher.create_teacher_account') + else + a.btn.btn-lg.btn-primary(href="/teachers/update-account" data-i18n="teachers_quote.convert_account_title") + button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out") - .teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3 - h5(data-i18n='teacher.what_is_a_teacher_account') - p(data-i18n='teacher.teacher_account_explanation') + .container + .teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3 + h5(data-i18n='teacher.what_is_a_teacher_account') + p(data-i18n='teacher.teacher_account_explanation') else if !me.isTeacher() diff --git a/app/templates/courses/restricted-to-students-view.jade b/app/templates/courses/restricted-to-students-view.jade index 9ffcb3909..6b4a48c59 100644 --- a/app/templates/courses/restricted-to-students-view.jade +++ b/app/templates/courses/restricted-to-students-view.jade @@ -17,6 +17,7 @@ block content button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out") if me.isTeacher() - .teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3 - h5(data-i18n='teacher.what_is_a_teacher_account') - p(data-i18n='teacher.teacher_account_explanation') + .container + .teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3 + h5(data-i18n='teacher.what_is_a_teacher_account') + p(data-i18n='teacher.teacher_account_explanation') diff --git a/app/templates/courses/teacher-classes-view.jade b/app/templates/courses/teacher-classes-view.jade index 9946d300c..82e11e3b9 100644 --- a/app/templates/courses/teacher-classes-view.jade +++ b/app/templates/courses/teacher-classes-view.jade @@ -15,9 +15,10 @@ block content button.btn.btn-lg.btn-primary.update-teacher-btn(data-event-action="Teachers Classes Convert Teacher Account", data-i18n="teachers_quote.convert_account_title") button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out") - .teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3 - h5(data-i18n='teacher.what_is_a_teacher_account') - p(data-i18n='teacher.teacher_account_explanation') + .container + .teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3 + h5(data-i18n='teacher.what_is_a_teacher_account') + p(data-i18n='teacher.teacher_account_explanation') else if !me.isTeacher() diff --git a/app/templates/teachers/restricted-to-teachers-view.jade b/app/templates/teachers/restricted-to-teachers-view.jade index 3bb07c1e5..a5c890f00 100644 --- a/app/templates/teachers/restricted-to-teachers-view.jade +++ b/app/templates/teachers/restricted-to-teachers-view.jade @@ -14,6 +14,7 @@ block content a.btn.btn-lg.btn-primary(href="/teachers/update-account" data-i18n="teachers_quote.convert_account_title") button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out") - .teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3 - h5(data-i18n='teacher.what_is_a_teacher_account') - p(data-i18n='teacher.teacher_account_explanation') \ No newline at end of file + .container + .teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3 + h5(data-i18n='teacher.what_is_a_teacher_account') + p(data-i18n='teacher.teacher_account_explanation') From e841334c86ee47ca51c66d538576aedbdfce66e9 Mon Sep 17 00:00:00 2001 From: phoenixeliot Date: Tue, 14 Jun 2016 17:15:48 -0700 Subject: [PATCH 164/167] Fix join class modal (temporary hack) --- app/core/Router.coffee | 4 ++-- app/views/courses/CoursesView.coffee | 16 +++++++++++++++- app/views/courses/JoinClassModal.coffee | 2 ++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/core/Router.coffee b/app/core/Router.coffee index 3ddbbf206..7030d7df6 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -69,8 +69,8 @@ module.exports = class CocoRouter extends Backbone.Router 'contribute/diplomat': go('contribute/DiplomatView') 'contribute/scribe': go('contribute/ScribeView') - 'courses': go('courses/CoursesView', { studentsOnly: true }) - 'Courses': go('courses/CoursesView', { studentsOnly: true }) + 'courses': go('courses/CoursesView') + 'Courses': go('courses/CoursesView') 'courses/students': redirect('/courses') 'courses/teachers': redirect('/teachers/classes') 'courses/purchase': redirect('/teachers/licenses') diff --git a/app/views/courses/CoursesView.coffee b/app/views/courses/CoursesView.coffee index e4caa6662..6079838c8 100644 --- a/app/views/courses/CoursesView.coffee +++ b/app/views/courses/CoursesView.coffee @@ -40,7 +40,7 @@ module.exports = class CoursesView extends RootView @courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance}) @courseInstances.comparator = (ci) -> return ci.get('classroomID') + ci.get('courseID') @listenToOnce @courseInstances, 'sync', @onCourseInstancesLoaded - @supermodel.loadCollection(@courseInstances) + @supermodel.loadCollection(@courseInstances, { cache: false }) @classrooms = new CocoCollection([], { url: "/db/classroom", model: Classroom }) @supermodel.loadCollection(@classrooms, { data: {memberID: me.id}, cache: false }) @ownedClassrooms = new Classrooms() @@ -59,6 +59,11 @@ module.exports = class CoursesView extends RootView @listenTo @hero, 'all', -> @render() window.tracker?.trackEvent 'Students Loaded', category: 'Students', ['Mixpanel'] + + afterInsert: -> + super() + unless me.isStudent() or (@classCodeQueryVar and not me.isTeacher()) + @onClassLoadError() onCourseInstancesLoaded: -> map = {} @@ -135,12 +140,21 @@ module.exports = class CoursesView extends RootView else modal = new JoinClassModal({ @classCode }) @openModalView modal + @listenTo modal, 'error', @onClassLoadError @listenTo modal, 'join:success', @onJoinClassroomSuccess @listenTo modal, 'join:error', @onJoinClassroomError + @listenToOnce modal, 'hidden', -> + unless me.isStudent() + @onClassLoadError() @listenTo modal, 'hidden', -> @state = null @renderSelectors '#join-class-form' + # Super hacky way to patch users being able to join class while hiding /courses from others + onClassLoadError: -> + _.defer -> + application.router.routeDirectly('courses/RestrictedToStudentsView') + onJoinClassroomError: (classroom, jqxhr, options) -> @state = null if jqxhr.status is 422 diff --git a/app/views/courses/JoinClassModal.coffee b/app/views/courses/JoinClassModal.coffee index 78b3ad161..35cb4d58c 100644 --- a/app/views/courses/JoinClassModal.coffee +++ b/app/views/courses/JoinClassModal.coffee @@ -16,6 +16,8 @@ module.exports = class JoinClassModal extends ModalView jqxhr = @supermodel.trackRequest @classroom.fetchByCode(@classCode) unless me.get('emailVerified') @supermodel.trackRequest $.post("/db/user/#{me.id}/request-verify-email") + @listenTo @classroom, 'error', -> + @trigger('error') @listenTo @classroom, 'sync', -> @render @listenTo @classroom, 'join:success', -> From de138c258134a05ce4a5001ce8004b3b2871a4f5 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 26 May 2016 17:36:21 -0700 Subject: [PATCH 165/167] Disable ModuleLoader debug logs because they get in the way --- app/core/ModuleLoader.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/core/ModuleLoader.coffee b/app/core/ModuleLoader.coffee index 401b35735..0771ab657 100644 --- a/app/core/ModuleLoader.coffee +++ b/app/core/ModuleLoader.coffee @@ -82,8 +82,8 @@ module.exports = ModuleLoader = class ModuleLoader extends CocoClass # a module and its dependencies have loaded! if @queue.progress is 1 @recentPaths.sort() - console.debug @recentPaths.join('\n') - console.debug 'loaded', @recentPaths.length, 'files,', parseInt(@recentLoadedBytes/1024), 'KB' +# console.debug @recentPaths.join('\n') +# console.debug 'loaded', @recentPaths.length, 'files,', parseInt(@recentLoadedBytes/1024), 'KB' @trigger 'load-complete' @trigger 'loaded', e.item From c8e7b79e5dd94ae1b4107c964fc7c4fef462dc70 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 26 May 2016 17:39:56 -0700 Subject: [PATCH 166/167] Make demoEl run _.once --- app/views/TestView.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/TestView.coffee b/app/views/TestView.coffee index 5475b6791..e2ad5d6f4 100644 --- a/app/views/TestView.coffee +++ b/app/views/TestView.coffee @@ -88,7 +88,7 @@ module.exports = TestView = class TestView extends RootView application.testing = true specFiles ?= @getAllSpecFiles() if demosOn - jasmine.demoEl = ($el) -> + jasmine.demoEl = _.once ($el) -> $('#demo-area').append($el) jasmine.demoModal = _.once (modal) -> currentView.openModalView(modal) From 86fc4a3846ac3ac272b99bade927cc8a71cd47a6 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 26 May 2016 17:46:49 -0700 Subject: [PATCH 167/167] Hints v1 Add per-level tips and tricks, available during gameplay to help unstick players. Closes #3736 --- app/locale/en.coffee | 2 + app/models/User.coffee | 12 +++ app/styles/play/level/hints-view.sass | 41 ++++++++ ...l_palette.sass => spell-palette-view.sass} | 3 - .../play/level/tome/spell_list_entry.sass | 27 ++---- .../play/{level.sass => play-level-view.sass} | 7 ++ app/templates/play/level/hints-view.jade | 21 +++++ .../play/level/tome/spell-palette-view.jade | 36 +++++++ .../play/level/tome/spell_list_tab_entry.jade | 14 +-- .../play/level/tome/spell_palette.jade | 37 -------- .../play/{level.jade => play-level-view.jade} | 2 + app/views/play/level/HintsState.coffee | 30 ++++++ app/views/play/level/HintsView.coffee | 94 +++++++++++++++++++ app/views/play/level/PlayLevelView.coffee | 8 +- app/views/play/level/tome/Spell.coffee | 7 +- .../play/level/tome/SpellListEntryView.coffee | 25 ----- .../level/tome/SpellListTabEntryView.coffee | 9 +- .../play/level/tome/SpellPaletteView.coffee | 12 +-- app/views/play/level/tome/TomeView.coffee | 3 +- .../views/play/level/HintsView.spec.coffee | 61 ++++++++++++ 20 files changed, 347 insertions(+), 104 deletions(-) create mode 100644 app/styles/play/level/hints-view.sass rename app/styles/play/level/tome/{spell_palette.sass => spell-palette-view.sass} (98%) rename app/styles/play/{level.sass => play-level-view.sass} (98%) create mode 100644 app/templates/play/level/hints-view.jade create mode 100644 app/templates/play/level/tome/spell-palette-view.jade delete mode 100644 app/templates/play/level/tome/spell_palette.jade rename app/templates/play/{level.jade => play-level-view.jade} (98%) create mode 100644 app/views/play/level/HintsState.coffee create mode 100644 app/views/play/level/HintsView.coffee create mode 100644 test/app/views/play/level/HintsView.spec.coffee diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 60842638d..3558d4236 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -440,6 +440,8 @@ tome_available_spells: "Available Spells" tome_your_skills: "Your Skills" tome_current_method: "Current Method" + hints: "Hints" + hints_title: "Hint {{number}}" code_saved: "Code Saved" skip_tutorial: "Skip (esc)" keyboard_shortcuts: "Key Shortcuts" diff --git a/app/models/User.coffee b/app/models/User.coffee index 6f7e98c2a..de4289ac6 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -179,6 +179,18 @@ module.exports = class User extends CocoModel application.tracker.identify fourthLevelGroup: @fourthLevelGroup unless me.isAdmin() @fourthLevelGroup + getHintsGroup: -> + # A/B testing two styles of hints + return @hintsGroup if @hintsGroup + group = me.get('testGroupNumber') % 3 + @hintsGroup = switch group + when 0 then 'no-hints' + when 1 then 'hints' + when 2 then 'hintsB' + @hintsGroup = 'hints' if me.isAdmin() + application.tracker.identify hintsGroup: @hintsGroup unless me.isAdmin() + @hintsGroup + getVideoTutorialStylesIndex: (numVideos=0)-> # A/B Testing video tutorial styles # Not a constant number of videos available (e.g. could be 0, 1, 3, or 4 currently) diff --git a/app/styles/play/level/hints-view.sass b/app/styles/play/level/hints-view.sass new file mode 100644 index 000000000..4cb9583a5 --- /dev/null +++ b/app/styles/play/level/hints-view.sass @@ -0,0 +1,41 @@ +@import "app/styles/mixins" + +.hints-view + position: relative + + width: 500px // TODO: should be in sync with surface min-width + padding: 10px 20px + border-style: solid + border-image: url(/images/level/popover_border_background.png) 16 12 fill round + border-width: 16px 12px + @include box-shadow(0 0 0 #000) + + .close-hint-btn + position: absolute + right: 5px + top: 5px + + .glyphicon-remove + position: relative + top: 4px + + h1 + margin-bottom: 30px + + .btn-area + margin-top: 20px + + .hint-title + font-size: 18px + text-transform: uppercase + + .hint-body + height: 390px + overflow-y: auto + img + width: 100% + + .hint-pagination + font-size: 18px + margin-top: 0px + text-transform: uppercase diff --git a/app/styles/play/level/tome/spell_palette.sass b/app/styles/play/level/tome/spell-palette-view.sass similarity index 98% rename from app/styles/play/level/tome/spell_palette.sass rename to app/styles/play/level/tome/spell-palette-view.sass index 287f4e8e8..1ba004ff4 100644 --- a/app/styles/play/level/tome/spell_palette.sass +++ b/app/styles/play/level/tome/spell-palette-view.sass @@ -78,9 +78,6 @@ @include flex-column() @include flex-align-content-start() - &.no-help - margin-top: 3% - .property-entry-item-group display: inline-block min-height: 38px diff --git a/app/styles/play/level/tome/spell_list_entry.sass b/app/styles/play/level/tome/spell_list_entry.sass index 9599039cb..3f7e3e4df 100644 --- a/app/styles/play/level/tome/spell_list_entry.sass +++ b/app/styles/play/level/tome/spell_list_entry.sass @@ -85,27 +85,18 @@ .glyphicon-fullscreen display: none + .hints-button + float: right + border-style: solid + border-image: url(/images/common/button-background-primary-active.png) 14 20 20 20 fill round + border-width: 7px 10px 10px 10px + color: white + &:hover, &:active + border-image: url(/images/common/button-background-primary-pressed.png) 14 20 20 20 fill round + .thang-avatar-wrapper border-width: 0 - .method-name-area - margin-left: 10px - margin-top: 10px - text-transform: uppercase - display: inline-block - font-family: $headings-font-family - font-weight: bold - - .method-label - font-size: 12px - color: rgb(243, 211, 59) - margin-bottom: -5px - - .method-signature - color: white - font-size: 18px - padding: 0 - .spell-list-entry-view:not(.spell-tab) cursor: pointer @include opacity(0.90) diff --git a/app/styles/play/level.sass b/app/styles/play/play-level-view.sass similarity index 98% rename from app/styles/play/level.sass rename to app/styles/play/play-level-view.sass index fb3f236bc..8b3c027ae 100644 --- a/app/styles/play/level.sass +++ b/app/styles/play/play-level-view.sass @@ -264,6 +264,13 @@ $level-resize-transition-time: 0.5s width: 100% height: 90px text-align: center + + .hints-view + position: absolute + top: 10px + bottom: 10px + right: 45% + z-index: 1000000 html.fullscreen-editor #level-view diff --git a/app/templates/play/level/hints-view.jade b/app/templates/play/level/hints-view.jade new file mode 100644 index 000000000..0d023d131 --- /dev/null +++ b/app/templates/play/level/hints-view.jade @@ -0,0 +1,21 @@ + +button.close-hint-btn.btn.btn-illustrated.btn-danger + span.glyphicon.glyphicon-remove + +h1.text-center.hint-title + span= view.state.get('hintsTitle') + +.hint-body + != view.getProcessedHint() + +.row.btn-area + .col-md-4 + if view.state.get('hintIndex') > 0 + button.previous-btn.btn.btn-illustrated.pull-left(data-i18n="about.previous") + .col-md-4 + h2.text-center.hint-pagination #{view.state.get('hintIndex')+1} / #{view.hintsState.get('total')} + .col-md-4 + if view.state.get('hintIndex') < view.hintsState.get('total') - 1 + button.next-btn.btn.btn-illustrated.pull-right(data-i18n="about.next") + +.clearfix diff --git a/app/templates/play/level/tome/spell-palette-view.jade b/app/templates/play/level/tome/spell-palette-view.jade new file mode 100644 index 000000000..7a29bf367 --- /dev/null +++ b/app/templates/play/level/tome/spell-palette-view.jade @@ -0,0 +1,36 @@ + +div + span.code-palette-background + if view.entryGroupSlugs + // Non-hero; group by entry groups, or maybe nothing. + ul(class="nav nav-pills" + (tabbed ? ' multiple-tabs' : '')) + each slug, group in view.entryGroupSlugs + li(class=group == "this" || slug == "available-spells" ? "active" : "") + a(data-toggle="pill", data-target='#palette-tab-' + slug) + h4= view.entryGroupNames[group] + .tab-content + each slug, group in view.entryGroupSlugs + div(id="palette-tab-" + slug, class="tab-pane nano" + (group == "this" || slug == view.defaultGroupSlug ? " active" : "")) + div(class="properties properties-" + slug + " nano-content") + + else if view.tabs + // Hero; group by items, but also include tabs + ul(class="nav nav-pills multiple-tabs") + li.active + a(data-toggle="pill", data-target="#palette-tab-this") + h4= view.thisName + each entries, tab in view.tabs + li + a(data-toggle="pill", data-target='#palette-tab-' + _.string.slugify(tab)) + h4= tab + .tab-content + div#palette-tab-this.tab-pane.active + .properties.properties-this + each entries, tab in tabs + div(id="palette-tab-" + _.string.slugify(tab), class="tab-pane") + div(class="properties properties-" + _.string.slugify(tab)) + + else + // Hero; group by items, no tabs. + br + .properties.properties-this diff --git a/app/templates/play/level/tome/spell_list_tab_entry.jade b/app/templates/play/level/tome/spell_list_tab_entry.jade index d1144f3a0..5f5b02af9 100644 --- a/app/templates/play/level/tome/spell_list_tab_entry.jade +++ b/app/templates/play/level/tome/spell_list_tab_entry.jade @@ -9,22 +9,22 @@ if includeSpellList .thang-avatar-placeholder -.method-name-area - .method-label(data-i18n="play_level.tome_current_method") Current Method - .method-signature #{methodSignature} - .spell-tool-buttons .btn.btn-small.btn-illustrated.btn-warning.reload-code(data-i18n="[title]play_level.tome_reload_method", title="Reload original code for this method") .glyphicon.glyphicon-repeat span.spl(data-i18n="play_level.reload") Reload - + if me.level() >= 15 .btn.btn-small.btn-illustrated.fullscreen-code(title=maximizeShortcutVerbose) .glyphicon.glyphicon-fullscreen .glyphicon.glyphicon-resize-small - + if codeLanguage === 'javascript' && me.level() >= 15 .btn.btn-small.btn-illustrated.beautify-code(title=beautifyShortcutVerbose) .glyphicon.glyphicon-magnet - .clearfix \ No newline at end of file + if view.hintsState && view.hintsState.get('total') > 0 + .btn.btn-small.btn-illustrated.hints-button + span(data-i18n="play_level.hints") + + .clearfix diff --git a/app/templates/play/level/tome/spell_palette.jade b/app/templates/play/level/tome/spell_palette.jade deleted file mode 100644 index 094dc7272..000000000 --- a/app/templates/play/level/tome/spell_palette.jade +++ /dev/null @@ -1,37 +0,0 @@ -span.code-palette-background -if entryGroupSlugs - // Non-hero; group by entry groups, or maybe nothing. - ul(class="nav nav-pills" + (tabbed ? ' multiple-tabs' : '')) - each slug, group in entryGroupSlugs - li(class=group == "this" || slug == "available-spells" ? "active" : "") - a(data-toggle="pill", data-target='#palette-tab-' + slug) - h4= entryGroupNames[group] - .tab-content - each slug, group in entryGroupSlugs - div(id="palette-tab-" + slug, class="tab-pane nano" + (group == "this" || slug == defaultGroupSlug ? " active" : "")) - div(class="properties properties-" + slug + " nano-content") - -else if tabs - // Hero; group by items, but also include tabs - ul(class="nav nav-pills multiple-tabs") - li.active - a(data-toggle="pill", data-target="#palette-tab-this") - h4= thisName - each entries, tab in tabs - li - a(data-toggle="pill", data-target='#palette-tab-' + _.string.slugify(tab)) - h4= tab - .tab-content - div#palette-tab-this.tab-pane.active - .properties.properties-this - each entries, tab in tabs - div(id="palette-tab-" + _.string.slugify(tab), class="tab-pane") - div(class="properties properties-" + _.string.slugify(tab)) - -else - // Hero; group by items, no tabs. - if showsHelp - button.btn.btn-sm.btn-info.banner#spell-palette-help-button(data-i18n="common.help") - .properties.properties-this - else - .properties.properties-this.no-help diff --git a/app/templates/play/level.jade b/app/templates/play/play-level-view.jade similarity index 98% rename from app/templates/play/level.jade rename to app/templates/play/play-level-view.jade index 5bf5a8efe..a225ea79b 100644 --- a/app/templates/play/level.jade +++ b/app/templates/play/play-level-view.jade @@ -49,6 +49,8 @@ if view.showAds() button.btn.btn-lg.btn-warning.banner.header-font#stop-real-time-playback-button(title="Stop real-time playback", data-i18n="play_level.skip") Skip + .hints-view.hide + #level-footer-shadow #level-footer-background diff --git a/app/views/play/level/HintsState.coffee b/app/views/play/level/HintsState.coffee new file mode 100644 index 000000000..bf0940a05 --- /dev/null +++ b/app/views/play/level/HintsState.coffee @@ -0,0 +1,30 @@ +module.exports = class HintsState extends Backbone.Model + + initialize: (attributes, options) -> + { @level, @session } = options + @listenTo(@level, 'change:documentation', @update) + @update() + + getHint: (index) -> + @get('hints')?[index] + + update: -> + hints = switch me.getHintsGroup() + when 'hints' then @level.get('documentation')?.hints or [] + when 'hintsB' then @level.get('documentation')?.hintsB or [] + else [] + haveIntro = false + haveOverview = false + for article in @level.get('documentation')?.specificArticles ? [] + if not haveIntro and article.name is 'Intro' + hints.unshift(article) + haveIntro = true + if not haveOverview and article.name is 'Overview' + hints.push(article) + haveOverview = true + break if haveIntro and haveOverview + total = _.size(hints) + @set({ + hints: hints + total + }) diff --git a/app/views/play/level/HintsView.coffee b/app/views/play/level/HintsView.coffee new file mode 100644 index 000000000..8c36add43 --- /dev/null +++ b/app/views/play/level/HintsView.coffee @@ -0,0 +1,94 @@ +CocoView = require 'views/core/CocoView' +State = require 'models/State' +utils = require 'core/utils' + +module.exports = class HintsView extends CocoView + template: require('templates/play/level/hints-view') + className: 'hints-view' + hintUsedThresholdSeconds: 10 + + events: + 'click .next-btn': 'onClickNextButton' + 'click .previous-btn': 'onClickPreviousButton' + 'click .close-hint-btn': 'hideView' + + subscriptions: + 'level:show-victory': 'hideView' + 'tome:manual-cast': 'hideView' + + initialize: (options) -> + {@level, @session, @hintsState} = options + @state = new State({ + hintIndex: 0 + hintsViewTime: {} + hintsUsed: {} + }) + @updateHint() + + debouncedRender = _.debounce(@render) + @listenTo(@state, 'change', debouncedRender) + @listenTo(@hintsState, 'change', debouncedRender) + @listenTo(@state, 'change:hintIndex', @updateHint) + @listenTo(@hintsState, 'change:hidden', @visibilityChanged) + + destroy: -> + clearInterval(@timerIntervalID) + super() + + afterRender: -> + @$el.toggleClass('hide', @hintsState.get('hidden')) + super() + + getProcessedHint: -> + language = @session.get('codeLanguage') + hint = @state.get('hint') + return unless hint + + # process + translated = utils.i18n(hint, 'body') + filtered = utils.filterMarkdownCodeLanguages(translated, language) + markedUp = marked(filtered) + + return markedUp + + updateHint: -> + index = @state.get('hintIndex') + hintsTitle = $.i18n.t('play_level.hints_title').replace('{{number}}', index + 1) + @state.set({ hintsTitle, hint: @hintsState.getHint(index) }) + + onClickNextButton: -> + window.tracker?.trackEvent 'Hints Next Clicked', category: 'Students', levelSlug: @level.get('slug'), hintCount: @hintsState.get('hints')?.length ? 0, hintCurrent: @state.get('hintIndex'), ['Mixpanel'] + max = @hintsState.get('total') - 1 + @state.set('hintIndex', Math.min(@state.get('hintIndex') + 1, max)) + @playSound 'menu-button-click' + @updateHintTimer() + + onClickPreviousButton: -> + window.tracker?.trackEvent 'Hints Previous Clicked', category: 'Students', levelSlug: @level.get('slug'), hintCount: @hintsState.get('hints')?.length ? 0, hintCurrent: @state.get('hintIndex'), ['Mixpanel'] + @state.set('hintIndex', Math.max(@state.get('hintIndex') - 1, 0)) + @playSound 'menu-button-click' + @updateHintTimer() + + hideView: -> @hintsState?.set('hidden', true) + + visibilityChanged: (e) -> + @updateHintTimer() + + updateHintTimer: -> + clearInterval(@timerIntervalID) + unless @hintsState.get('hidden') or @state.get('hintsUsed')?[@state.get('hintIndex')] + @timerIntervalID = setInterval(@incrementHintViewTime, 1000) + + incrementHintViewTime: => + hintIndex = @state.get('hintIndex') + hintsViewTime = @state.get('hintsViewTime') + hintsViewTime[hintIndex] ?= 0 + hintsViewTime[hintIndex]++ + hintsUsed = @state.get('hintsUsed') + if hintsViewTime[hintIndex] > @hintUsedThresholdSeconds and not hintsUsed[hintIndex] + window.tracker?.trackEvent 'Hint Used', category: 'Students', levelSlug: @level.get('slug'), hintCount: @hintsState.get('hints')?.length ? 0, hintCurrent: hintIndex, ['Mixpanel'] + hintsUsed[hintIndex] = true + @state.set('hintsUsed', hintsUsed) + clearInterval(@timerIntervalID) + @state.set('hintsViewTime', hintsViewTime) + diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index 8911ba832..249a92ebd 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -1,5 +1,5 @@ RootView = require 'views/core/RootView' -template = require 'templates/play/level' +template = require 'templates/play/play-level-view' {me} = require 'core/auth' ThangType = require 'models/ThangType' utils = require 'core/utils' @@ -41,6 +41,8 @@ PicoCTFVictoryModal = require './modal/PicoCTFVictoryModal' InfiniteLoopModal = require './modal/InfiniteLoopModal' LevelSetupManager = require 'lib/LevelSetupManager' ContactModal = require 'views/core/ContactModal' +HintsView = require './HintsView' +HintsState = require './HintsState' PROFILE_ME = false @@ -259,7 +261,8 @@ module.exports = class PlayLevelView extends RootView @god.setGoalManager @goalManager insertSubviews: -> - @insertSubView @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel, level: @level, observing: @observing, courseID: @courseID, courseInstanceID: @courseInstanceID, god: @god + @hintsState = new HintsState({ hidden: true }, { @session, @level }) + @insertSubView @tome = new TomeView { @levelID, @session, @otherSession, thangs: @world.thangs, @supermodel, @level, @observing, @courseID, @courseInstanceID, @god, @hintsState } @insertSubView new LevelPlaybackView session: @session, level: @level @insertSubView new GoalsView {} @insertSubView new LevelFlagsView levelID: @levelID, world: @world if @$el.hasClass 'flags' @@ -270,6 +273,7 @@ module.exports = class PlayLevelView extends RootView @insertSubView new ProblemAlertView session: @session, level: @level, supermodel: @supermodel @insertSubView new DuelStatsView level: @level, session: @session, otherSession: @otherSession, supermodel: @supermodel, thangs: @world.thangs if @level.get('type') in ['hero-ladder', 'course-ladder'] @insertSubView @controlBar = new ControlBarView {worldName: utils.i18n(@level.attributes, 'name'), session: @session, level: @level, supermodel: @supermodel, courseID: @courseID, courseInstanceID: @courseInstanceID} + @insertSubView @hintsView = new HintsView({ @session, @level, @hintsState }), @$('.hints-view') #_.delay (=> Backbone.Mediator.publish('level:set-debug', debug: true)), 5000 if @isIPadApp() # if me.displayName() is 'Nick' initVolume: -> diff --git a/app/views/play/level/tome/Spell.coffee b/app/views/play/level/tome/Spell.coffee index 358f2f6df..71b68fb3d 100644 --- a/app/views/play/level/tome/Spell.coffee +++ b/app/views/play/level/tome/Spell.coffee @@ -51,7 +51,12 @@ module.exports = class Spell if @canRead() # We can avoid creating these views if we'll never use them. @view = new SpellView {spell: @, level: options.level, session: @session, otherSession: @otherSession, worker: @worker, god: options.god, @supermodel} @view.render() # Get it ready and code loaded in advance - @tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, codeLanguage: @language, level: options.level + @tabView = new SpellListTabEntryView + hintsState: options.hintsState + spell: @ + supermodel: @supermodel + codeLanguage: @language + level: options.level @tabView.render() Backbone.Mediator.publish 'tome:spell-created', spell: @ diff --git a/app/views/play/level/tome/SpellListEntryView.coffee b/app/views/play/level/tome/SpellListEntryView.coffee index 7dc00f9b6..92c5462d0 100644 --- a/app/views/play/level/tome/SpellListEntryView.coffee +++ b/app/views/play/level/tome/SpellListEntryView.coffee @@ -31,35 +31,10 @@ module.exports = class SpellListEntryView extends CocoView getRenderData: (context={}) -> context = super context context.spell = @spell - context.methodSignature = @createMethodSignature() context.thangNames = (thangID for thangID, spellThang of @spell.thangs when spellThang.thang.exists).join(', ') # + ', Marcus, Robert, Phoebe, Will Smith, Zap Brannigan, You, Gandaaaaalf' context.showTopDivider = @showTopDivider context - createMethodSignature: -> - return @spell.name if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev'] - parameters = (@spell.parameters or []).slice() - if @spell.language in ['python', 'lua'] - parameters.unshift 'self' - else if @spell.language is 'io' - parameters.unshift '...' - paramString = parameters.join ', ' - name = @spell.name - switch @spell.language - when 'python' - "def #{name}(#{paramString}):" - when 'lua' - "function #{name}(#{paramString}) ... end" - when 'coffeescript' - if parameters.length - "@#{name} = (#{paramString}) ->" - else - "@#{name} = ->" - when 'javascript' - "function #{name}(#{paramString}) { ... }" - else - "#{name}(#{paramString})" - getPrimarySpellThang: -> if @lastSelectedThang spellThang = _.find @spell.thangs, (spellThang) => spellThang.thang.id is @lastSelectedThang.id diff --git a/app/views/play/level/tome/SpellListTabEntryView.coffee b/app/views/play/level/tome/SpellListTabEntryView.coffee index 23a012565..b15798bbb 100644 --- a/app/views/play/level/tome/SpellListTabEntryView.coffee +++ b/app/views/play/level/tome/SpellListTabEntryView.coffee @@ -23,9 +23,11 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView 'click .reload-code': 'onCodeReload' 'click .beautify-code': 'onBeautifyClick' 'click .fullscreen-code': 'onToggleMaximize' + 'click .hints-button': 'onClickHintsButton' constructor: (options) -> - super options + @hintsState = options.hintsState + super(options) getRenderData: (context={}) -> context = super context @@ -91,6 +93,11 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView onDisableControls: (e) -> @toggleControls e, false onEnableControls: (e) -> @toggleControls e, true + onClickHintsButton: -> + return unless @hintsState? + @hintsState.set('hidden', not @hintsState.get('hidden')) + window.tracker?.trackEvent 'Hints Clicked', category: 'Students', levelSlug: @options.level.get('slug'), hintCount: @hintsState.get('hints')?.length ? 0, ['Mixpanel'] + onDropdownClick: (e) -> return unless @controlsEnabled Backbone.Mediator.publish 'tome:toggle-spell-list', {} diff --git a/app/views/play/level/tome/SpellPaletteView.coffee b/app/views/play/level/tome/SpellPaletteView.coffee index be4abdc59..87f7b21f4 100644 --- a/app/views/play/level/tome/SpellPaletteView.coffee +++ b/app/views/play/level/tome/SpellPaletteView.coffee @@ -1,5 +1,4 @@ CocoView = require 'views/core/CocoView' -template = require 'templates/play/level/tome/spell_palette' {me} = require 'core/auth' filters = require 'lib/image_filter' SpellPaletteEntryView = require './SpellPaletteEntryView' @@ -12,7 +11,7 @@ N_ROWS = 4 module.exports = class SpellPaletteView extends CocoView id: 'spell-palette-view' - template: template + template: require 'templates/play/level/tome/spell-palette-view' controlsEnabled: true subscriptions: @@ -24,13 +23,8 @@ module.exports = class SpellPaletteView extends CocoView events: 'click #spell-palette-help-button': 'onClickHelp' - constructor: (options) -> - super options - @level = options.level - @session = options.session - @supermodel = options.supermodel - @thang = options.thang - @useHero = options.useHero + initialize: (options) -> + {@level, @session, @supermodel, @thang, @useHero} = options docs = @options.level.get('documentation') ? {} @showsHelp = docs.specificArticles?.length or docs.generalArticles?.length @createPalette() diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee index f15233079..98a11b42d 100644 --- a/app/views/play/level/tome/TomeView.coffee +++ b/app/views/play/level/tome/TomeView.coffee @@ -122,6 +122,7 @@ module.exports = class TomeView extends CocoView unless method.cloneOf skipProtectAPI = @getQueryVariable 'skip_protect_api', (@options.levelID in ['gridmancer', 'minimax-tic-tac-toe']) spell = @spells[spellKey] = new Spell + hintsState: @options.hintsState programmableMethod: method spellKey: spellKey pathComponents: pathPrefixComponents.concat(pathComponents) @@ -219,7 +220,7 @@ module.exports = class TomeView extends CocoView updateSpellPalette: (thang, spell) -> return unless thang and @spellPaletteView?.thang isnt thang and thang.programmableProperties or thang.apiProperties useHero = /hero/.test(spell.getSource()) or not /(self[\.\:]|this\.|\@)/.test(spell.getSource()) - @spellPaletteView = @insertSubView new SpellPaletteView thang: thang, supermodel: @supermodel, programmable: spell?.canRead(), language: spell?.language ? @options.session.get('codeLanguage'), session: @options.session, level: @options.level, courseID: @options.courseID, courseInstanceID: @options.courseInstanceID, useHero: useHero + @spellPaletteView = @insertSubView new SpellPaletteView { thang, @supermodel, programmable: spell?.canRead(), language: spell?.language ? @options.session.get('codeLanguage'), session: @options.session, level: @options.level, courseID: @options.courseID, courseInstanceID: @options.courseInstanceID, useHero } @spellPaletteView.toggleControls {}, spell.view.controlsEnabled if spell?.view # TODO: know when palette should have been disabled but didn't exist spellFor: (thang, spellName) -> diff --git a/test/app/views/play/level/HintsView.spec.coffee b/test/app/views/play/level/HintsView.spec.coffee new file mode 100644 index 000000000..8a053c61b --- /dev/null +++ b/test/app/views/play/level/HintsView.spec.coffee @@ -0,0 +1,61 @@ +HintsView = require 'views/play/level/HintsView' +factories = require 'test/app/factories' + +hintWithCode = """ +Hint #2 rosebud + +```python +print('Hello World') +``` + +```javascript +console.log('Hello World') +``` +""" + +longHint = _.times(100, -> 'Beuller...').join('\n\n') + +xdescribe 'HintsView', -> + beforeEach -> + level = factories.makeLevel({ + documentation: { + hints: [ + { body: 'Hint #1 xyzzy' } + { body: hintWithCode } + { body: longHint } + ] + } + }) + @session = factories.makeLevelSession({ playtime: 0 }) + @view = new HintsView({ level, @session }) + @view.render() + jasmine.demoEl(@view.$el) + + describe 'when the first hint is shown', -> + + it 'does not show the previous button', -> + expect(@view.$el.find('.previous-btn').length).toBe(0) + + describe 'when the user has played for a while', -> + + beforeEach -> + @view.render() + + it 'shows the first hint', -> + expect(_.string.contains(@view.$el.text(), 'xyzzy')).toBe(true) + + it 'shows the next hint button', -> + expect(@view.$el.find('.next-btn').length).toBe(1) + + it 'filters out all code blocks but those of the selected language', -> + @session.set({ + codeLanguage: 'javascript' + playtime: 9001 + }) + @view.state.set('hintIndex', 1) + @view.render() + + if _.string.contains(@view.$el.text(), 'print') + fail('Python code snippet found, should be filtered out') + if not _.string.contains(@view.$el.text(), 'console') + fail('JavaScript code snippet not found')