Merge branch 'master' into production

This commit is contained in:
Matt Lott 2016-05-27 11:24:22 -07:00
commit 864e18d23b
30 changed files with 145 additions and 49 deletions

View file

@ -69,15 +69,15 @@ module.exports = class CocoRouter extends Backbone.Router
'contribute/diplomat': go('contribute/DiplomatView')
'contribute/scribe': go('contribute/ScribeView')
'courses': go('courses/CoursesView') # , { studentsOnly: true }) # TODO: Enforce after session-less play for teachers
'Courses': go('courses/CoursesView') # , { studentsOnly: true })
'courses': go('courses/CoursesView', { studentsOnly: true })
'Courses': go('courses/CoursesView', { studentsOnly: true })
'courses/students': redirect('/courses')
'courses/teachers': redirect('/teachers/classes')
'courses/purchase': redirect('/teachers/licenses')
'courses/enroll(/:courseID)': redirect('/teachers/licenses')
'courses/update-account': go('courses/CoursesUpdateAccountView')
'courses/:classroomID': go('courses/ClassroomView') #, { studentsOnly: true })
'courses/:courseID/:courseInstanceID': go('courses/CourseDetailsView')
'courses/:classroomID': go('courses/ClassroomView', { studentsOnly: true })
'courses/:courseID/:courseInstanceID': go('courses/CourseDetailsView', { studentsOnly: true })
'db/*path': 'routeToServer'
'demo(/*subpath)': go('DemoView')
@ -142,14 +142,14 @@ module.exports = class CocoRouter extends Backbone.Router
'SEEN': go('NewHomeView')
'teachers': redirect('/teachers/classes')
'teachers/classes': go('courses/TeacherClassesView') #, { teachersOnly: true })
'teachers/classes/:classroomID': go('courses/TeacherClassView') #, { teachersOnly: true })
'teachers/classes': go('courses/TeacherClassesView', { teachersOnly: true })
'teachers/classes/:classroomID': go('courses/TeacherClassView', { teachersOnly: true })
'teachers/courses': go('courses/TeacherCoursesView')
'teachers/demo': go('teachers/RequestQuoteView')
'teachers/enrollments': redirect('/teachers/licenses')
'teachers/licenses': go('courses/EnrollmentsView') #, { teachersOnly: true })
'teachers/licenses': go('courses/EnrollmentsView', { teachersOnly: true })
'teachers/freetrial': go('teachers/RequestQuoteView')
'teachers/quote': go('teachers/RequestQuoteView')
'teachers/quote': redirect('/teachers/demo')
'teachers/signup': ->
return @routeDirectly('teachers/CreateTeacherAccountView', []) if me.isAnonymous()
@navigate('/teachers/update-account', {trigger: true, replace: true})
@ -174,7 +174,7 @@ module.exports = class CocoRouter extends Backbone.Router
routeDirectly: (path, args=[], options={}) ->
if options.teachersOnly and not me.isTeacher()
return @routeDirectly('teachers/RestrictedToTeachersView')
if options.studentsOnly and me.isTeacher()
if options.studentsOnly and not me.isStudent()
return @routeDirectly('courses/RestrictedToStudentsView')
leavingMessage = _.result(window.currentView, 'onLeaveMessage')
if leavingMessage

View file

@ -1279,7 +1279,8 @@
student_age_range_to: "to"
create_class: "Create Class"
class_name: "Class Name"
teacher_account_restricted: "Your account is a teacher account, and so cannot access student content."
teacher_account_restricted: "Your account is a teacher account and cannot access student content." # {change}
account_restricted: "A student account is required to access this page."
update_account_login_title: "Log in to update your account"
update_account_title: "Your account needs attention!"
update_account_blurb: "Before you can access your classes, choose how you want to use this account."

View file

@ -25,7 +25,7 @@ module.exports = class User extends CocoModel
return name if name
[emailName, emailDomain] = @get('email')?.split('@') or []
return emailName if emailName
return 'Anoner'
return 'Anonymous'
getPhotoURL: (size=80, useJobProfilePhoto=false, useEmployerPageAvatar=false) ->
photoURL = if useJobProfilePhoto then @get('jobProfile')?.photoURL else null

View file

