mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-18 11:32:25 -05:00
ed8e36f5cd
Closes #3909
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 + "/students?_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'))
|