diff --git a/app/core/social-handlers/FacebookHandler.coffee b/app/core/social-handlers/FacebookHandler.coffee index e3d27bf81..1e362bf37 100644 --- a/app/core/social-handlers/FacebookHandler.coffee +++ b/app/core/social-handlers/FacebookHandler.coffee @@ -55,7 +55,7 @@ module.exports = FacebookHandler = class FacebookHandler extends CocoClass js = d.createElement('script') js.id = id js.async = true - js.src = '//connect.facebook.net/en_US/all.js' + js.src = '//connect.facebook.net/en_US/sdk.js' #js.src = '//connect.facebook.net/en_US/all/debug.js' ref.parentNode.insertBefore js, ref @@ -63,12 +63,13 @@ module.exports = FacebookHandler = class FacebookHandler extends CocoClass )(document) window.fbAsyncInit = => - FB.init + FB.init({ appId: (if document.location.origin is 'http://localhost:3000' then '607435142676437' else '148832601965463') # App ID channelUrl: document.location.origin + '/channel.html' # Channel File cookie: true # enable cookies to allow the server to access the session xfbml: true # parse XFBML - + version: 'v2.7' + }) FB.getLoginStatus (response) => if response.status is 'connected' @connected = true diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 29ce12d0e..9aee76dfa 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -287,6 +287,7 @@ email_good: "Email looks good!" name_taken: "Username already taken! Try {{suggestedName}}?" name_available: "Username available!" + name_is_email: "Username may not be an email" choose_type: "Choose your account type:" teacher_type_1: "Teach programming using CodeCombat!" teacher_type_2: "Set up your class" diff --git a/app/models/User.coffee b/app/models/User.coffee index 5b6d72d80..d308c8ced 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -353,6 +353,16 @@ module.exports = class User extends CocoModel options.url = '/db/user/-/remain-teacher' options.type = 'PUT' @fetch(options) + + destudent: (options={}) -> + options.url = _.result(@, 'url') + '/destudent' + options.type = 'POST' + @fetch(options) + + deteacher: (options={}) -> + options.url = _.result(@, 'url') + '/deteacher' + options.type = 'POST' + @fetch(options) tiersByLevel = [-1, 0, 0.05, 0.14, 0.18, 0.32, 0.41, 0.5, 0.64, 0.82, 0.91, 1.04, 1.22, 1.35, 1.48, 1.65, 1.78, 1.96, 2.1, 2.24, 2.38, 2.55, 2.69, 2.86, 3.03, 3.16, 3.29, 3.42, 3.58, 3.74, 3.89, 4.04, 4.19, 4.32, 4.47, 4.64, 4.79, 4.96, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 10, 10.5, 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15 diff --git a/app/templates/admin/administer-user-modal.jade b/app/templates/admin/administer-user-modal.jade index 00b55b972..e644aa2af 100644 --- a/app/templates/admin/administer-user-modal.jade +++ b/app/templates/admin/administer-user-modal.jade @@ -3,6 +3,12 @@ extends /templates/core/modal-base-flat // DNT block modal-header-content + .pull-right + if view.user.isStudent() + button#destudent-btn.btn.btn-burgandy Destudent + if view.user.isTeacher() + button#deteacher-btn.btn.btn-burgandy Deteacher + h3 Administer User h4 #{view.user.get('name') || 'Unnamed'} / #{view.user.get('email')} span= view.user.id diff --git a/app/views/account/AccountSettingsView.coffee b/app/views/account/AccountSettingsView.coffee index c29034d93..70c945b8c 100644 --- a/app/views/account/AccountSettingsView.coffee +++ b/app/views/account/AccountSettingsView.coffee @@ -231,7 +231,7 @@ module.exports = class AccountSettingsView extends CocoView $('.nano').nanoScroller({scrollTo: @$el.find('.has-error')}) else noty - text: res.responseText + text: res.responseJSON?.message or res.responseText type: 'error' layout: 'topCenter' timeout: 5000 diff --git a/app/views/admin/AdministerUserModal.coffee b/app/views/admin/AdministerUserModal.coffee index c45caa27e..eae33a16e 100644 --- a/app/views/admin/AdministerUserModal.coffee +++ b/app/views/admin/AdministerUserModal.coffee @@ -13,6 +13,8 @@ module.exports = class AdministerUserModal extends ModalView events: 'click #save-changes': 'onClickSaveChanges' 'click #add-seats-btn': 'onClickAddSeatsButton' + 'click #destudent-btn': 'onClickDestudentButton' + 'click #deteacher-btn': 'onClickDeteacherButton' initialize: (options, @userHandle) -> @user = new User({_id:@userHandle}) @@ -71,3 +73,33 @@ module.exports = class AdministerUserModal extends ModalView @listenTo prepaid, 'sync', -> @state = 'made-prepaid' @renderSelectors('#prepaid-form') + + onClickDestudentButton: (e) -> + button = $(e.currentTarget) + button.attr('disabled', true).text('...') + Promise.resolve(@user.destudent()) + .then => + button.remove() + .catch (e) => + button.attr('disabled', false).text('Destudent') + noty { + text: e.message or e.responseJSON?.message or e.responseText or 'Unknown Error' + type: 'error' + } + if e.stack + throw e + + onClickDeteacherButton: (e) -> + button = $(e.currentTarget) + button.attr('disabled', true).text('...') + Promise.resolve(@user.deteacher()) + .then => + button.remove() + .catch (e) => + button.attr('disabled', false).text('Destudent') + noty { + text: e.message or e.responseJSON?.message or e.responseText or 'Unknown Error' + type: 'error' + } + if e.stack + throw e diff --git a/app/views/core/CreateAccountModal/BasicInfoView.coffee b/app/views/core/CreateAccountModal/BasicInfoView.coffee index 051b765a7..de5da382c 100644 --- a/app/views/core/CreateAccountModal/BasicInfoView.coffee +++ b/app/views/core/CreateAccountModal/BasicInfoView.coffee @@ -146,6 +146,11 @@ module.exports = class BasicInfoView extends CocoView }) forms.clearFormAlerts(@$el) + + if data.name and forms.validateEmail(data.name) + forms.setErrorToProperty(@$el, 'name', $.i18n.t('signup.name_is_email')) + return false + res = tv4.validateMultiple data, @formSchema() forms.applyErrorsToForm(@$('form'), res.errors) unless res.valid return res.valid diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index eac9acdc7..f491dea86 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -143,7 +143,7 @@ module.exports = class PlayLevelView extends RootView onLevelLoaded: (e) -> @god = new God({@gameUIState}) unless e.level.isType('web-dev') - @setUpGod() if @waitingToSetUpGod + @setupGod() if @waitingToSetUpGod trackLevelLoadEnd: -> return if @isEditorPreview diff --git a/scripts/mongodb/stored/destudent.js b/scripts/mongodb/stored/destudent.js index 06498404d..f7cac55fd 100644 --- a/scripts/mongodb/stored/destudent.js +++ b/scripts/mongodb/stored/destudent.js @@ -1,4 +1,3 @@ - // Unset someone with a student role. Remove from classrooms, unset role. // Usage diff --git a/server/commons/mapping.coffee b/server/commons/mapping.coffee index 90069e32a..997a8a83e 100644 --- a/server/commons/mapping.coffee +++ b/server/commons/mapping.coffee @@ -66,4 +66,5 @@ module.exports.modules = modules = # by collection name 'users': 'User' mongoose.modelNameByCollection = (collection) -> + return require('../models/LevelSession') if collection is 'level.sessions' mongoose.model modules[collection] if collection of modules diff --git a/server/middleware/users.coffee b/server/middleware/users.coffee index 968f725ab..c4edafe99 100644 --- a/server/middleware/users.coffee +++ b/server/middleware/users.coffee @@ -8,9 +8,11 @@ Promise = require 'bluebird' parse = require '../commons/parse' request = require 'request' mongoose = require 'mongoose' +database = require '../commons/database' sendwithus = require '../sendwithus' User = require '../models/User' Classroom = require '../models/Classroom' +CourseInstance = require '../models/CourseInstance' facebook = require '../lib/facebook' gplus = require '../lib/gplus' TrialRequest = require '../models/TrialRequest' @@ -211,3 +213,44 @@ module.exports = yield trialRequest.update({$unset: {applicant: ''}}) res.status(200).send(req.user.toObject({req: req})) + + destudent: wrap (req, res) -> + user = yield database.getDocFromHandle(req, User) + if not user + throw new errors.NotFound('User not found.') + + if not user.isStudent() + return res.status(200).send(user.toObject({req: req})) + + yield Classroom.update( + { members: user._id }, + { $pull: {members: user._id} }, + { multi: true } + ) + + yield CourseInstance.update( + { members: user._id }, + { $pull: {members: user._id} }, + { multi: true } + ) + + yield user.update({ $unset: {role: ''}}) + user.set('role', undefined) + return res.status(200).send(user.toObject({req: req})) + + + deteacher: wrap (req, res) -> + user = yield database.getDocFromHandle(req, User) + if not user + throw new errors.NotFound('User not found.') + + if not user.isTeacher() + return res.status(200).send(user.toObject({req: req})) + + yield TrialRequest.remove( + { applicant: user._id }, + ) + + yield user.update({ $unset: {role: ''}}) + user.set('role', undefined) + return res.status(200).send(user.toObject({req: req})) diff --git a/server/models/User.coffee b/server/models/User.coffee index c26edbafa..0ed8aab85 100644 --- a/server/models/User.coffee +++ b/server/models/User.coffee @@ -361,6 +361,10 @@ UserSchema.pre('save', (next) -> @set('email', undefined) @set('emailLower', undefined) if name = @get('name') + filter = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,63}$/i # https://news.ycombinator.com/item?id=5763990 + if filter.test(name) + return next(new errors.UnprocessableEntity('Name may not be an email')) + @set('nameLower', name.toLowerCase()) else @set('name', undefined) diff --git a/server/routes/index.coffee b/server/routes/index.coffee index 1c228b5a8..27264368c 100644 --- a/server/routes/index.coffee +++ b/server/routes/index.coffee @@ -104,6 +104,8 @@ module.exports.setup = (app) -> app.post('/db/user/:handle/signup-with-facebook', mw.users.signupWithFacebook) app.post('/db/user/:handle/signup-with-gplus', mw.users.signupWithGPlus) app.post('/db/user/:handle/signup-with-password', mw.users.signupWithPassword) + app.post('/db/user/:handle/destudent', mw.auth.checkHasPermission(['admin']), mw.users.destudent) + app.post('/db/user/:handle/deteacher', mw.auth.checkHasPermission(['admin']), mw.users.deteacher) app.get('/db/prepaid', mw.auth.checkLoggedIn(), mw.prepaids.fetchByCreator) app.get('/db/prepaid/-/active-schools', mw.auth.checkHasPermission(['admin']), mw.prepaids.fetchActiveSchools) diff --git a/spec/server/functional/user.spec.coffee b/spec/server/functional/user.spec.coffee index 6299b8933..cac865d89 100644 --- a/spec/server/functional/user.spec.coffee +++ b/spec/server/functional/user.spec.coffee @@ -3,6 +3,9 @@ utils = require '../utils' urlUser = '/db/user' User = require '../../../server/models/User' Classroom = require '../../../server/models/Classroom' +CourseInstance = require '../../../server/models/CourseInstance' +Course = require '../../../server/models/Course' +Campaign = require '../../../server/models/Campaign' TrialRequest = require '../../../server/models/TrialRequest' Prepaid = require '../../../server/models/Prepaid' request = require '../request' @@ -970,3 +973,62 @@ describe 'POST /db/user/:handle/signup-with-gplus', -> expect(res.statusCode).toBe(409) done() +describe 'POST /db/user/:handle/destudent', -> + beforeEach utils.wrap (done) -> + yield utils.clearModels([User, Classroom, CourseInstance, Course, Campaign]) + done() + + it 'removes a student user from all classrooms and unsets their role property', utils.wrap (done) -> + student1 = yield utils.initUser({role: 'student'}) + student2 = yield utils.initUser({role: 'student'}) + members = [student1._id, student2._id] + + classroom = new Classroom({members}) + yield classroom.save() + courseInstance = new CourseInstance({members}) + yield courseInstance.save() + + admin = yield utils.initAdmin() + yield utils.loginUser(admin) + + url = getURL("/db/user/#{student1.id}/destudent") + [res, body] = yield request.postAsync({url, json:true}) + + student1 = yield User.findById(student1.id) + student2 = yield User.findById(student2.id) + classroom = yield Classroom.findById(classroom.id) + courseInstance = yield CourseInstance.findById(courseInstance.id) + + expect(student1.get('role')).toBeUndefined() + expect(student2.get('role')).toBe('student') + expect(classroom.get('members').length).toBe(1) + expect(classroom.get('members')[0].toString()).toBe(student2.id) + expect(courseInstance.get('members').length).toBe(1) + expect(courseInstance.get('members')[0].toString()).toBe(student2.id) + done() + +describe 'POST /db/user/:handle/deteacher', -> + beforeEach utils.wrap (done) -> + yield utils.clearModels([User, TrialRequest]) + done() + + it 'removes a student user from all classrooms and unsets their role property', utils.wrap (done) -> + teacher = yield utils.initUser({role: 'teacher'}) + yield utils.loginUser(teacher) + trialRequest = yield utils.makeTrialRequest(teacher) + + admin = yield utils.initAdmin() + yield utils.loginUser(admin) + + trialRequest = yield TrialRequest.findById(trialRequest.id) + expect(trialRequest).toBeDefined() + expect(teacher.get('role')).toBe('teacher') + + url = getURL("/db/user/#{teacher.id}/deteacher") + [res, body] = yield request.postAsync({url, json:true}) + + trialRequest = yield TrialRequest.findById(trialRequest.id) + expect(trialRequest).toBeNull() + teacher = yield User.findById(teacher.id) + expect(teacher.get('role')).toBeUndefined() + done()