@ -6,7 +6,7 @@ UserSchema = c.object
default:
visa: 'Authorized to work in the US'
music: true
name: 'Anoner'
name: 'Anonymous'
autocastDelay: 5000
emails: {}
permissions: []

View file

@ -123,7 +123,7 @@ block content
if memberLanguageMap && memberLanguageMap[member.id]
span.code-language-cell(style="background-image: url(/images/common/code_languages/#{memberLanguageMap[member.id]}_small.png)", title=memberLanguageMap[member.id])
div
a(href="/user/#{member.id}")= member.get('name') || 'Anoner'
a(href="/user/#{member.id}")= member.get('name') || 'Anonymous'
div Level #{member.level()}
if isOwner && member.id !== clan.get('ownerID')
button.btn.btn-xs.btn-warning.remove-member-btn(data-id="#{member.id}", data-i18n="clans.rem_hero") Remove Hero
@ -220,7 +220,7 @@ block content
if memberLanguageMap && memberLanguageMap[member.id]
span.code-language-cell(style="background-image: url(/images/common/code_languages/#{memberLanguageMap[member.id]}_small.png)", title=memberLanguageMap[member.id])
td.name-cell
a(href="/user/#{member.id}")= member.get('name') || 'Anoner'
a(href="/user/#{member.id}")= member.get('name') || 'Anonymous'
td.level-cell= member.level()
td.achievements-cell
if memberAchievementsMap && memberAchievementsMap[member.id]

View file

@ -45,7 +45,7 @@ block content
if view.idNameMap && view.idNameMap[clan.get('ownerID')]
a(href="/user/#{clan.get('ownerID')}")= view.idNameMap[clan.get('ownerID')]
else
a(href="/user/#{clan.get('ownerID')}") Anoner
a(href="/user/#{clan.get('ownerID')}") Anonymous
td
if view.myClanIDs.indexOf(clan.id) < 0
button.btn.btn-success.join-clan-btn(data-id="#{clan.id}", data-i18n="clans.join_clan") Join Clan
@ -75,7 +75,7 @@ block content
if view.idNameMap && view.idNameMap[clan.get('ownerID')]
a(href="/user/#{clan.get('ownerID')}")= view.idNameMap[clan.get('ownerID')]
else
a(href="/user/#{clan.get('ownerID')}") Anoner
a(href="/user/#{clan.get('ownerID')}") Anonymous
td= clan.get('type')
td
if clan.get('ownerID') !== me.id

View file

@ -3,10 +3,19 @@ extends /templates/base-flat
block content
.access-restricted.container.text-center.m-y-3
h5(data-i18n='teacher.access_restricted')
if me.isTeacher()
p(data-i18n='courses.teacher_account_restricted')
a.btn.btn-lg.btn-primary(href="/teachers/classes" data-i18n="new_home.goto_classes")
button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out")
else
p(data-i18n='courses.account_restricted')
if me.isAnonymous()
.login-button.btn.btn-lg.btn-primary(data-i18n='login.log_in')
else
a.btn.btn-lg.btn-primary(href="/courses/update-account" data-i18n="courses.update_account_update_student")
button#logout-button.btn.btn-lg.btn-primary-alt(data-i18n="login.log_out")
if me.isTeacher()
.teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3.m-y-3
h5(data-i18n='teacher.what_is_a_teacher_account')
p(data-i18n='teacher.teacher_account_explanation')

View file

@ -11,7 +11,7 @@ ul.user-feedback-list.list-group
em= moment(new Date(feedback.created)).fromNow()
span.spl.spr -
a(href="/user/#{feedback.creator}")
strong= feedback.creatorName || 'Anoner'
strong= feedback.creatorName || 'Anonymous'
if feedback.review
span.spr :
span= feedback.review

View file

@ -12,7 +12,7 @@ for player in view.players
.player-gold
.gold-icon
.gold-value
.player-name= player.name || 'Anoner'
.player-name= player.name || 'Anonymous'
.player-health
.health-icon
.health-bar-container

View file

@ -81,7 +81,7 @@ block append content
if idNameMap && idNameMap[clan.get('ownerID')]
a(href="/user/#{clan.get('ownerID')}")= idNameMap[clan.get('ownerID')]
else
a(href="/user/#{clan.get('ownerID')}") Anoner
a(href="/user/#{clan.get('ownerID')}") Anonymous
td= clan.get('members').length
else
.panel-body

