Merge branch 'master' into production

This commit is contained in:
Scott Erickson 2016-07-26 10:52:11 -07:00
commit b837de2394
14 changed files with 172 additions and 6 deletions

View file

@ -55,7 +55,7 @@ module.exports = FacebookHandler = class FacebookHandler extends CocoClass
js = d.createElement('script') js = d.createElement('script')
js.id = id js.id = id
js.async = true 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' #js.src = '//connect.facebook.net/en_US/all/debug.js'
ref.parentNode.insertBefore js, ref ref.parentNode.insertBefore js, ref
@ -63,12 +63,13 @@ module.exports = FacebookHandler = class FacebookHandler extends CocoClass
)(document) )(document)
window.fbAsyncInit = => window.fbAsyncInit = =>
FB.init FB.init({
appId: (if document.location.origin is 'http://localhost:3000' then '607435142676437' else '148832601965463') # App ID appId: (if document.location.origin is 'http://localhost:3000' then '607435142676437' else '148832601965463') # App ID
channelUrl: document.location.origin + '/channel.html' # Channel File channelUrl: document.location.origin + '/channel.html' # Channel File
cookie: true # enable cookies to allow the server to access the session cookie: true # enable cookies to allow the server to access the session
xfbml: true # parse XFBML xfbml: true # parse XFBML
version: 'v2.7'
})
FB.getLoginStatus (response) => FB.getLoginStatus (response) =>
if response.status is 'connected' if response.status is 'connected'
@connected = true @connected = true

View file

@ -287,6 +287,7 @@
email_good: "Email looks good!" email_good: "Email looks good!"
name_taken: "Username already taken! Try {{suggestedName}}?" name_taken: "Username already taken! Try {{suggestedName}}?"
name_available: "Username available!" name_available: "Username available!"
name_is_email: "Username may not be an email"
choose_type: "Choose your account type:" choose_type: "Choose your account type:"
teacher_type_1: "Teach programming using CodeCombat!" teacher_type_1: "Teach programming using CodeCombat!"
teacher_type_2: "Set up your class" teacher_type_2: "Set up your class"

View file

@ -354,6 +354,16 @@ module.exports = class User extends CocoModel
options.type = 'PUT' options.type = 'PUT'
@fetch(options) @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, 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 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
] ]

View file

@ -3,6 +3,12 @@ extends /templates/core/modal-base-flat
// DNT // DNT
block modal-header-content 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 h3 Administer User
h4 #{view.user.get('name') || 'Unnamed'} / #{view.user.get('email')} h4 #{view.user.get('name') || 'Unnamed'} / #{view.user.get('email')}
span= view.user.id span= view.user.id

View file

@ -231,7 +231,7 @@ module.exports = class AccountSettingsView extends CocoView
$('.nano').nanoScroller({scrollTo: @$el.find('.has-error')}) $('.nano').nanoScroller({scrollTo: @$el.find('.has-error')})
else else
noty noty
text: res.responseText text: res.responseJSON?.message or res.responseText
type: 'error' type: 'error'
layout: 'topCenter' layout: 'topCenter'
timeout: 5000 timeout: 5000

View file

@ -13,6 +13,8 @@ module.exports = class AdministerUserModal extends ModalView
events: events:
'click #save-changes': 'onClickSaveChanges' 'click #save-changes': 'onClickSaveChanges'
'click #add-seats-btn': 'onClickAddSeatsButton' 'click #add-seats-btn': 'onClickAddSeatsButton'
'click #destudent-btn': 'onClickDestudentButton'
'click #deteacher-btn': 'onClickDeteacherButton'
initialize: (options, @userHandle) -> initialize: (options, @userHandle) ->
@user = new User({_id:@userHandle}) @user = new User({_id:@userHandle})
@ -71,3 +73,33 @@ module.exports = class AdministerUserModal extends ModalView
@listenTo prepaid, 'sync', -> @listenTo prepaid, 'sync', ->
@state = 'made-prepaid' @state = 'made-prepaid'
@renderSelectors('#prepaid-form') @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

