From ddbc6a70e2d478f78da0dc42369e994ba6295363 Mon Sep 17 00:00:00 2001 From: duybkict Date: Thu, 19 May 2016 17:05:18 +0700 Subject: [PATCH 01/25] 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 82f4419b6..eff9caf2c 100644 --- a/package.json +++ b/package.json @@ -93,19 +93,19 @@ "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", - "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", @@ -121,7 +121,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 36fdd79b6a1156ad30cd21b1113675568f888138 Mon Sep 17 00:00:00 2001 From: duybkict Date: Thu, 26 May 2016 15:44:35 +0700 Subject: [PATCH 02/25] add new vagrant box "brunchv2", revert npm package upgrades in package.json --- Vagrantfile | 10 +++++++++- package.json | 14 +++++++------- scripts/vagrant/core/update-brunchv2.sh | 13 +++++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 scripts/vagrant/core/update-brunchv2.sh diff --git a/Vagrantfile b/Vagrantfile index d79665c3c..a29e8fa74 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -17,7 +17,15 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.network "forwarded_port", guest: 3000, host: 13000 config.vm.network "forwarded_port", guest: 9485, host: 19485 - config.vm.provision "shell", path: "scripts/vagrant/core/provision.sh", privileged: false + config.vm.define "default" do |default| + default.vm.provision "shell", path: "scripts/vagrant/core/provision.sh", privileged: false + + end + + config.vm.define "brunchv2", autostart: false do |brunchv2| + brunchv2.vm.provision "shell", path: "scripts/vagrant/core/provision.sh", privileged: false + brunchv2.vm.provision "shell", path: "scripts/vagrant/core/update-brunchv2.sh", privileged: false + end config.vm.provider "virtualbox" do |v| v.memory = 2048 diff --git a/package.json b/package.json index 1475f6ff1..2844cd103 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": ">=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", - "css-brunch": ">=2.0.0", + "css-brunch": "^1.7.0", "country-data": "0.0.24", "country-list": "0.0.3", "fs-extra": "^0.26.2", "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" }, diff --git a/scripts/vagrant/core/update-brunchv2.sh b/scripts/vagrant/core/update-brunchv2.sh new file mode 100644 index 000000000..93e958970 --- /dev/null +++ b/scripts/vagrant/core/update-brunchv2.sh @@ -0,0 +1,13 @@ +#!/bin/bash -e +# Original content copyright (c) 2014 dpen2000 licensed under the MIT license + +echo "updating brunch to v2..." +cd /vagrant +npm install \ + brunch@">=2.0.0" \ + auto-reload-brunch@">=2.0.0" \ + coffee-script-brunch@">=2.0.0" \ + coffeelint-brunch@">=2.0.0" \ + css-brunch@">=2.0.0" \ + javascript-brunch@">=2.0.0" \ + sass-brunch@">=2.0.0" --no-bin-links \ No newline at end of file From 38ebd6111d7e0901722789d0adaf6020603f950d Mon Sep 17 00:00:00 2001 From: duybkict Date: Thu, 26 May 2016 16:52:57 +0700 Subject: [PATCH 03/25] minor fixes --- Vagrantfile | 1 - package.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index a29e8fa74..22228c451 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -19,7 +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 - end config.vm.define "brunchv2", autostart: false do |brunchv2| diff --git a/package.json b/package.json index 2844cd103..fc326df34 100644 --- a/package.json +++ b/package.json @@ -100,9 +100,9 @@ "coffeelint-brunch": "^1.7.1", "commonjs-require-definition": "0.2.0", "compressible": "~1.0.1", - "css-brunch": "^1.7.0", "country-data": "0.0.24", "country-list": "0.0.3", + "css-brunch": "^1.7.0", "fs-extra": "^0.26.2", "http-proxy": "^1.13.2", "jade-brunch": "1.7.5", From 7e958639b2b9872d9c2292bcbb7b57297cbd0d56 Mon Sep 17 00:00:00 2001 From: Bryukhanov Valentin Date: Thu, 26 May 2016 19:44:50 +0300 Subject: [PATCH 04/25] Add `choice` method in `rand` (#3682) As in python random module. It can be useful for levels instead "someArray[@world.rand.rand(someArray.length)]". --- app/lib/world/rand.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/lib/world/rand.coffee b/app/lib/world/rand.coffee index 5cc0c8d41..33555290e 100644 --- a/app/lib/world/rand.coffee +++ b/app/lib/world/rand.coffee @@ -48,6 +48,9 @@ class Rand arr[j] = arr[i] arr[i] = t arr + + choice: (arr) => + return arr[@rand arr.length] module.exports = Rand From 770987715e363b3797594b3bdf1a9d53ebcc24c6 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 26 May 2016 10:02:22 -0700 Subject: [PATCH 05/25] Update under 13 account creation copy --- app/locale/en.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 504663152..d546acda9 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -1767,7 +1767,7 @@ coppa_deny: text1: "Can’t wait to learn programming?" - text2: "Ask your parents to create an account for you!" + text2: "Your parents will need to create an account for you to use! Email team@codecombat.com if you have any questions." # {change} close: "Close Window" loading_error: From 886ddd381d413e9ae7f04b1ab7f4caec64186809 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 26 May 2016 10:50:35 -0700 Subject: [PATCH 06/25] Add getPrepaidsFor script for updating start/end dates --- scripts/mongodb/{ => stored}/deteacher.js | 4 +- scripts/mongodb/stored/getPrepaidsFor.js | 57 +++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) rename scripts/mongodb/{ => stored}/deteacher.js (99%) create mode 100644 scripts/mongodb/stored/getPrepaidsFor.js diff --git a/scripts/mongodb/deteacher.js b/scripts/mongodb/stored/deteacher.js similarity index 99% rename from scripts/mongodb/deteacher.js rename to scripts/mongodb/stored/deteacher.js index bb1dac2e3..28dd936af 100644 --- a/scripts/mongodb/deteacher.js +++ b/scripts/mongodb/stored/deteacher.js @@ -28,11 +28,11 @@ var deteacher = function deteacher(email) { else { print('Unset role', db.users.update({_id: user._id}, {$unset: {role: ''}})); } -} +}; db.system.js.save( { _id: 'deteacher', value: deteacher } -) +); diff --git a/scripts/mongodb/stored/getPrepaidsFor.js b/scripts/mongodb/stored/getPrepaidsFor.js new file mode 100644 index 000000000..08a5e7811 --- /dev/null +++ b/scripts/mongodb/stored/getPrepaidsFor.js @@ -0,0 +1,57 @@ +// Script for changing prepaid start/end dates and propagating them to users. + +/* + * Usage + * --------------- + * In mongo shell + * + * > db.loadServerScripts(); + * > var prepaids = getPrepaidsFor('some@email.com'); // prints basic stats for prepaids found + * > prepaids.models // Raw prepaid data + * > prepaids.setStart(2001,1,1) // Set start date + * > prepaids.setEnd(2100,1,1) // Set end date + */ + + +function getPrepaidsFor(email) { + var user = db.users.findOne({emailLower: email.toLowerCase()}); + if (!user) { + print('User not found'); + return; + } + + var result = {}; + result.models = db.prepaids.find({creator: user._id}).toArray(); + result.setStart = function(year, month, day) { + var startDate = new Date(Date.UTC(year, month-1, day)).toISOString(); + print('setting to', startDate); + for (var i in this.models) { + var prepaid = this.models[i]; + print('Prepaid update', db.prepaids.update({_id: prepaid._id}, {$set: {startDate: startDate}})); + print('User update', db.users.update({'coursePrepaid._id': prepaid._id}, {$set: {'coursePrepaid.startDate': startDate}}, {multi: true})); + } + }; + result.setEnd = function(year, month, day) { + var endDate = new Date(Date.UTC(year, month-1, day)).toISOString(); + print('setting to', endDate); + for (var i in this.models) { + var prepaid = this.models[i]; + print('Prepaid update', db.prepaids.update({_id: prepaid._id}, {$set: {endDate: endDate}})); + print('User update', db.users.update({'coursePrepaid._id': prepaid._id}, {$set: {'coursePrepaid.endDate': endDate}}, {multi: true})); + } + }; + + for (var i in result.models) { + var prepaid = result.models[i]; + print('Prepaid:', prepaid.startDate, 'to', prepaid.endDate, 'with', prepaid.redeemers.length, '/', prepaid.maxRedeemers, 'uses'); + } + return result; +} + + +db.system.js.save( + { + _id: 'getPrepaidsFor', + value: getPrepaidsFor + } +); From feeca7a586a4a65319f2d2be36f16b60c04a83a8 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Thu, 26 May 2016 13:34:36 -0700 Subject: [PATCH 07/25] 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 08/25] 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 09/25] 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 10/25] 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 11/25] 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 12/25] 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 13/25] 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 14/25] 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 15/25] 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 16/25] 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 17/25] 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 18/25] 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 19/25] 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 20/25] 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 21/25] 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 22/25] 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 23/25] :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 24/25] 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 25/25] 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) =>