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:
phoenixeliot 2016-04-19 13:44:48 -07:00
parent 0ed99565d3
commit 8223122a6b
26 changed files with 564 additions and 227 deletions

View file

@ -139,16 +139,21 @@ module.exports =
levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true})
for level in levels.models
levelID = level.get('original')
progressData[classroom.id][course.id][levelID] = { completed: students.size() > 0, started: false }
progressData[classroom.id][course.id][levelID] = {
completed: students.size() > 0,
started: false
numStarted: 0
# numCompleted: 0
}
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[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
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
@ -158,21 +163,30 @@ module.exports =
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
courseProgress[levelID][userID].lastPlayed = new Date(session.get('changed'))
courseProgress[levelID].numStarted += 1
if session?.completed() # have finished this level
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].numCompleted += 1
courseProgress[levelID][userID].completed = true
courseProgress[levelID][userID].dateFirstCompleted = new Date(session.get('dateFirstCompleted') || session.get('changed'))
else # level started but not completed
courseProgress.completed = false
courseProgress[userID].completed = false
courseProgress[levelID].completed = false
courseProgress[levelID][userID].completed = false
courseProgress[levelID].dateFirstCompleted = null
courseProgress[levelID][userID].dateFirstCompleted = null
_.assign(progressData, progressMixin)
return progressData

View file

@ -831,6 +831,7 @@
thanks_header: "Request Received!"
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:"
back_to_classes: "Back to Classes"
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."
signup_with: "Sign up with:"
@ -1291,6 +1292,7 @@
view_class: "view class"
archive_class: "archive class"
unarchive_class: "unarchive class"
unarchive_this_class: "Unarchive this class"
no_students_yet: "This class has no students yet."
add_students: "Add Students"
create_new_class: "Create a New Class"
@ -1312,6 +1314,10 @@
latest_completed: "Latest Completed"
sort_by: "Sort by"
progress: "Progress"
completed: "Completed"
started: "Started"
click_to_view_progress: "click to view progress"
no_progress: "No progress"
select_course: "Select course to view"
course_overview: "Course Overview"
copy_class_code: "Copy Class Code"

View file

@ -21,7 +21,7 @@ module.exports = class CourseInstance extends CocoModel
data: { userID: userID }
}
_.extend options, opts
@fetch(options)
@fetch options
if userID is me.id
unless me.get('courseInstances')
me.set('courseInstances', [])
@ -32,6 +32,8 @@ module.exports = class CourseInstance extends CocoModel
method: 'POST'
url: _.result(@, 'url') + '/members'
data: { userIDs }
success: =>
@trigger 'add-members', { userIDs }
}
_.extend options, opts
@fetch(options)

5
app/models/State.coffee Normal file
View file

@ -0,0 +1,5 @@
CocoModel = require './CocoModel'
schema = require 'schemas/models/poll.schema'
module.exports = class State extends CocoModel
@className: 'State'

View file

@ -54,6 +54,10 @@ _.extend LevelSessionSchema.properties,
changed: c.date
title: 'Changed'
readOnly: true
dateFirstCompleted: c.stringDate
title: 'Completed'
readOnly: true
team: c.shortString()
level: LevelSessionLevelSchema

View file

@ -1,6 +1,6 @@
@import "app/styles/bootstrap/variables"
@import "app/styles/mixins"
@import "app/styles/style-flat"
@import "app/styles/style-flat-variables"
#about-view
@ -331,4 +331,4 @@
content: ""
display: block
height: 55px
margin: -55px 0 0 0
margin: -55px 0 0 0

View file

@ -1,4 +1,13 @@
#activate-licenses-modal
select
min-width: 80%
.checkbox
margin: 0
input[type='checkbox']
margin-top: 8px
.modal-content
padding: 60px
width: 690px

View file

@ -1,6 +1,9 @@
@import "app/styles/bootstrap/variables"
@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)
background-color: #ebebeb
@ -187,6 +190,26 @@
.progress-dot
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
.checkbox-flat
@ -255,4 +278,4 @@
float: right
.export-student-progress-btn
margin-top: 10px
margin-top: 10px

