mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
Implement all of teacher-dashboard
This commit is contained in:
parent
bd3a77da9f
commit
4a72ffc185
85 changed files with 3480 additions and 509 deletions
12
app/collections/Campaigns.coffee
Normal file
12
app/collections/Campaigns.coffee
Normal file
|
@ -0,0 +1,12 @@
|
|||
Campaign = require 'models/Campaign'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
|
||||
module.exports = class Campaigns extends CocoCollection
|
||||
model: Campaign
|
||||
url: '/db/campaign'
|
||||
|
||||
fetchByType: (type, options={}) ->
|
||||
options.data ?= {}
|
||||
options.data.type = type
|
||||
@fetch(options)
|
||||
|
17
app/collections/Classrooms.coffee
Normal file
17
app/collections/Classrooms.coffee
Normal file
|
@ -0,0 +1,17 @@
|
|||
Classroom = require 'models/Classroom'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
|
||||
module.exports = class Classrooms extends CocoCollection
|
||||
model: Classroom
|
||||
url: '/db/classroom'
|
||||
|
||||
initialize: ->
|
||||
@on 'sync', =>
|
||||
for classroom in @models
|
||||
classroom.capitalizeLanguageName()
|
||||
super(arguments...)
|
||||
|
||||
fetchMine: (options={}) ->
|
||||
options.data ?= {}
|
||||
options.data.ownerID = me.id
|
||||
@fetch(options)
|
12
app/collections/CourseInstances.coffee
Normal file
12
app/collections/CourseInstances.coffee
Normal file
|
@ -0,0 +1,12 @@
|
|||
CourseInstance = require 'models/CourseInstance'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
|
||||
module.exports = class CourseInstances extends CocoCollection
|
||||
model: CourseInstance
|
||||
url: '/db/course_instance'
|
||||
|
||||
fetchByOwner: (ownerID, options={}) ->
|
||||
ownerID = ownerID.id or ownerID # handle if they pass in a user
|
||||
options.data ?= {}
|
||||
options.data.ownerID = ownerID
|
||||
@fetch(options)
|
6
app/collections/Courses.coffee
Normal file
6
app/collections/Courses.coffee
Normal file
|
@ -0,0 +1,6 @@
|
|||
Course = require 'models/Course'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
|
||||
module.exports = class Courses extends CocoCollection
|
||||
model: Course
|
||||
url: '/db/course'
|
|
@ -10,3 +10,25 @@ module.exports = class LevelSessionCollection extends CocoCollection
|
|||
url: "/db/course_instance/#{courseInstanceID}/my-course-level-sessions"
|
||||
}, options)
|
||||
@fetch(options)
|
||||
|
||||
fetchForClassroomMembers: (classroomID, options) ->
|
||||
# Params: memberSkip, memberLimit
|
||||
options = _.extend({
|
||||
url: "/db/classroom/#{classroomID}/member-sessions"
|
||||
}, options)
|
||||
@fetch(options)
|
||||
|
||||
fetchForAllClassroomMembers: (classroom, options={}) ->
|
||||
limit = 10
|
||||
skip = 0
|
||||
size = _.size(classroom.get('members'))
|
||||
options.data ?= {}
|
||||
options.data.memberLimit = limit
|
||||
options.remove = false
|
||||
jqxhrs = []
|
||||
while skip < size
|
||||
options = _.cloneDeep(options)
|
||||
options.data.memberSkip = skip
|
||||
jqxhrs.push(@fetchForClassroomMembers(classroom.id, options))
|
||||
skip += limit
|
||||
return jqxhrs
|
||||
|
|
6
app/collections/Levels.coffee
Normal file
6
app/collections/Levels.coffee
Normal file
|
@ -0,0 +1,6 @@
|
|||
CocoCollection = require 'collections/CocoCollection'
|
||||
Level = require 'models/Level'
|
||||
|
||||
module.exports = class LevelCollection extends CocoCollection
|
||||
url: '/db/level'
|
||||
model: Level
|
14
app/collections/Users.coffee
Normal file
14
app/collections/Users.coffee
Normal file
|
@ -0,0 +1,14 @@
|
|||
User = require 'models/User'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
|
||||
module.exports = class Users extends CocoCollection
|
||||
model: User
|
||||
url: '/db/user'
|
||||
|
||||
fetchForClassroom: (classroom, options) ->
|
||||
classroom = classroom.id or classroom
|
||||
options = _.extend({
|
||||
url: "/db/classroom/#{classroom}/members"
|
||||
}, options)
|
||||
@fetch(options)
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
go = (path) -> -> @routeDirectly path, arguments
|
||||
go = (path, options) -> -> @routeDirectly path, arguments, options
|
||||
redirect = (path) -> -> @navigate(path, { trigger: true, replace: true })
|
||||
|
||||
module.exports = class CocoRouter extends Backbone.Router
|
||||
|
||||
initialize: ->
|
||||
# http://nerds.airbnb.com/how-to-add-google-analytics-page-tracking-to-57536
|
||||
# http://nerds.airbnb.com/how-to-add-google-analytics-page-tracking-to-57536
|
||||
@bind 'route', @_trackPageView
|
||||
Backbone.Mediator.subscribe 'router:navigate', @onNavigate, @
|
||||
@initializeSocialMediaServices = _.once @initializeSocialMediaServices
|
||||
|
@ -57,13 +58,13 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
'contribute/diplomat': go('contribute/DiplomatView')
|
||||
'contribute/scribe': go('contribute/ScribeView')
|
||||
|
||||
'courses': go('courses/CoursesView')
|
||||
'Courses': go('courses/CoursesView')
|
||||
'courses/students': go('courses/StudentCoursesView')
|
||||
'courses/teachers': go('courses/TeacherCoursesView')
|
||||
'courses/purchase': go('courses/PurchaseCoursesView')
|
||||
'courses/enroll(/:courseID)': go('courses/CourseEnrollView')
|
||||
'courses/:classroomID': go('courses/ClassroomView')
|
||||
'courses': go('courses/CoursesView') # , { studentsOnly: true }) # TODO: Enforce after session-less play for teachers
|
||||
'Courses': go('courses/CoursesView') # , { studentsOnly: true })
|
||||
'courses/students': redirect('/courses')
|
||||
'courses/teachers': redirect('/teachers/classes')
|
||||
'courses/purchase': redirect('/teachers/enrollments')
|
||||
'courses/enroll(/:courseID)': redirect('/teachers/enrollments')
|
||||
'courses/:classroomID': go('courses/ClassroomView') #, { studentsOnly: true })
|
||||
'courses/:courseID/:courseInstanceID': go('courses/CourseDetailsView')
|
||||
|
||||
'db/*path': 'routeToServer'
|
||||
|
@ -122,8 +123,12 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
|
||||
'schools': go('NewHomeView')
|
||||
|
||||
'teachers': go('NewHomeView')
|
||||
'teachers': -> redirect('/teachers/classes')
|
||||
'teachers/classes': go('courses/TeacherClassesView') #, { teachersOnly: true })
|
||||
'teachers/classes/:classroomID': go('courses/TeacherClassView') #, { teachersOnly: true })
|
||||
'teachers/courses': go('courses/TeacherCoursesView')
|
||||
'teachers/demo': go('teachers/RequestQuoteView')
|
||||
'teachers/enrollments': go('courses/EnrollmentsView') #, { teachersOnly: true })
|
||||
'teachers/freetrial': go('teachers/RequestQuoteView')
|
||||
'teachers/quote': go('teachers/RequestQuoteView')
|
||||
'teachers/signup': ->
|
||||
|
@ -146,7 +151,12 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
removeTrailingSlash: (e) ->
|
||||
@navigate e, {trigger: true}
|
||||
|
||||
routeDirectly: (path, args, options={}) ->
|
||||
routeDirectly: (path, args=[], options={}) ->
|
||||
if options.teachersOnly and not me.isTeacher()
|
||||
return @routeDirectly('teachers/RestrictedToTeachersView')
|
||||
if options.studentsOnly and me.isTeacher()
|
||||
return @routeDirectly('courses/RestrictedToStudentsView')
|
||||
|
||||
path = 'play/CampaignView' if window.serverConfig.picoCTF and not /^(views)?\/?play/.test(path)
|
||||
path = "views/#{path}" if not _.string.startsWith(path, 'views/')
|
||||
ViewClass = @tryToLoadModule path
|
||||
|
@ -182,7 +192,7 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
window.currentView.destroy()
|
||||
$('.popover').popover 'hide'
|
||||
$('#flying-focus').css({top: 0, left: 0}) # otherwise it might make the page unnecessarily tall
|
||||
_.delay (->
|
||||
_.delay (->
|
||||
$('html')[0].scrollTop = 0
|
||||
$('body')[0].scrollTop = 0
|
||||
), 10
|
||||
|
@ -194,13 +204,13 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
require('core/services/twitter')()
|
||||
|
||||
renderSocialButtons: =>
|
||||
# TODO: Refactor remaining services to Handlers, use loadAPI success callback
|
||||
# TODO: Refactor remaining services to Handlers, use loadAPI success callback
|
||||
@initializeSocialMediaServices()
|
||||
$('.share-buttons, .partner-badges').addClass('fade-in').delay(10000).removeClass('fade-in', 5000)
|
||||
application.facebookHandler.renderButtons()
|
||||
application.gplusHandler.renderButtons()
|
||||
twttr?.widgets?.load?()
|
||||
|
||||
|
||||
activateTab: ->
|
||||
base = _.string.words(document.location.pathname[1..], '/')[0]
|
||||
$("ul.nav li.#{base}").addClass('active')
|
||||
|
|
|
@ -128,6 +128,7 @@ setUpIOSLogging = ->
|
|||
|
||||
loadOfflineFonts = ->
|
||||
$('head').prepend '<link rel="stylesheet" type="text/css" href="/fonts/openSansCondensed.css">'
|
||||
$('head').prepend '<link rel="stylesheet" type="text/css" href="/fonts/openSans.css">'
|
||||
|
||||
# This is so hacky... hopefully it's restrictive enough to not be slow.
|
||||
# We could also keep a list of events we are actually subscribed for and only try to send those over.
|
||||
|
|
|
@ -293,3 +293,14 @@ module.exports.initializeACE = (el, codeLanguage) ->
|
|||
session.setUseWrapMode true
|
||||
session.setNewLineMode 'unix'
|
||||
return editor
|
||||
|
||||
module.exports.capitalLanguages = capitalLanguages =
|
||||
'javascript': 'JavaScript'
|
||||
'coffeescript': 'CoffeeScript'
|
||||
'python': 'Python'
|
||||
'java': 'Java'
|
||||
'clojure': 'Clojure'
|
||||
'lua': 'Lua'
|
||||
'io': 'Io'
|
||||
|
||||
|
191
app/lib/coursesHelper.coffee
Normal file
191
app/lib/coursesHelper.coffee
Normal file
|
@ -0,0 +1,191 @@
|
|||
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) ->
|
||||
for classroom in classrooms.models
|
||||
# map [user, level] => session so we don't have to do find TODO
|
||||
for course, courseIndex in courses.models
|
||||
instance = courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
|
||||
continue if not instance
|
||||
instance.numCompleted = 0
|
||||
campaign = campaigns.get(course.get('campaignID'))
|
||||
for userID in instance.get('members')
|
||||
allComplete = _.every campaign.getNonLadderLevels().models, (level) ->
|
||||
return true if level.isLadder()
|
||||
#TODO: Hella slow! Do the mapping first!
|
||||
session = _.find classroom.sessions.models, (session) ->
|
||||
session.get('creator') is userID and session.get('level').original is level.get('original')
|
||||
# sessionMap[userID][level].completed()
|
||||
session?.completed()
|
||||
if allComplete
|
||||
instance.numCompleted += 1
|
||||
|
||||
calculateEarliestIncomplete: (classroom, courses, campaigns, 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
|
||||
userIDs = []
|
||||
for user in students.models
|
||||
userID = user.id
|
||||
session = _.find classroom.sessions.models, (session) ->
|
||||
session.get('creator') is userID and session.get('level').original is level.get('original')
|
||||
if not session?.completed()
|
||||
userIDs.push userID
|
||||
if userIDs.length > 0
|
||||
users = _.map userIDs, (id) ->
|
||||
students.get(id)
|
||||
return {
|
||||
courseNumber: courseIndex + 1
|
||||
levelNumber: levelIndex + 1
|
||||
levelName: level.get('name')
|
||||
users: users
|
||||
}
|
||||
null
|
||||
|
||||
calculateLatestComplete: (classroom, courses, campaigns, 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()
|
||||
for level, levelIndex in levelModels.reverse() #
|
||||
levelIndex = levelModels.length - levelIndex - 1 #compensate for reverse
|
||||
userIDs = []
|
||||
for user in students.models
|
||||
userID = user.id
|
||||
session = _.find classroom.sessions.models, (session) ->
|
||||
session.get('creator') is userID and session.get('level').original is level.get('original')
|
||||
if session?.completed() #
|
||||
userIDs.push userID
|
||||
if userIDs.length > 0
|
||||
users = _.map userIDs, (id) ->
|
||||
students.get(id)
|
||||
return {
|
||||
courseNumber: courseIndex + 1
|
||||
levelNumber: levelIndex + 1
|
||||
levelName: level.get('name')
|
||||
users: users
|
||||
}
|
||||
null
|
||||
|
||||
calculateConceptsCovered: (classrooms, courses, campaigns, courseInstances, students) ->
|
||||
# Loop through all level/user combination and record
|
||||
# whether they've started, and completed, each concept
|
||||
conceptData = {}
|
||||
for classroom in classrooms.models
|
||||
conceptData[classroom.id] = {}
|
||||
|
||||
for course, courseIndex in courses.models
|
||||
campaign = campaigns.get(course.get('campaignID'))
|
||||
|
||||
for level in campaign.getNonLadderLevels().models
|
||||
levelID = level.get('original')
|
||||
|
||||
for concept in level.get('concepts')
|
||||
unless conceptData[classroom.id][concept]
|
||||
conceptData[classroom.id][concept] = { completed: true, started: false }
|
||||
|
||||
for concept in level.get('concepts')
|
||||
for userID in classroom.get('members')
|
||||
session = _.find classroom.sessions.models, (session) ->
|
||||
session.get('creator') is userID and session.get('level').original is levelID
|
||||
|
||||
if not session # haven't gotten to this level yet, but might have completed others before
|
||||
for concept in level.get('concepts')
|
||||
conceptData[classroom.id][concept].completed = false
|
||||
if session # have gotten to the level and at least started it
|
||||
for concept in level.get('concepts')
|
||||
conceptData[classroom.id][concept].started = true
|
||||
if not session?.completed() # level started but not completed
|
||||
for concept in level.get('concepts')
|
||||
conceptData[classroom.id][concept].completed = false
|
||||
conceptData
|
||||
|
||||
calculateAllProgress: (classrooms, courses, campaigns, courseInstances, students) ->
|
||||
# Loop through all combinations and record:
|
||||
# Completeness for each student/course
|
||||
# Completeness for each student/level
|
||||
# Completeness for each class/course (across all students)
|
||||
# Completeness for each class/level (across all students)
|
||||
|
||||
# class -> course
|
||||
# class -> course -> student
|
||||
# class -> course -> level
|
||||
# class -> course -> level -> student
|
||||
|
||||
progressData = {}
|
||||
for classroom in classrooms.models
|
||||
progressData[classroom.id] = {}
|
||||
|
||||
for course, courseIndex in courses.models
|
||||
instance = courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
|
||||
if not instance
|
||||
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
|
||||
levelID = level.get('original')
|
||||
progressData[classroom.id][course.id][levelID] = { completed: true, started: false }
|
||||
|
||||
for user in students.models
|
||||
userID = user.id
|
||||
courseProgress = progressData[classroom.id][course.id]
|
||||
courseProgress[userID] ?= { completed: true, started: false } # Only set it the first time through a user
|
||||
courseProgress[levelID][userID] = { completed: true, started: false } # These don't matter, will always be set
|
||||
session = _.find classroom.sessions.models, (session) ->
|
||||
session.get('creator') is userID and session.get('level').original is levelID
|
||||
|
||||
if not session # haven't gotten to this level yet, but might have completed others before
|
||||
courseProgress.started ||= false #no-op
|
||||
courseProgress.completed = false
|
||||
courseProgress[userID].started ||= false #no-op
|
||||
courseProgress[userID].completed = false
|
||||
courseProgress[levelID].started ||= false #no-op
|
||||
courseProgress[levelID].completed = false
|
||||
courseProgress[levelID][userID].started = false
|
||||
courseProgress[levelID][userID].completed = false
|
||||
if session # have gotten to the level and at least started it
|
||||
courseProgress.started = true
|
||||
courseProgress[userID].started = true
|
||||
courseProgress[levelID].started = true
|
||||
courseProgress[levelID][userID].started = true
|
||||
if session?.completed() # have finished this level
|
||||
courseProgress.completed &&= true #no-op
|
||||
courseProgress[userID].completed = true
|
||||
courseProgress[levelID].completed &&= true #no-op
|
||||
courseProgress[levelID][userID].completed = true
|
||||
else # level started but not completed
|
||||
courseProgress.completed = false
|
||||
courseProgress[userID].completed = false
|
||||
courseProgress[levelID].completed = false
|
||||
courseProgress[levelID][userID].completed = false
|
||||
|
||||
_.assign(progressData, progressMixin)
|
||||
return progressData
|
||||
|
||||
progressMixin =
|
||||
get: (options={}) ->
|
||||
{ classroom, course, level, user } = options
|
||||
throw new Error "You must provide a classroom" unless classroom
|
||||
throw new Error "You must provide a course" unless course
|
||||
defaultValue = { completed: false, started: false }
|
||||
if options.level
|
||||
levelID = level.get('original')
|
||||
if options.user
|
||||
return @[classroom.id]?[course.id]?[levelID]?[user.id] or defaultValue
|
||||
else
|
||||
return @[classroom.id]?[course.id]?[levelID] or defaultValue
|
||||
else
|
||||
if options.user
|
||||
return @[classroom.id]?[course.id]?[user.id] or defaultValue
|
||||
else
|
||||
return @[classroom.id]?[course.id] or defaultValue
|
|
@ -263,6 +263,7 @@
|
|||
submit_patch: "Submit Patch"
|
||||
submit_changes: "Submit Changes"
|
||||
save_changes: "Save Changes"
|
||||
required_field: "Required field"
|
||||
|
||||
general:
|
||||
and: "and"
|
||||
|
@ -996,7 +997,6 @@
|
|||
average_levels: "Average levels completed:"
|
||||
total_levels: "Total levels completed:"
|
||||
furthest_level: "Furthest level completed:"
|
||||
concepts_covered: "Concepts Covered"
|
||||
students: "Students"
|
||||
students1: "students"
|
||||
concepts: "Concepts"
|
||||
|
@ -1011,13 +1011,10 @@
|
|||
capacity_used: "Course slots used:"
|
||||
enter_emails: "Enter student emails to invite, one per line"
|
||||
send_invites: "Send Invites"
|
||||
title: "Title"
|
||||
description: "Description"
|
||||
creating_class: "Creating class..."
|
||||
purchasing_course: "Purchasing course..."
|
||||
buy_course: "Buy Course"
|
||||
buy_course1: "Buy this course"
|
||||
create_class: "Create Class"
|
||||
select_all_courses: "Select 'All Courses' for a 50% discount!"
|
||||
all_courses: "All Courses"
|
||||
number_students: "Number of students"
|
||||
|
@ -1066,9 +1063,6 @@
|
|||
keep_using: "Keep Using"
|
||||
switch_to: "Switch To"
|
||||
greetings: "Greetings!"
|
||||
learn_p: "Learn Python"
|
||||
learn_j: "Learn JavaScript"
|
||||
language_cannot_change: "Language cannot be changed once students join a class."
|
||||
back_classrooms: "Back to my classrooms"
|
||||
back_courses: "Back to my courses"
|
||||
edit_details: "Edit class details"
|
||||
|
@ -1204,6 +1198,116 @@
|
|||
students_enrolled: "students enrolled"
|
||||
students_assigned: "students assigned"
|
||||
length: "Length:"
|
||||
|
||||
title: "Courses" # Flat style redesign
|
||||
subtitle: "Review course guidelines, solutions, and levels"
|
||||
select_language: "Select language"
|
||||
select_level: "Select level"
|
||||
play_level: "Play Level"
|
||||
concepts_covered: "Concepts covered"
|
||||
print_guide: "Print Guide (PDF)"
|
||||
view_guide_online: "View Guide Online (PDF)"
|
||||
|
||||
grants_lifetime_access: "Grants lifetime access to all Courses." # New enrollment modal
|
||||
enrollment_credits_available: "Enrollment Credits Available:"
|
||||
|
||||
description: "Description" # ClassroomSettingsModal
|
||||
language_select: "Select a language"
|
||||
language_cannot_change: "Language cannot be changed once students join a class."
|
||||
learn_p: "Learn Python"
|
||||
learn_j: "Learn JavaScript"
|
||||
avg_student_exp_label: "Average Student Programming Experience"
|
||||
avg_student_exp_desc: "This will help us understand how to pace courses better."
|
||||
avg_student_exp_select: "Select the best option"
|
||||
avg_student_exp_none: "No Experience - little to no experience"
|
||||
avg_student_exp_beginner: "Beginner - some exposure or block-based"
|
||||
avg_student_exp_intermediate: "Intermediate - some experience with typed code"
|
||||
avg_student_exp_advanced: "Advanced - extensive experience with typed code"
|
||||
avg_student_exp_varied: "Varied Levels of Experience"
|
||||
student_age_range_label: "Student Age Range"
|
||||
student_age_range_younger: "Younger than 6"
|
||||
student_age_range_older: "Older than 18"
|
||||
student_age_range_to: "to"
|
||||
create_class: "Create Class"
|
||||
|
||||
teacher:
|
||||
teacher_dashboard: "Teacher Dashboard" # Navbar
|
||||
my_classes: "My Classes"
|
||||
courses: "Courses"
|
||||
enrollments: "Enrollments"
|
||||
resources: "Resources"
|
||||
help: "Help"
|
||||
|
||||
students: "Students" # Shared
|
||||
language: "Language"
|
||||
edit_class_settings: "edit class settings"
|
||||
complete: "Complete"
|
||||
|
||||
access_restricted: "Access Restricted" # My Classes page
|
||||
teacher_account_required: "A teacher account is required to access this content."
|
||||
create_teacher_account: "Create Teacher Account"
|
||||
what_is_a_teacher_account: "What's a Teacher Account?"
|
||||
teacher_account_explanation: "A CodeCombat Teacher account allows you to set up classrooms, monitor students’ progress as they work through courses, manage enrollments and access resources to aid in your curriculum-building."
|
||||
current_classes: "Current Classes"
|
||||
archived_classes: "Archived Classes"
|
||||
archived_classes_blurb: "Classes can be archived for future reference. Unarchive a class to view it in the Current Classes list again."
|
||||
view_class: "view class"
|
||||
archive_class: "archive class"
|
||||
unarchive_class: "unarchive class"
|
||||
no_students_yet: "This class has no students yet."
|
||||
add_students: "Add Students"
|
||||
create_new_class: "Create a New Class"
|
||||
|
||||
class_overview: "Class Overview" # View Class page
|
||||
avg_playtime: "Average level playtime"
|
||||
total_playtime: "Total play time"
|
||||
avg_completed: "Average levels completed"
|
||||
total_completed: "Total levels completed"
|
||||
concepts_covered: "Concepts covered"
|
||||
earliest_incomplete: "Earliest incomplete level"
|
||||
latest_complete: "Latest completed level"
|
||||
enroll_student: "Enroll student"
|
||||
adding_students: "Adding students"
|
||||
course_progress: "Course Progress"
|
||||
not_applicable: "N/A"
|
||||
edit: "edit"
|
||||
latest_completed: "Latest Completed"
|
||||
sort_by: "Sort by"
|
||||
progress: "Progress"
|
||||
select_course: "Select course to view"
|
||||
course_overview: "Course Overview"
|
||||
copy_class_code: "Copy Class Code"
|
||||
class_code_blurb: "New students can enter this class code on their dashboard or visit codecombat.com/courses to join the class."
|
||||
copy_class_url: "Copy Class URL"
|
||||
class_join_url_blurb: "New students can visit this URL while logged in to join the class."
|
||||
add_students_manually: "Add Students Manually"
|
||||
bulk_assign: "Bulk-assign"
|
||||
assign_to_selected_students: "Assign to Selected Students"
|
||||
enroll_selected_students: "Enroll Selected Students"
|
||||
|
||||
guides_coming_soon: "Guides coming soon!" # Courses
|
||||
|
||||
show_students_from: "Show students from" # Enroll students modal
|
||||
enroll_the_following_students: "Enroll the following students"
|
||||
all_students: "All Students"
|
||||
|
||||
|
||||
enrollments_blurb_1: "Students taking Computer Science" # Enrollments page
|
||||
enrollments_blurb_2: "require enrollments to access the courses."
|
||||
credits_available: "Credits Available"
|
||||
total_unique_students: "Total Unique Students"
|
||||
total_enrolled_students: "Total Enrolled Students"
|
||||
unenrolled_students: "Unenrolled Students"
|
||||
add_enrollment_credits: "Add Enrollment Credits"
|
||||
purchasing: "Purchasing..."
|
||||
purchased: "Purchased!"
|
||||
purchase_now: "Purchase Now"
|
||||
how_to_enroll: "How to Enroll Students"
|
||||
how_to_enroll_blurb_1: "If a student is not enrolled yet, there will be an \"Enroll\" button next to their course progress in your class."
|
||||
how_to_enroll_blurb_2: "To bulk-enroll multiple students, select them using the checkboxes on the left side of the classroom page and click the \"Enroll Selected Students\" button."
|
||||
how_to_enroll_blurb_3: "Once a student is enrolled, they will have access to all of the course content forever, even if they leave your class."
|
||||
bulk_pricing_blurb: "Purchasing for more than 15 students? Get in touch with us for bulk pricing quotes."
|
||||
request_quote: "Request Quote"
|
||||
|
||||
classes:
|
||||
archmage_title: "Archmage"
|
||||
|
|
|
@ -82,6 +82,66 @@ module.exports = nativeDescription: "Bahasa Indonesia", englishDescription: "Ind
|
|||
winning: "Kombinasi yang unggul dari permainan RPG dan pemrograman pekerjaan rumah yang berhasil membuat pendidikan ramah anak yang sah dan menyenangkan."
|
||||
run_class: "Semua yang Anda butuhkan untuk menjalankan kelas ilmu komputer di sekolah Anda hari ini, latar belakang CS tidak diperlukan."
|
||||
|
||||
# new_home:
|
||||
# slogan: "The most engaging game for learning programming."
|
||||
# classroom_edition: "Classroom Edition:"
|
||||
# learn_to_code: "Learn to code:"
|
||||
# teacher: "Teacher"
|
||||
# student: "Student"
|
||||
# play_now: "Play Now"
|
||||
# im_a_teacher: "I'm a Teacher"
|
||||
# im_a_student: "I'm a Student"
|
||||
# learn_more: "Learn more"
|
||||
# classroom_in_a_box: "A classroom in-a-box for teaching computer science."
|
||||
# codecombat_is: "CodeCombat is a platform for students to learn computer science while playing through a real game."
|
||||
# our_courses: "Our courses have been specifically playtested to excel in a classroom setting, even by teachers with little to no prior programming experience."
|
||||
# designed_with: "Designed with teachers in mind"
|
||||
# real_code: "Real, typed code"
|
||||
# from_the_first_level: "from the first level"
|
||||
# getting_students: "Getting students to typed code as quickly as possible is critical to learning programming syntax and proper structure."
|
||||
# educator_resources: "Educator resources"
|
||||
# course_guides: "and course guides"
|
||||
# teaching_computer_science: "Teaching computer science does not require a costly degree, because we provide tools to support educators of all backgrounds."
|
||||
# accessible_to: "Accessible to"
|
||||
# everyone: "everyone"
|
||||
# democratizing: "Democratizing the process of learning coding is at the core of our philosophy. Everyone should be able to learn to code."
|
||||
# forgot_learning: "I think they actually forgot that they were actually learning something."
|
||||
# wanted_to_do: " Coding is something I've always wanted to do, and I never thought I would be able to learn it in school."
|
||||
# why_games: "Why is learning through games important?"
|
||||
# games_reward: "Games reward the productive struggle."
|
||||
# encourage: "Gaming is a medium that encourages interaction, discovery, and trial-and-error. A good game challenges the player to master skills over time, which is the same critical process students go through as they learn."
|
||||
# excel: "Games excel at rewarding"
|
||||
# struggle: "productive struggle"
|
||||
# kind_of_struggle: "the kind of struggle that results in learning that’s engaging and"
|
||||
# motivating: "motivating"
|
||||
# not_tedious: "not tedious."
|
||||
# gaming_is_good: "Studies suggest gaming is good for children’s brains. (it’s true!)"
|
||||
# game_based: "When game-based learning systems are"
|
||||
# compared: "compared"
|
||||
# conventional: "against conventional assessment methods, the difference is clear: games are better at helping students retain knowledge, concentrate and"
|
||||
# perform_at_higher_level: "perform at a higher level of achievement"
|
||||
# feedback: "Games also provide real-time feedback that allows students to adjust their solution path and understand concepts more holistically, instead of being limited to just “correct” or “incorrect” answers."
|
||||
# real_game: "A real game, played with real coding."
|
||||
# great_game: "A great game is more than just badges and achievements - it’s about a player’s journey, well-designed puzzles, and the ability to tackle challenges with agency and confidence."
|
||||
# agency: "CodeCombat is a game that gives players that agency and confidence with our robust typed code engine, which helps beginner and advanced students alike write proper, valid code."
|
||||
# curious: "Curious? Request a demo and we'll show you the ropes"
|
||||
# create_class: "Or create a class and see it for yourself!"
|
||||
# request_demo: "Request a Demo"
|
||||
# create_a_class: "Create a Class"
|
||||
# have_an_account: "Already have an account?"
|
||||
# logged_in_as: "You are currently logged in as"
|
||||
# view_my_classes: "View my classes"
|
||||
# computer_science: "Computer science courses for all ages"
|
||||
# show_me_lesson_time: "Show me lesson time estimates for:"
|
||||
# curriculum: "Total curriculum hours:"
|
||||
# ffa: "Free for all students"
|
||||
# lesson_time: "Lesson time:"
|
||||
# coming_soon: "Coming soon!"
|
||||
# courses_available_in: "Courses are available in JavaScript, Python, and Java (coming soon!)"
|
||||
# boast: "Boasts riddles that are complex enough to fascinate gamers and coders alike."
|
||||
# winning: "A winning combination of RPG gameplay and programming homework that pulls off making kid-friendly education legitimately enjoyable."
|
||||
# run_class: "Everything you need to run a computer science class in your school today, no CS background required."
|
||||
|
||||
nav:
|
||||
play: "Tingkatan" # The top nav bar entry where players choose which levels to play
|
||||
community: "Komunitas"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
CocoModel = require './CocoModel'
|
||||
schema = require 'schemas/models/campaign.schema'
|
||||
Level = require 'models/Level'
|
||||
Levels = require 'collections/Levels'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
|
||||
module.exports = class Campaign extends CocoModel
|
||||
|
@ -34,4 +35,17 @@ module.exports = class Campaign extends CocoModel
|
|||
}
|
||||
sum = (nums) -> _.reduce(nums, (s, num) -> s + num) or 0
|
||||
stats.playtime = sum((session.get('playtime') or 0 for session in sessions))
|
||||
return stats
|
||||
return stats
|
||||
|
||||
getLevels: ->
|
||||
levels = new Levels(_.values(@get('levels')))
|
||||
levels.comparator = 'campaignIndex'
|
||||
levels.sort()
|
||||
return levels
|
||||
|
||||
getNonLadderLevels: ->
|
||||
levels = new Levels(_.values(@get('levels')))
|
||||
levels.reset(levels.reject (level) -> level.isLadder())
|
||||
levels.comparator = 'campaignIndex'
|
||||
levels.sort()
|
||||
return levels
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
CocoModel = require './CocoModel'
|
||||
schema = require 'schemas/models/classroom.schema'
|
||||
utils = require 'core/utils'
|
||||
|
||||
module.exports = class Classroom extends CocoModel
|
||||
@className: 'Classroom'
|
||||
@schema: schema
|
||||
urlRoot: '/db/classroom'
|
||||
|
||||
initialize: () ->
|
||||
@listenTo @, 'change:aceConfig', @capitalizeLanguageName
|
||||
super(arguments...)
|
||||
|
||||
capitalizeLanguageName: ->
|
||||
language = @get('aceConfig')?.language
|
||||
@capitalLanguage = utils.capitalLanguages[language]
|
||||
|
||||
joinWithCode: (code, opts) ->
|
||||
options = {
|
||||
|
@ -22,4 +31,4 @@ module.exports = class Classroom extends CocoModel
|
|||
data: { userID: userID }
|
||||
}
|
||||
_.extend options, opts
|
||||
@fetch(options)
|
||||
@fetch(options)
|
||||
|
|
|
@ -26,6 +26,19 @@ module.exports = class CourseInstance extends CocoModel
|
|||
unless me.get('courseInstances')
|
||||
me.set('courseInstances', [])
|
||||
me.get('courseInstances').push(@id)
|
||||
|
||||
addMembers: (userIDs, opts) ->
|
||||
options = {
|
||||
method: 'POST'
|
||||
url: _.result(@, 'url') + '/members'
|
||||
data: { userIDs }
|
||||
}
|
||||
_.extend options, opts
|
||||
@fetch(options)
|
||||
if me.id in userIDs
|
||||
unless me.get('courseInstances')
|
||||
me.set('courseInstances', [])
|
||||
me.get('courseInstances').push(@id)
|
||||
|
||||
removeMember: (userID, opts) ->
|
||||
options = {
|
||||
|
@ -39,3 +52,7 @@ module.exports = class CourseInstance extends CocoModel
|
|||
|
||||
firstLevelURL: ->
|
||||
"/play/level/dungeons-of-kithgard?course=#{@get('courseID')}&course-instance=#{@id}"
|
||||
|
||||
hasMember: (userID, opts) ->
|
||||
userID = userID.id or userID
|
||||
userID in @get('members')
|
||||
|
|
|
@ -248,3 +248,6 @@ module.exports = class Level extends CocoModel
|
|||
width = c.width if c.width? and c.width > width
|
||||
height = c.height if c.height? and c.height > height
|
||||
return {width: width, height: height}
|
||||
|
||||
isLadder: ->
|
||||
return @get('type')?.indexOf('ladder') > -1
|
||||
|
|
|
@ -197,6 +197,9 @@ module.exports = class User extends CocoModel
|
|||
return true if me.isAdmin()
|
||||
return true if me.hasSubscription()
|
||||
return false
|
||||
|
||||
isEnrolled: ->
|
||||
Boolean(@get('coursePrepaidID'))
|
||||
|
||||
isOnPremiumServer: ->
|
||||
me.get('country') in ['china', 'brazil']
|
||||
|
|
|
@ -11,6 +11,13 @@ _.extend ClassroomSchema.properties,
|
|||
codeCamel: c.shortString(title: "UpperCamelCase version of code for display purposes")
|
||||
aceConfig:
|
||||
language: {type: 'string', 'enum': ['python', 'javascript']}
|
||||
averageStudentExp: { type: 'string' }
|
||||
ageRangeMin: { type: 'string' }
|
||||
ageRangeMax: { type: 'string' }
|
||||
archived:
|
||||
type: 'boolean'
|
||||
default: false
|
||||
description: 'Visual only; determines if the classroom is in the "archived" list of the normal list.'
|
||||
|
||||
c.extendBasicProperties ClassroomSchema, 'Classroom'
|
||||
|
||||
|
|
3
app/styles/admin/design-elements-view.sass
Normal file
3
app/styles/admin/design-elements-view.sass
Normal file
|
@ -0,0 +1,3 @@
|
|||
#design-elements-view
|
||||
.tooltip-btn
|
||||
margin: 50px 0
|
|
@ -1,7 +1,8 @@
|
|||
#activate-licenses-modal
|
||||
h2
|
||||
margin-top: -20px
|
||||
.modal-content
|
||||
padding: 60px
|
||||
width: 690px
|
||||
|
||||
.well
|
||||
max-height: 200px
|
||||
overflow: scroll
|
||||
max-height: 284px
|
||||
overflow: scroll
|
||||
|
|
13
app/styles/courses/classroom-settings-modal.sass
Normal file
13
app/styles/courses/classroom-settings-modal.sass
Normal file
|
@ -0,0 +1,13 @@
|
|||
#classroom-settings-modal
|
||||
#name-input
|
||||
width: 50%
|
||||
|
||||
.age-range-select
|
||||
width: 100px
|
||||
display: inline-block
|
||||
|
||||
label
|
||||
margin-top: 15px
|
||||
|
||||
.help-block
|
||||
margin: 0
|
8
app/styles/courses/enrollments-view.sass
Normal file
8
app/styles/courses/enrollments-view.sass
Normal file
|
@ -0,0 +1,8 @@
|
|||
#enrollments-view
|
||||
.how-to-enroll
|
||||
padding: 10px
|
||||
ol
|
||||
padding-left: 20px
|
||||
line-height: 18px
|
||||
border: thin gray solid
|
||||
border-radius: 5px
|
237
app/styles/courses/teacher-class-view.sass
Normal file
237
app/styles/courses/teacher-class-view.sass
Normal file
|
@ -0,0 +1,237 @@
|
|||
@import "app/styles/bootstrap/variables"
|
||||
@import "app/styles/mixins"
|
||||
@import "app/styles/style-flat"
|
||||
|
||||
.alternating-background:nth-child(2n+1)
|
||||
background-color: #ebebeb
|
||||
|
||||
.alternating-background:nth-child(2n)
|
||||
background-color: $gray-lighter
|
||||
|
||||
#teacher-class-view
|
||||
.breadcrumbs
|
||||
color: #065e73
|
||||
a
|
||||
color: #065e73
|
||||
|
||||
h3
|
||||
display: inline-block
|
||||
|
||||
h3 ~ .edit-classroom
|
||||
color: black
|
||||
text-decoration: underline
|
||||
|
||||
.concept
|
||||
& span
|
||||
white-space: nowrap
|
||||
&.forest
|
||||
color: $forest
|
||||
&.gold
|
||||
color: $gold
|
||||
&+.concept:before
|
||||
content: ', '
|
||||
|
||||
ul.nav-tabs
|
||||
border: none
|
||||
.tab-spacer
|
||||
float: left
|
||||
width: 15px
|
||||
height: 48px
|
||||
border-bottom: thin solid #979797
|
||||
|
||||
.tab-filler
|
||||
// Triggers block formatting context
|
||||
// http://stackoverflow.com/questions/1260122/expand-a-div-to-take-the-remaining-width
|
||||
overflow: hidden
|
||||
height: 48px
|
||||
border-bottom: thin solid #979797
|
||||
|
||||
li > a
|
||||
margin: 0
|
||||
border: thin solid #979797
|
||||
border-bottom: none
|
||||
background: white
|
||||
border-radius: 5px 5px 0 0
|
||||
width: 175px
|
||||
color: black
|
||||
font-family: $headline-font
|
||||
li
|
||||
border-bottom: thin solid #979797
|
||||
li.active
|
||||
border-bottom: none
|
||||
|
||||
.bulk-assign-controls
|
||||
float: right
|
||||
margin-bottom: -9999px
|
||||
margin-top: 20px
|
||||
select
|
||||
margin-left: 7px
|
||||
.assign-to-selected-students
|
||||
margin-left: 10px
|
||||
.enroll-selected-students
|
||||
margin-left: 56px
|
||||
|
||||
.students-table
|
||||
width: 100%
|
||||
.student-info-col
|
||||
width: 240px
|
||||
.checkbox-col
|
||||
width: 75px
|
||||
.latest-level-col
|
||||
width: 320px
|
||||
.view-class-col
|
||||
width: 20px
|
||||
|
||||
th
|
||||
font-weight: normal
|
||||
white-space: nowrap
|
||||
vertical-align: bottom
|
||||
td
|
||||
height: 66px
|
||||
|
||||
.enroll-student-button
|
||||
margin-left: 33%
|
||||
margin-left: calc((100% - 120px - 36px) * 0.6)
|
||||
|
||||
.inline-student-list
|
||||
padding-left: 0
|
||||
|
||||
// TODO: Fix highlighted text value
|
||||
&:before
|
||||
content: '('
|
||||
&:after
|
||||
content: ')'
|
||||
list-style: none
|
||||
|
||||
li
|
||||
display: inline
|
||||
.inline-student-name
|
||||
white-space: nowrap
|
||||
text-decoration: underline
|
||||
|
||||
li:not(:last-child):after
|
||||
content: ', '
|
||||
|
||||
.student-info
|
||||
max-width: 200px
|
||||
line-height: 20px
|
||||
|
||||
.student-name, .student-email
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
|
||||
.level-name
|
||||
white-space: nowrap
|
||||
overflow: ellipsis
|
||||
font-weight: bold
|
||||
// line-height: 20px
|
||||
|
||||
.sort-button
|
||||
border: none
|
||||
background: none
|
||||
font-weight: bold
|
||||
text-decoration: underline
|
||||
|
||||
.edit-student-button
|
||||
color: black
|
||||
font-weight: bold
|
||||
text-decoration: underline
|
||||
|
||||
.glyphicon
|
||||
color: $gray-light
|
||||
|
||||
// Course Progress tab
|
||||
|
||||
#course-progress-tab
|
||||
.course-overview-row
|
||||
margin-top: 50px
|
||||
border: thin solid gray
|
||||
border-radius: 8px
|
||||
|
||||
.student-levels-table
|
||||
margin-top: 80px
|
||||
|
||||
.sort-buttons
|
||||
padding-left: 75px
|
||||
margin-bottom: 5px
|
||||
|
||||
.student-levels-row, .course-overview-row
|
||||
padding-left: 75px
|
||||
padding-top: 15px
|
||||
padding-bottom: 30px
|
||||
|
||||
.student-levels-progress, .course-overview-progress
|
||||
max-width: 880px
|
||||
margin: -5px
|
||||
margin-top: 5px
|
||||
|
||||
.progress-dot
|
||||
margin: 5px
|
||||
|
||||
// Checkboxes
|
||||
.checkbox-flat
|
||||
margin: 8px auto
|
||||
|
||||
// TODO: Move to style-flat?
|
||||
.view-class-arrow
|
||||
float: right
|
||||
margin-right: 16px
|
||||
color: $gray-light
|
||||
// height: 100%
|
||||
font-size: 35px
|
||||
line-height: 35px
|
||||
align-self: center
|
||||
.view-class-arrow-inner
|
||||
color: $gray-lighter
|
||||
&:hover
|
||||
text-decoration: none
|
||||
|
||||
.progress-dot
|
||||
display: inline-block
|
||||
margin-right: 6px
|
||||
width: 34px
|
||||
height: 34px
|
||||
border-radius: 50%
|
||||
// margin-top: 23px
|
||||
// margin-bottom: 23px
|
||||
background: $gray-light
|
||||
position: relative
|
||||
.dot-label
|
||||
padding-top: 2px
|
||||
.dot-label-inner
|
||||
font-size: 11px
|
||||
font-weight: bold
|
||||
color: white
|
||||
|
||||
.progress-dot.forest
|
||||
background: $forest
|
||||
.tooltip-inner
|
||||
color: $forest
|
||||
border-color: $forest
|
||||
.tooltip-arrow
|
||||
border-top-color: $forest
|
||||
|
||||
.progress-dot.gold
|
||||
background: $gold
|
||||
.tooltip-inner
|
||||
color: $navy
|
||||
border-color: $navy
|
||||
.tooltip-arrow
|
||||
border-top-color: $navy
|
||||
|
||||
// Code copying buttons
|
||||
|
||||
.copy-button-group
|
||||
input
|
||||
height: 50px
|
||||
width: 220px
|
||||
background: #fafafa
|
||||
border: thin solid #979797
|
||||
text-align: center
|
||||
|
||||
button
|
||||
height: 50px
|
||||
width: 210px
|
||||
float: right
|
||||
|
105
app/styles/courses/teacher-classes-view.sass
Normal file
105
app/styles/courses/teacher-classes-view.sass
Normal file
|
@ -0,0 +1,105 @@
|
|||
@import "app/styles/bootstrap/variables"
|
||||
@import "app/styles/mixins"
|
||||
@import "app/styles/style-flat"
|
||||
|
||||
#teacher-classes-view
|
||||
|
||||
#site-content-area
|
||||
margin-bottom: 65px
|
||||
|
||||
.access-restricted
|
||||
margin-top: 100px
|
||||
|
||||
.teacher-account-blurb
|
||||
margin-top: 100px
|
||||
margin-bottom: 700px
|
||||
|
||||
h1
|
||||
margin-top: 50px
|
||||
|
||||
.language
|
||||
display: inline-block
|
||||
width: 140px
|
||||
|
||||
.student-count
|
||||
display: inline-block
|
||||
|
||||
.class-links
|
||||
a
|
||||
font-weight: bold
|
||||
color: black
|
||||
margin-right: 1rem
|
||||
text-decoration: underline
|
||||
|
||||
.classes
|
||||
margin-top: 20px
|
||||
|
||||
.class
|
||||
padding: 20px
|
||||
display: flex
|
||||
|
||||
.class:nth-child(2n+1)
|
||||
background-color: #ebebeb
|
||||
|
||||
.class:nth-child(2n)
|
||||
background-color: $gray-lighter
|
||||
|
||||
.view-class-arrow
|
||||
color: $gray-darker
|
||||
// height: 100%
|
||||
font-size: 35px
|
||||
line-height: 35px
|
||||
align-self: center
|
||||
.view-class-arrow-inner
|
||||
color: $gray-light
|
||||
&:hover
|
||||
text-decoration: none
|
||||
|
||||
.progress-col
|
||||
display: flex
|
||||
align-self: center
|
||||
align-items: center
|
||||
|
||||
.progress-dot
|
||||
display: inline-block
|
||||
margin-right: 20px
|
||||
width: 62px
|
||||
height: 62px
|
||||
border-radius: 50%
|
||||
margin-top: 23px
|
||||
margin-bottom: 23px
|
||||
background: $gray-light
|
||||
position: relative
|
||||
.dot-label
|
||||
color: $gray-dark
|
||||
position: absolute
|
||||
left: 50%
|
||||
top: 50%
|
||||
.text-h6
|
||||
margin-left: -50%
|
||||
margin-top: -50%
|
||||
color: white
|
||||
|
||||
.progress-dot.forest
|
||||
background: $forest
|
||||
.tooltip-inner
|
||||
color: $forest
|
||||
border: 1px solid $forest
|
||||
.tooltip-arrow
|
||||
border-top-color: $forest
|
||||
|
||||
.progress-dot.gold
|
||||
background: $gold
|
||||
.tooltip-inner
|
||||
color: $navy
|
||||
border: 1px solid $navy
|
||||
.tooltip-arrow
|
||||
border-top-color: $navy
|
||||
|
||||
.add-students
|
||||
margin-left: auto
|
||||
margin-right: auto
|
||||
|
||||
.create-class
|
||||
margin-top: 65px
|
||||
margin-bottom: 65px
|
|
@ -1,83 +1,31 @@
|
|||
@import "app/styles/bootstrap/variables"
|
||||
|
||||
#teacher-courses-view
|
||||
margin-bottom: 50px
|
||||
//TODO: Refactor colors into .style-flat?
|
||||
|
||||
h1
|
||||
margin-top: 50px
|
||||
|
||||
#activate-licenses-btn
|
||||
margin-left: 10px
|
||||
#site-content-area
|
||||
margin-bottom: 250px
|
||||
|
||||
.access-restricted
|
||||
margin-top: 75px
|
||||
|
||||
.courses
|
||||
margin-top: 20px
|
||||
|
||||
.course
|
||||
padding: 30px
|
||||
|
||||
.concepts
|
||||
font-weight: 600
|
||||
|
||||
.play-level-form
|
||||
font-weight: normal
|
||||
|
||||
.active-courses
|
||||
font-size: 12px
|
||||
font-weight: bold
|
||||
margin: 18px 0px 4px 0px
|
||||
text-transform: uppercase
|
||||
.course:nth-child(2n+1)
|
||||
background-color: #ebebeb
|
||||
|
||||
.class-count
|
||||
font-size: 30px
|
||||
|
||||
.class-name
|
||||
font-size: 20px
|
||||
font-weight: bold
|
||||
|
||||
.course-concept
|
||||
display: inline-block
|
||||
white-space: nowrap
|
||||
font-size: 12px
|
||||
line-height: 12px
|
||||
border: 1px solid lightgray
|
||||
margin-right: 4px
|
||||
margin-top: 4px
|
||||
padding: 6px
|
||||
background-color: #AAEA6E
|
||||
|
||||
.active-course-container
|
||||
tr
|
||||
margin-top: 30px
|
||||
td
|
||||
vertical-align: baseline
|
||||
|
||||
.course-enrolled
|
||||
font-size: 12px
|
||||
font-weight: bold
|
||||
margin-left: 40px
|
||||
|
||||
.course-name
|
||||
font-size: 18px
|
||||
font-weight: bold
|
||||
line-height: 30px
|
||||
|
||||
.divider
|
||||
border-bottom: 1px solid black
|
||||
margin-bottom: 20px
|
||||
|
||||
img.media-object
|
||||
width: 100%
|
||||
margin-bottom: 5px
|
||||
|
||||
.edit-classroom-small
|
||||
cursor: pointer
|
||||
&:hover
|
||||
color: grey
|
||||
|
||||
.no-students
|
||||
font-size: 22px
|
||||
font-style: italic
|
||||
margin: 10px
|
||||
text-align: center
|
||||
|
||||
.section-header
|
||||
border-bottom: 1px solid black
|
||||
font-size: 20px
|
||||
font-weight: bold
|
||||
margin-bottom: 20px
|
||||
text-transform: uppercase
|
||||
|
||||
|
||||
.text-center
|
||||
text-align: center
|
||||
|
||||
.uppercase
|
||||
text-transform: uppercase
|
||||
|
||||
.welcome
|
||||
font-size: 24px
|
||||
font-weight: bold
|
||||
margin-bottom: 20px
|
||||
.course:nth-child(2n)
|
||||
background-color: $gray-lighter
|
||||
|
|
46
app/styles/courses/teacher-dashboard-nav.sass
Normal file
46
app/styles/courses/teacher-dashboard-nav.sass
Normal file
|
@ -0,0 +1,46 @@
|
|||
@import "app/styles/bootstrap/variables"
|
||||
@import "app/styles/mixins"
|
||||
@import "app/styles/style-flat"
|
||||
|
||||
#teacher-dashboard-nav
|
||||
vertical-align: middle
|
||||
text-transform: uppercase
|
||||
|
||||
.navbar
|
||||
border-radius: 0
|
||||
background: $navy
|
||||
|
||||
.navbar-toggle
|
||||
margin-top:
|
||||
border-color: white
|
||||
|
||||
.icon-bar
|
||||
background-color: white
|
||||
|
||||
.navbar-brand
|
||||
color: white
|
||||
padding-top: 11px
|
||||
padding-bottom: 11px
|
||||
|
||||
li > a
|
||||
font-family: $body-font
|
||||
// font-size: 16px
|
||||
padding: 13px 12px 21px 12px
|
||||
// color: $burgandy
|
||||
// text-shadow: 0 0 0
|
||||
|
||||
li > a:hover
|
||||
background-color: white
|
||||
small
|
||||
color: $navy
|
||||
|
||||
li.active
|
||||
.label
|
||||
padding-left: 0
|
||||
padding-right: 0
|
||||
padding-bottom: 0
|
||||
margin-left: 0.6em
|
||||
margin-right: 0.6em
|
||||
border-bottom: 4px solid white
|
||||
border-radius: 0
|
||||
|
|
@ -68,12 +68,21 @@ $forest: #20572B
|
|||
line-height: 20px
|
||||
|
||||
p
|
||||
color: black
|
||||
margin: 0 0 14px
|
||||
|
||||
.small
|
||||
font-weight: normal
|
||||
font-size: 14px
|
||||
line-height: 20px
|
||||
|
||||
.semibold, .student-name
|
||||
font-weight: 600
|
||||
|
||||
.small-details
|
||||
font: $headline-font
|
||||
font-size: 15px
|
||||
line-height: 26px
|
||||
|
||||
font-family: $body-font
|
||||
font-size: 18px
|
||||
|
@ -93,7 +102,7 @@ $forest: #20572B
|
|||
|
||||
// Navbar
|
||||
|
||||
.navbar
|
||||
#main-nav.navbar
|
||||
background: white
|
||||
margin-bottom: 0
|
||||
white-space: nowrap // prevent home icon from going under brand
|
||||
|
@ -171,6 +180,11 @@ $forest: #20572B
|
|||
font-family: $body-font
|
||||
font-weight: normal
|
||||
background-image: none // overrides legacy buttons
|
||||
.disabled
|
||||
opacity: 50%
|
||||
|
||||
.btn ~ .btn
|
||||
margin-left: 12px
|
||||
|
||||
.btn-primary, .btn-navy
|
||||
background-color: $navy
|
||||
|
@ -215,6 +229,123 @@ $forest: #20572B
|
|||
.btn-lg
|
||||
font-size: 18px
|
||||
|
||||
// Dropdowns
|
||||
select
|
||||
height: 33px
|
||||
background-color: white
|
||||
border: 1px solid $navy
|
||||
color: $navy
|
||||
// TODO: Font size 18? Inconsistent with buttons on teacher-class-view bulk assign
|
||||
|
||||
// Tooltips
|
||||
|
||||
.tooltip .tooltip-arrow::after
|
||||
// Create a duplicate tooltip arrow which will cover the main arrow and make it seem like a line rather than filled
|
||||
content: ' '
|
||||
position: absolute
|
||||
width: 0
|
||||
height: 0
|
||||
border-color: transparent
|
||||
border-style: solid
|
||||
|
||||
// For each arrow position: make color gray-dark, make arrow larger, and position duplicate arrow
|
||||
.tooltip.top .tooltip-arrow
|
||||
margin-left: -10px
|
||||
border-width: 5px 10px 0
|
||||
border-top-color: $gray-dark
|
||||
&::after
|
||||
top: -6px
|
||||
left: 50%
|
||||
margin-left: -10px
|
||||
border-width: 5px 10px 0
|
||||
border-top-color: white
|
||||
|
||||
.tooltip.right .tooltip-arrow
|
||||
border-right-color: $gray-dark
|
||||
border-width: 5px 6px 5px 0
|
||||
|
||||
&::after
|
||||
top: 50%
|
||||
left: 1px
|
||||
margin-top: -5px
|
||||
border-width: 5px 6px 5px 0
|
||||
border-right-color: white
|
||||
|
||||
.tooltip.left .tooltip-arrow
|
||||
border-right-color: $gray-dark
|
||||
border-width: 5px 0 5px 6px
|
||||
|
||||
&::after
|
||||
top: 50%
|
||||
right: 1px
|
||||
margin-top: -5px
|
||||
border-width: 5px 0 5px 6px
|
||||
border-left-color: white
|
||||
|
||||
.tooltip.bottom .tooltip-arrow
|
||||
border-bottom-color: $gray-dark
|
||||
margin-left: -10px
|
||||
border-width: 0 10px 5px
|
||||
&::after
|
||||
top: 1px
|
||||
left: 50%
|
||||
margin-left: -10px
|
||||
border-width: 0 10px 5px
|
||||
border-bottom-color: white
|
||||
|
||||
.tooltip-inner
|
||||
padding: 10px 20px
|
||||
border: 1px solid $gray-dark
|
||||
color: black
|
||||
background: white
|
||||
border-radius: 20px
|
||||
min-width: 150px
|
||||
|
||||
|
||||
// Checkboxes
|
||||
// Note: You have to use this structure in your jade:
|
||||
// .checkbox-flat
|
||||
// input(type='checkbox' id='some-id')
|
||||
// label.checkmark(for='some-id')
|
||||
|
||||
.checkbox-flat
|
||||
position: relative
|
||||
background: white
|
||||
border: thin solid #979797
|
||||
width: 20px
|
||||
height: 20px
|
||||
|
||||
input
|
||||
visibility: hidden
|
||||
|
||||
label
|
||||
position: absolute
|
||||
width: 18px
|
||||
height: 18px
|
||||
left: 1px
|
||||
top: 1px
|
||||
|
||||
label:after
|
||||
opacity: 0
|
||||
content: ''
|
||||
position: absolute
|
||||
width: 14px
|
||||
height: 7px
|
||||
background: transparent
|
||||
top: 3px
|
||||
left: 1px
|
||||
border: 2px solid black
|
||||
border-top: none
|
||||
border-right: none
|
||||
transform: rotate(-45deg)
|
||||
|
||||
label:hover::after
|
||||
opacity: 0.3
|
||||
|
||||
input:checked + label:after
|
||||
opacity: 1
|
||||
|
||||
|
||||
.btn-gplus
|
||||
color: white
|
||||
background-color: #DD4B39
|
||||
|
|
|
@ -425,7 +425,16 @@
|
|||
a#tooltips.panel-title(href="#tooltips") Tooltips
|
||||
|
||||
.panel-body
|
||||
button#tooltip.btn.btn-default(title="tooltip text!", data-placement="right", data-trigger="click") Button w/Tooltip
|
||||
button.btn.btn-default(data-placement="right" data-toggle='tooltip') Button w/Tooltip
|
||||
|
||||
.panel-body.style-flat.text-right
|
||||
button.tooltip-btn.btn.btn-navy(data-placement="right" data-toggle='tooltip') Flat Right Tooltip
|
||||
br
|
||||
button.tooltip-btn.btn.btn-navy(data-placement="top" data-toggle='tooltip') Flat Top Tooltip
|
||||
br
|
||||
button.tooltip-btn.btn.btn-navy(data-placement="bottom" data-toggle='tooltip') Flat Bottom Tooltip
|
||||
br
|
||||
button.tooltip-btn.btn.btn-navy(data-placement="left" data-toggle='tooltip') Flat Left Tooltip
|
||||
|
||||
.col-sm-3
|
||||
.panel.panel-default
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
br
|
||||
span(data-i18n="home.old_browser_suffix")
|
||||
|
||||
nav.navbar.navbar-default
|
||||
nav#main-nav.navbar.navbar-default
|
||||
.container
|
||||
.row
|
||||
.col-md-3.col-sm-4
|
||||
|
@ -35,7 +35,7 @@
|
|||
li
|
||||
a(href="/courses", data-i18n="nav.courses")
|
||||
li
|
||||
a(href="/courses/teachers", data-i18n="nav.teachers")
|
||||
a(href="/teachers/classes", data-i18n="nav.teachers")
|
||||
li
|
||||
a(href="https://discourse.codecombat.com/", data-i18n="nav.forum")
|
||||
if me.isAnonymous()
|
||||
|
@ -72,8 +72,9 @@
|
|||
|
||||
li
|
||||
#language-dropdown-wrapper
|
||||
select.language-dropdown.form-control
|
||||
|
||||
select.language-dropdown.form-control
|
||||
|
||||
block page_nav
|
||||
|
||||
block outer_content
|
||||
#site-content-area
|
||||
|
|
|
@ -1,75 +1,54 @@
|
|||
extends /templates/core/modal-base
|
||||
extends /templates/core/modal-base-flat
|
||||
|
||||
block modal-header-content
|
||||
.clearfix
|
||||
.text-center
|
||||
h2(data-i18n="courses.enroll_paid")
|
||||
h1(data-i18n="courses.enroll_students")
|
||||
h2(data-i18n="courses.grants_lifetime_access")
|
||||
if view.classroom
|
||||
p= view.classroom.get('name')
|
||||
|
||||
block modal-body-content
|
||||
- var usedLic = view.prepaids.totalRedeemers();
|
||||
- var totalLic = view.prepaids.totalMaxRedeemers();
|
||||
- var remainingLic = totalLic - usedLic;
|
||||
.text-center
|
||||
p
|
||||
strong.spr(data-i18n="courses.you_have1")
|
||||
strong= remainingLic
|
||||
strong.spl(data-i18n="courses.you_have2")
|
||||
span(data-i18n='teacher.show_students_from')
|
||||
span.spr :
|
||||
select
|
||||
each classroom in view.classrooms.models
|
||||
option(selected=(classroom.id === view.classroom.id), value=classroom.id)
|
||||
= classroom.get('name')
|
||||
//- option(selected=!view.classroom, value='all-classrooms' data-i18n='teacher.all_students')
|
||||
form.form
|
||||
span(data-i18n="teacher.enroll_the_following_students")
|
||||
span :
|
||||
.well.form-group
|
||||
for user in view.users.models
|
||||
.checkbox
|
||||
label
|
||||
- var paid = user.get('coursePrepaidID')
|
||||
- var selected = (view.selectedUsers.get(user.id) ? true : false)
|
||||
input(type="checkbox", disabled=paid, checked=selected, data-user-id=user.id, name='user')
|
||||
span.spr= user.broadName()
|
||||
if paid
|
||||
span (
|
||||
span(data-i18n="courses.already_enrolled")
|
||||
span )
|
||||
|
||||
#error-alert.alert.alert-danger.hide
|
||||
|
||||
#submit-form-area.text-center
|
||||
p.small-details
|
||||
span.spr(data-i18n="courses.enrollment_credits_available")
|
||||
span#total-available= view.prepaids.totalAvailable()
|
||||
|
||||
.row
|
||||
.col-sm-10.col-sm-offset-1
|
||||
form.form
|
||||
if view.user
|
||||
.form-group
|
||||
.radio
|
||||
label
|
||||
input(type="radio", name="targets" value="given" checked=true)
|
||||
span.spr(data-i18n="courses.use_one")
|
||||
span= view.user.broadName()
|
||||
.radio
|
||||
label
|
||||
input(type="radio", name="targets" value="selection")
|
||||
span(data-i18n="courses.use_multiple")
|
||||
else
|
||||
p(data-i18n="courses.use_multiple")
|
||||
p
|
||||
button#activate-licenses-btn.btn.btn-lg.btn-primary(type="submit")
|
||||
span.spr(data-i18n="courses.enroll")
|
||||
| (
|
||||
span#total-selected-span
|
||||
| )
|
||||
span.spl(data-i18n="courses.students1")
|
||||
|
||||
.well.form-group
|
||||
for user in view.users.models
|
||||
.checkbox
|
||||
label
|
||||
- var paid = user.get('coursePrepaidID')
|
||||
input(type="checkbox", disabled=paid, checked=true, data-user-id=user.id, name='user')
|
||||
span.spr= user.broadName()
|
||||
if paid
|
||||
span (
|
||||
span(data-i18n="courses.already_enrolled")
|
||||
span )
|
||||
|
||||
#error-alert.alert.alert-danger.hide
|
||||
|
||||
#progress-area.hide
|
||||
.progress
|
||||
.progress-bar
|
||||
|
||||
#submit-form-area.text-center
|
||||
p
|
||||
span.spr(data-i18n="courses.total_students")
|
||||
span#total-selected-span.spr
|
||||
span#not-depleted-span
|
||||
| (
|
||||
span.spr(data-i18n="courses.licenses_remaining")
|
||||
span#licenses-remaining-span
|
||||
| )
|
||||
span#depleted-span
|
||||
| (
|
||||
span(data-i18n="courses.insufficient_enrollments")
|
||||
| )
|
||||
|
||||
p
|
||||
button#activate-licenses-btn.btn.btn-success.text-uppercase(type="submit", data-i18n="courses.enroll_students")
|
||||
|
||||
p
|
||||
a#get-more-licenses-btn.btn.btn-info.text-uppercase(href="/courses/purchase", data-i18n="courses.get_enrollments")
|
||||
p
|
||||
a#get-more-licenses-btn.btn.btn-lg.btn-primary-alt(href="/teachers/enrollments", data-i18n="courses.get_enrollments")
|
||||
|
||||
block modal-footer-content
|
||||
|
|
|
@ -1,31 +1,67 @@
|
|||
extends /templates/core/modal-base
|
||||
extends /templates/core/modal-base-flat
|
||||
|
||||
block modal-header-content
|
||||
if view.classroom
|
||||
h3.modal-title(data-i18n="courses.edit_settings1")
|
||||
else
|
||||
if view.classroom.isNew()
|
||||
h3.modal-title(data-i18n="courses.create_new_class")
|
||||
else
|
||||
h3.modal-title(data-i18n="courses.edit_settings1")
|
||||
|
||||
block modal-body-content
|
||||
.form
|
||||
form
|
||||
.form-group
|
||||
label(data-i18n="courses.title")
|
||||
- var name = view.classroom && view.classroom.get('name') ? view.classroom.get('name') : '';
|
||||
input.form-control.settings-name-input(type='text', value="#{name}")
|
||||
label(data-i18n="general.name")
|
||||
input#name-input.form-control(name="name" type='text' value=view.classroom.get('name'))
|
||||
|
||||
.form-group
|
||||
label(data-i18n="courses.description")
|
||||
- var description = view.classroom && view.classroom.get('description') ? view.classroom.get('description') : '';
|
||||
textarea.form-control.settings-description-input(rows=2)= description
|
||||
label
|
||||
span(data-i18n="courses.description")
|
||||
i.spl.text-muted(data-i18n="signup.optional")
|
||||
textarea.form-control(name="description" rows=2)= view.classroom.get('description')
|
||||
|
||||
.form-group
|
||||
label(data-i18n="choose_hero.programming_language")
|
||||
select.form-control#programming-language-select
|
||||
- var aceConfig = view.classroom ? view.classroom.get('aceConfig') || {} : {};
|
||||
option(value="python", selected=aceConfig.language==='python', data-i18n="courses.learn_p")
|
||||
option(value="javascript", selected=aceConfig.language==='javascript', data-i18n="courses.learn_j")
|
||||
.language-locked(data-i18n="courses.language_cannot_change")
|
||||
- var aceConfig = view.classroom.get('aceConfig') || {};
|
||||
- var languageDisabled = !!_.size(view.classroom.get('members'));
|
||||
.help-block.small.text-navy(data-i18n="courses.language_cannot_change")
|
||||
select#programming-language-select.form-control(name="language" value=aceConfig.language disabled=languageDisabled)
|
||||
- var aceConfig = view.classroom.get('aceConfig') || {};
|
||||
option(value="" data-i18n="courses.language_select")
|
||||
option(value="python", data-i18n="courses.learn_p")
|
||||
option(value="javascript", data-i18n="courses.learn_j")
|
||||
|
||||
.form-group
|
||||
label
|
||||
span(data-i18n="courses.avg_student_exp_label")
|
||||
span.spl.text-muted(data-i18n="signup.optional")
|
||||
.help-block.small.text-navy(data-i18n="courses.avg_student_exp_desc")
|
||||
select.form-control(name="averageStudentExp", value=view.classroom.get('averageStudentExp'))
|
||||
option(value="" data-i18n="courses.avg_student_exp_select")
|
||||
option(value="none" data-i18n="courses.avg_student_exp_none")
|
||||
option(value="beginner" data-i18n="courses.avg_student_exp_beginner")
|
||||
option(value="intermediate" data-i18n="courses.avg_student_exp_intermediate")
|
||||
option(value="advanced" data-i18n="courses.avg_student_exp_advanced")
|
||||
option(value="varied" data-i18n="courses.avg_student_exp_varied")
|
||||
|
||||
.form-group
|
||||
label
|
||||
span(data-i18n="courses.student_age_range_label")
|
||||
i.spl.text-muted(data-i18n="signup.optional")
|
||||
div
|
||||
+ageRange("ageRangeMin")
|
||||
span.spl.spr(data-i18n="courses.student_age_range_to")
|
||||
+ageRange("ageRangeMax")
|
||||
|
||||
mixin ageRange(name)
|
||||
select.age-range-select.form-control(name=name value=view.classroom.get(name))
|
||||
option(value="") -
|
||||
option(value="<6" data-i18n="courses.student_age_range_younger")
|
||||
for i in _.range(6,18)
|
||||
option(value=i)= i
|
||||
option(value="18" data-i18n="courses.student_age_range_older")
|
||||
|
||||
block modal-footer-content
|
||||
if view.classroom
|
||||
button#save-settings-btn.btn(data-i18n="common.save_changes")
|
||||
else
|
||||
button#save-settings-btn.btn(data-i18n="courses.create_class")
|
||||
.text-center
|
||||
if view.classroom.isNew()
|
||||
button#save-settings-btn.btn.btn-primary.btn-lg(data-i18n="courses.create_class")
|
||||
else
|
||||
button#save-settings-btn.btn.btn-primary.btn-lg(data-i18n="common.save_changes")
|
||||
|
|
72
app/templates/courses/enrollments-view.jade
Normal file
72
app/templates/courses/enrollments-view.jade
Normal file
|
@ -0,0 +1,72 @@
|
|||
extends /templates/base-flat
|
||||
|
||||
block page_nav
|
||||
include ./teacher-dashboard-nav.jade
|
||||
|
||||
block content
|
||||
.container
|
||||
h3(data-i18n='teacher.enrollments')
|
||||
h4
|
||||
span(data-i18n='teacher.enrollments_blurb_1')
|
||||
span 2–8
|
||||
span(data-i18n='teacher.enrollments_blurb_2')
|
||||
|
||||
.row
|
||||
.col-xs-4
|
||||
+enrollmentStats
|
||||
.col-xs-4
|
||||
+addCredits
|
||||
.col-xs-3.col-xs-offset-1
|
||||
+howToEnroll
|
||||
+quoteSection
|
||||
|
||||
mixin enrollmentStats
|
||||
h5
|
||||
span(data-i18n='teacher.credits_available')
|
||||
span.spr :
|
||||
= view.prepaids.totalAvailable()
|
||||
.small-details
|
||||
span(data-i18n='teacher.total_unique_students')
|
||||
span.spr :
|
||||
= view.members.length
|
||||
.small-details
|
||||
span(data-i18n='teacher.total_enrolled_students')
|
||||
span.spr :
|
||||
= view.prepaids.totalRedeemers()
|
||||
.small-details
|
||||
span(data-i18n='teacher.unenrolled_students')
|
||||
span.spr :
|
||||
= (view.members.length - view.prepaids.totalRedeemers())
|
||||
//- .enroll-students.btn.btn-lg.btn-navy
|
||||
//- | Enroll Students
|
||||
|
||||
mixin addCredits
|
||||
.text-center
|
||||
h5(data-i18n='teacher.add_enrollment_credits')
|
||||
div
|
||||
input#students-input.text-center.enrollment-count(value=view.numberOfStudents type='number')
|
||||
div
|
||||
if view.state === 'purchasing'
|
||||
.purchase-now.btn.btn-lg.btn-forest.disabled
|
||||
span(data-i18n='teacher.purchasing')
|
||||
else if view.state === 'purchased'
|
||||
.purchase-now.btn.btn-lg.btn-forest
|
||||
span(data-i18n='teacher.purchased')
|
||||
else
|
||||
.purchase-now.btn.btn-lg.btn-forest
|
||||
span(data-i18n='teacher.purchase_now')
|
||||
|
||||
mixin howToEnroll
|
||||
.how-to-enroll.small-details
|
||||
.text-center
|
||||
b(data-i18n='teacher.how_to_enroll')
|
||||
ol
|
||||
li(data-i18n='teacher.how_to_enroll_blurb_1')
|
||||
li(data-i18n='teacher.how_to_enroll_blurb_2')
|
||||
li(data-i18n='teacher.how_to_enroll_blurb_3')
|
||||
|
||||
mixin quoteSection
|
||||
.text-center
|
||||
h4(data-i18n='teacher.bulk_pricing_blurb')
|
||||
a.request-quote.btn.btn-lg.btn-navy(href='/teachers/quote')
|
||||
span(data-i18n='teacher.request_quote')
|
|
@ -1,4 +1,4 @@
|
|||
extends /templates/core/modal-base
|
||||
extends /templates/core/modal-base-flat
|
||||
|
||||
block modal-header-content
|
||||
h2(data-i18n="courses.add_students")
|
||||
|
|
15
app/templates/courses/progress-dot.jade
Normal file
15
app/templates/courses/progress-dot.jade
Normal file
|
@ -0,0 +1,15 @@
|
|||
if !total
|
||||
span.small-details(data-i18n='teacher.no_progress')
|
||||
| No Progress
|
||||
else
|
||||
.fraction-students.small-details
|
||||
= complete
|
||||
| /
|
||||
= total
|
||||
span.spl(data-i18n='teacher.students')
|
||||
| Students
|
||||
.percent-students.small-details
|
||||
= Math.floor(complete / total * 100)
|
||||
| %
|
||||
span.spl(data-i18n='teacher.complete')
|
||||
| Complete
|
279
app/templates/courses/teacher-class-view.jade
Normal file
279
app/templates/courses/teacher-class-view.jade
Normal file
|
@ -0,0 +1,279 @@
|
|||
extends /templates/base-flat
|
||||
|
||||
block page_nav
|
||||
include ./teacher-dashboard-nav.jade
|
||||
|
||||
block content
|
||||
- var classroom = view.classroom
|
||||
if classroom.loaded
|
||||
.container
|
||||
+breadcrumbs
|
||||
h3= classroom.get('name')
|
||||
a.label.edit-classroom(data-classroom-id=classroom.id)
|
||||
span(data-i18n='teacher.edit_class_settings')
|
||||
h4= classroom.get('description')
|
||||
|
||||
.classroom-info-row.row
|
||||
.classroom-details.col-md-3
|
||||
- var stats = view.classStats()
|
||||
h4(data-i18n='teacher.class_overview')
|
||||
|
||||
.language.small-details
|
||||
span(data-i18n='teacher.language')
|
||||
span.spr :
|
||||
span= classroom.capitalLanguage
|
||||
|
||||
.student-count.small-details
|
||||
span(data-i18n='teacher.students')
|
||||
span.spr :
|
||||
span= classroom.get('members').length
|
||||
|
||||
.average-playtime.small-details
|
||||
span(data-i18n='teacher.avg_playtime')
|
||||
span.spr :
|
||||
span= stats.averagePlaytime
|
||||
|
||||
.total-playtime.small-details
|
||||
span(data-i18n='teacher.total_playtime')
|
||||
span.spr :
|
||||
span= stats.totalPlaytime
|
||||
|
||||
.average-complete.small-details
|
||||
span(data-i18n='teacher.avg_completed')
|
||||
span.spr :
|
||||
span= stats.averageLevelsComplete
|
||||
|
||||
.total-complete.small-details
|
||||
span(data-i18n='teacher.total_completed')
|
||||
span.spr :
|
||||
span= stats.totalLevelsComplete
|
||||
|
||||
//- .concepts.small-details
|
||||
//- if view.progressData
|
||||
//- div
|
||||
//- span(data-i18n='teacher.concepts_covered')
|
||||
//- span :
|
||||
//- - console.log('concepts', view.conceptData)
|
||||
//- - concepts = view.conceptData
|
||||
//- each state, name in view.conceptData[view.classroom.id]
|
||||
//- if state.started
|
||||
//- b.concept(class=state.completed ? 'forest' : 'gold')
|
||||
//- span(data-i18n='concepts.'+name)
|
||||
|
||||
.completeness-info.col-md-4
|
||||
if view.earliestIncompleteLevel
|
||||
div
|
||||
span(data-i18n='teacher.earliest_incomplete')
|
||||
span :
|
||||
+longLevelName(view.earliestIncompleteLevel)
|
||||
+inlineUserList(view.earliestIncompleteLevel.users)
|
||||
|
||||
if view.latestCompleteLevel
|
||||
div
|
||||
span(data-i18n='teacher.latest_complete')
|
||||
span :
|
||||
+longLevelName(view.latestCompleteLevel)
|
||||
+inlineUserList(view.latestCompleteLevel.users)
|
||||
|
||||
.adding-students.col-md-5
|
||||
h4
|
||||
span(data-i18n='teacher.adding_students')
|
||||
span :
|
||||
+copyCodes
|
||||
+addStudentsButton
|
||||
|
||||
ul.nav.nav-tabs(role='tablist')
|
||||
li.active
|
||||
a(href='#students-tab' data-toggle='tab')
|
||||
.small-details.text-center(data-i18n='teacher.students')
|
||||
.tab-spacer
|
||||
li
|
||||
a(href='#course-progress-tab' data-toggle='tab')
|
||||
.small-details.text-center(data-i18n='teacher.course_progress')
|
||||
.tab-filler
|
||||
|
||||
.tab-content
|
||||
+studentsTab
|
||||
+courseProgressTab
|
||||
|
||||
mixin breadcrumbs
|
||||
.breadcrumbs
|
||||
a(data-i18n='teacher.my_classes' href='/teachers/classes')
|
||||
span.spl.spr >
|
||||
//- TODO: Use .glyphicon-menu-right when we update bootstrap
|
||||
span
|
||||
= view.classroom.get('name')
|
||||
|
||||
mixin longLevelName(data)
|
||||
if data
|
||||
div.level-name
|
||||
span.spr Course
|
||||
span= data.courseNumber
|
||||
span.spr , Level
|
||||
span= data.levelNumber
|
||||
span.spr :
|
||||
span= data.levelName
|
||||
else
|
||||
div.level-name(data-i18n='teacher.not_applicable')
|
||||
|
||||
mixin inlineUserList(users)
|
||||
if users
|
||||
ul.inline-student-list.small
|
||||
each student in users
|
||||
li
|
||||
//- a(href='TODO')
|
||||
//- = student.get('name')
|
||||
span.inline-student-name
|
||||
= student.get('name')
|
||||
|
||||
mixin addStudentsButton
|
||||
.add-students.text-center
|
||||
a.add-students-btn.btn.btn-lg.btn-primary(data-classroom-id=view.classroom.id)
|
||||
span(data-i18n='teacher.add_students_manually')
|
||||
|
||||
mixin studentsTab
|
||||
#students-tab.tab-pane.active
|
||||
+bulkAssignControls
|
||||
table.students-table
|
||||
thead
|
||||
th.checkbox-col.select-all
|
||||
span Select All
|
||||
.checkbox-flat
|
||||
input(type='checkbox' id='checkbox-all-students')
|
||||
label.checkmark(for='checkbox-all-students')
|
||||
th
|
||||
+sortButtons
|
||||
tbody
|
||||
each student in view.students.models
|
||||
+studentRow(student)
|
||||
|
||||
mixin sortButtons
|
||||
.sort-buttons.small
|
||||
span(data-i18n='teacher.sort_by')
|
||||
span.spr :
|
||||
button.sort-button.sort-by-name(data-i18n='general.name')
|
||||
button.sort-button.sort-by-progress(data-i18n='teacher.progress')
|
||||
|
||||
mixin studentRow(student)
|
||||
tr.student-row.alternating-background
|
||||
td.checkbox-col.student-checkbox
|
||||
.checkbox-flat
|
||||
input(type='checkbox' id='checkbox-student-' + student.id, data-student-id=student.id)
|
||||
label.checkmark(for='checkbox-student-' + student.id)
|
||||
td.student-info-col
|
||||
.student-info
|
||||
div.student-name= student.get('name')
|
||||
div.student-email.small-details= student.get('email')
|
||||
td.hidden
|
||||
a.edit-student-button(data-student-id=student.id)
|
||||
span.glyphicon.glyphicon-edit
|
||||
span(data-i18n='teacher.edit')
|
||||
td.latest-level-col.small
|
||||
div
|
||||
i
|
||||
span(data-i18n='teacher.latest_completed')
|
||||
span :
|
||||
div
|
||||
+longLevelName(student.latestCompleteLevel)
|
||||
td
|
||||
if view.progressData
|
||||
each course, index in view.courses.models
|
||||
- var instance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
|
||||
if instance && instance.hasMember(student)
|
||||
- var progress = view.progressData.get({ classroom: view.classroom, course: course, user: student })
|
||||
+progressDot(progress, 'CS' + (index+1))
|
||||
unless student.isEnrolled()
|
||||
+enrollStudentButton(student)
|
||||
//- td
|
||||
//- span.view-class-arrow.glyphicon.glyphicon-chevron-right
|
||||
|
||||
mixin enrollStudentButton(student)
|
||||
a.enroll-student-button.btn.btn-lg.btn-primary(data-classroom-id=view.classroom.id data-user-id=student.id)
|
||||
span(data-i18n='teacher.enroll_student')
|
||||
|
||||
mixin courseProgressTab
|
||||
#course-progress-tab.tab-pane.m-t-3
|
||||
if view.courses
|
||||
.text-center
|
||||
span(data-i18n='teacher.select_course')
|
||||
span.spr :
|
||||
select.course-select
|
||||
each course in view.courses.models
|
||||
option(value=course.id)
|
||||
= course.get('name')
|
||||
if view.progressData
|
||||
.render-on-course-sync
|
||||
+courseOverview
|
||||
.student-levels-table
|
||||
+sortButtons
|
||||
each student in view.students.models
|
||||
+studentLevelsRow(student)
|
||||
|
||||
mixin courseOverview
|
||||
- var course = view.selectedCourse
|
||||
- var campaign = view.campaigns.get(course.get('campaignID'))
|
||||
- var levels = campaign.getNonLadderLevels().models
|
||||
.course-overview-row
|
||||
.course-title.student-name
|
||||
span= course.get('name')
|
||||
span :
|
||||
span(data-i18n='teacher.course_overview')
|
||||
.course-overview-progress
|
||||
each level, index in levels
|
||||
- var progress = view.progressData.get({ classroom: view.classroom, course: course, level: level })
|
||||
+progressDot(progress, index+1)
|
||||
|
||||
mixin studentLevelsRow(student)
|
||||
.student-levels-row.alternating-background
|
||||
div.student-info
|
||||
div.student-name= student.get('name')
|
||||
div.student-email.small-details emailaddress@school.edu
|
||||
div.student-levels-progress
|
||||
- var course = view.selectedCourse
|
||||
- var campaign = view.campaigns.get(course.get('campaignID'))
|
||||
- var levels = campaign.getNonLadderLevels().models
|
||||
each level, index in levels
|
||||
- var progress = view.progressData.get({ classroom: view.classroom, course: course, level: level, user: student })
|
||||
+progressDot(progress, index+1)
|
||||
|
||||
mixin progressDot(progress, label)
|
||||
//- TODO: Refactor with TeacherClassesView jade
|
||||
//- TODO: Give classes abbreviations instead of using index?
|
||||
//- TODO: inefficient. Cache this in the view?
|
||||
- dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : '');
|
||||
.progress-dot(class=dotClass, data-html='true', data-title=view.progressDotTemplate(progressDotContext) data-toggle='tooltip')
|
||||
+progressDotLabel(label)
|
||||
|
||||
mixin progressDotLabel(label)
|
||||
.dot-label.text-center
|
||||
.dot-label-inner
|
||||
= label
|
||||
|
||||
mixin copyCodes
|
||||
div.copy-button-group.form-inline
|
||||
.form-group
|
||||
input.text-h4.semibold#join-code-input(value=view.classCode)
|
||||
button#copy-code-btn.form-control.btn.btn-lg.btn-forest
|
||||
span(data-i18n='teacher.copy_class_code')
|
||||
div.text-center.small(data-i18n='teacher.class_code_blurb')
|
||||
|
||||
div.copy-button-group.form-inline
|
||||
.form-group
|
||||
input.form-control.text-h4.semibold#join-url-input(value=view.joinURL)
|
||||
button#copy-url-btn.form-control.btn.btn-lg.btn-forest
|
||||
span(data-i18n='teacher.copy_class_url')
|
||||
div.text-center.small(data-i18n='teacher.class_join_url_blurb')
|
||||
|
||||
mixin bulkAssignControls
|
||||
.bulk-assign-controls.form-inline
|
||||
span.small
|
||||
span(data-i18n='teacher.bulk_assign')
|
||||
span :
|
||||
select.bulk-course-select.form-control
|
||||
each course in view.courses.models
|
||||
option(value=course.id)
|
||||
= course.get('name')
|
||||
button.btn.btn-primary-alt.assign-to-selected-students
|
||||
span(data-i18n='teacher.assign_to_selected_students')
|
||||
button.btn.btn-primary-alt.enroll-selected-students
|
||||
span(data-i18n='teacher.enroll_selected_students')
|
113
app/templates/courses/teacher-classes-view.jade
Normal file
113
app/templates/courses/teacher-classes-view.jade
Normal file
|
@ -0,0 +1,113 @@
|
|||
extends /templates/base-flat
|
||||
|
||||
block page_nav
|
||||
include ./teacher-dashboard-nav.jade
|
||||
|
||||
block content
|
||||
if me.isAnonymous()
|
||||
.access-restricted.container.text-center
|
||||
h5(data-i18n='teacher.access_restricted')
|
||||
p(data-i18n='teacher.teacher_account_required')
|
||||
.login-button.btn.btn-lg.btn-primary(data-i18n='login.log_in')
|
||||
.teacher-signup-button.btn.btn-lg.btn-primary-alt(data-i18n='teacher.create_teacher_account')
|
||||
|
||||
.teacher-account-blurb.text-center.col-xs-6.col-xs-offset-3
|
||||
h5(data-i18n='teacher.what_is_a_teacher_account')
|
||||
p(data-i18n='teacher.teacher_account_explanation')
|
||||
|
||||
|
||||
else
|
||||
.container
|
||||
h3(data-i18n='teacher.current_classes')
|
||||
|
||||
.classes.container
|
||||
// Loop each class
|
||||
each classroom in view.classrooms.models
|
||||
unless classroom.get('archived')
|
||||
+classRow(classroom)
|
||||
|
||||
+createClassButton
|
||||
|
||||
.container
|
||||
h3(data-i18n='teacher.archived_classes')
|
||||
h4(data-i18n='teacher.archived_classes_blurb')
|
||||
|
||||
.classes.container
|
||||
each classroom in view.classrooms.models
|
||||
if classroom.get('archived')
|
||||
+archivedClassRow(classroom)
|
||||
|
||||
mixin classRow(classroom)
|
||||
.class.row
|
||||
.col-xs-6
|
||||
.text-h4.semibold
|
||||
= classroom.get('name')
|
||||
.language.small
|
||||
span(data-i18n='teacher.language')
|
||||
| :
|
||||
span.language-name
|
||||
= classroom.capitalLanguage
|
||||
.student-count.small
|
||||
span(data-i18n='teacher.students')
|
||||
| :
|
||||
span
|
||||
= classroom.get('members').length
|
||||
.class-links
|
||||
a.text-h6(data-i18n='teacher.view_class' href=('/teachers/classes/' + classroom.id))
|
||||
a.edit-classroom.text-h6(data-i18n='teacher.edit_class_settings' data-classroom-id=classroom.id)
|
||||
a.archive-classroom.text-h6(data-i18n='teacher.archive_class' data-classroom-id=classroom.id)
|
||||
|
||||
.progress-col.col-xs-5
|
||||
if classroom.get('members').length == 0
|
||||
+addStudentsButton(classroom)
|
||||
else
|
||||
each course, index in view.courses.models
|
||||
+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))
|
||||
|
||||
|
||||
mixin addStudentsButton(classroom)
|
||||
.add-students
|
||||
.text-center
|
||||
div.small-details(data-i18n='teacher.no_students_yet')
|
||||
| This class has no students yet.
|
||||
a.add-students-btn.btn.btn-lg.btn-primary(data-classroom-id=classroom.id )
|
||||
span(data-i18n='teacher.add_students')
|
||||
| Add Students
|
||||
|
||||
mixin createClassButton
|
||||
.create-class
|
||||
.text-center
|
||||
a.create-classroom-btn.btn.btn-lg.btn-primary(data-i18n='teacher.create_new_class')
|
||||
| Create a New Class
|
||||
|
||||
mixin progressDot(classroom, course, index)
|
||||
//- TODO: Give classes abbreviations instead of using index?
|
||||
//- TODO: inefficient. Cache this in the view?
|
||||
- courseInstance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
|
||||
- var total = classroom.get('members').length
|
||||
- var complete = 0;
|
||||
- var dotClass = '';
|
||||
if courseInstance
|
||||
- complete = courseInstance.numCompleted
|
||||
- dotClass = complete === total ? 'forest' : 'gold';
|
||||
- var progressDotContext = {total: total, complete: complete};
|
||||
.progress-dot(class=dotClass, data-title=view.progressDotTemplate(progressDotContext))
|
||||
+progressDotLabel(index)
|
||||
|
||||
mixin progressDotLabel(index)
|
||||
.dot-label
|
||||
.text-h6
|
||||
| CS
|
||||
span
|
||||
= index + 1
|
||||
|
||||
mixin archivedClassRow(classroom)
|
||||
.class.row
|
||||
.col-xs-10
|
||||
span
|
||||
= classroom.get('name')
|
||||
.col-xs-2
|
||||
.class-links.pull-right
|
||||
a.unarchive-classroom.text-h6(data-i18n='teacher.unarchive_class' data-classroom-id=classroom.id)
|
|
@ -1,170 +1,82 @@
|
|||
extends /templates/base
|
||||
extends /templates/base-flat
|
||||
|
||||
block page_nav
|
||||
include ./teacher-dashboard-nav.jade
|
||||
|
||||
block content
|
||||
if me.isAnonymous()
|
||||
.access-restricted.container.text-center
|
||||
h5(data-i18n='teacher.access_restricted')
|
||||
p(data-i18n='teacher.teacher_account_required')
|
||||
.login-button.btn.btn-lg.btn-primary(data-i18n='login.log_in')
|
||||
.teacher-signup-button.btn.btn-lg.btn-primary-alt(data-i18n='teacher.create_teacher_account')
|
||||
|
||||
.text-center
|
||||
if me.isAnonymous() || !me.get('name')
|
||||
.welcome
|
||||
span(data-i18n="courses.welcome")
|
||||
span !
|
||||
else
|
||||
.welcome
|
||||
span(data-i18n="courses.welcome")
|
||||
span , #{me.get('name')}!
|
||||
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-2
|
||||
.col-md-8
|
||||
.well
|
||||
.text-center
|
||||
strong.uppercase(data-i18n="courses.getting_started")
|
||||
br
|
||||
.text-center
|
||||
a.btn.btn-info(href='http://codecombat.com/docs/CodeCombatCoursesGettingStartedGuide.pdf', data-i18n="courses.download_getting_started")
|
||||
br
|
||||
ol
|
||||
li(data-i18n="courses.getting_started_1")
|
||||
li(data-i18n="courses.getting_started_2")
|
||||
li(data-i18n="courses.getting_started_3")
|
||||
br
|
||||
.text-center
|
||||
strong(data-i18n="courses.additional_resources")
|
||||
ul
|
||||
li
|
||||
span.spr(data-i18n="courses.additional_resources_1_pref")
|
||||
a(href='http://codecombat.com/docs/CodeCombatTeacherGuideCourse1.pdf', data-i18n="courses.additional_resources_1_mid")
|
||||
span.spl.spr(data-i18n="courses.additional_resources_1_mid2")
|
||||
a(href='http://codecombat.com/docs/CodeCombatTeacherGuideCourse2.pdf', data-i18n="courses.additional_resources_1_mid3")
|
||||
span.spl.spr(data-i18n="courses.additional_resources_1_suff")
|
||||
li
|
||||
span.spr(data-i18n="courses.educator_wiki_pref")
|
||||
a(href='https://sites.google.com/a/codecombat.com/teacher-guides/', data-i18n="courses.educator_wiki_mid")
|
||||
span.spl(data-i18n="courses.educator_wiki_suff")
|
||||
li
|
||||
span.spr(data-i18n="courses.additional_resources_2_pref")
|
||||
a(href='/teachers/quote', data-i18n="teachers_quote.name")
|
||||
span.spl(data-i18n="courses.additional_resources_2_suff")
|
||||
li
|
||||
span.spr(data-i18n="courses.additional_resources_3_pref")
|
||||
a(href='http://discourse.codecombat.com/c/teachers', data-i18n="courses.additional_resources_3_mid")
|
||||
span.spl(data-i18n="courses.additional_resources_3_suff")
|
||||
li
|
||||
span.spr(data-i18n="courses.additional_resources_4_pref")
|
||||
a(href='/schools', data-i18n="courses.additional_resources_4_mid")
|
||||
span.spl(data-i18n="courses.additional_resources_4_suff")
|
||||
|
||||
.section-header(data-i18n="courses.your_classes")
|
||||
|
||||
if view.classrooms.models.length > 0
|
||||
.container-fluid
|
||||
each classroom in view.classrooms.models
|
||||
+classroom(classroom)
|
||||
else
|
||||
.no-students(data-i18n="courses.no_classes")
|
||||
|
||||
.text-center
|
||||
button.btn.btn-lg.btn-success.uppercase.create-new-class(data-i18n="courses.create_new_class1")
|
||||
|
||||
br
|
||||
.section-header(data-i18n="courses.available_courses")
|
||||
|
||||
if !me.isAnonymous()
|
||||
p.text-center
|
||||
strong.spr(data-i18n="courses.unused_enrollments")
|
||||
strong.spr= view.prepaids.totalAvailable()
|
||||
a.btn.btn-success.btn(href="/courses/purchase", data-i18n="courses.purchase_enrollments")
|
||||
button#activate-licenses-btn.btn.btn-info(data-i18n="courses.enroll_paid")
|
||||
|
||||
p(data-i18n="courses.students_access")
|
||||
|
||||
.container-fluid
|
||||
.container
|
||||
h1(data-i18n="courses.title")
|
||||
h2(data-i18n="courses.subtitle")
|
||||
|
||||
.courses.container
|
||||
- var courses = view.courses.models;
|
||||
- var i = 0;
|
||||
while i < courses.length
|
||||
- var course = courses[i];
|
||||
- i++;
|
||||
.row
|
||||
.col-md-6
|
||||
.course.row
|
||||
.col-sm-9
|
||||
+course-info(course)
|
||||
if i < courses.length
|
||||
- course = courses[i];
|
||||
- i++;
|
||||
.col-md-6
|
||||
+course-info(course)
|
||||
|
||||
block footer
|
||||
|
||||
mixin classroom(classroom)
|
||||
.row
|
||||
- var classMemberCount = classroom.get('members') ? classroom.get('members').length : 0;
|
||||
if classMemberCount > 0
|
||||
.col-md-8
|
||||
p
|
||||
span.spr.class-name= classroom.get('name')
|
||||
if classroom.get('aceConfig') && classroom.get('aceConfig').language === 'javascript'
|
||||
span.spr.class-name (JavaScript)
|
||||
else
|
||||
span.spr.class-name (Python)
|
||||
a.edit-classroom-small(data-i18n="courses.edit_settings", data-classroom-id="#{classroom.id}")
|
||||
if classMemberCount === 1
|
||||
p There is #{classMemberCount} student in this class.
|
||||
else
|
||||
p There are #{classMemberCount} students in this class.
|
||||
.active-courses(data-i18n="courses.active_courses")
|
||||
- var courseInstances = view.courseInstances.where({classroomID: classroom.id});
|
||||
table.active-course-container
|
||||
each courseInstance in courseInstances
|
||||
tr
|
||||
+course(courseInstance, classMemberCount)
|
||||
else
|
||||
.col-md-12
|
||||
p
|
||||
span.spr.class-name= classroom.get('name')
|
||||
if classroom.get('aceConfig') && classroom.get('aceConfig').language === 'javascript'
|
||||
span.spr.class-name (JavaScript)
|
||||
else
|
||||
span.spr.class-name (Python)
|
||||
a.edit-classroom-small(data-i18n="courses.edit_settings", data-classroom-id="#{classroom.id}")
|
||||
.no-students(data-i18n="courses.no_students")
|
||||
.text-center
|
||||
button.btn.btn-info.uppercase.btn-add-students(data-classroom-id="#{classroom.id}", data-i18n="courses.add_students1")
|
||||
br
|
||||
if classMemberCount > 0
|
||||
.col-md-4.text-center
|
||||
.class-count= classMemberCount
|
||||
.active-courses(style='margin:6px;', data-i18n="courses.students1")
|
||||
a.btn.btn-info.uppercase(href='/courses/#{classroom.id}', data-i18n="courses.view_edit")
|
||||
.row
|
||||
.col-md-12
|
||||
.divider
|
||||
|
||||
mixin course(courseInstance, classMemberCount)
|
||||
- var courseMemberCount = courseInstance.get('members') ? courseInstance.get('members').length : 0;
|
||||
if courseMemberCount > 0
|
||||
- var course = view.courses.get(courseInstance.get('courseID'));
|
||||
td
|
||||
span.course-name= course.get('name')
|
||||
td
|
||||
span.course-enrolled
|
||||
span #{courseMemberCount} / #{classMemberCount}
|
||||
span.spl(data-i18n="courses.students_assigned")
|
||||
|
||||
.col-sm-3.hidden
|
||||
.play-level-form
|
||||
.form-group
|
||||
label.control-label
|
||||
span(data-i18n="courses.select_language")
|
||||
| :
|
||||
select.form-control
|
||||
// TODO: Automate this list @scott
|
||||
option(value="python")
|
||||
| Python
|
||||
option(value="javascript")
|
||||
| JavaScript
|
||||
//- option(value="coffeescript")
|
||||
//- | CoffeeScript (Experimental)
|
||||
//- option(value="clojure")
|
||||
//- | Clojure (Experimental)
|
||||
//- option(value="lua")
|
||||
//- | Lua
|
||||
//- option(value="java")
|
||||
//- | Java
|
||||
.form-group
|
||||
label.control-label
|
||||
span(data-i18n="courses.select_level")
|
||||
| :
|
||||
select.form-control
|
||||
// TODO: Automate this list @scott
|
||||
option(value='TODO')
|
||||
| 1. Dungeons of Kithgard
|
||||
a.btn.btn-lg.btn-primary
|
||||
span(data-i18n="courses.play_level")
|
||||
.clearfix
|
||||
|
||||
|
||||
|
||||
mixin course-info(course)
|
||||
.media
|
||||
img.media-object(src=course.get('screenshot'))
|
||||
.media-body
|
||||
span.spr.course-name= course.get('name')
|
||||
p= course.get('description')
|
||||
p
|
||||
strong
|
||||
span(data-i18n="courses.concepts")
|
||||
span.spr :
|
||||
each concept in course.get('concepts')
|
||||
span(data-i18n="concepts." + concept)
|
||||
if course.get('concepts').indexOf(concept) !== course.get('concepts').length - 1
|
||||
span.spr ,
|
||||
p
|
||||
strong.spr(data-i18n="courses.length")
|
||||
span= course.get('duration') || 0
|
||||
span.spl(data-i18n="units.hours")
|
||||
.course-info
|
||||
.text-h4.semibold
|
||||
= course.get('name')
|
||||
p= course.get('description')
|
||||
p.concepts.semibold
|
||||
span(data-i18n="courses.concepts_covered")
|
||||
| :
|
||||
each concept in course.get('concepts')
|
||||
span(data-i18n="concepts." + concept)
|
||||
if course.get('concepts').indexOf(concept) !== course.get('concepts').length - 1
|
||||
span.spr ,
|
||||
if view.guideLinks[course.id]
|
||||
//- a.btn.btn-primary(href=view.guideLinks[course.id] class=(me.isAnonymous() ? 'disabled' : ''))
|
||||
//- span(data-i18n="courses.print_guide")
|
||||
a.btn.btn-primary(href=view.guideLinks[course.id] class=(me.isAnonymous() ? 'disabled' : ''))
|
||||
span(data-i18n="courses.view_guide_online")
|
||||
else
|
||||
i.small
|
||||
| (
|
||||
span(data-i18n='teacher.guides_coming_soon')
|
||||
| )
|
||||
|
|
29
app/templates/courses/teacher-dashboard-nav.jade
Normal file
29
app/templates/courses/teacher-dashboard-nav.jade
Normal file
|
@ -0,0 +1,29 @@
|
|||
- var path = document.location.pathname
|
||||
|
||||
#teacher-dashboard-nav
|
||||
nav.navbar
|
||||
.container-fluid.container
|
||||
.navbar-header
|
||||
button.navbar-toggle.collapsed(type='button' data-toggle='collapse' data-target='#teacher-dashboard-nav-collapse')
|
||||
span.sr-only Toggle navigation
|
||||
span.icon-bar
|
||||
span.icon-bar
|
||||
span.icon-bar
|
||||
span.navbar-brand.text-h4(data-i18n='teacher.teacher_dashboard')
|
||||
#teacher-dashboard-nav-collapse.collapse.navbar-collapse
|
||||
ul.nav.navbar-nav
|
||||
li(class= path.indexOf('/teachers/classes') === 0 ? 'active' : '')
|
||||
a(href='/teachers/classes')
|
||||
small.label(data-i18n='teacher.my_classes')
|
||||
li(class= path.indexOf('/teachers/courses') === 0 ? 'active' : '')
|
||||
a(href='/teachers/courses')
|
||||
small.label(data-i18n='teacher.courses')
|
||||
li(class= path.indexOf('/teachers/enrollments') === 0 ? 'active' : '')
|
||||
a(href='/teachers/enrollments')
|
||||
small.label(data-i18n='teacher.enrollments')
|
||||
//- li(class= path.indexOf('TODO') === 0 ? 'active' : '')
|
||||
//- a(href='TODO')
|
||||
//- small.label(data-i18n='teacher.resources')
|
||||
//- li(class= path.indexOf('TODO') === 0 ? 'active' : '')
|
||||
//- a(href='TODO')
|
||||
//- small.label(data-i18n='teacher.help')
|
|
@ -119,8 +119,12 @@ module.exports = class NewHomeView extends RootView
|
|||
@scrollToLink('#classroom-in-box-container')
|
||||
|
||||
onClickTeacherButton: ->
|
||||
window.tracker?.trackEvent 'Homepage Click Teacher Button', category: 'Homepage'
|
||||
@scrollToLink('.request-demo-row', 600)
|
||||
if me.isTeacher()
|
||||
window.tracker?.trackEvent 'Homepage Click Teacher Button (logged in)', category: 'Homepage'
|
||||
application.router.navigate('/teachers', { trigger: true })
|
||||
else
|
||||
window.tracker?.trackEvent 'Homepage Click Teacher Button', category: 'Homepage'
|
||||
@scrollToLink('.request-demo-row', 600)
|
||||
|
||||
onRightPressed: (event) ->
|
||||
# Special handling, otherwise after you click the control, keyboard presses move the slide twice
|
||||
|
|
|
@ -11,36 +11,30 @@ module.exports = class DesignElementsView extends RootView
|
|||
hash = document.location.hash
|
||||
document.location.hash = ''
|
||||
setTimeout((-> document.location.hash = hash), 10)
|
||||
|
||||
# modal
|
||||
@$('#modal-2').find('.background-wrapper').addClass('plain')
|
||||
|
||||
# tooltips
|
||||
@$('[data-toggle="tooltip"]').tooltip({
|
||||
title: 'Lorem ipsum'
|
||||
trigger: 'click'
|
||||
})
|
||||
if hash is '#tooltips'
|
||||
setTimeout((=> @$('#tooltip').tooltip('show')), 20)
|
||||
setTimeout((=> @$('[data-toggle="tooltip"]').tooltip('show')), 20)
|
||||
|
||||
# popovers
|
||||
if hash is '#popovers'
|
||||
setTimeout((=> @$('#popover').popover('show')), 20)
|
||||
|
||||
# autocomplete
|
||||
tags = [
|
||||
"ActionScript",
|
||||
"AppleScript",
|
||||
"Asp",
|
||||
"BASIC",
|
||||
"C",
|
||||
"C++",
|
||||
"Clojure",
|
||||
"COBOL",
|
||||
"ColdFusion",
|
||||
"Erlang",
|
||||
"Fortran",
|
||||
"Groovy",
|
||||
"Haskell",
|
||||
"Java",
|
||||
"JavaScript",
|
||||
"Lisp",
|
||||
"Perl",
|
||||
"PHP",
|
||||
"Python",
|
||||
"Ruby",
|
||||
"Scala",
|
||||
"Scheme"
|
||||
"ActionScript", "AppleScript", "Asp", "BASIC", "C", "C++", "Clojure", "COBOL", "ColdFusion", "Erlang",
|
||||
"Fortran", "Groovy", "Haskell", "Java", "JavaScript", "Lisp", "Perl", "PHP", "Python", "Ruby", "Scala", "Scheme"
|
||||
]
|
||||
@$('#tags').autocomplete({source: tags})
|
||||
if hash is '#autocomplete'
|
||||
setTimeout((=> @$('#tags').autocomplete("search", "t")), 20)
|
||||
|
||||
# slider
|
||||
@$('#slider-example').slider()
|
|
@ -2,7 +2,9 @@ ModalView = require 'views/core/ModalView'
|
|||
template = require 'templates/courses/activate-licenses-modal'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
Prepaids = require 'collections/Prepaids'
|
||||
Classrooms = require 'collections/Classrooms'
|
||||
User = require 'models/User'
|
||||
Users = require 'collections/Users'
|
||||
|
||||
module.exports = class ActivateLicensesModal extends ModalView
|
||||
id: 'activate-licenses-modal'
|
||||
|
@ -10,17 +12,28 @@ module.exports = class ActivateLicensesModal extends ModalView
|
|||
|
||||
events:
|
||||
'change input': 'updateSelectionSpans'
|
||||
'change select': 'replaceStudentList'
|
||||
'submit form': 'onSubmitForm'
|
||||
|
||||
initialize: (options) ->
|
||||
@classroom = options.classroom
|
||||
@users = options.users
|
||||
@user = options.user
|
||||
@selectedUsers = options.selectedUsers
|
||||
@prepaids = new Prepaids()
|
||||
@prepaids.comparator = '_id'
|
||||
@prepaids.fetchByCreator(me.id)
|
||||
@supermodel.loadCollection(@prepaids, 'prepaids')
|
||||
|
||||
@supermodel.trackCollection(@prepaids)
|
||||
@classrooms = new Classrooms()
|
||||
@classrooms.fetchMine({
|
||||
data: {archived: false}
|
||||
success: =>
|
||||
@classrooms.each (classroom) =>
|
||||
classroom.users = new Users()
|
||||
classroom.users.fetchForClassroom(classroom)
|
||||
@supermodel.trackCollection(classroom.users)
|
||||
})
|
||||
@supermodel.trackCollection(@classrooms)
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
@updateSelectionSpans()
|
||||
|
@ -38,6 +51,19 @@ module.exports = class ActivateLicensesModal extends ModalView
|
|||
@$('#not-depleted-span').toggleClass('hide', depleted)
|
||||
@$('#depleted-span').toggleClass('hide', !depleted)
|
||||
@$('#activate-licenses-btn').toggleClass('disabled', depleted).toggleClass('btn-success', not depleted).toggleClass('btn-default', depleted)
|
||||
|
||||
replaceStudentList: (e) ->
|
||||
selectedClassroomID = $(e.currentTarget).val()
|
||||
@classroom = @classrooms.get(selectedClassroomID)
|
||||
if selectedClassroomID == 'all-classrooms'
|
||||
@classroom = new Classroom({ id: 'all-students' }) # TODO: This is a horrible hack so the select shows the right option!
|
||||
users = _.uniq _.flatten @classrooms.map (classroom) -> classroom.users.models
|
||||
@users.reset(users)
|
||||
else
|
||||
@users.reset(@classrooms.get(selectedClassroomID).users.models)
|
||||
@trigger('users:change')
|
||||
@render()
|
||||
null
|
||||
|
||||
showProgress: ->
|
||||
@$('#submit-form-area').addClass('hide')
|
||||
|
|
|
@ -1,38 +1,52 @@
|
|||
Classroom = require 'models/Classroom'
|
||||
ModalView = require 'views/core/ModalView'
|
||||
template = require 'templates/courses/classroom-settings-modal'
|
||||
forms = require 'core/forms'
|
||||
errors = require 'core/errors'
|
||||
|
||||
module.exports = class ClassroomSettingsModal extends ModalView
|
||||
id: 'classroom-settings-modal'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click #save-settings-btn': 'onClickSaveSettingsButton'
|
||||
'click #save-settings-btn': 'onSubmitForm'
|
||||
'submit form': 'onSubmitForm'
|
||||
|
||||
initialize: (options) ->
|
||||
@classroom = options.classroom
|
||||
if @classroom
|
||||
application.tracker?.trackEvent 'Classroom started edit settings', category: 'Courses', classroomID: @classroom.id
|
||||
else
|
||||
initialize: (options={}) ->
|
||||
@classroom = options.classroom or new Classroom()
|
||||
if @classroom.isNew()
|
||||
application.tracker?.trackEvent 'Create new class', category: 'Courses'
|
||||
else
|
||||
application.tracker?.trackEvent 'Classroom started edit settings', category: 'Courses', classroomID: @classroom.id
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
disableLangSelect = @classroom?.get('members')?.length > 0
|
||||
@$('#programming-language-select').prop('disabled', disableLangSelect)
|
||||
@$('.language-locked').toggle(disableLangSelect)
|
||||
forms.updateSelects(@$('form'))
|
||||
|
||||
onClickSaveSettingsButton: ->
|
||||
name = $('.settings-name-input').val()
|
||||
unless @classroom
|
||||
return unless name
|
||||
@classroom = new Classroom({ name: name })
|
||||
if name
|
||||
@classroom.set('name', name)
|
||||
description = $('.settings-description-input').val()
|
||||
@classroom.set('description', description)
|
||||
@classroom.set('aceConfig', {
|
||||
language: @$('#programming-language-select').val()
|
||||
})
|
||||
onSubmitForm: (e) ->
|
||||
@classroom.notyErrors = false
|
||||
e.preventDefault()
|
||||
form = @$('form')
|
||||
forms.clearFormAlerts(form)
|
||||
attrs = forms.formToObject(form)
|
||||
if attrs.language
|
||||
attrs.aceConfig = { language: attrs.language }
|
||||
delete attrs.language
|
||||
else
|
||||
forms.setErrorToProperty(form, 'language', $.i18n.t('common.required_field'))
|
||||
return
|
||||
@classroom.set(attrs)
|
||||
schemaErrors = @classroom.getValidationErrors()
|
||||
if schemaErrors
|
||||
forms.applyErrorsToForm(form, schemaErrors)
|
||||
return
|
||||
|
||||
button = @$('#save-settings-btn')
|
||||
@oldButtonText = button.text()
|
||||
button.text($.i18n.t('common.saving')).attr('disabled', true)
|
||||
@classroom.save()
|
||||
@hide()
|
||||
@listenToOnce @classroom, 'error', (model, jqxhr) ->
|
||||
@stopListening @classroom, 'sync', @hide
|
||||
button.text(@oldButtonText).attr('disabled', false)
|
||||
errors.showNotyNetworkError(jqxhr)
|
||||
@listenToOnce @classroom, 'sync', @hide
|
133
app/views/courses/EnrollmentsView.coffee
Normal file
133
app/views/courses/EnrollmentsView.coffee
Normal file
|
@ -0,0 +1,133 @@
|
|||
app = require 'core/application'
|
||||
CreateAccountModal = require 'views/core/CreateAccountModal'
|
||||
Classroom = require 'models/Classroom'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
Course = require 'models/Course'
|
||||
Prepaids = require 'collections/Prepaids'
|
||||
RootView = require 'views/core/RootView'
|
||||
stripeHandler = require 'core/services/stripe'
|
||||
template = require 'templates/courses/enrollments-view'
|
||||
User = require 'models/User'
|
||||
utils = require 'core/utils'
|
||||
Products = require 'collections/Products'
|
||||
|
||||
module.exports = class EnrollmentsView extends RootView
|
||||
id: 'enrollments-view'
|
||||
template: template
|
||||
numberOfStudents: 30
|
||||
pricePerStudent: 0
|
||||
|
||||
initialize: (options) ->
|
||||
@listenTo stripeHandler, 'received-token', @onStripeReceivedToken
|
||||
@fromClassroom = utils.getQueryVariable('from-classroom')
|
||||
@members = new CocoCollection([], { model: User })
|
||||
@listenTo @members, 'sync', @membersSync
|
||||
@classrooms = new CocoCollection([], { url: "/db/classroom", model: Classroom })
|
||||
@classrooms.comparator = '_id'
|
||||
@listenToOnce @classrooms, 'sync', @onceClassroomsSync
|
||||
@supermodel.loadCollection(@classrooms, 'classrooms', {data: {ownerID: me.id}})
|
||||
@prepaids = new Prepaids()
|
||||
@prepaids.comparator = '_id'
|
||||
@prepaids.fetchByCreator(me.id)
|
||||
@supermodel.loadCollection(@prepaids, 'prepaids')
|
||||
@products = new Products()
|
||||
@supermodel.loadCollection(@products, 'products')
|
||||
super(options)
|
||||
|
||||
events:
|
||||
'input #students-input': 'onInputStudentsInput'
|
||||
'click .purchase-now': 'onClickPurchaseButton'
|
||||
# 'click .enroll-students': 'onClickEnrollStudents'
|
||||
|
||||
onLoaded: ->
|
||||
@pricePerStudent = @products.findWhere({name: 'course'}).get('amount')
|
||||
me.setRole 'teacher'
|
||||
super()
|
||||
|
||||
getPriceString: -> '$' + (@getPrice()/100).toFixed(2)
|
||||
getPrice: -> @pricePerStudent * @numberOfStudents
|
||||
|
||||
onceClassroomsSync: ->
|
||||
for classroom in @classrooms.models
|
||||
@members.fetch({
|
||||
remove: false
|
||||
url: "/db/classroom/#{classroom.id}/members"
|
||||
})
|
||||
|
||||
membersSync: ->
|
||||
@memberEnrolledMap = {}
|
||||
for user in @members.models
|
||||
@memberEnrolledMap[user.id] = user.get('coursePrepaidID')?
|
||||
@classroomNotEnrolledMap = {}
|
||||
@totalNotEnrolled = 0
|
||||
for classroom in @classrooms.models
|
||||
@classroomNotEnrolledMap[classroom.id] = 0
|
||||
for memberID in classroom.get('members')
|
||||
@classroomNotEnrolledMap[classroom.id]++ unless @memberEnrolledMap[memberID]
|
||||
@totalNotEnrolled += @classroomNotEnrolledMap[classroom.id]
|
||||
@numberOfStudents = @totalNotEnrolled
|
||||
@render?()
|
||||
|
||||
onInputStudentsInput: ->
|
||||
@numberOfStudents = Math.max(parseInt(@$('#students-input').val()) or 0, 0)
|
||||
@updatePrice()
|
||||
|
||||
updatePrice: ->
|
||||
@renderSelectors '#price-form-group'
|
||||
|
||||
numberOfStudentsIsValid: -> 0 < @numberOfStudents < 100000
|
||||
|
||||
# onClickEnrollStudents: ->
|
||||
# TODO: Needs "All students" in modal dropdown
|
||||
|
||||
onClickPurchaseButton: ->
|
||||
return @openModalView new CreateAccountModal() if me.isAnonymous()
|
||||
unless @numberOfStudentsIsValid()
|
||||
alert("Please enter the maximum number of students needed for your class.")
|
||||
return
|
||||
|
||||
@state = undefined
|
||||
@stateMessage = undefined
|
||||
@render()
|
||||
|
||||
# Show Stripe handler
|
||||
application.tracker?.trackEvent 'Started course prepaid purchase', {
|
||||
price: @pricePerStudent, students: @numberOfStudents}
|
||||
stripeHandler.open
|
||||
amount: @numberOfStudents * @pricePerStudent
|
||||
description: "Full course access for #{@numberOfStudents} students"
|
||||
bitcoin: true
|
||||
alipay: if me.get('country') is 'china' or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
|
||||
|
||||
onStripeReceivedToken: (e) ->
|
||||
@state = 'purchasing'
|
||||
@render?()
|
||||
|
||||
data =
|
||||
maxRedeemers: @numberOfStudents
|
||||
type: 'course'
|
||||
stripe:
|
||||
token: e.token.id
|
||||
timestamp: new Date().getTime()
|
||||
|
||||
$.ajax({
|
||||
url: '/db/prepaid/-/purchase',
|
||||
data: data,
|
||||
method: 'POST',
|
||||
context: @
|
||||
success: (prepaid) ->
|
||||
application.tracker?.trackEvent 'Finished course prepaid purchase', {price: @pricePerStudent, seats: @numberOfStudents}
|
||||
@state = 'purchased'
|
||||
@prepaids.add(prepaid)
|
||||
@render?()
|
||||
|
||||
error: (jqxhr, textStatus, errorThrown) ->
|
||||
application.tracker?.trackEvent 'Failed course prepaid purchase', status: textStatus
|
||||
if jqxhr.status is 402
|
||||
@state = 'error'
|
||||
@stateMessage = arguments[2]
|
||||
else
|
||||
@state = 'error'
|
||||
@stateMessage = "#{jqxhr.status}: #{jqxhr.responseText}"
|
||||
@render?()
|
||||
})
|
246
app/views/courses/TeacherClassView.coffee
Normal file
246
app/views/courses/TeacherClassView.coffee
Normal file
|
@ -0,0 +1,246 @@
|
|||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/courses/teacher-class-view'
|
||||
helper = require 'lib/coursesHelper'
|
||||
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
|
||||
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
|
||||
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
|
||||
|
||||
Classroom = require 'models/Classroom'
|
||||
Classrooms = require 'collections/Classrooms'
|
||||
LevelSessions = require 'collections/LevelSessions'
|
||||
User = require 'models/User'
|
||||
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'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click .edit-classroom': 'onClickEditClassroom'
|
||||
'click .add-students-btn': 'onClickAddStudents'
|
||||
'click .sort-by-name': 'sortByName'
|
||||
'click .sort-by-progress': 'sortByProgress'
|
||||
'click #copy-url-btn': 'copyURL'
|
||||
'click #copy-code-btn': 'copyCode'
|
||||
'click .enroll-student-button': 'onClickEnroll'
|
||||
'click .assign-to-selected-students': 'onClickBulkAssign'
|
||||
'click .enroll-selected-students': 'onClickBulkEnroll'
|
||||
'click .select-all': 'onClickSelectAll'
|
||||
'click .student-checkbox': 'onClickStudentCheckbox'
|
||||
'change .course-select': 'onChangeCourseSelect'
|
||||
|
||||
initialize: (options, classroomID) ->
|
||||
super(options)
|
||||
@progressDotTemplate = require 'templates/courses/progress-dot'
|
||||
|
||||
@sortAttribute = 'name'
|
||||
@sortDirection = 1
|
||||
|
||||
@classroom = new Classroom({ _id: classroomID })
|
||||
@classroom.fetch()
|
||||
@supermodel.trackModel(@classroom)
|
||||
|
||||
@listenTo @classroom, 'sync', ->
|
||||
@students = new Users()
|
||||
@students.fetchForClassroom(@classroom)
|
||||
@supermodel.trackCollection(@students)
|
||||
@listenTo @students, 'sync', @sortByName
|
||||
@listenTo @students, 'sort', @renderSelectors.bind(@, '.students-table', '.student-levels-table')
|
||||
|
||||
@classroom.sessions = new LevelSessions()
|
||||
if @classroom.get('members')?.length > 0
|
||||
@classroom.sessions.fetchForAllClassroomMembers(@classroom)
|
||||
@supermodel.trackCollection(@classroom.sessions)
|
||||
|
||||
@courses = new Courses()
|
||||
@courses.fetch()
|
||||
@supermodel.trackCollection(@courses)
|
||||
|
||||
@campaigns = new Campaigns()
|
||||
@campaigns.fetchByType('course')
|
||||
@supermodel.trackCollection(@campaigns)
|
||||
|
||||
@courseInstances = new CourseInstances()
|
||||
@courseInstances.fetchByOwner(me.id)
|
||||
@supermodel.trackCollection(@courseInstances)
|
||||
|
||||
onLoaded: ->
|
||||
console.log("loaded!")
|
||||
|
||||
@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)
|
||||
for student in @students.models
|
||||
# TODO: this is a weird hack
|
||||
studentsStub = new Users([ student ])
|
||||
student.latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @campaigns, @courseInstances, studentsStub)
|
||||
|
||||
classroomsStub = new Classrooms([ @classroom ])
|
||||
@progressData = helper.calculateAllProgress(classroomsStub, @courses, @campaigns, @courseInstances, @students)
|
||||
# @conceptData = helper.calculateConceptsCovered(classroomsStub, @courses, @campaigns, @courseInstances, @students)
|
||||
|
||||
@selectedCourse = @courses.first()
|
||||
super()
|
||||
|
||||
copyCode: ->
|
||||
@$('#join-code-input').val(@classCode).select()
|
||||
@tryCopy()
|
||||
|
||||
copyURL: ->
|
||||
@$('#join-url-input').val(@joinURL).select()
|
||||
@tryCopy()
|
||||
|
||||
tryCopy: ->
|
||||
try
|
||||
document.execCommand('copy')
|
||||
application.tracker?.trackEvent 'Classroom copy URL', category: 'Courses', classroomID: @classroom.id, url: @joinURL
|
||||
catch err
|
||||
message = 'Oops, unable to copy'
|
||||
noty text: message, layout: 'topCenter', type: 'error', killer: false
|
||||
|
||||
onClickEditClassroom: (e) ->
|
||||
classroom = @classroom
|
||||
modal = new ClassroomSettingsModal({ classroom: classroom })
|
||||
@openModalView(modal)
|
||||
@listenToOnce modal, 'hide', @render
|
||||
|
||||
onClickAddStudents: (e) =>
|
||||
modal = new InviteToClassroomModal({ classroom: @classroom })
|
||||
@openModalView(modal)
|
||||
@listenToOnce modal, 'hide', @render
|
||||
|
||||
sortByName: (e) ->
|
||||
if @sortValue is 'name'
|
||||
@sortDirection = -@sortDirection
|
||||
else
|
||||
@sortValue = 'name'
|
||||
@sortDirection = 1
|
||||
|
||||
dir = @sortDirection
|
||||
@students.comparator = (student1, student2) ->
|
||||
return (if student1.get('name') < student2.get('name') then -dir else dir)
|
||||
@students.sort()
|
||||
|
||||
sortByProgress: (e) ->
|
||||
if @sortValue is 'progress'
|
||||
@sortDirection = -@sortDirection
|
||||
else
|
||||
@sortValue = 'progress'
|
||||
@sortDirection = 1
|
||||
|
||||
dir = @sortDirection
|
||||
|
||||
@students.comparator = (student) ->
|
||||
#TODO: I would like for this to be in the Level model,
|
||||
# but it doesn't know about its own courseNumber
|
||||
level = student.latestCompleteLevel
|
||||
if not level
|
||||
return -dir
|
||||
return dir * ((1000 * level.courseNumber) + level.levelNumber)
|
||||
@students.sort()
|
||||
|
||||
getSelectedStudentIDs: ->
|
||||
$('.student-row .checkbox-flat input:checked').map (index, checkbox) ->
|
||||
$(checkbox).data('student-id')
|
||||
|
||||
ensureInstance: (courseID) ->
|
||||
|
||||
onClickEnroll: (e) ->
|
||||
userID = $(e.currentTarget).data('user-id')
|
||||
user = @students.get(userID)
|
||||
selectedUsers = new Users([user])
|
||||
modal = new ActivateLicensesModal { @classroom, selectedUsers, users: @students }
|
||||
@openModalView(modal)
|
||||
modal.once 'redeem-users', -> document.location.reload()
|
||||
application.tracker?.trackEvent 'Classroom started enroll students', category: 'Courses'
|
||||
|
||||
onClickBulkEnroll: ->
|
||||
courseID = $('.bulk-course-select').val()
|
||||
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
|
||||
userIDs = @getSelectedStudentIDs().toArray()
|
||||
selectedUsers = new Users(@students.get(userID) for userID in userIDs)
|
||||
modal = new ActivateLicensesModal { @classroom, selectedUsers, users: @students }
|
||||
@openModalView(modal)
|
||||
modal.once 'redeem-users', -> document.location.reload()
|
||||
application.tracker?.trackEvent 'Classroom started enroll students', category: 'Courses'
|
||||
|
||||
onClickBulkAssign: ->
|
||||
courseID = $('.bulk-course-select').val()
|
||||
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
|
||||
members = @getSelectedStudentIDs().filter((index, userID) =>
|
||||
user = @students.get(userID)
|
||||
user.isEnrolled()
|
||||
).toArray()
|
||||
|
||||
if courseInstance
|
||||
courseInstance.addMembers members, {
|
||||
success: =>
|
||||
@render() unless @destroyed
|
||||
}
|
||||
else
|
||||
courseInstance = new CourseInstance {
|
||||
courseID,
|
||||
classroomID: @classroom.id
|
||||
ownerID: @classroom.get('ownerID')
|
||||
aceConfig: {}
|
||||
}
|
||||
@courseInstances.add(courseInstance)
|
||||
courseInstance.save {}, {
|
||||
success: =>
|
||||
courseInstance.addMembers members, {
|
||||
success: =>
|
||||
@render() unless @destroyed
|
||||
}
|
||||
}
|
||||
null
|
||||
|
||||
onClickSelectAll: (e) ->
|
||||
e.preventDefault()
|
||||
checkboxes = $('.student-checkbox input')
|
||||
if _.all(checkboxes, 'checked')
|
||||
$('.select-all input').prop('checked', false)
|
||||
checkboxes.prop('checked', false)
|
||||
else
|
||||
$('.select-all input').prop('checked', true)
|
||||
checkboxes.prop('checked', true)
|
||||
null
|
||||
|
||||
onClickStudentCheckbox: (e) ->
|
||||
e.preventDefault()
|
||||
# $(e.target).$()
|
||||
checkbox = $(e.currentTarget).find('input')
|
||||
checkbox.prop('checked', not checkbox.prop('checked'))
|
||||
# checkboxes.prop('checked', false)
|
||||
checkboxes = $('.student-checkbox input')
|
||||
$('.select-all input').prop('checked', _.all(checkboxes, 'checked'))
|
||||
|
||||
onChangeCourseSelect: (e) ->
|
||||
@selectedCourse = @courses.get($(e.currentTarget).val())
|
||||
@renderSelectors('.render-on-course-sync')
|
||||
|
||||
classStats: ->
|
||||
stats = {}
|
||||
|
||||
playtime = 0
|
||||
total = 0
|
||||
for session in @classroom.sessions.models
|
||||
pt = session.get('playtime') or 0
|
||||
playtime += pt
|
||||
total += 1
|
||||
stats.averagePlaytime = if playtime and total then moment.duration(playtime / total, "seconds").humanize() else 0
|
||||
stats.totalPlaytime = if playtime then moment.duration(playtime, "seconds").humanize() else 0
|
||||
# TODO: Humanize differently ('1 hour' instead of 'an hour')
|
||||
|
||||
completeSessions = @classroom.sessions.filter (s) -> s.get('state')?.complete
|
||||
stats.averageLevelsComplete = if @students.size() then (_.size(completeSessions) / @students.size()).toFixed(1) else 'N/A' # '
|
||||
stats.totalLevelsComplete = _.size(completeSessions)
|
||||
|
||||
enrolledUsers = @students.filter (user) -> user.get('coursePrepaidID')
|
||||
stats.enrolledUsers = _.size(enrolledUsers)
|
||||
return stats
|
126
app/views/courses/TeacherClassesView.coffee
Normal file
126
app/views/courses/TeacherClassesView.coffee
Normal file
|
@ -0,0 +1,126 @@
|
|||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/courses/teacher-classes-view'
|
||||
Classroom = require 'models/Classroom'
|
||||
Classrooms = require 'collections/Classrooms'
|
||||
Courses = require 'collections/Courses'
|
||||
Campaign = require 'models/Campaign'
|
||||
Campaigns = require 'collections/Campaigns'
|
||||
LevelSessions = require 'collections/LevelSessions'
|
||||
CourseInstance = require 'models/CourseInstance'
|
||||
CourseInstances = require 'collections/CourseInstances'
|
||||
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
|
||||
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
|
||||
User = require 'models/User'
|
||||
utils = require 'core/utils'
|
||||
helper = require 'lib/coursesHelper'
|
||||
|
||||
module.exports = class TeacherClassesView extends RootView
|
||||
id: 'teacher-classes-view'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click .edit-classroom': 'onClickEditClassroom'
|
||||
'click .archive-classroom': 'onClickArchiveClassroom'
|
||||
'click .unarchive-classroom': 'onClickUnarchiveClassroom'
|
||||
'click .add-students-btn': 'onClickAddStudentsButton'
|
||||
'click .create-classroom-btn': 'onClickCreateClassroomButton'
|
||||
|
||||
initialize: (options) ->
|
||||
super(options)
|
||||
@classrooms = new Classrooms()
|
||||
@classrooms.fetchMine()
|
||||
@supermodel.trackCollection(@classrooms)
|
||||
@listenTo @classrooms, 'sync', ->
|
||||
for classroom in @classrooms.models
|
||||
classroom.sessions = new LevelSessions()
|
||||
jqxhrs = classroom.sessions.fetchForAllClassroomMembers(classroom)
|
||||
if jqxhrs.length > 0
|
||||
@supermodel.trackCollection(classroom.sessions)
|
||||
|
||||
@courses = new Courses()
|
||||
@courses.fetch()
|
||||
@supermodel.trackCollection(@courses)
|
||||
|
||||
@campaigns = new Campaigns()
|
||||
@campaigns.fetchByType('course')
|
||||
@supermodel.trackCollection(@campaigns)
|
||||
|
||||
@courseInstances = new CourseInstances()
|
||||
@courseInstances.fetchByOwner(me.id)
|
||||
@supermodel.trackCollection(@courseInstances)
|
||||
@progressDotTemplate = require 'templates/courses/progress-dot'
|
||||
|
||||
# Level Sessions loaded after onLoaded to prevent race condition in calculateDots
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
$('.progress-dot').each (i, el) ->
|
||||
dot = $(el)
|
||||
dot.tooltip({
|
||||
html: true
|
||||
container: dot
|
||||
})
|
||||
|
||||
onLoaded: ->
|
||||
helper.calculateDots(@classrooms, @courses, @courseInstances, @campaigns)
|
||||
super()
|
||||
|
||||
onClickEditClassroom: (e) ->
|
||||
classroomID = $(e.target).data('classroom-id')
|
||||
classroom = @classrooms.get(classroomID)
|
||||
modal = new ClassroomSettingsModal({ classroom: classroom })
|
||||
@openModalView(modal)
|
||||
@listenToOnce modal, 'hide', @render
|
||||
|
||||
onClickCreateClassroomButton: (e) ->
|
||||
classroom = new Classroom({ ownerID: me.id })
|
||||
modal = new ClassroomSettingsModal({ classroom: classroom })
|
||||
@openModalView(modal)
|
||||
@listenToOnce modal.classroom, 'sync', ->
|
||||
@classrooms.add(modal.classroom)
|
||||
@addFreeCourseInstances()
|
||||
@render()
|
||||
|
||||
onClickAddStudentsButton: (e) ->
|
||||
classroomID = $(e.currentTarget).data('classroom-id')
|
||||
classroom = @classrooms.get(classroomID)
|
||||
modal = new InviteToClassroomModal({ classroom: classroom })
|
||||
@openModalView(modal)
|
||||
@listenToOnce modal, 'hide', @render
|
||||
|
||||
onClickArchiveClassroom: (e) ->
|
||||
classroomID = $(e.currentTarget).data('classroom-id')
|
||||
classroom = @classrooms.get(classroomID)
|
||||
classroom.set('archived', true)
|
||||
classroom.save {}, {
|
||||
success: =>
|
||||
@render()
|
||||
}
|
||||
|
||||
onClickUnarchiveClassroom: (e) ->
|
||||
classroomID = $(e.currentTarget).data('classroom-id')
|
||||
classroom = @classrooms.get(classroomID)
|
||||
classroom.set('archived', false)
|
||||
classroom.save {}, {
|
||||
success: =>
|
||||
@render()
|
||||
}
|
||||
|
||||
addFreeCourseInstances: ->
|
||||
# so that when students join the classroom, they can automatically get free courses
|
||||
# non-free courses are generated when the teacher first adds a student to them
|
||||
for classroom in @classrooms.models
|
||||
for course in @courses.models
|
||||
continue if not course.get('free')
|
||||
courseInstance = @courseInstances.findWhere({classroomID: classroom.id, courseID: course.id})
|
||||
if not courseInstance
|
||||
courseInstance = new CourseInstance({
|
||||
classroomID: classroom.id
|
||||
courseID: course.id
|
||||
})
|
||||
# TODO: figure out a better way to get around triggering validation errors for properties
|
||||
# that the server will end up filling in, like an empty members array, ownerID
|
||||
courseInstance.save(null, {validate: false})
|
||||
@courseInstances.add(courseInstance)
|
||||
@listenToOnce courseInstance, 'sync', @addFreeCourseInstances
|
||||
return
|
|
@ -21,6 +21,15 @@ module.exports = class TeacherCoursesView extends RootView
|
|||
'click .btn-add-students': 'onClickAddStudents'
|
||||
'click .create-new-class': 'onClickCreateNewClassButton'
|
||||
'click .edit-classroom-small': 'onClickEditClassroomSmall'
|
||||
|
||||
guideLinks:
|
||||
{
|
||||
"560f1a9f22961295f9427742": 'http://codecombat.com/docs/CodeCombatTeacherGuideCourse1.pdf'
|
||||
"5632661322961295f9428638": 'https://docs.google.com/a/codecombat.com/viewer?a=v&pid=sites&srcid=Y29kZWNvbWJhdC5jb218dGVhY2hlci1ndWlkZXN8Z3g6NGEzMDFhZTZmMTg4YmRmZQ'
|
||||
"56462f935afde0c6fd30fc8c": 'https://docs.google.com/a/codecombat.com/viewer?a=v&pid=sites&srcid=Y29kZWNvbWJhdC5jb218dGVhY2hlci1ndWlkZXN8Z3g6NzY0Nzc1NWRjMTk4MGRiMQ'
|
||||
"56462f935afde0c6fd30fc8d": null
|
||||
"569ed916efa72b0ced971447": null
|
||||
}
|
||||
|
||||
constructor: (options) ->
|
||||
super(options)
|
||||
|
|
|
@ -115,6 +115,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"modernizr-mixin": "~3.0.0",
|
||||
"openSansCondensed": "https://google-fonts.azurewebsites.net/googleFonts/openSansCondensed?family=Open+Sans+Condensed:700&subset=latin,latin-ext,cyrillic-ext,greek-ext,greek,vietnamese,cyrillic"
|
||||
"openSansCondensed": "https://google-fonts.azurewebsites.net/googleFonts/openSansCondensed?family=Open+Sans+Condensed:700&subset=latin,latin-ext,cyrillic-ext,greek-ext,greek,vietnamese,cyrillic",
|
||||
"openSans": "https://google-fonts.azurewebsites.net/googleFonts/openSans?family=Open+Sans:600,700&subset=latin,latin-ext,cyrillic-ext,greek-ext,greek,vietnamese,cyrillic"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,7 +208,7 @@ exports.config =
|
|||
assetsmanager:
|
||||
copyTo:
|
||||
'lib/ace': ['node_modules/ace-builds/src-min-noconflict/*']
|
||||
'fonts': ['bower_components/openSansCondensed/*']
|
||||
'fonts': ['bower_components/openSansCondensed/*', 'bower_components/openSans/*']
|
||||
autoReload:
|
||||
delay: 1000
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ mongoose = require 'mongoose'
|
|||
plugins = require '../plugins/plugins'
|
||||
log = require 'winston'
|
||||
config = require '../../server_config'
|
||||
jsonSchema = require '../../app/schemas/models/campaign.schema'
|
||||
|
||||
CampaignSchema = new mongoose.Schema(body: String, {strict: false,read:config.mongo.readpref})
|
||||
|
||||
|
@ -35,4 +36,6 @@ CampaignSchema.statics.updateAdjacentCampaigns = (savedCampaign) ->
|
|||
|
||||
CampaignSchema.post 'save', -> @constructor.updateAdjacentCampaigns @
|
||||
|
||||
CampaignSchema.statics.jsonSchema = jsonSchema
|
||||
|
||||
module.exports = mongoose.model('campaign', CampaignSchema)
|
||||
|
|
|
@ -17,6 +17,10 @@ ClassroomSchema.statics.editableProperties = [
|
|||
'description'
|
||||
'name'
|
||||
'aceConfig'
|
||||
'averageStudentExp'
|
||||
'ageRangeMin'
|
||||
'ageRangeMax'
|
||||
'archived'
|
||||
]
|
||||
|
||||
ClassroomSchema.statics.generateNewCode = (done) ->
|
||||
|
@ -46,4 +50,14 @@ ClassroomSchema.methods.isMember = (userID) ->
|
|||
|
||||
ClassroomSchema.statics.jsonSchema = jsonSchema
|
||||
|
||||
ClassroomSchema.set('toObject', {
|
||||
transform: (doc, ret, options) ->
|
||||
return ret unless options.req
|
||||
user = options.req.user
|
||||
unless user?.isAdmin() or user?.get('_id').equals(doc.get('ownerID'))
|
||||
delete ret.code
|
||||
delete ret.codeCamel
|
||||
return ret
|
||||
})
|
||||
|
||||
module.exports = Classroom = mongoose.model 'classroom', ClassroomSchema, 'classrooms'
|
||||
|
|
|
@ -107,6 +107,10 @@ module.exports.NetworkError = NetworkError
|
|||
module.exports.Unauthorized = class Unauthorized extends NetworkError
|
||||
code: 401
|
||||
errorName: 'Unauthorized'
|
||||
|
||||
module.exports.PaymentRequired = class PaymentRequired extends NetworkError
|
||||
code: 402
|
||||
errorName: 'PaymentRequired'
|
||||
|
||||
module.exports.Forbidden = class Forbidden extends NetworkError
|
||||
code: 403
|
||||
|
|
|
@ -8,19 +8,20 @@ module.exports =
|
|||
options = _.extend({
|
||||
max: 1000
|
||||
default: 100
|
||||
param: 'limit'
|
||||
}, options)
|
||||
|
||||
limit = options.default
|
||||
|
||||
if req.query.limit
|
||||
limit = parseInt(req.query.limit)
|
||||
if req.query[options.param]
|
||||
limit = parseInt(req.query[options.param])
|
||||
valid = tv4.validate(limit, {
|
||||
type: 'integer'
|
||||
maximum: options.max
|
||||
minimum: 1
|
||||
})
|
||||
if not valid
|
||||
throw new errors.UnprocessableEntity('Invalid limit parameter.')
|
||||
throw new errors.UnprocessableEntity("Invalid #{options.param} parameter.")
|
||||
|
||||
return limit
|
||||
|
||||
|
@ -29,19 +30,20 @@ module.exports =
|
|||
options = _.extend({
|
||||
max: 1000000
|
||||
default: 0
|
||||
param: 'skip'
|
||||
}, options)
|
||||
|
||||
skip = options.default
|
||||
|
||||
if req.query.skip
|
||||
skip = parseInt(req.query.skip)
|
||||
if req.query[options.param]
|
||||
skip = parseInt(req.query[options.param])
|
||||
valid = tv4.validate(skip, {
|
||||
type: 'integer'
|
||||
maximum: options.max
|
||||
minimum: 0
|
||||
})
|
||||
if not valid
|
||||
throw new errors.UnprocessableEntity('Invalid sort parameter.')
|
||||
throw new errors.UnprocessableEntity("Invalid #{options.param} parameter.")
|
||||
|
||||
return skip
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
mongoose = require 'mongoose'
|
||||
Handler = require '../commons/Handler'
|
||||
Course = require './Course'
|
||||
|
||||
CourseHandler = class CourseHandler extends Handler
|
||||
modelClass: Course
|
||||
jsonSchema: require '../../app/schemas/models/course.schema'
|
||||
allowedMethods: ['GET']
|
||||
|
||||
hasAccess: (req) ->
|
||||
req.method in @allowedMethods or req.user?.isAdmin()
|
||||
|
||||
module.exports = new CourseHandler()
|
|
@ -2,7 +2,7 @@ async = require 'async'
|
|||
Handler = require '../commons/Handler'
|
||||
Campaign = require '../campaigns/Campaign'
|
||||
Classroom = require '../classrooms/Classroom'
|
||||
Course = require './Course'
|
||||
Course = require '../models/Course'
|
||||
CourseInstance = require './CourseInstance'
|
||||
LevelSession = require '../levels/sessions/LevelSession'
|
||||
LevelSessionHandler = require '../levels/sessions/level_session_handler'
|
||||
|
@ -35,7 +35,6 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
|||
relationship = args[1]
|
||||
return @createHOCAPI(req, res) if relationship is 'create-for-hoc'
|
||||
return @getLevelSessionsAPI(req, res, args[0]) if args[1] is 'level_sessions'
|
||||
return @addMember(req, res, args[0]) if req.method is 'POST' and args[1] is 'members'
|
||||
return @removeMember(req, res, args[0]) if req.method is 'DELETE' and args[1] is 'members'
|
||||
return @getMembersAPI(req, res, args[0]) if args[1] is 'members'
|
||||
return @inviteStudents(req, res, args[0]) if relationship is 'invite_students'
|
||||
|
@ -65,38 +64,6 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
|||
return @sendDatabaseError(res, err) if err
|
||||
@sendCreated(res, courseInstance)
|
||||
|
||||
addMember: (req, res, courseInstanceID) ->
|
||||
return @sendUnauthorizedError(res) if not req.user?
|
||||
userID = req.body.userID
|
||||
return @sendBadInputError(res, 'Input must be a MongoDB ID') unless utils.isID(userID)
|
||||
CourseInstance.findById courseInstanceID, (err, courseInstance) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
return @sendNotFoundError(res, 'Course instance not found') unless courseInstance
|
||||
Classroom.findById courseInstance.get('classroomID'), (err, classroom) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
return @sendNotFoundError(res, 'Classroom referenced by course instance not found') unless classroom
|
||||
return @sendForbiddenError(res) unless _.any(classroom.get('members'), (memberID) -> memberID.toString() is userID)
|
||||
ownsCourseInstance = courseInstance.get('ownerID').equals(req.user.get('_id'))
|
||||
addingSelf = userID is req.user.id
|
||||
return @sendForbiddenError(res) unless ownsCourseInstance or addingSelf
|
||||
alreadyInCourseInstance = _.any courseInstance.get('members') or [], (memberID) -> memberID.toString() is userID
|
||||
return @sendSuccess(res, @formatEntity(req, courseInstance)) if alreadyInCourseInstance
|
||||
Prepaid.find({ 'redeemers.userID': mongoose.Types.ObjectId(userID) }).count (err, userIsPrepaid) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
Course.findById courseInstance.get('courseID'), (err, course) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
return @sendNotFoundError(res, 'Course referenced by course instance not found') unless course
|
||||
if not (course.get('free') or userIsPrepaid)
|
||||
return @sendPaymentRequiredError(res, 'Cannot add this user to a course instance until they are added to a prepaid')
|
||||
members = courseInstance.get('members')
|
||||
members.push(userID)
|
||||
courseInstance.set('members', members)
|
||||
courseInstance.save (err, courseInstance) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
User.update {_id: mongoose.Types.ObjectId(userID)}, {$addToSet: {courseInstances: courseInstance.get('_id')}}, (err) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
@sendSuccess(res, @formatEntity(req, courseInstance))
|
||||
|
||||
removeMember: (req, res, courseInstanceID) ->
|
||||
return @sendUnauthorizedError(res) if not req.user?
|
||||
userID = req.body.userID
|
||||
|
@ -133,7 +100,7 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
|||
Course.findById req.body.courseID, (err, course) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
return @sendNotFoundError(res, 'Course not found') unless course
|
||||
q = {
|
||||
q = {
|
||||
courseID: mongoose.Types.ObjectId(req.body.courseID)
|
||||
classroomID: mongoose.Types.ObjectId(req.body.classroomID)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ async = require 'async'
|
|||
utils = require '../lib/utils'
|
||||
log = require 'winston'
|
||||
Campaign = require '../campaigns/Campaign'
|
||||
Course = require '../courses/Course'
|
||||
Course = require '../models/Course'
|
||||
CourseInstance = require '../courses/CourseInstance'
|
||||
Classroom = require '../classrooms/Classroom'
|
||||
|
||||
|
|
21
server/middleware/campaigns.coffee
Normal file
21
server/middleware/campaigns.coffee
Normal file
|
@ -0,0 +1,21 @@
|
|||
utils = require '../lib/utils'
|
||||
errors = require '../commons/errors'
|
||||
wrap = require 'co-express'
|
||||
Promise = require 'bluebird'
|
||||
database = require '../commons/database'
|
||||
mongoose = require 'mongoose'
|
||||
Campaign = require '../campaigns/Campaign'
|
||||
parse = require '../commons/parse'
|
||||
LevelSession = require '../levels/sessions/LevelSession'
|
||||
|
||||
module.exports =
|
||||
fetchByType: wrap (req, res, next) ->
|
||||
type = req.query.type
|
||||
return next() unless type
|
||||
unless _.contains(Campaign.jsonSchema.properties.type.enum, type)
|
||||
throw new errors.UnprocessableEntity('Bad campaign type')
|
||||
dbq = Campaign.find { type: type }
|
||||
dbq.select(parse.getProjectFromReq(req))
|
||||
campaigns = yield dbq.exec()
|
||||
campaigns = (campaign.toObject({req: req}) for campaign in campaigns)
|
||||
res.status(200).send(campaigns)
|
62
server/middleware/classrooms.coffee
Normal file
62
server/middleware/classrooms.coffee
Normal file
|
@ -0,0 +1,62 @@
|
|||
_ = require 'lodash'
|
||||
utils = require '../lib/utils'
|
||||
errors = require '../commons/errors'
|
||||
wrap = require 'co-express'
|
||||
Promise = require 'bluebird'
|
||||
database = require '../commons/database'
|
||||
mongoose = require 'mongoose'
|
||||
Classroom = require '../classrooms/Classroom'
|
||||
parse = require '../commons/parse'
|
||||
LevelSession = require '../levels/sessions/LevelSession'
|
||||
User = require '../users/User'
|
||||
|
||||
module.exports =
|
||||
getByOwner: wrap (req, res, next) ->
|
||||
options = req.query
|
||||
ownerID = options.ownerID
|
||||
return next() unless ownerID
|
||||
throw new errors.UnprocessableEntity('Bad ownerID') unless utils.isID ownerID
|
||||
throw new errors.Unauthorized() unless req.user
|
||||
throw new errors.Forbidden('"ownerID" must be yourself') unless req.user.isAdmin() or ownerID is req.user.id
|
||||
sanitizedOptions = {}
|
||||
unless _.isUndefined(options.archived)
|
||||
# Handles when .archived is true, vs false-or-null
|
||||
sanitizedOptions.archived = { $ne: not (options.archived is 'true') }
|
||||
console.log sanitizedOptions
|
||||
dbq = Classroom.find _.merge sanitizedOptions, { ownerID: mongoose.Types.ObjectId(ownerID) }
|
||||
dbq.select(parse.getProjectFromReq(req))
|
||||
classrooms = yield dbq
|
||||
classrooms = (classroom.toObject({req: req}) for classroom in classrooms)
|
||||
res.status(200).send(classrooms)
|
||||
|
||||
fetchMemberSessions: wrap (req, res, next) ->
|
||||
throw new errors.Unauthorized() unless req.user
|
||||
memberLimit = parse.getLimitFromReq(req, {default: 10, max: 100, param: 'memberLimit'})
|
||||
memberSkip = parse.getSkipFromReq(req, {param: 'memberSkip'})
|
||||
classroom = yield database.getDocFromHandle(req, Classroom)
|
||||
throw new errors.NotFound('Classroom not found.') if not classroom
|
||||
throw new errors.Forbidden('You do not own this classroom.') unless req.user.isAdmin() or classroom.get('ownerID').equals(req.user._id)
|
||||
members = classroom.get('members') or []
|
||||
members = members.slice(memberSkip, memberLimit)
|
||||
dbqs = []
|
||||
select = 'state.complete level creator playtime'
|
||||
for member in members
|
||||
dbqs.push(LevelSession.find({creator: member.toHexString(), team: {$exists: false}}).select(select).exec())
|
||||
results = yield dbqs
|
||||
sessions = _.flatten(results)
|
||||
res.status(200).send(sessions)
|
||||
|
||||
fetchMembers: wrap (req, res, next) ->
|
||||
throw new errors.Unauthorized() unless req.user
|
||||
memberLimit = parse.getLimitFromReq(req, {default: 10, max: 100, param: 'memberLimit'})
|
||||
memberSkip = parse.getSkipFromReq(req, {param: 'memberSkip'})
|
||||
classroom = yield database.getDocFromHandle(req, Classroom)
|
||||
throw new errors.NotFound('Classroom not found.') if not classroom
|
||||
throw new errors.Forbidden('You do not own this classroom.') unless req.user.isAdmin() or classroom.get('ownerID').equals(req.user._id)
|
||||
memberIDs = classroom.get('members') or []
|
||||
memberIDs = memberIDs.slice(memberSkip, memberLimit)
|
||||
|
||||
members = yield User.find({ _id: { $in: memberIDs }}).select(parse.getProjectFromReq(req))
|
||||
memberObjects = (member.toObject({ req: req, includedPrivates: ["name", "email"] }) for member in members)
|
||||
|
||||
res.status(200).send(memberObjects)
|
63
server/middleware/course-instances.coffee
Normal file
63
server/middleware/course-instances.coffee
Normal file
|
@ -0,0 +1,63 @@
|
|||
errors = require '../commons/errors'
|
||||
wrap = require 'co-express'
|
||||
Promise = require 'bluebird'
|
||||
database = require '../commons/database'
|
||||
mongoose = require 'mongoose'
|
||||
TrialRequest = require '../models/TrialRequest'
|
||||
CourseInstance = require '../models/CourseInstance'
|
||||
Classroom = require '../models/Classroom'
|
||||
Course = require '../models/Course'
|
||||
User = require '../models/User'
|
||||
|
||||
module.exports =
|
||||
addMembers: wrap (req, res) ->
|
||||
if req.body.userID
|
||||
userIDs = [req.body.userID]
|
||||
else if req.body.userIDs
|
||||
userIDs = req.body.userIDs
|
||||
else
|
||||
throw new errors.UnprocessableEntity('Must provide userID or userIDs')
|
||||
|
||||
for userID in userIDs
|
||||
unless _.all userIDs, database.isID
|
||||
throw new errors.UnprocessableEntity('Invalid list of user IDs')
|
||||
|
||||
courseInstance = yield database.getDocFromHandle(req, CourseInstance)
|
||||
if not courseInstance
|
||||
throw new errors.NotFound('Course Instance not found.')
|
||||
|
||||
classroom = yield Classroom.findById courseInstance.get('classroomID')
|
||||
if not classroom
|
||||
throw new errors.NotFound('Classroom not found.')
|
||||
|
||||
classroomMembers = (userID.toString() for userID in classroom.get('members'))
|
||||
unless _.all(userIDs, (userID) -> _.contains classroomMembers, userID)
|
||||
throw new errors.Forbidden('Users must be members of classroom')
|
||||
|
||||
unless classroom.get('ownerID').equals(req.user._id)
|
||||
throw new errors.Forbidden('You must own the classroom to add members')
|
||||
|
||||
# Only the enrolled users
|
||||
users = yield User.find({ _id: { $in: userIDs }}).select('coursePrepaidID')
|
||||
usersArePrepaid = _.all((user.get('coursePrepaidID') for user in users))
|
||||
|
||||
course = yield Course.findById courseInstance.get('courseID')
|
||||
throw new errors.NotFound('Course referenced by course instance not found') unless course
|
||||
|
||||
if not (course.get('free') or usersArePrepaid)
|
||||
throw new errors.PaymentRequired('Cannot add users to a course instance until they are added to a prepaid')
|
||||
|
||||
userObjectIDs = (mongoose.Types.ObjectId(userID) for userID in userIDs)
|
||||
|
||||
courseInstance = yield CourseInstance.findByIdAndUpdate(
|
||||
courseInstance._id,
|
||||
{ $addToSet: { members: { $each: userObjectIDs } } }
|
||||
{ new: true }
|
||||
)
|
||||
|
||||
userUpdateResult = yield User.update(
|
||||
{ _id: { $in: userObjectIDs } },
|
||||
{ $addToSet: { courseInstances: courseInstance._id } }
|
||||
)
|
||||
|
||||
res.status(200).send(courseInstance.toObject({ req }))
|
|
@ -1,9 +1,12 @@
|
|||
module.exports =
|
||||
auth: require './auth'
|
||||
classrooms: require './classrooms'
|
||||
campaigns: require './campaigns'
|
||||
courseInstances: require './course-instances'
|
||||
files: require './files'
|
||||
named: require './named'
|
||||
patchable: require './patchable'
|
||||
rest: require './rest'
|
||||
trialRequests: require './trial-requests'
|
||||
users: require './users'
|
||||
versions: require './versions'
|
||||
versions: require './versions'
|
||||
|
|
3
server/models/Campaign.coffee
Normal file
3
server/models/Campaign.coffee
Normal file
|
@ -0,0 +1,3 @@
|
|||
# TODO: Migrate Campaign to here
|
||||
|
||||
module.exports = require '../campaigns/Campaign'
|
3
server/models/Classroom.coffee
Normal file
3
server/models/Classroom.coffee
Normal file
|
@ -0,0 +1,3 @@
|
|||
# TODO: Migrate Classroom to here
|
||||
|
||||
module.exports = require '../classrooms/Classroom'
|
3
server/models/CourseInstance.coffee
Normal file
3
server/models/CourseInstance.coffee
Normal file
|
@ -0,0 +1,3 @@
|
|||
# TODO: Migrate CourseInstance to here
|
||||
|
||||
module.exports = require '../courses/CourseInstance'
|
|
@ -1,4 +1,4 @@
|
|||
Course = require '../courses/Course'
|
||||
Course = require '../models/Course'
|
||||
Handler = require '../commons/Handler'
|
||||
slack = require '../slack'
|
||||
Prepaid = require './Prepaid'
|
||||
|
|
|
@ -23,6 +23,18 @@ module.exports.setup = (app) ->
|
|||
app.post('/db/article/:handle/watchers', mw.patchable.joinWatchers(Article))
|
||||
app.delete('/db/article/:handle/watchers', mw.patchable.leaveWatchers(Article))
|
||||
|
||||
app.get('/db/classroom', mw.classrooms.getByOwner)
|
||||
app.get('/db/classroom/:handle/member-sessions', mw.classrooms.fetchMemberSessions)
|
||||
app.get('/db/classroom/:handle/members', mw.classrooms.fetchMembers) # TODO: Use mw.auth?
|
||||
|
||||
Course = require '../models/Course'
|
||||
app.get('/db/course', mw.rest.get(Course))
|
||||
app.get('/db/course/:handle', mw.rest.getByHandle(Course))
|
||||
|
||||
app.get('/db/campaign', mw.campaigns.fetchByType) #TODO
|
||||
|
||||
app.post('/db/course_instance/:handle/members', mw.auth.checkLoggedIn(), mw.courseInstances.addMembers)
|
||||
|
||||
app.get('/db/user', mw.users.fetchByGPlusID, mw.users.fetchByFacebookID)
|
||||
|
||||
app.get '/db/products', require('./db/product').get
|
||||
|
|
|
@ -365,7 +365,12 @@ UserSchema.set('toObject', {
|
|||
publicOnly = options.publicOnly
|
||||
delete ret[prop] for prop in User.serverProperties
|
||||
includePrivates = not publicOnly and (req.user and (req.user.isAdmin() or req.user._id.equals(doc._id) or req.session.amActually is doc.id))
|
||||
delete ret[prop] for prop in User.privateProperties unless includePrivates
|
||||
if options.includedPrivates
|
||||
excludedPrivates = _.reject User.privateProperties, (prop) ->
|
||||
prop in options.includedPrivates
|
||||
else
|
||||
excludedPrivates = User.privateProperties
|
||||
delete ret[prop] for prop in excludedPrivates unless includePrivates
|
||||
delete ret[prop] for prop in User.candidateProperties
|
||||
return ret
|
||||
})
|
||||
|
|
|
@ -19,7 +19,7 @@ models_path = [
|
|||
'../../server/campaigns/Campaign'
|
||||
'../../server/clans/Clan'
|
||||
'../../server/classrooms/Classroom'
|
||||
'../../server/courses/Course'
|
||||
'../../server/models/Course'
|
||||
'../../server/courses/CourseInstance'
|
||||
'../../server/levels/Level'
|
||||
'../../server/levels/components/LevelComponent'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
require '../common'
|
||||
User = require '../../../server/users/User'
|
||||
utils = require '../utils'
|
||||
_ = require 'lodash'
|
||||
Promise = require 'bluebird'
|
||||
|
@ -334,4 +335,4 @@ describe 'POST /auth/stop-spying', ->
|
|||
expect(body._id).toBe(@admin.id)
|
||||
[res, body] = yield request.getAsync {uri: getURL('/auth/whoami'), json: true}
|
||||
expect(body._id).toBe(@admin.id)
|
||||
done()
|
||||
done()
|
||||
|
|
36
spec/server/functional/campaigns.spec.coffee
Normal file
36
spec/server/functional/campaigns.spec.coffee
Normal file
|
@ -0,0 +1,36 @@
|
|||
config = require '../../../server_config'
|
||||
require '../common'
|
||||
clientUtils = require '../../../app/core/utils' # Must come after require /common
|
||||
mongoose = require 'mongoose'
|
||||
utils = require '../utils'
|
||||
_ = require 'lodash'
|
||||
Promise = require 'bluebird'
|
||||
requestAsync = Promise.promisify(request, {multiArgs: true})
|
||||
|
||||
campaignURL = getURL('/db/campaign')
|
||||
|
||||
describe '/db/campaign', ->
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([Campaign])
|
||||
@heroCampaign1 = yield new Campaign({name: 'Hero Campaign 1', type: 'hero'}).save()
|
||||
@heroCampaign2 = yield new Campaign({name: 'Hero Campaign 2', type: 'hero'}).save()
|
||||
@courseCampaign1 = yield new Campaign({name: 'Course Campaign 1', type: 'course'}).save()
|
||||
@courseCampaign2 = yield new Campaign({name: 'Course Campaign 2', type: 'course'}).save()
|
||||
done()
|
||||
|
||||
describe 'GET campaigns of a certain type', ->
|
||||
it 'returns only that type', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync getURL('/db/campaign?type=course'), { json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(2)
|
||||
for campaign in body
|
||||
expect(campaign.type).toBe('course')
|
||||
done()
|
||||
|
||||
describe 'GET all campaigns', ->
|
||||
it 'returns all of them regardless of type', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync getURL('/db/campaign'), { json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(4)
|
||||
done()
|
|
@ -1,38 +1,38 @@
|
|||
config = require '../../../server_config'
|
||||
require '../common'
|
||||
utils = require '../../../app/core/utils' # Must come after require /common
|
||||
clientUtils = require '../../../app/core/utils' # Must come after require /common
|
||||
mongoose = require 'mongoose'
|
||||
utils = require '../utils'
|
||||
_ = require 'lodash'
|
||||
Promise = require 'bluebird'
|
||||
requestAsync = Promise.promisify(request, {multiArgs: true})
|
||||
|
||||
classroomsURL = getURL('/db/classroom')
|
||||
|
||||
describe 'GET /db/classroom?ownerID=:id', ->
|
||||
it 'clears database users and classrooms', (done) ->
|
||||
clearModels [User, Classroom], (err) ->
|
||||
throw err if err
|
||||
done()
|
||||
|
||||
it 'returns an array of classrooms with the given owner', (done) ->
|
||||
loginNewUser (user1) ->
|
||||
new Classroom({name: 'Classroom 1', ownerID: user1.get('_id') }).save (err, classroom) ->
|
||||
expect(err).toBeNull()
|
||||
loginNewUser (user2) ->
|
||||
new Classroom({name: 'Classroom 2', ownerID: user2.get('_id') }).save (err, classroom) ->
|
||||
expect(err).toBeNull()
|
||||
url = getURL('/db/classroom?ownerID='+user2.id)
|
||||
request.get { uri: url, json: true }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(1)
|
||||
expect(body[0].name).toBe('Classroom 2')
|
||||
done()
|
||||
|
||||
it 'returns 403 when a non-admin tries to get classrooms for another user', (done) ->
|
||||
loginNewUser (user1) ->
|
||||
loginNewUser (user2) ->
|
||||
url = getURL('/db/classroom?ownerID='+user1.id)
|
||||
request.get { uri: url }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([User, Classroom])
|
||||
@user1 = yield utils.initUser()
|
||||
yield utils.loginUser(@user1)
|
||||
@classroom1 = yield new Classroom({name: 'Classroom 1', ownerID: @user1.get('_id') }).save()
|
||||
@user2 = yield utils.initUser()
|
||||
yield utils.loginUser(@user2)
|
||||
@classroom2 = yield new Classroom({name: 'Classroom 2', ownerID: @user2.get('_id') }).save()
|
||||
done()
|
||||
|
||||
it 'returns an array of classrooms with the given owner', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync getURL('/db/classroom?ownerID='+@user2.id), { json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(1)
|
||||
expect(body[0].name).toBe('Classroom 2')
|
||||
done()
|
||||
|
||||
it 'returns 403 when a non-admin tries to get classrooms for another user', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync getURL('/db/classroom?ownerID='+@user1.id), { json: true }
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
|
||||
describe 'GET /db/classroom/:id', ->
|
||||
it 'clears database users and classrooms', (done) ->
|
||||
|
@ -54,7 +54,7 @@ describe 'GET /db/classroom/:id', ->
|
|||
done()
|
||||
|
||||
describe 'POST /db/classroom', ->
|
||||
|
||||
|
||||
it 'clears database users and classrooms', (done) ->
|
||||
clearModels [User, Classroom], (err) ->
|
||||
throw err if err
|
||||
|
@ -71,7 +71,7 @@ describe 'POST /db/classroom', ->
|
|||
expect(body.members.length).toBe(0)
|
||||
expect(body.ownerID).toBe(user1.id)
|
||||
done()
|
||||
|
||||
|
||||
it 'does not work for anonymous users', (done) ->
|
||||
logoutUser ->
|
||||
data = { name: 'Classroom 2' }
|
||||
|
@ -79,15 +79,14 @@ describe 'POST /db/classroom', ->
|
|||
expect(res.statusCode).toBe(401)
|
||||
done()
|
||||
|
||||
# TODO: Re-enable when we enforce this again
|
||||
xit 'does not work for non-teacher users', (done) ->
|
||||
it 'does not work for non-teacher users', (done) ->
|
||||
loginNewUser (user1) ->
|
||||
data = { name: 'Classroom 1' }
|
||||
request.post {uri: classroomsURL, json: data }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
|
||||
|
||||
|
||||
describe 'PUT /db/classroom', ->
|
||||
|
||||
it 'clears database users and classrooms', (done) ->
|
||||
|
@ -108,7 +107,7 @@ describe 'PUT /db/classroom', ->
|
|||
expect(body.name).toBe('Classroom 3')
|
||||
expect(body.description).toBe('New Description')
|
||||
done()
|
||||
|
||||
|
||||
it 'is not allowed if you are just a member', (done) ->
|
||||
loginNewUser (user1) ->
|
||||
user1.set('role', 'teacher')
|
||||
|
@ -126,7 +125,7 @@ describe 'PUT /db/classroom', ->
|
|||
request.put { uri: url, json: data }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
|
||||
describe 'POST /db/classroom/~/members', ->
|
||||
|
||||
it 'clears database users and classrooms', (done) ->
|
||||
|
@ -155,8 +154,7 @@ describe 'POST /db/classroom/~/members', ->
|
|||
expect(user2.get('role')).toBe('student')
|
||||
done()
|
||||
|
||||
# TODO: Re-enable when we enforce this again
|
||||
xit 'does not work if the user is a teacher', (done) ->
|
||||
it 'does not work if the user is a teacher', (done) ->
|
||||
loginNewUser (user1) ->
|
||||
user1.set('role', 'teacher')
|
||||
user1.save (err) ->
|
||||
|
@ -176,6 +174,18 @@ describe 'POST /db/classroom/~/members', ->
|
|||
expect(classroom.get('members').length).toBe(0)
|
||||
done()
|
||||
|
||||
it 'does not work if the user is anonymous', utils.wrap (done) ->
|
||||
yield utils.clearModels([User, Classroom])
|
||||
teacher = yield utils.initUser({role: 'teacher'})
|
||||
yield utils.loginUser(teacher)
|
||||
[res, body] = yield request.postAsync {uri: classroomsURL, json: { name: 'Classroom' } }
|
||||
expect(res.statusCode).toBe(200)
|
||||
classroomCode = body.code
|
||||
yield utils.becomeAnonymous()
|
||||
[res, body] = yield request.postAsync { uri: getURL("/db/classroom/~/members"), json: { code: classroomCode } }
|
||||
expect(res.statusCode).toBe(401)
|
||||
done()
|
||||
|
||||
|
||||
describe 'DELETE /db/classroom/:id/members', ->
|
||||
|
||||
|
@ -223,3 +233,95 @@ describe 'POST /db/classroom/:id/invite-members', ->
|
|||
request.post { uri: url, json: data }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
done()
|
||||
|
||||
|
||||
describe 'GET /db/classroom/:handle/member-sessions', ->
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([User, Classroom, LevelSession, Level])
|
||||
@artisan = yield utils.initUser()
|
||||
@teacher = yield utils.initUser()
|
||||
@student1 = yield utils.initUser()
|
||||
@student2 = yield utils.initUser()
|
||||
@levelA = new Level({name: 'Level A', permissions: [{target: @artisan._id, access: 'owner'}]})
|
||||
@levelA.set('original', @levelA._id)
|
||||
@levelA = yield @levelA.save()
|
||||
@levelB = new Level({name: 'Level B', permissions: [{target: @artisan._id, access: 'owner'}]})
|
||||
@levelB.set('original', @levelB._id)
|
||||
@levelB = yield @levelB.save()
|
||||
@classroom = yield new Classroom({name: 'Classroom', ownerID: @teacher._id, members: [@student1._id, @student2._id] }).save()
|
||||
@session1A = yield new LevelSession({creator: @student1.id, state: { complete: true }, level: {original: @levelA._id}, permissions: [{target: @student1._id, access: 'owner'}]}).save()
|
||||
@session1B = yield new LevelSession({creator: @student1.id, state: { complete: false }, level: {original: @levelB._id}, permissions: [{target: @student1._id, access: 'owner'}]}).save()
|
||||
@session2A = yield new LevelSession({creator: @student2.id, state: { complete: true }, level: {original: @levelA._id}, permissions: [{target: @student2._id, access: 'owner'}]}).save()
|
||||
@session2B = yield new LevelSession({creator: @student2.id, state: { complete: false }, level: {original: @levelB._id}, permissions: [{target: @student2._id, access: 'owner'}]}).save()
|
||||
done()
|
||||
|
||||
it 'returns all sessions for all members in the classroom with only properties level, creator and state.complete', utils.wrap (done) ->
|
||||
yield utils.loginUser(@teacher)
|
||||
[res, body] = yield request.getAsync getURL("/db/classroom/#{@classroom.id}/member-sessions"), { json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(4)
|
||||
done()
|
||||
|
||||
it 'does not work if you are not the owner of the classroom', utils.wrap (done) ->
|
||||
yield utils.loginUser(@student1)
|
||||
[res, body] = yield request.getAsync getURL("/db/classroom/#{@classroom.id}/member-sessions"), { json: true }
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
it 'does not work if you are not logged in', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync getURL("/db/classroom/#{@classroom.id}/member-sessions"), { json: true }
|
||||
expect(res.statusCode).toBe(401)
|
||||
done()
|
||||
|
||||
it 'accepts memberSkip and memberLimit GET parameters', utils.wrap (done) ->
|
||||
yield utils.loginUser(@teacher)
|
||||
[res, body] = yield request.getAsync getURL("/db/classroom/#{@classroom.id}/member-sessions?memberLimit=1"), { json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(2)
|
||||
expect(session.creator).toBe(@student1.id) for session in body
|
||||
[res, body] = yield request.getAsync getURL("/db/classroom/#{@classroom.id}/member-sessions?memberSkip=1"), { json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(2)
|
||||
expect(session.creator).toBe(@student2.id) for session in body
|
||||
done()
|
||||
|
||||
describe 'GET /db/classroom/:handle/members', ->
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([User, Classroom])
|
||||
@teacher = yield utils.initUser()
|
||||
@student1 = yield utils.initUser({ name: "Firstname Lastname" })
|
||||
@student2 = yield utils.initUser({ name: "Student Nameynamington" })
|
||||
@classroom = yield new Classroom({name: 'Classroom', ownerID: @teacher._id, members: [@student1._id, @student2._id] }).save()
|
||||
@emptyClassroom = yield new Classroom({name: 'Empty Classroom', ownerID: @teacher._id, members: [] }).save()
|
||||
done()
|
||||
|
||||
it 'does not work if you are not the owner of the classroom', utils.wrap (done) ->
|
||||
yield utils.loginUser(@student1)
|
||||
[res, body] = yield request.getAsync getURL("/db/classroom/#{@classroom.id}/member-sessions"), { json: true }
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
||||
it 'does not work if you are not logged in', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync getURL("/db/classroom/#{@classroom.id}/member-sessions"), { json: true }
|
||||
expect(res.statusCode).toBe(401)
|
||||
done()
|
||||
|
||||
it 'works on an empty classroom', utils.wrap (done) ->
|
||||
yield utils.loginUser(@teacher)
|
||||
[res, body] = yield request.getAsync getURL("/db/classroom/#{@emptyClassroom.id}/members?name=true&email=true"), { json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body).toEqual([])
|
||||
done()
|
||||
|
||||
it 'returns all members with name and email', utils.wrap (done) ->
|
||||
yield utils.loginUser(@teacher)
|
||||
[res, body] = yield request.getAsync getURL("/db/classroom/#{@classroom.id}/members?name=true&email=true"), { json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toBe(2)
|
||||
for user in body
|
||||
expect(user.name).toBeDefined()
|
||||
expect(user.email).toBeDefined()
|
||||
expect(user.passwordHash).toBeUndefined()
|
||||
done()
|
|
@ -3,6 +3,7 @@ config = require '../../../server_config'
|
|||
require '../common'
|
||||
stripe = require('stripe')(config.stripe.secretKey)
|
||||
init = require '../init'
|
||||
utils = require '../utils'
|
||||
|
||||
describe 'POST /db/course_instance', ->
|
||||
|
||||
|
@ -82,6 +83,7 @@ describe 'POST /db/course_instance', ->
|
|||
|
||||
|
||||
describe 'POST /db/course_instance/:id/members', ->
|
||||
#TODO: Refactor to new yield system! @scott
|
||||
|
||||
beforeEach (done) -> clearModels([CourseInstance, Course, User, Classroom, Prepaid], done)
|
||||
beforeEach (done) -> loginJoe (@joe) => done()
|
||||
|
@ -91,6 +93,20 @@ describe 'POST /db/course_instance/:id/members', ->
|
|||
beforeEach init.user()
|
||||
beforeEach init.prepaid()
|
||||
|
||||
it 'adds an array of members to the given CourseInstance', (done) ->
|
||||
async.eachSeries([
|
||||
|
||||
addTestUserToClassroom,
|
||||
(test, cb) ->
|
||||
url = getURL("/db/course_instance/#{test.courseInstance.id}/members")
|
||||
request.post {uri: url, json: {userIDs: [test.user.id]}}, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.members.length).toBe(1)
|
||||
expect(body.members[0]).toBe(test.user.id)
|
||||
cb()
|
||||
|
||||
], makeTestIterator(@), done)
|
||||
|
||||
it 'adds a member to the given CourseInstance', (done) ->
|
||||
async.eachSeries([
|
||||
|
||||
|
@ -182,8 +198,8 @@ describe 'POST /db/course_instance/:id/members', ->
|
|||
test.classroom.save cb
|
||||
|
||||
addTestUserToPrepaid = (test, cb) ->
|
||||
test.prepaid.set('redeemers', [{userID: test.user.get('_id')}])
|
||||
test.prepaid.save cb
|
||||
test.user.set('coursePrepaidID', test.prepaid._id)
|
||||
test.user.save cb
|
||||
|
||||
|
||||
describe 'DELETE /db/course_instance/:id/members', ->
|
||||
|
@ -248,4 +264,4 @@ describe 'DELETE /db/course_instance/:id/members', ->
|
|||
|
||||
|
||||
makeTestIterator = (testObject) -> (func, callback) -> func(testObject, callback)
|
||||
|
||||
|
||||
|
|
45
spec/server/functional/courses.spec.coffee
Normal file
45
spec/server/functional/courses.spec.coffee
Normal file
|
@ -0,0 +1,45 @@
|
|||
require '../common'
|
||||
utils = require '../utils'
|
||||
_ = require 'lodash'
|
||||
Promise = require 'bluebird'
|
||||
requestAsync = Promise.promisify(request, {multiArgs: true})
|
||||
|
||||
describe 'GET /db/course', ->
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([Course, User])
|
||||
yield new Course({ name: 'Course 1' }).save()
|
||||
yield new Course({ name: 'Course 2' }).save()
|
||||
done()
|
||||
|
||||
|
||||
it 'returns an array of Course objects', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: getURL('/db/course'), json: true }
|
||||
expect(body.length).toBe(2)
|
||||
done()
|
||||
|
||||
describe 'GET /db/course/:handle', ->
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([Course, User])
|
||||
@course = yield new Course({ name: 'Some Name' }).save()
|
||||
done()
|
||||
|
||||
|
||||
it 'returns Course by id', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL("/db/course/#{@course.id}"), json: true}
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body._id).toBe(@course.id)
|
||||
done()
|
||||
|
||||
|
||||
it 'returns Course by slug', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL("/db/course/some-name"), json: true}
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body._id).toBe(@course.id)
|
||||
done()
|
||||
|
||||
|
||||
it 'returns not found if handle does not exist in the db', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {uri: getURL("/db/course/dne"), json: true}
|
||||
expect(res.statusCode).toBe(404)
|
||||
done()
|
34
test/app/fixtures/campaigns.coffee
Normal file
34
test/app/fixtures/campaigns.coffee
Normal file
|
@ -0,0 +1,34 @@
|
|||
Campaign = require 'models/Campaign';
|
||||
Campaigns = require 'collections/Campaigns';
|
||||
|
||||
module.exports = new Campaigns([
|
||||
new Campaign({
|
||||
_id: 'campaign0'
|
||||
levels: [
|
||||
{
|
||||
_id: 'level0_0'
|
||||
original: 'level0_0'
|
||||
name: 'level0_0'
|
||||
type: 'hero'
|
||||
},
|
||||
{
|
||||
_id: 'level0_1'
|
||||
original: 'level0_1'
|
||||
name: 'level0_1'
|
||||
type: 'hero'
|
||||
},
|
||||
{
|
||||
_id: 'level0_2'
|
||||
original: 'level0_2'
|
||||
name: 'level0_2'
|
||||
type: 'hero'
|
||||
},
|
||||
{
|
||||
_id: 'level0_3'
|
||||
original: 'level0_3'
|
||||
name: 'level0_3'
|
||||
type: 'hero'
|
||||
},
|
||||
]
|
||||
}),
|
||||
])
|
40
test/app/fixtures/classrooms.coffee
Normal file
40
test/app/fixtures/classrooms.coffee
Normal file
|
@ -0,0 +1,40 @@
|
|||
Classroom = require 'models/Classroom';
|
||||
Classrooms = require 'collections/Classrooms';
|
||||
|
||||
module.exports = new Classrooms([
|
||||
{
|
||||
_id: "classroom0",
|
||||
name: "Teacher Zero's Other Classroom"
|
||||
ownerID: "teacher0",
|
||||
aceConfig:
|
||||
language: 'python'
|
||||
members: []
|
||||
}
|
||||
|
||||
{
|
||||
_id: "classroom1",
|
||||
name: "Teacher Zero's Classroomiest Classroom"
|
||||
members: [
|
||||
"student0",
|
||||
"student1",
|
||||
"student2",
|
||||
"student3",
|
||||
],
|
||||
ownerID: "teacher0",
|
||||
aceConfig:
|
||||
language: 'python'
|
||||
}
|
||||
|
||||
{
|
||||
_id: "classroom_archived",
|
||||
name: "Teacher Zero's Archived Classroom"
|
||||
members: [
|
||||
"student0",
|
||||
"student4",
|
||||
],
|
||||
ownerID: "teacher0",
|
||||
aceConfig:
|
||||
language: 'python'
|
||||
archived: true
|
||||
}
|
||||
])
|
9
test/app/fixtures/course-instances.coffee
Normal file
9
test/app/fixtures/course-instances.coffee
Normal file
|
@ -0,0 +1,9 @@
|
|||
CourseInstances = require 'collections/CourseInstances';
|
||||
|
||||
module .exports = new CourseInstances([
|
||||
{
|
||||
_id: "instance0"
|
||||
courseID: "course0",
|
||||
classroomID: "classroom0"
|
||||
},
|
||||
])
|
16
test/app/fixtures/courses.coffee
Normal file
16
test/app/fixtures/courses.coffee
Normal file
|
@ -0,0 +1,16 @@
|
|||
Courses = require 'collections/Courses';
|
||||
|
||||
module.exports = new Courses(
|
||||
[
|
||||
{
|
||||
_id: "course0",
|
||||
name: "course0",
|
||||
campaignID: "campaign0",
|
||||
},
|
||||
{
|
||||
_id: "course1",
|
||||
name: "course1",
|
||||
campaignID: "campaign1",
|
||||
},
|
||||
]
|
||||
)
|
121
test/app/fixtures/level-sessions-completed.coffee
Normal file
121
test/app/fixtures/level-sessions-completed.coffee
Normal file
|
@ -0,0 +1,121 @@
|
|||
LevelSessions = require 'collections/LevelSessions';
|
||||
|
||||
module.exports = new LevelSessions(
|
||||
[
|
||||
{
|
||||
level:
|
||||
original: "level0_0",
|
||||
creator: "student0",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_1",
|
||||
creator: "student0",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_2",
|
||||
creator: "student0",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_3",
|
||||
creator: "student0",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
|
||||
{
|
||||
level:
|
||||
original: "level0_0",
|
||||
creator: "student1",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_1",
|
||||
creator: "student1",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_2",
|
||||
creator: "student1",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_3",
|
||||
creator: "student1",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
|
||||
{
|
||||
level:
|
||||
original: "level0_0",
|
||||
creator: "student2",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_1",
|
||||
creator: "student2",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_2",
|
||||
creator: "student2",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_3",
|
||||
creator: "student2",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
|
||||
{
|
||||
level:
|
||||
original: "level0_0",
|
||||
creator: "student3",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_1",
|
||||
creator: "student3",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_2",
|
||||
creator: "student3",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_3",
|
||||
creator: "student3",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
]
|
||||
)
|
69
test/app/fixtures/level-sessions-partially-completed.coffee
Normal file
69
test/app/fixtures/level-sessions-partially-completed.coffee
Normal file
|
@ -0,0 +1,69 @@
|
|||
LevelSessions = require 'collections/LevelSessions';
|
||||
|
||||
module.exports = new LevelSessions(
|
||||
[
|
||||
# student0 - 4/4
|
||||
{
|
||||
level:
|
||||
original: "level0_0",
|
||||
creator: "student0",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_1",
|
||||
creator: "student0",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_2",
|
||||
creator: "student0",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_3",
|
||||
creator: "student0",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
|
||||
# student1 - 2.5/4
|
||||
{
|
||||
level:
|
||||
original: "level0_0",
|
||||
creator: "student1",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_1",
|
||||
creator: "student1",
|
||||
state:
|
||||
"complete": true
|
||||
},
|
||||
{
|
||||
level:
|
||||
original: "level0_2",
|
||||
creator: "student1",
|
||||
state:
|
||||
"complete": false
|
||||
},
|
||||
|
||||
# student2 - 0.5/4
|
||||
{
|
||||
level:
|
||||
original: "level0_0",
|
||||
creator: "student2",
|
||||
state:
|
||||
"complete": false
|
||||
},
|
||||
|
||||
# student3 - 0/4
|
||||
]
|
||||
)
|
11
test/app/fixtures/prepaids.coffee
Normal file
11
test/app/fixtures/prepaids.coffee
Normal file
|
@ -0,0 +1,11 @@
|
|||
Prepaids = require 'collections/Prepaids';
|
||||
|
||||
module.exports = new Prepaids([
|
||||
{
|
||||
_id: 'unused-prepaid'
|
||||
creator: 'teacher1'
|
||||
exhausted: false
|
||||
maxRedeemers: 2
|
||||
redeemers: []
|
||||
}
|
||||
])
|
40
test/app/fixtures/students.coffee
Normal file
40
test/app/fixtures/students.coffee
Normal file
|
@ -0,0 +1,40 @@
|
|||
Users = require 'collections/Users';
|
||||
|
||||
module.exports = new Users(
|
||||
[
|
||||
{
|
||||
_id: "student0"
|
||||
name: "Student Zero"
|
||||
}
|
||||
|
||||
{
|
||||
_id: "student1"
|
||||
name: "Student One"
|
||||
}
|
||||
|
||||
{
|
||||
_id: "student2"
|
||||
name: "Student Two"
|
||||
}
|
||||
|
||||
{
|
||||
_id: "student3"
|
||||
name: "Student Three"
|
||||
}
|
||||
|
||||
{
|
||||
_id: "student4"
|
||||
name: "Student Four"
|
||||
}
|
||||
|
||||
{
|
||||
_id: "student5"
|
||||
name: "Student Five"
|
||||
}
|
||||
|
||||
{
|
||||
_id: "student6"
|
||||
name: "Student Six"
|
||||
}
|
||||
]
|
||||
)
|
42
test/app/fixtures/teacher.coffee
Normal file
42
test/app/fixtures/teacher.coffee
Normal file
|
@ -0,0 +1,42 @@
|
|||
User = require 'models/User';
|
||||
|
||||
module.exports = new User(
|
||||
{
|
||||
"_id": "teacher1",
|
||||
"testGroupNumber": 169,
|
||||
"anonymous": false,
|
||||
"__v": 0,
|
||||
"email": "teacher1@example.com",
|
||||
"emails": {
|
||||
"recruitNotes": {
|
||||
"enabled": true
|
||||
},
|
||||
"anyNotes": {
|
||||
"enabled": true
|
||||
},
|
||||
"generalNews": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"name": "Teacher Teacherson",
|
||||
"slug": "teacher-teacherson",
|
||||
"points": 20,
|
||||
"earned": {
|
||||
"gems": 0
|
||||
},
|
||||
"referrer": "http://localhost:3000/",
|
||||
"activity": {
|
||||
"login": {
|
||||
"last": "2016-03-07T19:57:05.007Z",
|
||||
"count": 8,
|
||||
"first": "2016-02-26T23:59:15.181Z"
|
||||
}
|
||||
},
|
||||
"volume": 1,
|
||||
"role": "teacher",
|
||||
"stripe": {
|
||||
"customerID": "cus_80OTFCpv2hArmT"
|
||||
},
|
||||
"dateCreated": "2016-02-26T23:49:23.696Z"
|
||||
}
|
||||
)
|
119
test/app/lib/CoursesHelper.spec.coffee
Normal file
119
test/app/lib/CoursesHelper.spec.coffee
Normal file
|
@ -0,0 +1,119 @@
|
|||
helper = require 'lib/coursesHelper'
|
||||
Campaigns = require 'collections/Campaigns'
|
||||
Users = require 'collections/Users'
|
||||
Courses = require 'collections/Courses'
|
||||
CourseInstances = require 'collections/CourseInstances'
|
||||
Classrooms = require 'collections/Classrooms'
|
||||
|
||||
# These got broken by changes to fixtures :(
|
||||
xdescribe 'CoursesHelper', ->
|
||||
|
||||
describe 'calculateAllProgress', ->
|
||||
|
||||
beforeEach ->
|
||||
# classrooms, courses, campaigns, courseInstances, students
|
||||
@classrooms = require 'test/app/fixtures/classrooms'
|
||||
@classroom = @classrooms.models[0]
|
||||
@courses = require 'test/app/fixtures/courses'
|
||||
@course = @courses.models[0]
|
||||
@campaigns = require 'test/app/fixtures/campaigns'
|
||||
@campaign = @campaigns.models[0]
|
||||
@students = require 'test/app/fixtures/students'
|
||||
|
||||
describe 'when all students have completed a course', ->
|
||||
beforeEach ->
|
||||
@classroom.sessions = require 'test/app/fixtures/level-sessions-completed'
|
||||
@courseInstances = require 'test/app/fixtures/course-instances'
|
||||
|
||||
describe 'progressData.get({classroom, course})', ->
|
||||
it 'returns object with .completed=true and .started=true', ->
|
||||
progressData = helper.calculateAllProgress(@classrooms, @courses, @campaigns, @courseInstances, @students)
|
||||
progress = progressData.get {@classroom, @course}
|
||||
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', ->
|
||||
for student in @students.models
|
||||
progressData = helper.calculateAllProgress(@classrooms, @courses, @campaigns, @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)
|
||||
for level in @campaign.getLevels().models
|
||||
progress = progressData.get {@classroom, @course, level}
|
||||
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)
|
||||
for level in @campaign.getLevels().models
|
||||
for user in @students.models
|
||||
progress = progressData.get {@classroom, @course, level, user}
|
||||
expect(progress.completed).toBe true
|
||||
expect(progress.started).toBe true
|
||||
|
||||
describe 'when NOT all students have completed a course', ->
|
||||
|
||||
beforeEach ->
|
||||
@classroom.sessions = require 'test/app/fixtures/level-sessions-partially-completed'
|
||||
@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)
|
||||
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)
|
||||
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)
|
||||
student = @students.get('student0')
|
||||
progress = progressData.get {@classroom, @course, user: student}
|
||||
expect(progress.completed).toBe true
|
||||
expect(progress.started).toBe true
|
||||
|
||||
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)
|
||||
student = @students.get('student1')
|
||||
progress = progressData.get {@classroom, @course, user: student}
|
||||
expect(progress.completed).toBe false
|
||||
expect(progress.started).toBe true
|
||||
|
||||
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)
|
||||
student = @students.get('student0')
|
||||
for level in @campaign.getLevels().models
|
||||
progress = progressData.get {@classroom, @course, level, user: student}
|
||||
expect(progress.completed).toBe true
|
||||
expect(progress.started).toBe true
|
||||
|
||||
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)
|
||||
user = @students.get('student2')
|
||||
level = @campaign.getLevels().get('level0_0')
|
||||
progress = progressData.get {@classroom, @course, level, user}
|
||||
expect(progress.completed).toBe false
|
||||
expect(progress.started).toBe true
|
||||
|
||||
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)
|
||||
user = @students.get('student3')
|
||||
level = @campaign.getLevels().get('level0_0')
|
||||
progress = progressData.get {@classroom, @course, level, user}
|
||||
expect(progress.completed).toBe false
|
||||
expect(progress.started).toBe false
|
109
test/app/views/teachers/ActivateLicensesModal.spec.coffee
Normal file
109
test/app/views/teachers/ActivateLicensesModal.spec.coffee
Normal file
|
@ -0,0 +1,109 @@
|
|||
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
|
||||
Users = require 'collections/Users'
|
||||
forms = require 'core/forms'
|
||||
|
||||
# Needs some fixing
|
||||
xdescribe 'ActivateLicensesModal', ->
|
||||
|
||||
@modal = null
|
||||
|
||||
me = require 'test/app/fixtures/teacher'
|
||||
prepaids = require 'test/app/fixtures/prepaids'
|
||||
classrooms = require 'test/app/fixtures/classrooms' # TODO: Don't use archived ones
|
||||
users = require 'test/app/fixtures/students'
|
||||
responses = {
|
||||
'/db/prepaid': prepaids.toJSON()
|
||||
'/db/classroom': classrooms.toJSON()
|
||||
'/db/users': users.toJSON() # TODO: Respond with different ones for different classrooms
|
||||
}
|
||||
|
||||
makeModal = (options) ->
|
||||
(done) ->
|
||||
@selectedUsers = new Users(@users.models.slice(0,(options?.numSelected or 3)))
|
||||
@modal = new ActivateLicensesModal({
|
||||
@classroom, @users, @selectedUsers
|
||||
})
|
||||
jasmine.Ajax.requests.sendResponses(responses)
|
||||
jasmine.demoModal(@modal)
|
||||
_.defer done
|
||||
|
||||
beforeEach ->
|
||||
@classroom = classrooms.get('classroom1')
|
||||
@users = require 'test/app/fixtures/students'
|
||||
|
||||
afterEach ->
|
||||
@modal.stopListening()
|
||||
|
||||
describe 'the class dropdown', ->
|
||||
beforeEach makeModal()
|
||||
|
||||
# punted indefinitely
|
||||
xit 'should contain an All Students option', ->
|
||||
expect(@modal.$('select option:last-child').html()).toBe('All Students')
|
||||
|
||||
it 'should display the current classname', ->
|
||||
expect(@modal.$('option:selected').html()).toBe('Teacher Zero\'s Classroomiest Classroom')
|
||||
|
||||
it 'should contain all of the teacher\'s classes'
|
||||
|
||||
it 'shouldn\'t contain anyone else\'s classrooms'
|
||||
|
||||
describe 'the checklist of students', ->
|
||||
it 'should separate the unenrolled from the enrolled students'
|
||||
|
||||
it 'should have a checkmark by the selected students'
|
||||
|
||||
it 'should display all the students'
|
||||
|
||||
|
||||
describe 'the credits availble count', ->
|
||||
beforeEach makeModal()
|
||||
it 'should match the number of unused prepaids', ->
|
||||
expect(@modal.$('#total-available').html()).toBe('2')
|
||||
|
||||
describe 'the Enroll button', ->
|
||||
beforeEach makeModal()
|
||||
it 'should show the number of selected students', ->
|
||||
expect(@modal.$('#total-selected-span').html()).toBe('3')
|
||||
|
||||
it 'should fire off one request when clicked'
|
||||
|
||||
describe 'when the teacher has enough enrollments', ->
|
||||
beforeEach makeModal({ numSelected: 2 })
|
||||
it 'should be enabled', ->
|
||||
expect(@modal.$('#activate-licenses-btn').hasClass('disabled')).toBe(false)
|
||||
|
||||
describe 'when the teacher doesn\'t have enough enrollments', ->
|
||||
it 'should be disabled', ->
|
||||
expect(@modal.$('#activate-licenses-btn').hasClass('disabled')).toBe(true)
|
||||
|
||||
describe 'the Purchase More button', ->
|
||||
it 'should redirect to the enrollment purchasing page'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#
|
||||
# describe 'enroll button', ->
|
||||
# beforeEach (done) ->
|
||||
# makeModal.bind(this)(done)
|
||||
#
|
||||
# it 'should display the correct total number of credits', ->
|
||||
# expect(@modal.$('#total-available').html()).toBe('2')
|
||||
#
|
||||
# it 'should be disabled when teacher doesn\'t have enough enrollments', ->
|
||||
# expect(@modal.$('#total-available').html()).toBe('2')
|
||||
#
|
||||
#
|
||||
#
|
||||
# describe 'when enrolling only a single student', ->
|
||||
# describe 'the list of students', ->
|
||||
# it 'should only have the one student selected'
|
||||
#
|
||||
# describe 'when bulk-enrolling students', ->
|
||||
# describe 'the list of students', ->
|
||||
# it 'should have the right students selected'
|
||||
#
|
||||
# describe 'selecting more students', ->
|
||||
# it 'should increase the student counter'
|
2
vendor/scripts/jasmine-mock-ajax.js
vendored
2
vendor/scripts/jasmine-mock-ajax.js
vendored
|
@ -529,7 +529,7 @@ getJasmineRequireObj().AjaxRequestTracker = function() {
|
|||
var requests = jasmine.Ajax.requests.all().slice();
|
||||
for(var j in requests) {
|
||||
var request = requests[j];
|
||||
if(_.string.startsWith(request.url, url)) {
|
||||
if(_.string.startsWith(request.url, url) && request.readyState < 4) {
|
||||
request.respondWith({status: 200, responseText: JSON.stringify(responseBody)});
|
||||
responded = true;
|
||||
break;
|
||||
|
|
Loading…
Reference in a new issue