View file

@ -146,6 +146,11 @@ module.exports = class BasicInfoView extends CocoView
}) })
forms.clearFormAlerts(@$el) 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() res = tv4.validateMultiple data, @formSchema()
forms.applyErrorsToForm(@$('form'), res.errors) unless res.valid forms.applyErrorsToForm(@$('form'), res.errors) unless res.valid
return res.valid return res.valid

View file

@ -143,7 +143,7 @@ module.exports = class PlayLevelView extends RootView
onLevelLoaded: (e) -> onLevelLoaded: (e) ->
@god = new God({@gameUIState}) unless e.level.isType('web-dev') @god = new God({@gameUIState}) unless e.level.isType('web-dev')
@setUpGod() if @waitingToSetUpGod @setupGod() if @waitingToSetUpGod
trackLevelLoadEnd: -> trackLevelLoadEnd: ->
return if @isEditorPreview return if @isEditorPreview

View file

@ -1,4 +1,3 @@
// Unset someone with a student role. Remove from classrooms, unset role. // Unset someone with a student role. Remove from classrooms, unset role.
// Usage // Usage

View file

@ -66,4 +66,5 @@ module.exports.modules = modules = # by collection name
'users': 'User' 'users': 'User'
mongoose.modelNameByCollection = (collection) -> mongoose.modelNameByCollection = (collection) ->
return require('../models/LevelSession') if collection is 'level.sessions'
mongoose.model modules[collection] if collection of modules mongoose.model modules[collection] if collection of modules

View file

@ -8,9 +8,11 @@ Promise = require 'bluebird'
parse = require '../commons/parse' parse = require '../commons/parse'
request = require 'request' request = require 'request'
mongoose = require 'mongoose' mongoose = require 'mongoose'
database = require '../commons/database'
sendwithus = require '../sendwithus' sendwithus = require '../sendwithus'
User = require '../models/User' User = require '../models/User'
Classroom = require '../models/Classroom' Classroom = require '../models/Classroom'
CourseInstance = require '../models/CourseInstance'
facebook = require '../lib/facebook' facebook = require '../lib/facebook'
gplus = require '../lib/gplus' gplus = require '../lib/gplus'
TrialRequest = require '../models/TrialRequest' TrialRequest = require '../models/TrialRequest'
@ -211,3 +213,44 @@ module.exports =
yield trialRequest.update({$unset: {applicant: ''}}) yield trialRequest.update({$unset: {applicant: ''}})
res.status(200).send(req.user.toObject({req: req})) 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}))

View file

@ -361,6 +361,10 @@ UserSchema.pre('save', (next) ->
@set('email', undefined) @set('email', undefined)
@set('emailLower', undefined) @set('emailLower', undefined)
if name = @get('name') 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()) @set('nameLower', name.toLowerCase())
else else
@set('name', undefined) @set('name', undefined)

View file

@ -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-facebook', mw.users.signupWithFacebook)
app.post('/db/user/:handle/signup-with-gplus', mw.users.signupWithGPlus) 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/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', mw.auth.checkLoggedIn(), mw.prepaids.fetchByCreator)
app.get('/db/prepaid/-/active-schools', mw.auth.checkHasPermission(['admin']), mw.prepaids.fetchActiveSchools) app.get('/db/prepaid/-/active-schools', mw.auth.checkHasPermission(['admin']), mw.prepaids.fetchActiveSchools)

View file

@ -3,6 +3,9 @@ utils = require '../utils'
urlUser = '/db/user' urlUser = '/db/user'
User = require '../../../server/models/User' User = require '../../../server/models/User'
Classroom = require '../../../server/models/Classroom' 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' TrialRequest = require '../../../server/models/TrialRequest'
Prepaid = require '../../../server/models/Prepaid' Prepaid = require '../../../server/models/Prepaid'
request = require '../request' request = require '../request'
@ -970,3 +973,62 @@ describe 'POST /db/user/:handle/signup-with-gplus', ->
expect(res.statusCode).toBe(409) expect(res.statusCode).toBe(409)
done() 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()