mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-11 08:11:21 -05:00
ef0547f72a
In TeacherClassView, when a teacher assigns a paid course to any unenrolled student, the view automatically enrolls those students, rather than requiring the teacher to enroll those students manually first. Update copy throughout. Also add back (smaller) padding to progress dots in TeacherClassView.
532 lines
22 KiB
CoffeeScript
532 lines
22 KiB
CoffeeScript
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'
|
|
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
|
|
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
|
|
EditStudentModal = require 'views/teachers/EditStudentModal'
|
|
RemoveStudentModal = require 'views/courses/RemoveStudentModal'
|
|
CoursesNotAssignedModal = require './CoursesNotAssignedModal'
|
|
|
|
Campaigns = require 'collections/Campaigns'
|
|
Classroom = require 'models/Classroom'
|
|
Classrooms = require 'collections/Classrooms'
|
|
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'
|
|
Prepaids = require 'collections/Prepaids'
|
|
|
|
module.exports = class TeacherClassView extends RootView
|
|
id: 'teacher-class-view'
|
|
template: template
|
|
helper: helper
|
|
|
|
events:
|
|
'click .nav-tabs a': 'onClickNavTabLink'
|
|
'click .unarchive-btn': 'onClickUnarchive'
|
|
'click .edit-classroom': 'onClickEditClassroom'
|
|
'click .add-students-btn': 'onClickAddStudents'
|
|
'click .edit-student-link': 'onClickEditStudentLink'
|
|
'click .sort-button': 'onClickSortButton'
|
|
'click #copy-url-btn': 'onClickCopyURLButton'
|
|
'click #copy-code-btn': 'onClickCopyCodeButton'
|
|
'click .remove-student-link': 'onClickRemoveStudentLink'
|
|
'click .assign-student-button': 'onClickAssignStudentButton'
|
|
'click .enroll-student-button': 'onClickEnrollStudentButton'
|
|
'click .assign-to-selected-students': 'onClickBulkAssign'
|
|
'click .export-student-progress-btn': 'onClickExportStudentProgress'
|
|
'click .select-all': 'onClickSelectAll'
|
|
'click .student-checkbox': 'onClickStudentCheckbox'
|
|
'keyup #student-search': 'onKeyPressStudentSearch'
|
|
'change .course-select, .bulk-course-select': 'onChangeCourseSelect'
|
|
|
|
getInitialState: ->
|
|
{
|
|
sortAttribute: 'name'
|
|
sortDirection: 1
|
|
activeTab: '#' + (Backbone.history.getHash() or 'students-tab')
|
|
students: new Users()
|
|
classCode: ""
|
|
joinURL: ""
|
|
errors:
|
|
assigningToNobody: false
|
|
selectedCourse: undefined
|
|
checkboxStates: {}
|
|
classStats:
|
|
averagePlaytime: ""
|
|
totalPlaytime: ""
|
|
averageLevelsComplete: ""
|
|
totalLevelsComplete: ""
|
|
enrolledUsers: ""
|
|
}
|
|
|
|
getTitle: -> return @classroom?.get('name')
|
|
|
|
initialize: (options, classroomID) ->
|
|
super(options)
|
|
@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'
|
|
@urls = require('core/urls')
|
|
|
|
@debouncedRender = _.debounce @render
|
|
|
|
@state = new State(@getInitialState())
|
|
@updateHash @state.get('activeTab') # TODO: Don't push to URL history (maybe don't use url fragment for default tab)
|
|
|
|
@classroom = new Classroom({ _id: classroomID })
|
|
@supermodel.trackRequest @classroom.fetch()
|
|
@onKeyPressStudentSearch = _.debounce(@onKeyPressStudentSearch, 200)
|
|
@sortedCourses = []
|
|
|
|
@prepaids = new Prepaids()
|
|
@prepaids.comparator = 'endDate' # use prepaids in order of expiration
|
|
@supermodel.trackRequest @prepaids.fetchByCreator(me.id)
|
|
|
|
@students = new Users()
|
|
@listenTo @classroom, 'sync', ->
|
|
jqxhrs = @students.fetchForClassroom(@classroom, removeDeleted: true)
|
|
@supermodel.trackRequests jqxhrs
|
|
|
|
@classroom.sessions = new LevelSessions()
|
|
requests = @classroom.sessions.fetchForAllClassroomMembers(@classroom)
|
|
@supermodel.trackRequests(requests)
|
|
|
|
@students.comparator = (student1, student2) =>
|
|
dir = @state.get('sortDirection')
|
|
value = @state.get('sortValue')
|
|
if value is 'name'
|
|
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
|
|
|
|
if value is 'progress'
|
|
# TODO: I would like for this to be in the Level model,
|
|
# but it doesn't know about its own courseNumber.
|
|
level1 = student1.latestCompleteLevel
|
|
level2 = student2.latestCompleteLevel
|
|
return -dir if not level1
|
|
return dir if not level2
|
|
return dir * (level1.courseNumber - level2.courseNumber or level1.levelNumber - level2.levelNumber)
|
|
|
|
if value is 'status'
|
|
statusMap = { expired: 0, 'not-enrolled': 1, enrolled: 2 }
|
|
diff = statusMap[student1.prepaidStatus()] - statusMap[student2.prepaidStatus()]
|
|
return dir * diff if diff
|
|
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
|
|
|
|
@courses = new Courses()
|
|
@supermodel.trackRequest @courses.fetch()
|
|
|
|
@courseInstances = new CourseInstances()
|
|
@supermodel.trackRequest @courseInstances.fetchForClassroom(classroomID)
|
|
|
|
@levels = new Levels()
|
|
@supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts,primerLanguage,practice,shareable,i18n'}})
|
|
|
|
@attachMediatorEvents()
|
|
window.tracker?.trackEvent 'Teachers Class Loaded', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
|
|
|
|
attachMediatorEvents: () ->
|
|
# Model/Collection events
|
|
@listenTo @classroom, 'sync change update', ->
|
|
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()
|
|
@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?
|
|
@calculateProgressAndLevels()
|
|
classStats = @calculateClassStats()
|
|
@state.set classStats: classStats if classStats
|
|
@state.set students: @students
|
|
checkboxStates = {}
|
|
for student in @students.models
|
|
checkboxStates[student.id] = @state.get('checkboxStates')[student.id] or false
|
|
@state.set { checkboxStates }
|
|
@listenTo @students, 'sort', ->
|
|
@state.set students: @students
|
|
@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: ->
|
|
@sortedCourses = @classroom.getSortedCourses()
|
|
@removeDeletedStudents() # TODO: Move this to mediator listeners? For both classroom and students?
|
|
@calculateProgressAndLevels()
|
|
|
|
# render callback setup
|
|
@listenTo @courseInstances, 'sync change update', @debouncedRender
|
|
@listenTo @state, 'sync change', ->
|
|
if _.isEmpty(_.omit(@state.changed, 'searchTerm'))
|
|
@renderSelectors('#license-status-table')
|
|
else
|
|
@debouncedRender()
|
|
@listenTo @students, 'sort', @debouncedRender
|
|
super()
|
|
|
|
afterRender: ->
|
|
super(arguments...)
|
|
$('.progress-dot, .btn-view-project-level').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)
|
|
|
|
@state.set {
|
|
earliestIncompleteLevel
|
|
latestCompleteLevel
|
|
progressData
|
|
classStats: @calculateClassStats()
|
|
}
|
|
|
|
onClickNavTabLink: (e) ->
|
|
e.preventDefault()
|
|
hash = $(e.target).closest('a').attr('href')
|
|
@updateHash(hash)
|
|
@state.set activeTab: hash
|
|
|
|
updateHash: (hash) ->
|
|
return if application.testing
|
|
window.location.hash = hash
|
|
|
|
onClickCopyCodeButton: ->
|
|
window.tracker?.trackEvent 'Teachers Class Copy Class Code', category: 'Teachers', classroomID: @classroom.id, classCode: @state.get('classCode'), ['Mixpanel']
|
|
@$('#join-code-input').val(@state.get('classCode')).select()
|
|
@tryCopy()
|
|
|
|
onClickCopyURLButton: ->
|
|
window.tracker?.trackEvent 'Teachers Class Copy Class URL', category: 'Teachers', classroomID: @classroom.id, url: @state.get('joinURL'), ['Mixpanel']
|
|
@$('#join-url-input').val(@state.get('joinURL')).select()
|
|
@tryCopy()
|
|
|
|
onClickUnarchive: ->
|
|
window.tracker?.trackEvent 'Teachers Class Unarchive', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
|
|
@classroom.save { archived: false }
|
|
|
|
onClickEditClassroom: (e) ->
|
|
window.tracker?.trackEvent 'Teachers Class Edit Class Started', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
|
|
classroom = @classroom
|
|
modal = new ClassroomSettingsModal({ classroom: classroom })
|
|
@openModalView(modal)
|
|
@listenToOnce modal, 'hide', @render
|
|
|
|
onClickEditStudentLink: (e) ->
|
|
window.tracker?.trackEvent 'Teachers Class Students Edit', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
|
|
user = @students.get($(e.currentTarget).data('student-id'))
|
|
modal = new EditStudentModal({ user, @classroom })
|
|
@openModalView(modal)
|
|
|
|
onClickRemoveStudentLink: (e) ->
|
|
user = @students.get($(e.currentTarget).data('student-id'))
|
|
modal = new RemoveStudentModal({
|
|
classroom: @classroom
|
|
user: user
|
|
courseInstances: @courseInstances
|
|
})
|
|
@openModalView(modal)
|
|
modal.once 'remove-student', @onStudentRemoved, @
|
|
|
|
onStudentRemoved: (e) ->
|
|
@students.remove(e.user)
|
|
window.tracker?.trackEvent 'Teachers Class Students Removed', category: 'Teachers', classroomID: @classroom.id, userID: e.user.id, ['Mixpanel']
|
|
|
|
onClickAddStudents: (e) =>
|
|
window.tracker?.trackEvent 'Teachers Class Add Students', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
|
|
modal = new InviteToClassroomModal({ classroom: @classroom })
|
|
@openModalView(modal)
|
|
@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
|
|
|
|
onClickSortButton: (e) ->
|
|
value = $(e.target).val()
|
|
if value is @state.get('sortValue')
|
|
@state.set('sortDirection', -@state.get('sortDirection'))
|
|
else
|
|
@state.set({
|
|
sortValue: value
|
|
sortDirection: 1
|
|
})
|
|
@students.sort()
|
|
|
|
onKeyPressStudentSearch: (e) ->
|
|
@state.set('searchTerm', $(e.target).val())
|
|
|
|
onChangeCourseSelect: (e) ->
|
|
@trigger 'course-select:change', { selectedCourse: @courses.get($(e.currentTarget).val()) }
|
|
|
|
getSelectedStudentIDs: ->
|
|
Object.keys(_.pick @state.get('checkboxStates'), (checked) -> checked)
|
|
|
|
ensureInstance: (courseID) ->
|
|
|
|
onClickEnrollStudentButton: (e) ->
|
|
userID = $(e.currentTarget).data('user-id')
|
|
user = @students.get(userID)
|
|
selectedUsers = new Users([user])
|
|
@enrollStudents(selectedUsers)
|
|
window.tracker?.trackEvent $(e.currentTarget).data('event-action'), category: 'Teachers', classroomID: @classroom.id, userID: userID, ['Mixpanel']
|
|
|
|
enrollStudents: (selectedUsers) ->
|
|
modal = new ActivateLicensesModal { @classroom, selectedUsers, users: @students }
|
|
@openModalView(modal)
|
|
modal.once 'redeem-users', (enrolledUsers) =>
|
|
enrolledUsers.each (newUser) =>
|
|
user = @students.get(newUser.id)
|
|
if user
|
|
user.set(newUser.attributes)
|
|
null
|
|
|
|
onClickExportStudentProgress: ->
|
|
# TODO: Does not yield .csv download on Safari, and instead opens a new tab with the .csv contents
|
|
window.tracker?.trackEvent 'Teachers Class Export CSV', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
|
|
courseLabels = ""
|
|
courseOrder = []
|
|
courses = (@courses.get(c._id) for c in @classroom.get('courses'))
|
|
courseLabelsArray = helper.courseLabelsArray courses
|
|
for course, index in courses
|
|
courseLabels += "#{courseLabelsArray[index]} Playtime,"
|
|
courseOrder.push(course.id)
|
|
csvContent = "data:text/csv;charset=utf-8,Username,Email,Total Playtime,#{courseLabels}Concepts\n"
|
|
levelCourseMap = {}
|
|
language = @classroom.get('aceConfig')?.language
|
|
for trimCourse in @classroom.get('courses')
|
|
for trimLevel in trimCourse.levels
|
|
continue if language and trimLevel.primerLanguage is language
|
|
levelCourseMap[trimLevel.original] = @courses.get(trimCourse._id)
|
|
for student in @students.models
|
|
concepts = []
|
|
for trimCourse in @classroom.get('courses')
|
|
course = @courses.get(trimCourse._id)
|
|
instance = @courseInstances.findWhere({ courseID: course.id, classroomID: @classroom.id })
|
|
if instance and instance.hasMember(student)
|
|
for trimLevel in trimCourse.levels
|
|
level = @levels.findWhere({ original: trimLevel.original })
|
|
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(', ')
|
|
coursePlaytimeMap = {}
|
|
playtime = 0
|
|
for session in @classroom.sessions.models when session.get('creator') is student.id
|
|
playtime += session.get('playtime') or 0
|
|
if courseID = levelCourseMap[session.get('level')?.original]?.id
|
|
coursePlaytimeMap[courseID] ?= 0
|
|
coursePlaytimeMap[courseID] += session.get('playtime') or 0
|
|
playtimeString = if playtime is 0 then "0" else moment.duration(playtime, 'seconds').humanize()
|
|
for course in courses
|
|
coursePlaytimeMap[course.id] ?= 0
|
|
coursePlaytimes = []
|
|
for courseID, playtime of coursePlaytimeMap
|
|
coursePlaytimes.push
|
|
courseID: courseID
|
|
playtime: playtime
|
|
coursePlaytimes.sort (a, b) ->
|
|
return -1 if courseOrder.indexOf(a.courseID) < courseOrder.indexOf(b.courseID)
|
|
return 0 if courseOrder.indexOf(a.courseID) is courseOrder.indexOf(b.courseID)
|
|
return 1
|
|
coursePlaytimesString = ""
|
|
for coursePlaytime, index in coursePlaytimes
|
|
if coursePlaytime.playtime is 0
|
|
coursePlaytimesString += "0,"
|
|
else
|
|
coursePlaytimesString += "#{moment.duration(coursePlaytime.playtime, 'seconds').humanize()},"
|
|
csvContent += "#{student.get('name')},#{student.get('email') or ''},#{playtimeString},#{coursePlaytimesString}\"#{conceptsString}\"\n"
|
|
csvContent = csvContent.substring(0, csvContent.length - 1)
|
|
encodedUri = encodeURI(csvContent)
|
|
window.open(encodedUri)
|
|
|
|
onClickAssignStudentButton: (e) ->
|
|
userID = $(e.currentTarget).data('user-id')
|
|
user = @students.get(userID)
|
|
members = [userID]
|
|
courseID = $(e.currentTarget).data('course-id')
|
|
@assignCourse courseID, members
|
|
window.tracker?.trackEvent 'Teachers Class Students Assign Selected', category: 'Teachers', classroomID: @classroom.id, courseID: courseID, userID: userID, ['Mixpanel']
|
|
|
|
onClickBulkAssign: ->
|
|
courseID = @$('.bulk-course-select').val()
|
|
selectedIDs = @getSelectedStudentIDs()
|
|
assigningToNobody = selectedIDs.length is 0
|
|
@state.set errors: { assigningToNobody }
|
|
return if assigningToNobody
|
|
@assignCourse courseID, selectedIDs
|
|
window.tracker?.trackEvent 'Teachers Class Students Assign Selected', category: 'Teachers', classroomID: @classroom.id, courseID: courseID, ['Mixpanel']
|
|
|
|
assignCourse: (courseID, members) ->
|
|
courseInstance = null
|
|
numberEnrolled = 0
|
|
remainingSpots = 0
|
|
|
|
return Promise.resolve()
|
|
.then =>
|
|
courseInstance = @courseInstances.findWhere({ courseID, classroomID: @classroom.id })
|
|
if not courseInstance
|
|
courseInstance = new CourseInstance {
|
|
courseID,
|
|
classroomID: @classroom.id
|
|
ownerID: @classroom.get('ownerID')
|
|
aceConfig: {}
|
|
}
|
|
courseInstance.notyErrors = false # handling manually
|
|
@courseInstances.add(courseInstance)
|
|
return courseInstance.save()
|
|
|
|
.then =>
|
|
availablePrepaids = @prepaids.filter((prepaid) -> prepaid.status() is 'available')
|
|
unenrolledStudents = _(members)
|
|
.map((userID) => @students.get(userID))
|
|
.filter((user) => user.prepaidStatus() isnt 'enrolled')
|
|
.value()
|
|
totalSpotsAvailable = _.reduce(prepaid.openSpots() for prepaid in availablePrepaids, (val, total) -> val + total) or 0
|
|
if totalSpotsAvailable < _.size(unenrolledStudents)
|
|
modal = new CoursesNotAssignedModal({
|
|
selected: members.length
|
|
totalSpotsAvailable
|
|
unenrolledStudents: _.size(unenrolledStudents)
|
|
})
|
|
@openModalView(modal)
|
|
error = new Error('Not enough licenses available')
|
|
error.handled = true
|
|
throw error
|
|
|
|
numberEnrolled = _.size(unenrolledStudents)
|
|
remainingSpots = totalSpotsAvailable - numberEnrolled
|
|
|
|
requests = []
|
|
for prepaid in availablePrepaids
|
|
for i in _.range(prepaid.openSpots())
|
|
break unless _.size(unenrolledStudents) > 0
|
|
user = unenrolledStudents.shift()
|
|
requests.push(prepaid.redeem(user))
|
|
|
|
@trigger 'begin-redeem-for-assign-course'
|
|
return $.when(requests...)
|
|
|
|
.then =>
|
|
# refresh prepaids, since the racing multiple parallel redeem requests in the previous `then` probably did not
|
|
# end up returning the final result of all those requests together.
|
|
@prepaids.fetchByCreator(me.id)
|
|
|
|
@trigger 'begin-assign-course'
|
|
if members.length
|
|
return courseInstance.addMembers(members)
|
|
|
|
.then =>
|
|
course = @courses.get(courseID)
|
|
lines = [
|
|
$.i18n.t('teacher.assigned_msg_1')
|
|
.replace('{{numberAssigned}}', members.length)
|
|
.replace('{{courseName}}', course.get('name'))
|
|
]
|
|
if numberEnrolled > 0
|
|
lines.push(
|
|
$.i18n.t('teacher.assigned_msg_2')
|
|
.replace('{{numberEnrolled}}', numberEnrolled)
|
|
)
|
|
lines.push(
|
|
$.i18n.t('teacher.assigned_msg_3')
|
|
.replace('{{remainingSpots}}', remainingSpots)
|
|
)
|
|
noty text: lines.join('<br />'), layout: 'center', type: 'information', killer: true, timeout: 5000
|
|
|
|
.catch (e) =>
|
|
# TODO: Use this handling for errors site-wide?
|
|
return if e.handled
|
|
throw e if e instanceof Error and application.testing
|
|
text = if e instanceof Error then 'Runtime error' else e.responseJSON?.message or e.message or $.i18n.t('loading_error.unknown')
|
|
noty { text, layout: 'center', type: 'error', killer: true, timeout: 5000 }
|
|
|
|
onClickSelectAll: (e) ->
|
|
e.preventDefault()
|
|
checkboxStates = _.clone @state.get('checkboxStates')
|
|
if _.all(checkboxStates)
|
|
for studentID of checkboxStates
|
|
checkboxStates[studentID] = false
|
|
else
|
|
for studentID of checkboxStates
|
|
checkboxStates[studentID] = true
|
|
@state.set { checkboxStates }
|
|
|
|
onClickStudentCheckbox: (e) ->
|
|
e.preventDefault()
|
|
checkbox = $(e.currentTarget).find('input')
|
|
studentID = checkbox.data('student-id')
|
|
checkboxStates = _.clone @state.get('checkboxStates')
|
|
checkboxStates[studentID] = not checkboxStates[studentID]
|
|
@state.set { checkboxStates }
|
|
|
|
calculateClassStats: ->
|
|
return {} unless @classroom.sessions?.loaded and @students.loaded
|
|
stats = {}
|
|
|
|
playtime = 0
|
|
total = 0
|
|
for session in @classroom.sessions.models
|
|
pt = session.get('playtime') or 0
|
|
playtime += pt
|
|
total += 1
|
|
stats.averagePlaytime = if playtime and total then moment.duration(playtime / total, "seconds").humanize() else 0
|
|
stats.totalPlaytime = if playtime then moment.duration(playtime, "seconds").humanize() else 0
|
|
# TODO: Humanize differently ('1 hour' instead of 'an hour')
|
|
|
|
levelIncludeMap = {}
|
|
language = @classroom.get('aceConfig')?.language
|
|
for level in @levels.models
|
|
levelIncludeMap[level.get('original')] = not level.get('practice') and (not language? or level.get('primerLanguage') isnt language)
|
|
completeSessions = @classroom.sessions.filter (s) -> s.get('state')?.complete and levelIncludeMap[s.get('level')?.original]
|
|
stats.averageLevelsComplete = if @students.size() then (_.size(completeSessions) / @students.size()).toFixed(1) else 'N/A' # '
|
|
stats.totalLevelsComplete = _.size(completeSessions)
|
|
|
|
enrolledUsers = @students.filter (user) -> user.isEnrolled()
|
|
stats.enrolledUsers = _.size(enrolledUsers)
|
|
|
|
return stats
|
|
|
|
studentStatusString: (student) ->
|
|
status = student.prepaidStatus()
|
|
expires = student.get('coursePrepaid')?.endDate
|
|
string = switch status
|
|
when 'not-enrolled' then $.i18n.t('teacher.status_not_enrolled')
|
|
when 'enrolled' then (if expires then $.i18n.t('teacher.status_enrolled') else '-')
|
|
when 'expired' then $.i18n.t('teacher.status_expired')
|
|
return string.replace('{{date}}', moment(expires).utc().format('l'))
|