View file

@ -23,6 +23,8 @@ module.exports = class AboutView extends RootView
'left': 'onLeftPressed'
'esc': 'onEscapePressed'
getTitle: -> return $.i18n.t('nav.about')
afterRender: ->
super(arguments...)
@$('#fixed-nav').affix({

View file

@ -20,6 +20,8 @@ module.exports = class MainAdminView extends RootView
'click #create-free-sub-btn': 'onClickFreeSubLink'
'click #terminal-create': 'onClickTerminalSubLink'
getTitle: -> return $.i18n.t('account_settings.admin')
initialize: ->
if window.amActually
@amActually = new User({_id: window.amActually})
@ -65,7 +67,7 @@ module.exports = class MainAdminView extends RootView
forms.enableSubmit(@$('#user-search-button'))
result = ''
if users.length
result = ("<tr data-user-id='#{user._id}'><td><code>#{user._id}</code></td><td>#{_.escape(user.name or 'Anoner')}</td><td>#{_.escape(user.email)}</td></tr>" for user in users)
result = ("<tr data-user-id='#{user._id}'><td><code>#{user._id}</code></td><td>#{_.escape(user.name or 'Anonymous')}</td><td>#{_.escape(user.email)}</td></tr>" for user in users)
result = "<table class=\"table\">#{result.join('\n')}</table>"
@$el.find('#user-search-result').html(result)

View file

@ -138,7 +138,7 @@ module.exports = class ClanDetailsView extends RootView
return unless @members? and @memberSort?
switch @memberSort
when "nameDesc"
@members.comparator = (a, b) -> return (b.get('name') or 'Anoner').localeCompare(a.get('name') or 'Anoner')
@members.comparator = (a, b) -> return (b.get('name') or 'Anonymous').localeCompare(a.get('name') or 'Anonymous')
when "progressAsc"
@members.comparator = (a, b) ->
aComplete = (concept for concept, state of userConceptsMap[a.id] when state is 'complete')
@ -151,7 +151,7 @@ module.exports = class ClanDetailsView extends RootView
else if aStarted > bStarted then return 1
if highestUserLevelCountMap[a.id] < highestUserLevelCountMap[b.id] then return -1
else if highestUserLevelCountMap[a.id] > highestUserLevelCountMap[b.id] then return 1
(a.get('name') or 'Anoner').localeCompare(b.get('name') or 'Anoner')
(a.get('name') or 'Anonymous').localeCompare(b.get('name') or 'Anonymous')
when "progressDesc"
@members.comparator = (a, b) ->
aComplete = (concept for concept, state of userConceptsMap[a.id] when state is 'complete')
@ -164,9 +164,9 @@ module.exports = class ClanDetailsView extends RootView
else if aStarted < bStarted then return 1
if highestUserLevelCountMap[a.id] > highestUserLevelCountMap[b.id] then return -1
else if highestUserLevelCountMap[a.id] < highestUserLevelCountMap[b.id] then return 1
(b.get('name') or 'Anoner').localeCompare(a.get('name') or 'Anoner')
(b.get('name') or 'Anonymous').localeCompare(a.get('name') or 'Anonymous')
else
@members.comparator = (a, b) -> return (a.get('name') or 'Anoner').localeCompare(b.get('name') or 'Anoner')
@members.comparator = (a, b) -> return (a.get('name') or 'Anonymous').localeCompare(b.get('name') or 'Anonymous')
@members.sort()
updateHeroIcons: ->

View file

@ -110,10 +110,8 @@ module.exports = class RootView extends CocoView
@buildLanguages()
$('body').removeClass('is-playing')
if application.isProduction()
title = 'CodeCombat - ' + (@getTitle() or 'Learn how to code by playing a game')
else
title = @getTitle() or @constructor.name
if title = @getTitle() then title += ' | CodeCombat'
else title = 'CodeCombat - Learn how to code by playing a game'
$('title').text(title)

View file

@ -28,6 +28,8 @@ module.exports = class CoursesView extends RootView
'submit #join-class-form': 'onSubmitJoinClassForm'
'click #change-language-link': 'onClickChangeLanguageLink'
getTitle: -> return $.i18n.t('teacher.students')
initialize: ->
@courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance})
@courseInstances.comparator = (ci) -> return ci.get('classroomID') + ci.get('courseID')