View file

@ -1,6 +1,6 @@
@import "app/styles/bootstrap/variables"
@import "app/styles/mixins"
@import "app/styles/style-flat"
@import "app/styles/style-flat-variables"
#teacher-classes-view

View file

@ -1,6 +1,6 @@
@import "app/styles/bootstrap/variables"
@import "app/styles/mixins"
@import "app/styles/style-flat"
@import "app/styles/style-flat-variables"
#teacher-dashboard-nav
vertical-align: middle

View file

@ -1,6 +1,6 @@
@import "app/styles/bootstrap/variables"
@import "app/styles/mixins"
@import "app/styles/style-flat"
@import "app/styles/style-flat-variables"
#new-home-view

View file

@ -0,0 +1,7 @@
$headline-font: 'Arvo', serif
$body-font: 'Open Sans', sans-serif
$burgandy: #7D0101
$gold: #F2BE19
$navy: #0E4C60
$forest: #20572B

View file

@ -1,13 +1,9 @@
@import "app/styles/bootstrap/variables"
@import "app/styles/mixins"
@import "app/styles/style-flat-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']
// Google Fonts version of Arvo only has Latin glyphs, not Cyrillic
// 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
font-family: 'Open Sans', serif
$burgandy: #7D0101
$gold: #F2BE19
$navy: #0E4C60
$forest: #20572B
.style-flat
background: white
color: black
@ -194,6 +185,15 @@ $forest: #20572B
color: $gold
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
.btn
@ -272,6 +272,8 @@ $forest: #20572B
// TODO: Font size 18? Inconsistent with buttons on teacher-class-view bulk assign
// Tooltips
.tooltip.in
opacity: 1
.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
@ -334,6 +336,7 @@ $forest: #20572B
background: white
border-radius: 20px
min-width: 150px
max-width: 600px
// Checkboxes

View file

@ -5,49 +5,67 @@ block modal-header-content
.text-center
h1(data-i18n="teacher.enroll_students")
h2(data-i18n="courses.grants_lifetime_access")
if view.classroom
p= view.classroom.get('name')
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
.text-center
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
.row
.col-sm-10.col-sm-offset-1
.text-center.m-b-3
.small.color-navy
span(data-i18n='teacher.show_students_from')
span.spr :
select.classroom-select
each classroom in view.classrooms.models
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 :
.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
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')
input.user-checkbox(type="checkbox", disabled=false, checked=selected, data-user-id=user.id, name='user')
span.spr= user.broadName()
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()
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
p.small-details.not-enough-enrollments(class=(tooManySelected ? 'visible' : ''))
span(data-i18n='teacher.not_enough_enrollments')
p.small-details
span.spr(data-i18n="courses.enrollment_credits_available")
span#total-available= view.prepaids.totalAvailable()
p.small-details.not-enough-enrollments
span(data-i18n='teacher.not_enough_enrollments')
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#total-selected-span
= numToEnroll
| )
span.spl(data-i18n="courses.students1")

View file

@ -18,6 +18,11 @@ block content
if classroom.loaded
.container
+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')
a.label.edit-classroom(data-classroom-id=classroom.id)
span(data-i18n='teacher.edit_class_settings')
@ -25,7 +30,7 @@ block content
.classroom-info-row.row.m-t-5
.classroom-details.col-md-3
- var stats = view.classStats()
- var stats = state.get('classStats')
h4.m-b-2(data-i18n='teacher.class_overview')
.language.small-details
@ -68,55 +73,57 @@ block content
span(data-i18n='teacher.export_student_progress')
//- .concepts.small-details
//- if view.progressData
//- if state.get('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')
//- if state.get('started')
//- b.concept(class=state.get('completed') ? 'forest' : 'gold')
//- span(data-i18n='concepts.'+name)
.completeness-info.col-md-4
h4.m-b-2
 
