mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-13 09:11:22 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
152e468865
26 changed files with 508 additions and 151 deletions
|
@ -325,3 +325,48 @@ module.exports.capitalLanguages = capitalLanguages =
|
||||||
'python': 'Python'
|
'python': 'Python'
|
||||||
'java': 'Java'
|
'java': 'Java'
|
||||||
'lua': 'Lua'
|
'lua': 'Lua'
|
||||||
|
|
||||||
|
module.exports.createLevelNumberMap = (levels) ->
|
||||||
|
levelNumberMap = {}
|
||||||
|
practiceLevelTotalCount = 0
|
||||||
|
practiceLevelCurrentCount = 0
|
||||||
|
for level, i in levels
|
||||||
|
levelNumber = i - practiceLevelTotalCount + 1
|
||||||
|
if level.practice
|
||||||
|
levelNumber = i - practiceLevelTotalCount + String.fromCharCode('a'.charCodeAt(0) + practiceLevelCurrentCount)
|
||||||
|
practiceLevelTotalCount++
|
||||||
|
practiceLevelCurrentCount++
|
||||||
|
else
|
||||||
|
practiceLevelCurrentCount = 0
|
||||||
|
levelNumberMap[level.key] = levelNumber
|
||||||
|
levelNumberMap
|
||||||
|
|
||||||
|
module.exports.findNextLevel = (levels, currentIndex, needsPractice) ->
|
||||||
|
# levels = [{practice: true/false, complete: true/false}]
|
||||||
|
index = currentIndex
|
||||||
|
index++
|
||||||
|
if needsPractice
|
||||||
|
if levels[currentIndex].practice or index < levels.length and levels[index].practice
|
||||||
|
# Needs practice, on practice or next practice, choose next incomplete level
|
||||||
|
# May leave earlier practice levels incomplete and reach end of course
|
||||||
|
index++ while index < levels.length and levels[index].complete
|
||||||
|
else
|
||||||
|
# Needs practice, on required, next required, choose first incomplete level of previous practice chain
|
||||||
|
index--
|
||||||
|
index-- while index >= 0 and not levels[index].practice
|
||||||
|
if index >= 0
|
||||||
|
index-- while index >= 0 and levels[index].practice
|
||||||
|
if index >= 0
|
||||||
|
index++
|
||||||
|
index++ while index < levels.length and levels[index].practice and levels[index].complete
|
||||||
|
if levels[index].practice and not levels[index].complete
|
||||||
|
return index
|
||||||
|
index = currentIndex + 1
|
||||||
|
index++ while index < levels.length and levels[index].complete
|
||||||
|
else
|
||||||
|
# No practice needed, next required incomplete level
|
||||||
|
index++ while index < levels.length and (levels[index].practice or levels[index].complete)
|
||||||
|
index
|
||||||
|
|
||||||
|
module.exports.needsPractice = (playtime=0, threshold=2) ->
|
||||||
|
playtime / 60 > threshold
|
||||||
|
|
|
@ -295,6 +295,7 @@
|
||||||
saving: "Saving..."
|
saving: "Saving..."
|
||||||
sending: "Sending..."
|
sending: "Sending..."
|
||||||
send: "Send"
|
send: "Send"
|
||||||
|
type: "Type"
|
||||||
cancel: "Cancel"
|
cancel: "Cancel"
|
||||||
save: "Save"
|
save: "Save"
|
||||||
publish: "Publish"
|
publish: "Publish"
|
||||||
|
|
|
@ -3,6 +3,7 @@ schema = require 'schemas/models/campaign.schema'
|
||||||
Level = require 'models/Level'
|
Level = require 'models/Level'
|
||||||
Levels = require 'collections/Levels'
|
Levels = require 'collections/Levels'
|
||||||
CocoCollection = require 'collections/CocoCollection'
|
CocoCollection = require 'collections/CocoCollection'
|
||||||
|
utils = require '../core/utils'
|
||||||
|
|
||||||
module.exports = class Campaign extends CocoModel
|
module.exports = class Campaign extends CocoModel
|
||||||
@className: 'Campaign'
|
@className: 'Campaign'
|
||||||
|
@ -23,3 +24,11 @@ module.exports = class Campaign extends CocoModel
|
||||||
levels.comparator = 'campaignIndex'
|
levels.comparator = 'campaignIndex'
|
||||||
levels.sort()
|
levels.sort()
|
||||||
return levels
|
return levels
|
||||||
|
|
||||||
|
getLevelNumber: (levelID, defaultNumber) ->
|
||||||
|
unless @levelNumberMap
|
||||||
|
levels = []
|
||||||
|
for level in @getLevels().models when level.get('original')
|
||||||
|
levels.push({key: level.get('original'), practice: level.get('practice') ? false})
|
||||||
|
@levelNumberMap = utils.createLevelNumberMap(levels)
|
||||||
|
@levelNumberMap[levelID] ? defaultNumber
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
CocoModel = require './CocoModel'
|
CocoModel = require './CocoModel'
|
||||||
schema = require 'schemas/models/classroom.schema'
|
schema = require 'schemas/models/classroom.schema'
|
||||||
utils = require 'core/utils'
|
utils = require '../core/utils'
|
||||||
User = require 'models/User'
|
User = require 'models/User'
|
||||||
|
|
||||||
module.exports = class Classroom extends CocoModel
|
module.exports = class Classroom extends CocoModel
|
||||||
|
@ -44,6 +44,16 @@ module.exports = class Classroom extends CocoModel
|
||||||
_.extend options, opts
|
_.extend options, opts
|
||||||
@fetch(options)
|
@fetch(options)
|
||||||
|
|
||||||
|
getLevelNumber: (levelID, defaultNumber) ->
|
||||||
|
unless @levelNumberMap
|
||||||
|
@levelNumberMap = {}
|
||||||
|
for course in @get('courses') ? []
|
||||||
|
levels = []
|
||||||
|
for level in course.levels when level.original
|
||||||
|
levels.push({key: level.original, practice: level.practice ? false})
|
||||||
|
_.assign(@levelNumberMap, utils.createLevelNumberMap(levels))
|
||||||
|
@levelNumberMap[levelID] ? defaultNumber
|
||||||
|
|
||||||
removeMember: (userID, opts) ->
|
removeMember: (userID, opts) ->
|
||||||
options = {
|
options = {
|
||||||
url: _.result(@, 'url') + '/members'
|
url: _.result(@, 'url') + '/members'
|
||||||
|
@ -88,30 +98,59 @@ module.exports = class Classroom extends CocoModel
|
||||||
|
|
||||||
statsForSessions: (sessions, courseID) ->
|
statsForSessions: (sessions, courseID) ->
|
||||||
return null unless sessions
|
return null unless sessions
|
||||||
stats = {}
|
|
||||||
sessions = sessions.models or sessions
|
sessions = sessions.models or sessions
|
||||||
arena = @getLadderLevel(courseID)
|
arena = @getLadderLevel(courseID)
|
||||||
levels = @getLevels({courseID: courseID, withoutLadderLevels: true})
|
courseLevels = @getLevels({courseID: courseID, withoutLadderLevels: true})
|
||||||
levelOriginals = levels.pluck('original')
|
levelSessionMap = {}
|
||||||
completeSessionOriginals = (session.get('level').original for session in sessions when session.get('state').complete)
|
levelSessionMap[session.get('level').original] = session for session in sessions
|
||||||
incompleteSessionOriginals = (session.get('level').original for session in sessions when not session.get('state').complete)
|
currentIndex = -1
|
||||||
levelsLeft = _.size(_.difference(levelOriginals, completeSessionOriginals))
|
lastStarted = null
|
||||||
next = _.find levels.models, (level) -> level.get('original') not in completeSessionOriginals
|
levelsTotal = 0
|
||||||
lastPlayed = _.find levels.models, (level) -> level.get('original') in incompleteSessionOriginals
|
levelsLeft = 0
|
||||||
stats.levels = {
|
lastPlayed = null
|
||||||
size: levels.size()
|
playtime = 0
|
||||||
left: levelsLeft
|
levels = []
|
||||||
done: levelsLeft is 0
|
for level, index in courseLevels.models
|
||||||
numDone: levels.size() - levelsLeft
|
levelsTotal++ unless level.get('practice')
|
||||||
pctDone: (100 * (levels.size() - levelsLeft) / levels.size()).toFixed(1) + '%'
|
complete = false
|
||||||
lastPlayed: lastPlayed
|
if session = levelSessionMap[level.get('original')]
|
||||||
next: next
|
complete = session.get('state').complete ? false
|
||||||
first: levels.first()
|
playtime += session.get('playtime') ? 0
|
||||||
arena: arena
|
lastPlayed = level
|
||||||
}
|
if complete
|
||||||
sum = (nums) -> _.reduce(nums, (s, num) -> s + num) or 0
|
currentIndex = index
|
||||||
stats.playtime = sum((session.get('playtime') or 0 for session in sessions))
|
else
|
||||||
return stats
|
lastStarted = level
|
||||||
|
levelsLeft++ unless level.get('practice')
|
||||||
|
else if not level.get('practice')
|
||||||
|
levelsLeft++
|
||||||
|
levels.push
|
||||||
|
practice: level.get('practice') ? false
|
||||||
|
complete: complete
|
||||||
|
lastPlayed = lastStarted ? lastPlayed
|
||||||
|
needsPractice = false
|
||||||
|
nextIndex = 0
|
||||||
|
if currentIndex >= 0
|
||||||
|
currentLevel = courseLevels.models[currentIndex]
|
||||||
|
currentPlaytime = levelSessionMap[currentLevel.get('original')]?.get('playtime') ? 0
|
||||||
|
needsPractice = utils.needsPractice(currentPlaytime, currentLevel.get('practiceThresholdMinutes'))
|
||||||
|
nextIndex = utils.findNextLevel(levels, currentIndex, needsPractice)
|
||||||
|
nextLevel = courseLevels.models[nextIndex]
|
||||||
|
nextLevel ?= _.find courseLevels.models, (level) -> not levelSessionMap[level.get('original')]?.get('state')?.complete
|
||||||
|
|
||||||
|
stats =
|
||||||
|
levels:
|
||||||
|
size: levelsTotal
|
||||||
|
left: levelsLeft
|
||||||
|
done: levelsLeft is 0
|
||||||
|
numDone: levelsTotal - levelsLeft
|
||||||
|
pctDone: (100 * (levelsTotal - levelsLeft) / levelsTotal).toFixed(1) + '%'
|
||||||
|
lastPlayed: lastPlayed
|
||||||
|
next: nextLevel
|
||||||
|
first: courseLevels.first()
|
||||||
|
arena: arena
|
||||||
|
playtime: playtime
|
||||||
|
stats
|
||||||
|
|
||||||
fetchForCourseInstance: (courseInstanceID, options={}) ->
|
fetchForCourseInstance: (courseInstanceID, options={}) ->
|
||||||
return unless courseInstanceID
|
return unless courseInstanceID
|
||||||
|
|
|
@ -262,9 +262,9 @@ module.exports = class Level extends CocoModel
|
||||||
isLadder: ->
|
isLadder: ->
|
||||||
return @get('type')?.indexOf('ladder') > -1
|
return @get('type')?.indexOf('ladder') > -1
|
||||||
|
|
||||||
fetchNextForCourse: ({ levelOriginalID, courseInstanceID, courseID }, options={}) ->
|
fetchNextForCourse: ({ levelOriginalID, courseInstanceID, courseID, sessionID }, options={}) ->
|
||||||
if courseInstanceID
|
if courseInstanceID
|
||||||
options.url = "/db/course_instance/#{courseInstanceID}/levels/#{levelOriginalID}/next"
|
options.url = "/db/course_instance/#{courseInstanceID}/levels/#{levelOriginalID}/sessions/#{sessionID}/next"
|
||||||
else
|
else
|
||||||
options.url = "/db/course/#{courseID}/levels/#{levelOriginalID}/next"
|
options.url = "/db/course/#{courseID}/levels/#{levelOriginalID}/next"
|
||||||
@fetch(options)
|
@fetch(options)
|
||||||
|
|
|
@ -66,6 +66,7 @@ _.extend CampaignSchema.properties, {
|
||||||
original: { type: 'string', format: 'hidden' }
|
original: { type: 'string', format: 'hidden' }
|
||||||
adventurer: { type: 'boolean' }
|
adventurer: { type: 'boolean' }
|
||||||
practice: { type: 'boolean' }
|
practice: { type: 'boolean' }
|
||||||
|
practiceThresholdMinutes: {type: 'number'}
|
||||||
adminOnly: { type: 'boolean' }
|
adminOnly: { type: 'boolean' }
|
||||||
disableSpaces: { type: ['boolean','number'] }
|
disableSpaces: { type: ['boolean','number'] }
|
||||||
hidesSubmitUntilRun: { type: 'boolean' }
|
hidesSubmitUntilRun: { type: 'boolean' }
|
||||||
|
|
|
@ -23,6 +23,8 @@ _.extend ClassroomSchema.properties,
|
||||||
courses: c.array { title: 'Courses' }, c.object { title: 'Course' }, {
|
courses: c.array { title: 'Courses' }, c.object { title: 'Course' }, {
|
||||||
_id: c.objectId()
|
_id: c.objectId()
|
||||||
levels: c.array { title: 'Levels' }, c.object { title: 'Level' }, {
|
levels: c.array { title: 'Levels' }, c.object { title: 'Level' }, {
|
||||||
|
practice: {type: 'boolean'}
|
||||||
|
practiceThresholdMinutes: {type: 'number'}
|
||||||
type: c.shortString()
|
type: c.shortString()
|
||||||
original: c.objectId()
|
original: c.objectId()
|
||||||
name: {type: 'string'}
|
name: {type: 'string'}
|
||||||
|
|
|
@ -32,10 +32,6 @@ block content
|
||||||
td(data-i18n="courses.total_students")
|
td(data-i18n="courses.total_students")
|
||||||
td
|
td
|
||||||
span.spr= _.size(view.classroom.get('members'))
|
span.spr= _.size(view.classroom.get('members'))
|
||||||
span (
|
|
||||||
span.spr(data-i18n="courses.enrolled_courses")
|
|
||||||
span= stats.enrolledUsers
|
|
||||||
span )
|
|
||||||
tr
|
tr
|
||||||
td(data-i18n="courses.average_time")
|
td(data-i18n="courses.average_time")
|
||||||
td= stats.averagePlaytime
|
td= stats.averagePlaytime
|
||||||
|
@ -108,16 +104,17 @@ block content
|
||||||
.progress
|
.progress
|
||||||
each trimModel in levels.models
|
each trimModel in levels.models
|
||||||
- var level = view.levels.get(trimModel.get('original')); // get the level loaded through the db
|
- var level = view.levels.get(trimModel.get('original')); // get the level loaded through the db
|
||||||
|
- var levelNumber = view.classroom.getLevelNumber(level.get('original'), i + 1)
|
||||||
- i++
|
- i++
|
||||||
- var session = sessionMap[level.get('original')];
|
- var session = sessionMap[level.get('original')];
|
||||||
a(href=view.getLevelURL(level, course, courseInstance, session))
|
a(href=view.getLevelURL(level, course, courseInstance, session))
|
||||||
- var content = view.levelPopoverContent(level, session, i);
|
- var content = view.levelPopoverContent(level, session, levelNumber);
|
||||||
if session && session.get('state') && session.get('state').complete
|
if session && session.get('state') && session.get('state').complete
|
||||||
.progress-bar.progress-bar-complete(style=css, data-content=content, data-toggle='popover')= i
|
.progress-bar.progress-bar-complete(style=css, data-content=content, data-toggle='popover')= levelNumber
|
||||||
else if session
|
else if session
|
||||||
.progress-bar.progress-bar-started(style=css, data-content=content, data-toggle='popover')= i
|
.progress-bar.progress-bar-started(style=css, data-content=content, data-toggle='popover')= levelNumber
|
||||||
else
|
else
|
||||||
.progress-bar.progress-bar-default(style=css, data-content=content, data-toggle='popover')= i
|
.progress-bar.progress-bar-default(style=css, data-content=content, data-toggle='popover')= levelNumber
|
||||||
else if paidFor
|
else if paidFor
|
||||||
.text-center
|
.text-center
|
||||||
button.enable-btn.btn.btn-info.btn-sm.text-uppercase(data-user-id=user.id, data-course-instance-cid=courseInstance.cid)
|
button.enable-btn.btn.btn-info.btn-sm.text-uppercase(data-user-id=user.id, data-course-instance-cid=courseInstance.cid)
|
||||||
|
|
|
@ -88,6 +88,7 @@ block content
|
||||||
tr
|
tr
|
||||||
th
|
th
|
||||||
th(data-i18n="clans.status")
|
th(data-i18n="clans.status")
|
||||||
|
th(data-i18n="common.type")
|
||||||
th(data-i18n="resources.level")
|
th(data-i18n="resources.level")
|
||||||
th(data-i18n="courses.concepts")
|
th(data-i18n="courses.concepts")
|
||||||
tbody
|
tbody
|
||||||
|
@ -97,6 +98,7 @@ block content
|
||||||
- var levelCount = 0;
|
- var levelCount = 0;
|
||||||
each level in view.levels.models
|
each level in view.levels.models
|
||||||
- var levelStatus = null;
|
- var levelStatus = null;
|
||||||
|
- var levelNumber = view.classroom.getLevelNumber(level.get('original'), ++levelCount);
|
||||||
if view.userLevelStateMap[me.id]
|
if view.userLevelStateMap[me.id]
|
||||||
- levelStatus = view.userLevelStateMap[me.id][level.get('original')]
|
- levelStatus = view.userLevelStateMap[me.id][level.get('original')]
|
||||||
tr
|
tr
|
||||||
|
@ -107,10 +109,8 @@ block content
|
||||||
td
|
td
|
||||||
if view.userLevelStateMap[me.id]
|
if view.userLevelStateMap[me.id]
|
||||||
div= view.userLevelStateMap[me.id][level.get('original')]
|
div= view.userLevelStateMap[me.id][level.get('original')]
|
||||||
- previousLevelCompleted = view.userLevelStateMap[me.id][level.get('original')] === 'complete'
|
td #{level.get('practice') ? 'practice' : 'required'}
|
||||||
else
|
td #{levelNumber}. #{level.get('name').replace('Course: ', '')}
|
||||||
- previousLevelCompleted = false
|
|
||||||
td= ++levelCount + '. ' + level.get('name').replace('Course: ', '')
|
|
||||||
td
|
td
|
||||||
if view.levelConceptMap[level.get('original')]
|
if view.levelConceptMap[level.get('original')]
|
||||||
each concept in view.course.get('concepts')
|
each concept in view.course.get('concepts')
|
||||||
|
@ -118,3 +118,8 @@ block content
|
||||||
span.spr.concept(data-i18n="concepts." + concept)
|
span.spr.concept(data-i18n="concepts." + concept)
|
||||||
if level.get('original') === lastLevelCompleted
|
if level.get('original') === lastLevelCompleted
|
||||||
- passedLastCompletedLevel = true
|
- passedLastCompletedLevel = true
|
||||||
|
if !level.get('practice')
|
||||||
|
if view.userLevelStateMap[me.id]
|
||||||
|
- previousLevelCompleted = view.userLevelStateMap[me.id][level.get('original')] === 'complete'
|
||||||
|
else
|
||||||
|
- previousLevelCompleted = false
|
||||||
|
|
|
@ -309,7 +309,8 @@ mixin courseOverview
|
||||||
.course-overview-progress
|
.course-overview-progress
|
||||||
each level, index in levels
|
each level, index in levels
|
||||||
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level })
|
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level })
|
||||||
+allStudentsLevelProgressDot(progress, level, index+1)
|
- var levelNumber = view.classroom.getLevelNumber(level.get('original'), index + 1)
|
||||||
|
+allStudentsLevelProgressDot(progress, level, levelNumber)
|
||||||
|
|
||||||
mixin studentLevelsRow(student)
|
mixin studentLevelsRow(student)
|
||||||
.student-levels-row.alternating-background
|
.student-levels-row.alternating-background
|
||||||
|
@ -321,7 +322,8 @@ mixin studentLevelsRow(student)
|
||||||
- var levels = view.classroom.getLevels({courseID: course.id}).models
|
- var levels = view.classroom.getLevels({courseID: course.id}).models
|
||||||
each level, index in levels
|
each level, index in levels
|
||||||
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level, user: student })
|
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level, user: student })
|
||||||
+studentLevelProgressDot(progress, level, index+1, session)
|
- var levelNumber = view.classroom.getLevelNumber(level.get('original'), index + 1)
|
||||||
|
+studentLevelProgressDot(progress, level, levelNumber, session)
|
||||||
|
|
||||||
mixin studentCourseProgressDot(progress, levelsTotal, level, label)
|
mixin studentCourseProgressDot(progress, levelsTotal, level, label)
|
||||||
//- TODO: Refactor with TeacherClassesView jade
|
//- TODO: Refactor with TeacherClassesView jade
|
||||||
|
|
|
@ -56,12 +56,12 @@ block content
|
||||||
| :
|
| :
|
||||||
select.level-select.form-control
|
select.level-select.form-control
|
||||||
if view.campaigns.loaded
|
if view.campaigns.loaded
|
||||||
each level, levelIndex in view.campaigns.get(course.get('campaignID')).getLevels().models
|
- var campaign = view.campaigns.get(course.get('campaignID'))
|
||||||
if level.get('practice')
|
each level, levelIndex in campaign.getLevels().models
|
||||||
- continue;
|
- var levelNumber = campaign.getLevelNumber(level.get('original'), levelIndex + 1)
|
||||||
option(value=level.get('slug'))
|
option(value=level.get('slug'))
|
||||||
span
|
span
|
||||||
= levelIndex + 1
|
= levelNumber
|
||||||
span.spr
|
span.spr
|
||||||
| .
|
| .
|
||||||
span
|
span
|
||||||
|
|
|
@ -22,7 +22,7 @@ else
|
||||||
.level-name-area
|
.level-name-area
|
||||||
.level-label(data-i18n="play_level.level")
|
.level-label(data-i18n="play_level.level")
|
||||||
.level-name(title=difficultyTitle || "")
|
.level-name(title=difficultyTitle || "")
|
||||||
span= (campaignIndex ? campaignIndex + '. ' : '') + worldName.replace('Course: ', '')
|
span #{view.levelNumber ? view.levelNumber + '. ' : ''}#{worldName.replace('Course: ', '')}
|
||||||
if levelDifficulty
|
if levelDifficulty
|
||||||
sup.level-difficulty= levelDifficulty
|
sup.level-difficulty= levelDifficulty
|
||||||
|
|
||||||
|
|
|
@ -17,5 +17,9 @@ if started
|
||||||
//- .small-details
|
//- .small-details
|
||||||
//- i(data-i18n='teacher.click_to_view_progress')
|
//- i(data-i18n='teacher.click_to_view_progress')
|
||||||
else
|
else
|
||||||
|
.small-details.nowrap
|
||||||
|
span= levelNumber
|
||||||
|
span.spr .
|
||||||
|
span= levelName
|
||||||
span.small-details.nowrap(data-i18n='teacher.no_progress')
|
span.small-details.nowrap(data-i18n='teacher.no_progress')
|
||||||
| No progress
|
| No progress
|
||||||
|
|
|
@ -13,7 +13,7 @@ if completed
|
||||||
.small-details.nowrap
|
.small-details.nowrap
|
||||||
span.spr(data-i18n='teacher.completed')
|
span.spr(data-i18n='teacher.completed')
|
||||||
| Completed
|
| Completed
|
||||||
span= new Date(dateFirstCompleted).toLocaleString()
|
span= new Date(session.get('dateFirstCompleted')).toLocaleString()
|
||||||
+timePlayed
|
+timePlayed
|
||||||
//- .small-details
|
//- .small-details
|
||||||
//- i(data-i18n='teacher.click_to_view_solution')
|
//- i(data-i18n='teacher.click_to_view_solution')
|
||||||
|
@ -26,11 +26,15 @@ else if started
|
||||||
.small-details.nowrap
|
.small-details.nowrap
|
||||||
span.spr(data-i18n='teacher.last_played')
|
span.spr(data-i18n='teacher.last_played')
|
||||||
| Last played
|
| Last played
|
||||||
span= new Date(lastPlayed).toLocaleString()
|
span= new Date(session.get('changed')).toLocaleString()
|
||||||
+timePlayed
|
+timePlayed
|
||||||
//- .small-details
|
//- .small-details
|
||||||
//- i(data-i18n='teacher.click_to_view_progress')
|
//- i(data-i18n='teacher.click_to_view_progress')
|
||||||
//- | click to view progress
|
//- | click to view progress
|
||||||
else
|
else
|
||||||
|
.small-details.nowrap
|
||||||
|
span= levelNumber
|
||||||
|
span.spr .
|
||||||
|
span= levelName
|
||||||
span.small-details.nowrap(data-i18n='teacher.no_progress')
|
span.small-details.nowrap(data-i18n='teacher.no_progress')
|
||||||
| No progress
|
| No progress
|
||||||
|
|
|
@ -55,7 +55,7 @@ module.exports = class ClassroomView extends RootView
|
||||||
@ownedClassrooms.fetchMine({data: {project: '_id'}})
|
@ownedClassrooms.fetchMine({data: {project: '_id'}})
|
||||||
@supermodel.trackCollection(@ownedClassrooms)
|
@supermodel.trackCollection(@ownedClassrooms)
|
||||||
@levels = new Levels()
|
@levels = new Levels()
|
||||||
@levels.fetchForClassroom(classroomID, {data: {project: 'name,slug,original'}})
|
@levels.fetchForClassroom(classroomID, {data: {project: 'name,original,practice,slug'}})
|
||||||
@levels.on 'add', (model) -> @_byId[model.get('original')] = model # so you can 'get' them
|
@levels.on 'add', (model) -> @_byId[model.get('original')] = model # so you can 'get' them
|
||||||
@supermodel.trackCollection(@levels)
|
@supermodel.trackCollection(@levels)
|
||||||
window.tracker?.trackEvent 'Students Class Loaded', category: 'Students', classroomID: classroomID, ['Mixpanel']
|
window.tracker?.trackEvent 'Students Class Loaded', category: 'Students', classroomID: classroomID, ['Mixpanel']
|
||||||
|
@ -177,7 +177,9 @@ module.exports = class ClassroomView extends RootView
|
||||||
stats.averagePlaytime = if playtime and total then moment.duration(playtime / total, "seconds").humanize() else 0
|
stats.averagePlaytime = if playtime and total then moment.duration(playtime / total, "seconds").humanize() else 0
|
||||||
stats.totalPlaytime = if playtime then moment.duration(playtime, "seconds").humanize() else 0
|
stats.totalPlaytime = if playtime then moment.duration(playtime, "seconds").humanize() else 0
|
||||||
|
|
||||||
completeSessions = @sessions.filter (s) -> s.get('state')?.complete
|
levelPracticeMap = {}
|
||||||
|
levelPracticeMap[level.id] = level.get('practice') ? false for level in @levels.models
|
||||||
|
completeSessions = @sessions.filter (s) -> s.get('state')?.complete and not levelPracticeMap[s.get('levelID')]
|
||||||
stats.averageLevelsComplete = if @users.size() then (_.size(completeSessions) / @users.size()).toFixed(1) else 'N/A' # '
|
stats.averageLevelsComplete = if @users.size() then (_.size(completeSessions) / @users.size()).toFixed(1) else 'N/A' # '
|
||||||
stats.totalLevelsComplete = _.size(completeSessions)
|
stats.totalLevelsComplete = _.size(completeSessions)
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ module.exports = class CourseDetailsView extends RootView
|
||||||
@supermodel.trackRequest(@classroom.fetch())
|
@supermodel.trackRequest(@classroom.fetch())
|
||||||
|
|
||||||
levelsLoaded = @supermodel.trackRequest(@levels.fetchForClassroomAndCourse(classroomID, @courseID, {
|
levelsLoaded = @supermodel.trackRequest(@levels.fetchForClassroomAndCourse(classroomID, @courseID, {
|
||||||
data: { project: 'concepts,type,slug,name,original,description' }
|
data: { project: 'concepts,practice,type,slug,name,original,description' }
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@supermodel.trackRequest($.when(levelsLoaded, sessionsLoaded).then(=>
|
@supermodel.trackRequest($.when(levelsLoaded, sessionsLoaded).then(=>
|
||||||
|
|
|
@ -118,7 +118,7 @@ module.exports = class TeacherClassView extends RootView
|
||||||
@supermodel.trackRequest @courseInstances.fetchForClassroom(classroomID)
|
@supermodel.trackRequest @courseInstances.fetchForClassroom(classroomID)
|
||||||
|
|
||||||
@levels = new Levels()
|
@levels = new Levels()
|
||||||
@supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts'}})
|
@supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts,practice'}})
|
||||||
|
|
||||||
@attachMediatorEvents()
|
@attachMediatorEvents()
|
||||||
window.tracker?.trackEvent 'Teachers Class Loaded', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
|
window.tracker?.trackEvent 'Teachers Class Loaded', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
|
||||||
|
@ -449,7 +449,9 @@ module.exports = class TeacherClassView extends RootView
|
||||||
stats.totalPlaytime = if playtime then moment.duration(playtime, "seconds").humanize() else 0
|
stats.totalPlaytime = if playtime then moment.duration(playtime, "seconds").humanize() else 0
|
||||||
# TODO: Humanize differently ('1 hour' instead of 'an hour')
|
# TODO: Humanize differently ('1 hour' instead of 'an hour')
|
||||||
|
|
||||||
completeSessions = @classroom.sessions.filter (s) -> s.get('state')?.complete
|
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')]
|
||||||
stats.averageLevelsComplete = if @students.size() then (_.size(completeSessions) / @students.size()).toFixed(1) else 'N/A' # '
|
stats.averageLevelsComplete = if @students.size() then (_.size(completeSessions) / @students.size()).toFixed(1) else 'N/A' # '
|
||||||
stats.totalLevelsComplete = _.size(completeSessions)
|
stats.totalLevelsComplete = _.size(completeSessions)
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,10 @@ CocoView = require 'views/core/CocoView'
|
||||||
template = require 'templates/play/level/control_bar'
|
template = require 'templates/play/level/control_bar'
|
||||||
{me} = require 'core/auth'
|
{me} = require 'core/auth'
|
||||||
|
|
||||||
|
Campaign = require 'models/Campaign'
|
||||||
|
Classroom = require 'models/Classroom'
|
||||||
|
Course = require 'models/Course'
|
||||||
|
CourseInstance = require 'models/CourseInstance'
|
||||||
GameMenuModal = require 'views/play/menu/GameMenuModal'
|
GameMenuModal = require 'views/play/menu/GameMenuModal'
|
||||||
RealTimeModel = require 'models/RealTimeModel'
|
RealTimeModel = require 'models/RealTimeModel'
|
||||||
RealTimeCollection = require 'collections/RealTimeCollection'
|
RealTimeCollection = require 'collections/RealTimeCollection'
|
||||||
|
@ -28,6 +32,7 @@ module.exports = class ControlBarView extends CocoView
|
||||||
'click #control-bar-sign-up-button': 'onClickSignupButton'
|
'click #control-bar-sign-up-button': 'onClickSignupButton'
|
||||||
|
|
||||||
constructor: (options) ->
|
constructor: (options) ->
|
||||||
|
@supermodel = options.supermodel
|
||||||
@courseID = options.courseID
|
@courseID = options.courseID
|
||||||
@courseInstanceID = options.courseInstanceID
|
@courseInstanceID = options.courseInstanceID
|
||||||
|
|
||||||
|
@ -38,6 +43,26 @@ module.exports = class ControlBarView extends CocoView
|
||||||
@levelID = @levelSlug or @level.id
|
@levelID = @levelSlug or @level.id
|
||||||
@spectateGame = options.spectateGame ? false
|
@spectateGame = options.spectateGame ? false
|
||||||
@observing = options.session.get('creator') isnt me.id
|
@observing = options.session.get('creator') isnt me.id
|
||||||
|
|
||||||
|
@levelNumber = ''
|
||||||
|
if @level.get('type') is 'course' and @level.get('campaignIndex')?
|
||||||
|
@levelNumber = @level.get('campaignIndex') + 1
|
||||||
|
if @courseInstanceID
|
||||||
|
@courseInstance = new CourseInstance(_id: @courseInstanceID)
|
||||||
|
jqxhr = @courseInstance.fetch()
|
||||||
|
@supermodel.trackRequest(jqxhr)
|
||||||
|
new Promise(jqxhr.then).then(=>
|
||||||
|
@classroom = new Classroom(_id: @courseInstance.get('classroomID'))
|
||||||
|
@supermodel.trackRequest @classroom.fetch()
|
||||||
|
)
|
||||||
|
else if @courseID
|
||||||
|
@course = new Course(_id: @courseID)
|
||||||
|
jqxhr = @course.fetch()
|
||||||
|
@supermodel.trackRequest(jqxhr)
|
||||||
|
new Promise(jqxhr.then).then(=>
|
||||||
|
@campaign = new Campaign(_id: @course.get('campaignID'))
|
||||||
|
@supermodel.trackRequest(@campaign.fetch())
|
||||||
|
)
|
||||||
super options
|
super options
|
||||||
if @level.get('type') in ['hero-ladder', 'course-ladder'] and me.isAdmin()
|
if @level.get('type') in ['hero-ladder', 'course-ladder'] and me.isAdmin()
|
||||||
@isMultiplayerLevel = true
|
@isMultiplayerLevel = true
|
||||||
|
@ -45,6 +70,13 @@ module.exports = class ControlBarView extends CocoView
|
||||||
if @level.get 'replayable'
|
if @level.get 'replayable'
|
||||||
@listenTo @session, 'change-difficulty', @onSessionDifficultyChanged
|
@listenTo @session, 'change-difficulty', @onSessionDifficultyChanged
|
||||||
|
|
||||||
|
onLoaded: ->
|
||||||
|
if @classroom
|
||||||
|
@levelNumber = @classroom.getLevelNumber(@level.get('original'), @levelNumber)
|
||||||
|
else if @campaign
|
||||||
|
@levelNumber = @campaign.getLevelNumber(@level.get('original'), @levelNumber)
|
||||||
|
super()
|
||||||
|
|
||||||
setBus: (@bus) ->
|
setBus: (@bus) ->
|
||||||
|
|
||||||
onPlayerStatesChanged: (e) ->
|
onPlayerStatesChanged: (e) ->
|
||||||
|
@ -62,7 +94,6 @@ module.exports = class ControlBarView extends CocoView
|
||||||
getRenderData: (c={}) ->
|
getRenderData: (c={}) ->
|
||||||
super c
|
super c
|
||||||
c.worldName = @worldName
|
c.worldName = @worldName
|
||||||
c.campaignIndex = @level.get('campaignIndex') + 1 if @level.get('type') is 'course' and @level.get('campaignIndex')? # TODO: support 'game-dev' levels in courses
|
|
||||||
c.multiplayerEnabled = @session.get('multiplayer')
|
c.multiplayerEnabled = @session.get('multiplayer')
|
||||||
c.ladderGame = @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder']
|
c.ladderGame = @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder']
|
||||||
if c.isMultiplayerLevel = @isMultiplayerLevel
|
if c.isMultiplayerLevel = @isMultiplayerLevel
|
||||||
|
|
|
@ -43,7 +43,12 @@ module.exports = class CourseVictoryModal extends ModalView
|
||||||
|
|
||||||
@playSound 'victory'
|
@playSound 'victory'
|
||||||
@nextLevel = new Level()
|
@nextLevel = new Level()
|
||||||
@nextLevelRequest = @supermodel.trackRequest @nextLevel.fetchNextForCourse({ levelOriginalID: @level.get('original'), @courseInstanceID, @courseID })
|
@nextLevelRequest = @supermodel.trackRequest(@nextLevel.fetchNextForCourse({
|
||||||
|
levelOriginalID: @level.get('original')
|
||||||
|
@courseInstanceID
|
||||||
|
@courseID
|
||||||
|
sessionID: @session.id
|
||||||
|
}))
|
||||||
|
|
||||||
@course = options.course
|
@course = options.course
|
||||||
if @courseID and not @course
|
if @courseID and not @course
|
||||||
|
|
|
@ -151,12 +151,10 @@ module.exports =
|
||||||
courseData = { _id: course._id, levels: [] }
|
courseData = { _id: course._id, levels: [] }
|
||||||
campaign = campaignMap[course.get('campaignID').toString()]
|
campaign = campaignMap[course.get('campaignID').toString()]
|
||||||
levels = _.values(campaign.get('levels'))
|
levels = _.values(campaign.get('levels'))
|
||||||
# TODO: remove practice filter after classroom Ux supports practice levels
|
|
||||||
levels = _.reject(levels, {'practice': true})
|
|
||||||
levels = _.sortBy(levels, 'campaignIndex')
|
levels = _.sortBy(levels, 'campaignIndex')
|
||||||
for level in levels
|
for level in levels
|
||||||
levelData = { original: mongoose.Types.ObjectId(level.original) }
|
levelData = { original: mongoose.Types.ObjectId(level.original) }
|
||||||
_.extend(levelData, _.pick(level, 'type', 'slug', 'name'))
|
_.extend(levelData, _.pick(level, 'type', 'slug', 'name', 'practice', 'practiceThresholdMinutes'))
|
||||||
courseData.levels.push(levelData)
|
courseData.levels.push(levelData)
|
||||||
coursesData.push(courseData)
|
coursesData.push(courseData)
|
||||||
classroom.set('courses', coursesData)
|
classroom.set('courses', coursesData)
|
||||||
|
|
|
@ -9,8 +9,10 @@ Classroom = require '../models/Classroom'
|
||||||
Course = require '../models/Course'
|
Course = require '../models/Course'
|
||||||
User = require '../models/User'
|
User = require '../models/User'
|
||||||
Level = require '../models/Level'
|
Level = require '../models/Level'
|
||||||
|
LevelSession = require '../models/LevelSession'
|
||||||
parse = require '../commons/parse'
|
parse = require '../commons/parse'
|
||||||
{objectIdFromTimestamp} = require '../lib/utils'
|
{objectIdFromTimestamp} = require '../lib/utils'
|
||||||
|
utils = require '../../app/core/utils'
|
||||||
Prepaid = require '../models/Prepaid'
|
Prepaid = require '../models/Prepaid'
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
|
@ -71,35 +73,46 @@ module.exports =
|
||||||
|
|
||||||
fetchNextLevel: wrap (req, res) ->
|
fetchNextLevel: wrap (req, res) ->
|
||||||
levelOriginal = req.params.levelOriginal
|
levelOriginal = req.params.levelOriginal
|
||||||
if not database.isID(levelOriginal)
|
unless database.isID(levelOriginal) then throw new errors.UnprocessableEntity('Invalid level original ObjectId')
|
||||||
throw new errors.UnprocessableEntity('Invalid level original ObjectId')
|
sessionID = req.params.sessionID
|
||||||
|
unless database.isID(sessionID) then throw new errors.UnprocessableEntity('Invalid session ObjectId')
|
||||||
courseInstance = yield database.getDocFromHandle(req, CourseInstance)
|
courseInstance = yield database.getDocFromHandle(req, CourseInstance)
|
||||||
if not courseInstance
|
unless courseInstance then throw new errors.NotFound('Course Instance not found.')
|
||||||
throw new errors.NotFound('Course Instance not found.')
|
|
||||||
courseID = courseInstance.get('courseID')
|
|
||||||
|
|
||||||
classroom = yield Classroom.findById courseInstance.get('classroomID')
|
classroom = yield Classroom.findById courseInstance.get('classroomID')
|
||||||
if not classroom
|
unless classroom then throw new errors.NotFound('Classroom not found.')
|
||||||
throw new errors.NotFound('Classroom not found.')
|
currentLevel = yield Level.findOne({original: mongoose.Types.ObjectId(levelOriginal)}, {practiceThresholdMinutes: 1, type: 1})
|
||||||
|
unless currentLevel then throw new errors.NotFound('Current level not found.')
|
||||||
|
|
||||||
nextLevelOriginal = null
|
courseID = courseInstance.get('courseID')
|
||||||
foundLevelOriginal = false
|
courseLevels = []
|
||||||
for course in classroom.get('courses') or []
|
courseLevels = course.levels for course in classroom.get('courses') or [] when courseID.equals(course._id)
|
||||||
if not courseID.equals(course._id)
|
|
||||||
continue
|
|
||||||
for level, index in course.levels
|
|
||||||
if level.original.toString() is levelOriginal
|
|
||||||
foundLevelOriginal = true
|
|
||||||
nextLevelOriginal = course.levels[index+1]?.original
|
|
||||||
break
|
|
||||||
|
|
||||||
if not foundLevelOriginal
|
# Get level completions and playtime
|
||||||
throw new errors.NotFound('Level original ObjectId not found in Classroom courses')
|
currentLevelSession = null
|
||||||
|
levelIDs = (level.original.toString() for level in courseLevels)
|
||||||
|
query = {$and: [{creator: req.user.id}, {'level.original': {$in: levelIDs}}]}
|
||||||
|
levelSessions = yield LevelSession.find(query, {level: 1, playtime: 1, state: 1})
|
||||||
|
levelCompleteMap = {}
|
||||||
|
for levelSession in levelSessions
|
||||||
|
currentLevelSession = levelSession if levelSession.id is sessionID
|
||||||
|
levelCompleteMap[levelSession.get('level')?.original] = levelSession.get('state')?.complete
|
||||||
|
unless currentLevelSession then throw new errors.NotFound('Level session not found.')
|
||||||
|
needsPractice = utils.needsPractice(currentLevelSession.get('playtime'), currentLevel.get('practiceThresholdMinutes'))
|
||||||
|
|
||||||
if not nextLevelOriginal
|
# Find next level
|
||||||
return res.status(200).send({})
|
levels = []
|
||||||
|
currentIndex = -1
|
||||||
|
for level, index in courseLevels
|
||||||
|
currentIndex = index if level.original.toString() is levelOriginal
|
||||||
|
levels.push
|
||||||
|
practice: level.practice ? false
|
||||||
|
complete: levelCompleteMap[level.original?.toString()] or currentIndex is index
|
||||||
|
unless currentIndex >=0 then throw new errors.NotFound('Level original ObjectId not found in Classroom courses')
|
||||||
|
nextLevelIndex = utils.findNextLevel(levels, currentIndex, needsPractice)
|
||||||
|
nextLevelOriginal = courseLevels[nextLevelIndex]?.original
|
||||||
|
unless nextLevelOriginal then return res.status(200).send({})
|
||||||
|
|
||||||
|
# Return full Level object
|
||||||
dbq = Level.findOne({original: mongoose.Types.ObjectId(nextLevelOriginal)})
|
dbq = Level.findOne({original: mongoose.Types.ObjectId(nextLevelOriginal)})
|
||||||
dbq.sort({ 'version.major': -1, 'version.minor': -1 })
|
dbq.sort({ 'version.major': -1, 'version.minor': -1 })
|
||||||
dbq.select(parse.getProjectFromReq(req))
|
dbq.select(parse.getProjectFromReq(req))
|
||||||
|
@ -107,7 +120,6 @@ module.exports =
|
||||||
level = level.toObject({req: req})
|
level = level.toObject({req: req})
|
||||||
res.status(200).send(level)
|
res.status(200).send(level)
|
||||||
|
|
||||||
|
|
||||||
fetchClassroom: wrap (req, res) ->
|
fetchClassroom: wrap (req, res) ->
|
||||||
courseInstance = yield database.getDocFromHandle(req, CourseInstance)
|
courseInstance = yield database.getDocFromHandle(req, CourseInstance)
|
||||||
if not courseInstance
|
if not courseInstance
|
||||||
|
|
|
@ -55,6 +55,7 @@ module.exports =
|
||||||
classroomID = courseInstance.get('classroomID')
|
classroomID = courseInstance.get('classroomID')
|
||||||
continue unless classroomID
|
continue unless classroomID
|
||||||
classroom = classroomMap[classroomID.toString()]
|
classroom = classroomMap[classroomID.toString()]
|
||||||
|
continue unless classroom
|
||||||
courseID = courseInstance.get('courseID')
|
courseID = courseInstance.get('courseID')
|
||||||
classroomCourse = _.find(classroom.get('courses'), (c) -> c._id.equals(courseID))
|
classroomCourse = _.find(classroom.get('courses'), (c) -> c._id.equals(courseID))
|
||||||
for courseLevel in classroomCourse.levels
|
for courseLevel in classroomCourse.levels
|
||||||
|
|
|
@ -81,7 +81,7 @@ module.exports.setup = (app) ->
|
||||||
app.get('/db/course/:handle/levels/:levelOriginal/next', mw.courses.fetchNextLevel)
|
app.get('/db/course/:handle/levels/:levelOriginal/next', mw.courses.fetchNextLevel)
|
||||||
|
|
||||||
app.post('/db/course_instance/-/recent', mw.auth.checkHasPermission(['admin']), mw.courseInstances.fetchRecent)
|
app.post('/db/course_instance/-/recent', mw.auth.checkHasPermission(['admin']), mw.courseInstances.fetchRecent)
|
||||||
app.get('/db/course_instance/:handle/levels/:levelOriginal/next', mw.courseInstances.fetchNextLevel)
|
app.get('/db/course_instance/:handle/levels/:levelOriginal/sessions/:sessionID/next', mw.courseInstances.fetchNextLevel)
|
||||||
app.post('/db/course_instance/:handle/members', mw.auth.checkLoggedIn(), mw.courseInstances.addMembers)
|
app.post('/db/course_instance/:handle/members', mw.auth.checkLoggedIn(), mw.courseInstances.addMembers)
|
||||||
app.get('/db/course_instance/:handle/classroom', mw.auth.checkLoggedIn(), mw.courseInstances.fetchClassroom)
|
app.get('/db/course_instance/:handle/classroom', mw.auth.checkLoggedIn(), mw.courseInstances.fetchClassroom)
|
||||||
|
|
||||||
|
|
|
@ -144,16 +144,6 @@ describe 'POST /db/classroom', ->
|
||||||
expect(classroom.get('courses')[0].levels[0].name).toBe('Level A')
|
expect(classroom.get('courses')[0].levels[0].name).toBe('Level A')
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'makes a copy of the list of all non-practice levels in all courses', utils.wrap (done) ->
|
|
||||||
teacher = yield utils.initUser({role: 'teacher'})
|
|
||||||
yield utils.loginUser(teacher)
|
|
||||||
data = { name: 'tmp Classroom 2' }
|
|
||||||
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
|
|
||||||
classroom = yield Classroom.findById(res.body._id)
|
|
||||||
# console.log(JSON.stringify(classroom.get('courses')[0], null, 2));
|
|
||||||
expect(classroom.get('courses')[0].levels.length).toEqual(2)
|
|
||||||
done()
|
|
||||||
|
|
||||||
describe 'GET /db/classroom/:handle/levels', ->
|
describe 'GET /db/classroom/:handle/levels', ->
|
||||||
|
|
||||||
beforeEach utils.wrap (done) ->
|
beforeEach utils.wrap (done) ->
|
||||||
|
|
|
@ -9,6 +9,7 @@ User = require '../../../server/models/User'
|
||||||
Classroom = require '../../../server/models/Classroom'
|
Classroom = require '../../../server/models/Classroom'
|
||||||
Campaign = require '../../../server/models/Campaign'
|
Campaign = require '../../../server/models/Campaign'
|
||||||
Level = require '../../../server/models/Level'
|
Level = require '../../../server/models/Level'
|
||||||
|
LevelSession = require '../../../server/models/LevelSession'
|
||||||
Prepaid = require '../../../server/models/Prepaid'
|
Prepaid = require '../../../server/models/Prepaid'
|
||||||
request = require '../request'
|
request = require '../request'
|
||||||
moment = require 'moment'
|
moment = require 'moment'
|
||||||
|
@ -246,6 +247,7 @@ describe 'GET /db/course_instance/:handle/levels/:levelOriginal/next', ->
|
||||||
yield utils.clearModels [User, Classroom, Course, Level, Campaign]
|
yield utils.clearModels [User, Classroom, Course, Level, Campaign]
|
||||||
admin = yield utils.initAdmin()
|
admin = yield utils.initAdmin()
|
||||||
yield utils.loginUser(admin)
|
yield utils.loginUser(admin)
|
||||||
|
teacher = yield utils.initUser({role: 'teacher'})
|
||||||
|
|
||||||
levelJSON = { name: 'A', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
|
levelJSON = { name: 'A', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
|
||||||
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
|
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
|
||||||
|
@ -253,12 +255,25 @@ describe 'GET /db/course_instance/:handle/levels/:levelOriginal/next', ->
|
||||||
@levelA = yield Level.findById(res.body._id)
|
@levelA = yield Level.findById(res.body._id)
|
||||||
paredLevelA = _.pick(res.body, 'name', 'original', 'type')
|
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' }
|
levelJSON = { name: 'B', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
|
||||||
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
|
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
|
||||||
expect(res.statusCode).toBe(200)
|
expect(res.statusCode).toBe(200)
|
||||||
@levelB = yield Level.findById(res.body._id)
|
@levelB = yield Level.findById(res.body._id)
|
||||||
paredLevelB = _.pick(res.body, 'name', 'original', 'type')
|
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' }
|
levelJSON = { name: 'C', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
|
||||||
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
|
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
|
||||||
expect(res.statusCode).toBe(200)
|
expect(res.statusCode).toBe(200)
|
||||||
|
@ -282,7 +297,6 @@ describe 'GET /db/course_instance/:handle/levels/:levelOriginal/next', ->
|
||||||
@courseB = Course({name: 'Course B', campaignID: @campaignB._id})
|
@courseB = Course({name: 'Course B', campaignID: @campaignB._id})
|
||||||
yield @courseB.save()
|
yield @courseB.save()
|
||||||
|
|
||||||
teacher = yield utils.initUser({role: 'teacher'})
|
|
||||||
yield utils.loginUser(teacher)
|
yield utils.loginUser(teacher)
|
||||||
data = { name: 'Classroom 1' }
|
data = { name: 'Classroom 1' }
|
||||||
classroomsURL = getURL('/db/classroom')
|
classroomsURL = getURL('/db/classroom')
|
||||||
|
@ -305,19 +319,19 @@ describe 'GET /db/course_instance/:handle/levels/:levelOriginal/next', ->
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'returns the next level for the course in the linked classroom', utils.wrap (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}/next"), json: true }
|
[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.statusCode).toBe(200)
|
||||||
expect(res.body.original).toBe(@levelB.original.toString())
|
expect(res.body.original).toBe(@levelB.original.toString())
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'returns empty object if the given level is the last level in its course', utils.wrap (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}/next"), json: true }
|
[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.statusCode).toBe(200)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it 'returns 404 if the given level is not in the course instance\'s course', utils.wrap (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}/next"), json: true }
|
[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)
|
expect(res.statusCode).toBe(404)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
describe 'Utility library', ->
|
describe 'Utility library', ->
|
||||||
util = require 'core/utils'
|
utils = require 'core/utils'
|
||||||
|
|
||||||
describe 'i18n', ->
|
describe 'i18n', ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
|
@ -25,24 +25,217 @@ describe 'Utility library', ->
|
||||||
'text': 'Godagens, trollkarl! Kommit för att öva? Nå, låt oss börja...'
|
'text': 'Godagens, trollkarl! Kommit för att öva? Nå, låt oss börja...'
|
||||||
|
|
||||||
it 'i18n should find a valid target string', ->
|
it 'i18n should find a valid target string', ->
|
||||||
expect(util.i18n(this.fixture1, 'text', 'sv')).toEqual(this.fixture1.i18n['sv'].text)
|
expect(utils.i18n(this.fixture1, 'text', 'sv')).toEqual(this.fixture1.i18n['sv'].text)
|
||||||
expect(util.i18n(this.fixture1, 'text', 'es-ES')).toEqual(this.fixture1.i18n['es-ES'].text)
|
expect(utils.i18n(this.fixture1, 'text', 'es-ES')).toEqual(this.fixture1.i18n['es-ES'].text)
|
||||||
|
|
||||||
it 'i18n picks the correct fallback for a specific language', ->
|
it 'i18n picks the correct fallback for a specific language', ->
|
||||||
expect(util.i18n(this.fixture1, 'text', 'fr-be')).toEqual(this.fixture1.i18n['fr'].text)
|
expect(utils.i18n(this.fixture1, 'text', 'fr-be')).toEqual(this.fixture1.i18n['fr'].text)
|
||||||
|
|
||||||
it 'i18n picks the correct fallback', ->
|
it 'i18n picks the correct fallback', ->
|
||||||
expect(util.i18n(this.fixture1, 'text', 'nl')).toEqual(this.fixture1.i18n['en'].text)
|
expect(utils.i18n(this.fixture1, 'text', 'nl')).toEqual(this.fixture1.i18n['en'].text)
|
||||||
expect(util.i18n(this.fixture1, 'text', 'nl', 'de')).toEqual(this.fixture1.i18n['de'].text)
|
expect(utils.i18n(this.fixture1, 'text', 'nl', 'de')).toEqual(this.fixture1.i18n['de'].text)
|
||||||
|
|
||||||
it 'i18n falls back to the default text, even for other targets (like blurb)', ->
|
it 'i18n falls back to the default text, even for other targets (like blurb)', ->
|
||||||
delete this.fixture1.i18n['en']
|
delete this.fixture1.i18n['en']
|
||||||
expect(util.i18n(this.fixture1, 'text', 'en')).toEqual(this.fixture1.text)
|
expect(utils.i18n(this.fixture1, 'text', 'en')).toEqual(this.fixture1.text)
|
||||||
expect(util.i18n(this.fixture1, 'blurb', 'en')).toEqual(this.fixture1.blurb)
|
expect(utils.i18n(this.fixture1, 'blurb', 'en')).toEqual(this.fixture1.blurb)
|
||||||
delete this.fixture1.blurb
|
delete this.fixture1.blurb
|
||||||
expect(util.i18n(this.fixture1, 'blurb', 'en')).toEqual(null)
|
expect(utils.i18n(this.fixture1, 'blurb', 'en')).toEqual(null)
|
||||||
|
|
||||||
it 'i18n can fall forward if a general language is not found', ->
|
it 'i18n can fall forward if a general language is not found', ->
|
||||||
expect(util.i18n(this.fixture1, 'text', 'pt')).toEqual(this.fixture1.i18n['pt-BR'].text)
|
expect(utils.i18n(this.fixture1, 'text', 'pt')).toEqual(this.fixture1.i18n['pt-BR'].text)
|
||||||
|
|
||||||
describe 'Miscellaneous utility', ->
|
describe 'createLevelNumberMap', ->
|
||||||
|
it 'returns correct map for r', ->
|
||||||
|
levels = [
|
||||||
|
{key: 1, practice: false}
|
||||||
|
]
|
||||||
|
levelNumberMap = utils.createLevelNumberMap(levels)
|
||||||
|
expect((val.toString() for key, val of levelNumberMap)).toEqual(['1'])
|
||||||
|
it 'returns correct map for r r', ->
|
||||||
|
levels = [
|
||||||
|
{key: 1, practice: false}
|
||||||
|
{key: 2, practice: false}
|
||||||
|
]
|
||||||
|
levelNumberMap = utils.createLevelNumberMap(levels)
|
||||||
|
expect((val.toString() for key, val of levelNumberMap)).toEqual(['1', '2'])
|
||||||
|
it 'returns correct map for p', ->
|
||||||
|
levels = [
|
||||||
|
{key: 1, practice: true}
|
||||||
|
]
|
||||||
|
levelNumberMap = utils.createLevelNumberMap(levels)
|
||||||
|
expect((val.toString() for key, val of levelNumberMap)).toEqual(['0a'])
|
||||||
|
it 'returns correct map for r p r', ->
|
||||||
|
levels = [
|
||||||
|
{key: 1, practice: false}
|
||||||
|
{key: 2, practice: true}
|
||||||
|
{key: 3, practice: false}
|
||||||
|
]
|
||||||
|
levelNumberMap = utils.createLevelNumberMap(levels)
|
||||||
|
expect((val.toString() for key, val of levelNumberMap)).toEqual(['1', '1a', '2'])
|
||||||
|
it 'returns correct map for r p p p', ->
|
||||||
|
levels = [
|
||||||
|
{key: 1, practice: false}
|
||||||
|
{key: 2, practice: true}
|
||||||
|
{key: 3, practice: true}
|
||||||
|
{key: 4, practice: true}
|
||||||
|
]
|
||||||
|
levelNumberMap = utils.createLevelNumberMap(levels)
|
||||||
|
expect((val.toString() for key, val of levelNumberMap)).toEqual(['1', '1a', '1b', '1c'])
|
||||||
|
it 'returns correct map for r p p p r p p r r p r', ->
|
||||||
|
levels = [
|
||||||
|
{key: 1, practice: false}
|
||||||
|
{key: 2, practice: true}
|
||||||
|
{key: 3, practice: true}
|
||||||
|
{key: 4, practice: true}
|
||||||
|
{key: 5, practice: false}
|
||||||
|
{key: 6, practice: true}
|
||||||
|
{key: 7, practice: true}
|
||||||
|
{key: 8, practice: false}
|
||||||
|
{key: 9, practice: false}
|
||||||
|
{key: 10, practice: true}
|
||||||
|
{key: 11, practice: false}
|
||||||
|
]
|
||||||
|
levelNumberMap = utils.createLevelNumberMap(levels)
|
||||||
|
expect((val.toString() for key, val of levelNumberMap)).toEqual(['1', '1a', '1b', '1c', '2', '2a', '2b', '3', '4', '4a', '5'])
|
||||||
|
|
||||||
|
describe 'findNextlevel', ->
|
||||||
|
describe 'when no practice needed', ->
|
||||||
|
needsPractice = false
|
||||||
|
it 'returns next level when rc* p', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 0, needsPractice)).toEqual(2)
|
||||||
|
done()
|
||||||
|
it 'returns next level when pc* p r', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: true, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
{practice: false, complete: false}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 0, needsPractice)).toEqual(2)
|
||||||
|
done()
|
||||||
|
it 'returns next level when pc* p p', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: true, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 0, needsPractice)).toEqual(3)
|
||||||
|
done()
|
||||||
|
it 'returns next level when rc* p rc', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
{practice: false, complete: true}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 0, needsPractice)).toEqual(3)
|
||||||
|
done()
|
||||||
|
describe 'when needs practice', ->
|
||||||
|
needsPractice = true
|
||||||
|
it 'returns next level when rc* p', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 0, needsPractice)).toEqual(1)
|
||||||
|
done()
|
||||||
|
it 'returns next level when rc* rc', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: false, complete: true}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 0, needsPractice)).toEqual(2)
|
||||||
|
done()
|
||||||
|
it 'returns next level when rc p rc*', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
{practice: false, complete: true}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 2, needsPractice)).toEqual(1)
|
||||||
|
done()
|
||||||
|
it 'returns next level when rc pc p rc*', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
{practice: false, complete: true}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 3, needsPractice)).toEqual(2)
|
||||||
|
done()
|
||||||
|
it 'returns next level when rc pc p rc* p', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 3, needsPractice)).toEqual(4)
|
||||||
|
done()
|
||||||
|
it 'returns next level when rc pc p rc* pc', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: true}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 3, needsPractice)).toEqual(5)
|
||||||
|
done()
|
||||||
|
it 'returns next level when rc pc p rc* pc p', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 3, needsPractice)).toEqual(5)
|
||||||
|
done()
|
||||||
|
it 'returns next level when rc pc p rc* pc r', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: true}
|
||||||
|
{practice: false, complete: false}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 3, needsPractice)).toEqual(5)
|
||||||
|
done()
|
||||||
|
it 'returns next level when rc pc p rc* pc p r', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: true}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
{practice: false, complete: false}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 3, needsPractice)).toEqual(5)
|
||||||
|
done()
|
||||||
|
it 'returns next level when rc pc pc rc* r p', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: true}
|
||||||
|
{practice: true, complete: true}
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: false, complete: false}
|
||||||
|
{practice: true, complete: false}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 3, needsPractice)).toEqual(4)
|
||||||
|
done()
|
||||||
|
it 'returns next level when rc* pc rc', (done) ->
|
||||||
|
levels = [
|
||||||
|
{practice: false, complete: true}
|
||||||
|
{practice: true, complete: true}
|
||||||
|
{practice: false, complete: true}
|
||||||
|
]
|
||||||
|
expect(utils.findNextLevel(levels, 0, needsPractice)).toEqual(3)
|
||||||
|
done()
|
||||||
|
|
Loading…
Reference in a new issue