mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-20 12:32:28 -05:00
d72e4eb750
Update classroom and gameplay Ux to surface practice levels as 3a, 3b, etc. Update next level logic to leverage practice levels based on per level completion playtime thresholds. Patrol buster and patrol buster A are live for testing. Fix a few classroom Ux progress hover bubble info bugs. Closes #3767
429 lines
17 KiB
CoffeeScript
429 lines
17 KiB
CoffeeScript
async = require 'async'
|
|
config = require '../../../server_config'
|
|
require '../common'
|
|
stripe = require('stripe')(config.stripe.secretKey)
|
|
utils = require '../utils'
|
|
CourseInstance = require '../../../server/models/CourseInstance'
|
|
Course = require '../../../server/models/Course'
|
|
User = require '../../../server/models/User'
|
|
Classroom = require '../../../server/models/Classroom'
|
|
Campaign = require '../../../server/models/Campaign'
|
|
Level = require '../../../server/models/Level'
|
|
LevelSession = require '../../../server/models/LevelSession'
|
|
Prepaid = require '../../../server/models/Prepaid'
|
|
request = require '../request'
|
|
moment = require 'moment'
|
|
|
|
courseFixture = {
|
|
name: 'Unnamed course'
|
|
campaignID: ObjectId("55b29efd1cd6abe8ce07db0d")
|
|
concepts: ['basic_syntax', 'arguments', 'while_loops', 'strings', 'variables']
|
|
description: "Learn basic syntax, while loops, and the CodeCombat environment."
|
|
screenshot: "/images/pages/courses/101_info.png"
|
|
}
|
|
|
|
classroomFixture = {
|
|
name: 'Unnamed classroom'
|
|
members: []
|
|
}
|
|
|
|
describe 'POST /db/course_instance', ->
|
|
url = getURL('/db/course_instance')
|
|
|
|
beforeEach utils.wrap (done) ->
|
|
yield utils.clearModels([CourseInstance, Course, User, Classroom])
|
|
@teacher = yield utils.initUser({role: 'teacher'})
|
|
yield utils.loginUser(@teacher)
|
|
@course = yield new Course(courseFixture).save()
|
|
classroomData = _.extend({ownerID: @teacher._id}, classroomFixture)
|
|
@classroom = yield new Classroom(classroomData).save()
|
|
done()
|
|
|
|
it 'creates a CourseInstance', utils.wrap (done) ->
|
|
data = {
|
|
name: 'Some Name'
|
|
courseID: @course.id
|
|
classroomID: @classroom.id
|
|
}
|
|
[res, body] = yield request.postAsync {uri: url, json: data}
|
|
expect(res.statusCode).toBe(200)
|
|
expect(body.classroomID).toBeDefined()
|
|
done()
|
|
|
|
it 'returns the same CourseInstance if you POST twice', utils.wrap (done) ->
|
|
data = {
|
|
name: 'Some Name'
|
|
courseID: @course.id
|
|
classroomID: @classroom.id
|
|
}
|
|
[res, body] = yield request.postAsync {uri: url, json: data}
|
|
expect(res.statusCode).toBe(200)
|
|
expect(body.classroomID).toBeDefined()
|
|
firstID = body._id
|
|
[res, body] = yield request.postAsync {uri: url, json: data}
|
|
expect(res.statusCode).toBe(200)
|
|
expect(body.classroomID).toBeDefined()
|
|
secondID = body._id
|
|
expect(firstID).toBe(secondID)
|
|
done()
|
|
|
|
it 'returns 404 if the Course does not exist', utils.wrap (done) ->
|
|
data = {
|
|
name: 'Some Name'
|
|
courseID: '123456789012345678901234'
|
|
classroomID: @classroom.id
|
|
}
|
|
[res, body] = yield request.postAsync {uri: url, json: data}
|
|
expect(res.statusCode).toBe(404)
|
|
done()
|
|
|
|
it 'returns 404 if the Classroom does not exist', utils.wrap (done) ->
|
|
data = {
|
|
name: 'Some Name'
|
|
courseID: @course.id
|
|
classroomID: '123456789012345678901234'
|
|
}
|
|
[res, body] = yield request.postAsync {uri: url, json: data}
|
|
expect(res.statusCode).toBe(404)
|
|
done()
|
|
|
|
it 'return 403 if the logged in user does not own the Classroom', utils.wrap (done) ->
|
|
user = yield utils.initUser()
|
|
yield utils.loginUser(user)
|
|
data = {
|
|
name: 'Some Name'
|
|
courseID: @course.id
|
|
classroomID: @classroom.id
|
|
}
|
|
[res, body] = yield request.postAsync {uri: url, json: data}
|
|
expect(res.statusCode).toBe(403)
|
|
done()
|
|
|
|
|
|
describe 'POST /db/course_instance/:id/members', ->
|
|
|
|
beforeEach utils.wrap (done) ->
|
|
yield utils.clearModels([CourseInstance, Course, User, Classroom, Prepaid, Campaign, Level])
|
|
@teacher = yield utils.initUser({role: 'teacher'})
|
|
admin = yield utils.initAdmin()
|
|
yield utils.loginUser(admin)
|
|
@level = yield utils.makeLevel({type: 'course'})
|
|
@campaign = yield utils.makeCampaign({}, {levels: [@level]})
|
|
@course = yield utils.makeCourse({free: true}, {campaign: @campaign})
|
|
@student = yield utils.initUser({role: 'student'})
|
|
@prepaid = yield utils.makePrepaid({creator: @teacher.id})
|
|
members = [@student]
|
|
yield utils.loginUser(@teacher)
|
|
@classroom = yield utils.makeClassroom({aceConfig: { language: 'javascript' }}, { members })
|
|
@courseInstance = yield utils.makeCourseInstance({}, { @course, @classroom })
|
|
done()
|
|
|
|
it 'adds an array of members to the given CourseInstance', utils.wrap (done) ->
|
|
@classroom.set('members', [@student._id])
|
|
yield @classroom.save()
|
|
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
|
[res, body] = yield request.postAsync {uri: url, json: {userIDs: [@student.id]}}
|
|
expect(res.statusCode).toBe(200)
|
|
expect(body.members.length).toBe(1)
|
|
expect(body.members[0]).toBe(@student.id)
|
|
done()
|
|
|
|
it 'adds a member to the given CourseInstance', utils.wrap (done) ->
|
|
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
|
[res, body] = yield request.postAsync {uri: url, json: {userID: @student.id}}
|
|
expect(res.statusCode).toBe(200)
|
|
expect(res.body.members.length).toBe(1)
|
|
expect(res.body.members[0]).toBe(@student.id)
|
|
done()
|
|
|
|
it 'adds the CourseInstance id to the user', utils.wrap (done) ->
|
|
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
|
[res, body] = yield request.postAsync {uri: url, json: {userID: @student.id}}
|
|
user = yield User.findById(@student.id)
|
|
expect(_.size(user.get('courseInstances'))).toBe(1)
|
|
done()
|
|
|
|
it 'return 403 if the member is not in the classroom', utils.wrap (done) ->
|
|
@classroom.set('members', [])
|
|
yield @classroom.save()
|
|
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
|
[res, body] = yield request.postAsync {uri: url, json: {userID: @student.id}}
|
|
expect(res.statusCode).toBe(403)
|
|
done()
|
|
|
|
it 'returns 403 if the user does not own the course instance and is not adding self', utils.wrap (done) ->
|
|
otherUser = yield utils.initUser()
|
|
yield utils.loginUser(otherUser)
|
|
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
|
[res, body] = yield request.postAsync {uri: url, json: {userID: @student.id}}
|
|
expect(res.statusCode).toBe(403)
|
|
done()
|
|
|
|
it 'returns 200 if the user is a member of the classroom and is adding self', ->
|
|
|
|
it 'return 402 if the course is not free and the user is not enrolled', utils.wrap (done) ->
|
|
@course.set('free', false)
|
|
yield @course.save()
|
|
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
|
[res, body] = yield request.postAsync {uri: url, json: {userID: @student.id}}
|
|
expect(res.statusCode).toBe(402)
|
|
done()
|
|
|
|
it 'works if the course is not free and the user is enrolled', utils.wrap (done) ->
|
|
@course.set('free', false)
|
|
yield @course.save()
|
|
@student.set('coursePrepaid', _.pick(@prepaid.toObject(), '_id', 'startDate', 'endDate'))
|
|
yield @student.save()
|
|
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
|
[res, body] = yield request.postAsync {uri: url, json: {userID: @student.id}}
|
|
expect(res.statusCode).toBe(200)
|
|
done()
|
|
|
|
it 'works if the course is not free and the user is enrolled but is not migrated', utils.wrap (done) ->
|
|
@course.set('free', false)
|
|
yield @course.save()
|
|
@student.set('coursePrepaidID', @prepaid._id)
|
|
yield @student.save()
|
|
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
|
[res, body] = yield request.postAsync {uri: url, json: {userID: @student.id}}
|
|
expect(res.statusCode).toBe(200)
|
|
done()
|
|
|
|
describe 'DELETE /db/course_instance/:id/members', ->
|
|
|
|
beforeEach utils.wrap (done) ->
|
|
utils.clearModels([CourseInstance, Course, User, Classroom, Prepaid])
|
|
|
|
# create, login user
|
|
@teacher = yield utils.initUser({role: 'teacher'})
|
|
yield utils.loginUser(@teacher)
|
|
|
|
# create student, course, classroom and course instance
|
|
@student = yield utils.initUser()
|
|
courseData = _.extend({free: true}, courseFixture)
|
|
@course = yield new Course(courseData).save()
|
|
classroomData = _.extend({}, classroomFixture, {ownerID: @teacher._id, members: [@student._id]})
|
|
@classroom = yield new Classroom(classroomData).save()
|
|
url = getURL('/db/course_instance')
|
|
data = {
|
|
name: 'Some Name'
|
|
courseID: @course.id
|
|
classroomID: @classroom.id
|
|
}
|
|
[res, body] = yield request.postAsync {uri: url, json: data}
|
|
@courseInstance = yield CourseInstance.findById res.body._id
|
|
|
|
# add user to course instance
|
|
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
|
[res, body] = yield request.postAsync {uri: url, json: {userID: @student.id}}
|
|
@prepaid = yield new Prepaid({
|
|
type: 'course'
|
|
maxRedeemers: 10
|
|
redeemers: []
|
|
}).save()
|
|
done()
|
|
|
|
it 'removes a member to the given CourseInstance', utils.wrap (done) ->
|
|
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
|
[res, body] = yield request.delAsync {uri: url, json: {userID: @student.id}}
|
|
expect(res.statusCode).toBe(200)
|
|
expect(res.body.members.length).toBe(0)
|
|
done()
|
|
|
|
it 'removes the CourseInstance from the User.courseInstances', utils.wrap (done) ->
|
|
url = getURL("/db/course_instance/#{@courseInstance.id}/members")
|
|
user = yield User.findById(@student.id)
|
|
expect(_.size(user.get('courseInstances'))).toBe(1)
|
|
[res, body] = yield request.delAsync {uri: url, json: {userID: @student.id}}
|
|
expect(res.statusCode).toBe(200)
|
|
expect(res.body.members.length).toBe(0)
|
|
user = yield User.findById(@student.id)
|
|
expect(_.size(user.get('courseInstances'))).toBe(0)
|
|
done()
|
|
|
|
describe 'GET /db/course_instance/:handle/levels/:levelOriginal/next', ->
|
|
|
|
beforeEach utils.wrap (done) ->
|
|
yield utils.clearModels [User, Classroom, Course, Level, Campaign]
|
|
admin = yield utils.initAdmin()
|
|
yield utils.loginUser(admin)
|
|
teacher = yield utils.initUser({role: 'teacher'})
|
|
|
|
levelJSON = { name: 'A', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
|
|
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
|
|
expect(res.statusCode).toBe(200)
|
|
@levelA = yield Level.findById(res.body._id)
|
|
paredLevelA = _.pick(res.body, 'name', 'original', 'type')
|
|
|
|
@sessionA = new LevelSession
|
|
creator: teacher.id
|
|
level: original: @levelA.get('original').toString()
|
|
permissions: simplePermissions
|
|
state: complete: true
|
|
yield @sessionA.save()
|
|
|
|
levelJSON = { name: 'B', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
|
|
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
|
|
expect(res.statusCode).toBe(200)
|
|
@levelB = yield Level.findById(res.body._id)
|
|
paredLevelB = _.pick(res.body, 'name', 'original', 'type')
|
|
|
|
@sessionB = new LevelSession
|
|
creator: teacher.id
|
|
level: original: @levelB.get('original').toString()
|
|
permissions: simplePermissions
|
|
yield @sessionB.save()
|
|
|
|
levelJSON = { name: 'C', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
|
|
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
|
|
expect(res.statusCode).toBe(200)
|
|
@levelC = yield Level.findById(res.body._id)
|
|
paredLevelC = _.pick(res.body, 'name', 'original', 'type')
|
|
|
|
campaignJSONA = { name: 'Campaign A', levels: {} }
|
|
campaignJSONA.levels[paredLevelA.original] = paredLevelA
|
|
campaignJSONA.levels[paredLevelB.original] = paredLevelB
|
|
[res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSONA})
|
|
@campaignA = yield Campaign.findById(res.body._id)
|
|
|
|
campaignJSONB = { name: 'Campaign B', levels: {} }
|
|
campaignJSONB.levels[paredLevelC.original] = paredLevelC
|
|
[res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSONB})
|
|
@campaignB = yield Campaign.findById(res.body._id)
|
|
|
|
@courseA = Course({name: 'Course A', campaignID: @campaignA._id})
|
|
yield @courseA.save()
|
|
|
|
@courseB = Course({name: 'Course B', campaignID: @campaignB._id})
|
|
yield @courseB.save()
|
|
|
|
yield utils.loginUser(teacher)
|
|
data = { name: 'Classroom 1' }
|
|
classroomsURL = getURL('/db/classroom')
|
|
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
|
|
expect(res.statusCode).toBe(201)
|
|
@classroom = yield Classroom.findById(res.body._id)
|
|
|
|
url = getURL('/db/course_instance')
|
|
|
|
dataA = { name: 'Some Name', courseID: @courseA.id, classroomID: @classroom.id }
|
|
[res, body] = yield request.postAsync {uri: url, json: dataA}
|
|
expect(res.statusCode).toBe(200)
|
|
@courseInstanceA = yield CourseInstance.findById(res.body._id)
|
|
|
|
dataB = { name: 'Some Other Name', courseID: @courseB.id, classroomID: @classroom.id }
|
|
[res, body] = yield request.postAsync {uri: url, json: dataB}
|
|
expect(res.statusCode).toBe(200)
|
|
@courseInstanceB = yield CourseInstance.findById(res.body._id)
|
|
|
|
done()
|
|
|
|
it 'returns the next level for the course in the linked classroom', utils.wrap (done) ->
|
|
[res, body] = yield request.getAsync { uri: utils.getURL("/db/course_instance/#{@courseInstanceA.id}/levels/#{@levelA.id}/sessions/#{@sessionA.id}/next"), json: true }
|
|
expect(res.statusCode).toBe(200)
|
|
expect(res.body.original).toBe(@levelB.original.toString())
|
|
done()
|
|
|
|
it 'returns empty object if the given level is the last level in its course', utils.wrap (done) ->
|
|
[res, body] = yield request.getAsync { uri: utils.getURL("/db/course_instance/#{@courseInstanceA.id}/levels/#{@levelB.id}/sessions/#{@sessionB.id}/next"), json: true }
|
|
expect(res.statusCode).toBe(200)
|
|
expect(res.body).toEqual({})
|
|
done()
|
|
|
|
it 'returns 404 if the given level is not in the course instance\'s course', utils.wrap (done) ->
|
|
[res, body] = yield request.getAsync { uri: utils.getURL("/db/course_instance/#{@courseInstanceB.id}/levels/#{@levelA.id}/sessions/#{@sessionA.id}/next"), json: true }
|
|
expect(res.statusCode).toBe(404)
|
|
done()
|
|
|
|
|
|
describe 'GET /db/course_instance/:handle/classroom', ->
|
|
|
|
beforeEach utils.wrap (done) ->
|
|
yield utils.clearModels [User, CourseInstance, Classroom]
|
|
@owner = yield utils.initUser()
|
|
yield @owner.save()
|
|
@member = yield utils.initUser()
|
|
yield @member.save()
|
|
@classroom = new Classroom({
|
|
ownerID: @owner._id
|
|
members: [@member._id]
|
|
})
|
|
yield @classroom.save()
|
|
@courseInstance = new CourseInstance({classroomID: @classroom._id})
|
|
yield @courseInstance.save()
|
|
@url = getURL("/db/course_instance/#{@courseInstance.id}/classroom")
|
|
done()
|
|
|
|
it 'returns the course instance\'s referenced classroom', utils.wrap (done) ->
|
|
yield utils.loginUser @owner
|
|
[res, body] = yield request.getAsync(@url, {json: true})
|
|
expect(res.statusCode).toBe(200)
|
|
expect(body.code).toBeDefined()
|
|
done()
|
|
|
|
it 'works if you are the owner or member', utils.wrap (done) ->
|
|
yield utils.loginUser @member
|
|
[res, body] = yield request.getAsync(@url, {json: true})
|
|
expect(res.statusCode).toBe(200)
|
|
expect(body.code).toBeUndefined()
|
|
done()
|
|
|
|
it 'does not work if you are not the owner or a member', utils.wrap (done) ->
|
|
@user = yield utils.initUser()
|
|
yield utils.loginUser @user
|
|
[res, body] = yield request.getAsync(@url, {json: true})
|
|
expect(res.statusCode).toBe(403)
|
|
done()
|
|
|
|
describe 'POST /db/course_instance/-/recent', ->
|
|
|
|
url = getURL('/db/course_instance/-/recent')
|
|
|
|
beforeEach utils.wrap (done) ->
|
|
yield utils.clearModels([CourseInstance, Course, User, Classroom, Prepaid, Campaign, Level])
|
|
@teacher = yield utils.initUser({role: 'teacher'})
|
|
@admin = yield utils.initAdmin()
|
|
yield utils.loginUser(@admin)
|
|
@campaign = yield utils.makeCampaign()
|
|
@course = yield utils.makeCourse({free: true}, {campaign: @campaign})
|
|
@student = yield utils.initUser({role: 'student'})
|
|
@prepaid = yield utils.makePrepaid({creator: @teacher.id})
|
|
members = [@student]
|
|
yield utils.loginUser(@teacher)
|
|
@classroom = yield utils.makeClassroom({aceConfig: { language: 'javascript' }}, { members })
|
|
@courseInstance = yield utils.makeCourseInstance({}, { @course, @classroom, members })
|
|
[res, body] = yield request.postAsync({url: getURL("/db/prepaid/#{@prepaid.id}/redeemers"), json: { userID: @student.id} })
|
|
yield utils.loginUser(@admin)
|
|
done()
|
|
|
|
it 'returns all non-HoC course instances and their related users and prepaids', utils.wrap (done) ->
|
|
[res, body] = yield request.postAsync(url, { json: true })
|
|
expect(res.statusCode).toBe(200)
|
|
expect(res.body.courseInstances[0]._id).toBe(@courseInstance.id)
|
|
expect(res.body.students[0]._id).toBe(@student.id)
|
|
expect(res.body.prepaids[0]._id).toBe(@prepaid.id)
|
|
done()
|
|
|
|
it 'returns course instances within a specified range', utils.wrap (done) ->
|
|
startDay = moment().subtract(1, 'day').format('YYYY-MM-DD')
|
|
endDay = moment().add(1, 'day').format('YYYY-MM-DD')
|
|
[res, body] = yield request.postAsync(url, { json: { startDay, endDay } })
|
|
expect(res.body.courseInstances.length).toBe(1)
|
|
|
|
startDay = moment().add(1, 'day').format('YYYY-MM-DD')
|
|
endDay = moment().add(2, 'day').format('YYYY-MM-DD')
|
|
[res, body] = yield request.postAsync(url, { json: { startDay, endDay } })
|
|
expect(res.body.courseInstances.length).toBe(0)
|
|
|
|
startDay = moment().subtract(2, 'day').format('YYYY-MM-DD')
|
|
endDay = moment().subtract(1, 'day').format('YYYY-MM-DD')
|
|
[res, body] = yield request.postAsync(url, { json: { startDay, endDay } })
|
|
expect(res.body.courseInstances.length).toBe(0)
|
|
|
|
done()
|
|
|
|
it 'returns 403 if not an admin', utils.wrap (done) ->
|
|
yield utils.loginUser(@teacher)
|
|
[res, body] = yield request.postAsync(url, { json: true })
|
|
expect(res.statusCode).toBe(403)
|
|
done()
|