Add remove student modal to ClassroomView

This commit is contained in:
Scott Erickson 2015-12-02 11:56:38 -08:00
parent a0e5126ab7
commit 7f2ddba089
11 changed files with 256 additions and 3 deletions

View file

@ -14,3 +14,12 @@ module.exports = class Classroom extends CocoModel
}
_.extend options, opts
@fetch(options)
removeMember: (userID, opts) ->
options = {
url: _.result(@, 'url') + '/members'
type: 'DELETE'
data: { userID: userID }
}
_.extend options, opts
@fetch(options)

View file

@ -23,5 +23,14 @@ module.exports = class CourseInstance extends CocoModel
_.extend options, opts
@fetch(options)
removeMember: (userID, opts) ->
options = {
url: _.result(@, 'url') + '/members'
type: 'DELETE'
data: { userID: userID }
}
_.extend options, opts
@fetch(options)
firstLevelURL: ->
"/play/level/dungeons-of-kithgard?course=#{@get('courseID')}&course-instance=#{@id}"

View file

@ -0,0 +1,3 @@
#remove-student-modal
.glyphicon-warning-sign
font-size: 40px

View file

@ -29,6 +29,10 @@ block content
hr
for user in view.users.models
a.remove-student-link.pull-right.text-uppercase(data-user-id=user.id)
span.glyphicon.glyphicon-remove
span.spl remove student
h2= user.broadName()
- var lastPlayedString = view.makeLastPlayedString(user);
if lastPlayedString

View file

@ -0,0 +1,27 @@
extends /templates/core/modal-base
block modal-header-content
.text-center
h3.modal-title Remove Student
span.glyphicon.glyphicon-warning-sign.text-danger
h3 Are you sure you want to remove this student from this class?
block modal-body-content
p.text-center
| Student will lose access to this classroom and assigned classes.
| Progress and gameplay is NOT lost, and the student can be added back to the classroom at any time.
if view.user.get('coursePrepaidID')
| The activated paid license will not be returned.
block modal-footer-content
#remove-student-buttons.text-center
p
button.btn.btn-lg.btn-success.text-uppercase(data-dismiss="modal") Keep Student
p - OR -
p
button#remove-student-btn.btn.btn-lg.btn-default.text-uppercase Remove Student
#remove-student-progress.text-center.hide
.progress
.progress-bar
p.text-info Removing user

View file

@ -12,6 +12,7 @@ Prepaid = require 'models/Prepaid'
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
RemoveStudentModal = require 'views/courses/RemoveStudentModal'
module.exports = class ClassroomView extends RootView
id: 'classroom-view'
@ -23,6 +24,7 @@ module.exports = class ClassroomView extends RootView
'click .activate-single-license-btn': 'onClickActivateSingleLicenseButton'
'click #add-students-btn': 'onClickAddStudentsButton'
'click .enable-btn': 'onClickEnableButton'
'click .remove-student-link': 'onClickRemoveStudentLink'
initialize: (options, classroomID) ->
@classroom = new Classroom({_id: classroomID})
@ -108,3 +110,17 @@ module.exports = class ClassroomView extends RootView
courseInstance.addMember(userID)
$(e.target).attr('disabled', true)
@listenToOnce courseInstance, 'sync', @render
onClickRemoveStudentLink: (e) ->
user = @users.get($(e.target).closest('a').data('user-id'))
modal = new RemoveStudentModal({
classroom: @classroom
user: user
courseInstances: @courseInstances
})
@openModalView(modal)
modal.once 'remove-student', @onStudentRemoved, @
onStudentRemoved: (e) ->
@users.remove(e.user)
@render()

View file

@ -0,0 +1,37 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/courses/remove-student-modal'
module.exports = class RemoveStudentModal extends ModalView
id: 'remove-student-modal'
template: template
events:
'click #remove-student-btn': 'onClickRemoveStudentButton'
initialize: (options) ->
@classroom = options.classroom
@user = options.user
@courseInstances = options.courseInstances
onClickRemoveStudentButton: ->
@$('#remove-student-buttons').addClass('hide')
@$('#remove-student-progress').removeClass('hide')
userID = @user.id
@toRemove = @courseInstances.filter (courseInstance) -> _.contains(courseInstance.get('members'), userID)
@toRemove.push @classroom
@totalJobs = _.size(@toRemove)
@removeStudent()
removeStudent: ->
model = @toRemove.shift()
if not model
@trigger 'remove-student', { user: @user }
@hide()
return
model.removeMember(@user.id)
pct = (100 * (@totalJobs - @toRemove.length) / @totalJobs).toFixed(1) + '%'
@$('#remove-student-progress .progress-bar').css('width', pct)
@listenToOnce model, 'sync', ->
@removeStudent()