View file

@ -19,6 +19,8 @@ module.exports = class EnrollmentsView extends RootView
'click #how-to-enroll-link': 'onClickHowToEnrollLink'
'click #contact-us-btn': 'onClickContactUsButton'
getTitle: -> return $.i18n.t('teacher.enrollments')
initialize: ->
@state = new State({
totalEnrolled: 0

View file

@ -63,6 +63,8 @@ module.exports = class TeacherClassView extends RootView
enrolledUsers: ""
}
getTitle: -> return @classroom?.get('name')
initialize: (options, classroomID) ->
super(options)
@singleStudentCourseProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-course'
@ -277,7 +279,6 @@ module.exports = class TeacherClassView extends RootView
@state.set('searchTerm', $(e.target).val())
onChangeCourseSelect: (e) ->
console.log '??'
@trigger 'course-select:change', { selectedCourse: @courses.get($(e.currentTarget).val()) }
getSelectedStudentIDs: ->

View file

@ -25,6 +25,8 @@ module.exports = class TeacherClassesView extends RootView
'click .add-students-btn': 'onClickAddStudentsButton'
'click .create-classroom-btn': 'onClickCreateClassroomButton'
getTitle: -> return $.i18n.t('teacher.my_classes')
initialize: (options) ->
super(options)
@classrooms = new Classrooms()

View file

@ -39,6 +39,8 @@ module.exports = class TeacherCoursesView extends RootView
"569ed916efa72b0ced971447": null
}
getTitle: -> return $.i18n.t('teacher.courses')
constructor: (options) ->
super(options)
@ownedClassrooms = new Classrooms()

View file

@ -61,7 +61,7 @@ module.exports = class LadderPlayModal extends ModalView
success = (@nameMap) =>
for challenger in _.values(@challengers)
challenger.opponentName = @nameMap[challenger.opponentID]?.name or 'Anoner'
challenger.opponentName = @nameMap[challenger.opponentID]?.name or 'Anonymous'
challenger.opponentWizard = @nameMap[challenger.opponentID]?.wizard or {}
@checkWizardLoaded()

View file

@ -207,7 +207,7 @@ module.exports = class SpectateLevelView extends RootView
findPlayerNames: ->
playerNames = {}
for session in [@session, @otherSession] when session?.get('team')
playerNames[session.get('team')] = session.get('creatorName') or 'Anoner'
playerNames[session.get('team')] = session.get('creatorName') or 'Anonymous'
playerNames
initGoalManager: ->

View file

@ -353,7 +353,7 @@ module.exports = class PlayLevelView extends RootView
return {} unless @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder']
playerNames = {}
for session in [@session, @otherSession] when session?.get('team')
playerNames[session.get('team')] = session.get('creatorName') or 'Anoner'
playerNames[session.get('team')] = session.get('creatorName') or 'Anonymous'
playerNames
# Once Surface is Loaded ####################################################

View file

@ -87,7 +87,7 @@ emailUserInitialRecruiting = (user, callback) ->
return callback null, false if DEBUGGING and (totalEmailsSent > 1 or Math.random() > 0.05)
++totalEmailsSent
name = if user.firstName and user.lastName then "#{user.firstName}" else user.name
name = 'Wizard' if not name or name is 'Anoner'
name = 'Wizard' if not name or name in ['Anoner', 'Anonymous']
team = user.session.levelInfo.team
team = team.substr(0, team.length - 1)
context =

View file

@ -69,6 +69,7 @@ ClassroomHandler = class ClassroomHandler extends Handler
return _.omit(doc.toObject(), 'code', 'codeCamel')
inviteStudents: (req, res, classroomID) ->
return @sendUnauthorizedError(res) if not req.user?
if not req.body.emails
return @sendBadInputError(res, 'Emails not included')
@ -86,6 +87,7 @@ ClassroomHandler = class ClassroomHandler extends Handler
recipient:
address: email
email_data:
teacher_name: req.user.broadName()
class_name: classroom.get('name')
join_link: "https://codecombat.com/courses?_cc=" + joinCode
join_code: joinCode
@ -103,7 +105,7 @@ ClassroomHandler = class ClassroomHandler extends Handler
return @sendSuccess(res, (@formatEntity(req, classroom) for classroom in classrooms))
else if memberID = req.query.memberID
unless req.user and (req.user.isAdmin() or memberID is req.user.id)
log.debug "classroom_handler.get: memberID (#{memberID}) must be yourself (#{req.user.id})"
log.debug "classroom_handler.get: memberID (#{memberID}) must be yourself (#{req.user?.id})"
return @sendForbiddenError(res)
return @sendBadInputError(res, 'Bad memberID') unless utils.isID memberID
Classroom.find {members: mongoose.Types.ObjectId(memberID)}, (err, classrooms) =>

