mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 09:35:39 -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
|
@ -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)
|
|
@ -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}"
|
||||
|
|
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
|
||||
|
||||
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
|
||||
|
|
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'
|
||||
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()
|
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()
|
||||
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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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)
|
||||
|
Loading…
Reference in a new issue