Add primer level support to classroom Ux

Exclude levels if classroom.aceConfig.language == level.primerLanguage

Closes #3856
This commit is contained in:
Matt Lott 2016-08-16 15:19:03 -07:00
parent efa4b2b158
commit 84e3ee270a
12 changed files with 429 additions and 231 deletions

View file

@ -47,9 +47,11 @@ module.exports = class Classroom extends CocoModel
getLevelNumber: (levelID, defaultNumber) ->
unless @levelNumberMap
@levelNumberMap = {}
language = @get('aceConfig')?.language
for course in @get('courses') ? []
levels = []
for level in course.levels when level.original
continue if language? and level.primerLanguage is language
levels.push({key: level.original, practice: level.practice ? false})
_.assign(@levelNumberMap, utils.createLevelNumberMap(levels))
@levelNumberMap[levelID] ? defaultNumber
@ -84,6 +86,8 @@ module.exports = class Classroom extends CocoModel
continue
levelObjects.push(course.levels)
levels = new Levels(_.flatten(levelObjects))
language = @get('aceConfig')?.language
levels.remove(levels.filter((level) => level.get('primerLanguage') is language)) if language
if options.withoutLadderLevels
levels.remove(levels.filter((level) -> level.isLadder()))
if options.projectLevels

View file

@ -37,11 +37,19 @@ block content
span(data-i18n="courses.select_language")
| :
select.language-select.form-control
// TODO: Automate this list @scott
option(value="python")
| Python
option(value="javascript")
| JavaScript
//- TODO: Automate this list @scott
//- Web dev courses use HTML and JavaScript, except web-dev-1 which doesn't have scripting
if course.get('slug') === 'web-dev-1'
option(value="javascript")
| HTML
else if course.get('slug').indexOf('web-dev') >= 0
option(value="javascript")
| HTML / JavaScript
else
option(value="python")
| Python
option(value="javascript")
| JavaScript
//- option(value="coffeescript")
//- | CoffeeScript (Experimental)
//- option(value="clojure")
@ -86,11 +94,19 @@ mixin course-info(course)
span.spr ,
if me.isTeacher() || view.ownedClassrooms.size() || me.isAdmin()
p
a.guide-btn.btn.btn-primary(href=("/teachers/course-solution/" + course.id + "/python") data-course-id=course.id data-course-name=course.get('name') data-event-action="Classes Guides Guide Python" class=(me.isTeacher() || me.isAdmin() ? '': 'disabled'))
//- Web dev courses use HTML and JavaScript, except web-dev-1 which doesn't have scripting
if course.get('slug') === 'web-dev-1'
a.guide-btn.btn.btn-primary(href=("/teachers/course-solution/" + course.id + "/javascript") data-course-id=course.id data-course-name=course.get('name') data-event-action="Classes Guides Guide JavaScript" class=(me.isTeacher() || me.isAdmin() ? '': 'disabled'))
span(data-i18n="courses.view_guide_online")
| — Python
p
| — HTML
else if course.get('slug').indexOf('web-dev') >= 0
a.guide-btn.btn.btn-primary(href=("/teachers/course-solution/" + course.id + "/javascript") data-course-id=course.id data-course-name=course.get('name') data-event-action="Classes Guides Guide JavaScript" class=(me.isTeacher() || me.isAdmin() ? '': 'disabled'))
span(data-i18n="courses.view_guide_online")
| — HTML / JavaScript
else
a.guide-btn.btn.btn-primary(href=("/teachers/course-solution/" + course.id + "/javascript") data-course-id=course.id data-course-name=course.get('name') data-event-action="Classes Guides Guide JavaScript" class=(me.isTeacher() || me.isAdmin() ? '': 'disabled'))
span(data-i18n="courses.view_guide_online")
| — JavaScript
a.guide-btn.btn.btn-primary(href=("/teachers/course-solution/" + course.id + "/python") data-course-id=course.id data-course-name=course.get('name') data-event-action="Classes Guides Guide Python" class=(me.isTeacher() || me.isAdmin() ? '': 'disabled'))
span(data-i18n="courses.view_guide_online")
| — Python

View file

