2015-11-30 14:14:27 -05:00
|
|
|
Campaign = require 'models/Campaign'
|
|
|
|
CocoCollection = require 'collections/CocoCollection'
|
|
|
|
Course = require 'models/Course'
|
|
|
|
CourseInstance = require 'models/CourseInstance'
|
|
|
|
Classroom = require 'models/Classroom'
|
|
|
|
LevelSession = require 'models/LevelSession'
|
2015-12-21 18:45:14 -05:00
|
|
|
Prepaids = require 'collections/Prepaids'
|
2015-11-30 14:14:27 -05:00
|
|
|
RootView = require 'views/core/RootView'
|
|
|
|
template = require 'templates/courses/classroom-view'
|
|
|
|
User = require 'models/User'
|
|
|
|
utils = require 'core/utils'
|
|
|
|
Prepaid = require 'models/Prepaid'
|
|
|
|
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
|
2015-11-30 16:59:22 -05:00
|
|
|
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
|
2015-12-01 16:17:21 -05:00
|
|
|
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
|
2015-12-02 14:56:38 -05:00
|
|
|
RemoveStudentModal = require 'views/courses/RemoveStudentModal'
|
2015-12-03 15:07:12 -05:00
|
|
|
popoverTemplate = require 'templates/courses/classroom-level-popover'
|
2015-11-30 14:14:27 -05:00
|
|
|
|
|
|
|
module.exports = class ClassroomView extends RootView
|
|
|
|
id: 'classroom-view'
|
|
|
|
template: template
|
2015-12-03 15:07:12 -05:00
|
|
|
teacherMode: false
|
2015-12-04 14:59:51 -05:00
|
|
|
|
2015-11-30 14:14:27 -05:00
|
|
|
events:
|
|
|
|
'click #edit-class-details-link': 'onClickEditClassDetailsLink'
|
2015-11-30 16:59:22 -05:00
|
|
|
'click #activate-licenses-btn': 'onClickActivateLicensesButton'
|
|
|
|
'click .activate-single-license-btn': 'onClickActivateSingleLicenseButton'
|
2015-12-01 16:17:21 -05:00
|
|
|
'click #add-students-btn': 'onClickAddStudentsButton'
|
2015-12-01 16:27:12 -05:00
|
|
|
'click .enable-btn': 'onClickEnableButton'
|
2015-12-02 14:56:38 -05:00
|
|
|
'click .remove-student-link': 'onClickRemoveStudentLink'
|
2015-11-30 14:14:27 -05:00
|
|
|
|
|
|
|
initialize: (options, classroomID) ->
|
2015-12-04 19:37:13 -05:00
|
|
|
return if me.isAnonymous()
|
2015-11-30 14:14:27 -05:00
|
|
|
@classroom = new Classroom({_id: classroomID})
|
2016-01-25 19:52:14 -05:00
|
|
|
@supermodel.loadModel @classroom
|
2015-11-30 14:14:27 -05:00
|
|
|
@courses = new CocoCollection([], { url: "/db/course", model: Course})
|
|
|
|
@courses.comparator = '_id'
|
2016-01-25 19:52:14 -05:00
|
|
|
@supermodel.loadCollection(@courses)
|
2015-11-30 14:14:27 -05:00
|
|
|
@campaigns = new CocoCollection([], { url: "/db/campaign", model: Campaign })
|
|
|
|
@courses.comparator = '_id'
|
2016-01-25 19:52:14 -05:00
|
|
|
@supermodel.loadCollection(@campaigns, { data: { type: 'course' }})
|
2015-11-30 14:14:27 -05:00
|
|
|
@courseInstances = new CocoCollection([], { url: "/db/course_instance", model: CourseInstance})
|
|
|
|
@courseInstances.comparator = 'courseID'
|
2016-01-25 19:52:14 -05:00
|
|
|
@supermodel.loadCollection(@courseInstances, { data: { classroomID: classroomID } })
|
2015-12-21 18:45:14 -05:00
|
|
|
@prepaids = new Prepaids()
|
|
|
|
@prepaids.comparator = '_id'
|
|
|
|
@prepaids.fetchByCreator(me.id)
|
2016-01-25 19:52:14 -05:00
|
|
|
@supermodel.loadCollection(@prepaids)
|
2015-11-30 14:14:27 -05:00
|
|
|
@users = new CocoCollection([], { url: "/db/classroom/#{classroomID}/members", model: User })
|
2015-11-30 16:59:22 -05:00
|
|
|
@users.comparator = (user) => user.broadName().toLowerCase()
|
2016-01-25 19:52:14 -05:00
|
|
|
@supermodel.loadCollection(@users)
|
2015-11-30 14:14:27 -05:00
|
|
|
@listenToOnce @courseInstances, 'sync', @onCourseInstancesSync
|
2015-12-03 17:51:38 -05:00
|
|
|
@sessions = new CocoCollection([], { model: LevelSession })
|
2015-11-30 14:14:27 -05:00
|
|
|
|
|
|
|
onCourseInstancesSync: ->
|
2016-02-05 17:03:43 -05:00
|
|
|
# clear duplicates with lodash magic
|
|
|
|
groups = _.groupBy @courseInstances.models, (ci) -> ci.get('courseID')
|
|
|
|
@courseInstances.reset(_.map(_.values(groups), _.first))
|
|
|
|
# TODO: Make having multiple course instances for a classroom/course pair impossible in the db
|
|
|
|
|
2015-11-30 14:14:27 -05:00
|
|
|
@sessions = new CocoCollection([], { model: LevelSession })
|
|
|
|
for courseInstance in @courseInstances.models
|
|
|
|
sessions = new CocoCollection([], { url: "/db/course_instance/#{courseInstance.id}/level_sessions", model: LevelSession })
|
2016-01-25 19:52:14 -05:00
|
|
|
@supermodel.loadCollection(sessions, { data: { project: ['level', 'playtime', 'creator', 'changed', 'state.complete'].join(' ') } })
|
2015-11-30 14:14:27 -05:00
|
|
|
courseInstance.sessions = sessions
|
|
|
|
sessions.courseInstance = courseInstance
|
2015-12-02 19:07:46 -05:00
|
|
|
courseInstance.sessionsByUser = {}
|
2015-11-30 14:14:27 -05:00
|
|
|
@listenToOnce sessions, 'sync', (sessions) ->
|
|
|
|
@sessions.add(sessions.slice())
|
2015-12-07 17:15:53 -05:00
|
|
|
for courseInstance in @courseInstances.models
|
|
|
|
courseInstance.sessionsByUser = courseInstance.sessions.groupBy('creator')
|
2015-12-04 14:59:51 -05:00
|
|
|
|
2015-12-07 17:15:53 -05:00
|
|
|
# Generate course instance JIT, in the meantime have models w/out equivalents in the db
|
2015-12-02 19:07:46 -05:00
|
|
|
for course in @courses.models
|
|
|
|
query = {courseID: course.id, classroomID: @classroom.id}
|
|
|
|
courseInstance = @courseInstances.findWhere(query)
|
|
|
|
if not courseInstance
|
|
|
|
courseInstance = new CourseInstance(query)
|
|
|
|
@courseInstances.add(courseInstance)
|
|
|
|
courseInstance.sessions = new CocoCollection([], {model: LevelSession})
|
|
|
|
sessions.courseInstance = courseInstance
|
|
|
|
courseInstance.sessionsByUser = {}
|
|
|
|
|
2015-11-30 14:14:27 -05:00
|
|
|
onLoaded: ->
|
2015-12-03 16:16:57 -05:00
|
|
|
@teacherMode = me.isAdmin() or @classroom.get('ownerID') is me.id
|
2015-11-30 14:14:27 -05:00
|
|
|
userSessions = @sessions.groupBy('creator')
|
2016-01-22 14:32:39 -05:00
|
|
|
@users.remove(@users.where({ deleted: true }))
|
2015-11-30 14:14:27 -05:00
|
|
|
for user in @users.models
|
|
|
|
user.sessions = new CocoCollection(userSessions[user.id], { model: LevelSession })
|
|
|
|
user.sessions.comparator = 'changed'
|
|
|
|
user.sessions.sort()
|
|
|
|
for courseInstance in @courseInstances.models
|
|
|
|
courseID = courseInstance.get('courseID')
|
|
|
|
course = @courses.get(courseID)
|
|
|
|
campaignID = course.get('campaignID')
|
|
|
|
campaign = @campaigns.get(campaignID)
|
|
|
|
courseInstance.sessions.campaign = campaign
|
|
|
|
super()
|
2015-12-04 14:59:51 -05:00
|
|
|
|
2015-12-03 15:07:12 -05:00
|
|
|
afterRender: ->
|
|
|
|
@$('[data-toggle="popover"]').popover({
|
|
|
|
html: true
|
|
|
|
trigger: 'hover'
|
|
|
|
placement: 'top'
|
|
|
|
})
|
|
|
|
super()
|
2015-11-30 14:14:27 -05:00
|
|
|
|
2015-11-30 16:59:22 -05:00
|
|
|
onClickActivateLicensesButton: ->
|
|
|
|
modal = new ActivateLicensesModal({
|
|
|
|
classroom: @classroom
|
|
|
|
users: @users
|
|
|
|
})
|
|
|
|
@openModalView(modal)
|
|
|
|
modal.once 'redeem-users', -> document.location.reload()
|
2015-12-04 17:11:17 -05:00
|
|
|
application.tracker?.trackEvent 'Classroom started enroll students', category: 'Courses'
|
2015-11-30 16:59:22 -05:00
|
|
|
|
|
|
|
onClickActivateSingleLicenseButton: (e) ->
|
2015-12-09 09:57:33 -05:00
|
|
|
userID = $(e.target).closest('.btn').data('user-id')
|
2015-12-21 18:45:14 -05:00
|
|
|
if @prepaids.totalMaxRedeemers() - @prepaids.totalRedeemers() > 0
|
|
|
|
# Have an unused enrollment, enroll student immediately instead of opening the enroll modal
|
|
|
|
prepaid = @prepaids.find((prepaid) -> prepaid.get('properties')?.endDate? and prepaid.openSpots())
|
|
|
|
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots()) unless prepaid
|
|
|
|
$.ajax({
|
|
|
|
method: 'POST'
|
|
|
|
url: _.result(prepaid, 'url') + '/redeemers'
|
|
|
|
data: { userID: userID }
|
|
|
|
success: =>
|
|
|
|
application.tracker?.trackEvent 'Classroom finished enroll student', category: 'Courses', userID: userID
|
|
|
|
# TODO: do a lighter refresh here. @render() did not work out.
|
|
|
|
document.location.reload()
|
|
|
|
error: (jqxhr, textStatus, errorThrown) ->
|
|
|
|
if jqxhr.status is 402
|
|
|
|
message = arguments[2]
|
|
|
|
else
|
|
|
|
message = "#{jqxhr.status}: #{jqxhr.responseText}"
|
|
|
|
console.err message
|
|
|
|
})
|
|
|
|
else
|
|
|
|
user = @users.get(userID)
|
|
|
|
modal = new ActivateLicensesModal({
|
|
|
|
classroom: @classroom
|
|
|
|
users: @users
|
|
|
|
user: user
|
|
|
|
})
|
|
|
|
@openModalView(modal)
|
|
|
|
modal.once 'redeem-users', -> document.location.reload()
|
|
|
|
application.tracker?.trackEvent 'Classroom started enroll student', category: 'Courses', userID: userID
|
2015-11-30 16:59:22 -05:00
|
|
|
|
2015-11-30 14:14:27 -05:00
|
|
|
onClickEditClassDetailsLink: ->
|
|
|
|
modal = new ClassroomSettingsModal({classroom: @classroom})
|
|
|
|
@openModalView(modal)
|
|
|
|
@listenToOnce modal, 'hidden', @render
|
|
|
|
|
2015-12-03 17:51:38 -05:00
|
|
|
userLastPlayedString: (user) ->
|
2015-12-07 17:15:53 -05:00
|
|
|
return '' unless user.sessions?
|
2015-11-30 14:14:27 -05:00
|
|
|
session = user.sessions.last()
|
2015-12-07 17:15:53 -05:00
|
|
|
return '' unless session
|
2015-11-30 14:14:27 -05:00
|
|
|
campaign = session.collection.campaign
|
|
|
|
levelOriginal = session.get('level').original
|
|
|
|
campaignLevel = campaign.get('levels')[levelOriginal]
|
|
|
|
return "#{campaign.get('fullName')}, #{campaignLevel.name}"
|
2015-12-01 16:17:21 -05:00
|
|
|
|
2015-12-03 17:51:38 -05:00
|
|
|
userPlaytimeString: (user) ->
|
2015-12-07 17:15:53 -05:00
|
|
|
return '' unless user.sessions?
|
2015-12-03 15:29:22 -05:00
|
|
|
playtime = _.reduce user.sessions.pluck('playtime'), (s1, s2) -> (s1 or 0) + (s2 or 0)
|
|
|
|
return '' unless playtime
|
|
|
|
return moment.duration(playtime, 'seconds').humanize()
|
|
|
|
|
2015-12-03 17:51:38 -05:00
|
|
|
classStats: ->
|
|
|
|
stats = {}
|
2015-12-04 14:59:51 -05:00
|
|
|
|
2015-12-03 17:51:38 -05:00
|
|
|
playtime = 0
|
|
|
|
total = 0
|
|
|
|
for session in @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
|
2015-12-04 14:59:51 -05:00
|
|
|
|
|
|
|
completeSessions = @sessions.filter (s) -> s.get('state')?.complete
|
2015-12-09 09:57:33 -05:00
|
|
|
stats.averageLevelsComplete = if @users.size() then (_.size(completeSessions) / @users.size()).toFixed(1) else 'N/A' # '
|
2015-12-03 17:51:38 -05:00
|
|
|
stats.totalLevelsComplete = _.size(completeSessions)
|
2015-12-04 17:40:22 -05:00
|
|
|
|
|
|
|
enrolledUsers = @users.filter (user) -> user.get('coursePrepaidID')
|
|
|
|
stats.enrolledUsers = _.size(enrolledUsers)
|
2015-12-03 17:51:38 -05:00
|
|
|
return stats
|
|
|
|
|
2015-12-01 16:17:21 -05:00
|
|
|
onClickAddStudentsButton: (e) ->
|
|
|
|
modal = new InviteToClassroomModal({classroom: @classroom})
|
2015-12-01 16:27:12 -05:00
|
|
|
@openModalView(modal)
|
2015-12-04 17:11:17 -05:00
|
|
|
application.tracker?.trackEvent 'Classroom started add students', category: 'Courses', classroomID: @classroom.id
|
2015-12-01 16:27:12 -05:00
|
|
|
|
|
|
|
onClickEnableButton: (e) ->
|
2015-12-09 09:57:33 -05:00
|
|
|
$button = $(e.target).closest('.btn')
|
|
|
|
courseInstance = @courseInstances.get($button.data('course-instance-cid'))
|
|
|
|
console.log 'looking for course instance', courseInstance, 'for', $button.data('course-instance-cid'), 'out of', @courseInstances
|
|
|
|
userID = $button.data('user-id')
|
|
|
|
$button.attr('disabled', true)
|
2015-12-04 17:11:17 -05:00
|
|
|
application.tracker?.trackEvent 'Course assign student', category: 'Courses', courseInstanceID: courseInstance.id, userID: userID
|
2015-12-02 19:07:46 -05:00
|
|
|
|
|
|
|
onCourseInstanceCreated = =>
|
|
|
|
courseInstance.addMember(userID)
|
|
|
|
@listenToOnce courseInstance, 'sync', @render
|
2015-12-04 14:59:51 -05:00
|
|
|
|
2015-12-02 19:07:46 -05:00
|
|
|
if courseInstance.isNew()
|
|
|
|
# adding the first student to this course, so generate the course instance for it
|
|
|
|
courseInstance.save(null, {validate: false})
|
|
|
|
courseInstance.once 'sync', onCourseInstanceCreated
|
|
|
|
else
|
|
|
|
onCourseInstanceCreated()
|
2015-12-02 14:56:38 -05:00
|
|
|
|
2015-12-06 18:00:00 -05:00
|
|
|
# TODO: update newly visible level progress bar (currently all white)
|
|
|
|
|
2015-12-02 14:56:38 -05:00
|
|
|
onClickRemoveStudentLink: (e) ->
|
|
|
|
user = @users.get($(e.target).closest('a').data('user-id'))
|
|
|
|
modal = new RemoveStudentModal({
|
|
|
|
classroom: @classroom
|
|
|
|
user: user
|
|
|
|
courseInstances: @courseInstances
|
|
|
|
})
|
|
|
|
@openModalView(modal)
|
|
|
|
modal.once 'remove-student', @onStudentRemoved, @
|
2015-12-04 14:59:51 -05:00
|
|
|
|
2015-12-02 14:56:38 -05:00
|
|
|
onStudentRemoved: (e) ->
|
|
|
|
@users.remove(e.user)
|
2015-12-03 15:07:12 -05:00
|
|
|
@render()
|
2015-12-06 23:01:52 -05:00
|
|
|
application.tracker?.trackEvent 'Classroom removed student', category: 'Courses', classroomID: @classroom.id, userID: e.user.id
|
2015-12-03 15:07:12 -05:00
|
|
|
|
|
|
|
levelPopoverContent: (level, session, i) ->
|
2015-12-03 16:32:22 -05:00
|
|
|
return null unless level
|
2015-12-03 15:07:12 -05:00
|
|
|
context = {
|
|
|
|
moment: moment
|
|
|
|
level: level
|
|
|
|
session: session
|
|
|
|
i: i
|
2015-12-03 16:16:57 -05:00
|
|
|
canViewSolution: @teacherMode
|
2015-12-03 15:07:12 -05:00
|
|
|
}
|
|
|
|
return popoverTemplate(context)
|
|
|
|
|
|
|
|
getLevelURL: (level, course, courseInstance, session) ->
|
2015-12-03 16:16:57 -05:00
|
|
|
return null unless @teacherMode and _.all(arguments)
|
2015-12-04 14:59:51 -05:00
|
|
|
"/play/level/#{level.slug}?course=#{course.id}&course-instance=#{courseInstance.id}&session=#{session.id}&observing=true"
|