if view.earliestIncompleteLevel
if state.get('earliestIncompleteLevel')
div.small-details
span(data-i18n='teacher.earliest_incomplete')
span :
+longLevelName(view.earliestIncompleteLevel)
+inlineUserList(view.earliestIncompleteLevel.users)
if view.latestCompleteLevel
+longLevelName(state.get('earliestIncompleteLevel'))
+inlineUserList(state.get('earliestIncompleteLevel').users)
if state.get('latestCompleteLevel')
div.small-details.m-t-3
span(data-i18n='teacher.latest_complete')
span :
+longLevelName(view.latestCompleteLevel)
+inlineUserList(view.latestCompleteLevel.users)
+longLevelName(state.get('latestCompleteLevel'))
+inlineUserList(state.get('latestCompleteLevel').users)
.adding-students.col-md-5
h4.m-b-2
span(data-i18n='teacher.adding_students')
span :
+copyCodes
+addStudentsButton
ul.nav.nav-tabs.m-t-5(role='tablist')
li.active
a(href='#students-tab' data-toggle='tab')
ul#student-info-tabs.nav.nav-tabs.m-t-5(role='tablist')
li(class=(state.get('activeTab')==="#students-tab" ? 'active' : ''))
a.students-tab-btn(href='#students-tab')
.small-details.text-center(data-i18n='teacher.students')
.tab-spacer
li
a(href='#course-progress-tab' data-toggle='tab')
li(class=(state.get('activeTab')==="#course-progress-tab" ? 'active' : ''))
a.course-progress-tab-btn(href='#course-progress-tab')
.small-details.text-center(data-i18n='teacher.course_progress')
.tab-filler
.tab-content
+studentsTab
+courseProgressTab
if state.get('activeTab')=='#students-tab'
+studentsTab
else
+courseProgressTab
mixin breadcrumbs
.breadcrumbs
a(data-i18n='teacher.my_classes' href='/teachers/classes')
@ -153,7 +160,7 @@ mixin addStudentsButton
span(data-i18n='teacher.add_students_manually')
mixin studentsTab
#students-tab.tab-pane.active
#students-tab
+bulkAssignControls
table.students-table
thead
@ -165,7 +172,7 @@ mixin studentsTab
th
+sortButtons
tbody
each student in view.students.models
each student in state.get('students').models
+studentRow(student)
mixin sortButtons
@ -199,12 +206,15 @@ mixin studentRow(student)
div
+longLevelName(student.latestCompleteLevel)
td
if view.progressData
each course, index in view.courses.models
if state.get('progressData')
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 })
if instance && instance.hasMember(student)
- var progress = view.progressData.get({ classroom: view.classroom, course: course, user: student })
+progressDot(progress, 'CS' + (index+1))
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, user: student })
- var levelsTotal = trimCourse.levels.length
//- - var level = ???
+studentCourseProgressDot(progress, levelsTotal, level, 'CS' + (index+1))
unless student.isEnrolled()
+enrollStudentButton(student)
//- td
@ -219,7 +229,7 @@ mixin enrollStudentButton(student)
span(data-i18n='teacher.enroll_student')
mixin courseProgressTab
#course-progress-tab.tab-pane.m-t-3
#course-progress-tab.m-t-3
if view.courses
.text-center
span(data-i18n='teacher.select_course')
@ -227,18 +237,50 @@ mixin courseProgressTab
select.course-select
each trimCourse in view.classroom.get('courses')
- var course = view.courses.get(trimCourse._id);
option(value=course.id)
option(value=course.id selected=(course===state.get('selectedCourse')))
= course.get('name')
if view.progressData
if state.get('progressData')
.render-on-course-sync
+courseOverview
.student-levels-table
+sortButtons
each student in view.students.models
+studentLevelsRow(student)
each student in state.get('students').models
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
- var course = view.selectedCourse
- var course = state.get('selectedCourse')
- var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models
.course-overview-row
.course-title.student-name
@ -247,8 +289,8 @@ mixin courseOverview
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)
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level })
+allStudentsLevelProgressDot(progress, level, index+1)
mixin studentLevelsRow(student)
.student-levels-row.alternating-background
@ -256,20 +298,35 @@ mixin studentLevelsRow(student)
div.student-name= student.broadName()
div.student-email.small-details= student.get('email')
div.student-levels-progress
- var course = view.selectedCourse
- var course = state.get('selectedCourse')
- var levels = view.classroom.getLevels({courseID: course.id, withoutLadderLevels: true}).models
each level, index in levels
- var progress = view.progressData.get({ classroom: view.classroom, course: course, level: level, user: student })
+progressDot(progress, index+1)
mixin progressDot(progress, label)
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level, user: student })
+studentLevelProgressDot(progress, level, index+1)
mixin studentCourseProgressDot(progress, levelsTotal, level, 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')
- _.assign(progress, { levelsTotal: levelsTotal })
.progress-dot(class=dotClass, data-html='true', data-title=view.singleStudentCourseProgressDotTemplate(progress))
+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)
.dot-label.text-center
.dot-label-inner
@ -278,23 +335,23 @@ mixin progressDotLabel(label)
mixin copyCodes
div.copy-button-group.form-inline.m-b-3
.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
span(data-i18n='teacher.copy_class_code')
div.text-center.small(data-i18n='teacher.class_code_blurb')
div.copy-button-group.form-inline.m-b-3
.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
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
.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')
.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.small
span(data-i18n='teacher.bulk_assign')
@ -302,9 +359,9 @@ mixin bulkAssignControls
select.bulk-course-select.form-control
each trimCourse in _.rest(view.classroom.get('courses'))
- var course = view.courses.get(trimCourse._id)
option(value=course.id)
option(value=course.id selected=(course===state.get('selectedCourse')))
= 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')
span(data-i18n='teacher.enroll_selected_students')

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -219,6 +219,10 @@ block content
p
span.spr(data-i18n="teachers_quote.thanks_p")
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()
h5(data-i18n="teachers_quote.finish_signup")

