Lock course content to classrooms

This commit is contained in:
Scott Erickson 2016-04-13 09:54:24 -07:00
parent c9ed76471a
commit 675e3290ac
44 changed files with 857 additions and 513 deletions

View file

@ -10,3 +10,9 @@ module.exports = class CourseInstances extends CocoCollection
options.data ?= {}
options.data.ownerID = ownerID
@fetch(options)
fetchForClassroom: (classroomID, options={}) ->
classroomID = classroomID.id or classroomID # handle if they pass in a user
options.data ?= {}
options.data.classroomID = classroomID
@fetch(options)

View file

@ -5,6 +5,12 @@ module.exports = class LevelSessionCollection extends CocoCollection
url: '/db/level.session'
model: LevelSession
fetchMineForCourseInstance: (courseInstanceID, options) ->
options = _.extend({
url: "/db/course_instance/#{courseInstanceID}/my-course-level-sessions"
}, options)
@fetch(options)
fetchForCourseInstance: (courseInstanceID, options) ->
options = _.extend({
url: "/db/course_instance/#{courseInstanceID}/my-course-level-sessions"

View file

@ -4,3 +4,12 @@ Level = require 'models/Level'
module.exports = class LevelCollection extends CocoCollection
url: '/db/level'
model: Level
fetchForClassroom: (classroomID, options={}) ->
options.url = "/db/classroom/#{classroomID}/levels"
@fetch(options)
fetchForClassroomAndCourse: (classroomID, courseID, options={}) ->
options.url = "/db/classroom/#{classroomID}/courses/#{courseID}/levels"
@fetch(options)

View file

@ -1,5 +1,6 @@
go = (path, options) -> -> @routeDirectly path, arguments, options
redirect = (path) -> -> @navigate(path, { trigger: true, replace: true })
utils = require './utils'
module.exports = class CocoRouter extends Backbone.Router
@ -13,6 +14,8 @@ module.exports = class CocoRouter extends Backbone.Router
'': ->
if window.serverConfig.picoCTF
return @routeDirectly 'play/CampaignView', ['picoctf'], {}
if utils.getQueryVariable 'hour_of_code'
return @navigate "/play", {trigger: true, replace: true}
return @routeDirectly('NewHomeView', [])
'about': go('AboutView')

View file

@ -178,4 +178,5 @@ prunePath = (delta, path) ->
module.exports.DOC_SKIP_PATHS = [
'_id','version', 'commitMessage', 'parent', 'created',
'slug', 'index', '__v', 'patches', 'creator', 'js', 'watchers']
'slug', 'index', '__v', 'patches', 'creator', 'js', 'watchers', 'levelsUpdated'
]

View file

@ -1,8 +1,10 @@
Levels = require 'collections/Levels'
module.exports =
# Result: Each course instance gains a property, numCompleted, that is the
# number of students in that course instance who have completed ALL of
# the levels in thate course
calculateDots: (classrooms, courses, courseInstances, campaigns) ->
calculateDots: (classrooms, courses, courseInstances) ->
for classroom in classrooms.models
# map [user, level] => session so we don't have to do find TODO
for course, courseIndex in courses.models
@ -10,9 +12,9 @@ module.exports =
continue if not instance
instance.numCompleted = 0
instance.numStarted = 0
campaign = campaigns.get(course.get('campaignID'))
levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true})
for userID in instance.get('members')
levelCompletes = _.map campaign.getNonLadderLevels().models, (level) ->
levelCompletes = _.map levels.models, (level) ->
return true if level.isLadder()
#TODO: Hella slow! Do the mapping first!
session = _.find classroom.sessions.models, (session) ->
@ -24,13 +26,13 @@ module.exports =
if _.any levelCompletes
instance.numStarted += 1
calculateEarliestIncomplete: (classroom, courses, campaigns, courseInstances, students) ->
calculateEarliestIncomplete: (classroom, courses, courseInstances, students) ->
# Loop through all the combinations of things, return the first one that somebody hasn't finished
for course, courseIndex in courses.models
instance = courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
continue if not instance
campaign = campaigns.get(course.get('campaignID'))
for level, levelIndex in campaign.getNonLadderLevels().models
levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true})
for level, levelIndex in levels.models
userIDs = []
for user in students.models
userID = user.id
@ -49,15 +51,15 @@ module.exports =
}
null
calculateLatestComplete: (classroom, courses, campaigns, courseInstances, students) ->
calculateLatestComplete: (classroom, courses, courseInstances, students) ->
# Loop through all the combinations of things in reverse order, return the level that anyone's finished
courseModels = courses.models.slice()
for course, courseIndex in courseModels.reverse() #
courseIndex = courses.models.length - courseIndex - 1 #compensate for reverse
instance = courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
continue if not instance
campaign = campaigns.get(course.get('campaignID'))
levelModels = campaign.getNonLadderLevels().models.slice()
levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true})
levelModels = levels.models.slice()
for level, levelIndex in levelModels.reverse() #
levelIndex = levelModels.length - levelIndex - 1 #compensate for reverse
userIDs = []
@ -86,9 +88,9 @@ module.exports =
conceptData[classroom.id] = {}
for course, courseIndex in courses.models
campaign = campaigns.get(course.get('campaignID'))
levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true})
for level in campaign.getNonLadderLevels().models
for level in levels.models
levelID = level.get('original')
for concept in level.get('concepts')
@ -111,7 +113,7 @@ module.exports =
conceptData[classroom.id][concept].completed = false
conceptData
calculateAllProgress: (classrooms, courses, campaigns, courseInstances, students) ->
calculateAllProgress: (classrooms, courses, courseInstances, students) ->
# Loop through all combinations and record:
# Completeness for each student/course
# Completeness for each student/level
@ -133,9 +135,9 @@ module.exports =
progressData[classroom.id][course.id] = { completed: false, started: false }
continue
progressData[classroom.id][course.id] = { completed: true, started: false } # to be updated
campaign = campaigns.get(course.get('campaignID'))
for level in campaign.getNonLadderLevels().models
levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true})
for level in levels.models
levelID = level.get('original')
progressData[classroom.id][course.id][levelID] = { completed: students.size() > 0, started: false }

View file

@ -1254,6 +1254,7 @@
concepts_covered: "Concepts covered"
print_guide: "Print Guide (PDF)"
view_guide_online: "View Guide Online (PDF)"
last_updated: "Last updated:"
grants_lifetime_access: "Grants lifetime access to all Courses." # New enrollment modal
enrollment_credits_available: "Enrollment Credits Available:"
description: "Description" # ClassroomSettingsModal

View file

@ -11,31 +11,6 @@ module.exports = class Campaign extends CocoModel
saveBackups: true
@denormalizedLevelProperties: _.keys(_.omit(schema.properties.levels.additionalProperties.properties, ['unlocks', 'position', 'rewards']))
@denormalizedCampaignProperties: ['name', 'i18n', 'slug']
statsForSessions: (sessions) ->
return null unless sessions
stats = {}
sessions = sessions.models or sessions
sessions = _.sortBy sessions, (s) -> s.get('changed')
levels = _.values(@get('levels'))
levels = (level for level in levels when not _.contains(level.type, 'ladder'))
levelOriginals = _.pluck(levels, 'original')
sessionOriginals = (session.get('level').original for session in sessions when session.get('state').complete)
levelsLeft = _.size(_.difference(levelOriginals, sessionOriginals))
lastSession = _.last(sessions)
stats.levels = {
size: _.size(levels)
left: levelsLeft
done: levelsLeft is 0
numDone: _.size(levels) - levelsLeft
pctDone: (100 * (_.size(levels) - levelsLeft) / _.size(levels)).toFixed(1) + '%'
lastPlayed: if lastSession then _.findWhere levels, { original: lastSession.get('level').original } else null
first: _.first(levels)
arena: _.find _.values(@get('levels')), (level) -> _.contains(level.type, 'ladder')
}
sum = (nums) -> _.reduce(nums, (s, num) -> s + num) or 0
stats.playtime = sum((session.get('playtime') or 0 for session in sessions))
return stats
getLevels: ->
levels = new Levels(_.values(@get('levels')))

View file

@ -32,3 +32,59 @@ module.exports = class Classroom extends CocoModel
}
_.extend options, opts
@fetch(options)
getLevels: (options={}) ->
# options: courseID, withoutLadderLevels
Levels = require 'collections/Levels'
courses = @get('courses')
return new Levels() unless courses
levelObjects = []
for course in courses
if options.courseID and options.courseID isnt course._id
continue
levelObjects.push(course.levels)
levels = new Levels(_.flatten(levelObjects))
if options.withoutLadderLevels
levels.remove(levels.filter((level) -> level.isLadder()))
return levels
getLadderLevel: (courseID) ->
Levels = require 'collections/Levels'
courses = @get('courses')
course = _.findWhere(courses, {_id: courseID})
return unless course
levels = new Levels(course.levels)
return levels.find (l) -> l.isLadder()
statsForSessions: (sessions, courseID) ->
return null unless sessions
stats = {}
sessions = sessions.models or sessions
sessions = _.sortBy sessions, (s) -> s.get('changed')
arena = @getLadderLevel(courseID)
levels = @getLevels({courseID: courseID, withoutLadderLevels: true})
levelOriginals = levels.pluck('original')
sessionOriginals = (session.get('level').original for session in sessions when session.get('state').complete)
levelsLeft = _.size(_.difference(levelOriginals, sessionOriginals))
lastSession = _.last(sessions)
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: if lastSession then levels.findWhere({ original: lastSession.get('level').original }) else null
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
fetchForCourseInstance: (courseInstanceID, options={}) ->
CourseInstance = require 'models/CourseInstance'
courseInstance = if _.isString(courseInstanceID) then new CourseInstance({_id:courseInstanceID}) else courseInstanceID
options = _.extend(options, {
url: _.result(courseInstance, 'url') + '/classroom'
})
@fetch(options)

View file

@ -251,3 +251,7 @@ module.exports = class Level extends CocoModel
isLadder: ->
return @get('type')?.indexOf('ladder') > -1
fetchNextForCourse: (levelOriginalID, courseInstanceID, options={}) ->
options.url = "/db/course_instance/#{courseInstanceID}/levels/#{levelOriginalID}/next"
@fetch(options)

View file

@ -96,8 +96,9 @@ module.exports = class SuperModel extends Backbone.Model
jqxhr.done -> res.markLoaded()
jqxhr.fail -> res.markFailed()
@storeResource(res, value)
return jqxhr
trackRequests: (jqxhrs, value=1) -> @trackRequest(jqxhr) for jqxhr in jqxhrs
trackRequests: (jqxhrs, value=1) -> @trackRequest(jqxhr, value) for jqxhr in jqxhrs
# replace or overwrite
shouldSaveBackups: (model) -> false

View file

