mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-23 15:48:11 -05:00
Refactor and update teacher-dashboard
This updates TeacherClassView and ActivateLicensesModal to use the new state-based rendering system, making it much snappier and less clunky feeling, and improving data consistency. Features also included in this: - Hover details for progress dots in TeacherClassView - ActivateLicensesModal has an "All Students" option and better handling when you switch classrooms in the dropdown - Unenrolled/Unassigned students are shown separately in Course Progress and can be enrolled/assigned from there. Add Back to Classes button on demo-request submitted view Delete temporary patch file Show unenrolled students separately in Course Progress (incomplete) Migrate TeacherClassView to use orchestrator-style events, add unassigned students section, replace bootstrap tabs with state-based tabs Convert missed instance variables to be in @state Fix merge errors (in progress) Convert a bunch of stuff to use state and events (removing student needs fixing) Fix up modal interactions, some bugs Switch state to be a Model, sync up course dropdowns Convert student sorting to use state model Add hover tooltips to TeacherClassView Students tab Don't keep tooltip open when you mouse into it Add dateFirstCompleted and Course Progress tooltips Course Overview progress tooltips Refactor ActivateLicensesModal Refactors: Uses state object for view state Passes back the updated users in 'redeem-users' event instead of modifying given collection Features: Add 'All Students' dropdown option Don't forget checked students if you change classroom from dropdown, but only enroll the ones visible when you click "Enroll (n) Students" Separate enrolled students; improve style Rearrange error text Disable enroll-students button when none are selected Remove console.logs Move style-flat variables to another file This prevents .style-flat from being copied in multiple times to the resulting CSS. Show Unarchive button when on the page for an archived class Move text to en.coffee Only sort students on first classroom sync Fix merge error Handle sessions missing completion date in view logic instead of migration script Listen to classroom sync more than once in case it gets unarchived
This commit is contained in:
parent
0ed99565d3
commit
8223122a6b
26 changed files with 564 additions and 227 deletions
|
@ -139,16 +139,21 @@ module.exports =
|
||||||
levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true})
|
levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true})
|
||||||
for level in levels.models
|
for level in levels.models
|
||||||
levelID = level.get('original')
|
levelID = level.get('original')
|
||||||
progressData[classroom.id][course.id][levelID] = { completed: students.size() > 0, started: false }
|
progressData[classroom.id][course.id][levelID] = {
|
||||||
|
completed: students.size() > 0,
|
||||||
|
started: false
|
||||||
|
numStarted: 0
|
||||||
|
# numCompleted: 0
|
||||||
|
}
|
||||||
|
|
||||||
for user in students.models
|
for user in students.models
|
||||||
userID = user.id
|
userID = user.id
|
||||||
courseProgress = progressData[classroom.id][course.id]
|
courseProgress = progressData[classroom.id][course.id]
|
||||||
courseProgress[userID] ?= { completed: true, started: false } # Only set it the first time through a user
|
courseProgress[userID] ?= { completed: true, started: false, levelsCompleted: 0 } # Only set it the first time through a user
|
||||||
courseProgress[levelID][userID] = { completed: true, started: false } # These don't matter, will always be set
|
courseProgress[levelID][userID] = { completed: true, started: false } # These don't matter, will always be set
|
||||||
session = _.find classroom.sessions.models, (session) ->
|
session = _.find classroom.sessions.models, (session) ->
|
||||||
session.get('creator') is userID and session.get('level').original is levelID
|
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
|
if not session # haven't gotten to this level yet, but might have completed others before
|
||||||
courseProgress.started ||= false #no-op
|
courseProgress.started ||= false #no-op
|
||||||
courseProgress.completed = false
|
courseProgress.completed = false
|
||||||
|
@ -158,21 +163,30 @@ module.exports =
|
||||||
courseProgress[levelID].completed = false
|
courseProgress[levelID].completed = false
|
||||||
courseProgress[levelID][userID].started = false
|
courseProgress[levelID][userID].started = false
|
||||||
courseProgress[levelID][userID].completed = false
|
courseProgress[levelID][userID].completed = false
|
||||||
|
|
||||||
if session # have gotten to the level and at least started it
|
if session # have gotten to the level and at least started it
|
||||||
courseProgress.started = true
|
courseProgress.started = true
|
||||||
courseProgress[userID].started = true
|
courseProgress[userID].started = true
|
||||||
courseProgress[levelID].started = true
|
courseProgress[levelID].started = true
|
||||||
courseProgress[levelID][userID].started = true
|
courseProgress[levelID][userID].started = true
|
||||||
|
courseProgress[levelID][userID].lastPlayed = new Date(session.get('changed'))
|
||||||
|
courseProgress[levelID].numStarted += 1
|
||||||
|
|
||||||
if session?.completed() # have finished this level
|
if session?.completed() # have finished this level
|
||||||
courseProgress.completed &&= true #no-op
|
courseProgress.completed &&= true #no-op
|
||||||
courseProgress[userID].completed = true
|
courseProgress[userID].completed &&= true #no-op
|
||||||
|
courseProgress[userID].levelsCompleted += 1
|
||||||
courseProgress[levelID].completed &&= true #no-op
|
courseProgress[levelID].completed &&= true #no-op
|
||||||
|
# courseProgress[levelID].numCompleted += 1
|
||||||
courseProgress[levelID][userID].completed = true
|
courseProgress[levelID][userID].completed = true
|
||||||
|
courseProgress[levelID][userID].dateFirstCompleted = new Date(session.get('dateFirstCompleted') || session.get('changed'))
|
||||||
else # level started but not completed
|
else # level started but not completed
|
||||||
courseProgress.completed = false
|
courseProgress.completed = false
|
||||||
courseProgress[userID].completed = false
|
courseProgress[userID].completed = false
|
||||||
courseProgress[levelID].completed = false
|
courseProgress[levelID].completed = false
|
||||||
courseProgress[levelID][userID].completed = false
|
courseProgress[levelID][userID].completed = false
|
||||||
|
courseProgress[levelID].dateFirstCompleted = null
|
||||||
|
courseProgress[levelID][userID].dateFirstCompleted = null
|
||||||
|
|
||||||
_.assign(progressData, progressMixin)
|
_.assign(progressData, progressMixin)
|
||||||
return progressData
|
return progressData
|
||||||
|
|
|
@ -831,6 +831,7 @@
|
||||||
thanks_header: "Request Received!"
|
thanks_header: "Request Received!"
|
||||||
thanks_sub_header: "Thanks for expressing interest in CodeCombat for your school."
|
thanks_sub_header: "Thanks for expressing interest in CodeCombat for your school."
|
||||||
thanks_p: "We'll be in touch soon! If you need to get in contact, you can reach us at:"
|
thanks_p: "We'll be in touch soon! If you need to get in contact, you can reach us at:"
|
||||||
|
back_to_classes: "Back to Classes"
|
||||||
finish_signup: "Finish creating your teacher account:"
|
finish_signup: "Finish creating your teacher account:"
|
||||||
finish_signup_p: "Create an account to set up a class, add your students, and monitor their progress as they learn computer science."
|
finish_signup_p: "Create an account to set up a class, add your students, and monitor their progress as they learn computer science."
|
||||||
signup_with: "Sign up with:"
|
signup_with: "Sign up with:"
|
||||||
|
@ -1291,6 +1292,7 @@
|
||||||
view_class: "view class"
|
view_class: "view class"
|
||||||
archive_class: "archive class"
|
archive_class: "archive class"
|
||||||
unarchive_class: "unarchive class"
|
unarchive_class: "unarchive class"
|
||||||
|
unarchive_this_class: "Unarchive this class"
|
||||||
no_students_yet: "This class has no students yet."
|
no_students_yet: "This class has no students yet."
|
||||||
add_students: "Add Students"
|
add_students: "Add Students"
|
||||||
create_new_class: "Create a New Class"
|
create_new_class: "Create a New Class"
|
||||||
|
@ -1312,6 +1314,10 @@
|
||||||
latest_completed: "Latest Completed"
|
latest_completed: "Latest Completed"
|
||||||
sort_by: "Sort by"
|
sort_by: "Sort by"
|
||||||
progress: "Progress"
|
progress: "Progress"
|
||||||
|
completed: "Completed"
|
||||||
|
started: "Started"
|
||||||
|
click_to_view_progress: "click to view progress"
|
||||||
|
no_progress: "No progress"
|
||||||
select_course: "Select course to view"
|
select_course: "Select course to view"
|
||||||
course_overview: "Course Overview"
|
course_overview: "Course Overview"
|
||||||
copy_class_code: "Copy Class Code"
|
copy_class_code: "Copy Class Code"
|
||||||
|
|
|
@ -21,7 +21,7 @@ module.exports = class CourseInstance extends CocoModel
|
||||||
data: { userID: userID }
|
data: { userID: userID }
|
||||||
}
|
}
|
||||||
_.extend options, opts
|
_.extend options, opts
|
||||||
@fetch(options)
|
@fetch options
|
||||||
if userID is me.id
|
if userID is me.id
|
||||||
unless me.get('courseInstances')
|
unless me.get('courseInstances')
|
||||||
me.set('courseInstances', [])
|
me.set('courseInstances', [])
|
||||||
|
@ -32,6 +32,8 @@ module.exports = class CourseInstance extends CocoModel
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
url: _.result(@, 'url') + '/members'
|
url: _.result(@, 'url') + '/members'
|
||||||
data: { userIDs }
|
data: { userIDs }
|
||||||
|
success: =>
|
||||||
|
@trigger 'add-members', { userIDs }
|
||||||
}
|
}
|
||||||
_.extend options, opts
|
_.extend options, opts
|
||||||
@fetch(options)
|
@fetch(options)
|
||||||
|
|
5
app/models/State.coffee
Normal file
5
app/models/State.coffee
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
CocoModel = require './CocoModel'
|
||||||
|
schema = require 'schemas/models/poll.schema'
|
||||||
|
|
||||||
|
module.exports = class State extends CocoModel
|
||||||
|
@className: 'State'
|
|
@ -54,6 +54,10 @@ _.extend LevelSessionSchema.properties,
|
||||||
changed: c.date
|
changed: c.date
|
||||||
title: 'Changed'
|
title: 'Changed'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
|
||||||
|
dateFirstCompleted: c.stringDate
|
||||||
|
title: 'Completed'
|
||||||
|
readOnly: true
|
||||||
|
|
||||||
team: c.shortString()
|
team: c.shortString()
|
||||||
level: LevelSessionLevelSchema
|
level: LevelSessionLevelSchema
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
@import "app/styles/bootstrap/variables"
|
@import "app/styles/bootstrap/variables"
|
||||||
@import "app/styles/mixins"
|
@import "app/styles/mixins"
|
||||||
@import "app/styles/style-flat"
|
@import "app/styles/style-flat-variables"
|
||||||
|
|
||||||
#about-view
|
#about-view
|
||||||
|
|
||||||
|
@ -331,4 +331,4 @@
|
||||||
content: ""
|
content: ""
|
||||||
display: block
|
display: block
|
||||||
height: 55px
|
height: 55px
|
||||||
margin: -55px 0 0 0
|
margin: -55px 0 0 0
|
||||||
|
|
|
@ -1,4 +1,13 @@
|
||||||
#activate-licenses-modal
|
#activate-licenses-modal
|
||||||
|
select
|
||||||
|
min-width: 80%
|
||||||
|
|
||||||
|
.checkbox
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
input[type='checkbox']
|
||||||
|
margin-top: 8px
|
||||||
|
|
||||||
.modal-content
|
.modal-content
|
||||||
padding: 60px
|
padding: 60px
|
||||||
width: 690px
|
width: 690px
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
@import "app/styles/bootstrap/variables"
|
@import "app/styles/bootstrap/variables"
|
||||||
@import "app/styles/mixins"
|
@import "app/styles/mixins"
|
||||||
@import "app/styles/style-flat"
|
@import "app/styles/style-flat-variables"
|
||||||
|
|
||||||
|
.nowrap
|
||||||
|
white-space: nowrap
|
||||||
|
|
||||||
.alternating-background:nth-child(2n+1)
|
.alternating-background:nth-child(2n+1)
|
||||||
background-color: #ebebeb
|
background-color: #ebebeb
|
||||||
|
@ -187,6 +190,26 @@
|
||||||
|
|
||||||
.progress-dot
|
.progress-dot
|
||||||
margin: 5px
|
margin: 5px
|
||||||
|
|
||||||
|
.unassigned-students
|
||||||
|
margin-top: 75px
|
||||||
|
line-height: 45px
|
||||||
|
.student-name, .student-email, .latest-completed
|
||||||
|
white-space: nowrap
|
||||||
|
overflow: hidden
|
||||||
|
text-overflow: ellipsis
|
||||||
|
|
||||||
|
.small-details, .small
|
||||||
|
line-height: 45px
|
||||||
|
.latest-completed
|
||||||
|
white-space: nowrap
|
||||||
|
.level-name
|
||||||
|
display: inline
|
||||||
|
|
||||||
|
.btn
|
||||||
|
margin-top: 6.5px
|
||||||
|
margin-bottom: 6.5px
|
||||||
|
|
||||||
|
|
||||||
// Checkboxes
|
// Checkboxes
|
||||||
.checkbox-flat
|
.checkbox-flat
|
||||||
|
@ -255,4 +278,4 @@
|
||||||
float: right
|
float: right
|
||||||
|
|
||||||
.export-student-progress-btn
|
.export-student-progress-btn
|
||||||
margin-top: 10px
|
margin-top: 10px
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
@import "app/styles/bootstrap/variables"
|
@import "app/styles/bootstrap/variables"
|
||||||
@import "app/styles/mixins"
|
@import "app/styles/mixins"
|
||||||
@import "app/styles/style-flat"
|
@import "app/styles/style-flat-variables"
|
||||||
|
|
||||||
#teacher-classes-view
|
#teacher-classes-view
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
@import "app/styles/bootstrap/variables"
|
@import "app/styles/bootstrap/variables"
|
||||||
@import "app/styles/mixins"
|
@import "app/styles/mixins"
|
||||||
@import "app/styles/style-flat"
|
@import "app/styles/style-flat-variables"
|
||||||
|
|
||||||
#teacher-dashboard-nav
|
#teacher-dashboard-nav
|
||||||
vertical-align: middle
|
vertical-align: middle
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
@import "app/styles/bootstrap/variables"
|
@import "app/styles/bootstrap/variables"
|
||||||
@import "app/styles/mixins"
|
@import "app/styles/mixins"
|
||||||
@import "app/styles/style-flat"
|
@import "app/styles/style-flat-variables"
|
||||||
|
|
||||||
#new-home-view
|
#new-home-view
|
||||||
|
|
||||||
|
|
7
app/styles/style-flat-variables.sass
Normal file
7
app/styles/style-flat-variables.sass
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
$headline-font: 'Arvo', serif
|
||||||
|
$body-font: 'Open Sans', sans-serif
|
||||||
|
|
||||||
|
$burgandy: #7D0101
|
||||||
|
$gold: #F2BE19
|
||||||
|
$navy: #0E4C60
|
||||||
|
$forest: #20572B
|
|
@ -1,13 +1,9 @@
|
||||||
@import "app/styles/bootstrap/variables"
|
@import "app/styles/bootstrap/variables"
|
||||||
@import "app/styles/mixins"
|
@import "app/styles/mixins"
|
||||||
|
@import "app/styles/style-flat-variables"
|
||||||
|
|
||||||
// TODO: Move flat style into probably several files and Bootstrap variables
|
// TODO: Move flat style into probably several files and Bootstrap variables
|
||||||
|
|
||||||
// Variables
|
|
||||||
|
|
||||||
$headline-font: 'Arvo', serif
|
|
||||||
$body-font: 'Open Sans', sans-serif
|
|
||||||
|
|
||||||
body[lang='ru'], body[lang='uk'], body[lang='bg'], body[lang^='mk'], body[lang='sr']
|
body[lang='ru'], body[lang='uk'], body[lang='bg'], body[lang^='mk'], body[lang='sr']
|
||||||
// Google Fonts version of Arvo only has Latin glyphs, not Cyrillic
|
// Google Fonts version of Arvo only has Latin glyphs, not Cyrillic
|
||||||
// TODO: figure out font fallbacks for other languages not covered by Arvo
|
// TODO: figure out font fallbacks for other languages not covered by Arvo
|
||||||
|
@ -15,11 +11,6 @@ body[lang='ru'], body[lang='uk'], body[lang='bg'], body[lang^='mk'], body[lang='
|
||||||
h1, .text-h1, h3, .text-h3, h5, .text-h5
|
h1, .text-h1, h3, .text-h3, h5, .text-h5
|
||||||
font-family: 'Open Sans', serif
|
font-family: 'Open Sans', serif
|
||||||
|
|
||||||
$burgandy: #7D0101
|
|
||||||
$gold: #F2BE19
|
|
||||||
$navy: #0E4C60
|
|
||||||
$forest: #20572B
|
|
||||||
|
|
||||||
.style-flat
|
.style-flat
|
||||||
background: white
|
background: white
|
||||||
color: black
|
color: black
|
||||||
|
@ -194,6 +185,15 @@ $forest: #20572B
|
||||||
color: $gold
|
color: $gold
|
||||||
text-shadow: 1px 1px black, -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black
|
text-shadow: 1px 1px black, -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black
|
||||||
|
|
||||||
|
// Wells
|
||||||
|
|
||||||
|
.well
|
||||||
|
padding: 8px
|
||||||
|
background-color: transparent
|
||||||
|
border: thin solid lightgray
|
||||||
|
border-radius: 0
|
||||||
|
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
|
|
||||||
.btn
|
.btn
|
||||||
|
@ -272,6 +272,8 @@ $forest: #20572B
|
||||||
// TODO: Font size 18? Inconsistent with buttons on teacher-class-view bulk assign
|
// TODO: Font size 18? Inconsistent with buttons on teacher-class-view bulk assign
|
||||||
|
|
||||||
// Tooltips
|
// Tooltips
|
||||||
|
.tooltip.in
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
.tooltip .tooltip-arrow::after
|
.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
|
// Create a duplicate tooltip arrow which will cover the main arrow and make it seem like a line rather than filled
|
||||||
|
@ -334,6 +336,7 @@ $forest: #20572B
|
||||||
background: white
|
background: white
|
||||||
border-radius: 20px
|
border-radius: 20px
|
||||||
min-width: 150px
|
min-width: 150px
|
||||||
|
max-width: 600px
|
||||||
|
|
||||||
|
|
||||||
// Checkboxes
|
// Checkboxes
|
||||||
|
|
|
@ -5,49 +5,67 @@ block modal-header-content
|
||||||
.text-center
|
.text-center
|
||||||
h1(data-i18n="teacher.enroll_students")
|
h1(data-i18n="teacher.enroll_students")
|
||||||
h2(data-i18n="courses.grants_lifetime_access")
|
h2(data-i18n="courses.grants_lifetime_access")
|
||||||
if view.classroom
|
|
||||||
p= view.classroom.get('name')
|
|
||||||
|
|
||||||
block modal-body-content
|
block modal-body-content
|
||||||
|
- var numToEnroll = state.get('visibleSelectedUsers').length
|
||||||
|
- var unusedEnrollments = view.prepaids.totalMaxRedeemers() - view.prepaids.totalRedeemers()
|
||||||
|
- var tooManySelected = numToEnroll > unusedEnrollments
|
||||||
|
- var noneSelected = numToEnroll == 0
|
||||||
|
|
||||||
if view.classrooms.length > 1
|
if view.classrooms.length > 1
|
||||||
.text-center
|
.row
|
||||||
span(data-i18n='teacher.show_students_from')
|
.col-sm-10.col-sm-offset-1
|
||||||
span.spr :
|
.text-center.m-b-3
|
||||||
select
|
.small.color-navy
|
||||||
each classroom in view.classrooms.models
|
span(data-i18n='teacher.show_students_from')
|
||||||
option(selected=(classroom.id === view.classroom.id), value=classroom.id)
|
span.spr :
|
||||||
= classroom.get('name')
|
select.classroom-select
|
||||||
//- option(selected=!view.classroom, value='all-classrooms' data-i18n='teacher.all_students')
|
each classroom in view.classrooms.models
|
||||||
form.form
|
option(selected=(classroom.id === view.classroom.id), value=classroom.id)
|
||||||
|
= classroom.get('name')
|
||||||
|
option(selected=(view.classroom.id === 'all-students'), value='all-students' data-i18n='teacher.all_students')
|
||||||
|
|
||||||
|
form.form.m-t-3
|
||||||
span(data-i18n="teacher.enroll_the_following_students")
|
span(data-i18n="teacher.enroll_the_following_students")
|
||||||
span :
|
span :
|
||||||
.well.form-group
|
.well.form-group
|
||||||
for user in view.users.models
|
- var enrolledUsers = view.users.filter(function(user){ return Boolean(user.get('coursePrepaidID')) })
|
||||||
|
- var unenrolledUsers = view.users.filter(function(user){ return !Boolean(user.get('coursePrepaidID')) })
|
||||||
|
for user in unenrolledUsers
|
||||||
|
- var selected = Boolean(paid || state.get('selectedUsers').get(user.id))
|
||||||
.checkbox
|
.checkbox
|
||||||
label
|
label
|
||||||
- var paid = user.get('coursePrepaidID')
|
input.user-checkbox(type="checkbox", disabled=false, checked=selected, data-user-id=user.id, name='user')
|
||||||
- var selected = (view.selectedUsers.get(user.id) ? true : false)
|
span.spr= user.broadName()
|
||||||
input(type="checkbox", disabled=paid, checked=selected, data-user-id=user.id, name='user')
|
if enrolledUsers.length > 0
|
||||||
|
.small-details.m-t-3
|
||||||
|
span(data-i18n='TODO')
|
||||||
|
| The following students are already enrolled:
|
||||||
|
for user in enrolledUsers
|
||||||
|
- var selected = Boolean(paid || state.get('selectedUsers').get(user.id))
|
||||||
|
.checkbox
|
||||||
|
label
|
||||||
|
input.user-checkbox(type="checkbox", disabled=true, checked=true, data-user-id=user.id, name='user')
|
||||||
span.spr= user.broadName()
|
span.spr= user.broadName()
|
||||||
if paid
|
|
||||||
span (
|
|
||||||
span(data-i18n="courses.already_enrolled")
|
|
||||||
span )
|
|
||||||
|
|
||||||
#error-alert.alert.alert-danger.hide
|
if state.get('error')
|
||||||
|
.alert.alert-danger
|
||||||
|
= state.get('error')
|
||||||
|
|
||||||
#submit-form-area.text-center
|
#submit-form-area.text-center
|
||||||
|
p.small-details.not-enough-enrollments(class=(tooManySelected ? 'visible' : ''))
|
||||||
|
span(data-i18n='teacher.not_enough_enrollments')
|
||||||
|
|
||||||
p.small-details
|
p.small-details
|
||||||
span.spr(data-i18n="courses.enrollment_credits_available")
|
span.spr(data-i18n="courses.enrollment_credits_available")
|
||||||
span#total-available= view.prepaids.totalAvailable()
|
span#total-available= view.prepaids.totalAvailable()
|
||||||
|
|
||||||
p.small-details.not-enough-enrollments
|
|
||||||
span(data-i18n='teacher.not_enough_enrollments')
|
|
||||||
p
|
p
|
||||||
button#activate-licenses-btn.btn.btn-lg.btn-primary(type="submit")
|
button#activate-licenses-btn.btn.btn-lg.btn-primary(type="submit" class=(tooManySelected || noneSelected ? 'disabled' : ''))
|
||||||
span.spr(data-i18n="courses.enroll")
|
span.spr(data-i18n="courses.enroll")
|
||||||
| (
|
| (
|
||||||
span#total-selected-span
|
span#total-selected-span
|
||||||
|
= numToEnroll
|
||||||
| )
|
| )
|
||||||
span.spl(data-i18n="courses.students1")
|
span.spl(data-i18n="courses.students1")
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,11 @@ block content
|
||||||
if classroom.loaded
|
if classroom.loaded
|
||||||
.container
|
.container
|
||||||
+breadcrumbs
|
+breadcrumbs
|
||||||
|
if classroom.get('archived')
|
||||||
|
.row.center-block.text-center.m-t-3.m-b-3
|
||||||
|
.unarchive-btn.btn.btn-lg.btn-navy
|
||||||
|
span(data-i18n='teacher.unarchive_this_class')
|
||||||
|
|
||||||
h3.m-t-2= classroom.get('name')
|
h3.m-t-2= classroom.get('name')
|
||||||
a.label.edit-classroom(data-classroom-id=classroom.id)
|
a.label.edit-classroom(data-classroom-id=classroom.id)
|
||||||
span(data-i18n='teacher.edit_class_settings')
|
span(data-i18n='teacher.edit_class_settings')
|
||||||
|
@ -25,7 +30,7 @@ block content
|
||||||
|
|
||||||
.classroom-info-row.row.m-t-5
|
.classroom-info-row.row.m-t-5
|
||||||
.classroom-details.col-md-3
|
.classroom-details.col-md-3
|
||||||
- var stats = view.classStats()
|
- var stats = state.get('classStats')
|
||||||
h4.m-b-2(data-i18n='teacher.class_overview')
|
h4.m-b-2(data-i18n='teacher.class_overview')
|
||||||
|
|
||||||
.language.small-details
|
.language.small-details
|
||||||
|
@ -68,55 +73,57 @@ block content
|
||||||
span(data-i18n='teacher.export_student_progress')
|
span(data-i18n='teacher.export_student_progress')
|
||||||
|
|
||||||
//- .concepts.small-details
|
//- .concepts.small-details
|
||||||
//- if view.progressData
|
//- if state.get('progressData')
|
||||||
//- div
|
//- div
|
||||||
//- span(data-i18n='teacher.concepts_covered')
|
//- span(data-i18n='teacher.concepts_covered')
|
||||||
//- span :
|
//- span :
|
||||||
//- - console.log('concepts', view.conceptData)
|
//- - console.log('concepts', view.conceptData)
|
||||||
//- - concepts = view.conceptData
|
//- - concepts = view.conceptData
|
||||||
//- each state, name in view.conceptData[view.classroom.id]
|
//- each state, name in view.conceptData[view.classroom.id]
|
||||||
//- if state.started
|
//- if state.get('started')
|
||||||
//- b.concept(class=state.completed ? 'forest' : 'gold')
|
//- b.concept(class=state.get('completed') ? 'forest' : 'gold')
|
||||||
//- span(data-i18n='concepts.'+name)
|
//- span(data-i18n='concepts.'+name)
|
||||||
|
|
||||||
.completeness-info.col-md-4
|
.completeness-info.col-md-4
|
||||||
h4.m-b-2
|
h4.m-b-2
|
||||||
|
|
||||||
if view.earliestIncompleteLevel
|
if state.get('earliestIncompleteLevel')
|
||||||
div.small-details
|
div.small-details
|
||||||
span(data-i18n='teacher.earliest_incomplete')
|
span(data-i18n='teacher.earliest_incomplete')
|
||||||
span :
|
span :
|
||||||
+longLevelName(view.earliestIncompleteLevel)
|
+longLevelName(state.get('earliestIncompleteLevel'))
|
||||||
+inlineUserList(view.earliestIncompleteLevel.users)
|
+inlineUserList(state.get('earliestIncompleteLevel').users)
|
||||||
|
|
||||||
if view.latestCompleteLevel
|
if state.get('latestCompleteLevel')
|
||||||
div.small-details.m-t-3
|
div.small-details.m-t-3
|
||||||
span(data-i18n='teacher.latest_complete')
|
span(data-i18n='teacher.latest_complete')
|
||||||
span :
|
span :
|
||||||
+longLevelName(view.latestCompleteLevel)
|
+longLevelName(state.get('latestCompleteLevel'))
|
||||||
+inlineUserList(view.latestCompleteLevel.users)
|
+inlineUserList(state.get('latestCompleteLevel').users)
|
||||||
|
|
||||||
.adding-students.col-md-5
|
.adding-students.col-md-5
|
||||||
h4.m-b-2
|
h4.m-b-2
|
||||||
span(data-i18n='teacher.adding_students')
|
span(data-i18n='teacher.adding_students')
|
||||||
span :
|
span :
|
||||||
+copyCodes
|
+copyCodes
|
||||||
+addStudentsButton
|
+addStudentsButton
|
||||||
|
|
||||||
ul.nav.nav-tabs.m-t-5(role='tablist')
|
ul#student-info-tabs.nav.nav-tabs.m-t-5(role='tablist')
|
||||||
li.active
|
li(class=(state.get('activeTab')==="#students-tab" ? 'active' : ''))
|
||||||
a(href='#students-tab' data-toggle='tab')
|
a.students-tab-btn(href='#students-tab')
|
||||||
.small-details.text-center(data-i18n='teacher.students')
|
.small-details.text-center(data-i18n='teacher.students')
|
||||||
.tab-spacer
|
.tab-spacer
|
||||||
li
|
li(class=(state.get('activeTab')==="#course-progress-tab" ? 'active' : ''))
|
||||||
a(href='#course-progress-tab' data-toggle='tab')
|
a.course-progress-tab-btn(href='#course-progress-tab')
|
||||||
.small-details.text-center(data-i18n='teacher.course_progress')
|
.small-details.text-center(data-i18n='teacher.course_progress')
|
||||||
.tab-filler
|
.tab-filler
|
||||||
|
|
||||||
.tab-content
|
.tab-content
|
||||||
+studentsTab
|
if state.get('activeTab')=='#students-tab'
|
||||||
+courseProgressTab
|
+studentsTab
|
||||||
|
else
|
||||||
|
+courseProgressTab
|
||||||
|
|
||||||
mixin breadcrumbs
|
mixin breadcrumbs
|
||||||
.breadcrumbs
|
.breadcrumbs
|
||||||
a(data-i18n='teacher.my_classes' href='/teachers/classes')
|
a(data-i18n='teacher.my_classes' href='/teachers/classes')
|
||||||
|
@ -153,7 +160,7 @@ mixin addStudentsButton
|
||||||
span(data-i18n='teacher.add_students_manually')
|
span(data-i18n='teacher.add_students_manually')
|
||||||
|
|
||||||
mixin studentsTab
|
mixin studentsTab
|
||||||
#students-tab.tab-pane.active
|
#students-tab
|
||||||
+bulkAssignControls
|
+bulkAssignControls
|
||||||
table.students-table
|
table.students-table
|
||||||
thead
|
thead
|
||||||
|
@ -165,7 +172,7 @@ mixin studentsTab
|
||||||
th
|
th
|
||||||
+sortButtons
|
+sortButtons
|
||||||
tbody
|
tbody
|
||||||
each student in view.students.models
|
each student in state.get('students').models
|
||||||
+studentRow(student)
|
+studentRow(student)
|
||||||
|
|
||||||
mixin sortButtons
|
mixin sortButtons
|
||||||
|
@ -199,12 +206,15 @@ mixin studentRow(student)
|
||||||
div
|
div
|
||||||
+longLevelName(student.latestCompleteLevel)
|
+longLevelName(student.latestCompleteLevel)
|
||||||
td
|
td
|
||||||
if view.progressData
|
if state.get('progressData')
|
||||||
each course, index in view.courses.models
|
each trimCourse, index in view.classroom.get('courses')
|
||||||
|
- var course = view.courses.get(trimCourse._id);
|
||||||
- var instance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
|
- var instance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
|
||||||
if instance && instance.hasMember(student)
|
if instance && instance.hasMember(student)
|
||||||
- var progress = view.progressData.get({ classroom: view.classroom, course: course, user: student })
|
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, user: student })
|
||||||
+progressDot(progress, 'CS' + (index+1))
|
- var levelsTotal = trimCourse.levels.length
|
||||||
|
//- - var level = ???
|
||||||
|
+studentCourseProgressDot(progress, levelsTotal, level, 'CS' + (index+1))
|
||||||
unless student.isEnrolled()
|
unless student.isEnrolled()
|
||||||
+enrollStudentButton(student)
|
+enrollStudentButton(student)
|
||||||
//- td
|
//- td
|
||||||
|
@ -219,7 +229,7 @@ mixin enrollStudentButton(student)
|
||||||
span(data-i18n='teacher.enroll_student')
|
span(data-i18n='teacher.enroll_student')
|
||||||
|
|
||||||
mixin courseProgressTab
|
mixin courseProgressTab
|
||||||
#course-progress-tab.tab-pane.m-t-3
|
#course-progress-tab.m-t-3
|
||||||
if view.courses
|
if view.courses
|
||||||
.text-center
|
.text-center
|
||||||
span(data-i18n='teacher.select_course')
|
span(data-i18n='teacher.select_course')
|
||||||
|
@ -227,18 +237,50 @@ mixin courseProgressTab
|
||||||
select.course-select
|
select.course-select
|
||||||
each trimCourse in view.classroom.get('courses')
|
each trimCourse in view.classroom.get('courses')
|
||||||
- var course = view.courses.get(trimCourse._id);
|
- var course = view.courses.get(trimCourse._id);
|
||||||
option(value=course.id)
|
option(value=course.id selected=(course===state.get('selectedCourse')))
|
||||||
= course.get('name')
|
= course.get('name')
|
||||||
if view.progressData
|
if state.get('progressData')
|
||||||
.render-on-course-sync
|
.render-on-course-sync
|
||||||
+courseOverview
|
+courseOverview
|
||||||
.student-levels-table
|
.student-levels-table
|
||||||
+sortButtons
|
+sortButtons
|
||||||
each student in view.students.models
|
each student in state.get('students').models
|
||||||
+studentLevelsRow(student)
|
if _.contains(state.get('selectedCourse').members, student.id)
|
||||||
|
+studentLevelsRow(student)
|
||||||
|
//- TODO: If any students aren't assigned the course
|
||||||
|
.unassigned-students.render-on-course-sync
|
||||||
|
if state.get('selectedCourse') && state.get('selectedCourse').members.length < state.get('students').length
|
||||||
|
h2
|
||||||
|
span(data-i18n='TODO')
|
||||||
|
| Students who have not been assigned
|
||||||
|
|
|
||||||
|
span= state.get('selectedCourse').get('name')
|
||||||
|
for student in state.get('students').models
|
||||||
|
unless _.contains(state.get('selectedCourse').members, student.id)
|
||||||
|
.row.unassigned-student-row.alternating-background
|
||||||
|
.student-name.col-sm-3
|
||||||
|
= student.broadName()
|
||||||
|
.student-email.small-details.col-sm-3
|
||||||
|
= student.get('email')
|
||||||
|
.col-sm-4
|
||||||
|
.latest-completed.truncate.small
|
||||||
|
i.m-r-1
|
||||||
|
span(data-i18n='TODO')
|
||||||
|
| Latest completed
|
||||||
|
| :
|
||||||
|
+longLevelName(student.latestCompleteLevel)
|
||||||
|
.col-sm-2
|
||||||
|
if student.isEnrolled()
|
||||||
|
.assign-student-button.btn.btn-md.btn-navy.pull-right(data-user-id=student.id data-course-id=state.get('selectedCourse').id)
|
||||||
|
span(data-i18n='TODO')
|
||||||
|
| Assign Course
|
||||||
|
else
|
||||||
|
.enroll-student-button.btn.btn-md.btn-navy.pull-right(data-user-id=student.id)
|
||||||
|
span(data-i18n='TODO')
|
||||||
|
| Enroll Student
|
||||||
|
|
||||||
mixin courseOverview
|
mixin courseOverview
|
||||||
- var course = view.selectedCourse
|
- var course = state.get('selectedCourse')
|
||||||
- var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models
|
- var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models
|
||||||
.course-overview-row
|
.course-overview-row
|
||||||
.course-title.student-name
|
.course-title.student-name
|
||||||
|
@ -247,8 +289,8 @@ mixin courseOverview
|
||||||
span(data-i18n='teacher.course_overview')
|
span(data-i18n='teacher.course_overview')
|
||||||
.course-overview-progress
|
.course-overview-progress
|
||||||
each level, index in levels
|
each level, index in levels
|
||||||
- var progress = view.progressData.get({ classroom: view.classroom, course: course, level: level })
|
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level })
|
||||||
+progressDot(progress, index+1)
|
+allStudentsLevelProgressDot(progress, level, index+1)
|
||||||
|
|
||||||
mixin studentLevelsRow(student)
|
mixin studentLevelsRow(student)
|
||||||
.student-levels-row.alternating-background
|
.student-levels-row.alternating-background
|
||||||
|
@ -256,20 +298,35 @@ mixin studentLevelsRow(student)
|
||||||
div.student-name= student.broadName()
|
div.student-name= student.broadName()
|
||||||
div.student-email.small-details= student.get('email')
|
div.student-email.small-details= student.get('email')
|
||||||
div.student-levels-progress
|
div.student-levels-progress
|
||||||
- var course = view.selectedCourse
|
- var course = state.get('selectedCourse')
|
||||||
- var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models
|
- var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models
|
||||||
each level, index in levels
|
each level, index in levels
|
||||||
- var progress = view.progressData.get({ classroom: view.classroom, course: course, level: level, user: student })
|
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level, user: student })
|
||||||
+progressDot(progress, index+1)
|
+studentLevelProgressDot(progress, level, index+1)
|
||||||
|
|
||||||
mixin progressDot(progress, label)
|
mixin studentCourseProgressDot(progress, levelsTotal, level, label)
|
||||||
//- TODO: Refactor with TeacherClassesView jade
|
//- TODO: Refactor with TeacherClassesView jade
|
||||||
//- TODO: Give classes abbreviations instead of using index?
|
//- TODO: Give classes abbreviations instead of using index?
|
||||||
//- TODO: inefficient. Cache this in the view?
|
|
||||||
- dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : '');
|
- dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : '');
|
||||||
.progress-dot(class=dotClass, data-html='true', data-title=view.progressDotTemplate(progressDotContext) data-toggle='tooltip')
|
- _.assign(progress, { levelsTotal: levelsTotal })
|
||||||
|
.progress-dot(class=dotClass, data-html='true', data-title=view.singleStudentCourseProgressDotTemplate(progress))
|
||||||
+progressDotLabel(label)
|
+progressDotLabel(label)
|
||||||
|
|
||||||
|
mixin allStudentsLevelProgressDot(progress, level, levelNumber)
|
||||||
|
- dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : '');
|
||||||
|
- levelName = level.get('name')
|
||||||
|
- context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber, numStudents: view.students.length })
|
||||||
|
.progress-dot.level-progress-dot(class=dotClass, data-html='true', data-title=view.allStudentsLevelProgressDotTemplate(context))
|
||||||
|
+progressDotLabel(levelNumber)
|
||||||
|
|
||||||
|
mixin studentLevelProgressDot(progress, level, levelNumber)
|
||||||
|
//- TODO: Refactor with TeacherClassesView jade
|
||||||
|
- dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : '');
|
||||||
|
- levelName = level.get('name')
|
||||||
|
- context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber })
|
||||||
|
.progress-dot.level-progress-dot(class=dotClass, data-html='true', data-title=view.singleStudentLevelProgressDotTemplate(context))
|
||||||
|
+progressDotLabel(levelNumber)
|
||||||
|
|
||||||
mixin progressDotLabel(label)
|
mixin progressDotLabel(label)
|
||||||
.dot-label.text-center
|
.dot-label.text-center
|
||||||
.dot-label-inner
|
.dot-label-inner
|
||||||
|
@ -278,23 +335,23 @@ mixin progressDotLabel(label)
|
||||||
mixin copyCodes
|
mixin copyCodes
|
||||||
div.copy-button-group.form-inline.m-b-3
|
div.copy-button-group.form-inline.m-b-3
|
||||||
.form-group
|
.form-group
|
||||||
input.text-h4.semibold#join-code-input(value=view.classCode)
|
input.text-h4.semibold#join-code-input(value=state.get('classCode'))
|
||||||
button#copy-code-btn.form-control.btn.btn-lg.btn-forest
|
button#copy-code-btn.form-control.btn.btn-lg.btn-forest
|
||||||
span(data-i18n='teacher.copy_class_code')
|
span(data-i18n='teacher.copy_class_code')
|
||||||
div.text-center.small(data-i18n='teacher.class_code_blurb')
|
div.text-center.small(data-i18n='teacher.class_code_blurb')
|
||||||
|
|
||||||
div.copy-button-group.form-inline.m-b-3
|
div.copy-button-group.form-inline.m-b-3
|
||||||
.form-group
|
.form-group
|
||||||
input.form-control.text-h4.semibold#join-url-input(value=view.joinURL)
|
input.form-control.text-h4.semibold#join-url-input(value=state.get('joinURL'))
|
||||||
button#copy-url-btn.form-control.btn.btn-lg.btn-forest
|
button#copy-url-btn.form-control.btn.btn-lg.btn-forest
|
||||||
span(data-i18n='teacher.copy_class_url')
|
span(data-i18n='teacher.copy_class_url')
|
||||||
div.text-center.small(data-i18n='teacher.class_join_url_blurb')
|
div.text-center.small(data-i18n='teacher.class_join_url_blurb')
|
||||||
|
|
||||||
mixin bulkAssignControls
|
mixin bulkAssignControls
|
||||||
.bulk-assign-controls.form-inline
|
.bulk-assign-controls.form-inline
|
||||||
.no-students-selected.small-details(class=view.assigningToNobody ? 'visible' : '')
|
.no-students-selected.small-details(class=state.get('errors').assigningToNobody ? 'visible' : '')
|
||||||
span(data-i18n='teacher.no_students_selected')
|
span(data-i18n='teacher.no_students_selected')
|
||||||
.cant-assign-to-unenrolled.small-details(class=view.assigningToUnenrolled ? 'visible' : '')
|
.cant-assign-to-unenrolled.small-details(class=state.get('errors').assigningToUnenrolled ? 'visible' : '')
|
||||||
span(data-i18n='teacher.cant_assign_to_unenrolled')
|
span(data-i18n='teacher.cant_assign_to_unenrolled')
|
||||||
span.small
|
span.small
|
||||||
span(data-i18n='teacher.bulk_assign')
|
span(data-i18n='teacher.bulk_assign')
|
||||||
|
@ -302,9 +359,9 @@ mixin bulkAssignControls
|
||||||
select.bulk-course-select.form-control
|
select.bulk-course-select.form-control
|
||||||
each trimCourse in _.rest(view.classroom.get('courses'))
|
each trimCourse in _.rest(view.classroom.get('courses'))
|
||||||
- var course = view.courses.get(trimCourse._id)
|
- var course = view.courses.get(trimCourse._id)
|
||||||
option(value=course.id)
|
option(value=course.id selected=(course===state.get('selectedCourse')))
|
||||||
= course.get('name')
|
= course.get('name')
|
||||||
button.btn.btn-primary-alt.assign-to-selected-students
|
button.btn.btn-primary-alt.assign-to-selected-students
|
||||||
span(data-i18n='teacher.assign_to_selected_students')
|
span(data-i18n='teacher.assign_to_selected_students')
|
||||||
button.btn.btn-primary-alt.enroll-selected-students
|
button.btn.btn-primary-alt.enroll-selected-students
|
||||||
span(data-i18n='teacher.enroll_selected_students')
|
span(data-i18n='teacher.enroll_selected_students')
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
if started
|
||||||
|
.small-details.nowrap
|
||||||
|
span= levelNumber
|
||||||
|
span.spr .
|
||||||
|
span= levelName
|
||||||
|
.small-details.nowrap
|
||||||
|
.fraction-students.small-details
|
||||||
|
= numStarted
|
||||||
|
| /
|
||||||
|
= numStudents
|
||||||
|
if completed
|
||||||
|
span.spl(data-i18n='teacher.completed')
|
||||||
|
| Completed
|
||||||
|
else
|
||||||
|
span.spl(data-i18n='teacher.started')
|
||||||
|
| Started
|
||||||
|
//- .small-details
|
||||||
|
//- i(data-i18n='teacher.click_to_view_progress')
|
||||||
|
else
|
||||||
|
span.small-details.nowrap(data-i18n='teacher.no_progress')
|
||||||
|
| No progress
|
|
@ -0,0 +1,18 @@
|
||||||
|
if completed
|
||||||
|
span.small-details(data-i18n='teacher.complete')
|
||||||
|
| Complete
|
||||||
|
else if started
|
||||||
|
.fraction-students.small-details
|
||||||
|
= levelsCompleted
|
||||||
|
| /
|
||||||
|
= levelsTotal
|
||||||
|
span.spl(data-i18n='teacher.levels')
|
||||||
|
| Levels
|
||||||
|
.percent-students.small-details
|
||||||
|
= Math.floor(levelsCompleted / levelsTotal * 100)
|
||||||
|
| %
|
||||||
|
span.spl(data-i18n='teacher.complete')
|
||||||
|
| Complete
|
||||||
|
else
|
||||||
|
span.small-details(data-i18n='teacher.assigned')
|
||||||
|
| Assigned
|
|
@ -0,0 +1,27 @@
|
||||||
|
if completed
|
||||||
|
.small-details.nowrap
|
||||||
|
span= levelNumber
|
||||||
|
span.spr .
|
||||||
|
span= levelName
|
||||||
|
.small-details.nowrap
|
||||||
|
span.spr(data-i18n='teacher.completed')
|
||||||
|
| Completed
|
||||||
|
span= new Date(dateFirstCompleted).toLocaleString()
|
||||||
|
//- .small-details
|
||||||
|
//- i(data-i18n='teacher.click_to_view_solution')
|
||||||
|
//- | click to view solution
|
||||||
|
else if started
|
||||||
|
.small-details.nowrap
|
||||||
|
span= levelNumber
|
||||||
|
span.spr .
|
||||||
|
span= levelName
|
||||||
|
.small-details.nowrap
|
||||||
|
span.spr(data-i18n='teacher.last_played')
|
||||||
|
| Last played
|
||||||
|
span= new Date(lastPlayed).toLocaleString()
|
||||||
|
//- .small-details
|
||||||
|
//- i(data-i18n='teacher.click_to_view_progress')
|
||||||
|
//- | click to view progress
|
||||||
|
else
|
||||||
|
span.small-details.nowrap(data-i18n='teacher.no_progress')
|
||||||
|
| No progress
|
|
@ -219,6 +219,10 @@ block content
|
||||||
p
|
p
|
||||||
span.spr(data-i18n="teachers_quote.thanks_p")
|
span.spr(data-i18n="teachers_quote.thanks_p")
|
||||||
a.spl(href="mailto:team@codecombat.com") team@codecombat.com
|
a.spl(href="mailto:team@codecombat.com") team@codecombat.com
|
||||||
|
|
||||||
|
unless me.isAnonymous()
|
||||||
|
a.btn.btn-lg.btn-navy(href="/teachers/classes")
|
||||||
|
span(data-i18n='teachers_quote.back_to_classes')
|
||||||
|
|
||||||
if me.isAnonymous()
|
if me.isAnonymous()
|
||||||
h5(data-i18n="teachers_quote.finish_signup")
|
h5(data-i18n="teachers_quote.finish_signup")
|
||||||
|
|
|
@ -137,6 +137,7 @@ module.exports = class CocoView extends Backbone.View
|
||||||
context._ = _
|
context._ = _
|
||||||
context.document = document
|
context.document = document
|
||||||
context.i18n = utils.i18n
|
context.i18n = utils.i18n
|
||||||
|
context.state = @state
|
||||||
context
|
context
|
||||||
|
|
||||||
afterRender: ->
|
afterRender: ->
|
||||||
|
@ -390,7 +391,7 @@ module.exports = class CocoView extends Backbone.View
|
||||||
setTimeout (=> $pointer.css transition: 'all 0.4s ease-in', transform: "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance}px)"), 800
|
setTimeout (=> $pointer.css transition: 'all 0.4s ease-in', transform: "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance}px)"), 800
|
||||||
|
|
||||||
endHighlight: ->
|
endHighlight: ->
|
||||||
@getPointer(false).css({'opacity': 0.0, 'transition': 'none', top: '-50px', right: '-50px'})
|
@getPointer(false).css({'opacity': 0.0, 'transition': 'none', top: '-50px', right: '-50px'})
|
||||||
clearInterval @pointerInterval
|
clearInterval @pointerInterval
|
||||||
clearTimeout @pointerDelayTimeout
|
clearTimeout @pointerDelayTimeout
|
||||||
clearTimeout @pointerDurationTimeout
|
clearTimeout @pointerDurationTimeout
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
ModalView = require 'views/core/ModalView'
|
ModalView = require 'views/core/ModalView'
|
||||||
|
State = require 'models/State'
|
||||||
template = require 'templates/courses/activate-licenses-modal'
|
template = require 'templates/courses/activate-licenses-modal'
|
||||||
CocoCollection = require 'collections/CocoCollection'
|
CocoCollection = require 'collections/CocoCollection'
|
||||||
Prepaids = require 'collections/Prepaids'
|
Prepaids = require 'collections/Prepaids'
|
||||||
|
Classroom = require 'models/Classroom'
|
||||||
Classrooms = require 'collections/Classrooms'
|
Classrooms = require 'collections/Classrooms'
|
||||||
User = require 'models/User'
|
User = require 'models/User'
|
||||||
Users = require 'collections/Users'
|
Users = require 'collections/Users'
|
||||||
|
@ -11,14 +13,23 @@ module.exports = class ActivateLicensesModal extends ModalView
|
||||||
template: template
|
template: template
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'change input': 'updateSelectionSpans'
|
'change input[type="checkbox"][name="user"]': 'updateSelectedStudents'
|
||||||
'change select': 'replaceStudentList'
|
'change select.classroom-select': 'replaceStudentList'
|
||||||
'submit form': 'onSubmitForm'
|
'submit form': 'onSubmitForm'
|
||||||
|
|
||||||
|
getInitialState: (options) ->
|
||||||
|
selectedUserModels = _.filter(options.selectedUsers.models, (user) -> not user.isEnrolled())
|
||||||
|
{
|
||||||
|
selectedUsers: new Users(selectedUserModels)
|
||||||
|
visibleSelectedUsers: new Users(selectedUserModels)
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
|
||||||
initialize: (options) ->
|
initialize: (options) ->
|
||||||
|
@state = new State(@getInitialState(options))
|
||||||
@classroom = options.classroom
|
@classroom = options.classroom
|
||||||
@users = options.users
|
@users = options.users.clone()
|
||||||
@selectedUsers = options.selectedUsers
|
@users.comparator = (user) -> user.broadName().toLowerCase()
|
||||||
@prepaids = new Prepaids()
|
@prepaids = new Prepaids()
|
||||||
@prepaids.comparator = '_id'
|
@prepaids.comparator = '_id'
|
||||||
@prepaids.fetchByCreator(me.id)
|
@prepaids.fetchByCreator(me.id)
|
||||||
|
@ -33,68 +44,58 @@ module.exports = class ActivateLicensesModal extends ModalView
|
||||||
@supermodel.trackRequests(jqxhrs)
|
@supermodel.trackRequests(jqxhrs)
|
||||||
})
|
})
|
||||||
@supermodel.trackCollection(@classrooms)
|
@supermodel.trackCollection(@classrooms)
|
||||||
|
|
||||||
|
@listenTo @state, 'change', @render
|
||||||
|
@listenTo @state.get('selectedUsers'), 'change add remove reset', ->
|
||||||
|
@state.set { visibleSelectedUsers: new Users(@state.get('selectedUsers').filter (u) => @users.get(u)) }
|
||||||
|
@render()
|
||||||
|
@listenTo @users, 'change add remove reset', ->
|
||||||
|
@state.set { visibleSelectedUsers: new Users(@state.get('selectedUsers').filter (u) => @users.get(u)) }
|
||||||
|
@render()
|
||||||
|
@listenTo @prepaids, 'sync add remove', ->
|
||||||
|
@state.set {
|
||||||
|
unusedEnrollments: @prepaids.totalMaxRedeemers() - @prepaids.totalRedeemers()
|
||||||
|
}
|
||||||
|
|
||||||
afterRender: ->
|
afterRender: ->
|
||||||
super()
|
super()
|
||||||
@updateSelectionSpans()
|
# @updateSelectedStudents() # TODO: refactor to event/state style
|
||||||
|
|
||||||
updateSelectionSpans: ->
|
updateSelectedStudents: (e) ->
|
||||||
targets = @$('input[name="targets"]:checked').val()
|
userID = $(e.currentTarget).data('user-id')
|
||||||
if targets is 'given'
|
user = @users.get(userID)
|
||||||
numToActivate = 1
|
if @state.get('selectedUsers').contains(user)
|
||||||
|
@state.get('selectedUsers').remove(user)
|
||||||
else
|
else
|
||||||
numToActivate = @$('input[name="user"]:checked:not(:disabled)').length
|
@state.get('selectedUsers').add(user)
|
||||||
@$('#total-selected-span').text(numToActivate)
|
# @render() # TODO: Have @state automatically listen to children's change events?
|
||||||
remaining = @prepaids.totalMaxRedeemers() - @prepaids.totalRedeemers() - numToActivate
|
|
||||||
depleted = remaining < 0
|
|
||||||
@$('.not-enough-enrollments').toggleClass('visible', depleted)
|
|
||||||
@$('#activate-licenses-btn').toggleClass('disabled', depleted).toggleClass('btn-success', not depleted).toggleClass('btn-default', depleted)
|
|
||||||
|
|
||||||
replaceStudentList: (e) ->
|
replaceStudentList: (e) ->
|
||||||
selectedClassroomID = $(e.currentTarget).val()
|
selectedClassroomID = $(e.currentTarget).val()
|
||||||
@classroom = @classrooms.get(selectedClassroomID)
|
@classroom = @classrooms.get(selectedClassroomID)
|
||||||
if selectedClassroomID == 'all-classrooms'
|
if selectedClassroomID is 'all-students'
|
||||||
@classroom = new Classroom({ id: 'all-students' }) # TODO: This is a horrible hack so the select shows the right option!
|
@classroom = new Classroom({ _id: 'all-students', name: '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 = _.uniq _.flatten @classrooms.map (classroom) -> classroom.users.models
|
||||||
@users.reset(users)
|
@users.reset(users)
|
||||||
|
@users.sort()
|
||||||
else
|
else
|
||||||
@users.reset(@classrooms.get(selectedClassroomID).users.models)
|
@users.reset(@classrooms.get(selectedClassroomID).users.models)
|
||||||
@trigger('users:change')
|
|
||||||
@render()
|
@render()
|
||||||
null
|
null
|
||||||
|
|
||||||
showProgress: ->
|
|
||||||
@$('#submit-form-area').addClass('hide')
|
|
||||||
@$('#progress-area').removeClass('hide')
|
|
||||||
|
|
||||||
hideProgress: ->
|
|
||||||
@$('#submit-form-area').removeClass('hide')
|
|
||||||
@$('#progress-area').addClass('hide')
|
|
||||||
|
|
||||||
onSubmitForm: (e) ->
|
onSubmitForm: (e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@$('#error-alert').addClass('hide')
|
@state.set error: null
|
||||||
@usersToRedeem = new CocoCollection([], {model: User})
|
usersToRedeem = @state.get('visibleSelectedUsers')
|
||||||
targets = @$('input[name="targets"]:checked').val()
|
@redeemUsers(usersToRedeem)
|
||||||
if targets is 'given'
|
|
||||||
@usersToRedeem.add(@user)
|
|
||||||
else
|
|
||||||
checkedBoxes = @$('input[name="user"]:checked:not(:disabled)')
|
|
||||||
_.each checkedBoxes, (el) =>
|
|
||||||
$el = $(el)
|
|
||||||
userID = $el.data('user-id')
|
|
||||||
@usersToRedeem.add @users.get(userID)
|
|
||||||
return unless @usersToRedeem.size()
|
|
||||||
@usersToRedeem.originalSize = @usersToRedeem.size()
|
|
||||||
@showProgress()
|
|
||||||
@redeemUsers()
|
|
||||||
|
|
||||||
redeemUsers: ->
|
redeemUsers: (usersToRedeem) ->
|
||||||
if not @usersToRedeem.size()
|
if not usersToRedeem.size()
|
||||||
@finishRedeemUsers()
|
@finishRedeemUsers()
|
||||||
|
@hide()
|
||||||
return
|
return
|
||||||
|
|
||||||
user = @usersToRedeem.first()
|
user = usersToRedeem.first()
|
||||||
prepaid = @prepaids.find((prepaid) -> prepaid.get('properties')?.endDate? and prepaid.openSpots() > 0)
|
prepaid = @prepaids.find((prepaid) -> prepaid.get('properties')?.endDate? and prepaid.openSpots() > 0)
|
||||||
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots() > 0) unless prepaid
|
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots() > 0) unless prepaid
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
@ -102,19 +103,20 @@ module.exports = class ActivateLicensesModal extends ModalView
|
||||||
url: _.result(prepaid, 'url') + '/redeemers'
|
url: _.result(prepaid, 'url') + '/redeemers'
|
||||||
data: { userID: user.id }
|
data: { userID: user.id }
|
||||||
context: @
|
context: @
|
||||||
success: ->
|
success: (prepaid) ->
|
||||||
@usersToRedeem.remove(user)
|
user.set('coursePrepaidID', prepaid._id)
|
||||||
pct = 100 * (@usersToRedeem.originalSize - @usersToRedeem.size() / @usersToRedeem.originalSize)
|
usersToRedeem.remove(user)
|
||||||
@$('#progress-area .progress-bar').css('width', "#{pct.toFixed(1)}%")
|
# pct = 100 * (usersToRedeem.originalSize - usersToRedeem.size() / usersToRedeem.originalSize)
|
||||||
|
# @$('#progress-area .progress-bar').css('width', "#{pct.toFixed(1)}%")
|
||||||
application.tracker?.trackEvent 'Enroll modal finished enroll student', category: 'Courses', userID: user.id
|
application.tracker?.trackEvent 'Enroll modal finished enroll student', category: 'Courses', userID: user.id
|
||||||
@redeemUsers()
|
@redeemUsers(usersToRedeem)
|
||||||
error: (jqxhr, textStatus, errorThrown) ->
|
error: (jqxhr, textStatus, errorThrown) ->
|
||||||
if jqxhr.status is 402
|
if jqxhr.status is 402
|
||||||
message = arguments[2]
|
message = arguments[2]
|
||||||
else
|
else
|
||||||
message = "#{jqxhr.status}: #{jqxhr.responseText}"
|
message = "#{jqxhr.status}: #{jqxhr.responseText}"
|
||||||
@$('#error-alert').text(message).removeClass('hide')
|
@state.set { error: message } # TODO: Test this! ("should" never happen. Only on server responding with an error.)
|
||||||
})
|
})
|
||||||
|
|
||||||
finishRedeemUsers: ->
|
finishRedeemUsers: ->
|
||||||
@trigger 'redeem-users'
|
@trigger 'redeem-users', @state.get('selectedUsers')
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
RootView = require 'views/core/RootView'
|
RootView = require 'views/core/RootView'
|
||||||
|
State = require 'models/State'
|
||||||
template = require 'templates/courses/teacher-class-view'
|
template = require 'templates/courses/teacher-class-view'
|
||||||
helper = require 'lib/coursesHelper'
|
helper = require 'lib/coursesHelper'
|
||||||
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
|
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
|
||||||
|
@ -12,6 +13,7 @@ Levels = require 'collections/Levels'
|
||||||
LevelSessions = require 'collections/LevelSessions'
|
LevelSessions = require 'collections/LevelSessions'
|
||||||
User = require 'models/User'
|
User = require 'models/User'
|
||||||
Users = require 'collections/Users'
|
Users = require 'collections/Users'
|
||||||
|
Course = require 'models/Course'
|
||||||
Courses = require 'collections/Courses'
|
Courses = require 'collections/Courses'
|
||||||
CourseInstance = require 'models/CourseInstance'
|
CourseInstance = require 'models/CourseInstance'
|
||||||
CourseInstances = require 'collections/CourseInstances'
|
CourseInstances = require 'collections/CourseInstances'
|
||||||
|
@ -21,6 +23,13 @@ module.exports = class TeacherClassView extends RootView
|
||||||
template: template
|
template: template
|
||||||
|
|
||||||
events:
|
events:
|
||||||
|
'click .students-tab-btn': (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
@trigger 'open-students-tab'
|
||||||
|
'click .course-progress-tab-btn': (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
@trigger 'open-course-progress-tab'
|
||||||
|
'click .unarchive-btn': 'onClickUnarchive'
|
||||||
'click .edit-classroom': 'onClickEditClassroom'
|
'click .edit-classroom': 'onClickEditClassroom'
|
||||||
'click .add-students-btn': 'onClickAddStudents'
|
'click .add-students-btn': 'onClickAddStudents'
|
||||||
'click .sort-by-name': 'sortByName'
|
'click .sort-by-name': 'sortByName'
|
||||||
|
@ -28,33 +37,59 @@ module.exports = class TeacherClassView extends RootView
|
||||||
'click #copy-url-btn': 'copyURL'
|
'click #copy-url-btn': 'copyURL'
|
||||||
'click #copy-code-btn': 'copyCode'
|
'click #copy-code-btn': 'copyCode'
|
||||||
'click .remove-student-link': 'onClickRemoveStudentLink'
|
'click .remove-student-link': 'onClickRemoveStudentLink'
|
||||||
|
'click .assign-student-button': 'onClickAssign'
|
||||||
'click .enroll-student-button': 'onClickEnroll'
|
'click .enroll-student-button': 'onClickEnroll'
|
||||||
'click .assign-to-selected-students': 'onClickBulkAssign'
|
'click .assign-to-selected-students': 'onClickBulkAssign'
|
||||||
'click .enroll-selected-students': 'onClickBulkEnroll'
|
'click .enroll-selected-students': 'onClickBulkEnroll'
|
||||||
'click .export-student-progress-btn': 'onClickExportStudentProgress'
|
'click .export-student-progress-btn': 'onClickExportStudentProgress'
|
||||||
'click .select-all': 'onClickSelectAll'
|
'click .select-all': 'onClickSelectAll'
|
||||||
'click .student-checkbox': 'onClickStudentCheckbox'
|
'click .student-checkbox': 'onClickStudentCheckbox'
|
||||||
'change .course-select': 'onChangeCourseSelect'
|
'change .course-select, .bulk-course-select': (e) ->
|
||||||
|
@trigger 'course-select:change', { selectedCourse: @courses.get($(e.currentTarget).val()) }
|
||||||
|
|
||||||
|
getInitialState: ->
|
||||||
|
if Backbone.history.getHash() in ['students-tab', 'course-progress-tab']
|
||||||
|
activeTab = '#' + Backbone.history.getHash()
|
||||||
|
else
|
||||||
|
activeTab = '#students-tab'
|
||||||
|
{
|
||||||
|
sortAttribute: 'name'
|
||||||
|
sortDirection: 1
|
||||||
|
activeTab
|
||||||
|
students: new Users()
|
||||||
|
classCode: ""
|
||||||
|
joinURL: ""
|
||||||
|
errors:
|
||||||
|
assigningToNobody: false
|
||||||
|
assigningToUnenrolled: false
|
||||||
|
selectedCourse: undefined
|
||||||
|
classStats:
|
||||||
|
averagePlaytime: ""
|
||||||
|
totalPlaytime: ""
|
||||||
|
averageLevelsComplete: ""
|
||||||
|
totalLevelsComplete: ""
|
||||||
|
enrolledUsers: ""
|
||||||
|
}
|
||||||
|
|
||||||
initialize: (options, classroomID) ->
|
initialize: (options, classroomID) ->
|
||||||
super(options)
|
super(options)
|
||||||
@progressDotTemplate = require 'templates/courses/progress-dot'
|
@singleStudentCourseProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-course'
|
||||||
|
@singleStudentLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-level'
|
||||||
@sortAttribute = 'name'
|
@allStudentsLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-all-students-single-level'
|
||||||
@sortDirection = 1
|
|
||||||
|
@state = new State(@getInitialState())
|
||||||
|
window.location.hash = @state.get('activeTab') # TODO: Don't push to URL history (maybe don't use url fragment for default tab)
|
||||||
|
|
||||||
@classroom = new Classroom({ _id: classroomID })
|
@classroom = new Classroom({ _id: classroomID })
|
||||||
@classroom.fetch()
|
@classroom.fetch()
|
||||||
@supermodel.trackModel(@classroom)
|
@supermodel.trackModel(@classroom)
|
||||||
|
|
||||||
|
@students = new Users()
|
||||||
@listenTo @classroom, 'sync', ->
|
@listenTo @classroom, 'sync', ->
|
||||||
@students = new Users()
|
|
||||||
jqxhrs = @students.fetchForClassroom(@classroom, removeDeleted: true)
|
jqxhrs = @students.fetchForClassroom(@classroom, removeDeleted: true)
|
||||||
if jqxhrs.length > 0
|
if jqxhrs.length > 0
|
||||||
@supermodel.trackCollection(@students)
|
@supermodel.trackCollection(@students)
|
||||||
@listenTo @students, 'sync', @sortByName
|
|
||||||
@listenTo @students, 'sort', @renderSelectors.bind(@, '.students-table', '.student-levels-table')
|
|
||||||
|
|
||||||
@classroom.sessions = new LevelSessions()
|
@classroom.sessions = new LevelSessions()
|
||||||
requests = @classroom.sessions.fetchForAllClassroomMembers(@classroom)
|
requests = @classroom.sessions.fetchForAllClassroomMembers(@classroom)
|
||||||
@supermodel.trackRequests(requests)
|
@supermodel.trackRequests(requests)
|
||||||
|
@ -66,47 +101,119 @@ module.exports = class TeacherClassView extends RootView
|
||||||
@courseInstances = new CourseInstances()
|
@courseInstances = new CourseInstances()
|
||||||
@courseInstances.fetchForClassroom(classroomID)
|
@courseInstances.fetchForClassroom(classroomID)
|
||||||
@supermodel.trackCollection(@courseInstances)
|
@supermodel.trackCollection(@courseInstances)
|
||||||
|
|
||||||
@levels = new Levels()
|
@levels = new Levels()
|
||||||
@levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts'}})
|
@levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts'}})
|
||||||
@supermodel.trackCollection(@levels)
|
@supermodel.trackCollection(@levels)
|
||||||
|
|
||||||
|
@attachMediatorEvents()
|
||||||
|
|
||||||
|
attachMediatorEvents: () ->
|
||||||
|
@listenTo @state, 'sync change', @render
|
||||||
|
# Model/Collection events
|
||||||
|
@listenTo @classroom, 'sync change update', ->
|
||||||
|
@removeDeletedStudents()
|
||||||
|
classCode = @classroom.get('codeCamel') or @classroom.get('code')
|
||||||
|
@state.set {
|
||||||
|
classCode: classCode
|
||||||
|
joinURL: document.location.origin + "/courses?_cc=" + classCode
|
||||||
|
}
|
||||||
|
@listenTo @courses, 'sync change update', ->
|
||||||
|
@setCourseMembers() # Is this necessary?
|
||||||
|
@state.set selectedCourse: @courses.first() unless @state.get('selectedCourse')
|
||||||
|
@listenTo @courseInstances, 'sync change update', ->
|
||||||
|
@setCourseMembers()
|
||||||
|
@render() # TODO: use state
|
||||||
|
@listenTo @courseInstances, 'add-members', ->
|
||||||
|
noty text: $.i18n.t('teacher.assigned'), layout: 'center', type: 'information', killer: true, timeout: 5000
|
||||||
|
@listenToOnce @students, 'sync', # TODO: This seems like it's in the wrong place?
|
||||||
|
@sortByName
|
||||||
|
@listenTo @students, 'sync change update add remove reset', ->
|
||||||
|
# Set state/props of things that depend on students?
|
||||||
|
# Set specific parts of state based on the models, rather than just dumping the collection there?
|
||||||
|
@removeDeletedStudents()
|
||||||
|
@calculateProgressAndLevels()
|
||||||
|
classStats = @calculateClassStats()
|
||||||
|
@state.set classStats: classStats if classStats
|
||||||
|
@state.set students: @students
|
||||||
|
@listenTo @students, 'sort', ->
|
||||||
|
@state.set students: @students
|
||||||
|
@render()
|
||||||
|
|
||||||
|
# DOM events
|
||||||
|
@listenTo @, 'open-students-tab', ->
|
||||||
|
if window.location.hash isnt '#students-tab'
|
||||||
|
window.location.hash = '#students-tab'
|
||||||
|
@state.set activeTab: '#students-tab'
|
||||||
|
@listenTo @, 'open-course-progress-tab', ->
|
||||||
|
if window.location.hash isnt '#course-progress-tab'
|
||||||
|
window.location.hash = '#course-progress-tab'
|
||||||
|
@state.set activeTab: '#course-progress-tab'
|
||||||
|
@listenTo @, 'course-select:change', ({ selectedCourse }) ->
|
||||||
|
@state.set selectedCourse: selectedCourse
|
||||||
|
|
||||||
|
setCourseMembers: =>
|
||||||
|
for course in @courses.models
|
||||||
|
course.instance = @courseInstances.findWhere({ courseID: course.id, classroomID: @classroom.id })
|
||||||
|
course.members = course.instance?.get('members') or []
|
||||||
|
null
|
||||||
|
|
||||||
onLoaded: ->
|
onLoaded: ->
|
||||||
@removeDeletedStudents()
|
@removeDeletedStudents() # TODO: Move this to mediator listeners? For both classroom and students?
|
||||||
|
@calculateProgressAndLevels()
|
||||||
@classCode = @classroom.get('codeCamel') or @classroom.get('code')
|
super()
|
||||||
@joinURL = document.location.origin + "/courses?_cc=" + @classCode
|
|
||||||
|
afterRender: ->
|
||||||
@earliestIncompleteLevel = helper.calculateEarliestIncomplete(@classroom, @courses, @courseInstances, @students)
|
super(arguments...)
|
||||||
@latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @courseInstances, @students)
|
$('.progress-dot').each (i, el) ->
|
||||||
|
dot = $(el)
|
||||||
|
dot.tooltip({
|
||||||
|
html: true
|
||||||
|
container: dot
|
||||||
|
}).delegate '.tooltip', 'mousemove', ->
|
||||||
|
dot.tooltip('hide')
|
||||||
|
|
||||||
|
calculateProgressAndLevels: ->
|
||||||
|
return unless @supermodel.progress is 1
|
||||||
|
# TODO: How to structure this in @state?
|
||||||
for student in @students.models
|
for student in @students.models
|
||||||
# TODO: this is a weird hack
|
# TODO: this is a weird hack
|
||||||
studentsStub = new Users([ student ])
|
studentsStub = new Users([ student ])
|
||||||
student.latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @courseInstances, studentsStub)
|
student.latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @courseInstances, studentsStub)
|
||||||
|
|
||||||
|
earliestIncompleteLevel = helper.calculateEarliestIncomplete(@classroom, @courses, @courseInstances, @students)
|
||||||
|
latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @courseInstances, @students)
|
||||||
|
|
||||||
classroomsStub = new Classrooms([ @classroom ])
|
classroomsStub = new Classrooms([ @classroom ])
|
||||||
@progressData = helper.calculateAllProgress(classroomsStub, @courses, @courseInstances, @students)
|
progressData = helper.calculateAllProgress(classroomsStub, @courses, @courseInstances, @students)
|
||||||
# @conceptData = helper.calculateConceptsCovered(classroomsStub, @courses, @campaigns, @courseInstances, @students)
|
# conceptData: helper.calculateConceptsCovered(classroomsStub, @courses, @campaigns, @courseInstances, @students)
|
||||||
|
|
||||||
@selectedCourse = @courses.first()
|
@state.set {
|
||||||
super()
|
earliestIncompleteLevel
|
||||||
|
latestCompleteLevel
|
||||||
|
progressData
|
||||||
|
classStats: @calculateClassStats()
|
||||||
|
}
|
||||||
|
|
||||||
copyCode: ->
|
copyCode: ->
|
||||||
@$('#join-code-input').val(@classCode).select()
|
@$('#join-code-input').val(@state.get('classCode')).select()
|
||||||
@tryCopy()
|
@tryCopy()
|
||||||
|
|
||||||
copyURL: ->
|
copyURL: ->
|
||||||
@$('#join-url-input').val(@joinURL).select()
|
@$('#join-url-input').val(@state.get('joinURL')).select()
|
||||||
@tryCopy()
|
@tryCopy()
|
||||||
|
|
||||||
tryCopy: ->
|
tryCopy: ->
|
||||||
try
|
try
|
||||||
document.execCommand('copy')
|
document.execCommand('copy')
|
||||||
application.tracker?.trackEvent 'Classroom copy URL', category: 'Courses', classroomID: @classroom.id, url: @joinURL
|
application.tracker?.trackEvent 'Classroom copy URL', category: 'Courses', classroomID: @classroom.id, url: @state.joinURL
|
||||||
catch err
|
catch err
|
||||||
message = 'Oops, unable to copy'
|
message = 'Oops, unable to copy'
|
||||||
noty text: message, layout: 'topCenter', type: 'error', killer: false
|
noty text: message, layout: 'topCenter', type: 'error', killer: false
|
||||||
|
|
||||||
|
onClickUnarchive: ->
|
||||||
|
@classroom.save { archived: false }
|
||||||
|
|
||||||
onClickEditClassroom: (e) ->
|
onClickEditClassroom: (e) ->
|
||||||
classroom = @classroom
|
classroom = @classroom
|
||||||
modal = new ClassroomSettingsModal({ classroom: classroom })
|
modal = new ClassroomSettingsModal({ classroom: classroom })
|
||||||
|
@ -125,7 +232,6 @@ module.exports = class TeacherClassView extends RootView
|
||||||
|
|
||||||
onStudentRemoved: (e) ->
|
onStudentRemoved: (e) ->
|
||||||
@students.remove(e.user)
|
@students.remove(e.user)
|
||||||
@render()
|
|
||||||
application.tracker?.trackEvent 'Classroom removed student', category: 'Courses', classroomID: @classroom.id, userID: e.user.id
|
application.tracker?.trackEvent 'Classroom removed student', category: 'Courses', classroomID: @classroom.id, userID: e.user.id
|
||||||
|
|
||||||
onClickAddStudents: (e) =>
|
onClickAddStudents: (e) =>
|
||||||
|
@ -134,32 +240,33 @@ module.exports = class TeacherClassView extends RootView
|
||||||
@listenToOnce modal, 'hide', @render
|
@listenToOnce modal, 'hide', @render
|
||||||
|
|
||||||
removeDeletedStudents: () ->
|
removeDeletedStudents: () ->
|
||||||
|
return unless @classroom.loaded and @students.loaded
|
||||||
_.remove(@classroom.get('members'), (memberID) =>
|
_.remove(@classroom.get('members'), (memberID) =>
|
||||||
not @students.get(memberID) or @students.get(memberID)?.get('deleted')
|
not @students.get(memberID) or @students.get(memberID)?.get('deleted')
|
||||||
)
|
)
|
||||||
true
|
true
|
||||||
|
|
||||||
sortByName: (e) ->
|
sortByName: (e) ->
|
||||||
if @sortValue is 'name'
|
if @state.get('sortValue') is 'name'
|
||||||
@sortDirection = -@sortDirection
|
@state.set('sortDirection', -@state.get('sortDirection'))
|
||||||
else
|
else
|
||||||
@sortValue = 'name'
|
@state.set('sortValue', 'name')
|
||||||
@sortDirection = 1
|
@state.set('sortDirection', 1)
|
||||||
|
|
||||||
dir = @sortDirection
|
dir = @state.get('sortDirection')
|
||||||
@students.comparator = (student1, student2) ->
|
@students.comparator = (student1, student2) ->
|
||||||
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
|
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
|
||||||
@students.sort()
|
@students.sort()
|
||||||
|
|
||||||
sortByProgress: (e) ->
|
sortByProgress: (e) ->
|
||||||
if @sortValue is 'progress'
|
if @state.get('sortValue') is 'progress'
|
||||||
@sortDirection = -@sortDirection
|
@state.set('sortDirection', -@state.get('sortDirection'))
|
||||||
else
|
else
|
||||||
@sortValue = 'progress'
|
@state.set('sortValue', 'progress')
|
||||||
@sortDirection = 1
|
@state.set('sortDirection', 1)
|
||||||
|
|
||||||
dir = @sortDirection
|
dir = @state.get('sortDirection')
|
||||||
|
|
||||||
@students.comparator = (student) ->
|
@students.comparator = (student) ->
|
||||||
#TODO: I would like for this to be in the Level model,
|
#TODO: I would like for this to be in the Level model,
|
||||||
# but it doesn't know about its own courseNumber
|
# but it doesn't know about its own courseNumber
|
||||||
|
@ -179,19 +286,24 @@ module.exports = class TeacherClassView extends RootView
|
||||||
userID = $(e.currentTarget).data('user-id')
|
userID = $(e.currentTarget).data('user-id')
|
||||||
user = @students.get(userID)
|
user = @students.get(userID)
|
||||||
selectedUsers = new Users([user])
|
selectedUsers = new Users([user])
|
||||||
modal = new ActivateLicensesModal { @classroom, selectedUsers, users: @students }
|
@enrollStudents(selectedUsers)
|
||||||
@openModalView(modal)
|
|
||||||
modal.once 'redeem-users', -> document.location.reload()
|
|
||||||
application.tracker?.trackEvent 'Classroom started enroll students', category: 'Courses'
|
|
||||||
|
|
||||||
onClickBulkEnroll: ->
|
onClickBulkEnroll: ->
|
||||||
courseID = @$('.bulk-course-select').val()
|
courseID = @$('.bulk-course-select').val()
|
||||||
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
|
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
|
||||||
userIDs = @getSelectedStudentIDs().toArray()
|
userIDs = @getSelectedStudentIDs().toArray()
|
||||||
selectedUsers = new Users(@students.get(userID) for userID in userIDs)
|
selectedUsers = new Users(@students.get(userID) for userID in userIDs)
|
||||||
|
@enrollStudents(selectedUsers)
|
||||||
|
|
||||||
|
enrollStudents: (selectedUsers) ->
|
||||||
modal = new ActivateLicensesModal { @classroom, selectedUsers, users: @students }
|
modal = new ActivateLicensesModal { @classroom, selectedUsers, users: @students }
|
||||||
@openModalView(modal)
|
@openModalView(modal)
|
||||||
modal.once 'redeem-users', -> document.location.reload()
|
modal.once 'redeem-users', (enrolledUsers) =>
|
||||||
|
enrolledUsers.each (newUser) =>
|
||||||
|
user = @students.get(newUser.id)
|
||||||
|
if user
|
||||||
|
user.set(newUser.attributes)
|
||||||
|
null
|
||||||
application.tracker?.trackEvent 'Classroom started enroll students', category: 'Courses'
|
application.tracker?.trackEvent 'Classroom started enroll students', category: 'Courses'
|
||||||
|
|
||||||
onClickExportStudentProgress: ->
|
onClickExportStudentProgress: ->
|
||||||
|
@ -201,10 +313,10 @@ module.exports = class TeacherClassView extends RootView
|
||||||
concepts = []
|
concepts = []
|
||||||
for course, index in @courses.models
|
for course, index in @courses.models
|
||||||
instance = @courseInstances.findWhere({ courseID: course.id, classroomID: @classroom.id })
|
instance = @courseInstances.findWhere({ courseID: course.id, classroomID: @classroom.id })
|
||||||
if instance && instance.hasMember(student)
|
if instance and instance.hasMember(student)
|
||||||
# TODO: @levels collection is for the classroom, and not per-course
|
# TODO: @levels collection is for the classroom, and not per-course
|
||||||
for level, index in @levels.models
|
for level, index in @levels.models
|
||||||
progress = @progressData.get({ classroom: @classroom, course: course, level: level, user: student })
|
progress = @state.get('progressData').get({ classroom: @classroom, course: course, level: level, user: student })
|
||||||
concepts.push(level.get('concepts') ? []) if progress?.completed
|
concepts.push(level.get('concepts') ? []) if progress?.completed
|
||||||
concepts = _.union(_.flatten(concepts))
|
concepts = _.union(_.flatten(concepts))
|
||||||
conceptsString = _.map(concepts, (c) -> $.i18n.t("concepts." + c)).join(', ')
|
conceptsString = _.map(concepts, (c) -> $.i18n.t("concepts." + c)).join(', ')
|
||||||
|
@ -217,27 +329,37 @@ module.exports = class TeacherClassView extends RootView
|
||||||
encodedUri = encodeURI(csvContent)
|
encodedUri = encodeURI(csvContent)
|
||||||
window.open(encodedUri)
|
window.open(encodedUri)
|
||||||
|
|
||||||
|
|
||||||
|
onClickAssign: (e) ->
|
||||||
|
userID = $(e.currentTarget).data('user-id')
|
||||||
|
user = @students.get(userID)
|
||||||
|
members = [userID]
|
||||||
|
courseID = $(e.currentTarget).data('course-id')
|
||||||
|
|
||||||
|
@assignCourse courseID, members
|
||||||
|
|
||||||
onClickBulkAssign: ->
|
onClickBulkAssign: ->
|
||||||
courseID = @$('.bulk-course-select').val()
|
courseID = @$('.bulk-course-select').val()
|
||||||
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
|
|
||||||
selectedIDs = @getSelectedStudentIDs()
|
selectedIDs = @getSelectedStudentIDs()
|
||||||
members = selectedIDs.filter((index, userID) =>
|
members = selectedIDs.filter((index, userID) =>
|
||||||
user = @students.get(userID)
|
user = @students.get(userID)
|
||||||
user.isEnrolled()
|
user.isEnrolled()
|
||||||
).toArray()
|
).toArray()
|
||||||
|
|
||||||
@assigningToUnenrolled = _.any selectedIDs, (userID) =>
|
assigningToUnenrolled = _.any selectedIDs, (userID) =>
|
||||||
not @students.get(userID).isEnrolled()
|
not @students.get(userID).isEnrolled()
|
||||||
|
|
||||||
@$('.cant-assign-to-unenrolled').toggleClass('visible', @assigningToUnenrolled)
|
assigningToNobody = selectedIDs.length is 0
|
||||||
|
|
||||||
@assigningToNobody = selectedIDs.length is 0
|
@state.set errors: { assigningToNobody, assigningToUnenrolled }
|
||||||
@$('.no-students-selected').toggleClass('visible', @assigningToNobody)
|
|
||||||
|
@assignCourse courseID, members
|
||||||
|
|
||||||
|
# TODO: Move this to the model. Use promises/callbacks?
|
||||||
|
assignCourse: (courseID, members) ->
|
||||||
|
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
|
||||||
if courseInstance
|
if courseInstance
|
||||||
courseInstance.addMembers members, {
|
courseInstance.addMembers members
|
||||||
success: @onBulkAssignSuccess
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
courseInstance = new CourseInstance {
|
courseInstance = new CourseInstance {
|
||||||
courseID,
|
courseID,
|
||||||
|
@ -247,17 +369,11 @@ module.exports = class TeacherClassView extends RootView
|
||||||
}
|
}
|
||||||
@courseInstances.add(courseInstance)
|
@courseInstances.add(courseInstance)
|
||||||
courseInstance.save {}, {
|
courseInstance.save {}, {
|
||||||
success: =>
|
success: ->
|
||||||
courseInstance.addMembers members, {
|
courseInstance.addMembers members
|
||||||
success: @onBulkAssignSuccess
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
|
|
||||||
onBulkAssignSuccess: =>
|
|
||||||
@render() unless @destroyed
|
|
||||||
noty text: $.i18n.t('teacher.assigned'), layout: 'center', type: 'information', killer: true, timeout: 5000
|
|
||||||
|
|
||||||
onClickSelectAll: (e) ->
|
onClickSelectAll: (e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
checkboxes = @$('.student-checkbox input')
|
checkboxes = @$('.student-checkbox input')
|
||||||
|
@ -278,11 +394,8 @@ module.exports = class TeacherClassView extends RootView
|
||||||
checkboxes = @$('.student-checkbox input')
|
checkboxes = @$('.student-checkbox input')
|
||||||
@$('.select-all input').prop('checked', _.all(checkboxes, 'checked'))
|
@$('.select-all input').prop('checked', _.all(checkboxes, 'checked'))
|
||||||
|
|
||||||
onChangeCourseSelect: (e) ->
|
calculateClassStats: ->
|
||||||
@selectedCourse = @courses.get($(e.currentTarget).val())
|
return {} unless @classroom.sessions?.loaded and @students.loaded
|
||||||
@renderSelectors('.render-on-course-sync')
|
|
||||||
|
|
||||||
classStats: ->
|
|
||||||
stats = {}
|
stats = {}
|
||||||
|
|
||||||
playtime = 0
|
playtime = 0
|
||||||
|
@ -301,4 +414,5 @@ module.exports = class TeacherClassView extends RootView
|
||||||
|
|
||||||
enrolledUsers = @students.filter (user) -> user.get('coursePrepaidID')
|
enrolledUsers = @students.filter (user) -> user.get('coursePrepaidID')
|
||||||
stats.enrolledUsers = _.size(enrolledUsers)
|
stats.enrolledUsers = _.size(enrolledUsers)
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
|
@ -44,7 +44,7 @@ module.exports = class TeacherClassesView extends RootView
|
||||||
@courseInstances = new CourseInstances()
|
@courseInstances = new CourseInstances()
|
||||||
@courseInstances.fetchByOwner(me.id)
|
@courseInstances.fetchByOwner(me.id)
|
||||||
@supermodel.trackCollection(@courseInstances)
|
@supermodel.trackCollection(@courseInstances)
|
||||||
@progressDotTemplate = require 'templates/courses/progress-dot'
|
@progressDotTemplate = require 'templates/teachers/hovers/progress-dot-whole-course'
|
||||||
|
|
||||||
# Level Sessions loaded after onLoaded to prevent race condition in calculateDots
|
# Level Sessions loaded after onLoaded to prevent race condition in calculateDots
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ module.exports =
|
||||||
members = classroom.get('members') or []
|
members = classroom.get('members') or []
|
||||||
members = members.slice(memberSkip, memberSkip + memberLimit)
|
members = members.slice(memberSkip, memberSkip + memberLimit)
|
||||||
dbqs = []
|
dbqs = []
|
||||||
select = 'state.complete level creator playtime'
|
select = 'state.complete level creator playtime changed dateFirstCompleted'
|
||||||
for member in members
|
for member in members
|
||||||
dbqs.push(LevelSession.find({creator: member.toHexString()}).select(select).exec())
|
dbqs.push(LevelSession.find({creator: member.toHexString()}).select(select).exec())
|
||||||
results = yield dbqs
|
results = yield dbqs
|
||||||
|
|
|
@ -45,7 +45,8 @@ LevelSessionSchema.post 'init', (doc) ->
|
||||||
LevelSessionSchema.pre 'save', (next) ->
|
LevelSessionSchema.pre 'save', (next) ->
|
||||||
User = require './User' # Avoid mutual inclusion cycles
|
User = require './User' # Avoid mutual inclusion cycles
|
||||||
Level = require './Level'
|
Level = require './Level'
|
||||||
@set('changed', new Date())
|
now = new Date()
|
||||||
|
@set('changed', now)
|
||||||
|
|
||||||
id = @get('id')
|
id = @get('id')
|
||||||
initd = @previousStateInfo?
|
initd = @previousStateInfo?
|
||||||
|
@ -55,6 +56,7 @@ LevelSessionSchema.pre 'save', (next) ->
|
||||||
|
|
||||||
# Newly completed level
|
# Newly completed level
|
||||||
if not (initd and @previousStateInfo['state.complete']) and @get('state.complete')
|
if not (initd and @previousStateInfo['state.complete']) and @get('state.complete')
|
||||||
|
@set('dateFirstCompleted', now)
|
||||||
Level.findOne({slug: levelID}).select('concepts -_id').lean().exec (err, level) ->
|
Level.findOne({slug: levelID}).select('concepts -_id').lean().exec (err, level) ->
|
||||||
log.error err if err?
|
log.error err if err?
|
||||||
update = $inc: {'stats.gamesCompleted': 1}
|
update = $inc: {'stats.gamesCompleted': 1}
|
||||||
|
|
Loading…
Reference in a new issue