Merge branch 'master' into production

This commit is contained in:
Matt Lott 2016-06-27 15:17:20 -07:00
commit 152e468865
26 changed files with 508 additions and 151 deletions

View file

@ -325,3 +325,48 @@ module.exports.capitalLanguages = capitalLanguages =
'python': 'Python'
'java': 'Java'
'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

View file

@ -295,6 +295,7 @@
saving: "Saving..."
sending: "Sending..."
send: "Send"
type: "Type"
cancel: "Cancel"
save: "Save"
publish: "Publish"

View file

@ -3,6 +3,7 @@ schema = require 'schemas/models/campaign.schema'
Level = require 'models/Level'
Levels = require 'collections/Levels'
CocoCollection = require 'collections/CocoCollection'
utils = require '../core/utils'
module.exports = class Campaign extends CocoModel
@className: 'Campaign'
@ -23,3 +24,11 @@ module.exports = class Campaign extends CocoModel
levels.comparator = 'campaignIndex'
levels.sort()
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

View file

@ -1,6 +1,6 @@
CocoModel = require './CocoModel'
schema = require 'schemas/models/classroom.schema'
utils = require 'core/utils'
utils = require '../core/utils'
User = require 'models/User'
module.exports = class Classroom extends CocoModel
@ -44,6 +44,16 @@ module.exports = class Classroom extends CocoModel
_.extend options, opts
@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) ->
options = {
url: _.result(@, 'url') + '/members'
@ -88,30 +98,59 @@ module.exports = class Classroom extends CocoModel
statsForSessions: (sessions, courseID) ->
return null unless sessions
stats = {}
sessions = sessions.models or sessions
arena = @getLadderLevel(courseID)
levels = @getLevels({courseID: courseID, withoutLadderLevels: true})
levelOriginals = levels.pluck('original')
completeSessionOriginals = (session.get('level').original for session in sessions when session.get('state').complete)
incompleteSessionOriginals = (session.get('level').original for session in sessions when not session.get('state').complete)
levelsLeft = _.size(_.difference(levelOriginals, completeSessionOriginals))
next = _.find levels.models, (level) -> level.get('original') not in completeSessionOriginals
lastPlayed = _.find levels.models, (level) -> level.get('original') in incompleteSessionOriginals
stats.levels = {
size: levels.size()
left: levelsLeft
done: levelsLeft is 0
numDone: levels.size() - levelsLeft
pctDone: (100 * (levels.size() - levelsLeft) / levels.size()).toFixed(1) + '%'
lastPlayed: lastPlayed
next: next
first: levels.first()
arena: arena
}
sum = (nums) -> _.reduce(nums, (s, num) -> s + num) or 0
stats.playtime = sum((session.get('playtime') or 0 for session in sessions))
return stats
courseLevels = @getLevels({courseID: courseID, withoutLadderLevels: true})
levelSessionMap = {}
levelSessionMap[session.get('level').original] = session for session in sessions
currentIndex = -1
lastStarted = null
levelsTotal = 0
levelsLeft = 0
lastPlayed = null
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
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={}) ->
return unless courseInstanceID

View file

@ -262,9 +262,9 @@ module.exports = class Level extends CocoModel
isLadder: ->
return @get('type')?.indexOf('ladder') > -1
fetchNextForCourse: ({ levelOriginalID, courseInstanceID, courseID }, options={}) ->
fetchNextForCourse: ({ levelOriginalID, courseInstanceID, courseID, sessionID }, options={}) ->
if courseInstanceID
options.url = "/db/course_instance/#{courseInstanceID}/levels/#{levelOriginalID}/next"
options.url = "/db/course_instance/#{courseInstanceID}/levels/#{levelOriginalID}/sessions/#{sessionID}/next"
else
options.url = "/db/course/#{courseID}/levels/#{levelOriginalID}/next"
@fetch(options)

View file