@ -45,6 +45,7 @@ _.extend CampaignSchema.properties, {
showIfUnlocked: { type: 'string', links: [{rel: 'db', href: '/db/level/{($)}/version'}], format: 'latest-version-original-reference' }
}
}}
levelsUpdated: c.date()
levels: { type: 'object', format: 'levels', additionalProperties: {
title: 'Level'

View file

@ -20,6 +20,15 @@ _.extend ClassroomSchema.properties,
type: 'boolean'
default: false
description: 'Visual only; determines if the classroom is in the "archived" list of the normal list.'
courses: c.array { title: 'Courses' }, c.object { title: 'Course' }, {
_id: c.objectId()
levels: c.array { title: 'Levels' }, c.object { title: 'Level' }, {
type: c.shortString()
original: c.objectId()
name: {type: 'string'}
slug: {type: 'string'}
}
}
c.extendBasicProperties ClassroomSchema, 'Classroom'

View file

@ -1,5 +1,5 @@
- var completed = session && session.get('state') && session.get('state').complete;
h3 #{i}. #{level.name.replace('Course: ', '')}
h3 #{i}. #{level.get('name').replace('Course: ', '')}
if session
p
span.spr(data-i18n="courses.play_time")

View file

@ -89,7 +89,6 @@ block content
if !(inCourse || view.teacherMode)
- continue;
- var course = view.courses.get(courseInstance.get('courseID'));
- var campaign = view.campaigns.get(course.get('campaignID'));
- var sessions = courseInstance.sessionsByUser[user.id] || [];
if !(course.get('free') || paidFor)
- continue;
@ -98,8 +97,8 @@ block content
.col-sm-3.text-right= course.get('name')
.col-sm-9
if inCourse
- var levels = campaign.get('levels');
- var numLevels = Object.keys(levels).length;
- var levels = view.classroom.getLevels({courseID: course.id});
- var numLevels = levels.size();
- var sessionMap = _.zipObject(_.map(sessions, function(s) { return s.get('level').original; }), sessions);
- var levelCellWidth = 100.00;
if numLevels > 0
@ -107,9 +106,10 @@ block content
- var css = "width:"+levelCellWidth+"%;"
- var i = 0;
.progress
each level, levelID in campaign.get('levels')
each trimModel in levels.models
- var level = view.levels.get(trimModel.get('original')); // get the level loaded through the db
- i++
- var session = sessionMap[levelID];
- var session = sessionMap[level.get('original')];
a(href=view.getLevelURL(level, course, courseInstance, session))
- var content = view.levelPopoverContent(level, session, i);
if session && session.get('state') && session.get('state').complete

View file

@ -16,160 +16,105 @@ block content
br
br
if (noCourseInstance || noCourseInstanceSelected) && course
h1= course.get('name')
if noCourseInstance
p(data-i18n="courses.not_enrolled")
p
span.spr(data-i18n="courses.visit_pref")
a(href="/courses", data-i18n="courses.courses")
span.spl(data-i18n="courses.visit_suf")
else if noCourseInstanceSelected
p(data-i18n="courses.select_class")
.container-fluid
.row
.col-md-6
select.form-control.select-instance
each courseInstance in courseInstances
if courseInstance.get('name')
option(value="#{courseInstance.id}")= courseInstance.get('name')
else
option(value="#{courseInstance.id}", data-i18n="courses.unnamed")
.col-md-6
button.btn.btn-success.btn-select-instance(data-i18n="courses.select")
else if !course || !courseInstance
h1(data-i18n="common.loading")
else
p
// TODO: format this text all good and stuff
strong
if courseInstance.get('name')
span= courseInstance.get('name')
else if view.classroom.get('name')
span= view.classroom.get('name')
else
span(data-i18n='courses.unnamed_class')
p
// TODO: format this text all good and stuff
strong
if view.courseInstance.get('name')
span= view.courseInstance.get('name')
else if view.classroom.get('name')
span= view.classroom.get('name')
else
span(data-i18n='courses.unnamed_class')
if !view.owner.isNew() && view.getOwnerName() && courseInstance.get('name') != 'Single Player'
span.spl -
span.spl(data-i18n='courses.teacher')
span.spr :
//a(href="/user/#{view.owner.id}") // Don't link to profiles until we improve them
span
strong= view.getOwnerName()
if !view.owner.isNew() && view.getOwnerName() && view.courseInstance.get('name') != 'Single Player'
span.spl -
span.spl(data-i18n='courses.teacher')
span.spr :
//a(href="/user/#{view.owner.id}") // Don't link to profiles until we improve them
span
strong= view.getOwnerName()
h1
| #{course.get('name')}
if view.courseComplete
span.spl -
span.spl(data-i18n='courses.complete')
span !
h1
| #{view.course.get('name')}
if view.courseComplete
span.spl -
span.spl(data-i18n='courses.complete')
span !
p
if courseInstance.get('description')
each line in courseInstance.get('description').split('\n')
div= line
p
if view.courseInstance.get('description')
each line in view.courseInstance.get('description').split('\n')
div= line
if view.courseComplete && !view.teacherMode
.jumbotron
if promptForSchool
.row
.col-md-6.col-md-offset-3
form.form#school-form
.form-group
label.control-label(for="course-complete-school-input")
span.spr(data-i18n="signup.school_name")
em.optional-note
| (
span(data-i18n="signup.optional")
| ):
.input-border
input#course-complete-school-input.input-large.form-control(name="schoolName", data-i18n="[placeholder]signup.school_name_placeholder")
button.btn.btn-primary.btn-submit.no-school(type="submit", data-i18n='courses.none')
button.btn.btn-info.btn-submit.save-school(type="submit", data-i18n='courses.save')
.row
if view.singlePlayerMode && !me.isAnonymous()
.col-md-6.col-md-offset-3
a.btn.btn-lg.btn-success(href="/play")
h1(data-i18n='courses.play_campaign_title')
p(data-i18n='courses.play_campaign_description')
else if view.singlePlayerMode && me.isAnonymous()
.col-md-6
a.btn.btn-lg.btn-success.signup-button
h1(data-i18n='courses.create_account_title')
p(data-i18n='courses.create_account_description')
.col-md-6
a.btn.btn-lg.btn-success(href="/play")
h1(data-i18n='courses.preview_campaign_title')
p(data-i18n='courses.preview_campaign_description')
else if !view.singlePlayerMode
.col-md-6
if view.arenaLevel
a.btn.btn-lg.btn-success.btn-play-level(data-level-slug=view.arenaLevel.slug, data-level-id=view.arenaLevel.original)
h1
span(data-i18n='courses.arena')
span.spr :
span= view.arenaLevel.name
p= view.arenaLevel.description.replace(/!\[.*?\)/, '')
else
a.btn.btn-lg.btn-success.disabled
h1(data-i18n='courses.arena_soon_title')
p
span.spr(data-i18n='courses.arena_soon_description')
span= course.get('name')
span .
.col-md-6
if view.nextCourseInstance
a.btn.btn-lg.btn-success(href="/courses/#{view.nextCourse.id}/#{view.nextCourseInstance.id}")
h1= view.nextCourse.get('name')
p= view.nextCourse.get('description')
else if view.nextCourse
a.btn.btn-lg.btn-success.disabled
h1= view.nextCourse.get('name')
p.text-uppercase
em(data-i18n='courses.not_enrolled1')
p(data-i18n='courses.not_enrolled2')
else
a.btn.btn-lg.btn-success(disabled=!view.nextCourse ? "disabled" : "")
h1(data-i18n='courses.next_course')
p.text-uppercase
em(data-i18n='courses.coming_soon1')
p(data-i18n='courses.coming_soon2')
if view.courseComplete && !view.teacherMode
.jumbotron
.row
.col-md-6
if view.arenaLevel
a.btn.btn-lg.btn-success.btn-play-level(data-level-slug=view.arenaLevel.get('slug'), data-level-id=view.arenaLevel.get('original'))
h1
span(data-i18n='courses.arena')
span.spr :
span= view.arenaLevel.get('name')
p= view.arenaLevel.get('description').replace(/!\[.*?\)/, '')
else
a.btn.btn-lg.btn-success.disabled
h1(data-i18n='courses.arena_soon_title')
p
span.spr(data-i18n='courses.arena_soon_description')
span= view.course.get('name')
span .
.col-md-6
if view.nextCourseInstance
a.btn.btn-lg.btn-success(href="/courses/#{view.nextCourse.id}/#{view.nextCourseInstance.id}")
h1= view.nextCourse.get('name')
p= view.nextCourse.get('description')
else if view.nextCourse
a.btn.btn-lg.btn-success.disabled
h1= view.nextCourse.get('name')
p.text-uppercase
em(data-i18n='courses.not_enrolled1')
p(data-i18n='courses.not_enrolled2')
else
a.btn.btn-lg.btn-success(disabled=!view.nextCourse ? "disabled" : "")
h1(data-i18n='courses.next_course')
p.text-uppercase
em(data-i18n='courses.coming_soon1')
p(data-i18n='courses.coming_soon2')
.available-courses-title(data-i18n='courses.available_levels')
table.table.table-striped.table-condensed
thead
.available-courses-title(data-i18n='courses.available_levels')
table.table.table-striped.table-condensed
thead
tr
th
th(data-i18n="clans.status")
th(data-i18n="resources.level")
th(data-i18n="courses.concepts")
tbody
- var previousLevelCompleted = true;
- var lastLevelCompleted = view.getLastLevelCompleted();
- var passedLastCompletedLevel = !lastLevelCompleted;
- var levelCount = 0;
each level in view.levels.models
- var levelStatus = null;
if view.userLevelStateMap[me.id]
- levelStatus = view.userLevelStateMap[me.id][level.get('original')]
tr
th
th(data-i18n="clans.status")
th(data-i18n="resources.level")
th(data-i18n="courses.concepts")
tbody
if campaign
- var previousLevelCompleted = true;
- var lastLevelCompleted = view.getLastLevelCompleted();
- var passedLastCompletedLevel = false;
- var levelCount = 0;
each level, levelID in campaign.get('levels')
- var levelStatus = null;
if userLevelStateMap[me.id]
- levelStatus = userLevelStateMap[me.id][levelID]
tr
td
if previousLevelCompleted || view.teacherMode || !passedLastCompletedLevel || levelStatus
- var i18n = level.type === 'course-ladder' ? 'play.compete' : 'home.play';
button.btn.btn-success.btn-play-level(data-level-slug=level.slug, data-i18n=i18n, data-level-id=levelID)
td
if userLevelStateMap[me.id]
div= userLevelStateMap[me.id][levelID]
- previousLevelCompleted = userLevelStateMap[me.id][levelID] === 'complete'
else
- previousLevelCompleted = false
td= ++levelCount + '. ' + level.name.replace('Course: ', '')
td
if levelConceptMap[levelID]
each concept in course.get('concepts')
if levelConceptMap[levelID][concept]
span.spr.concept(data-i18n="concepts." + concept)
if levelID === lastLevelCompleted
- passedLastCompletedLevel = true
td
if previousLevelCompleted || view.teacherMode || !passedLastCompletedLevel || levelStatus
- var i18n = level.get('type') === 'course-ladder' ? 'play.compete' : 'home.play';
button.btn.btn-success.btn-play-level(data-level-slug=level.get('slug'), data-i18n=i18n, data-level-id=level.get('original'))
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
if view.levelConceptMap[level.get('original')]
each concept in view.course.get('concepts')
if view.levelConceptMap[level.get('original')][concept]
span.spr.concept(data-i18n="concepts." + concept)
if level.get('original') === lastLevelCompleted
- passedLastCompletedLevel = true

View file

@ -45,42 +45,10 @@ block content
else
- var showHOCComplete = false;
if view.hocCourseInstance
- var course = view.courses.get(view.hocCourseInstance.get('courseID'));
- var campaign = view.campaigns.get(course.get('campaignID'));
- var stats = campaign.statsForSessions(view.hocCourseInstance.sessions);
- showHOCComplete = stats.levels.done && !view.classrooms.size();
.text-center
if !showHOCComplete
h1(data-i18n="courses.welcome_to_page") Welcome to your Courses page!
else
h1(data-i18n="courses.completed_hoc")
h2(data-i18n="courses.ready_for_more_header")
ul.text-left
li(data-i18n="courses.ready_for_more_1")
li(data-i18n="courses.ready_for_more_2")
li(data-i18n="courses.ready_for_more_3")
a.btn.btn-lg.btn-success(href="/play") Play Now
h1(data-i18n="courses.welcome_to_page") Welcome to your Courses page!
if view.hocCourseInstance && !view.classrooms.size()
h3(data-i18n="courses.saved_games")
hr
.course-instance-entry
h3
span(data-i18n="courses.hoc")
span.spr :
span.spr(data-i18n="courses.course")
span 1
span.spr= (me.get('aceConfig') || {}).language === 'javascript' ? 'JavaScript' : 'Python'
small
a#change-language-link(data-i18n="courses.change_language")
+course-instance-body(view.hocCourseInstance)
.clearfix
else if view.classrooms.size()
if view.classrooms.size()
h3.text-uppercase(data-i18n="courses.my_classes")
hr
@ -106,13 +74,9 @@ block content
span.spr= course.get('name')
small
a(href="/courses/"+courseInstance.get('courseID')+'/'+courseInstance.id, data-i18n="courses.view_levels")
+course-instance-body(courseInstance)
+course-instance-body(courseInstance, classroom)
.clearfix
else
.text-center
button#start-new-game-btn.btn.btn-success.btn-lg(data-i18n="courses.start_new_game")
h3.text-uppercase(data-i18n="courses.join_class")
hr
@ -131,16 +95,9 @@ block content
.alert.alert-danger= view.errorMessage
#begin-hoc-area.hide
h3.text-center(data-i18n="common.loading")
.progress.progress-striped.active
.progress-bar(style="width: 100%")
mixin course-instance-body(courseInstance)
mixin course-instance-body(courseInstance, classroom)
- var course = view.courses.get(courseInstance.get('courseID'));
- var campaign = view.campaigns.get(course.get('campaignID'));
- var stats = campaign.statsForSessions(courseInstance.sessions);
- var stats = classroom.statsForSessions(courseInstance.sessions, course.id);
if stats.levels.done
.text-success
span.glyphicon.glyphicon-ok
@ -150,19 +107,19 @@ mixin course-instance-body(courseInstance)
if stats.levels.done
- var arenaLevel = stats.levels.arena;
if arenaLevel
- var arenaURL = "/play/ladder/"+arenaLevel.slug+"/course/"+courseInstance.id;
- var arenaURL = "/play/ladder/"+arenaLevel.get('slug')+"/course/"+courseInstance.id;
a.btn.btn-warning.btn-lg(href=arenaURL)
span(data-i18n="courses.play_arena")
else
a.btn.btn-default.btn-lg(disabled=true, data-i18n="courses.course_complete")
else if courseInstance.sessions.size()
- var lastLevel = stats.levels.lastPlayed;
- var levelURL = "/play/level/"+lastLevel.slug+"?course="+courseInstance.get('courseID')+"&course-instance="+courseInstance.id;
- var levelURL = "/play/level/"+lastLevel.get('slug')+"?course="+courseInstance.get('courseID')+"&course-instance="+courseInstance.id;
a.btn.btn-success.btn-lg(href=levelURL)
span(data-i18n="common.continue")
else
- var firstLevel = stats.levels.first;
- var levelURL = "/play/level/"+firstLevel.slug+"?course="+courseInstance.get('courseID')+"&course-instance="+courseInstance.id;
- var levelURL = "/play/level/"+firstLevel.get('slug')+"?course="+courseInstance.get('courseID')+"&course-instance="+courseInstance.id;
a.btn.btn-info.btn-lg(href=levelURL)
span(data-i18n="courses.start")

View file

@ -216,7 +216,8 @@ mixin courseProgressTab
span(data-i18n='teacher.select_course')
span.spr :
select.course-select
each course in view.courses.models
each trimCourse in view.classroom.get('courses')
- var course = view.courses.get(trimCourse._id);
option(value=course.id)
= course.get('name')
if view.progressData
@ -229,8 +230,7 @@ mixin courseProgressTab
mixin courseOverview
- var course = view.selectedCourse
- var campaign = view.campaigns.get(course.get('campaignID'))
- var levels = campaign.getNonLadderLevels().models
- var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models
.course-overview-row
.course-title.student-name
span= course.get('name')
@ -248,8 +248,7 @@ mixin studentLevelsRow(student)
div.student-email.small-details= student.get('email')
div.student-levels-progress
- var course = view.selectedCourse
- var campaign = view.campaigns.get(course.get('campaignID'))
- var levels = campaign.getNonLadderLevels().models
- var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models
each level, index in levels
- var progress = view.progressData.get({ classroom: view.classroom, course: course, level: level, user: student })
+progressDot(progress, index+1)
@ -292,7 +291,8 @@ mixin bulkAssignControls
span(data-i18n='teacher.bulk_assign')
span :
select.bulk-course-select.form-control
each course in view.courses.models
each trimCourse in view.classroom.get('courses')
- var course = view.courses.get(trimCourse._id)
option(value=course.id)
= course.get('name')
button.btn.btn-primary-alt.assign-to-selected-students

View file

@ -75,7 +75,8 @@ mixin classRow(classroom)
if classroom.get('members').length == 0
+addStudentsButton(classroom)
else
each course, index in view.courses.models
each trimCourse, index in classroom.get('courses') || []
- var course = view.courses.get(trimCourse._id);
+progressDot(classroom, course, index)
.view-class-arrow.col-xs-1
a.view-class-arrow-inner.glyphicon.glyphicon-chevron-right(data-classroom-id=classroom.id, href=('/teachers/classes/' + classroom.id))

View file

@ -66,6 +66,7 @@ block content
.clearfix
mixin course-info(course)
- var campaign = view.campaigns.get(course.get('campaignID'));
.course-info
.text-h4.semibold
= course.get('name')
@ -93,3 +94,7 @@ mixin course-info(course)
| (
span(data-i18n='teacher.guides_coming_soon')
| )
if campaign && campaign.get('levelsUpdated')
p.small.m-t-2
span.spr(data-i18n="courses.last_updated")
span= moment(campaign.get('levelsUpdated')).format('LL')

View file

@ -7,7 +7,7 @@
.modal-body
.container-fluid
.row
- var colClass = view.nextLevel ? 'col-sm-7' : 'col-sm-12'
- var colClass = !view.nextLevel.isNew() ? 'col-sm-7' : 'col-sm-12'
div(class=colClass)
.well.well-sm.well-parchment
h3.text-uppercase(data-i18n='play_level.completed_level')
@ -25,12 +25,12 @@
.col-sm-8
h3.text-uppercase.text-center= i18n(view.course.attributes, 'name')
.col-sm-4
- var stats = view.campaign.statsForSessions(view.levelSessions)
- var stats = view.classroom.statsForSessions(view.levelSessions, view.course.id)
h1
span #{stats.levels.numDone}/#{stats.levels.size}
if view.nextLevel
if !view.nextLevel.isNew()
.col-sm-5
.well.well-sm.well-parchment
h3.text-uppercase
@ -45,7 +45,7 @@
// TODO: Add this and rest of campaign functionality
// button#continue-btn.btn.btn-illustrated.btn-default.btn-block.btn-lg.text-uppercase View Leaderboards
.col-sm-5
if view.nextLevel
if !view.nextLevel.isNew()
button#next-level-btn.btn.btn-illustrated.btn-primary.btn-block.btn-lg.text-uppercase(data-i18n='play_level.next_level')
else
button#done-btn.btn.btn-illustrated.btn-primary.btn-block.btn-lg.text-uppercase(data-i18n='play_level.done')

View file

@ -38,9 +38,6 @@ module.exports = class NewHomeView extends RootView
@variation ?= me.getHomepageGroup()
window.tracker?.trackEvent 'Homepage Loaded', category: 'Homepage'
if @getQueryVariable 'hour_of_code'
application.router.navigate "/hoc", trigger: true
if me.isTeacher()
@trialRequests = new TrialRequests()
@trialRequests.fetchOwn()

View file

@ -6,6 +6,7 @@ Classroom = require 'models/Classroom'
Classrooms = require 'collections/Classrooms'
LevelSession = require 'models/LevelSession'
Prepaids = require 'collections/Prepaids'
Levels = require 'collections/Levels'
RootView = require 'views/core/RootView'
template = require 'templates/courses/classroom-view'
User = require 'models/User'
@ -37,9 +38,7 @@ module.exports = class ClassroomView extends RootView
@courses = new CocoCollection([], { url: "/db/course", model: Course})
@courses.comparator = '_id'
@supermodel.loadCollection(@courses)
@campaigns = new CocoCollection([], { url: "/db/campaign", model: Campaign })
@courses.comparator = '_id'
@supermodel.loadCollection(@campaigns, { data: { type: 'course' }})
@courseInstances = new CocoCollection([], { url: "/db/course_instance", model: CourseInstance})
@courseInstances.comparator = 'courseID'
@supermodel.loadCollection(@courseInstances, { data: { classroomID: classroomID } })
@ -55,6 +54,11 @@ module.exports = class ClassroomView extends RootView
@ownedClassrooms = new Classrooms()
@ownedClassrooms.fetchMine({data: {project: '_id'}})
@supermodel.trackCollection(@ownedClassrooms)
@levels = new Levels()
@levels.fetchForClassroom(classroomID, {data: {project: 'name,slug,original'}})
@levels.on 'add', (model) -> @_byId[model.get('original')] = model # so you can 'get' them
@supermodel.trackCollection(@levels)
onCourseInstancesSync: ->
@sessions = new CocoCollection([], { model: LevelSession })
@ -90,9 +94,7 @@ module.exports = class ClassroomView extends RootView
for courseInstance in @courseInstances.models
courseID = courseInstance.get('courseID')
course = @courses.get(courseID)
campaignID = course.get('campaignID')
campaign = @campaigns.get(campaignID)
courseInstance.sessions.campaign = campaign
courseInstance.sessions.course = course
super()
afterRender: ->
@ -153,10 +155,10 @@ module.exports = class ClassroomView extends RootView
return '' unless user.sessions?
session = user.sessions.last()
return '' unless session
campaign = session.collection.campaign
course = session.collection.course
levelOriginal = session.get('level').original
campaignLevel = campaign.get('levels')[levelOriginal]
return "#{campaign.get('fullName')}, #{campaignLevel.name}"
level = @levels.findWhere({original: levelOriginal})
return "#{course.get('name')}, #{level.get('name')}"
userPlaytimeString: (user) ->
return '' unless user.sessions?
@ -240,4 +242,4 @@ module.exports = class ClassroomView extends RootView
getLevelURL: (level, course, courseInstance, session) ->
return null unless @teacherMode and _.all(arguments)
"/play/level/#{level.slug}?course=#{course.id}&course-instance=#{courseInstance.id}&session=#{session.id}&observing=true"
"/play/level/#{level.get('slug')}?course=#{course.id}&course-instance=#{courseInstance.id}&session=#{session.id}&observing=true"

View file

@ -1,10 +1,11 @@
Campaign = require 'models/Campaign'
CocoCollection = require 'collections/CocoCollection'
Course = require 'models/Course'
Courses = require 'collections/Courses'
LevelSessions = require 'collections/LevelSessions'
CourseInstance = require 'models/CourseInstance'
CourseInstances = require 'collections/CourseInstances'
Classroom = require 'models/Classroom'
Classrooms = require 'collections/Classrooms'
LevelSession = require 'models/LevelSession'
Levels = require 'collections/Levels'
RootView = require 'views/core/RootView'
template = require 'templates/courses/course-details'
User = require 'models/User'
@ -14,7 +15,6 @@ module.exports = class CourseDetailsView extends RootView
id: 'course-details-view'
template: template
teacherMode: false
singlePlayerMode: false
memberSort: 'nameAsc'
events:
@ -25,125 +25,64 @@ module.exports = class CourseDetailsView extends RootView
constructor: (options, @courseID, @courseInstanceID) ->
super options
@ownedClassrooms = new Classrooms()
@ownedClassrooms.fetchMine({data: {project: '_id'}})
@supermodel.trackCollection(@ownedClassrooms)
@courseID ?= options.courseID
@courseInstanceID ?= options.courseInstanceID
@courses = new Courses()
@course = new Course()
@levelSessions = new LevelSessions()
@courseInstance = new CourseInstance({_id: @courseInstanceID})
@owner = new User()
@classroom = new Classroom()
@course = @supermodel.getModel(Course, @courseID) or new Course _id: @courseID
@listenTo @course, 'sync', @onCourseSync
if @course.loaded
@onCourseSync()
else
@supermodel.loadModel @course
@levels = new Levels()
@courseInstances = new CourseInstances()
getRenderData: ->
context = super()
context.campaign = @campaign
context.course = @course if @course?.loaded
context.courseInstance = @courseInstance if @courseInstance?.loaded
context.courseInstances = @courseInstances?.models ? []
context.levelConceptMap = @levelConceptMap ? {}
context.noCourseInstance = @noCourseInstance
context.noCourseInstanceSelected = @noCourseInstanceSelected
context.userLevelStateMap = @userLevelStateMap ? {}
context.promptForSchool = @courseComplete and not me.isAnonymous() and not me.get('schoolName') and not storage.load('no-school')
context
@supermodel.trackRequest @ownedClassrooms.fetchMine({data: {project: '_id'}})
@supermodel.trackRequest(@courses.fetch().then(=>
@course = @courses.get(@courseID)
))
sessionsLoaded = @supermodel.trackRequest(@levelSessions.fetchForCourseInstance(@courseInstanceID, {cache: false}))
afterRender: ->
super()
if @supermodel.finished() and @courseComplete and me.isAnonymous() and @options.justBeatLevel
# TODO: Make an intermediate modal that tells them they've finished HoC and has some snazzy stuff for convincing players to sign up instead of just throwing up the bare CreateAccountModal
CreateAccountModal = require 'views/core/CreateAccountModal'
@openModalView new CreateAccountModal showSignupRationale: true
@supermodel.trackRequest(@courseInstance.fetch().then(=>
return if @destroyed
@teacherMode = @courseInstance.get('ownerID') is me.id
onCourseSync: ->
@owner = new User({_id: @courseInstance.get('ownerID')})
@supermodel.trackRequest(@owner.fetch())
classroomID = @courseInstance.get('classroomID')
@classroom = new Classroom({ _id: classroomID })
@supermodel.trackRequest(@classroom.fetch())
levelsLoaded = @supermodel.trackRequest(@levels.fetchForClassroomAndCourse(classroomID, @courseID, {
data: { project: 'concepts,type,slug,name,original,description' }
}))
@supermodel.trackRequest($.when(levelsLoaded, sessionsLoaded).then(=>
@buildSessionStats()
return if @destroyed
if @memberStats[me.id]?.totalLevelsCompleted >= @levels.size() - 1 # Don't need to complete arena
# need to figure out the next course instance
@courseComplete = true
@courseInstances.comparator = 'courseID'
@supermodel.trackRequest(@courseInstances.fetchForClassroom(classroomID).then(=>
@nextCourseInstance = _.find @courseInstances.models, (ci) => ci.get('courseID') > @courseID
if @nextCourseInstance
nextCourseID = @nextCourseInstance.get('courseID')
@nextCourse = @courses.get(nextCourseID)
))
@promptForSchool = @courseComplete and not me.isAnonymous() and not me.get('schoolName') and not storage.load('no-school')
))
))
buildSessionStats: ->
return if @destroyed
# console.log 'onCourseSync'
if me.isAnonymous() and (not me.get('hourOfCode') and not @course.get('hourOfCode'))
@noCourseInstance = true
@render()
return
return if @campaign?
campaignID = @course.get('campaignID')
@campaign = @supermodel.getModel(Campaign, campaignID) or new Campaign _id: campaignID
@listenTo @campaign, 'sync', @onCampaignSync
if @campaign.loaded
@onCampaignSync()
else
@supermodel.loadModel @campaign
@render()
onCampaignSync: ->
return if @destroyed
# console.log 'onCampaignSync'
if @courseInstanceID
@loadCourseInstance(@courseInstanceID)
else unless me.isAnonymous()
@loadCourseInstances()
@levelConceptMap = {}
for levelID, level of @campaign.get('levels')
@levelConceptMap[levelID] ?= {}
for concept in level.concepts
@levelConceptMap[levelID][concept] = true
if level.type is 'course-ladder'
for level in @levels.models
@levelConceptMap[level.get('original')] ?= {}
for concept in level.get('concepts')
@levelConceptMap[level.get('original')][concept] = true
if level.get('type') is 'course-ladder'
@arenaLevel = level
@render()
loadCourseInstances: ->
@courseInstances = new CocoCollection [], {url: "/db/user/#{me.id}/course_instances", model: CourseInstance, comparator: 'courseID'}
@listenToOnce @courseInstances, 'sync', @onCourseInstancesSync
@supermodel.loadCollection @courseInstances, 'course_instances'
loadAllCourses: ->
@allCourses = new CocoCollection [], {url: "/db/course", model: Course, comparator: '_id'}
@listenToOnce @allCourses, 'sync', @onAllCoursesSync
@supermodel.loadCollection @allCourses, 'courses'
loadCourseInstance: (courseInstanceID) ->
return if @destroyed
# console.log 'loadCourseInstance'
return if @courseInstance?
@courseInstanceID = courseInstanceID
@courseInstance = @supermodel.getModel(CourseInstance, @courseInstanceID) or new CourseInstance _id: @courseInstanceID
@listenTo @courseInstance, 'sync', @onCourseInstanceSync
if @courseInstance.loaded
@onCourseInstanceSync()
else
@courseInstance = @supermodel.loadModel(@courseInstance).model
onCourseInstancesSync: ->
return if @destroyed
# console.log 'onCourseInstancesSync'
@findNextCourseInstance()
if not @courseInstance
# We are loading these to find the one we want to display.
if @courseInstances.models.length is 1
@loadCourseInstance(@courseInstances.models[0].id)
else
if @courseInstances.models.length is 0
@noCourseInstance = true
else
@noCourseInstanceSelected = true
@render()
onCourseInstanceSync: ->
return if @destroyed
# console.log 'onCourseInstanceSync'
if @courseInstance.get('classroomID')
@classroom = new Classroom({_id: @courseInstance.get('classroomID')})
@supermodel.loadModel @classroom
@singlePlayerMode = @courseInstance.get('name') is 'Single Player'
@teacherMode = @courseInstance.get('ownerID') is me.id and not @singlePlayerMode
@levelSessions = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/level_sessions", model: LevelSession, comparator: '_id' })
@listenToOnce @levelSessions, 'sync', @onLevelSessionsSync
@supermodel.loadCollection @levelSessions, 'level_sessions', cache: false
@owner = new User({_id: @courseInstance.get('ownerID')})
@supermodel.loadModel @owner
@render()
onLevelSessionsSync: ->
return if @destroyed
# console.log 'onLevelSessionsSync'
@memberStats = {}
@userConceptStateMap = {}
@ -179,40 +118,17 @@ module.exports = class CourseDetailsView extends RootView
for concept, state of conceptStateMap
@conceptsCompleted[concept] ?= 0
@conceptsCompleted[concept]++
if @memberStats[me.id]?.totalLevelsCompleted >= _.size(@campaign.get('levels')) - 1 # Don't need to complete arena
@courseComplete = true
@loadCourseInstances() unless @courseInstances # Find the next course instance to do.
@render()
onAllCoursesSync: ->
@findNextCourseInstance()
findNextCourseInstance: ->
@nextCourseInstance = _.find @courseInstances.models, (ci) =>
# Sorted by courseID
ci.get('classroomID') is @courseInstance.get('classroomID') and ci.id isnt @courseInstance.id and ci.get('courseID') > @course.id
if @nextCourseInstance
nextCourseID = @nextCourseInstance.get('courseID')
@nextCourse = @supermodel.getModel(Course, nextCourseID) or new Course _id: nextCourseID
@nextCourse = @supermodel.loadModel(@nextCourse).model
else if @allCourses?.loaded
@nextCourse = _.find @allCourses.models, (course) => course.id > @course.id
else
@loadAllCourses()
onClickPlayLevel: (e) ->
levelSlug = $(e.target).closest('.btn-play-level').data('level-slug')
levelID = $(e.target).closest('.btn-play-level').data('level-id')
level = @campaign.get('levels')[levelID]
if level.type is 'course-ladder'
level = @levels.findWhere({original: levelID})
if level.get('type') is 'course-ladder'
viewClass = 'views/ladder/LadderView'
viewArgs = [{supermodel: @supermodel}, levelSlug]
route = '/play/ladder/' + levelSlug
unless @singlePlayerMode # No league for solo courses
route += '/course/' + @courseInstance.id
viewArgs = viewArgs.concat ['course', @courseInstance.id]
route += '/course/' + @courseInstance.id
viewArgs = viewArgs.concat ['course', @courseInstance.id]
else
route = @getLevelURL levelSlug
viewClass = 'views/play/level/PlayLevelView'
@ -222,30 +138,15 @@ module.exports = class CourseDetailsView extends RootView
getLevelURL: (levelSlug) ->
"/play/level/#{levelSlug}?course=#{@courseID}&course-instance=#{@courseInstanceID}"
onClickSelectInstance: (e) ->
courseInstanceID = $('.select-instance').val()
@noCourseInstanceSelected = false
@loadCourseInstance(courseInstanceID)
getOwnerName: ->
return if @owner.isNew()
if @owner.get('firstName') and @owner.get('lastName')
return "#{@owner.get('firstName')} #{@owner.get('lastName')}"
@owner.get('name') or @owner.get('email')
onSubmitSchoolForm: (e) ->
e.preventDefault()
schoolName = @$el.find('#course-complete-school-input').val().trim()
if schoolName and schoolName isnt me.get('schoolName')
me.set 'schoolName', schoolName
me.patch()
else
storage.save 'no-school', true
@$el.find('#school-form').slideUp('slow')
getLastLevelCompleted: ->
lastLevelCompleted = null
for levelID in _.keys(@campaign.get('levels'))
for levelID in @levels.pluck('original')
if @userLevelStateMap?[me.id]?[levelID] is 'complete'
lastLevelCompleted = levelID
return lastLevelCompleted

View file

@ -39,8 +39,6 @@ module.exports = class CoursesView extends RootView
@supermodel.trackCollection(@ownedClassrooms)
@courses = new CocoCollection([], { url: "/db/course", model: Course})
@supermodel.loadCollection(@courses)
@campaigns = new CocoCollection([], { url: "/db/campaign", model: Campaign })
@supermodel.loadCollection(@campaigns, { data: { type: 'course' }})
onCourseInstancesLoaded: ->
map = {}
@ -56,26 +54,15 @@ module.exports = class CoursesView extends RootView
courseInstance.sessions.comparator = 'changed'
@supermodel.loadCollection(courseInstance.sessions, { data: { project: 'state.complete level.original playtime changed' }})
@hocCourseInstance = @courseInstances.findWhere({hourOfCode: true})
if @hocCourseInstance
@courseInstances.remove(@hocCourseInstance)
hocCourseInstance = @courseInstances.findWhere({hourOfCode: true})
if hocCourseInstance
@courseInstances.remove(hocCourseInstance)
onLoaded: ->
super()
if utils.getQueryVariable('_cc', false) and not me.isAnonymous()
@joinClass()
onClickStartNewGameButton: ->
if me.isAnonymous()
@openSignUpModal()
else
modal = new ChooseLanguageModal()
@openModalView(modal)
@listenToOnce modal, 'set-language', =>
@startHourOfCodePlay()
application.tracker?.trackEvent 'Automatic start hour of code play', category: 'Courses', label: 'set language'
application.tracker?.trackEvent 'Start New Game', category: 'Courses'
onClickLogInButton: ->
modal = new StudentLogInModal()
@openModalView(modal)
@ -85,21 +72,8 @@ module.exports = class CoursesView extends RootView
openSignUpModal: ->
modal = new StudentSignUpModal({ willPlay: true })
@openModalView(modal)
modal.once 'click-skip-link', (=>
@startHourOfCodePlay()
application.tracker?.trackEvent 'Automatic start hour of code play', category: 'Courses', label: 'skip link'
), @
application.tracker?.trackEvent 'Started Student Signup', category: 'Courses'
startHourOfCodePlay: ->
@$('#main-content').hide()
@$('#begin-hoc-area').removeClass('hide')
hocCourseInstance = new CourseInstance()
hocCourseInstance.upsertForHOC()
@listenToOnce hocCourseInstance, 'sync', ->
url = hocCourseInstance.firstLevelURL()
app.router.navigate(url, { trigger: true })
onSubmitJoinClassForm: (e) ->
e.preventDefault()
@joinClass()

View file

@ -14,7 +14,6 @@ Users = require 'collections/Users'
Courses = require 'collections/Courses'
CourseInstance = require 'models/CourseInstance'
CourseInstances = require 'collections/CourseInstances'
Campaigns = require 'collections/Campaigns'
module.exports = class TeacherClassView extends RootView
id: 'teacher-class-view'
@ -62,10 +61,6 @@ module.exports = class TeacherClassView extends RootView
@courses.fetch()
@supermodel.trackCollection(@courses)
@campaigns = new Campaigns()
@campaigns.fetchByType('course')
@supermodel.trackCollection(@campaigns)
@courseInstances = new CourseInstances()
@courseInstances.fetchByOwner(me.id)
@supermodel.trackCollection(@courseInstances)
@ -76,15 +71,15 @@ module.exports = class TeacherClassView extends RootView
@classCode = @classroom.get('codeCamel') or @classroom.get('code')
@joinURL = document.location.origin + "/courses?_cc=" + @classCode
@earliestIncompleteLevel = helper.calculateEarliestIncomplete(@classroom, @courses, @campaigns, @courseInstances, @students)
@latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @campaigns, @courseInstances, @students)
@earliestIncompleteLevel = helper.calculateEarliestIncomplete(@classroom, @courses, @courseInstances, @students)
@latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @courseInstances, @students)
for student in @students.models
# TODO: this is a weird hack
studentsStub = new Users([ student ])
student.latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @campaigns, @courseInstances, studentsStub)
student.latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @courseInstances, studentsStub)
classroomsStub = new Classrooms([ @classroom ])
@progressData = helper.calculateAllProgress(classroomsStub, @courses, @campaigns, @courseInstances, @students)
@progressData = helper.calculateAllProgress(classroomsStub, @courses, @courseInstances, @students)
# @conceptData = helper.calculateConceptsCovered(classroomsStub, @courses, @campaigns, @courseInstances, @students)
@selectedCourse = @courses.first()

