diff --git a/app/collections/CourseInstances.coffee b/app/collections/CourseInstances.coffee index 7c44900be..242c39362 100644 --- a/app/collections/CourseInstances.coffee +++ b/app/collections/CourseInstances.coffee @@ -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) \ No newline at end of file diff --git a/app/collections/LevelSessions.coffee b/app/collections/LevelSessions.coffee index 20de80e42..dbbc343a5 100644 --- a/app/collections/LevelSessions.coffee +++ b/app/collections/LevelSessions.coffee @@ -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" diff --git a/app/collections/Levels.coffee b/app/collections/Levels.coffee index 5448f9e7e..0d87816ad 100644 --- a/app/collections/Levels.coffee +++ b/app/collections/Levels.coffee @@ -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) + \ No newline at end of file diff --git a/app/core/Router.coffee b/app/core/Router.coffee index 56ec24230..c63d26e8f 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -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') diff --git a/app/core/deltas.coffee b/app/core/deltas.coffee index 3b81c69ed..5ecb8cfff 100644 --- a/app/core/deltas.coffee +++ b/app/core/deltas.coffee @@ -178,4 +178,5 @@ prunePath = (delta, path) -> module.exports.DOC_SKIP_PATHS = [ '_id','version', 'commitMessage', 'parent', 'created', - 'slug', 'index', '__v', 'patches', 'creator', 'js', 'watchers'] \ No newline at end of file + 'slug', 'index', '__v', 'patches', 'creator', 'js', 'watchers', 'levelsUpdated' +] \ No newline at end of file diff --git a/app/lib/coursesHelper.coffee b/app/lib/coursesHelper.coffee index 999cbcfa8..cc7a56274 100644 --- a/app/lib/coursesHelper.coffee +++ b/app/lib/coursesHelper.coffee @@ -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 } diff --git a/app/locale/en.coffee b/app/locale/en.coffee index fbdc33837..f475ee041 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -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 diff --git a/app/models/Campaign.coffee b/app/models/Campaign.coffee index 7d76e9279..495ca7ac5 100644 --- a/app/models/Campaign.coffee +++ b/app/models/Campaign.coffee @@ -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'))) diff --git a/app/models/Classroom.coffee b/app/models/Classroom.coffee index 7a0537d94..8d3b86646 100644 --- a/app/models/Classroom.coffee +++ b/app/models/Classroom.coffee @@ -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) \ No newline at end of file diff --git a/app/models/Level.coffee b/app/models/Level.coffee index ff0783cab..f3b6e4914 100644 --- a/app/models/Level.coffee +++ b/app/models/Level.coffee @@ -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) \ No newline at end of file diff --git a/app/models/SuperModel.coffee b/app/models/SuperModel.coffee index 3a2c28767..aca0021bd 100644 --- a/app/models/SuperModel.coffee +++ b/app/models/SuperModel.coffee @@ -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 diff --git a/app/schemas/models/campaign.schema.coffee b/app/schemas/models/campaign.schema.coffee index e8da4d884..770a2f81b 100644 --- a/app/schemas/models/campaign.schema.coffee +++ b/app/schemas/models/campaign.schema.coffee @@ -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' diff --git a/app/schemas/models/classroom.schema.coffee b/app/schemas/models/classroom.schema.coffee index b5a29560c..9c6a7fe4c 100644 --- a/app/schemas/models/classroom.schema.coffee +++ b/app/schemas/models/classroom.schema.coffee @@ -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' diff --git a/app/templates/courses/classroom-level-popover.jade b/app/templates/courses/classroom-level-popover.jade index f80389ad6..d2244913a 100644 --- a/app/templates/courses/classroom-level-popover.jade +++ b/app/templates/courses/classroom-level-popover.jade @@ -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") diff --git a/app/templates/courses/classroom-view.jade b/app/templates/courses/classroom-view.jade index acbdd7e88..288d45ba4 100644 --- a/app/templates/courses/classroom-view.jade +++ b/app/templates/courses/classroom-view.jade @@ -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 diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade index 5df87a69a..2bb9c549d 100644 --- a/app/templates/courses/course-details.jade +++ b/app/templates/courses/course-details.jade @@ -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 diff --git a/app/templates/courses/courses-view.jade b/app/templates/courses/courses-view.jade index bfb028bb8..33e3103a1 100644 --- a/app/templates/courses/courses-view.jade +++ b/app/templates/courses/courses-view.jade @@ -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") diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade index 79467dc24..752724453 100644 --- a/app/templates/courses/teacher-class-view.jade +++ b/app/templates/courses/teacher-class-view.jade @@ -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 diff --git a/app/templates/courses/teacher-classes-view.jade b/app/templates/courses/teacher-classes-view.jade index c520c4fd6..7b6bafdd2 100644 --- a/app/templates/courses/teacher-classes-view.jade +++ b/app/templates/courses/teacher-classes-view.jade @@ -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)) diff --git a/app/templates/courses/teacher-courses-view.jade b/app/templates/courses/teacher-courses-view.jade index 853b99681..6cdb3289d 100644 --- a/app/templates/courses/teacher-courses-view.jade +++ b/app/templates/courses/teacher-courses-view.jade @@ -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') diff --git a/app/templates/play/level/modal/progress-view.jade b/app/templates/play/level/modal/progress-view.jade index 916962fa4..c5f56051c 100644 --- a/app/templates/play/level/modal/progress-view.jade +++ b/app/templates/play/level/modal/progress-view.jade @@ -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') diff --git a/app/views/NewHomeView.coffee b/app/views/NewHomeView.coffee index a045f1578..4a98d6c73 100644 --- a/app/views/NewHomeView.coffee +++ b/app/views/NewHomeView.coffee @@ -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() diff --git a/app/views/courses/ClassroomView.coffee b/app/views/courses/ClassroomView.coffee index 45d19cdcf..b16e9ec6c 100644 --- a/app/views/courses/ClassroomView.coffee +++ b/app/views/courses/ClassroomView.coffee @@ -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" diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee index 92dbfe237..12ba1d63f 100644 --- a/app/views/courses/CourseDetailsView.coffee +++ b/app/views/courses/CourseDetailsView.coffee @@ -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 diff --git a/app/views/courses/CoursesView.coffee b/app/views/courses/CoursesView.coffee index 68a55f59a..dcd6bc242 100644 --- a/app/views/courses/CoursesView.coffee +++ b/app/views/courses/CoursesView.coffee @@ -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() diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee index fe6fe93a3..c17a1b24c 100644 --- a/app/views/courses/TeacherClassView.coffee +++ b/app/views/courses/TeacherClassView.coffee @@ -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() diff --git a/app/views/courses/TeacherClassesView.coffee b/app/views/courses/TeacherClassesView.coffee index 539549d6f..e969d12e0 100644 --- a/app/views/courses/TeacherClassesView.coffee +++ b/app/views/courses/TeacherClassesView.coffee @@ -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) -> diff --git a/app/views/play/level/modal/CourseVictoryModal.coffee b/app/views/play/level/modal/CourseVictoryModal.coffee index e3d705ed8..47b599207 100644 --- a/app/views/play/level/modal/CourseVictoryModal.coffee +++ b/app/views/play/level/modal/CourseVictoryModal.coffee @@ -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 }) diff --git a/app/views/play/level/modal/ProgressView.coffee b/app/views/play/level/modal/ProgressView.coffee index ba0e5e1c7..14afaf48f 100644 --- a/app/views/play/level/modal/ProgressView.coffee +++ b/app/views/play/level/modal/ProgressView.coffee @@ -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: -> diff --git a/scripts/mongodb/migrations/2016-04-14-populate-classrooms-with-levels.js b/scripts/mongodb/migrations/2016-04-14-populate-classrooms-with-levels.js new file mode 100644 index 000000000..83855682c --- /dev/null +++ b/scripts/mongodb/migrations/2016-04-14-populate-classrooms-with-levels.js @@ -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} +); \ No newline at end of file diff --git a/server/commons/database.coffee b/server/commons/database.coffee index e1dd132d1..41027df82 100644 --- a/server/commons/database.coffee +++ b/server/commons/database.coffee @@ -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 }) diff --git a/server/middleware/campaigns.coffee b/server/middleware/campaigns.coffee index 754966576..df1cbbe5e 100644 --- a/server/middleware/campaigns.coffee +++ b/server/middleware/campaigns.coffee @@ -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']}" diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee index ef6ad0ec6..77d1b8a3a 100644 --- a/server/middleware/classrooms.coffee +++ b/server/middleware/classrooms.coffee @@ -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})) \ No newline at end of file diff --git a/server/middleware/course-instances.coffee b/server/middleware/course-instances.coffee index 7defb5530..aa6b9b4b9 100644 --- a/server/middleware/course-instances.coffee +++ b/server/middleware/course-instances.coffee @@ -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) \ No newline at end of file diff --git a/server/models/Campaign.coffee b/server/models/Campaign.coffee index 0fc68d2bd..a5a8f0998 100644 --- a/server/models/Campaign.coffee +++ b/server/models/Campaign.coffee @@ -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 diff --git a/server/models/Classroom.coffee b/server/models/Classroom.coffee index c49533273..26edcb769 100644 --- a/server/models/Classroom.coffee +++ b/server/models/Classroom.coffee @@ -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.' \ No newline at end of file diff --git a/server/routes/index.coffee b/server/routes/index.coffee index dc318cdc7..d4c920102 100644 --- a/server/routes/index.coffee +++ b/server/routes/index.coffee @@ -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) diff --git a/spec/server/functional/campaign_handler.spec.coffee b/spec/server/functional/campaign_handler.spec.coffee index 940cd5f86..f2bfaf482 100644 --- a/spec/server/functional/campaign_handler.spec.coffee +++ b/spec/server/functional/campaign_handler.spec.coffee @@ -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) -> diff --git a/spec/server/functional/classrooms.spec.coffee b/spec/server/functional/classrooms.spec.coffee index e8f10dc31..b047fbbb3 100644 --- a/spec/server/functional/classrooms.spec.coffee +++ b/spec/server/functional/classrooms.spec.coffee @@ -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) -> diff --git a/spec/server/functional/course_instance.spec.coffee b/spec/server/functional/course_instance.spec.coffee index dc31f1252..9395f6866 100644 --- a/spec/server/functional/course_instance.spec.coffee +++ b/spec/server/functional/course_instance.spec.coffee @@ -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() \ No newline at end of file diff --git a/test/app/fixtures/classrooms/active-classroom.coffee b/test/app/fixtures/classrooms/active-classroom.coffee index 326391c32..d53f5ca80 100644 --- a/test/app/fixtures/classrooms/active-classroom.coffee +++ b/test/app/fixtures/classrooms/active-classroom.coffee @@ -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: [] + }, + ] } ) diff --git a/test/app/lib/CoursesHelper.spec.coffee b/test/app/lib/CoursesHelper.spec.coffee index ece911977..e228b9365 100644 --- a/test/app/lib/CoursesHelper.spec.coffee +++ b/test/app/lib/CoursesHelper.spec.coffee @@ -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} diff --git a/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee b/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee index 3e0d30696..cf5b5ed22 100644 --- a/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee +++ b/test/app/views/play/level/modal/CourseVictoryModal.spec.coffee @@ -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', -> diff --git a/test/app/views/teachers/TeacherClassView.spec.coffee b/test/app/views/teachers/TeacherClassView.spec.coffee index e20b749ac..9244b7a3c 100644 --- a/test/app/views/teachers/TeacherClassView.spec.coffee +++ b/test/app/views/teachers/TeacherClassView.spec.coffee @@ -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) })