@ -66,6 +66,7 @@ _.extend CampaignSchema.properties, {
original: { type: 'string', format: 'hidden' }
adventurer: { type: 'boolean' }
practice: { type: 'boolean' }
practiceThresholdMinutes: {type: 'number'}
adminOnly: { type: 'boolean' }
disableSpaces: { type: ['boolean','number'] }
hidesSubmitUntilRun: { type: 'boolean' }

View file

@ -23,6 +23,8 @@ _.extend ClassroomSchema.properties,
courses: c.array { title: 'Courses' }, c.object { title: 'Course' }, {
_id: c.objectId()
levels: c.array { title: 'Levels' }, c.object { title: 'Level' }, {
practice: {type: 'boolean'}
practiceThresholdMinutes: {type: 'number'}
type: c.shortString()
original: c.objectId()
name: {type: 'string'}

View file

@ -32,10 +32,6 @@ block content
td(data-i18n="courses.total_students")
td
span.spr= _.size(view.classroom.get('members'))
span (
span.spr(data-i18n="courses.enrolled_courses")
span= stats.enrolledUsers
span )
tr
td(data-i18n="courses.average_time")
td= stats.averagePlaytime
@ -108,16 +104,17 @@ block content
.progress
each trimModel in levels.models
- 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++
- var session = sessionMap[level.get('original')];
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
.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
.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
.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
.text-center
button.enable-btn.btn.btn-info.btn-sm.text-uppercase(data-user-id=user.id, data-course-instance-cid=courseInstance.cid)

View file

@ -88,6 +88,7 @@ block content
tr
th
th(data-i18n="clans.status")
th(data-i18n="common.type")
th(data-i18n="resources.level")
th(data-i18n="courses.concepts")
tbody
@ -97,6 +98,7 @@ block content
- var levelCount = 0;
each level in view.levels.models
- var levelStatus = null;
- var levelNumber = view.classroom.getLevelNumber(level.get('original'), ++levelCount);
if view.userLevelStateMap[me.id]
- levelStatus = view.userLevelStateMap[me.id][level.get('original')]
tr
@ -107,10 +109,8 @@ block content
td
if view.userLevelStateMap[me.id]
div= view.userLevelStateMap[me.id][level.get('original')]
- previousLevelCompleted = view.userLevelStateMap[me.id][level.get('original')] === 'complete'
else
- previousLevelCompleted = false
td= ++levelCount + '. ' + level.get('name').replace('Course: ', '')
td #{level.get('practice') ? 'practice' : 'required'}
td #{levelNumber}. #{level.get('name').replace('Course: ', '')}
td
if view.levelConceptMap[level.get('original')]
each concept in view.course.get('concepts')
@ -118,3 +118,8 @@ block content
span.spr.concept(data-i18n="concepts." + concept)
if level.get('original') === lastLevelCompleted
- passedLastCompletedLevel = true
if !level.get('practice')
if view.userLevelStateMap[me.id]
- previousLevelCompleted = view.userLevelStateMap[me.id][level.get('original')] === 'complete'
else
- previousLevelCompleted = false

View file

@ -309,7 +309,8 @@ mixin courseOverview
.course-overview-progress
each level, index in levels
- 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)
.student-levels-row.alternating-background
@ -321,7 +322,8 @@ mixin studentLevelsRow(student)
- var levels = view.classroom.getLevels({courseID: course.id}).models
each level, index in levels
- 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)
//- TODO: Refactor with TeacherClassesView jade

View file

@ -56,12 +56,12 @@ block content
| :
select.level-select.form-control
if view.campaigns.loaded
each level, levelIndex in view.campaigns.get(course.get('campaignID')).getLevels().models
if level.get('practice')
- continue;
- var campaign = view.campaigns.get(course.get('campaignID'))
each level, levelIndex in campaign.getLevels().models
- var levelNumber = campaign.getLevelNumber(level.get('original'), levelIndex + 1)
option(value=level.get('slug'))
span
= levelIndex + 1
= levelNumber
span.spr
| .
span

View file

@ -22,7 +22,7 @@ else
.level-name-area
.level-label(data-i18n="play_level.level")
.level-name(title=difficultyTitle || "")
span= (campaignIndex ? campaignIndex + '. ' : '') + worldName.replace('Course: ', '')
span #{view.levelNumber ? view.levelNumber + '. ' : ''}#{worldName.replace('Course: ', '')}
if levelDifficulty
sup.level-difficulty= levelDifficulty

View file

@ -17,5 +17,9 @@ if started
//- .small-details
//- i(data-i18n='teacher.click_to_view_progress')
else
.small-details.nowrap
span= levelNumber
span.spr .
span= levelName
span.small-details.nowrap(data-i18n='teacher.no_progress')
| No progress

View file

@ -13,7 +13,7 @@ if completed
.small-details.nowrap
span.spr(data-i18n='teacher.completed')
| Completed
span= new Date(dateFirstCompleted).toLocaleString()
span= new Date(session.get('dateFirstCompleted')).toLocaleString()
+timePlayed
//- .small-details
//- i(data-i18n='teacher.click_to_view_solution')
@ -26,11 +26,15 @@ else if started
.small-details.nowrap
span.spr(data-i18n='teacher.last_played')
| Last played
span= new Date(lastPlayed).toLocaleString()
span= new Date(session.get('changed')).toLocaleString()
+timePlayed
//- .small-details
//- i(data-i18n='teacher.click_to_view_progress')
//- | click to view progress
else
.small-details.nowrap
span= levelNumber
span.spr .
span= levelName
span.small-details.nowrap(data-i18n='teacher.no_progress')
| No progress

View file

@ -55,7 +55,7 @@ module.exports = class ClassroomView extends RootView
@ownedClassrooms.fetchMine({data: {project: '_id'}})
@supermodel.trackCollection(@ownedClassrooms)
@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
@supermodel.trackCollection(@levels)
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.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.totalLevelsComplete = _.size(completeSessions)

View file

@ -52,7 +52,7 @@ module.exports = class CourseDetailsView extends RootView
@supermodel.trackRequest(@classroom.fetch())
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(=>

View file

@ -118,7 +118,7 @@ module.exports = class TeacherClassView extends RootView
@supermodel.trackRequest @courseInstances.fetchForClassroom(classroomID)
@levels = new Levels()
@supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts'}})
@supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts,practice'}})
@attachMediatorEvents()
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
# 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.totalLevelsComplete = _.size(completeSessions)

View file

@ -2,6 +2,10 @@ CocoView = require 'views/core/CocoView'
template = require 'templates/play/level/control_bar'
{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'
RealTimeModel = require 'models/RealTimeModel'
RealTimeCollection = require 'collections/RealTimeCollection'
@ -28,6 +32,7 @@ module.exports = class ControlBarView extends CocoView
'click #control-bar-sign-up-button': 'onClickSignupButton'
constructor: (options) ->
@supermodel = options.supermodel
@courseID = options.courseID
@courseInstanceID = options.courseInstanceID
@ -38,6 +43,26 @@ module.exports = class ControlBarView extends CocoView
@levelID = @levelSlug or @level.id
@spectateGame = options.spectateGame ? false
@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
if @level.get('type') in ['hero-ladder', 'course-ladder'] and me.isAdmin()
@isMultiplayerLevel = true
@ -45,6 +70,13 @@ module.exports = class ControlBarView extends CocoView
if @level.get 'replayable'
@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) ->
onPlayerStatesChanged: (e) ->
@ -62,7 +94,6 @@ module.exports = class ControlBarView extends CocoView
getRenderData: (c={}) ->
super c
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.ladderGame = @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder']
if c.isMultiplayerLevel = @isMultiplayerLevel

View file

@ -43,7 +43,12 @@ module.exports = class CourseVictoryModal extends ModalView
@playSound 'victory'
@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
if @courseID and not @course

View file

@ -151,12 +151,10 @@ module.exports =
courseData = { _id: course._id, levels: [] }
campaign = campaignMap[course.get('campaignID').toString()]
levels = _.values(campaign.get('levels'))
# TODO: remove practice filter after classroom Ux supports practice levels
levels = _.reject(levels, {'practice': true})
levels = _.sortBy(levels, 'campaignIndex')
for level in levels
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)
coursesData.push(courseData)
classroom.set('courses', coursesData)

View file

@ -9,8 +9,10 @@ Classroom = require '../models/Classroom'
Course = require '../models/Course'
User = require '../models/User'
Level = require '../models/Level'
LevelSession = require '../models/LevelSession'
parse = require '../commons/parse'
{objectIdFromTimestamp} = require '../lib/utils'
utils = require '../../app/core/utils'
Prepaid = require '../models/Prepaid'
module.exports =
@ -71,35 +73,46 @@ module.exports =
fetchNextLevel: wrap (req, res) ->
levelOriginal = req.params.levelOriginal
if not database.isID(levelOriginal)
throw new errors.UnprocessableEntity('Invalid level original ObjectId')
unless database.isID(levelOriginal) then 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)
if not courseInstance
throw new errors.NotFound('Course Instance not found.')
courseID = courseInstance.get('courseID')
unless courseInstance then throw new errors.NotFound('Course Instance not found.')
classroom = yield Classroom.findById courseInstance.get('classroomID')
if not classroom
throw new errors.NotFound('Classroom not found.')
unless classroom then 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
foundLevelOriginal = false
for course in classroom.get('courses') or []
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
courseID = courseInstance.get('courseID')
courseLevels = []
courseLevels = course.levels for course in classroom.get('courses') or [] when courseID.equals(course._id)
if not foundLevelOriginal
throw new errors.NotFound('Level original ObjectId not found in Classroom courses')
# Get level completions and playtime
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
return res.status(200).send({})
# Find next level
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.sort({ 'version.major': -1, 'version.minor': -1 })
dbq.select(parse.getProjectFromReq(req))
@ -107,7 +120,6 @@ module.exports =
level = level.toObject({req: req})
res.status(200).send(level)
fetchClassroom: wrap (req, res) ->
courseInstance = yield database.getDocFromHandle(req, CourseInstance)
if not courseInstance