View file

@ -41,10 +41,6 @@ module.exports = class TeacherClassesView extends RootView
@courses.fetch()
@supermodel.trackCollection(@courses)
@campaigns = new Campaigns()
@campaigns.fetchByType('course')
@supermodel.trackCollection(@campaigns)
@courseInstances = new CourseInstances()
@courseInstances.fetchByOwner(me.id)
@supermodel.trackCollection(@courseInstances)
@ -62,7 +58,7 @@ module.exports = class TeacherClassesView extends RootView
})
onLoaded: ->
helper.calculateDots(@classrooms, @courses, @courseInstances, @campaigns)
helper.calculateDots(@classrooms, @courses, @courseInstances)
super()
onClickEditClassroom: (e) ->

View file

@ -2,7 +2,6 @@ ModalView = require 'views/core/ModalView'
template = require 'templates/play/level/modal/course-victory-modal'
Achievements = require 'collections/Achievements'
Level = require 'models/Level'
Campaign = require 'models/Campaign'
Course = require 'models/Course'
ThangType = require 'models/ThangType'
ThangTypes = require 'collections/ThangTypes'
@ -11,6 +10,7 @@ EarnedAchievement = require 'models/EarnedAchievement'
LocalMongo = require 'lib/LocalMongo'
ProgressView = require './ProgressView'
NewItemView = require './NewItemView'
Classroom = require 'models/Classroom'
utils = require 'core/utils'
module.exports = class CourseVictoryModal extends ModalView
@ -28,6 +28,9 @@ module.exports = class CourseVictoryModal extends ModalView
@level = options.level
@newItems = new ThangTypes()
@newHeroes = new ThangTypes()
@classroom = new Classroom()
@supermodel.trackRequest(@classroom.fetchForCourseInstance(@courseInstanceID))
@achievements = options.achievements
if not @achievements
@ -39,22 +42,13 @@ module.exports = class CourseVictoryModal extends ModalView
@onAchievementsLoaded()
@playSound 'victory'
@nextLevel = options.nextLevel
if (nextLevel = @level.get('nextLevel')) and not @nextLevel
@nextLevel = new Level().setURL "/db/level/#{nextLevel.original}/version/#{nextLevel.majorVersion}"
@nextLevel = @supermodel.loadModel(@nextLevel).model
@nextLevel = new Level()
@nextLevelRequest = @supermodel.trackRequest @nextLevel.fetchNextForCourse(@level.get('original'), @courseInstanceID)
@campaign = new Campaign()
@course = options.course
if @courseID and not @course
@course = new Course().setURL "/db/course/#{@courseID}"
@course = @supermodel.loadModel(@course).model
if @course.loading
@listenToOnce @course, 'sync', @onCourseLoaded
else
@onCourseLoaded()
else if @course
@onCourseLoaded()
if @courseInstanceID
@levelSessions = new LevelSessions()
@ -63,10 +57,10 @@ module.exports = class CourseVictoryModal extends ModalView
data: { project: 'state.complete level.original playtime changed' }
}).model
onCourseLoaded: ->
@campaign.set('_id', @course.get('campaignID'))
@campaign = @supermodel.loadModel(@campaign).model
onResourceLoadFailed: (e) ->
if e.resource.jqxhr is @nextLevelRequest
return
super(arguments...)
onAchievementsLoaded: ->
@ -135,7 +129,7 @@ module.exports = class CourseVictoryModal extends ModalView
level: @level
nextLevel: @nextLevel
course: @course
campaign: @campaign
classroom: @classroom
levelSessions: @levelSessions
})