@ -47,7 +47,7 @@ module.exports = class CourseDetailsView extends RootView
@supermodel.trackRequest(@classroom.fetch())
levelsLoaded = @supermodel.trackRequest(@levels.fetchForClassroomAndCourse(classroomID, @courseID, {
data: { project: 'concepts,practice,type,slug,name,original,description,shareable,i18n' }
data: { project: 'concepts,practice,primerLanguage,type,slug,name,original,description,shareable,i18n' }
}))
@supermodel.trackRequest($.when(levelsLoaded, sessionsLoaded).then(=>

View file

@ -120,7 +120,7 @@ module.exports = class TeacherClassView extends RootView
@supermodel.trackRequest @courseInstances.fetchForClassroom(classroomID)
@levels = new Levels()
@supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts,practice,shareable,i18n'}})
@supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts,primerLanguage,practice,shareable,i18n'}})
@attachMediatorEvents()
window.tracker?.trackEvent 'Teachers Class Loaded', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
@ -329,8 +329,10 @@ module.exports = class TeacherClassView extends RootView
courseOrder.push(course.id)
csvContent = "data:text/csv;charset=utf-8,Username,Email,Total Playtime,#{courseLabels}Concepts\n"
levelCourseMap = {}
language = @classroom.get('aceConfig')?.language
for trimCourse in @classroom.get('courses')
for trimLevel in trimCourse.levels
continue if language and trimLevel.primerLanguage is language
levelCourseMap[trimLevel.original] = @courses.get(trimCourse._id)
for student in @students.models
concepts = []
@ -448,9 +450,11 @@ module.exports = class TeacherClassView extends RootView
stats.totalPlaytime = if playtime then moment.duration(playtime, "seconds").humanize() else 0
# TODO: Humanize differently ('1 hour' instead of 'an hour')
levelPracticeMap = {}
levelPracticeMap[level.id] = level.get('practice') ? false for level in @levels.models
completeSessions = @classroom.sessions.filter (s) -> s.get('state')?.complete and not levelPracticeMap[s.get('levelID')]
levelIncludeMap = {}
language = @classroom.get('aceConfig')?.language
for level in @levels.models
levelIncludeMap[level.get('original')] = not level.get('practice') and (not language? or level.get('primerLanguage') isnt language)
completeSessions = @classroom.sessions.filter (s) -> s.get('state')?.complete and levelIncludeMap[s.get('level')?.original]
stats.averageLevelsComplete = if @students.size() then (_.size(completeSessions) / @students.size()).toFixed(1) else 'N/A' # '
stats.totalLevelsComplete = _.size(completeSessions)

View file

@ -64,7 +64,7 @@ module.exports = class TeacherCoursesView extends RootView
form = $(e.currentTarget).closest('.play-level-form')
levelSlug = form.find('.level-select').val()
courseID = form.data('course-id')
language = form.find('.language-select').val()
language = form.find('.language-select').val() or 'javascript'
window.tracker?.trackEvent 'Classes Guides Play Level', category: 'Teachers', courseID: courseID, language: language, levelSlug: levelSlug, ['Mixpanel']
url = "/play/level/#{levelSlug}?course=#{courseID}&codeLanguage=#{language}"
firstLevelSlug = @campaigns.get(@courses.at(0).get('campaignID')).getLevels().at(0).get('slug')

View file

@ -31,7 +31,7 @@ module.exports = class TeacherCourseSolutionView extends RootView
onLoaded: ->
for level in @levels?.models
articles = level.get('documentation').specificArticles
articles = level.get('documentation')?.specificArticles
if articles
guide = articles.filter((x) => x.name == "Overview").pop()
level.set 'guide', marked(@hideWrongLanguage(guide.body)) if guide

View file

@ -55,13 +55,18 @@ module.exports =
classroom = yield database.getDocFromHandle(req, Classroom)
if not classroom
throw new errors.NotFound('Classroom not found.')
levelOriginals = []
for course in classroom.get('courses') or []
for level in course.levels
levelOriginals.push(level.original)
levels = yield Level.find({ original: { $in: levelOriginals }, slug: { $exists: true }}).select(parse.getProjectFromReq(req))
query = {$and: [
{original: { $in: levelOriginals }}
{$or: [{primerLanguage: {$exists: false}}, {primerLanguage: { $ne: classroom.get('aceConfig')?.language }}]}
{slug: { $exists: true }}
]}
levels = yield Level.find(query).select(parse.getProjectFromReq(req))
levels = (level.toObject({ req: req }) for level in levels)
# maintain course order
@ -76,7 +81,7 @@ module.exports =
classroom = yield database.getDocFromHandle(req, Classroom)
if not classroom
throw new errors.NotFound('Classroom not found.')
levelOriginals = []
for course in classroom.get('courses') or []
if course._id.toString() isnt req.params.courseID
@ -84,15 +89,20 @@ module.exports =
for level in course.levels
levelOriginals.push(level.original)
levels = yield Level.find({ original: { $in: levelOriginals }, slug: { $exists: true }}).select(parse.getProjectFromReq(req))
query = {$and: [
{original: { $in: levelOriginals }}
{$or: [{primerLanguage: {$exists: false}}, {primerLanguage: { $ne: classroom.get('aceConfig')?.language }}]}
{slug: { $exists: true }}
]}
levels = yield Level.find(query).select(parse.getProjectFromReq(req))
levels = (level.toObject({ req: req }) for level in levels)
# maintain course order
levelMap = {}
for level in levels
levelMap[level.original] = level
levels = (levelMap[levelOriginal.toString()] for levelOriginal in levelOriginals)
levels = (levelMap[levelOriginal.toString()] for levelOriginal in levelOriginals when levelMap[levelOriginal.toString()])
res.status(200).send(levels)
fetchMemberSessions: wrap (req, res, next) ->
@ -143,14 +153,14 @@ module.exports =
database.assignBody(req, classroom)
# Copy over data from how courses are right now
coursesData = yield module.exports.generateCoursesData(req)
coursesData = yield module.exports.generateCoursesData(classroom.get('aceConfig')?.language, req.user?.isAdmin())
classroom.set('courses', coursesData)
# finish
database.validateDoc(classroom)
classroom = yield classroom.save()
res.status(201).send(classroom.toObject({req: req}))
updateCourses: wrap (req, res) ->
throw new errors.Unauthorized() unless req.user and not req.user.isAnonymous()
classroom = yield database.getDocFromHandle(req, Classroom)
@ -159,15 +169,15 @@ module.exports =
unless req.user._id.equals(classroom.get('ownerID'))
throw new errors.Forbidden('Only the owner may update their classroom content')
coursesData = yield module.exports.generateCoursesData(req)
coursesData = yield module.exports.generateCoursesData(classroom.get('aceConfig')?.language, req.user?.isAdmin())
classroom.set('courses', coursesData)
classroom = yield classroom.save()
res.status(200).send(classroom.toObject({req: req}))
generateCoursesData: co.wrap (req) ->
generateCoursesData: co.wrap (classLanguage, isAdmin) ->
# helper function for generating the latest version of courses
query = {}
query = {releasePhase: 'released'} unless req.user?.isAdmin()
query = {releasePhase: 'released'} unless isAdmin
courses = yield Course.find(query)
courses = Course.sortCourses courses
campaigns = yield Campaign.find({_id: {$in: (course.get('campaignID') for course in courses)}})
@ -180,8 +190,9 @@ module.exports =
levels = _.values(campaign.get('levels'))
levels = _.sortBy(levels, 'campaignIndex')
for level in levels
continue if classLanguage and level.primerLanguage is classLanguage
levelData = { original: mongoose.Types.ObjectId(level.original) }
_.extend(levelData, _.pick(level, 'type', 'slug', 'name', 'practice', 'practiceThresholdMinutes', 'shareable'))
_.extend(levelData, _.pick(level, 'type', 'slug', 'name', 'practice', 'practiceThresholdMinutes', 'primerLanguage', 'shareable'))
courseData.levels.push(levelData)
coursesData.push(courseData)
return coursesData

View file

@ -87,6 +87,8 @@ module.exports =
courseID = courseInstance.get('courseID')
courseLevels = []
courseLevels = course.levels for course in classroom.get('courses') or [] when courseID.equals(course._id)
classLanguage = classroom.get('aceConfig')?.language
_.remove(courseLevels, (level) -> level.primerLanguage is classLanguage) if classLanguage
# Get level completions and playtime
currentLevelSession = null

View file

@ -90,8 +90,15 @@ describe 'POST /db/classroom', ->
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSONC})
expect(res.statusCode).toBe(200)
@levelC = yield Level.findById(res.body._id)
levelJSONJSPrimer1 = { name: 'JS Primer 1', permissions: [{access: 'owner', target: admin.id}], type: 'hero', primerLanguage: 'javascript' }
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSONJSPrimer1})
expect(res.statusCode).toBe(200)
@levelJSPrimer1 = yield Level.findById(res.body._id)
campaignJSON = { name: 'Campaign', levels: {} }
paredLevelJSPrimer1 = _.pick(@levelJSPrimer1.toObject(), 'name', 'original', 'type', 'slug', 'primerLanguage')
paredLevelJSPrimer1.campaignIndex = 3
campaignJSON.levels[@levelJSPrimer1.get('original').toString()] = paredLevelJSPrimer1
paredLevelC = _.pick(@levelC.toObject(), 'name', 'original', 'type', 'slug', 'practice')
paredLevelC.campaignIndex = 2
campaignJSON.levels[@levelC.get('original').toString()] = paredLevelC
@ -134,17 +141,42 @@ describe 'POST /db/classroom', ->
expect(res.statusCode).toBe(403)
done()
it 'makes a copy of the list of all levels in all courses', utils.wrap (done) ->
teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(teacher)
data = { name: 'Classroom 2' }
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
classroom = yield Classroom.findById(res.body._id)
expect(classroom.get('courses')[0].levels[0].original.toString()).toBe(@levelA.get('original').toString())
expect(classroom.get('courses')[0].levels[0].type).toBe('course')
expect(classroom.get('courses')[0].levels[0].slug).toBe('level-a')
expect(classroom.get('courses')[0].levels[0].name).toBe('Level A')
done()
describe 'when javascript classroom', ->
beforeEach utils.wrap (done) ->
teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(teacher)
data = { name: 'Classroom 2', aceConfig: { language: 'javascript' } }
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
@classroom = yield Classroom.findById(res.body._id)
done()
it 'makes a copy of the list of all levels in all courses', utils.wrap (done) ->
expect(@classroom.get('courses')[0].levels.length).toEqual(3)
expect(@classroom.get('courses')[0].levels[0].original.toString()).toBe(@levelA.get('original').toString())
expect(@classroom.get('courses')[0].levels[0].type).toBe('course')
expect(@classroom.get('courses')[0].levels[0].slug).toBe('level-a')
expect(@classroom.get('courses')[0].levels[0].name).toBe('Level A')
done()
describe 'when python classroom', ->
beforeEach utils.wrap (done) ->
teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(teacher)
data = { name: 'Classroom 2', aceConfig: { language: 'python' } }
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
@classroom = yield Classroom.findById(res.body._id)
done()
it 'makes a copy all levels in all courses', utils.wrap (done) ->
expect(@classroom.get('courses')[0].levels.length).toEqual(4)
expect(@classroom.get('courses')[0].levels[0].original.toString()).toBe(@levelA.get('original').toString())
expect(@classroom.get('courses')[0].levels[0].type).toBe('course')
expect(@classroom.get('courses')[0].levels[0].slug).toBe('level-a')
expect(@classroom.get('courses')[0].levels[0].name).toBe('Level A')
done()
describe 'when there are unreleased courses', ->
beforeEach utils.wrap (done) ->
@ -203,40 +235,7 @@ describe 'GET /db/classroom/:handle/levels', ->
yield utils.clearModels [User, Classroom, Course, Level, Campaign]
admin = yield utils.initAdmin()
yield utils.loginUser(admin)
levelJSON = { name: 'King\'s Peak 3', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
expect(res.statusCode).toBe(200)
@level = yield Level.findById(res.body._id)
campaignJSON = { name: 'Campaign', levels: {} }
paredLevel = _.pick(res.body, 'name', 'original', 'type')
campaignJSON.levels[res.body.original] = paredLevel
[res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSON})
@campaign = yield Campaign.findById(res.body._id)
@course = Course({name: 'Course', campaignID: @campaign._id, releasePhase: 'released'})
yield @course.save()
teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(teacher)
data = { name: 'Classroom 1' }
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
expect(res.statusCode).toBe(201)
@classroom = yield Classroom.findById(res.body._id)
done()
it 'returns all levels referenced in in the classroom\'s copy of course levels', utils.wrap (done) ->
[res, body] = yield request.getAsync { uri: getURL("/db/classroom/#{@classroom.id}/levels"), json: true }
expect(res.statusCode).toBe(200)
levels = res.body
expect(levels.length).toBe(1)
expect(levels[0].name).toBe("King's Peak 3")
done()
describe 'GET /db/classroom/:handle/levels', ->
beforeEach utils.wrap (done) ->
yield utils.clearModels [User, Classroom, Course, Level, Campaign]
admin = yield utils.initAdmin()
yield utils.loginUser(admin)
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)
@ -248,7 +247,13 @@ describe 'GET /db/classroom/:handle/levels', ->
expect(res.statusCode).toBe(200)
@levelB = yield Level.findById(res.body._id)
paredLevelB = _.pick(res.body, 'name', 'original', 'type')
levelJSON = { name: 'JS Primer 1', permissions: [{access: 'owner', target: admin.id}], type: 'course', primerLanguage: 'javascript' }
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
expect(res.statusCode).toBe(200)
@levelJSPrimer1 = yield Level.findById(res.body._id)
paredLevelJSPrimer1 = _.pick(res.body, 'name', 'original', 'type')
campaignJSONA = { name: 'Campaign A', levels: {} }
campaignJSONA.levels[paredLevelA.original] = paredLevelA
[res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSONA})
@ -256,6 +261,7 @@ describe 'GET /db/classroom/:handle/levels', ->
campaignJSONB = { name: 'Campaign B', levels: {} }
campaignJSONB.levels[paredLevelB.original] = paredLevelB
campaignJSONB.levels[paredLevelJSPrimer1.original] = paredLevelJSPrimer1
[res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSONB})
@campaignB = yield Campaign.findById(res.body._id)
@ -265,34 +271,70 @@ describe 'GET /db/classroom/:handle/levels', ->
@courseB = Course({name: 'Course B', campaignID: @campaignB._id, releasePhase: 'released'})
yield @courseB.save()
teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(teacher)
data = { name: 'Classroom 1' }
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
expect(res.statusCode).toBe(201)
@classroom = yield Classroom.findById(res.body._id)
done()
it 'returns all levels referenced in in the classroom\'s copy of course levels', utils.wrap (done) ->
[res, body] = yield request.getAsync { uri: getURL("/db/classroom/#{@classroom.id}/levels"), json: true }
expect(res.statusCode).toBe(200)
levels = res.body
expect(levels.length).toBe(2)
describe 'when javascript classroom', ->
[res, body] = yield request.getAsync { uri: getURL("/db/classroom/#{@classroom.id}/courses/#{@courseA.id}/levels"), json: true }
expect(res.statusCode).toBe(200)
levels = res.body
expect(levels.length).toBe(1)
expect(levels[0].original).toBe(@levelA.get('original').toString())
beforeEach utils.wrap (done) ->
teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(teacher)
data = { name: 'Classroom 1', aceConfig: { language: 'javascript' } }
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
expect(res.statusCode).toBe(201)
@classroom = yield Classroom.findById(res.body._id)
done()
[res, body] = yield request.getAsync { uri: getURL("/db/classroom/#{@classroom.id}/courses/#{@courseB.id}/levels"), json: true }
expect(res.statusCode).toBe(200)
levels = res.body
expect(levels.length).toBe(1)
expect(levels[0].original).toBe(@levelB.get('original').toString())
done()
it 'returns all levels referenced in in the classroom\'s copy of course levels', utils.wrap (done) ->
[res, body] = yield request.getAsync { uri: getURL("/db/classroom/#{@classroom.id}/levels"), json: true }
expect(res.statusCode).toBe(200)
levels = res.body
expect(levels.length).toBe(2)
[res, body] = yield request.getAsync { uri: getURL("/db/classroom/#{@classroom.id}/courses/#{@courseA.id}/levels"), json: true }
expect(res.statusCode).toBe(200)
levels = res.body
expect(levels.length).toBe(1)
expect(levels[0].original).toBe(@levelA.get('original').toString())
[res, body] = yield request.getAsync { uri: getURL("/db/classroom/#{@classroom.id}/courses/#{@courseB.id}/levels"), json: true }
expect(res.statusCode).toBe(200)
levels = res.body
expect(levels.length).toBe(1)
expect(levels[0].original).toBe(@levelB.get('original').toString())
done()
describe 'when python classroom', ->
beforeEach utils.wrap (done) ->
teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(teacher)
data = { name: 'Classroom 1', aceConfig: { language: 'python' } }
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
expect(res.statusCode).toBe(201)
@classroom = yield Classroom.findById(res.body._id)
done()
it 'returns all levels referenced in in the classroom\'s copy of course levels', utils.wrap (done) ->
[res, body] = yield request.getAsync { uri: getURL("/db/classroom/#{@classroom.id}/levels"), json: true }
expect(res.statusCode).toBe(200)
levels = res.body
expect(levels.length).toBe(3)
[res, body] = yield request.getAsync { uri: getURL("/db/classroom/#{@classroom.id}/courses/#{@courseA.id}/levels"), json: true }
expect(res.statusCode).toBe(200)
levels = res.body
expect(levels.length).toBe(1)
expect(levels[0].original).toBe(@levelA.get('original').toString())
[res, body] = yield request.getAsync { uri: getURL("/db/classroom/#{@classroom.id}/courses/#{@courseB.id}/levels"), json: true }
expect(res.statusCode).toBe(200)
levels = res.body
expect(levels.length).toBe(2)
expect(levels[0].original).toBe(@levelB.get('original').toString())
expect(levels[1].original).toBe(@levelJSPrimer1.get('original').toString())
done()
describe 'PUT /db/classroom', ->