View file

@ -36,6 +36,7 @@ ClassroomHandler = class ClassroomHandler extends Handler
method = req.method.toLowerCase()
return @inviteStudents(req, res, args[0]) if args[1] is 'invite-members'
return @joinClassroomAPI(req, res, args[0]) if method is 'post' and args[1] is 'members'
return @removeMember(req, res, args[0]) if req.method is 'DELETE' and args[1] is 'members'
return @getMembersAPI(req, res, args[0]) if args[1] is 'members'
super(arguments...)
@ -65,6 +66,25 @@ ClassroomHandler = class ClassroomHandler extends Handler
classroom.set('members', members)
return @sendSuccess(res, @formatEntity(req, classroom))
removeMember: (req, res, classroomID) ->
userID = req.body.userID
return @sendBadInputError(res, 'Input must be a MongoDB ID') unless utils.isID(userID)
Classroom.findById classroomID, (err, classroom) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'Classroom referenced by course instance not found') unless classroom
return @sendForbiddenError(res) unless _.any(classroom.get('members'), (memberID) -> memberID.toString() is userID)
ownsClassroom = classroom.get('ownerID').equals(req.user.get('_id'))
removingSelf = userID is req.user.id
return @sendForbiddenError(res) unless ownsClassroom or removingSelf
alreadyNotInClassroom = not _.any classroom.get('members') or [], (memberID) -> memberID.toString() is userID
return @sendSuccess(res, @formatEntity(req, classroom)) if alreadyNotInClassroom
members = _.clone(classroom.get('members'))
members.splice(members.indexOf(userID), 1)
classroom.set('members', members)
classroom.save (err, classroom) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, classroom))
formatEntity: (req, doc) ->
if req.user?.isAdmin() or req.user?.get('_id').equals(doc.get('ownerID'))
return doc.toObject()

View file

@ -35,6 +35,7 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
return @createHOCAPI(req, res) if relationship is 'create-for-hoc'
return @getLevelSessionsAPI(req, res, args[0]) if args[1] is 'level_sessions'
return @addMember(req, res, args[0]) if req.method is 'POST' and args[1] is 'members'
return @removeMember(req, res, args[0]) if req.method is 'DELETE' and args[1] is 'members'
return @getMembersAPI(req, res, args[0]) if args[1] is 'members'
return @inviteStudents(req, res, args[0]) if relationship is 'invite_students'
return @redeemPrepaidCodeAPI(req, res) if args[1] is 'redeem_prepaid'
@ -94,6 +95,30 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, courseInstance))
removeMember: (req, res, courseInstanceID) ->
userID = req.body.userID
return @sendBadInputError(res, 'Input must be a MongoDB ID') unless utils.isID(userID)
CourseInstance.findById courseInstanceID, (err, courseInstance) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'Course instance not found') unless courseInstance
Classroom.findById courseInstance.get('classroomID'), (err, classroom) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'Classroom referenced by course instance not found') unless classroom
return @sendForbiddenError(res) unless _.any(classroom.get('members'), (memberID) -> memberID.toString() is userID)
ownsCourseInstance = courseInstance.get('ownerID').equals(req.user.get('_id'))
removingSelf = userID is req.user.id
return @sendForbiddenError(res) unless ownsCourseInstance or removingSelf
alreadyNotInCourseInstance = not _.any courseInstance.get('members') or [], (memberID) -> memberID.toString() is userID
return @sendSuccess(res, @formatEntity(req, courseInstance)) if alreadyNotInCourseInstance
members = _.clone(courseInstance.get('members'))
members.splice(members.indexOf(userID), 1)
courseInstance.set('members', members)
courseInstance.save (err, courseInstance) =>
return @sendDatabaseError(res, err) if err
User.update {_id: mongoose.Types.ObjectId(userID)}, {$pull: {courseInstances: courseInstance.get('_id')}}, (err) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, courseInstance))
post: (req, res) ->
return @sendBadInputError(res, 'No classroomID') unless req.body.classroomID
return @sendBadInputError(res, 'No courseID') unless req.body.courseID

View file