View file

@ -13,8 +13,8 @@ module.exports = class ProgressView extends CocoView
initialize: (options) ->
@level = options.level
@course = options.course
@classroom = options.classroom
@nextLevel = options.nextLevel
@campaign = options.campaign
@levelSessions = options.levelSessions
onClickDoneButton: ->

View file

@ -0,0 +1,34 @@
load('bower_components/lodash/dist/lodash.js');
var courses = db.courses.find({}).sort({_id:1}).toArray();
var ids = _.pluck(courses, 'campaignID');
var campaigns = db.campaigns.find({_id: {$in: ids}}).toArray();
var campaignMap = {};
for (var campaignIndex in campaigns) {
var campaign = campaigns[campaignIndex];
campaignMap[campaign._id.str] = campaign;
}
var coursesData = [];
for (var courseIndex in courses) {
var course = courses[courseIndex];
var courseData = { _id: course._id, levels: [] };
var campaign = campaignMap[course.campaignID.str];
var levels = _.values(campaign.levels);
levels = _.sortBy(levels, 'campaignIndex');
_.forEach(levels, function(level) {
levelData = { original: ObjectId(level.original) };
_.extend(levelData, _.pick(level, 'type', 'slug', 'name'));
courseData.levels.push(levelData);
});
coursesData.push(courseData);
}
print('constructed', JSON.stringify(coursesData, null, '\t'));
db.classrooms.update(
{}, // Set all
//{courses: {$exists: false}}, // Set all w/out values
{$set: {courses: coursesData}},
{multi: true}
);

