mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 17:45:40 -05:00
Practice levels Ux and next level algorithm
Update classroom and gameplay Ux to surface practice levels as 3a, 3b, etc. Update next level logic to leverage practice levels based on per level completion playtime thresholds. Patrol buster and patrol buster A are live for testing. Fix a few classroom Ux progress hover bubble info bugs. Closes #3767
This commit is contained in:
parent
5edffa8fcd
commit
d72e4eb750
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
|
||||||
|
levels = []
|
||||||
|
for level, index in courseLevels.models
|
||||||
|
levelsTotal++ unless level.get('practice')
|
||||||
|
complete = false
|
||||||
|
if session = levelSessionMap[level.get('original')]
|
||||||
|
complete = session.get('state').complete ? false
|
||||||
|
playtime += session.get('playtime') ? 0
|
||||||
|
lastPlayed = level
|
||||||
|
if complete
|
||||||
|
currentIndex = index
|
||||||
|
else
|
||||||
|
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
|
left: levelsLeft
|
||||||
done: levelsLeft is 0
|
done: levelsLeft is 0
|
||||||
numDone: levels.size() - levelsLeft
|
numDone: levelsTotal - levelsLeft
|
||||||
pctDone: (100 * (levels.size() - levelsLeft) / levels.size()).toFixed(1) + '%'
|
pctDone: (100 * (levelsTotal - levelsLeft) / levelsTotal).toFixed(1) + '%'
|
||||||
lastPlayed: lastPlayed
|
lastPlayed: lastPlayed
|
||||||
next: next
|
next: nextLevel
|
||||||
first: levels.first()
|
first: courseLevels.first()
|
||||||
arena: arena
|
arena: arena
|
||||||
}
|
playtime: playtime
|
||||||
sum = (nums) -> _.reduce(nums, (s, num) -> s + num) or 0
|
stats
|
||||||
stats.playtime = sum((session.get('playtime') or 0 for session in sessions))
|
|
||||||
return 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