mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-02-24 13:34:08 -05:00
Add remove student modal to ClassroomView
This commit is contained in:
parent
a0e5126ab7
commit
7f2ddba089
11 changed files with 256 additions and 3 deletions
|
@ -13,4 +13,13 @@ module.exports = class Classroom extends CocoModel
|
||||||
data: { code: code }
|
data: { code: code }
|
||||||
}
|
}
|
||||||
_.extend options, opts
|
_.extend options, opts
|
||||||
|
@fetch(options)
|
||||||
|
|
||||||
|
removeMember: (userID, opts) ->
|
||||||
|
options = {
|
||||||
|
url: _.result(@, 'url') + '/members'
|
||||||
|
type: 'DELETE'
|
||||||
|
data: { userID: userID }
|
||||||
|
}
|
||||||
|
_.extend options, opts
|
||||||
@fetch(options)
|
@fetch(options)
|
|
@ -23,5 +23,14 @@ module.exports = class CourseInstance extends CocoModel
|
||||||
_.extend options, opts
|
_.extend options, opts
|
||||||
@fetch(options)
|
@fetch(options)
|
||||||
|
|
||||||
|
removeMember: (userID, opts) ->
|
||||||
|
options = {
|
||||||
|
url: _.result(@, 'url') + '/members'
|
||||||
|
type: 'DELETE'
|
||||||
|
data: { userID: userID }
|
||||||
|
}
|
||||||
|
_.extend options, opts
|
||||||
|
@fetch(options)
|
||||||
|
|
||||||
firstLevelURL: ->
|
firstLevelURL: ->
|
||||||
"/play/level/dungeons-of-kithgard?course=#{@get('courseID')}&course-instance=#{@id}"
|
"/play/level/dungeons-of-kithgard?course=#{@get('courseID')}&course-instance=#{@id}"
|
||||||
|
|
3
app/styles/courses/remove-student-modal.sass
Normal file
3
app/styles/courses/remove-student-modal.sass
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#remove-student-modal
|
||||||
|
.glyphicon-warning-sign
|
||||||
|
font-size: 40px
|
|
@ -29,6 +29,10 @@ block content
|
||||||
hr
|
hr
|
||||||
|
|
||||||
for user in view.users.models
|
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()
|
h2= user.broadName()
|
||||||
- var lastPlayedString = view.makeLastPlayedString(user);
|
- var lastPlayedString = view.makeLastPlayedString(user);
|
||||||
if lastPlayedString
|
if lastPlayedString
|
||||||
|
|
27
app/templates/courses/remove-student-modal.jade
Normal file
27
app/templates/courses/remove-student-modal.jade
Normal 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
|
|
@ -12,6 +12,7 @@ Prepaid = require 'models/Prepaid'
|
||||||
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
|
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
|
||||||
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
|
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
|
||||||
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
|
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
|
||||||
|
RemoveStudentModal = require 'views/courses/RemoveStudentModal'
|
||||||
|
|
||||||
module.exports = class ClassroomView extends RootView
|
module.exports = class ClassroomView extends RootView
|
||||||
id: 'classroom-view'
|
id: 'classroom-view'
|
||||||
|
@ -23,6 +24,7 @@ module.exports = class ClassroomView extends RootView
|
||||||
'click .activate-single-license-btn': 'onClickActivateSingleLicenseButton'
|
'click .activate-single-license-btn': 'onClickActivateSingleLicenseButton'
|
||||||
'click #add-students-btn': 'onClickAddStudentsButton'
|
'click #add-students-btn': 'onClickAddStudentsButton'
|
||||||
'click .enable-btn': 'onClickEnableButton'
|
'click .enable-btn': 'onClickEnableButton'
|
||||||
|
'click .remove-student-link': 'onClickRemoveStudentLink'
|
||||||
|
|
||||||
initialize: (options, classroomID) ->
|
initialize: (options, classroomID) ->
|
||||||
@classroom = new Classroom({_id: classroomID})
|
@classroom = new Classroom({_id: classroomID})
|
||||||
|
@ -107,4 +109,18 @@ module.exports = class ClassroomView extends RootView
|
||||||
userID = $(e.target).data('user-id')
|
userID = $(e.target).data('user-id')
|
||||||
courseInstance.addMember(userID)
|
courseInstance.addMember(userID)
|
||||||
$(e.target).attr('disabled', true)
|
$(e.target).attr('disabled', true)
|
||||||
@listenToOnce courseInstance, 'sync', @render
|
@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()
|
37
app/views/courses/RemoveStudentModal.coffee
Normal file
37
app/views/courses/RemoveStudentModal.coffee
Normal 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()
|
||||||
|
|
|
@ -36,6 +36,7 @@ ClassroomHandler = class ClassroomHandler extends Handler
|
||||||
method = req.method.toLowerCase()
|
method = req.method.toLowerCase()
|
||||||
return @inviteStudents(req, res, args[0]) if args[1] is 'invite-members'
|
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 @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'
|
return @getMembersAPI(req, res, args[0]) if args[1] is 'members'
|
||||||
super(arguments...)
|
super(arguments...)
|
||||||
|
|
||||||
|
@ -65,6 +66,25 @@ ClassroomHandler = class ClassroomHandler extends Handler
|
||||||
classroom.set('members', members)
|
classroom.set('members', members)
|
||||||
return @sendSuccess(res, @formatEntity(req, classroom))
|
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) ->
|
formatEntity: (req, doc) ->
|
||||||
if req.user?.isAdmin() or req.user?.get('_id').equals(doc.get('ownerID'))
|
if req.user?.isAdmin() or req.user?.get('_id').equals(doc.get('ownerID'))
|
||||||
return doc.toObject()
|
return doc.toObject()
|
||||||
|
|
|
@ -35,6 +35,7 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
||||||
return @createHOCAPI(req, res) if relationship is 'create-for-hoc'
|
return @createHOCAPI(req, res) if relationship is 'create-for-hoc'
|
||||||
return @getLevelSessionsAPI(req, res, args[0]) if args[1] is 'level_sessions'
|
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 @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 @getMembersAPI(req, res, args[0]) if args[1] is 'members'
|
||||||
return @inviteStudents(req, res, args[0]) if relationship is 'invite_students'
|
return @inviteStudents(req, res, args[0]) if relationship is 'invite_students'
|
||||||
return @redeemPrepaidCodeAPI(req, res) if args[1] is 'redeem_prepaid'
|
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
|
return @sendDatabaseError(res, err) if err
|
||||||
@sendSuccess(res, @formatEntity(req, courseInstance))
|
@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) ->
|
post: (req, res) ->
|
||||||
return @sendBadInputError(res, 'No classroomID') unless req.body.classroomID
|
return @sendBadInputError(res, 'No classroomID') unless req.body.classroomID
|
||||||
return @sendBadInputError(res, 'No courseID') unless req.body.courseID
|
return @sendBadInputError(res, 'No courseID') unless req.body.courseID
|
||||||
|
|
|
@ -111,7 +111,7 @@ describe 'PUT /db/classroom', ->
|
||||||
expect(res.statusCode).toBe(403)
|
expect(res.statusCode).toBe(403)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
describe 'POST /db/classroom/:id/members', ->
|
describe 'POST /db/classroom/~/members', ->
|
||||||
|
|
||||||
it 'clears database users and classrooms', (done) ->
|
it 'clears database users and classrooms', (done) ->
|
||||||
clearModels [User, Classroom], (err) ->
|
clearModels [User, Classroom], (err) ->
|
||||||
|
@ -135,6 +135,36 @@ describe 'POST /db/classroom/:id/members', ->
|
||||||
done()
|
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', ->
|
describe 'POST /db/classroom/:id/invite-members', ->
|
||||||
|
|
||||||
it 'takes a list of emails and sends invites', (done) ->
|
it 'takes a list of emails and sends invites', (done) ->
|
||||||
|
|
|
@ -85,8 +85,19 @@ describe 'POST /db/course_instance/:id/members', ->
|
||||||
cb()
|
cb()
|
||||||
|
|
||||||
], makeTestIterator(@), done)
|
], 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) ->
|
it 'return 403 if the member is not in the classroom', (done) ->
|
||||||
async.eachSeries([
|
async.eachSeries([
|
||||||
|
|
||||||
|
@ -155,5 +166,67 @@ describe 'POST /db/course_instance/:id/members', ->
|
||||||
test.prepaid.set('redeemers', [{userID: test.user.get('_id')}])
|
test.prepaid.set('redeemers', [{userID: test.user.get('_id')}])
|
||||||
test.prepaid.save cb
|
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)
|
makeTestIterator = (testObject) -> (func, callback) -> func(testObject, callback)
|
||||||
|
|
Loading…
Reference in a new issue