View file

@ -155,6 +155,8 @@ module.exports =
tv4 = require('tv4').tv4
result = tv4.validateMultiple(obj, doc.schema.statics.jsonSchema)
if not result.valid
prunedErrors = (_.omit(error, 'stack') for error in result.errors)
winston.debug('Validation errors: ', JSON.stringify(prunedErrors, null, '\t'))
throw new errors.UnprocessableEntity('JSON-schema validation failed', { validationErrors: result.errors })

View file

@ -25,12 +25,16 @@ module.exports =
campaign = yield database.getDocFromHandle(req, Campaign)
if not campaign
throw new errors.NotFound('Campaign not found.')
levelsBefore = _.keys(campaign.get('levels'))
hasPermission = req.user.isAdmin()
unless hasPermission or database.isJustFillingTranslations(req, campaign)
throw new errors.Forbidden('Must be an admin or submitting translations to edit a campaign')
database.assignBody(req, campaign)
database.validateDoc(campaign)
levelsAfter = _.keys(campaign.get('levels'))
if not _.isEqual(levelsBefore, levelsAfter)
campaign.set('levelsUpdated', new Date())
campaign = yield campaign.save()
res.status(200).send(campaign.toObject())
docLink = "http://codecombat.com#{req.headers['x-current-path']}"