View file

@ -137,6 +137,7 @@ module.exports = class CocoView extends Backbone.View
context._ = _
context.document = document
context.i18n = utils.i18n
context.state = @state
context
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
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
clearTimeout @pointerDelayTimeout
clearTimeout @pointerDurationTimeout

View file

@ -1,7 +1,9 @@
ModalView = require 'views/core/ModalView'
State = require 'models/State'
template = require 'templates/courses/activate-licenses-modal'
CocoCollection = require 'collections/CocoCollection'
Prepaids = require 'collections/Prepaids'
Classroom = require 'models/Classroom'
Classrooms = require 'collections/Classrooms'
User = require 'models/User'
Users = require 'collections/Users'
@ -11,14 +13,23 @@ module.exports = class ActivateLicensesModal extends ModalView
template: template
events:
'change input': 'updateSelectionSpans'
'change select': 'replaceStudentList'
'change input[type="checkbox"][name="user"]': 'updateSelectedStudents'
'change select.classroom-select': 'replaceStudentList'
'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) ->
@state = new State(@getInitialState(options))
@classroom = options.classroom
@users = options.users
@selectedUsers = options.selectedUsers
@users = options.users.clone()
@users.comparator = (user) -> user.broadName().toLowerCase()
@prepaids = new Prepaids()
@prepaids.comparator = '_id'
@prepaids.fetchByCreator(me.id)
@ -33,68 +44,58 @@ module.exports = class ActivateLicensesModal extends ModalView
@supermodel.trackRequests(jqxhrs)
})
@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: ->
super()
@updateSelectionSpans()
# @updateSelectedStudents() # TODO: refactor to event/state style
updateSelectionSpans: ->
targets = @$('input[name="targets"]:checked').val()
if targets is 'given'
numToActivate = 1
updateSelectedStudents: (e) ->
userID = $(e.currentTarget).data('user-id')
user = @users.get(userID)
if @state.get('selectedUsers').contains(user)
@state.get('selectedUsers').remove(user)
else
numToActivate = @$('input[name="user"]:checked:not(:disabled)').length
@$('#total-selected-span').text(numToActivate)
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)
@state.get('selectedUsers').add(user)
# @render() # TODO: Have @state automatically listen to children's change events?
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!
if selectedClassroomID is 'all-students'
@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.reset(users)
@users.sort()
else
@users.reset(@classrooms.get(selectedClassroomID).users.models)
@trigger('users:change')
@render()
null
showProgress: ->
@$('#submit-form-area').addClass('hide')
@$('#progress-area').removeClass('hide')
hideProgress: ->
@$('#submit-form-area').removeClass('hide')
@$('#progress-area').addClass('hide')
onSubmitForm: (e) ->
e.preventDefault()
@$('#error-alert').addClass('hide')
@usersToRedeem = new CocoCollection([], {model: User})
targets = @$('input[name="targets"]:checked').val()
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()
@state.set error: null
usersToRedeem = @state.get('visibleSelectedUsers')
@redeemUsers(usersToRedeem)
redeemUsers: ->
if not @usersToRedeem.size()
redeemUsers: (usersToRedeem) ->
if not usersToRedeem.size()
@finishRedeemUsers()
@hide()
return
user = @usersToRedeem.first()
user = usersToRedeem.first()
prepaid = @prepaids.find((prepaid) -> prepaid.get('properties')?.endDate? and prepaid.openSpots() > 0)
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots() > 0) unless prepaid
$.ajax({
@ -102,19 +103,20 @@ module.exports = class ActivateLicensesModal extends ModalView
url: _.result(prepaid, 'url') + '/redeemers'
data: { userID: user.id }
context: @
success: ->
@usersToRedeem.remove(user)
pct = 100 * (@usersToRedeem.originalSize - @usersToRedeem.size() / @usersToRedeem.originalSize)
@$('#progress-area .progress-bar').css('width', "#{pct.toFixed(1)}%")
success: (prepaid) ->
user.set('coursePrepaidID', prepaid._id)
usersToRedeem.remove(user)
# 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
@redeemUsers()
@redeemUsers(usersToRedeem)
error: (jqxhr, textStatus, errorThrown) ->
if jqxhr.status is 402
message = arguments[2]
else
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: ->
@trigger 'redeem-users'
@trigger 'redeem-users', @state.get('selectedUsers')

View file

@ -1,4 +1,5 @@
RootView = require 'views/core/RootView'
State = require 'models/State'
template = require 'templates/courses/teacher-class-view'
helper = require 'lib/coursesHelper'
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
@ -12,6 +13,7 @@ Levels = require 'collections/Levels'
LevelSessions = require 'collections/LevelSessions'
User = require 'models/User'
Users = require 'collections/Users'
Course = require 'models/Course'
Courses = require 'collections/Courses'
CourseInstance = require 'models/CourseInstance'
CourseInstances = require 'collections/CourseInstances'
@ -21,6 +23,13 @@ module.exports = class TeacherClassView extends RootView
template: template
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 .add-students-btn': 'onClickAddStudents'
'click .sort-by-name': 'sortByName'
@ -28,33 +37,59 @@ module.exports = class TeacherClassView extends RootView
'click #copy-url-btn': 'copyURL'
'click #copy-code-btn': 'copyCode'
'click .remove-student-link': 'onClickRemoveStudentLink'
'click .assign-student-button': 'onClickAssign'
'click .enroll-student-button': 'onClickEnroll'
'click .assign-to-selected-students': 'onClickBulkAssign'
'click .enroll-selected-students': 'onClickBulkEnroll'
'click .export-student-progress-btn': 'onClickExportStudentProgress'
'click .select-all': 'onClickSelectAll'
'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) ->
super(options)
@progressDotTemplate = require 'templates/courses/progress-dot'
@sortAttribute = 'name'
@sortDirection = 1
@singleStudentCourseProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-course'
@singleStudentLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-level'
@allStudentsLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-all-students-single-level'
@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.fetch()
@supermodel.trackModel(@classroom)
@students = new Users()
@listenTo @classroom, 'sync', ->
@students = new Users()
jqxhrs = @students.fetchForClassroom(@classroom, removeDeleted: true)
if jqxhrs.length > 0
@supermodel.trackCollection(@students)
@listenTo @students, 'sync', @sortByName
@listenTo @students, 'sort', @renderSelectors.bind(@, '.students-table', '.student-levels-table')
@classroom.sessions = new LevelSessions()
requests = @classroom.sessions.fetchForAllClassroomMembers(@classroom)
@supermodel.trackRequests(requests)
@ -66,47 +101,119 @@ module.exports = class TeacherClassView extends RootView
@courseInstances = new CourseInstances()
@courseInstances.fetchForClassroom(classroomID)
@supermodel.trackCollection(@courseInstances)
@levels = new Levels()
@levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts'}})
@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: ->
@removeDeletedStudents()
@classCode = @classroom.get('codeCamel') or @classroom.get('code')
@joinURL = document.location.origin + "/courses?_cc=" + @classCode
@earliestIncompleteLevel = helper.calculateEarliestIncomplete(@classroom, @courses, @courseInstances, @students)
@latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @courseInstances, @students)
@removeDeletedStudents() # TODO: Move this to mediator listeners? For both classroom and students?
@calculateProgressAndLevels()
super()
afterRender: ->
super(arguments...)
$('.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
# TODO: this is a weird hack
studentsStub = new Users([ student ])
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 ])
@progressData = helper.calculateAllProgress(classroomsStub, @courses, @courseInstances, @students)
# @conceptData = helper.calculateConceptsCovered(classroomsStub, @courses, @campaigns, @courseInstances, @students)
@selectedCourse = @courses.first()
super()
progressData = helper.calculateAllProgress(classroomsStub, @courses, @courseInstances, @students)
# conceptData: helper.calculateConceptsCovered(classroomsStub, @courses, @campaigns, @courseInstances, @students)
@state.set {
earliestIncompleteLevel
latestCompleteLevel
progressData
classStats: @calculateClassStats()
}
copyCode: ->
@$('#join-code-input').val(@classCode).select()
@$('#join-code-input').val(@state.get('classCode')).select()
@tryCopy()
copyURL: ->
@$('#join-url-input').val(@joinURL).select()
@$('#join-url-input').val(@state.get('joinURL')).select()
@tryCopy()
tryCopy: ->
try
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
message = 'Oops, unable to copy'
noty text: message, layout: 'topCenter', type: 'error', killer: false
onClickUnarchive: ->
@classroom.save { archived: false }
onClickEditClassroom: (e) ->
classroom = @classroom
modal = new ClassroomSettingsModal({ classroom: classroom })
@ -125,7 +232,6 @@ module.exports = class TeacherClassView extends RootView
onStudentRemoved: (e) ->
@students.remove(e.user)
@render()
application.tracker?.trackEvent 'Classroom removed student', category: 'Courses', classroomID: @classroom.id, userID: e.user.id
onClickAddStudents: (e) =>
@ -134,32 +240,33 @@ module.exports = class TeacherClassView extends RootView
@listenToOnce modal, 'hide', @render
removeDeletedStudents: () ->
return unless @classroom.loaded and @students.loaded
_.remove(@classroom.get('members'), (memberID) =>
not @students.get(memberID) or @students.get(memberID)?.get('deleted')
)
true
sortByName: (e) ->
if @sortValue is 'name'
@sortDirection = -@sortDirection
if @state.get('sortValue') is 'name'
@state.set('sortDirection', -@state.get('sortDirection'))
else
@sortValue = 'name'
@sortDirection = 1
dir = @sortDirection
@state.set('sortValue', 'name')
@state.set('sortDirection', 1)
dir = @state.get('sortDirection')
@students.comparator = (student1, student2) ->
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
@students.sort()
sortByProgress: (e) ->
if @sortValue is 'progress'
@sortDirection = -@sortDirection
if @state.get('sortValue') is 'progress'
@state.set('sortDirection', -@state.get('sortDirection'))
else
@sortValue = 'progress'
@sortDirection = 1
dir = @sortDirection
@state.set('sortValue', 'progress')
@state.set('sortDirection', 1)
dir = @state.get('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
@ -179,19 +286,24 @@ module.exports = class TeacherClassView extends RootView
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'
@enrollStudents(selectedUsers)
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)
@enrollStudents(selectedUsers)
enrollStudents: (selectedUsers) ->
modal = new ActivateLicensesModal { @classroom, selectedUsers, users: @students }
@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'
onClickExportStudentProgress: ->
@ -201,10 +313,10 @@ module.exports = class TeacherClassView extends RootView
concepts = []
for course, index in @courses.models
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
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 = _.union(_.flatten(concepts))
conceptsString = _.map(concepts, (c) -> $.i18n.t("concepts." + c)).join(', ')
@ -217,27 +329,37 @@ module.exports = class TeacherClassView extends RootView
encodedUri = encodeURI(csvContent)
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: ->
courseID = @$('.bulk-course-select').val()
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
selectedIDs = @getSelectedStudentIDs()
members = selectedIDs.filter((index, userID) =>
user = @students.get(userID)
user.isEnrolled()
).toArray()
@assigningToUnenrolled = _.any selectedIDs, (userID) =>
assigningToUnenrolled = _.any selectedIDs, (userID) =>
not @students.get(userID).isEnrolled()
@$('.cant-assign-to-unenrolled').toggleClass('visible', @assigningToUnenrolled)
@assigningToNobody = selectedIDs.length is 0
@$('.no-students-selected').toggleClass('visible', @assigningToNobody)
assigningToNobody = selectedIDs.length is 0
@state.set errors: { assigningToNobody, assigningToUnenrolled }
@assignCourse courseID, members
# TODO: Move this to the model. Use promises/callbacks?
assignCourse: (courseID, members) ->
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
if courseInstance
courseInstance.addMembers members, {
success: @onBulkAssignSuccess
}
courseInstance.addMembers members
else
courseInstance = new CourseInstance {
courseID,
@ -247,17 +369,11 @@ module.exports = class TeacherClassView extends RootView
}
@courseInstances.add(courseInstance)
courseInstance.save {}, {
success: =>
courseInstance.addMembers members, {
success: @onBulkAssignSuccess
}
success: ->
courseInstance.addMembers members
}
null
onBulkAssignSuccess: =>
@render() unless @destroyed
noty text: $.i18n.t('teacher.assigned'), layout: 'center', type: 'information', killer: true, timeout: 5000
onClickSelectAll: (e) ->
e.preventDefault()
checkboxes = @$('.student-checkbox input')
@ -278,11 +394,8 @@ module.exports = class TeacherClassView extends RootView
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: ->
calculateClassStats: ->
return {} unless @classroom.sessions?.loaded and @students.loaded
stats = {}
playtime = 0
@ -301,4 +414,5 @@ module.exports = class TeacherClassView extends RootView
enrolledUsers = @students.filter (user) -> user.get('coursePrepaidID')
stats.enrolledUsers = _.size(enrolledUsers)
return stats

View file

@ -44,7 +44,7 @@ module.exports = class TeacherClassesView extends RootView
@courseInstances = new CourseInstances()
@courseInstances.fetchByOwner(me.id)
@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

View file

@ -85,7 +85,7 @@ module.exports =
members = classroom.get('members') or []
members = members.slice(memberSkip, memberSkip + memberLimit)
dbqs = []
select = 'state.complete level creator playtime'
select = 'state.complete level creator playtime changed dateFirstCompleted'
for member in members
dbqs.push(LevelSession.find({creator: member.toHexString()}).select(select).exec())
results = yield dbqs

View file

@ -45,7 +45,8 @@ LevelSessionSchema.post 'init', (doc) ->
LevelSessionSchema.pre 'save', (next) ->
User = require './User' # Avoid mutual inclusion cycles
Level = require './Level'
@set('changed', new Date())
now = new Date()
@set('changed', now)
id = @get('id')
initd = @previousStateInfo?
@ -55,6 +56,7 @@ LevelSessionSchema.pre 'save', (next) ->
# Newly completed level
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) ->
log.error err if err?
update = $inc: {'stats.gamesCompleted': 1}