mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-23 15:48:11 -05:00
Lock course content to classrooms
This commit is contained in:
parent
c9ed76471a
commit
675e3290ac
44 changed files with 857 additions and 513 deletions
|
@ -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)
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
@ -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')
|
||||
|
|
|
@ -178,4 +178,5 @@ prunePath = (delta, path) ->
|
|||
|
||||
module.exports.DOC_SKIP_PATHS = [
|
||||
'_id','version', 'commitMessage', 'parent', 'created',
|
||||
'slug', 'index', '__v', 'patches', 'creator', 'js', 'watchers']
|
||||
'slug', 'index', '__v', 'patches', 'creator', 'js', 'watchers', 'levelsUpdated'
|
||||
]
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')))
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
||||
|
|
|
@ -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: ->
|
||||
|
|
|
@ -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}
|
||||
);
|
|
@ -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 })
|
||||
|
||||
|
||||
|
|
|
@ -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']}"
|
||||
|
|
|
@ -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}))
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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.'
|
|
@ -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)
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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()
|
|
@ -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: []
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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', ->
|
||||
|
|
|
@ -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) })
|
||||
|
|
Loading…
Reference in a new issue