View file

@ -6,6 +6,9 @@ Promise = require 'bluebird'
database = require '../commons/database'
mongoose = require 'mongoose'
Classroom = require '../models/Classroom'
Course = require '../models/Course'
Campaign = require '../models/Campaign'
Level = require '../models/Level'
parse = require '../commons/parse'
LevelSession = require '../models/LevelSession'
User = require '../models/User'
@ -28,6 +31,50 @@ module.exports =
classrooms = (classroom.toObject({req: req}) for classroom in classrooms)
res.status(200).send(classrooms)
fetchAllLevels: wrap (req, res, next) ->
classroom = yield database.getDocFromHandle(req, Classroom)
if not classroom
throw new errors.NotFound('Classroom not found.')
levelOriginals = []
for course in classroom.get('courses') or []
for level in course.levels
levelOriginals.push(level.original)
levels = yield Level.find({ original: { $in: levelOriginals }, slug: { $exists: true }}).select(parse.getProjectFromReq(req))
levels = (level.toObject({ req: req }) for level in levels)
# maintain course order
levelMap = {}
for level in levels
levelMap[level.original] = level
levels = (levelMap[levelOriginal.toString()] for levelOriginal in levelOriginals)
res.status(200).send(levels)
fetchLevelsForCourse: wrap (req, res) ->
classroom = yield database.getDocFromHandle(req, Classroom)
if not classroom
throw new errors.NotFound('Classroom not found.')
levelOriginals = []
for course in classroom.get('courses') or []
if course._id.toString() isnt req.params.courseID
continue
for level in course.levels
levelOriginals.push(level.original)
levels = yield Level.find({ original: { $in: levelOriginals }, slug: { $exists: true }}).select(parse.getProjectFromReq(req))
levels = (level.toObject({ req: req }) for level in levels)
# maintain course order
levelMap = {}
for level in levels
levelMap[level.original] = level
levels = (levelMap[levelOriginal.toString()] for levelOriginal in levelOriginals)
res.status(200).send(levels)
fetchMemberSessions: wrap (req, res, next) ->
throw new errors.Unauthorized() unless req.user
memberLimit = parse.getLimitFromReq(req, {default: 10, max: 100, param: 'memberLimit'})
@ -71,6 +118,26 @@ module.exports =
classroom.set 'ownerID', req.user._id
classroom.set 'members', []
database.assignBody(req, classroom)
# copy over data from how courses are right now
courses = yield Course.find()
campaigns = yield Campaign.find({_id: {$in: (course.get('campaignID') for course in courses)}})
campaignMap = {}
campaignMap[campaign.id] = campaign for campaign in campaigns
coursesData = []
for course in courses
courseData = { _id: course._id, levels: [] }
campaign = campaignMap[course.get('campaignID').toString()]
levels = _.values(campaign.get('levels'))
levels = _.sortBy(levels, 'campaignIndex')
for level in levels
levelData = { original: mongoose.Types.ObjectId(level.original) }
_.extend(levelData, _.pick(level, 'type', 'slug', 'name'))
courseData.levels.push(levelData)
coursesData.push(courseData)
classroom.set('courses', coursesData)
# finish
database.validateDoc(classroom)
classroom = yield classroom.save()
res.status(201).send(classroom.toObject({req: req}))

View file