View file

@ -192,6 +192,7 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
address: email
subject: course.get('name')
email_data:
teacher_name: req.user.broadName()
class_name: course.get('name')
join_link: "https://codecombat.com/courses/students?_ppc=" + prepaid.get('code')
sendwithus.api.send context, _.noop

View file

@ -68,4 +68,5 @@ module.exports =
return done(null, activity.sender) if /@codecombat\.com/ig.test(activity.sender)
return done(null, config.mail.supportSchools)
catch error
log.error("closeIO.getSalesContactEmail Error for #{email}: #{JSON.stringify(error)}")
return done(error, config.mail.supportSchools)

View file

@ -132,7 +132,7 @@ module.exports =
post: wrap (req, res) ->
throw new errors.Unauthorized() unless req.user and not req.user.isAnonymous()
unless req.user?.isTeacher()
console.log "classrooms.post: Can't create classroom if you (#{req.user?.id}) aren't a teacher."
log.debug "classrooms.post: Can't create classroom if you (#{req.user?.id}) aren't a teacher."
throw new errors.Forbidden()
classroom = database.initDoc(req, Classroom)
classroom.set 'ownerID', req.user._id
@ -205,7 +205,8 @@ module.exports =
ownedStudentIDs = _.flatten ownedClassrooms.map (c) ->
c.get('members').map (id) ->
id.toString()
return next() unless memberID in ownedStudentIDs
unless memberID in ownedStudentIDs
throw new errors.Forbidden("Can't reset the password of a student that's not in one of your classrooms.")
student = yield User.findById(memberID)
if student.get('emailVerified')
log.debug "classrooms.setStudentPassword: Can't reset password for a student (#{memberID}) that has verified their email address."

View file

@ -49,7 +49,7 @@ UserSchema.methods.broadName = ->
return name if name
[emailName, emailDomain] = @get('email').split('@')
return emailName if emailName
return 'Anoner'
return 'Anonymous'
UserSchema.methods.isInGodMode = ->
p = @get('permissions')

View file

@ -578,7 +578,7 @@ sendLadderUpdateEmail = (session, now, daysAgo) ->
#log.info "Not sending email to #{user.get('email')} #{user.get('name')} because the session had levelName #{session.levelName} or team #{session.team} in it."
return
name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name')
name = 'Wizard' if not name or name is 'Anoner'
name = 'Wizard' if not name or name is 'Anonymous'
# Fetch the most recent defeat and victory, if there are any.
# (We could look at strongest/weakest, but we'd have to fetch everyone, or denormalize more.)
@ -622,13 +622,13 @@ sendLadderUpdateEmail = (session, now, daysAgo) ->
if err
log.error "Couldn't find defeateded opponent: #{err}"
defeatedOpponent = null
victoryContext = {opponent_name: defeatedOpponent?.name ? 'Anoner', url: urlForMatch(victory)} if victory
victoryContext = {opponent_name: defeatedOpponent?.name ? 'Anonymous', url: urlForMatch(victory)} if victory
onFetchedVictoriousOpponent = (err, victoriousOpponent) ->
if err
log.error "Couldn't find victorious opponent: #{err}"
victoriousOpponent = null
defeatContext = {opponent_name: victoriousOpponent?.name ? 'Anoner', url: urlForMatch(defeat)} if defeat
defeatContext = {opponent_name: victoriousOpponent?.name ? 'Anonymous', url: urlForMatch(defeat)} if defeat
Level.find({original: session.level.original, created: {$gt: session.submitDate}}).select('created commitMessage version').sort('-created').lean().exec (err, levelVersions) ->
sendEmail defeatContext, victoryContext, (if levelVersions.length then levelVersions else null)
@ -706,7 +706,7 @@ sendNextStepsEmail = (user, now, daysAgo) ->
do (err, nextLevel) ->
return log.error "Couldn't find next level for #{user.get('email')}: #{err}" if err
name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name')
name = 'hero' if not name or name is 'Anoner'
name = 'Hero' if not name or name in ['Anoner', 'Anonymous']
#secretLevel = switch user.get('testGroupNumber') % 8
# when 0, 1, 2, 3 then name: 'Forgetful Gemsmith', slug: 'forgetful-gemsmith'
# when 4, 5, 6, 7 then name: 'Signs and Portents', slug: 'signs-and-portents'