View file

@ -246,7 +246,7 @@ describe 'GET /db/course_instance/:handle/levels/:levelOriginal/next', ->
yield utils.clearModels [User, Classroom, Course, Level, Campaign]
admin = yield utils.initAdmin()
yield utils.loginUser(admin)
teacher = yield utils.initUser({role: 'teacher'})
@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})
@ -255,7 +255,7 @@ describe 'GET /db/course_instance/:handle/levels/:levelOriginal/next', ->
paredLevelA = _.pick(res.body, 'name', 'original', 'type')
@sessionA = new LevelSession
creator: teacher.id
creator: @teacher.id
level: original: @levelA.get('original').toString()
permissions: simplePermissions
state: complete: true
@ -268,7 +268,7 @@ describe 'GET /db/course_instance/:handle/levels/:levelOriginal/next', ->
paredLevelB = _.pick(res.body, 'name', 'original', 'type')
@sessionB = new LevelSession
creator: teacher.id
creator: @teacher.id
level: original: @levelB.get('original').toString()
permissions: simplePermissions
yield @sessionB.save()
@ -279,9 +279,22 @@ describe 'GET /db/course_instance/:handle/levels/:levelOriginal/next', ->
@levelC = yield Level.findById(res.body._id)
paredLevelC = _.pick(res.body, 'name', 'original', 'type')
levelJSON = { name: 'JS Primer 1', permissions: [{access: 'owner', target: admin.id}], type: 'course', primerLanguage: 'javascript' }
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
expect(res.statusCode).toBe(200)
@levelJSPrimer1 = yield Level.findById(res.body._id)
paredLevelJSPrimer1 = _.pick(res.body, 'name', 'original', 'primerLanguage', 'type')
@sessionJSPrimer1 = new LevelSession
creator: @teacher.id
level: original: @levelJSPrimer1.get('original').toString()
permissions: simplePermissions
yield @sessionJSPrimer1.save()
campaignJSONA = { name: 'Campaign A', levels: {} }
campaignJSONA.levels[paredLevelA.original] = paredLevelA
campaignJSONA.levels[paredLevelB.original] = paredLevelB
campaignJSONA.levels[paredLevelJSPrimer1.original] = paredLevelJSPrimer1
[res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSONA})
@campaignA = yield Campaign.findById(res.body._id)
@ -296,44 +309,87 @@ describe 'GET /db/course_instance/:handle/levels/:levelOriginal/next', ->
@courseB = Course({name: 'Course B', campaignID: @campaignB._id, releasePhase: 'released'})
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()
describe 'when javascript classroom', ->
beforeEach utils.wrap (done) ->
yield utils.loginUser(@teacher)
data = { name: 'Classroom 1', aceConfig: { language: 'javascript' } }
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)
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()
url = getURL('/db/course_instance')
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()
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()
it 'returns 404 if the given level is no applicable primer level', utils.wrap (done) ->
[res, body] = yield request.getAsync { uri: utils.getURL("/db/course_instance/#{@courseInstanceA.id}/levels/#{@levelJSPrimer1.id}/sessions/#{@sessionJSPrimer1.id}/next"), json: true }
expect(res.statusCode).toBe(404)
done()
describe 'when python classroom', ->
beforeEach utils.wrap (done) ->
yield utils.loginUser(@teacher)
data = { name: 'Classroom 1', aceConfig: { language: 'python' } }
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/#{@levelB.id}/sessions/#{@sessionB.id}/next"), json: true }
expect(res.statusCode).toBe(200)
expect(res.body.original).toBe(@levelJSPrimer1.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/#{@levelJSPrimer1.id}/sessions/#{@sessionJSPrimer1.id}/next"), json: true }
expect(res.statusCode).toBe(200)
expect(res.body).toEqual({})
done()
describe 'GET /db/course_instance/:handle/classroom', ->

View file

@ -94,7 +94,7 @@ module.exports = {
break if not courseAttrs
course ?= @makeCourse()
levels ?= new Levels()
courseAttrs.levels = (level.pick('_id', 'slug', 'name', 'original', 'type') for level in levels.models)
courseAttrs.levels = (level.pick('_id', 'slug', 'name', 'original', 'primerLanguage', 'type') for level in levels.models)
# populate members
if not attrs.members
@ -111,6 +111,7 @@ module.exports = {
original: level.get('original'),
creator: creator.id,
}, attrs)
attrs.level.primerLanguage = level.get('primerLanguage') if level.get('primerLanguage')
return new LevelSession(attrs)
makeCourseInstance: (attrs, sources={}) ->

View file

@ -41,107 +41,169 @@ describe 'TeacherClassView', ->
factories.makeUser({name: 'Ebner'}, {prepaid: expired})
])
@levels = new Levels(_.times(2, -> factories.makeLevel({ concepts: ['basic_syntax', 'arguments', 'functions'] })))
@classroom = factories.makeClassroom({}, { courses: @releasedCourses, members: @students, levels: [@levels, new Levels()] })
@courseInstances = new CourseInstances([
factories.makeCourseInstance({}, { course: @releasedCourses.first(), @classroom, members: @students })
factories.makeCourseInstance({}, { course: @releasedCourses.last(), @classroom, members: @students })
])
@levels.push(factories.makeLevel({ concepts: ['basic_syntax', 'arguments', 'functions'], primerLanguage: 'javascript' }))
sessions = []
@finishedStudent = @students.first()
@unfinishedStudent = @students.last()
for level in @levels.models
_.defer done
describe 'when python classroom', ->
beforeEach (done) ->
@classroom = factories.makeClassroom({ aceConfig: { language: 'python' }}, { courses: @releasedCourses, members: @students, levels: [@levels, new Levels()] })
@courseInstances = new CourseInstances([
factories.makeCourseInstance({}, { course: @releasedCourses.first(), @classroom, members: @students })
factories.makeCourseInstance({}, { course: @releasedCourses.last(), @classroom, members: @students })
])
sessions = []
@finishedStudent = @students.first()
@unfinishedStudent = @students.last()
for level in @levels.models
sessions.push(factories.makeLevelSession(
{state: {complete: true}, playtime: 60},
{level, creator: @finishedStudent})
)
sessions.push(factories.makeLevelSession(
{state: {complete: true}, playtime: 60},
{level, creator: @finishedStudent})
{level: @levels.first(), creator: @unfinishedStudent})
)
sessions.push(factories.makeLevelSession(
{state: {complete: true}, playtime: 60},
{level: @levels.first(), creator: @unfinishedStudent})
)
@levelSessions = new LevelSessions(sessions)
@view = new TeacherClassView({}, @courseInstances.first().id)
@view.classroom.fakeRequests[0].respondWith({ status: 200, responseText: @classroom.stringify() })
@view.courses.fakeRequests[0].respondWith({ status: 200, responseText: @courses.stringify() })
@view.courseInstances.fakeRequests[0].respondWith({ status: 200, responseText: @courseInstances.stringify() })
@view.students.fakeRequests[0].respondWith({ status: 200, responseText: @students.stringify() })
@view.classroom.sessions.fakeRequests[0].respondWith({ status: 200, responseText: @levelSessions.stringify() })
@view.levels.fakeRequests[0].respondWith({ status: 200, responseText: @levels.stringify() })
jasmine.demoEl(@view.$el)
_.defer done
it 'has contents', ->
expect(@view.$el.children().length).toBeGreaterThan(0)
@levelSessions = new LevelSessions(sessions)
@view = new TeacherClassView({}, @courseInstances.first().id)
@view.classroom.fakeRequests[0].respondWith({ status: 200, responseText: @classroom.stringify() })
@view.courses.fakeRequests[0].respondWith({ status: 200, responseText: @courses.stringify() })
@view.courseInstances.fakeRequests[0].respondWith({ status: 200, responseText: @courseInstances.stringify() })
@view.students.fakeRequests[0].respondWith({ status: 200, responseText: @students.stringify() })
@view.classroom.sessions.fakeRequests[0].respondWith({ status: 200, responseText: @levelSessions.stringify() })
@view.levels.fakeRequests[0].respondWith({ status: 200, responseText: @levels.stringify() })
jasmine.demoEl(@view.$el)
_.defer done
it 'has contents', ->
expect(@view.$el.children().length).toBeGreaterThan(0)
# it "shows the classroom's name and description"
# it "shows the classroom's join code"
# it "shows the classroom's name and description"
# it "shows the classroom's join code"
describe 'the Students tab', ->
describe 'the Students tab', ->
beforeEach (done) ->
@view.state.set('activeTab', '#students-tab')
_.defer(done)
# it 'shows all of the students'
# it 'sorts correctly by Name'
# it 'sorts correctly by Progress'
describe 'bulk-assign controls', ->
it 'shows alert when assigning course 2 to unenrolled students', (done) ->
expect(@view.$('.cant-assign-to-unenrolled').hasClass('visible')).toBe(false)
@view.$('.student-row .checkbox-flat').click()
@view.$('.assign-to-selected-students').click()
_.defer =>
expect(@view.$('.cant-assign-to-unenrolled').hasClass('visible')).toBe(true)
done()
it 'shows alert when assigning but no students are selected', (done) ->
expect(@view.$('.no-students-selected').hasClass('visible')).toBe(false)
@view.$('.assign-to-selected-students').click()
_.defer =>
expect(@view.$('.no-students-selected').hasClass('visible')).toBe(true)
done()
# describe 'the Course Progress tab', ->
# it 'shows the correct Course Overview progress'
#
# describe 'when viewing another course'
# it 'still shows the correct Course Overview progress'
#
describe 'the Enrollment Status tab', ->
beforeEach ->
@view.state.set('activeTab', '#enrollment-status-tab')
describe 'Enroll button', ->
it 'calls enrollStudents with that user when clicked', ->
spyOn(@view, 'enrollStudents')
@view.$('.enroll-student-button:first').click()
expect(@view.enrollStudents).toHaveBeenCalled()
users = @view.enrollStudents.calls.argsFor(0)[0]
expect(users.size()).toBe(1)
expect(users.first().id).toBe(@view.students.first().id)
describe 'Export Student Progress (CSV) button', ->
it 'downloads a CSV file', ->
spyOn(window, 'open').and.callFake (encodedCSV) =>
progressData = decodeURI(encodedCSV)
CSVHeader = 'data:text\/csv;charset=utf-8,'
expect(progressData).toMatch new RegExp('^' + CSVHeader)
lines = progressData.slice(CSVHeader.length).split('\n')
expect(lines.length).toBe(@students.length + 1)
for line in lines
simplerLine = line.replace(/"[^"]+"/g, '""')
# Username,Email,Total Playtime, [CS1-? Playtime], Concepts
expect(simplerLine.match(/[^,]+/g).length).toBe(3 + @releasedCourses.length + 1)
if simplerLine.match new RegExp(@finishedStudent.get('email'))
expect(simplerLine).toMatch /3 minutes,3 minutes,0/
else if simplerLine.match new RegExp(@unfinishedStudent.get('email'))
expect(simplerLine).toMatch /a minute,a minute,0/
else if simplerLine.match /@/
expect(simplerLine).toMatch /0,0,0/
return true
@view.$('.export-student-progress-btn').click()
expect(window.open).toHaveBeenCalled()
describe 'when javascript classroom', ->
beforeEach (done) ->
@view.state.set('activeTab', '#students-tab')
_.defer(done)
@classroom = factories.makeClassroom({ aceConfig: { language: 'javascript' }}, { courses: @releasedCourses, members: @students, levels: [@levels, new Levels()]})
@courseInstances = new CourseInstances([
factories.makeCourseInstance({}, { course: @releasedCourses.first(), @classroom, members: @students })
factories.makeCourseInstance({}, { course: @releasedCourses.last(), @classroom, members: @students })
])
# it 'shows all of the students'
# it 'sorts correctly by Name'
# it 'sorts correctly by Progress'
describe 'bulk-assign controls', ->
it 'shows alert when assigning course 2 to unenrolled students', (done) ->
expect(@view.$('.cant-assign-to-unenrolled').hasClass('visible')).toBe(false)
@view.$('.student-row .checkbox-flat').click()
@view.$('.assign-to-selected-students').click()
_.defer =>
expect(@view.$('.cant-assign-to-unenrolled').hasClass('visible')).toBe(true)
done()
it 'shows alert when assigning but no students are selected', (done) ->
expect(@view.$('.no-students-selected').hasClass('visible')).toBe(false)
@view.$('.assign-to-selected-students').click()
_.defer =>
expect(@view.$('.no-students-selected').hasClass('visible')).toBe(true)
done()
# describe 'the Course Progress tab', ->
# it 'shows the correct Course Overview progress'
#
# describe 'when viewing another course'
# it 'still shows the correct Course Overview progress'
#
describe 'the Enrollment Status tab', ->
beforeEach ->
@view.state.set('activeTab', '#enrollment-status-tab')
describe 'Enroll button', ->
it 'calls enrollStudents with that user when clicked', ->
spyOn(@view, 'enrollStudents')
@view.$('.enroll-student-button:first').click()
expect(@view.enrollStudents).toHaveBeenCalled()
users = @view.enrollStudents.calls.argsFor(0)[0]
expect(users.size()).toBe(1)
expect(users.first().id).toBe(@view.students.first().id)
sessions = []
@finishedStudent = @students.first()
@unfinishedStudent = @students.last()
classLanguage = @classroom.get('aceConfig')?.language
for level in @levels.models
continue if classLanguage and classLanguage is level.get('primerLanguage')
sessions.push(factories.makeLevelSession(
{state: {complete: true}, playtime: 60},
{level, creator: @finishedStudent})
)
sessions.push(factories.makeLevelSession(
{state: {complete: true}, playtime: 60},
{level: @levels.first(), creator: @unfinishedStudent})
)
@levelSessions = new LevelSessions(sessions)
describe 'Export Student Progress (CSV) button', ->
it 'downloads a CSV file', ->
spyOn(window, 'open').and.callFake (encodedCSV) =>
progressData = decodeURI(encodedCSV)
CSVHeader = 'data:text\/csv;charset=utf-8,'
expect(progressData).toMatch new RegExp('^' + CSVHeader)
lines = progressData.slice(CSVHeader.length).split('\n')
expect(lines.length).toBe(@students.length + 1)
for line in lines
simplerLine = line.replace(/"[^"]+"/g, '""')
# Username,Email,Total Playtime, [CS1-? Playtime], Concepts
expect(simplerLine.match(/[^,]+/g).length).toBe(3 + @releasedCourses.length + 1)
if simplerLine.match new RegExp(@finishedStudent.get('email'))
expect(simplerLine).toMatch /2 minutes,2 minutes,0/
else if simplerLine.match new RegExp(@unfinishedStudent.get('email'))
expect(simplerLine).toMatch /a minute,a minute,0/
else if simplerLine.match /@/
expect(simplerLine).toMatch /0,0,0/
return true
@view.$('.export-student-progress-btn').click()
expect(window.open).toHaveBeenCalled()
@view = new TeacherClassView({}, @courseInstances.first().id)
@view.classroom.fakeRequests[0].respondWith({ status: 200, responseText: @classroom.stringify() })
@view.courses.fakeRequests[0].respondWith({ status: 200, responseText: @courses.stringify() })
@view.courseInstances.fakeRequests[0].respondWith({ status: 200, responseText: @courseInstances.stringify() })
@view.students.fakeRequests[0].respondWith({ status: 200, responseText: @students.stringify() })
@view.classroom.sessions.fakeRequests[0].respondWith({ status: 200, responseText: @levelSessions.stringify() })
@view.levels.fakeRequests[0].respondWith({ status: 200, responseText: @levels.stringify() })
jasmine.demoEl(@view.$el)
_.defer done
describe 'Export Student Progress (CSV) button', ->
it 'downloads a CSV file', ->
spyOn(window, 'open').and.callFake (encodedCSV) =>
progressData = decodeURI(encodedCSV)
CSVHeader = 'data:text\/csv;charset=utf-8,'
expect(progressData).toMatch new RegExp('^' + CSVHeader)
lines = progressData.slice(CSVHeader.length).split('\n')
expect(lines.length).toBe(@students.length + 1)
for line in lines
simplerLine = line.replace(/"[^"]+"/g, '""')
# Username,Email,Total Playtime, [CS1-? Playtime], Concepts
expect(simplerLine.match(/[^,]+/g).length).toBe(3 + @releasedCourses.length + 1)
if simplerLine.match new RegExp(@finishedStudent.get('email'))
expect(simplerLine).toMatch /2 minutes,2 minutes,0/
else if simplerLine.match new RegExp(@unfinishedStudent.get('email'))
expect(simplerLine).toMatch /a minute,a minute,0/
else if simplerLine.match /@/
expect(simplerLine).toMatch /0,0,0/
return true
@view.$('.export-student-progress-btn').click()
expect(window.open).toHaveBeenCalled()