@ -8,6 +8,8 @@ CourseInstance = require '../models/CourseInstance'
Classroom = require '../models/Classroom'
Course = require '../models/Course'
User = require '../models/User'
Level = require '../models/Level'
parse = require '../commons/parse'
module.exports =
addMembers: wrap (req, res) ->
@ -63,3 +65,61 @@ module.exports =
)
res.status(200).send(courseInstance.toObject({ req }))
fetchNextLevel: wrap (req, res) ->
levelOriginal = req.params.levelOriginal
if not database.isID(levelOriginal)
throw new errors.UnprocessableEntity('Invalid level original ObjectId')
courseInstance = yield database.getDocFromHandle(req, CourseInstance)
if not courseInstance
throw new errors.NotFound('Course Instance not found.')
courseID = courseInstance.get('courseID')
classroom = yield Classroom.findById courseInstance.get('classroomID')
if not classroom
throw new errors.NotFound('Classroom 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
if not foundLevelOriginal
throw new errors.NotFound('Level original ObjectId not found in Classroom courses')
if not nextLevelOriginal
throw new errors.NotFound('No more levels in that course')
dbq = Level.findOne({original: mongoose.Types.ObjectId(nextLevelOriginal)})
dbq.sort({ 'version.major': -1, 'version.minor': -1 })
dbq.select(parse.getProjectFromReq(req))
level = yield dbq
level = level.toObject({req: req})
res.status(200).send(level)
fetchClassroom: wrap (req, res) ->
courseInstance = yield database.getDocFromHandle(req, CourseInstance)
if not courseInstance
throw new errors.NotFound('Course Instance not found.')
classroom = yield Classroom.findById(courseInstance.get('classroomID')).select(parse.getProjectFromReq(req))
if not classroom
throw new errors.NotFound('Classroom not found.')
isOwner = classroom.get('ownerID')?.equals req.user?._id
isMember = _.any(classroom.get('members') or [], (memberID) -> memberID.equals(req.user.get('_id')))
if not (isOwner or isMember)
throw new errors.Forbidden('You do not have access to this classroom')
classroom = classroom.toObject({req: req})
res.status(200).send(classroom)

View file

@ -34,6 +34,11 @@ CampaignSchema.statics.updateAdjacentCampaigns = (savedCampaign) ->
Campaign.findByIdAndUpdate campaign._id, {$set: {adjacentCampaigns: acs}}, (err, doc) ->
return log.error "Couldn't save updated adjacent campaign because of #{err}" if err
CampaignSchema.pre 'save', (done) ->
if not @get('levelsUpdated')
@set('levelsUpdated', @_id.getTimestamp())
done()
CampaignSchema.post 'save', -> @constructor.updateAdjacentCampaigns @
CampaignSchema.statics.jsonSchema = jsonSchema

View file

@ -5,6 +5,7 @@ plugins = require '../plugins/plugins'
User = require './User'
jsonSchema = require '../../app/schemas/models/classroom.schema.coffee'
utils = require '../lib/utils'
co = require 'co'
ClassroomSchema = new mongoose.Schema {}, {strict: false, minimize: false, read:config.mongo.readpref}
@ -52,6 +53,9 @@ ClassroomSchema.statics.jsonSchema = jsonSchema
ClassroomSchema.set('toObject', {
transform: (doc, ret, options) ->
# TODO: Remove this once classrooms are populated. This is only for when we are testing locked course content.
if not ret.courses
ret.courses = coursesData
return ret unless options.req
user = options.req.user
unless user and (user.isAdmin() or user._id.equals(doc.get('ownerID')))
@ -61,3 +65,26 @@ ClassroomSchema.set('toObject', {
})
module.exports = Classroom = mongoose.model 'classroom', ClassroomSchema, 'classrooms'
coursesData = []
co ->
console.log 'Populating courses data...'
Course = require './Course'
Campaign = require './Campaign'
courses = yield Course.find()
campaigns = yield Campaign.find({_id: {$in: (course.get('campaignID') for course in courses)}})
campaignMap = {}
campaignMap[campaign.id] = campaign for campaign in campaigns
coursesData = []
for course in courses
courseData = { _id: course._id, levels: [] }
campaign = campaignMap[course.get('campaignID').toString()]
levels = _.values(campaign.get('levels'))
levels = _.sortBy(levels, 'campaignIndex')
for level in levels
levelData = { original: mongoose.Types.ObjectId(level.original) }
_.extend(levelData, _.pick(level, 'type', 'slug', 'name'))
courseData.levels.push(levelData)
coursesData.push(courseData)
console.log 'Populated courses data.'

View file

@ -46,6 +46,8 @@ module.exports.setup = (app) ->
app.post('/db/classroom', mw.classrooms.post)
app.get('/db/classroom', mw.classrooms.getByOwner)
app.get('/db/classroom/:handle/levels', mw.classrooms.fetchAllLevels)
app.get('/db/classroom/:handle/courses/:courseID/levels', mw.classrooms.fetchLevelsForCourse)
app.get('/db/classroom/:handle/member-sessions', mw.classrooms.fetchMemberSessions)
app.get('/db/classroom/:handle/members', mw.classrooms.fetchMembers) # TODO: Use mw.auth?
app.get('/db/classroom/:handle', mw.auth.checkLoggedIn()) # TODO: Finish migrating route, adding now so 401 is returned
@ -58,7 +60,9 @@ module.exports.setup = (app) ->
app.get('/db/course', mw.rest.get(Course))
app.get('/db/course/:handle', mw.rest.getByHandle(Course))
app.get('/db/course_instance/:handle/levels/:levelOriginal/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)
app.delete('/db/user/:handle', mw.users.removeFromClassrooms)
app.get('/db/user', mw.users.fetchByGPlusID, mw.users.fetchByFacebookID)

View file

@ -37,6 +37,7 @@ User = require '../../../server/models/User'
request = require '../request'
utils = require '../utils'
slack = require '../../../server/slack'
Promise = require 'bluebird'
describe 'PUT /db/campaign', ->
beforeEach utils.wrap (done) ->
@ -44,6 +45,7 @@ describe 'PUT /db/campaign', ->
admin = yield utils.initAdmin()
yield utils.loginUser(admin)
[res, body] = yield request.postAsync { uri: campaignURL, json: campaign }
@levelsUpdated = body.levelsUpdated
@campaign = yield Campaign.findById(body._id)
done()
@ -75,6 +77,16 @@ describe 'PUT /db/campaign', ->
[res, body] = yield request.putAsync { uri: campaignURL+'/'+@campaign.id, json: { name: 'A new name' } }
expect(slack.sendSlackMessage).toHaveBeenCalled()
done()
it 'sets campaign.levelsUpdated to now iff levels are changed', utils.wrap (done) ->
data = {name: 'whatever'}
[res, body] = yield request.putAsync { uri: campaignURL+'/'+@campaign.id, json: data }
expect(body.levelsUpdated).toBe(@levelsUpdated)
yield new Promise((resolve) -> setTimeout(resolve, 10))
data = {levels: {'a': {original: 'a'}}}
[res, body] = yield request.putAsync { uri: campaignURL+'/'+@campaign.id, json: data }
expect(body.levelsUpdated).not.toBe(@levelsUpdated)
done()
describe '/db/campaign', ->
it 'prepares the db first', (done) ->

View file

@ -8,6 +8,8 @@ request = require '../request'
requestAsync = Promise.promisify(request, {multiArgs: true})
User = require '../../../server/models/User'
Classroom = require '../../../server/models/Classroom'
Course = require '../../../server/models/Course'
Campaign = require '../../../server/models/Campaign'
LevelSession = require '../../../server/models/LevelSession'
Level = require '../../../server/models/Level'
@ -60,7 +62,28 @@ describe 'GET /db/classroom/:id', ->
describe 'POST /db/classroom', ->
beforeEach utils.wrap (done) ->
yield utils.clearModels [User, Classroom]
yield utils.clearModels [User, Classroom, Course, Level, Campaign]
admin = yield utils.initAdmin()
yield utils.loginUser(admin)
levelJSONA = { name: 'Level A', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSONA})
expect(res.statusCode).toBe(200)
@levelA = yield Level.findById(res.body._id)
levelJSONB = { name: 'Level B', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSONB})
expect(res.statusCode).toBe(200)
@levelB = yield Level.findById(res.body._id)
campaignJSON = { name: 'Campaign', levels: {} }
paredLevelB = _.pick(@levelB.toObject(), 'name', 'original', 'type', 'slug')
paredLevelB.campaignIndex = 1
campaignJSON.levels[@levelB.get('original').toString()] = paredLevelB
paredLevelA = _.pick(@levelA.toObject(), 'name', 'original', 'type', 'slug')
paredLevelA.campaignIndex = 0
campaignJSON.levels[@levelA.get('original').toString()] = paredLevelA
[res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSON})
@campaign = yield Campaign.findById(res.body._id)
@course = Course({name: 'Course', campaignID: @campaign._id})
yield @course.save()
done()
it 'creates a new classroom for the given user with teacher role', utils.wrap (done) ->
@ -75,6 +98,7 @@ describe 'POST /db/classroom', ->
done()
it 'returns 401 for anonymous users', utils.wrap (done) ->
yield utils.logout()
data = { name: 'Classroom 2' }
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
expect(res.statusCode).toBe(401)
@ -87,8 +111,116 @@ describe 'POST /db/classroom', ->
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
expect(res.statusCode).toBe(403)
done()
it 'makes a copy of the list of all levels in all courses', utils.wrap (done) ->
teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(teacher)
data = { name: 'Classroom 2' }
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
classroom = yield Classroom.findById(res.body._id)
expect(classroom.get('courses')[0].levels[0].original.toString()).toBe(@levelA.get('original').toString())
expect(classroom.get('courses')[0].levels[0].type).toBe('course')
expect(classroom.get('courses')[0].levels[0].slug).toBe('level-a')
expect(classroom.get('courses')[0].levels[0].name).toBe('Level A')
done()
describe 'GET /db/classroom/:handle/levels', ->
beforeEach utils.wrap (done) ->
yield utils.clearModels [User, Classroom, Course, Level, Campaign]
admin = yield utils.initAdmin()
yield utils.loginUser(admin)
levelJSON = { name: 'King\'s Peak 3', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
expect(res.statusCode).toBe(200)
@level = yield Level.findById(res.body._id)
campaignJSON = { name: 'Campaign', levels: {} }
paredLevel = _.pick(res.body, 'name', 'original', 'type')
campaignJSON.levels[res.body.original] = paredLevel
[res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSON})
@campaign = yield Campaign.findById(res.body._id)
@course = Course({name: 'Course', campaignID: @campaign._id})
yield @course.save()
teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(teacher)
data = { name: 'Classroom 1' }
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
expect(res.statusCode).toBe(201)
@classroom = yield Classroom.findById(res.body._id)
done()
it 'returns all levels referenced in in the classroom\'s copy of course levels', utils.wrap (done) ->
[res, body] = yield request.getAsync { uri: getURL("/db/classroom/#{@classroom.id}/levels"), json: true }
expect(res.statusCode).toBe(200)
levels = res.body
expect(levels.length).toBe(1)
expect(levels[0].name).toBe("King's Peak 3")
done()
describe 'GET /db/classroom/:handle/levels', ->
beforeEach utils.wrap (done) ->
yield utils.clearModels [User, Classroom, Course, Level, Campaign]
admin = yield utils.initAdmin()
yield utils.loginUser(admin)
levelJSON = { name: 'A', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
expect(res.statusCode).toBe(200)
@levelA = yield Level.findById(res.body._id)
paredLevelA = _.pick(res.body, 'name', 'original', 'type')
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')
campaignJSONA = { name: 'Campaign A', levels: {} }
campaignJSONA.levels[paredLevelA.original] = paredLevelA
[res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSONA})
@campaignA = yield Campaign.findById(res.body._id)
campaignJSONB = { name: 'Campaign B', levels: {} }
campaignJSONB.levels[paredLevelB.original] = paredLevelB
[res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSONB})
@campaignB = yield Campaign.findById(res.body._id)
@courseA = Course({name: 'Course A', campaignID: @campaignA._id})
yield @courseA.save()
@courseB = Course({name: 'Course B', campaignID: @campaignB._id})
yield @courseB.save()
teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(teacher)
data = { name: 'Classroom 1' }
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
expect(res.statusCode).toBe(201)
@classroom = yield Classroom.findById(res.body._id)
done()
it 'returns all levels referenced in in the classroom\'s copy of course levels', utils.wrap (done) ->
[res, body] = yield request.getAsync { uri: getURL("/db/classroom/#{@classroom.id}/levels"), json: true }
expect(res.statusCode).toBe(200)
levels = res.body
expect(levels.length).toBe(2)
[res, body] = yield request.getAsync { uri: getURL("/db/classroom/#{@classroom.id}/courses/#{@courseA.id}/levels"), json: true }
expect(res.statusCode).toBe(200)
levels = res.body
expect(levels.length).toBe(1)
expect(levels[0].original).toBe(@levelA.get('original').toString())
[res, body] = yield request.getAsync { uri: getURL("/db/classroom/#{@classroom.id}/courses/#{@courseB.id}/levels"), json: true }
expect(res.statusCode).toBe(200)
levels = res.body
expect(levels.length).toBe(1)
expect(levels[0].original).toBe(@levelB.get('original').toString())
done()
describe 'PUT /db/classroom', ->
it 'clears database users and classrooms', (done) ->

View file

@ -7,6 +7,8 @@ CourseInstance = require '../../../server/models/CourseInstance'
Course = require '../../../server/models/Course'
User = require '../../../server/models/User'
Classroom = require '../../../server/models/Classroom'
Campaign = require '../../../server/models/Campaign'
Level = require '../../../server/models/Level'
Prepaid = require '../../../server/models/Prepaid'
request = require '../request'
@ -241,4 +243,124 @@ describe 'DELETE /db/course_instance/:id/members', ->
expect(res.body.members.length).toBe(0)
user = yield User.findById(@student.id)
expect(_.size(user.get('courseInstances'))).toBe(0)
done()
describe 'GET /db/course_instance/:handle/levels/:levelOriginal/next', ->
beforeEach utils.wrap (done) ->
yield utils.clearModels [User, Classroom, Course, Level, Campaign]
admin = yield utils.initAdmin()
yield utils.loginUser(admin)
levelJSON = { name: 'A', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
expect(res.statusCode).toBe(200)
@levelA = yield Level.findById(res.body._id)
paredLevelA = _.pick(res.body, 'name', 'original', 'type')
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')
levelJSON = { name: 'C', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
expect(res.statusCode).toBe(200)
@levelC = yield Level.findById(res.body._id)
paredLevelC = _.pick(res.body, 'name', 'original', 'type')
campaignJSONA = { name: 'Campaign A', levels: {} }
campaignJSONA.levels[paredLevelA.original] = paredLevelA
campaignJSONA.levels[paredLevelB.original] = paredLevelB
[res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSONA})
@campaignA = yield Campaign.findById(res.body._id)
campaignJSONB = { name: 'Campaign B', levels: {} }
campaignJSONB.levels[paredLevelC.original] = paredLevelC
[res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSONB})
@campaignB = yield Campaign.findById(res.body._id)
@courseA = Course({name: 'Course A', campaignID: @campaignA._id})
yield @courseA.save()
@courseB = Course({name: 'Course B', campaignID: @campaignB._id})
yield @courseB.save()
teacher = yield utils.initUser({role: 'teacher'})
yield utils.loginUser(teacher)
data = { name: 'Classroom 1' }
classroomsURL = getURL('/db/classroom')
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
expect(res.statusCode).toBe(201)
@classroom = yield Classroom.findById(res.body._id)
url = getURL('/db/course_instance')
dataA = { name: 'Some Name', courseID: @courseA.id, classroomID: @classroom.id }
[res, body] = yield request.postAsync {uri: url, json: dataA}
expect(res.statusCode).toBe(200)
@courseInstanceA = yield CourseInstance.findById(res.body._id)
dataB = { name: 'Some Other Name', courseID: @courseB.id, classroomID: @classroom.id }
[res, body] = yield request.postAsync {uri: url, json: dataB}
expect(res.statusCode).toBe(200)
@courseInstanceB = yield CourseInstance.findById(res.body._id)
done()
it 'returns the next level for the course in the linked classroom', utils.wrap (done) ->
[res, body] = yield request.getAsync { uri: utils.getURL("/db/course_instance/#{@courseInstanceA.id}/levels/#{@levelA.id}/next"), json: true }
expect(res.statusCode).toBe(200)
expect(res.body.original).toBe(@levelB.original.toString())
done()
it 'returns 404 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 }
expect(res.statusCode).toBe(404)
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 }
expect(res.statusCode).toBe(404)
done()
describe 'GET /db/course_instance/:handle/classroom', ->
beforeEach utils.wrap (done) ->
yield utils.clearModels [User, CourseInstance, Classroom]
@owner = yield utils.initUser()
yield @owner.save()
@member = yield utils.initUser()
yield @member.save()
@classroom = new Classroom({
ownerID: @owner._id
members: [@member._id]
})
yield @classroom.save()
@courseInstance = new CourseInstance({classroomID: @classroom._id})
yield @courseInstance.save()
@url = getURL("/db/course_instance/#{@courseInstance.id}/classroom")
done()
it 'returns the course instance\'s referenced classroom', utils.wrap (done) ->
yield utils.loginUser @owner
[res, body] = yield request.getAsync(@url, {json: true})
expect(res.statusCode).toBe(200)
expect(body.code).toBeDefined()
done()
it 'works if you are the owner or member', utils.wrap (done) ->
yield utils.loginUser @member
[res, body] = yield request.getAsync(@url, {json: true})
expect(res.statusCode).toBe(200)
expect(body.code).toBeUndefined()
done()
it 'does not work if you are not the owner or a member', utils.wrap (done) ->
@user = yield utils.initUser()
yield utils.loginUser @user
[res, body] = yield request.getAsync(@url, {json: true})
expect(res.statusCode).toBe(403)
done()