View file

@ -447,3 +447,73 @@ describe 'GET /db/classroom/:handle/members', ->
expect(user.email).toBeDefined()
expect(user.passwordHash).toBeUndefined()
done()
describe 'POST /db/classroom/:classroomID/members/:memberID/reset-password', ->
it 'changes the password', utils.wrap (done) ->
yield utils.clearModels([User, Classroom])
teacher = yield utils.initUser()
yield utils.loginUser(teacher)
student = yield utils.initUser({ name: "Firstname Lastname" })
newPassword = "this is a new password"
classroom = yield new Classroom({name: 'Classroom', ownerID: teacher._id, members: [student._id] }).save()
expect(student.get('passwordHash')).not.toEqual(User.hashPassword(newPassword))
[res, body] = yield request.postAsync({
uri: getURL("/db/classroom/#{classroom.id}/members/#{student.id}/reset-password")
json: { password: newPassword }
})
expect(res.statusCode).toBe(200)
changedStudent = yield User.findById(student.id)
expect(changedStudent.get('passwordHash')).toEqual(User.hashPassword(newPassword))
done()
it "doesn't change the password if you're not their teacher", utils.wrap (done) ->
yield utils.clearModels([User, Classroom])
teacher = yield utils.initUser()
yield utils.loginUser(teacher)
student = yield utils.initUser({ name: "Firstname Lastname" })
student2 = yield utils.initUser({ name: "Firstname Lastname 2" })
newPassword = "this is a new password"
classroom = yield new Classroom({name: 'Classroom', ownerID: teacher._id, members: [student2._id] }).save()
expect(student.get('passwordHash')).not.toEqual(User.hashPassword(newPassword))
[res, body] = yield request.postAsync({
uri: getURL("/db/classroom/#{classroom.id}/members/#{student.id}/reset-password")
json: { password: newPassword }
})
expect(res.statusCode).toBe(403)
changedStudent = yield User.findById(student.id)
expect(changedStudent.get('passwordHash')).toEqual(student.get('passwordHash'))
done()
it "doesn't change the password if their email is verified", utils.wrap (done) ->
yield utils.clearModels([User, Classroom])
teacher = yield utils.initUser()
yield utils.loginUser(teacher)
student = yield utils.initUser({ name: "Firstname Lastname", emailVerified: true })
newPassword = "this is a new password"
classroom = yield new Classroom({name: 'Classroom', ownerID: teacher._id, members: [student._id] }).save()
expect(student.get('passwordHash')).not.toEqual(User.hashPassword(newPassword))
[res, body] = yield request.postAsync({
uri: getURL("/db/classroom/#{classroom.id}/members/#{student.id}/reset-password")
json: { password: newPassword }
})
expect(res.statusCode).toBe(403)
changedStudent = yield User.findById(student.id)
expect(changedStudent.get('passwordHash')).toEqual(student.get('passwordHash'))
done()
it "doesn't let you set a 1-character password", utils.wrap (done) ->
yield utils.clearModels([User, Classroom])
teacher = yield utils.initUser()
yield utils.loginUser(teacher)
student = yield utils.initUser({ name: "Firstname Lastname" })
newPassword = "e"
classroom = yield new Classroom({name: 'Classroom', ownerID: teacher._id, members: [student._id] }).save()
expect(student.get('passwordHash')).not.toEqual(User.hashPassword(newPassword))
[res, body] = yield request.postAsync({
uri: getURL("/db/classroom/#{classroom.id}/members/#{student.id}/reset-password")
json: { password: newPassword }
})
expect(res.statusCode).toBe(422)
changedStudent = yield User.findById(student.id)
expect(changedStudent.get('passwordHash')).toEqual(student.get('passwordHash'))
done()