View file

@ -55,6 +55,7 @@ module.exports =
classroomID = courseInstance.get('classroomID')
continue unless classroomID
classroom = classroomMap[classroomID.toString()]
continue unless classroom
courseID = courseInstance.get('courseID')
classroomCourse = _.find(classroom.get('courses'), (c) -> c._id.equals(courseID))
for courseLevel in classroomCourse.levels

View file

@ -81,7 +81,7 @@ module.exports.setup = (app) ->
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.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.get('/db/course_instance/:handle/classroom', mw.auth.checkLoggedIn(), mw.courseInstances.fetchClassroom)

View file

@ -144,16 +144,6 @@ describe 'POST /db/classroom', ->
expect(classroom.get('courses')[0].levels[0].name).toBe('Level A')
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', ->
beforeEach utils.wrap (done) ->

View file

@ -9,6 +9,7 @@ User = require '../../../server/models/User'
Classroom = require '../../../server/models/Classroom'
Campaign = require '../../../server/models/Campaign'
Level = require '../../../server/models/Level'
LevelSession = require '../../../server/models/LevelSession'
Prepaid = require '../../../server/models/Prepaid'
request = require '../request'
moment = require 'moment'
@ -246,6 +247,7 @@ describe 'GET /db/course_instance/:handle/levels/:levelOriginal/next', ->
yield utils.clearModels [User, Classroom, Course, Level, Campaign]
admin = yield utils.initAdmin()
yield utils.loginUser(admin)
teacher = yield utils.initUser({role: 'teacher'})
levelJSON = { name: 'A', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
[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)
paredLevelA = _.pick(res.body, 'name', 'original', 'type')
@sessionA = new LevelSession
creator: teacher.id
level: original: @levelA.get('original').toString()
permissions: simplePermissions
state: complete: true
yield @sessionA.save()
levelJSON = { name: 'B', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
expect(res.statusCode).toBe(200)
@levelB = yield Level.findById(res.body._id)
paredLevelB = _.pick(res.body, 'name', 'original', 'type')
@sessionB = new LevelSession
creator: teacher.id
level: original: @levelB.get('original').toString()
permissions: simplePermissions
yield @sessionB.save()
levelJSON = { name: 'C', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
expect(res.statusCode).toBe(200)
@ -282,7 +297,6 @@ describe 'GET /db/course_instance/:handle/levels/:levelOriginal/next', ->
@courseB = Course({name: 'Course B', campaignID: @campaignB._id})
yield @courseB.save()
teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(teacher)
data = { name: 'Classroom 1' }
classroomsURL = getURL('/db/classroom')
@ -305,19 +319,19 @@ describe 'GET /db/course_instance/:handle/levels/:levelOriginal/next', ->
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.body.original).toBe(@levelB.original.toString())
done()
it 'returns empty object if the given level is the last level in its course', utils.wrap (done) ->
[res, body] = yield request.getAsync { uri: utils.getURL("/db/course_instance/#{@courseInstanceA.id}/levels/#{@levelB.id}/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.body).toEqual({})
done()
it 'returns 404 if the given level is not in the course instance\'s course', utils.wrap (done) ->
[res, body] = yield request.getAsync { uri: utils.getURL("/db/course_instance/#{@courseInstanceB.id}/levels/#{@levelA.id}/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)
done()

View file

@ -1,5 +1,5 @@
describe 'Utility library', ->
util = require 'core/utils'
utils = require 'core/utils'
describe 'i18n', ->
beforeEach ->
@ -25,24 +25,217 @@ describe 'Utility library', ->
'text': 'Godagens, trollkarl! Kommit för att öva? Nå, låt oss börja...'
it 'i18n should find a valid target string', ->
expect(util.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', 'sv')).toEqual(this.fixture1.i18n['sv'].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', ->
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', ->
expect(util.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')).toEqual(this.fixture1.i18n['en'].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)', ->
delete this.fixture1.i18n['en']
expect(util.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, 'text', 'en')).toEqual(this.fixture1.text)
expect(utils.i18n(this.fixture1, 'blurb', 'en')).toEqual(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', ->
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()