@ -111,7 +111,7 @@ describe 'PUT /db/classroom', ->
expect(res.statusCode).toBe(403)
done()
describe 'POST /db/classroom/:id/members', ->
describe 'POST /db/classroom/~/members', ->
it 'clears database users and classrooms', (done) ->
clearModels [User, Classroom], (err) ->
@ -135,6 +135,36 @@ describe 'POST /db/classroom/:id/members', ->
done()
describe 'DELETE /db/classroom/:id/members', ->
it 'clears database users and classrooms', (done) ->
clearModels [User, Classroom], (err) ->
throw err if err
done()
it 'removes the given user from the list of members in the classroom', (done) ->
loginNewUser (user1) ->
data = { name: 'Classroom 6' }
request.post {uri: classroomsURL, json: data }, (err, res, body) ->
classroomCode = body.code
classroomID = body._id
expect(res.statusCode).toBe(200)
loginNewUser (user2) ->
url = getURL("/db/classroom/~/members")
data = { code: classroomCode }
request.post { uri: url, json: data }, (err, res, body) ->
expect(res.statusCode).toBe(200)
Classroom.findById classroomID, (err, classroom) ->
expect(classroom.get('members').length).toBe(1)
url = getURL("/db/classroom/#{classroom.id}/members")
data = { userID: user2.id }
request.del { uri: url, json: data }, (err, res, body) ->
expect(res.statusCode).toBe(200)
Classroom.findById classroomID, (err, classroom) ->
expect(classroom.get('members').length).toBe(0)
done()
describe 'POST /db/classroom/:id/invite-members', ->
it 'takes a list of emails and sends invites', (done) ->

View file

@ -86,6 +86,17 @@ describe 'POST /db/course_instance/:id/members', ->
], makeTestIterator(@), done)
it 'adds the CourseInstance id to the user', (done) ->
async.eachSeries([
addTestUserToClassroom,
(test, cb) ->
url = getURL("/db/course_instance/#{test.courseInstance.id}/members")
request.post {uri: url, json: {userID: test.user.id}}, (err, res, body) ->
User.findById(test.user.id).exec (err, user) ->
expect(_.size(user.get('courseInstances'))).toBe(1)
cb()
], makeTestIterator(@), done)
it 'return 403 if the member is not in the classroom', (done) ->
async.eachSeries([
@ -155,5 +166,67 @@ describe 'POST /db/course_instance/:id/members', ->
test.prepaid.set('redeemers', [{userID: test.user.get('_id')}])
test.prepaid.save cb
describe 'DELETE /db/course_instance/:id/members', ->
beforeEach (done) -> clearModels([CourseInstance, Course, User, Classroom, Prepaid], done)
beforeEach (done) -> loginJoe (@joe) => done()
beforeEach init.course({free: true})
beforeEach init.classroom()
beforeEach init.courseInstance()
beforeEach init.user()
beforeEach init.prepaid()
it 'removes a member to the given CourseInstance', (done) ->
async.eachSeries([
addTestUserToClassroom,
addTestUserToCourseInstance,
(test, cb) ->
url = getURL("/db/course_instance/#{test.courseInstance.id}/members")
request.del {uri: url, json: {userID: test.user.id}}, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.members.length).toBe(0)
cb()
], makeTestIterator(@), done)
it 'removes the CourseInstance from the User.courseInstances', (done) ->
async.eachSeries([
addTestUserToClassroom,
addTestUserToCourseInstance,
(test, cb) ->
User.findById(test.user.id).exec (err, user) ->
expect(_.size(user.get('courseInstances'))).toBe(1)
cb()
removeTestUserFromCourseInstance,
(test, cb) ->
User.findById(test.user.id).exec (err, user) ->
expect(_.size(user.get('courseInstances'))).toBe(0)
cb()
], makeTestIterator(@), done)
addTestUserToClassroom = (test, cb) ->
test.classroom.set('members', [test.user.get('_id')])
test.classroom.save cb
addTestUserToCourseInstance = (test, cb) ->
url = getURL("/db/course_instance/#{test.courseInstance.id}/members")
request.post {uri: url, json: {userID: test.user.id}}, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.members.length).toBe(1)
expect(body.members[0]).toBe(test.user.id)
cb()
removeTestUserFromCourseInstance = (test, cb) ->
url = getURL("/db/course_instance/#{test.courseInstance.id}/members")
request.del {uri: url, json: {userID: test.user.id}}, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.members.length).toBe(0)
cb()
makeTestIterator = (testObject) -> (func, callback) -> func(testObject, callback)