From 972c632d85d0f9664d12d08d015e0c6602d7bc8f Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 16 Jun 2016 14:32:43 -0700 Subject: [PATCH 1/8] Fix fr.coffee indentation --- app/locale/fr.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/locale/fr.coffee b/app/locale/fr.coffee index a4d27068c..14839df57 100644 --- a/app/locale/fr.coffee +++ b/app/locale/fr.coffee @@ -14,7 +14,7 @@ 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: + new_home: slogan: "Le jeu le plus engageant pour apprendre la programmation." classroom_edition: "Édition Classe:" learn_to_code: "Apprend à programmer:" From ca83ed05e484239bdf3c2036f51f7cc07a8eb59d Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Thu, 16 Jun 2016 16:00:45 -0700 Subject: [PATCH 2/8] Only require user sessions on /db requests that are not GET --- server/routes/index.coffee | 5 ++++- spec/server/functional/prepaid.spec.coffee | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/server/routes/index.coffee b/server/routes/index.coffee index c523fcc3d..184ba069b 100644 --- a/server/routes/index.coffee +++ b/server/routes/index.coffee @@ -14,7 +14,10 @@ module.exports.setup = (app) -> app.get('/auth/unsubscribe', mw.auth.unsubscribe) app.get('/auth/whoami', mw.auth.whoAmI) - app.all('/db/*', mw.auth.checkHasUser()) + app.delete('/db/*', mw.auth.checkHasUser()) + app.patch('/db/*', mw.auth.checkHasUser()) + app.post('/db/*', mw.auth.checkHasUser()) + app.put('/db/*', mw.auth.checkHasUser()) Achievement = require '../models/Achievement' app.get('/db/achievement', mw.achievements.fetchByRelated, mw.rest.get(Achievement)) diff --git a/spec/server/functional/prepaid.spec.coffee b/spec/server/functional/prepaid.spec.coffee index 9aaf1fc6d..4baacae41 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(401) + expect(res.statusCode).toEqual(403) done() it 'User can fetch a prepaid code', (done) -> From 0581ffde82cdf7280a36ad881f44a25e04120882 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Fri, 17 Jun 2016 10:35:22 -0700 Subject: [PATCH 3/8] Clean server test logging --- server/commons/logging.coffee | 9 +- .../earned_achievement_handler.coffee | 4 +- server/handlers/payment_handler.coffee | 2 +- server/handlers/subscription_handler.coffee | 2 +- server/handlers/user_handler.coffee | 12 +- server/middleware/courses.coffee | 2 +- server/models/User.coffee | 1 - server_setup.coffee | 2 +- spec/helpers/helper.js | 12 + spec/server/common.coffee | 38 +- spec/server/functional/contact.spec.coffee | 40 +- spec/server/functional/file.spec.coffee | 356 +++++++++--------- .../functional/level_session.spec.coffee | 16 +- spec/server/functional/nocked.spec.coffee | 29 -- spec/server/functional/queue.spec.coffee | 48 +-- .../functional/subscription.spec.coffee | 113 +++--- spec/server/functional/user.spec.coffee | 88 ++--- spec/server/nock-utils.coffee | 2 +- spec/server/unit/analytics.spec.coffee | 154 ++++---- 19 files changed, 445 insertions(+), 485 deletions(-) delete mode 100644 spec/server/functional/nocked.spec.coffee diff --git a/server/commons/logging.coffee b/server/commons/logging.coffee index 537e045e0..d0b7f6972 100644 --- a/server/commons/logging.coffee +++ b/server/commons/logging.coffee @@ -2,7 +2,8 @@ winston = require 'winston' module.exports.setup = -> winston.remove(winston.transports.Console) - winston.add(winston.transports.Console, - colorize: true, - timestamp: true - ) + if not global.testing + winston.add(winston.transports.Console, + colorize: true, + timestamp: true + ) diff --git a/server/handlers/earned_achievement_handler.coffee b/server/handlers/earned_achievement_handler.coffee index 53c3d8979..4a609d5a9 100644 --- a/server/handlers/earned_achievement_handler.coffee +++ b/server/handlers/earned_achievement_handler.coffee @@ -158,7 +158,7 @@ class EarnedAchievementHandler extends Handler onFinished = -> t1 = new Date().getTime() runningTime = ((t1-t0)/1000/60/60).toFixed(2) - console.log "we finished in #{runningTime} hours" + log.info "we finished in #{runningTime} hours" callback arguments... filter = {} @@ -278,7 +278,7 @@ class EarnedAchievementHandler extends Handler #log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}" pointDelta = newTotalPoints - previousPoints pctDone = (100 * usersFinished / total).toFixed(2) - console.log "Updated points to #{newTotalPoints} (#{if pointDelta < 0 then '' else '+'}#{pointDelta}) for #{user.get('name') or '???'} (#{user.get('_id')}) (#{pctDone}%)" + log.info "Updated points to #{newTotalPoints} (#{if pointDelta < 0 then '' else '+'}#{pointDelta}) for #{user.get('name') or '???'} (#{user.get('_id')}) (#{pctDone}%)" if recalculatingAll update = {$set: {points: newTotalPoints, 'earned.gems': 0, 'earned.heroes': [], 'earned.items': [], 'earned.levels': []}} else diff --git a/server/handlers/payment_handler.coffee b/server/handlers/payment_handler.coffee index c43cb77d8..0487d0a8e 100644 --- a/server/handlers/payment_handler.coffee +++ b/server/handlers/payment_handler.coffee @@ -34,7 +34,7 @@ PaymentHandler = class PaymentHandler extends Handler super arguments... logPaymentError: (req, msg) -> - console.warn "Payment Error: #{req.user.get('slug')} (#{req.user._id}): '#{msg}'" + log.warn "Payment Error: #{req.user.get('slug')} (#{req.user._id}): '#{msg}'" makeNewInstance: (req) -> payment = super(req) diff --git a/server/handlers/subscription_handler.coffee b/server/handlers/subscription_handler.coffee index a01421404..cf1fe8134 100644 --- a/server/handlers/subscription_handler.coffee +++ b/server/handlers/subscription_handler.coffee @@ -21,7 +21,7 @@ recipientCouponID = 'free' class SubscriptionHandler extends Handler logSubscriptionError: (user, msg) -> - console.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'" + log.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'" getByRelationship: (req, res, args...) -> return @getStripeEvents(req, res) if args[1] is 'stripe_events' diff --git a/server/handlers/user_handler.coffee b/server/handlers/user_handler.coffee index 455aa90bb..73be9b4ad 100644 --- a/server/handlers/user_handler.coffee +++ b/server/handlers/user_handler.coffee @@ -119,7 +119,7 @@ UserHandler = class UserHandler extends Handler log.error "Database error setting user name: #{err}" if err return callback(res: 'Database error.', code: 500) if err r = {message: 'is already used by another account', property: 'name'} - console.log 'Another user exists' if otherUser + log.info 'Another user exists' if otherUser return callback({res: r, code: 409}) if otherUser user.set('name', req.body.name) callback(null, req, user) @@ -775,7 +775,7 @@ UserHandler = class UserHandler extends Handler else update = $unset: {} update.$unset[statKey] = '' - console.log "... updating #{userStringID} patches #{statKey} to #{count}, #{usersTotal} players found so far." if count + log.info "... updating #{userStringID} patches #{statKey} to #{count}, #{usersTotal} players found so far." if count User.findByIdAndUpdate user.get('_id'), update, (err) -> log.error err if err? doneWithUser() @@ -801,7 +801,7 @@ UserHandler = class UserHandler extends Handler update = {} update[method] = {} update[method][statName] = count or '' - console.log "... updating #{user.get('_id')} patches #{JSON.stringify(query)} #{statName} to #{count}, #{usersTotal} players found so far." if count + log.info "... updating #{user.get('_id')} patches #{JSON.stringify(query)} #{statName} to #{count}, #{usersTotal} players found so far." if count User.findByIdAndUpdate user.get('_id'), update, doneUpdatingUser userStream = User.find({anonymous: false}).sort('_id').stream() @@ -865,7 +865,7 @@ UserHandler = class UserHandler extends Handler update = {} update[method] = {} update[method][statName] = count or '' - console.log "... updating #{userStringID} patches #{query} to #{count}, #{usersTotal} players found so far." if count + log.info "... updating #{userStringID} patches #{query} to #{count}, #{usersTotal} players found so far." if count User.findByIdAndUpdate user.get('_id'), update, doneWithUser statRecalculators: @@ -883,7 +883,7 @@ UserHandler = class UserHandler extends Handler --numberRunning userStream.resume() if streamFinished and usersFinished is usersTotal - console.log "----------- Finished recalculating statistics for gamesCompleted for #{usersFinished} players. -----------" + log.info "----------- Finished recalculating statistics for gamesCompleted for #{usersFinished} players. -----------" done?() userStream.on 'error', (err) -> log.error err userStream.on 'close', -> streamFinished = true @@ -895,7 +895,7 @@ UserHandler = class UserHandler extends Handler LevelSession.count {creator: userID, 'state.complete': true}, (err, count) -> update = if count then {$set: 'stats.gamesCompleted': count} else {$unset: 'stats.gamesCompleted': ''} - console.log "... updating #{userID} gamesCompleted to #{count}, #{usersTotal} players found so far." if Math.random() < 0.001 + log.info "... updating #{userID} gamesCompleted to #{count}, #{usersTotal} players found so far." if Math.random() < 0.001 User.findByIdAndUpdate user.get('_id'), update, doneWithUser articleEdits: (done) -> diff --git a/server/middleware/courses.coffee b/server/middleware/courses.coffee index 678b9f601..85b0a1240 100644 --- a/server/middleware/courses.coffee +++ b/server/middleware/courses.coffee @@ -39,7 +39,7 @@ module.exports = throw new errors.NotFound('Level original ObjectId not found in that course') if not nextLevelOriginal - res.status(200).send({}) + return res.status(200).send({}) dbq = Level.findOne({original: mongoose.Types.ObjectId(nextLevelOriginal)}) diff --git a/server/models/User.coffee b/server/models/User.coffee index fa43f8c32..56cae0eb6 100644 --- a/server/models/User.coffee +++ b/server/models/User.coffee @@ -350,7 +350,6 @@ UserSchema.pre('save', (next) -> Classroom = require './Classroom' if @isTeacher() and not @wasTeacher Classroom.update({members: @_id}, {$pull: {members: @_id}}, {multi: true}).exec (err, res) -> - console.log 'removed self from all classrooms as a member', err, res if email = @get('email') @set('emailLower', email.toLowerCase()) if name = @get('name') diff --git a/server_setup.coffee b/server_setup.coffee index c1adc9b79..4bf4fe632 100644 --- a/server_setup.coffee +++ b/server_setup.coffee @@ -84,7 +84,7 @@ setupExpressMiddleware = (app) -> app.use express.compress filter: (req, res) -> return false if req.headers.host is 'codecombat.com' # CloudFlare will gzip it for us on codecombat.com compressible res.getHeader('Content-Type') - else + else if not global.testing express.logger.format('dev', developmentLogging) app.use(express.logger('dev')) app.use(express.static(path.join(__dirname, 'public'), maxAge: 0)) # CloudFlare overrides maxAge, and we don't want local development caching. diff --git a/spec/helpers/helper.js b/spec/helpers/helper.js index 09be41fa3..095663f95 100644 --- a/spec/helpers/helper.js +++ b/spec/helpers/helper.js @@ -39,11 +39,22 @@ if (database.generateMongoConnectionString() !== dbString) { jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 10; // for long Stripe tests require('../server/common'); // Make sure global testing functions are set up +// Ignore Stripe/Nocking erroring +console.error = function() { + try { + if(arguments[1].stack.indexOf('An error occurred with our connection to Stripe') > -1) + return; + } + catch (e) { } + console.log.apply(console, arguments); +}; + var initialized = false; beforeEach(function(done) { if (initialized) { return done(); } + console.log('/spec/helpers/helper.js - Initializing spec environment...'); var async = require('async'); async.series([ @@ -103,6 +114,7 @@ beforeEach(function(done) { process.exit(1); } initialized = true; + console.log('/spec/helpers/helper.js - Done'); done(); }); }); diff --git a/spec/server/common.coffee b/spec/server/common.coffee index ae6fa307c..29c47d0c3 100644 --- a/spec/server/common.coffee +++ b/spec/server/common.coffee @@ -1,7 +1,7 @@ # import this at the top of every file so we're not juggling connections # and common libraries are available -console.log 'IT BEGINS' +console.log '/spec/server/common.coffee - Setting up spec globals...' if process.env.COCO_MONGO_HOST throw Error('Tests may not run with production environment') @@ -60,7 +60,7 @@ unittest.getUser = (name, email, password, done, force) -> return done(unittest.users[email]) if unittest.users[email] and not force request.post getURL('/auth/logout'), -> request.get getURL('/auth/whoami'), -> - req = request.post(getURL('/db/user'), (err, response, body) -> + req = request.post({url: getURL('/db/user'), json: {email, password}}, (err, response, body) -> throw err if err User.findOne({email: email}).exec((err, user) -> throw err if err @@ -70,9 +70,6 @@ unittest.getUser = (name, email, password, done, force) -> wrapUpGetUser(email, user, done) ) ) - form = req.form() - form.append('email', email) - form.append('password', password) wrapUpGetUser = (email, user, done) -> unittest.users[email] = user @@ -139,58 +136,48 @@ GLOBAL.loginNewUser = (done) -> email = "#{name}@me.com" request.post getURL('/auth/logout'), -> unittest.getUser name, email, password, (user) -> - req = request.post(getURL('/auth/login'), (error, response) -> + json = {username: email, password} + req = request.post({url: getURL('/auth/login'), json}, (error, response) -> expect(response.statusCode).toBe(200) done(user) ) - form = req.form() - form.append('username', email) - form.append('password', password) , true GLOBAL.loginJoe = (done) -> request.post getURL('/auth/logout'), -> unittest.getNormalJoe (user) -> - req = request.post(getURL('/auth/login'), (error, response) -> + json = {username: 'normal@jo.com', password: 'food'} + req = request.post({url: getURL('/auth/login'), json}, (error, response) -> expect(response.statusCode).toBe(200) done(user) ) - form = req.form() - form.append('username', 'normal@jo.com') - form.append('password', 'food') GLOBAL.loginSam = (done) -> request.post getURL('/auth/logout'), -> unittest.getOtherSam (user) -> - req = request.post(getURL('/auth/login'), (error, response) -> + json = { username: 'other@sam.com', password: 'beer'} + req = request.post({url: getURL('/auth/login'), json}, (error, response) -> expect(response.statusCode).toBe(200) done(user) ) - form = req.form() - form.append('username', 'other@sam.com') - form.append('password', 'beer') GLOBAL.loginAdmin = (done) -> request.post getURL('/auth/logout'), -> unittest.getAdmin (user) -> - req = request.post(getURL('/auth/login'), (error, response) -> + json = { username: 'admin@afc.com', password: '80yqxpb38j' } + req = request.post({url: getURL('/auth/login'), json}, (error, response) -> expect(response.statusCode).toBe(200) done(user) ) - form = req.form() - form.append('username', 'admin@afc.com') - form.append('password', '80yqxpb38j') # find some other way to make the admin object an admin... maybe directly? GLOBAL.loginUser = (user, done) -> request.post getURL('/auth/logout'), -> - req = request.post(getURL('/auth/login'), (error, response) -> + json = { username: user.get('email'), password: user.get('name') } + req = request.post({ url: getURL('/auth/login'), json}, (error, response) -> expect(response.statusCode).toBe(200) done(user) ) - form = req.form() - form.append('username', user.get('email')) - form.append('password', user.get('name')) GLOBAL.logoutUser = (done) -> request.post getURL('/auth/logout'), -> @@ -213,3 +200,4 @@ _drop = (done) -> GLOBAL.resetUserIDCounter = (number=0) -> User.idCounter = number +console.log '/spec/server/common.coffee - Done' diff --git a/spec/server/functional/contact.spec.coffee b/spec/server/functional/contact.spec.coffee index 0c18a201e..906b3baba 100644 --- a/spec/server/functional/contact.spec.coffee +++ b/spec/server/functional/contact.spec.coffee @@ -4,23 +4,23 @@ request = require '../request' User = require '../../../server/models/User' # TODO: need to update this test since /contact calls external Close.io API now -xdescribe 'POST /contact', -> - - beforeEach utils.wrap (done) -> - spyOn(sendwithus.api, 'send') - @teacher = yield utils.initUser({role: 'teacher'}) - yield utils.loginUser(@teacher) - done() - - describe 'when recipientID is "schools@codecombat.com"', -> - it 'sends to that email', utils.wrap (done) -> - [res, body] = yield request.postAsync({url: getURL('/contact'), json: { - sender: 'some@email.com' - message: 'A message' - recipientID: 'schools@codecombat.com' - }}) - expect(sendwithus.api.send).toHaveBeenCalled() - user = yield User.findById(@teacher.id) - yield new Promise((resolve) -> setTimeout(resolve, 10)) - expect(user.get('enrollmentRequestSent')).toBe(true) - done() +#xdescribe 'POST /contact', -> +# +# beforeEach utils.wrap (done) -> +# spyOn(sendwithus.api, 'send') +# @teacher = yield utils.initUser({role: 'teacher'}) +# yield utils.loginUser(@teacher) +# done() +# +# describe 'when recipientID is "schools@codecombat.com"', -> +# it 'sends to that email', utils.wrap (done) -> +# [res, body] = yield request.postAsync({url: getURL('/contact'), json: { +# sender: 'some@email.com' +# message: 'A message' +# recipientID: 'schools@codecombat.com' +# }}) +# expect(sendwithus.api.send).toHaveBeenCalled() +# user = yield User.findById(@teacher.id) +# yield new Promise((resolve) -> setTimeout(resolve, 10)) +# expect(user.get('enrollmentRequestSent')).toBe(true) +# done() diff --git a/spec/server/functional/file.spec.coffee b/spec/server/functional/file.spec.coffee index 26699bd0c..2282dbf4d 100644 --- a/spec/server/functional/file.spec.coffee +++ b/spec/server/functional/file.spec.coffee @@ -1,178 +1,178 @@ -require '../common' - -# Doesn't work on Travis. Need to figure out why, probably by having the -# url not depend on some external resource. -mongoose = require 'mongoose' -request = require '../request' - -xdescribe '/file', -> - url = getURL('/file') - files = [] - options = { - uri: url - json: { - # url: 'http://scotterickson.info/images/where-are-you.jpg' - url: 'http://fc07.deviantart.net/fs37/f/2008/283/5/1/Chu_Chu_Pikachu_by_angelishi.gif' - filename: 'where-are-you.jpg' - mimetype: 'image/jpeg' - description: 'None!' - } - } - filepath = 'tmp/file' # TODO Warning hard coded path !!! - - jsonOptions= { - path: 'my_path' - postName: 'my_buffer' - filename: 'ittybitty.data' - mimetype: 'application/octet-stream' - description: 'rando-info' - # my_buffer_url: 'http://scotterickson.info/images/where-are-you.jpg' - my_buffer_url: 'http://fc07.deviantart.net/fs37/f/2008/283/5/1/Chu_Chu_Pikachu_by_angelishi.gif' - } - - allowHeader = 'GET, POST' - - it 'preparing test : deletes all the files first', (done) -> - dropGridFS -> - done() - - it 'can\'t be created if invalid (property path is required)', (done) -> - func = (err, res, body) -> - expect(res.statusCode).toBe(422) - done() - - loginAdmin -> - request.post(options, func) - - it 'can be created by an admin', (done) -> - func = (err, res, body) -> - expect(res.statusCode).toBe(200) - expect(body._id).toBeDefined() - expect(body.filename).toBe(options.json.filename) - expect(body.contentType).toBe(options.json.mimetype) - expect(body.length).toBeDefined() - expect(body.uploadDate).toBeDefined() - expect(body.metadata).toBeDefined() - expect(body.metadata.name).toBeDefined() - expect(body.metadata.path).toBe(options.json.path) - expect(body.metadata.creator).toBeDefined() - expect(body.metadata.description).toBe(options.json.description) - expect(body.md5).toBeDefined() - files.push(body) - done() - - options.json.path = filepath - request.post(options, func) - - it 'can be read by an admin.', (done) -> - request.get {uri: url+'/'+files[0]._id}, (err, res) -> - expect(res.statusCode).toBe(200) - expect(res.headers['content-type']).toBe(files[0].contentType) - done() - - it 'returns 404 for missing files', (done) -> - id = '000000000000000000000000' - request.get {uri: url+'/'+id}, (err, res) -> - expect(res.statusCode).toBe(404) - done() - - it 'returns 404 for invalid ids', (done) -> - request.get {uri: url+'/thiswillnotwork'}, (err, res) -> - expect(res.statusCode).toBe(404) - done() - - it 'can be created directly with form parameters', (done) -> - options2 = { - uri: url - } - - func = (err, res, body) -> - expect(res.statusCode).toBe(200) - body = JSON.parse(body) - expect(body._id).toBeDefined() - expect(body.filename).toBe(jsonOptions.filename) - expect(body.contentType).toBe(jsonOptions.mimetype) - expect(body.length).toBeDefined() - expect(body.uploadDate).toBeDefined() - expect(body.metadata).toBeDefined() - expect(body.metadata.name).toBeDefined() - expect(body.metadata.path).toBe(jsonOptions.path) - expect(body.metadata.creator).toBeDefined() - expect(body.metadata.description).toBe(jsonOptions.description) - expect(body.md5).toBeDefined() - files.push(body) - done() - - # the only way I could figure out how to get request to do what I wanted... - r = request.post(options2, func) - form = r.form() - form.append('path', jsonOptions.path) - form.append('postName', jsonOptions.postName) - form.append('filename', jsonOptions.filename) - form.append('mimetype', jsonOptions.mimetype) - form.append('description', jsonOptions.description) - form.append('my_buffer', request(jsonOptions.my_buffer_url)) - - it 'created directly, can be read', (done) -> - request.get {uri: url+'/'+files[1]._id}, (err, res) -> - expect(res.statusCode).toBe(200) - expect(res.headers['content-type']).toBe(files[1].contentType) - done() - - it 'does not overwrite existing files', (done) -> - options.json.description = 'Face' - - func = (err, res, body) -> - expect(res.statusCode).toBe(409) - collection = mongoose.connection.db.collection('media.files') - collection.find({}).toArray (err, results) -> - # ittybitty.data, and just one Where are you.jpg - expect(results.length).toBe(2) - for f in results - expect(f.metadata.description).not.toBe('Face') - done() - - request.post(options, func) - - it 'does overwrite existing files if force is true', (done) -> - options.json.force = 'true' # TODO ask why it's a string and not a boolean ? - - func = (err, res, body) -> - expect(res.statusCode).toBe(200) - collection = mongoose.connection.db.collection('media.files') - collection.find({}).toArray (err, results) -> - # ittybitty.data, and just one Where are you.jpg - expect(results.length).toBe(2) - hit = false - for f in results - hit = true if f.metadata.description is 'Face' - expect(hit).toBe(true) - done() - - request.post(options, func) - - it ' can\'t be requested with HTTP PATCH method', (done) -> - request {method: 'patch', uri: url}, (err, res) -> - expect(res.statusCode).toBe(405) - expect(res.headers.allow).toBe(allowHeader) - done() - - it ' can\'t be requested with HTTP PUT method', (done) -> - request.put {uri: url}, (err, res) -> - expect(res.statusCode).toBe(405) - expect(res.headers.allow).toBe(allowHeader) - done() - - it ' can\'t be requested with HTTP HEAD method', (done) -> - request.head {uri: url}, (err, res) -> - expect(res.statusCode).toBe(405) - expect(res.headers.allow).toBe(allowHeader) - done() - - it ' can\'t be requested with HTTP DEL method', (done) -> - request.del {uri: url}, (err, res) -> - expect(res.statusCode).toBe(405) - expect(res.headers.allow).toBe(allowHeader) - done() - -# TODO: test server errors, see what they do +#require '../common' +# +## Doesn't work on Travis. Need to figure out why, probably by having the +## url not depend on some external resource. +#mongoose = require 'mongoose' +#request = require '../request' +# +#xdescribe '/file', -> +# url = getURL('/file') +# files = [] +# options = { +# uri: url +# json: { +# # url: 'http://scotterickson.info/images/where-are-you.jpg' +# url: 'http://fc07.deviantart.net/fs37/f/2008/283/5/1/Chu_Chu_Pikachu_by_angelishi.gif' +# filename: 'where-are-you.jpg' +# mimetype: 'image/jpeg' +# description: 'None!' +# } +# } +# filepath = 'tmp/file' # TODO Warning hard coded path !!! +# +# jsonOptions= { +# path: 'my_path' +# postName: 'my_buffer' +# filename: 'ittybitty.data' +# mimetype: 'application/octet-stream' +# description: 'rando-info' +# # my_buffer_url: 'http://scotterickson.info/images/where-are-you.jpg' +# my_buffer_url: 'http://fc07.deviantart.net/fs37/f/2008/283/5/1/Chu_Chu_Pikachu_by_angelishi.gif' +# } +# +# allowHeader = 'GET, POST' +# +# it 'preparing test : deletes all the files first', (done) -> +# dropGridFS -> +# done() +# +# it 'can\'t be created if invalid (property path is required)', (done) -> +# func = (err, res, body) -> +# expect(res.statusCode).toBe(422) +# done() +# +# loginAdmin -> +# request.post(options, func) +# +# it 'can be created by an admin', (done) -> +# func = (err, res, body) -> +# expect(res.statusCode).toBe(200) +# expect(body._id).toBeDefined() +# expect(body.filename).toBe(options.json.filename) +# expect(body.contentType).toBe(options.json.mimetype) +# expect(body.length).toBeDefined() +# expect(body.uploadDate).toBeDefined() +# expect(body.metadata).toBeDefined() +# expect(body.metadata.name).toBeDefined() +# expect(body.metadata.path).toBe(options.json.path) +# expect(body.metadata.creator).toBeDefined() +# expect(body.metadata.description).toBe(options.json.description) +# expect(body.md5).toBeDefined() +# files.push(body) +# done() +# +# options.json.path = filepath +# request.post(options, func) +# +# it 'can be read by an admin.', (done) -> +# request.get {uri: url+'/'+files[0]._id}, (err, res) -> +# expect(res.statusCode).toBe(200) +# expect(res.headers['content-type']).toBe(files[0].contentType) +# done() +# +# it 'returns 404 for missing files', (done) -> +# id = '000000000000000000000000' +# request.get {uri: url+'/'+id}, (err, res) -> +# expect(res.statusCode).toBe(404) +# done() +# +# it 'returns 404 for invalid ids', (done) -> +# request.get {uri: url+'/thiswillnotwork'}, (err, res) -> +# expect(res.statusCode).toBe(404) +# done() +# +# it 'can be created directly with form parameters', (done) -> +# options2 = { +# uri: url +# } +# +# func = (err, res, body) -> +# expect(res.statusCode).toBe(200) +# body = JSON.parse(body) +# expect(body._id).toBeDefined() +# expect(body.filename).toBe(jsonOptions.filename) +# expect(body.contentType).toBe(jsonOptions.mimetype) +# expect(body.length).toBeDefined() +# expect(body.uploadDate).toBeDefined() +# expect(body.metadata).toBeDefined() +# expect(body.metadata.name).toBeDefined() +# expect(body.metadata.path).toBe(jsonOptions.path) +# expect(body.metadata.creator).toBeDefined() +# expect(body.metadata.description).toBe(jsonOptions.description) +# expect(body.md5).toBeDefined() +# files.push(body) +# done() +# +# # the only way I could figure out how to get request to do what I wanted... +# r = request.post(options2, func) +# form = r.form() +# form.append('path', jsonOptions.path) +# form.append('postName', jsonOptions.postName) +# form.append('filename', jsonOptions.filename) +# form.append('mimetype', jsonOptions.mimetype) +# form.append('description', jsonOptions.description) +# form.append('my_buffer', request(jsonOptions.my_buffer_url)) +# +# it 'created directly, can be read', (done) -> +# request.get {uri: url+'/'+files[1]._id}, (err, res) -> +# expect(res.statusCode).toBe(200) +# expect(res.headers['content-type']).toBe(files[1].contentType) +# done() +# +# it 'does not overwrite existing files', (done) -> +# options.json.description = 'Face' +# +# func = (err, res, body) -> +# expect(res.statusCode).toBe(409) +# collection = mongoose.connection.db.collection('media.files') +# collection.find({}).toArray (err, results) -> +# # ittybitty.data, and just one Where are you.jpg +# expect(results.length).toBe(2) +# for f in results +# expect(f.metadata.description).not.toBe('Face') +# done() +# +# request.post(options, func) +# +# it 'does overwrite existing files if force is true', (done) -> +# options.json.force = 'true' # TODO ask why it's a string and not a boolean ? +# +# func = (err, res, body) -> +# expect(res.statusCode).toBe(200) +# collection = mongoose.connection.db.collection('media.files') +# collection.find({}).toArray (err, results) -> +# # ittybitty.data, and just one Where are you.jpg +# expect(results.length).toBe(2) +# hit = false +# for f in results +# hit = true if f.metadata.description is 'Face' +# expect(hit).toBe(true) +# done() +# +# request.post(options, func) +# +# it ' can\'t be requested with HTTP PATCH method', (done) -> +# request {method: 'patch', uri: url}, (err, res) -> +# expect(res.statusCode).toBe(405) +# expect(res.headers.allow).toBe(allowHeader) +# done() +# +# it ' can\'t be requested with HTTP PUT method', (done) -> +# request.put {uri: url}, (err, res) -> +# expect(res.statusCode).toBe(405) +# expect(res.headers.allow).toBe(allowHeader) +# done() +# +# it ' can\'t be requested with HTTP HEAD method', (done) -> +# request.head {uri: url}, (err, res) -> +# expect(res.statusCode).toBe(405) +# expect(res.headers.allow).toBe(allowHeader) +# done() +# +# it ' can\'t be requested with HTTP DEL method', (done) -> +# request.del {uri: url}, (err, res) -> +# expect(res.statusCode).toBe(405) +# expect(res.headers.allow).toBe(allowHeader) +# done() +# +## TODO: test server errors, see what they do diff --git a/spec/server/functional/level_session.spec.coffee b/spec/server/functional/level_session.spec.coffee index 02b48b220..5b445b029 100644 --- a/spec/server/functional/level_session.spec.coffee +++ b/spec/server/functional/level_session.spec.coffee @@ -23,14 +23,14 @@ describe '/db/level.session', -> # TODO Tried to mimic what happens on the site. Why is this even so hard to do. # Right now it's even possible to create ownerless sessions through POST - xit 'allows users to create level sessions through PATCH', (done) -> - loginJoe (joe) -> - request {method: 'patch', uri: url + mongoose.Types.ObjectId(), json: session}, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe 200 - console.log body - expect(body.creator).toEqual joe.get('_id').toHexString() - done() +# xit 'allows users to create level sessions through PATCH', (done) -> +# loginJoe (joe) -> +# request {method: 'patch', uri: url + mongoose.Types.ObjectId(), json: session}, (err, res, body) -> +# expect(err).toBeNull() +# expect(res.statusCode).toBe 200 +# console.log body +# expect(body.creator).toEqual joe.get('_id').toHexString() +# done() # Should remove this as soon as the PATCH test case above works it 'create a level session', (done) -> diff --git a/spec/server/functional/nocked.spec.coffee b/spec/server/functional/nocked.spec.coffee deleted file mode 100644 index 3c02c0b9f..000000000 --- a/spec/server/functional/nocked.spec.coffee +++ /dev/null @@ -1,29 +0,0 @@ -require '../common' -config = require '../../../server_config' -nockUtils = require('../nock-utils') -request = require '../request' - -xdescribe 'nock-utils', -> - afterEach nockUtils.teardownNock - - describe 'a test using setupNock', -> - it 'records and plays back third-party requests, but not localhost requests', (done) -> - nockUtils.setupNock 'nock-test.json', (err, nockDone) -> - request.get { uri: getURL('/db/level') }, (err) -> - expect(err).toBeNull() - t0 = new Date().getTime() - request.get { uri: 'http://zombo.com/' }, (err) -> - console.log 'cached speed', new Date().getTime() - t0 - expect(err).toBeNull() - nockDone() - done() - - describe 'another, sibling test that does not use setupNock', -> - it 'is proceeds normally', (done) -> - request.get { uri: getURL('/db/level') }, (err) -> - expect(err).toBeNull() - t0 = new Date().getTime() - request.get { uri: 'http://zombo.com/' }, (err) -> - console.log 'uncached speed', new Date().getTime() - t0 - expect(err).toBeNull() - done() diff --git a/spec/server/functional/queue.spec.coffee b/spec/server/functional/queue.spec.coffee index 4f622d310..91cd90f35 100644 --- a/spec/server/functional/queue.spec.coffee +++ b/spec/server/functional/queue.spec.coffee @@ -1,24 +1,24 @@ -require '../common' -request = require '../request' - -describe 'queue', -> - someURL = getURL('/queue/') - allowHeader = 'GET, POST, PUT' - - xit 'can\'t be requested with HTTP PATCH method', (done) -> - request {method: 'patch', uri: someURL}, (err, res, body) -> - expect(res.statusCode).toBe(405) - expect(res.headers.allow).toBe(allowHeader) - done() - - xit 'can\'t be requested with HTTP HEAD method', (done) -> - request.head {uri: someURL}, (err, res, body) -> - expect(res.statusCode).toBe(405) - expect(res.headers.allow).toBe(allowHeader) - done() - - xit 'can\'t be requested with HTTP DELETE method', (done) -> - request.del {uri: someURL}, (err, res, body) -> - expect(res.statusCode).toBe(405) - expect(res.headers.allow).toBe(allowHeader) - done() +#require '../common' +#request = require '../request' +# +#describe 'queue', -> +# someURL = getURL('/queue/') +# allowHeader = 'GET, POST, PUT' +# +# xit 'can\'t be requested with HTTP PATCH method', (done) -> +# request {method: 'patch', uri: someURL}, (err, res, body) -> +# expect(res.statusCode).toBe(405) +# expect(res.headers.allow).toBe(allowHeader) +# done() +# +# xit 'can\'t be requested with HTTP HEAD method', (done) -> +# request.head {uri: someURL}, (err, res, body) -> +# expect(res.statusCode).toBe(405) +# expect(res.headers.allow).toBe(allowHeader) +# done() +# +# xit 'can\'t be requested with HTTP DELETE method', (done) -> +# request.del {uri: someURL}, (err, res, body) -> +# expect(res.statusCode).toBe(405) +# expect(res.headers.allow).toBe(allowHeader) +# done() diff --git a/spec/server/functional/subscription.spec.coffee b/spec/server/functional/subscription.spec.coffee index 08425668e..57c2b2c26 100644 --- a/spec/server/functional/subscription.spec.coffee +++ b/spec/server/functional/subscription.spec.coffee @@ -1441,62 +1441,62 @@ describe 'Subscriptions', -> nockDone() done() - xit 'Unsubscribed user1 subscribes 13 users, unsubcribes 2', (done) -> - nockUtils.setupNock 'sub-test-34.json', (err, nockDone) -> - # TODO: Hits the Stripe error 'Request rate limit exceeded'. - # TODO: Need a better test for 12+ bulk discounts. Or, we could update the bulk disount logic. - # TODO: verify interim invoices? - recipientCount = 13 - recipientsToVerify = [0, 1, 10, 11, 12] - recipients = new SubbedRecipients recipientCount, recipientsToVerify - - # Create recipients - recipients.createRecipients -> - expect(recipients.length()).toEqual(recipientCount) - - stripe.tokens.create { - card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } - }, (err, token) -> - - # Create sponsor user - loginNewUser (user1) -> - - # Subscribe recipients - recipients.subRecipients user1, token, -> - User.findById user1.id, (err, user1) -> - - # Unsubscribe first recipient - unsubscribeRecipient user1, recipients.get(0), -> - User.findById user1.id, (err, user1) -> - - stripeInfo = user1.get('stripe') - expect(stripeInfo.recipients.length).toEqual(recipientCount - 1) - verifyNotSponsoring user1.id, recipients.get(0).id, -> - verifyNotRecipient recipients.get(0).id, -> - stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> - expect(err).toBeNull() - expect(subscription).not.toBeNull() - expect(subscription.quantity).toEqual(getUnsubscribedQuantity(recipientCount - 1)) - - # Unsubscribe last recipient - unsubscribeRecipient user1, recipients.get(recipientCount - 1), -> - User.findById user1.id, (err, user1) -> - stripeInfo = user1.get('stripe') - expect(stripeInfo.recipients.length).toEqual(recipientCount - 2) - verifyNotSponsoring user1.id, recipients.get(recipientCount - 1).id, -> - verifyNotRecipient recipients.get(recipientCount - 1).id, -> - stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> - expect(err).toBeNull() - expect(subscription).not.toBeNull() - numSponsored = recipientCount - 2 - if numSponsored <= 1 - expect(subscription.quantity).toEqual(subPrice) - else if numSponsored <= 11 - expect(subscription.quantity).toEqual(subPrice + (numSponsored - 1) * subPrice * 0.8) - else - expect(subscription.quantity).toEqual(subPrice + 10 * subPrice * 0.8 + (numSponsored - 11) * subPrice * 0.6) - nockDone() - done() +# xit 'Unsubscribed user1 subscribes 13 users, unsubcribes 2', (done) -> +# nockUtils.setupNock 'sub-test-34.json', (err, nockDone) -> +# # TODO: Hits the Stripe error 'Request rate limit exceeded'. +# # TODO: Need a better test for 12+ bulk discounts. Or, we could update the bulk disount logic. +# # TODO: verify interim invoices? +# recipientCount = 13 +# recipientsToVerify = [0, 1, 10, 11, 12] +# recipients = new SubbedRecipients recipientCount, recipientsToVerify +# +# # Create recipients +# recipients.createRecipients -> +# expect(recipients.length()).toEqual(recipientCount) +# +# stripe.tokens.create { +# card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } +# }, (err, token) -> +# +# # Create sponsor user +# loginNewUser (user1) -> +# +# # Subscribe recipients +# recipients.subRecipients user1, token, -> +# User.findById user1.id, (err, user1) -> +# +# # Unsubscribe first recipient +# unsubscribeRecipient user1, recipients.get(0), -> +# User.findById user1.id, (err, user1) -> +# +# stripeInfo = user1.get('stripe') +# expect(stripeInfo.recipients.length).toEqual(recipientCount - 1) +# verifyNotSponsoring user1.id, recipients.get(0).id, -> +# verifyNotRecipient recipients.get(0).id, -> +# stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> +# expect(err).toBeNull() +# expect(subscription).not.toBeNull() +# expect(subscription.quantity).toEqual(getUnsubscribedQuantity(recipientCount - 1)) +# +# # Unsubscribe last recipient +# unsubscribeRecipient user1, recipients.get(recipientCount - 1), -> +# User.findById user1.id, (err, user1) -> +# stripeInfo = user1.get('stripe') +# expect(stripeInfo.recipients.length).toEqual(recipientCount - 2) +# verifyNotSponsoring user1.id, recipients.get(recipientCount - 1).id, -> +# verifyNotRecipient recipients.get(recipientCount - 1).id, -> +# stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> +# expect(err).toBeNull() +# expect(subscription).not.toBeNull() +# numSponsored = recipientCount - 2 +# if numSponsored <= 1 +# expect(subscription.quantity).toEqual(subPrice) +# else if numSponsored <= 11 +# expect(subscription.quantity).toEqual(subPrice + (numSponsored - 1) * subPrice * 0.8) +# else +# expect(subscription.quantity).toEqual(subPrice + 10 * subPrice * 0.8 + (numSponsored - 11) * subPrice * 0.6) +# nockDone() +# done() describe 'APIs', -> subscriptionURL = getURL('/db/subscription') @@ -1694,7 +1694,6 @@ describe 'Subscriptions', -> token: token.id timestamp: new Date() request.put {uri: "#{subscriptionURL}/-/year_sale", json: requestBody, headers: headers }, (err, res) -> - console.log err expect(err).toBeNull() nockDone() done() diff --git a/spec/server/functional/user.spec.coffee b/spec/server/functional/user.spec.coffee index 69e22e1c8..a735088d6 100644 --- a/spec/server/functional/user.spec.coffee +++ b/spec/server/functional/user.spec.coffee @@ -11,16 +11,13 @@ describe 'POST /db/user', -> createAnonNameUser = (name, done)-> request.post getURL('/auth/logout'), -> request.get getURL('/auth/whoami'), -> - req = request.post(getURL('/db/user'), (err, response) -> + req = request.post({ url: getURL('/db/user'), json: {name}}, (err, response) -> expect(response.statusCode).toBe(200) - request.get getURL('/auth/whoami'), (request, response, body) -> - res = JSON.parse(response.body) - expect(res.anonymous).toBeTruthy() - expect(res.name).toEqual(name) + request.get { url: getURL('/auth/whoami'), json: true }, (request, response, body) -> + expect(body.anonymous).toBeTruthy() + expect(body.name).toEqual(name) done() ) - form = req.form() - form.append('name', name) it 'preparing test : clears the db first', (done) -> clearModels [User], (err) -> @@ -77,16 +74,13 @@ describe 'POST /db/user', -> createAnonNameUser('Jim', done) it 'should allow setting existing user name to anonymous user', (done) -> - req = request.post(getURL('/db/user'), (err, response, body) -> + req = request.post({url: getURL('/db/user'), json: {email: 'new@user.com', password: 'new'}}, (err, response, body) -> expect(response.statusCode).toBe(200) request.get getURL('/auth/whoami'), (request, response, body) -> res = JSON.parse(response.body) expect(res.anonymous).toBeFalsy() createAnonNameUser 'Jim', done ) - form = req.form() - form.append('email', 'new@user.com') - form.append('password', 'new') describe 'PUT /db/user', -> @@ -103,23 +97,22 @@ describe 'PUT /db/user', -> it 'denies requests to edit someone who is not joe', (done) -> unittest.getAdmin (admin) -> - req = request.put getURL(urlUser), - (err, res) -> + request.put {url: getURL(urlUser), json: {_id: admin.id}}, (err, res) -> expect(res.statusCode).toBe(403) done() - req.form().append('_id', admin.id) it 'denies invalid data', (done) -> unittest.getNormalJoe (joe) -> - req = request.put getURL(urlUser), - (err, res) -> + json = { + _id: joe.id + email: 'farghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlar +ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl' + } + request.put { url: getURL(urlUser), json }, (err, res) -> expect(res.statusCode).toBe(422) - expect(res.body.indexOf('too long')).toBeGreaterThan(-1) + expect(res.body[0].message.indexOf('too long')).toBeGreaterThan(-1) done() - form = req.form() - form.append('_id', joe.id) - form.append('email', 'farghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlar -ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl') + it 'does not allow normals to edit their permissions', utils.wrap (done) -> user = yield utils.initUser() @@ -132,47 +125,45 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl loginAdmin -> done() it 'denies non-existent ids', (done) -> - req = request.put getURL(urlUser), - (err, res) -> + json = { + _id: '513108d4cb8b610000000004', + email: 'perfectly@good.com' + } + request.put {url: getURL(urlUser), json}, (err, res) -> expect(res.statusCode).toBe(404) done() - form = req.form() - form.append('_id', '513108d4cb8b610000000004') - form.append('email', 'perfectly@good.com') it 'denies if the email being changed is already taken', (done) -> unittest.getNormalJoe (joe) -> unittest.getAdmin (admin) -> - req = request.put getURL(urlUser), (err, res) -> + json = { _id: admin.id, email: joe.get('email').toUpperCase() } + request.put { url: getURL(urlUser), json }, (err, res) -> expect(res.statusCode).toBe(409) - expect(res.body.indexOf('already used')).toBeGreaterThan(-1) + expect(res.body.message.indexOf('already used')).toBeGreaterThan(-1) done() - form = req.form() - form.append('_id', String(admin._id)) - form.append('email', joe.get('email').toUpperCase()) it 'does not care if you include your existing name', (done) -> unittest.getNormalJoe (joe) -> - req = request.put getURL(urlUser+'/'+joe._id), (err, res) -> + json = { _id: joe._id, name: 'Joe' } + request.put { url: getURL(urlUser+'/'+joe._id), json }, (err, res) -> expect(res.statusCode).toBe(200) done() - form = req.form() - form.append('_id', String(joe._id)) - form.append('name', 'Joe') it 'accepts name and email changes', (done) -> unittest.getNormalJoe (joe) -> - req = request.put getURL(urlUser), (err, res) -> + json = { + _id: joe.id + email: 'New@email.com' + name: 'Wilhelm' + } + request.put { url: getURL(urlUser), json }, (err, res) -> expect(res.statusCode).toBe(200) unittest.getUser('Wilhelm', 'New@email.com', 'null', (joe) -> expect(joe.get('name')).toBe('Wilhelm') expect(joe.get('emailLower')).toBe('new@email.com') expect(joe.get('email')).toBe('New@email.com') done()) - form = req.form() - form.append('_id', String(joe._id)) - form.append('email', 'New@email.com') - form.append('name', 'Wilhelm') + it 'should not allow two users with the same name slug', (done) -> loginSam (sam) -> @@ -189,7 +180,8 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl it 'should silently rename an anonymous user if their name conflicts upon signup', (done) -> request.post getURL('/auth/logout'), -> request.get getURL('/auth/whoami'), -> - req = request.post getURL('/db/user'), (err, response) -> + json = { name: 'admin' } + request.post { url: getURL('/db/user'), json }, (err, response) -> expect(response.statusCode).toBe(200) request.get getURL('/auth/whoami'), (err, response) -> expect(err).toBeNull() @@ -205,8 +197,6 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl expect(finalGuy.name).not.toEqual guy.name expect(finalGuy.name.length).toBe guy.name.length + 1 done() - form = req.form() - form.append('name', 'admin') it 'should be able to unset a slug by setting an empty name', (done) -> loginSam (sam) -> @@ -467,13 +457,13 @@ describe 'PUT /db/user/-/remain-teacher', -> describe 'GET /db/user', -> it 'logs in as admin', (done) -> - req = request.post(getURL('/auth/login'), (error, response) -> + json = { + username: 'admin@afc.com' + password: '80yqxpb38j' + } + request.post { url: getURL('/auth/login'), json }, (error, response) -> expect(response.statusCode).toBe(200) done() - ) - form = req.form() - form.append('username', 'admin@afc.com') - form.append('password', '80yqxpb38j') it 'get schema', (done) -> request.get {uri: getURL(urlUser+'/schema')}, (err, res, body) -> @@ -523,7 +513,7 @@ describe 'GET /db/user', -> # TODO Ruben should be able to fetch other users but probably with restricted data access # Add to the test case above an extra data check - xit 'can fetch another user with restricted fields' +# xit 'can fetch another user with restricted fields' describe 'GET /db/user/:handle', -> diff --git a/spec/server/nock-utils.coffee b/spec/server/nock-utils.coffee index 308e373da..d18d63d1c 100644 --- a/spec/server/nock-utils.coffee +++ b/spec/server/nock-utils.coffee @@ -77,4 +77,4 @@ module.exports.teardownNock = -> before = (scope) -> scope.body = (body) -> true -Promise.promisifyAll(module.exports) \ No newline at end of file +Promise.promisifyAll(module.exports) diff --git a/spec/server/unit/analytics.spec.coffee b/spec/server/unit/analytics.spec.coffee index c6dbc2605..22f1bc66a 100644 --- a/spec/server/unit/analytics.spec.coffee +++ b/spec/server/unit/analytics.spec.coffee @@ -1,77 +1,77 @@ -GLOBAL._ = require 'lodash' - -require '../common' -AnalyticsUsersActive = require '../../../server/models/AnalyticsUsersActive' -LevelSession = require '../../../server/models/LevelSession' -User = require '../../../server/models/User' -mongoose = require 'mongoose' - -# TODO: these tests have some rerun/cleanup issues -# TODO: add tests for purchase, payment, subscribe, unsubscribe, and earned achievements - -# TODO: AnalyticsUsersActive collection isn't currently used. -# TODO: Will remove these tests if we end up ripping out the disabled saveActiveUser calls. - -describe 'Analytics', -> - - xit 'registered user', (done) -> - clearModels [AnalyticsUsersActive], (err) -> - expect(err).toBeNull() - user = new User - permissions: [] - name: "Fred" + Math.floor(Math.random() * 10000) - user.save (err) -> - expect(err).toBeNull() - userID = mongoose.Types.ObjectId(user.get('_id')) - AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) -> - expect(activeUsers.length).toEqual(0) - user.register -> - AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) -> - expect(err).toBeNull() - expect(activeUsers.length).toEqual(1) - expect(activeUsers[0]?.get('event')).toEqual('register') - done() - - xit 'level completed', (done) -> - clearModels [AnalyticsUsersActive], (err) -> - expect(err).toBeNull() - unittest.getNormalJoe (joe) -> - userID = mongoose.Types.ObjectId(joe.get('_id')) - session = new LevelSession - name: 'Beat Gandalf' - levelID: 'lotr' - permissions: simplePermissions - state: complete: false - creator: userID - session.save (err) -> - expect(err).toBeNull() - AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) -> - expect(activeUsers.length).toEqual(0) - session.set 'state', complete: true - session.save (err) -> - expect(err).toBeNull() - AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) -> - expect(err).toBeNull() - expect(activeUsers.length).toEqual(1) - expect(activeUsers[0]?.get('event')).toEqual('level-completed/lotr') - done() - - xit 'level playtime', (done) -> - clearModels [AnalyticsUsersActive], (err) -> - expect(err).toBeNull() - unittest.getNormalJoe (joe) -> - userID = mongoose.Types.ObjectId(joe.get('_id')) - session = new LevelSession - name: 'Beat Gandalf' - levelID: 'lotr' - permissions: simplePermissions - playtime: 60 - creator: userID - session.save (err) -> - expect(err).toBeNull() - AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) -> - expect(err).toBeNull() - expect(activeUsers.length).toEqual(1) - expect(activeUsers[0]?.get('event')).toEqual('level-playtime/lotr') - done() - +#GLOBAL._ = require 'lodash' +# +#require '../common' +#AnalyticsUsersActive = require '../../../server/models/AnalyticsUsersActive' +#LevelSession = require '../../../server/models/LevelSession' +#User = require '../../../server/models/User' +#mongoose = require 'mongoose' +# +## TODO: these tests have some rerun/cleanup issues +## TODO: add tests for purchase, payment, subscribe, unsubscribe, and earned achievements +# +## TODO: AnalyticsUsersActive collection isn't currently used. +## TODO: Will remove these tests if we end up ripping out the disabled saveActiveUser calls. +# +#describe 'Analytics', -> +# +# xit 'registered user', (done) -> +# clearModels [AnalyticsUsersActive], (err) -> +# expect(err).toBeNull() +# user = new User +# permissions: [] +# name: "Fred" + Math.floor(Math.random() * 10000) +# user.save (err) -> +# expect(err).toBeNull() +# userID = mongoose.Types.ObjectId(user.get('_id')) +# AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) -> +# expect(activeUsers.length).toEqual(0) +# user.register -> +# AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) -> +# expect(err).toBeNull() +# expect(activeUsers.length).toEqual(1) +# expect(activeUsers[0]?.get('event')).toEqual('register') +# done() +# +# xit 'level completed', (done) -> +# clearModels [AnalyticsUsersActive], (err) -> +# expect(err).toBeNull() +# unittest.getNormalJoe (joe) -> +# userID = mongoose.Types.ObjectId(joe.get('_id')) +# session = new LevelSession +# name: 'Beat Gandalf' +# levelID: 'lotr' +# permissions: simplePermissions +# state: complete: false +# creator: userID +# session.save (err) -> +# expect(err).toBeNull() +# AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) -> +# expect(activeUsers.length).toEqual(0) +# session.set 'state', complete: true +# session.save (err) -> +# expect(err).toBeNull() +# AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) -> +# expect(err).toBeNull() +# expect(activeUsers.length).toEqual(1) +# expect(activeUsers[0]?.get('event')).toEqual('level-completed/lotr') +# done() +# +# xit 'level playtime', (done) -> +# clearModels [AnalyticsUsersActive], (err) -> +# expect(err).toBeNull() +# unittest.getNormalJoe (joe) -> +# userID = mongoose.Types.ObjectId(joe.get('_id')) +# session = new LevelSession +# name: 'Beat Gandalf' +# levelID: 'lotr' +# permissions: simplePermissions +# playtime: 60 +# creator: userID +# session.save (err) -> +# expect(err).toBeNull() +# AnalyticsUsersActive.find {creator : userID}, (err, activeUsers) -> +# expect(err).toBeNull() +# expect(activeUsers.length).toEqual(1) +# expect(activeUsers[0]?.get('event')).toEqual('level-playtime/lotr') +# done() +# From 8657f978672d5201396e0eadb14d24aaa9c6e715 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Fri, 17 Jun 2016 10:42:59 -0700 Subject: [PATCH 4/8] Switch travis client test progress to dots --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f3c0f484b..de84f4bd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,7 +28,7 @@ before_script: - "sleep 15" # to give node a chance to start script: - - "./node_modules/karma/bin/karma start --browsers Firefox --single-run --reporters progress" + - "./node_modules/karma/bin/karma start --browsers Firefox --single-run --reporters dots" - "npm run jasmine" notifications: From b4baad82b0a89f56c4a47eddcb25131980ba2ce4 Mon Sep 17 00:00:00 2001 From: Rob Date: Fri, 17 Jun 2016 11:41:43 -0700 Subject: [PATCH 5/8] Don't set up the analytics log model in proxy mode. --- server/models/AnalyticsLogEvent.coffee | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/models/AnalyticsLogEvent.coffee b/server/models/AnalyticsLogEvent.coffee index db974f4a9..77241b0b5 100644 --- a/server/models/AnalyticsLogEvent.coffee +++ b/server/models/AnalyticsLogEvent.coffee @@ -28,6 +28,9 @@ AnalyticsLogEventSchema.statics.logEvent = (user, event, properties={}) -> doc.save() -analyticsMongoose = mongoose.createConnection "mongodb://#{config.mongo.analytics_host}:#{config.mongo.analytics_port}/#{config.mongo.analytics_db}" - -module.exports = AnalyticsLogEvent = analyticsMongoose.model('analytics.log.event', AnalyticsLogEventSchema, config.mongo.analytics_collection) +unless config.proxy + analyticsMongoose = mongoose.createConnection() + analyticsMongoose.open "mongodb://#{config.mongo.analytics_host}:#{config.mongo.analytics_port}/#{config.mongo.analytics_db}", (error) -> + console.log "Couldnt connect to analytics", error + + module.exports = AnalyticsLogEvent = analyticsMongoose.model('analytics.log.event', AnalyticsLogEventSchema, config.mongo.analytics_collection) From 490ea8d1bc4f8020a125ca808e9cbec6273c8451 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Fri, 17 Jun 2016 13:53:49 -0700 Subject: [PATCH 6/8] Add state to delighted data if state was populated by nces --- server/delighted.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/delighted.coffee b/server/delighted.coffee index 1e603b451..636074a7a 100644 --- a/server/delighted.coffee +++ b/server/delighted.coffee @@ -17,6 +17,8 @@ module.exports.addDelightedUser = addDelightedUser = (user, trialRequest) -> testGroupNumber: user.get('testGroupNumber') gender: user.get('gender') lastLevel: user.get('lastLevel') + state: if props.nces_id and props.country is 'USA' then props.state else 'other' + @postPeople(form) module.exports.postPeople = (form) -> From 514fce349a8f58388e1275cc6406c42fdf5ad78a Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Fri, 17 Jun 2016 15:15:13 -0700 Subject: [PATCH 7/8] Include ladder levels in level stats in teacher views, clean client test logs --- app/lib/LevelLoader.coffee | 14 +++--- app/lib/coursesHelper.coffee | 49 +++++++++---------- app/models/CocoModel.coffee | 10 ++-- app/models/LevelSession.coffee | 2 +- app/templates/account/payments-view.jade | 1 - app/templates/courses/teacher-class-view.jade | 4 +- server/middleware/classrooms.coffee | 6 +-- test/app/lib/CoursesHelper.spec.coffee | 1 - 8 files changed, 43 insertions(+), 44 deletions(-) diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index b4b03df8c..cbf7bc450 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -12,6 +12,8 @@ app = require 'core/application' World = require 'lib/world/world' utils = require 'core/utils' +LOG = false + # This is an initial stab at unifying loading and setup into a single place which can # monitor everything and keep a LoadingScreen visible overall progress. # @@ -147,7 +149,7 @@ module.exports = class LevelLoader extends CocoClass @listenToOnce @opponentSession, 'sync', @loadDependenciesForSession loadDependenciesForSession: (session) -> - console.log "Loading dependencies for session: ", session + console.log "Loading dependencies for session: ", session if LOG 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' @@ -172,11 +174,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 + console.log "Course mode, loading custom hero: ", heroConfig if LOG return if not heroConfig url = "/db/thang.type/#{heroConfig.thangType}/version" if heroResource = @maybeLoadURL(url, ThangType, 'thang') - console.log "Pushing resource: ", heroResource + console.log "Pushing resource: ", heroResource if LOG @worldNecessities.push heroResource @sessionDependenciesRegistered[session.id] = true return @@ -345,7 +347,7 @@ module.exports = class LevelLoader extends CocoClass true onWorldNecessitiesLoaded: -> - console.log "World necessities loaded." + console.log "World necessities loaded." if LOG @initWorld() @supermodel.clearMaxProgress() @trigger 'world-necessities-loaded' @@ -374,7 +376,7 @@ module.exports = class LevelLoader extends CocoClass onSupermodelLoaded: -> return if @destroyed - console.log 'SuperModel for Level loaded in', new Date().getTime() - @t0, 'ms' + console.log 'SuperModel for Level loaded in', new Date().getTime() - @t0, 'ms' if LOG @loadLevelSounds() @denormalizeSession() @@ -482,7 +484,7 @@ module.exports = class LevelLoader extends CocoClass @world.difficulty = Math.max 0, @world.difficulty - 1 # Show the difficulty they won, not the next one. serializedLevel = @level.serialize(@supermodel, @session, @opponentSession) @world.loadFromLevel serializedLevel, false - console.log 'World has been initialized from level loader.' + console.log 'World has been initialized from level loader.' if LOG # Initial Sound Loading diff --git a/app/lib/coursesHelper.coffee b/app/lib/coursesHelper.coffee index 693234fa2..ad68f1fee 100644 --- a/app/lib/coursesHelper.coffee +++ b/app/lib/coursesHelper.coffee @@ -12,20 +12,18 @@ module.exports = continue if not instance instance.numCompleted = 0 instance.started = false - levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true}) + levels = classroom.getLevels({courseID: course.id}) for userID in instance.get('members') instance.started ||= _.any levels.models, (level) -> - return false if level.isLadder() session = _.find classroom.sessions.models, (session) -> session.get('creator') is userID and session.get('level').original is level.get('original') session? levelCompletes = _.map levels.models, (level) -> - return true if level.isLadder() #TODO: Hella slow! Do the mapping first! - session = _.find classroom.sessions.models, (session) -> + sessions = _.filter classroom.sessions.models, (session) -> session.get('creator') is userID and session.get('level').original is level.get('original') # sessionMap[userID][level].completed() - session?.completed() + _.find(sessions, (s) -> s.completed()) if _.every levelCompletes instance.numCompleted += 1 @@ -34,14 +32,14 @@ module.exports = for course, courseIndex in courses.models instance = courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id }) continue if not instance - levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true}) + levels = classroom.getLevels({courseID: course.id}) for level, levelIndex in levels.models userIDs = [] for user in students.models userID = user.id - session = _.find classroom.sessions.models, (session) -> + sessions = _.filter classroom.sessions.models, (session) -> session.get('creator') is userID and session.get('level').original is level.get('original') - if not session?.completed() + if not _.find(sessions, (s) -> s.completed()) userIDs.push userID if userIDs.length > 0 users = _.map userIDs, (id) -> @@ -61,16 +59,16 @@ module.exports = courseIndex = courses.models.length - courseIndex - 1 #compensate for reverse instance = courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id }) continue if not instance - levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true}) + levels = classroom.getLevels({courseID: course.id}) levelModels = levels.models.slice() for level, levelIndex in levelModels.reverse() # levelIndex = levelModels.length - levelIndex - 1 #compensate for reverse userIDs = [] for user in students.models userID = user.id - session = _.find classroom.sessions.models, (session) -> + sessions = _.filter classroom.sessions.models, (session) -> session.get('creator') is userID and session.get('level').original is level.get('original') - if session?.completed() # + if _.find(sessions, (s) -> s.completed()) # userIDs.push userID if userIDs.length > 0 users = _.map userIDs, (id) -> @@ -91,7 +89,7 @@ module.exports = conceptData[classroom.id] = {} for course, courseIndex in courses.models - levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true}) + levels = classroom.getLevels({courseID: course.id}) for level in levels.models levelID = level.get('original') @@ -102,16 +100,16 @@ module.exports = for concept in level.get('concepts') for userID in classroom.get('members') - session = _.find classroom.sessions.models, (session) -> + sessions = _.filter classroom.sessions.models, (session) -> session.get('creator') is userID and session.get('level').original is levelID - if not session # haven't gotten to this level yet, but might have completed others before + if _.size(sessions) is 0 # haven't gotten to this level yet, but might have completed others before for concept in level.get('concepts') conceptData[classroom.id][concept].completed = false - if session # have gotten to the level and at least started it + if _.size(sessions) > 0 # have gotten to the level and at least started it for concept in level.get('concepts') conceptData[classroom.id][concept].started = true - if not session?.completed() # level started but not completed + if not _.find(sessions, (s) -> s.completed()) # level started but not completed for concept in level.get('concepts') conceptData[classroom.id][concept].completed = false conceptData @@ -139,7 +137,7 @@ module.exports = continue progressData[classroom.id][course.id] = { completed: true, started: false } # to be updated - levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true}) + levels = classroom.getLevels({courseID: course.id}) for level in levels.models levelID = level.get('original') progressData[classroom.id][course.id][levelID] = { @@ -154,12 +152,12 @@ module.exports = courseProgress = progressData[classroom.id][course.id] courseProgress[userID] ?= { completed: true, started: false, levelsCompleted: 0 } # Only set it the first time through a user courseProgress[levelID][userID] = { completed: true, started: false } # These don't matter, will always be set - session = _.find classroom.sessions.models, (session) -> + sessions = _.filter 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[levelID][userID].session = _.find(sessions, (s) -> s.completed()) or _.first(sessions) + + if _.size(sessions) is 0 # haven't gotten to this level yet, but might have completed others before courseProgress.started ||= false #no-op courseProgress.completed = false courseProgress[userID].started ||= false #no-op @@ -169,22 +167,23 @@ module.exports = courseProgress[levelID][userID].started = false courseProgress[levelID][userID].completed = false - if session # have gotten to the level and at least started it + if _.size(sessions) > 0 # have gotten to the level and at least started it courseProgress.started = true courseProgress[userID].started = true courseProgress[levelID].started = true courseProgress[levelID][userID].started = true - courseProgress[levelID][userID].lastPlayed = new Date(session.get('changed')) + courseProgress[levelID][userID].lastPlayed = new Date(Math.max(_.map(sessions, 'changed'))) courseProgress[levelID].numStarted += 1 - if session?.completed() # have finished this level + if _.find(sessions, (s) -> s.completed()) # have finished this level courseProgress.completed &&= true #no-op courseProgress[userID].completed &&= true #no-op courseProgress[userID].levelsCompleted += 1 courseProgress[levelID].completed &&= true #no-op # courseProgress[levelID].numCompleted += 1 courseProgress[levelID][userID].completed = true - courseProgress[levelID][userID].dateFirstCompleted = new Date(session.get('dateFirstCompleted') || session.get('changed')) + dates = (s.get('dateFirstCompleted') || s.get('changed') for s in sessions) + courseProgress[levelID][userID].dateFirstCompleted = new Date(Math.max(dates...)) else # level started but not completed courseProgress.completed = false courseProgress[userID].completed = false diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index 4a5c84fb5..90310d5de 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -124,10 +124,11 @@ class CocoModel extends Backbone.Model validate: -> errors = @getValidationErrors() if errors?.length - console.debug "Validation failed for #{@constructor.className}: '#{@get('name') or @}'." - for error in errors - console.debug "\t", error.dataPath, ':', error.message - console.trace?() + unless application.testing + console.debug "Validation failed for #{@constructor.className}: '#{@get('name') or @}'." + for error in errors + console.debug "\t", error.dataPath, ':', error.message + console.trace?() return errors save: (attrs, options) -> @@ -188,7 +189,6 @@ class CocoModel extends Backbone.Model keys.push key return unless keys.length - console.debug 'Patching', @get('name') or @, keys @save(attrs, options) fetch: (options) -> diff --git a/app/models/LevelSession.coffee b/app/models/LevelSession.coffee index 63621fa66..8b2b73104 100644 --- a/app/models/LevelSession.coffee +++ b/app/models/LevelSession.coffee @@ -41,7 +41,7 @@ module.exports = class LevelSession extends CocoModel @get('submittedCodeLanguage')? and @get('team')? completed: -> - @get('state')?.complete || false + @get('state')?.complete || @get('submitted') || false shouldAvoidCorruptData: (attrs) -> return false unless me.team is 'humans' diff --git a/app/templates/account/payments-view.jade b/app/templates/account/payments-view.jade index fc625f656..f9a55bd8b 100644 --- a/app/templates/account/payments-view.jade +++ b/app/templates/account/payments-view.jade @@ -10,7 +10,6 @@ block content a(href="/account", data-i18n="nav.account") li.active(data-i18n="account.payments") - - console.log('render', view.payments.size()) if view.payments.size() table.table.table-striped tr diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade index 7215a08cf..453f8485c 100644 --- a/app/templates/courses/teacher-class-view.jade +++ b/app/templates/courses/teacher-class-view.jade @@ -300,7 +300,7 @@ mixin courseProgressTab mixin courseOverview - var course = state.get('selectedCourse') - - var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models + - var levels = view.classroom.getLevels({courseID: course.id}).models .course-overview-row .course-title.student-name span= course.get('name') @@ -318,7 +318,7 @@ mixin studentLevelsRow(student) div.student-email.small-details= student.get('email') div.student-levels-progress - var course = state.get('selectedCourse') - - var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models + - var levels = view.classroom.getLevels({courseID: course.id}).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, session) diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index 1c2ec0f78..8a12b3c5a 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().replace(/ /g, '') }).select('name ownerID aceConfig') + classroom = yield Classroom.findOne({ code: code.toLowerCase().replace(RegExp(' ', '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.') @@ -104,7 +104,7 @@ module.exports = members = classroom.get('members') or [] members = members.slice(memberSkip, memberSkip + memberLimit) dbqs = [] - select = 'state.complete level creator playtime changed dateFirstCompleted' + select = 'state.complete level creator playtime changed dateFirstCompleted submitted' for member in members dbqs.push(LevelSession.find({creator: member.toHexString()}).select(select).exec()) results = yield dbqs @@ -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().replace(/ /g, '') + code = req.body.code.toLowerCase().replace(RegExp(' ', 'g'), '') classroom = yield Classroom.findOne({code: code}) if not classroom log.debug("classrooms.join: Classroom not found with code #{code}") diff --git a/test/app/lib/CoursesHelper.spec.coffee b/test/app/lib/CoursesHelper.spec.coffee index 243f01920..2512c8eb5 100644 --- a/test/app/lib/CoursesHelper.spec.coffee +++ b/test/app/lib/CoursesHelper.spec.coffee @@ -36,7 +36,6 @@ describe 'CoursesHelper', -> describe 'progressData.get({classroom, course})', -> it 'returns object with .completed=true and .started=true', -> progressData = helper.calculateAllProgress(@classrooms, @courses, @courseInstances, @members) - console.log 'progress data?', progressData progress = progressData.get {@classroom, @course} expect(progress.completed).toBe true expect(progress.started).toBe true From 4622337d82efdcb02c38ecfeb4b5d24dc770b01b Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Fri, 17 Jun 2016 15:40:03 -0700 Subject: [PATCH 8/8] Update licenses needed form Ensure the needed licenses are in the subject resulting email. --- app/locale/en.coffee | 1 + app/templates/courses/enrollments-view.jade | 3 -- .../teachers/teachers-contact-modal.jade | 24 ++++++++--- app/views/courses/EnrollmentsView.coffee | 14 +----- .../teachers/TeachersContactModal.coffee | 43 +++++++++++-------- server/routes/contact.coffee | 4 +- .../views/courses/EnrollmentsView.spec.coffee | 10 ----- .../courses/TeachersContactModal.spec.coffee | 12 +++++- 8 files changed, 59 insertions(+), 52 deletions(-) diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 29528035a..3bf4586f5 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -815,6 +815,7 @@ more_info_1: "Our" more_info_2: "teachers forum" more_info_3: "is a good place to connect with fellow educators who are using CodeCombat." + licenses_needed: "Licenses needed" teachers_quote: name: "Demo Form" diff --git a/app/templates/courses/enrollments-view.jade b/app/templates/courses/enrollments-view.jade index babd88a79..0ec9f35fb 100644 --- a/app/templates/courses/enrollments-view.jade +++ b/app/templates/courses/enrollments-view.jade @@ -101,9 +101,6 @@ mixin addCredits button#request-sent-btn.btn-lg.btn.btn-forest(disabled=true, data-i18n="teacher.request_sent") else p(data-i18n="teacher.num_enrollments_needed") - div.m-t-2 - input#students-input.enrollment-count.text-center(value=view.state.get('numberOfStudents') type='number') - strong(data-i18n="teacher.credits") p.m-y-2(data-i18n="teacher.get_enrollments_blurb") button#contact-us-btn.btn-lg.btn.btn-forest(data-i18n="contribute.contact_us_url") diff --git a/app/templates/teachers/teachers-contact-modal.jade b/app/templates/teachers/teachers-contact-modal.jade index de3826063..a795d9792 100644 --- a/app/templates/teachers/teachers-contact-modal.jade +++ b/app/templates/teachers/teachers-contact-modal.jade @@ -1,5 +1,7 @@ extends /templates/core/modal-base-flat +//- TODO: i18n + block modal-header-content .text-center h3 Contact Our Classroom Team @@ -11,26 +13,36 @@ block modal-body-content - var sent = view.state.get('sendingState') === 'sent'; - var values = view.state.get('formValues'); - var errors = view.state.get('formErrors'); - + + .form-group(class=errors.name ? 'has-error' : '') + label.control-label(for="name" data-i18n="general.name") + +formErrors(errors.name) + input.form-control(name="name", type="text", value=values.name || '', tabindex=1, disabled=sending || sent) + .form-group(class=errors.email ? 'has-error' : '') label.control-label(for="email" data-i18n="general.email") +formErrors(errors.email) input.form-control(name="email", type="email", value=values.email || '', tabindex=1, disabled=sending || sent) - + + .form-group(class=errors.licensesNeeded ? 'has-error' : '') + label.control-label(for="licensesNeeded" data-i18n="teachers.licenses_needed") + +formErrors(errors.licensesNeeded) + input.form-control(name="licensesNeeded", type="text", value=values.licensesNeeded || '', tabindex=1, disabled=sending || sent) + .form-group(class=errors.message ? 'has-error' : '') label.control-label(for="message" data-i18n="general.message") +formErrors(errors.message) textarea.form-control(name="message", tabindex=1 disabled=sending || sent)= values.message - + if view.state.get('sendingState') === 'error' .alert.alert-danger Could not send message. - + if sent .alert.alert-success Message sent! - + .text-right button#submit-btn.btn.btn-navy.btn-lg(type='submit' disabled=sending || sent) Submit - + block modal-footer mixin formErrors(errors) diff --git a/app/views/courses/EnrollmentsView.coffee b/app/views/courses/EnrollmentsView.coffee index bcc3b3596..4e91ae1a3 100644 --- a/app/views/courses/EnrollmentsView.coffee +++ b/app/views/courses/EnrollmentsView.coffee @@ -14,7 +14,6 @@ module.exports = class EnrollmentsView extends RootView template: template events: - 'input #students-input': 'onInputStudentsInput' 'click #enroll-students-btn': 'onClickEnrollStudentsButton' 'click #how-to-enroll-link': 'onClickHowToEnrollLink' 'click #contact-us-btn': 'onClickContactUsButton' @@ -96,17 +95,8 @@ 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)) - @$('#students-input').val(@state.get('numberOfStudents')) - else - @state.set({'numberOfStudents': Math.max(parseInt(@$('#students-input').val()) or 0, 0)}, {silent: true}) # do not re-render - - numberOfStudentsIsValid: -> 0 < @get('numberOfStudents') < 100000 + window.tracker?.trackEvent 'Classes Licenses Contact Us', category: 'Teachers', ['Mixpanel'] + @openModalView(new TeachersContactModal()) onClickEnrollStudentsButton: -> window.tracker?.trackEvent 'Classes Licenses Enroll Students', category: 'Teachers', ['Mixpanel'] diff --git a/app/views/teachers/TeachersContactModal.coffee b/app/views/teachers/TeachersContactModal.coffee index ec19bd824..ab8fbf06b 100644 --- a/app/views/teachers/TeachersContactModal.coffee +++ b/app/views/teachers/TeachersContactModal.coffee @@ -7,20 +7,22 @@ contact = require 'core/contact' module.exports = class TeachersContactModal extends ModalView id: 'teachers-contact-modal' template: require 'templates/teachers/teachers-contact-modal' - + defaultLicenses: 15 + events: 'submit form': 'onSubmitForm' - + initialize: (options={}) -> @state = new State({ formValues: { + name: '' email: '' + licensesNeeded: @defaultLicenses message: '' } formErrors: {} sendingState: 'standby' # 'sending', 'sent', 'error' }) - @enrollmentsNeeded = options.enrollmentsNeeded or '-' @trialRequests = new TrialRequests() @supermodel.trackRequest @trialRequests.fetchOwn() @state.on 'change', @render, @ @@ -28,41 +30,46 @@ module.exports = class TeachersContactModal extends ModalView onLoaded: -> trialRequest = @trialRequests.first() props = trialRequest?.get('properties') or {} - message = """ - Name of School/District: #{props.organization or ''} - Your Name: #{props.name || ''} - Enrollments Needed: #{@enrollmentsNeeded} - - Message: Hi CodeCombat! I want to learn more about the Classroom experience and get licenses so that my students can access Computer Science 2 and on. - """ + name = if props.firstName and props.lastName then "#{props.firstName} #{props.lastName}" else me.get('name') ? '' email = props.email or me.get('email') or '' - @state.set('formValues', { email, message }) + message = """ + Hi CodeCombat! I want to learn more about the Classroom experience and get licenses so that my students can access Computer Science 2 and on. + + Name of School/District: #{props.organization or ''} + Role: #{props.role or ''} + Phone Number: #{props.phoneNumber or ''} + """ + @state.set('formValues', { name, email, licensesNeeded: @defaultLicenses, message }) super() onSubmitForm: (e) -> e.preventDefault() return if @state.get('sendingState') is 'sending' - + formValues = forms.formToObject @$el @state.set('formValues', formValues) - + formErrors = {} - if not forms.validateEmail(formValues.email) + unless formValues.name + formErrors.name = 'Name required.' + unless forms.validateEmail(formValues.email) formErrors.email = 'Invalid email.' - if not formValues.message + unless parseInt(formValues.licensesNeeded) > 0 + formErrors.licensesNeeded = 'Licenses needed is required.' + unless formValues.message formErrors.message = 'Message required.' @state.set({ formErrors, formValues, sendingState: 'standby' }) return unless _.isEmpty(formErrors) - + @state.set('sendingState', 'sending') - data = _.extend({ country: me.get('country'), recipientID: 'schools@codecombat.com', enrollmentsNeeded: @enrollmentsNeeded }, formValues) + data = _.extend({ country: me.get('country'), recipientID: 'schools@codecombat.com' }, formValues) contact.send({ data context: @ success: -> @state.set({ sendingState: 'sent' }) me.set('enrollmentRequestSent', true) - setTimeout(=> + setTimeout(=> @hide?() , 3000) error: -> @state.set({ sendingState: 'error' }) diff --git a/server/routes/contact.coffee b/server/routes/contact.coffee index 651639bcb..f391a9050 100644 --- a/server/routes/contact.coffee +++ b/server/routes/contact.coffee @@ -25,11 +25,11 @@ module.exports.setup = (app) -> createMailContent = (req, fromAddress, done) -> country = req.body.country - enrollmentsNeeded = req.body.enrollmentsNeeded + licensesNeeded = req.body.licensesNeeded message = req.body.message user = req.user subject = switch - when enrollmentsNeeded then "#{enrollmentsNeeded} Licenses needed for #{fromAddress}" + when licensesNeeded then "#{licensesNeeded} 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 diff --git a/test/app/views/courses/EnrollmentsView.spec.coffee b/test/app/views/courses/EnrollmentsView.spec.coffee index 75ba21deb..52c91a6e1 100644 --- a/test/app/views/courses/EnrollmentsView.spec.coffee +++ b/test/app/views/courses/EnrollmentsView.spec.coffee @@ -66,16 +66,6 @@ describe 'EnrollmentsView', -> fail('There should be an #action-col, other tests depend on it.') describe '"Get Licenses" area', -> - - describe '"Contact Us" button', -> - it 'opens a TeachersContactModal, passing in the number of licenses', -> - spyOn(@view, 'openModalView') - @view.state.set('numberOfStudents', 20) - @view.$('#contact-us-btn').click() - expect(view.openModalView).toHaveBeenCalled() - args = view.openModalView.calls.argsFor(0) - expect(args[0] instanceof TeachersContactModal).toBe(true) - expect(args[0].enrollmentsNeeded).toBe(20) describe 'when the teacher has made contact', -> beforeEach -> diff --git a/test/app/views/courses/TeachersContactModal.spec.coffee b/test/app/views/courses/TeachersContactModal.spec.coffee index 69552323e..0750b74f9 100644 --- a/test/app/views/courses/TeachersContactModal.spec.coffee +++ b/test/app/views/courses/TeachersContactModal.spec.coffee @@ -4,18 +4,28 @@ factories = require 'test/app/factories' describe 'TeachersContactModal', -> beforeEach (done) -> - @modal = new TeachersContactModal({ enrollmentsNeeded: 10 }) + @modal = new TeachersContactModal() @modal.render() trialRequests = new TrialRequests([factories.makeTrialRequest()]) @modal.trialRequests.fakeRequests[0].respondWith({ status: 200, responseText: trialRequests.stringify() }) @modal.supermodel.once('loaded-all', done) jasmine.demoModal(@modal) + it 'shows an error when the name is empty and the form is submitted', -> + @modal.$('input[name="name"]').val('') + @modal.$('form').submit() + expect(@modal.$('input[name="name"]').closest('.form-group').hasClass('has-error')).toBe(true) + it 'shows an error when the email is invalid and the form is submitted', -> @modal.$('input[name="email"]').val('not an email') @modal.$('form').submit() expect(@modal.$('input[name="email"]').closest('.form-group').hasClass('has-error')).toBe(true) + it 'shows an error when licensesNeeded is not > 0 and the form is submitted', -> + @modal.$('input[name="licensesNeeded"]').val('') + @modal.$('form').submit() + expect(@modal.$('input[name="licensesNeeded"]').closest('.form-group').hasClass('has-error')).toBe(true) + it 'shows an error when the message is empty and the form is submitted', -> @modal.$('textarea[name="message"]').val('') @modal.$('form').submit()