View file

@ -13,5 +13,36 @@ module.exports = new Classroom(
ownerID: "teacher0",
aceConfig:
language: 'python'
courses: [
{
_id: "course0",
levels: [
{
original: 'level0_0'
name: 'level0_0'
type: 'hero'
},
{
original: 'level0_1'
name: 'level0_1'
type: 'hero'
},
{
original: 'level0_2'
name: 'level0_2'
type: 'hero'
},
{
original: 'level0_3'
name: 'level0_3'
type: 'hero'
},
]
},
{
_id: "course1",
levels: []
},
]
}
)

View file

@ -27,7 +27,7 @@ describe 'CoursesHelper', ->
describe 'progressData.get({classroom, course})', ->
it 'returns object with .completed=true and .started=true', ->
progressData = helper.calculateAllProgress(@classrooms, @courses, @campaigns, @courseInstances, @students)
progressData = helper.calculateAllProgress(@classrooms, @courses, @courseInstances, @students)
progress = progressData.get {@classroom, @course}
expect(progress.completed).toBe true
expect(progress.started).toBe true
@ -35,14 +35,14 @@ describe 'CoursesHelper', ->
describe 'progressData.get({classroom, course, level, user})', ->
it 'returns object with .completed=true and .started=true', ->
for student in @students.models
progressData = helper.calculateAllProgress(@classrooms, @courses, @campaigns, @courseInstances, @students)
progressData = helper.calculateAllProgress(@classrooms, @courses, @courseInstances, @students)
progress = progressData.get {@classroom, @course, user: student}
expect(progress.completed).toBe true
expect(progress.started).toBe true
describe 'progressData.get({classroom, course, level, user})', ->
it 'returns object with .completed=true and .started=true', ->
progressData = helper.calculateAllProgress(@classrooms, @courses, @campaigns, @courseInstances, @students)
progressData = helper.calculateAllProgress(@classrooms, @courses, @courseInstances, @students)
for level in @campaign.getLevels().models
progress = progressData.get {@classroom, @course, level}
expect(progress.completed).toBe true
@ -50,7 +50,7 @@ describe 'CoursesHelper', ->
describe 'progressData.get({classroom, course, level, user})', ->
it 'returns object with .completed=true and .started=true', ->
progressData = helper.calculateAllProgress(@classrooms, @courses, @campaigns, @courseInstances, @students)
progressData = helper.calculateAllProgress(@classrooms, @courses, @courseInstances, @students)
for level in @campaign.getLevels().models
for user in @students.models
progress = progressData.get {@classroom, @course, level, user}
@ -64,20 +64,20 @@ describe 'CoursesHelper', ->
@courseInstances = require 'test/app/fixtures/course-instances'
it 'progressData.get({classroom, course}) returns object with .completed=false', ->
progressData = helper.calculateAllProgress(@classrooms, @courses, @campaigns, @courseInstances, @students)
progressData = helper.calculateAllProgress(@classrooms, @courses, @courseInstances, @students)
progress = progressData.get {@classroom, @course}
expect(progress.completed).toBe false
describe 'when NOT all students have completed a level', ->
it 'progressData.get({classroom, course, level}) returns object with .completed=false and .started=true', ->
progressData = helper.calculateAllProgress(@classrooms, @courses, @campaigns, @courseInstances, @students)
progressData = helper.calculateAllProgress(@classrooms, @courses, @courseInstances, @students)
for level in @campaign.getLevels().models
progress = progressData.get {@classroom, @course, level}
expect(progress.completed).toBe false
describe 'when the student has completed the course', ->
it 'progressData.get({classroom, course, user}) returns object with .completed=true and .started=true', ->
progressData = helper.calculateAllProgress(@classrooms, @courses, @campaigns, @courseInstances, @students)
progressData = helper.calculateAllProgress(@classrooms, @courses, @courseInstances, @students)
student = @students.get('student0')
progress = progressData.get {@classroom, @course, user: student}
expect(progress.completed).toBe true
@ -85,7 +85,7 @@ describe 'CoursesHelper', ->
describe 'when the student has NOT completed the course', ->
it 'progressData.get({classroom, course, user}) returns object with .completed=false and .started=true', ->
progressData = helper.calculateAllProgress(@classrooms, @courses, @campaigns, @courseInstances, @students)
progressData = helper.calculateAllProgress(@classrooms, @courses, @courseInstances, @students)
student = @students.get('student1')
progress = progressData.get {@classroom, @course, user: student}
expect(progress.completed).toBe false
@ -93,7 +93,7 @@ describe 'CoursesHelper', ->
describe 'when the student has completed the level', ->
it 'progressData.get({classroom, course, level, user}) returns object with .completed=true and .started=true', ->
progressData = helper.calculateAllProgress(@classrooms, @courses, @campaigns, @courseInstances, @students)
progressData = helper.calculateAllProgress(@classrooms, @courses, @courseInstances, @students)
student = @students.get('student0')
for level in @campaign.getLevels().models
progress = progressData.get {@classroom, @course, level, user: student}
@ -102,7 +102,7 @@ describe 'CoursesHelper', ->
describe 'when the student has NOT completed the level but has started', ->
it 'progressData.get({classroom, course, level, user}) returns object with .completed=true and .started=true', ->
progressData = helper.calculateAllProgress(@classrooms, @courses, @campaigns, @courseInstances, @students)
progressData = helper.calculateAllProgress(@classrooms, @courses, @courseInstances, @students)
user = @students.get('student2')
level = @campaign.getLevels().get('level0_0')
progress = progressData.get {@classroom, @course, level, user}
@ -111,7 +111,7 @@ describe 'CoursesHelper', ->
describe 'when the student has NOT started the level', ->
it 'progressData.get({classroom, course, level, user}) returns object with .completed=false and .started=false', ->
progressData = helper.calculateAllProgress(@classrooms, @courses, @campaigns, @courseInstances, @students)
progressData = helper.calculateAllProgress(@classrooms, @courses, @courseInstances, @students)
user = @students.get('student3')
level = @campaign.getLevels().get('level0_0')
progress = progressData.get {@classroom, @course, level, user}

View file

@ -24,6 +24,8 @@ describe 'CourseVictoryModal', ->
courseInstanceID: '56414c3868785b5f152424f1'
courseID: '560f1a9f22961295f9427742'
}
nextLevelRequest = null
handleRequests = ->
requests = jasmine.Ajax.requests.all()
@ -33,12 +35,14 @@ describe 'CourseVictoryModal', ->
earnedAchievementRequests = _.where(requests, {url: '/db/earned_achievement'})
for [request, response] in _.zip(earnedAchievementRequests, fixtures.earnedAchievements)
request.respondWith({status: 200, responseText: JSON.stringify(response)})
sessionsRequest = _.find(requests, (r) -> _.string.startsWith(r.url, '/db/course_instance'))
sessionsRequest = _.findWhere(requests, {url: '/db/course_instance/56414c3868785b5f152424f1/my-course-level-sessions'})
sessionsRequest.respondWith({status: 200, responseText: JSON.stringify(fixtures.courseInstanceSessions)})
campaignRequest = _.findWhere(requests, {url: '/db/campaign/55b29efd1cd6abe8ce07db0d'})
campaignRequest.respondWith({status: 200, responseText: JSON.stringify(fixtures.campaign)})
classroomRequest = _.findWhere(requests, {url: '/db/course_instance/56414c3868785b5f152424f1/classroom'})
classroomRequest.respondWith({status: 200, responseText: JSON.stringify(fixtures.campaign)}) # TODO: Fix this...
nextLevelRequest = _.findWhere(requests, {url: '/db/course_instance/56414c3868785b5f152424f1/levels/54173c90844506ae0195a0b4/next'})
describe 'given a course level with a next level and no item or hero rewards', ->
modal = null
@ -47,6 +51,7 @@ describe 'CourseVictoryModal', ->
options = makeViewOptions()
modal = new CourseVictoryModal(options)
handleRequests()
nextLevelRequest.respondWith({status: 200, responseText: JSON.stringify(fixtures.nextLevel)})
_.defer done
it 'only shows the ProgressView', ->
@ -80,6 +85,7 @@ describe 'CourseVictoryModal', ->
delete options.nextLevel
modal = new CourseVictoryModal(options)
handleRequests()
nextLevelRequest.respondWith({status: 404, responseText: '{}'})
_.defer done
describe 'its ProgressView', ->
@ -112,6 +118,7 @@ describe 'CourseVictoryModal', ->
modal = new CourseVictoryModal(options)
handleRequests()
nextLevelRequest.respondWith({status: 200, responseText: JSON.stringify(fixtures.nextLevel)})
_.defer done
it 'includes a NewItemView when the level rewards a new item', ->

View file

@ -10,8 +10,6 @@ describe 'TeacherClassView', ->
# it 'responds with 401 error'
# it 'shows Log In and Create Account buttons'
@view = null
# describe "when you don't own the class", ->
# it 'responds with 403 error'
# it 'shows Log Out button'
@ -22,14 +20,12 @@ describe 'TeacherClassView', ->
@classroom = require 'test/app/fixtures/classrooms/active-classroom'
@students = require 'test/app/fixtures/students'
@courses = require 'test/app/fixtures/courses'
@campaigns = require 'test/app/fixtures/campaigns'
@courseInstances = require 'test/app/fixtures/course-instances'
@levelSessions = require 'test/app/fixtures/level-sessions-partially-completed'
@view = new TeacherClassView()
@view.classroom.fakeRequests.forEach (r, index) => r.respondWith({ status: 200, responseText: JSON.stringify(@classroom) })
@view.courses.fakeRequests.forEach (r, index) => r.respondWith({ status: 200, responseText: JSON.stringify(@courses) })
@view.campaigns.fakeRequests.forEach (r, index) => r.respondWith({ status: 200, responseText: JSON.stringify(@campaigns) })
@view.courseInstances.fakeRequests.forEach (r, index) => r.respondWith({ status: 200, responseText: JSON.stringify(@courseInstances) })
@view.students.fakeRequests.forEach (r, index) => r.respondWith({ status: 200, responseText: JSON.stringify(@students) })
@view.classroom.sessions.fakeRequests.forEach (r, index) => r.respondWith({ status: 200, responseText: JSON.stringify(@levelSessions) })