2015-09-13 01:01:59 -04:00
|
|
|
Campaign = require 'models/Campaign'
|
2015-09-03 14:04:40 -04:00
|
|
|
CocoCollection = require 'collections/CocoCollection'
|
|
|
|
Course = require 'models/Course'
|
|
|
|
CourseInstance = require 'models/CourseInstance'
|
2015-09-13 01:01:59 -04:00
|
|
|
LevelSession = require 'models/LevelSession'
|
|
|
|
RootView = require 'views/core/RootView'
|
|
|
|
template = require 'templates/courses/course-details'
|
|
|
|
User = require 'models/User'
|
|
|
|
utils = require 'core/utils'
|
2015-10-05 16:34:37 -04:00
|
|
|
Prepaid = require 'models/Prepaid'
|
2015-09-03 14:04:40 -04:00
|
|
|
|
2015-10-13 11:11:55 -04:00
|
|
|
autoplayedOnce = false
|
|
|
|
|
2015-09-03 14:04:40 -04:00
|
|
|
module.exports = class CourseDetailsView extends RootView
|
|
|
|
id: 'course-details-view'
|
|
|
|
template: template
|
|
|
|
|
2015-09-13 01:01:59 -04:00
|
|
|
events:
|
|
|
|
'change .progress-expand-checkbox': 'onCheckExpandedProgress'
|
|
|
|
'click .btn-play-level': 'onClickPlayLevel'
|
|
|
|
'click .btn-save-settings': 'onClickSaveSettings'
|
2015-09-23 19:27:45 -04:00
|
|
|
'click .btn-select-instance': 'onClickSelectInstance'
|
2015-09-13 01:01:59 -04:00
|
|
|
'click .progress-member-header': 'onClickMemberHeader'
|
|
|
|
'click .progress-header': 'onClickProgressHeader'
|
2015-09-24 17:48:54 -04:00
|
|
|
'click .progress-level-cell': 'onClickProgressLevelCell'
|
2015-09-13 01:01:59 -04:00
|
|
|
'mouseenter .progress-level-cell': 'onMouseEnterPoint'
|
|
|
|
'mouseleave .progress-level-cell': 'onMouseLeavePoint'
|
2015-10-05 19:00:47 -04:00
|
|
|
'click #invite-btn': 'onClickInviteButton'
|
2015-09-13 01:01:59 -04:00
|
|
|
|
2015-09-24 20:12:18 -04:00
|
|
|
constructor: (options, @courseID, @courseInstanceID) ->
|
2015-09-03 14:04:40 -04:00
|
|
|
super options
|
2015-09-24 20:52:00 -04:00
|
|
|
@courseID ?= options.courseID
|
|
|
|
@courseInstanceID ?= options.courseInstanceID
|
2015-09-13 01:01:59 -04:00
|
|
|
@adminMode = me.isAdmin()
|
|
|
|
@memberSort = 'nameAsc'
|
2015-09-23 19:27:45 -04:00
|
|
|
@course = @supermodel.getModel(Course, @courseID) or new Course _id: @courseID
|
|
|
|
@listenTo @course, 'sync', @onCourseSync
|
2015-10-05 16:34:37 -04:00
|
|
|
@prepaid = new Prepaid()
|
2015-09-23 19:27:45 -04:00
|
|
|
if @course.loaded
|
|
|
|
@onCourseSync()
|
|
|
|
else
|
|
|
|
@supermodel.loadModel @course, 'course'
|
2015-09-13 01:01:59 -04:00
|
|
|
|
|
|
|
getRenderData: ->
|
|
|
|
context = super()
|
|
|
|
context.adminMode = @adminMode ? false
|
|
|
|
context.campaign = @campaign
|
|
|
|
context.conceptsCompleted = @conceptsCompleted ? {}
|
|
|
|
context.course = @course if @course?.loaded
|
|
|
|
context.courseInstance = @courseInstance if @courseInstance?.loaded
|
2015-09-23 19:27:45 -04:00
|
|
|
context.courseInstances = @courseInstances?.models ? []
|
2015-09-24 10:28:43 -04:00
|
|
|
context.instanceStats = @instanceStats
|
2015-09-13 01:01:59 -04:00
|
|
|
context.levelConceptMap = @levelConceptMap ? {}
|
|
|
|
context.memberSort = @memberSort
|
2015-09-24 10:28:43 -04:00
|
|
|
context.memberStats = @memberStats
|
2015-09-13 01:01:59 -04:00
|
|
|
context.memberUserMap = @memberUserMap ? {}
|
2015-09-23 19:27:45 -04:00
|
|
|
context.noCourseInstance = @noCourseInstance
|
|
|
|
context.noCourseInstanceSelected = @noCourseInstanceSelected
|
2015-10-07 20:08:22 -04:00
|
|
|
context.pricePerSeat = @course.get('pricePerSeat')
|
2015-09-13 01:01:59 -04:00
|
|
|
context.showExpandedProgress = @showExpandedProgress
|
|
|
|
context.sortedMembers = @sortedMembers ? []
|
|
|
|
context.userConceptStateMap = @userConceptStateMap ? {}
|
|
|
|
context.userLevelStateMap = @userLevelStateMap ? {}
|
2015-10-05 16:34:37 -04:00
|
|
|
context.document = document
|
2015-09-13 01:01:59 -04:00
|
|
|
context
|
|
|
|
|
|
|
|
onCourseSync: ->
|
|
|
|
# console.log 'onCourseSync'
|
2015-10-13 11:11:55 -04:00
|
|
|
if me.isAnonymous() and not me.get('hourOfCode')
|
2015-09-23 19:27:45 -04:00
|
|
|
@noCourseInstance = true
|
|
|
|
@render?()
|
|
|
|
return
|
2015-09-13 01:01:59 -04:00
|
|
|
return if @campaign?
|
2015-09-23 19:27:45 -04:00
|
|
|
campaignID = @course.get('campaignID')
|
|
|
|
@campaign = @supermodel.getModel(Campaign, campaignID) or new Campaign _id: campaignID
|
2015-09-13 01:01:59 -04:00
|
|
|
@listenTo @campaign, 'sync', @onCampaignSync
|
2015-09-23 19:27:45 -04:00
|
|
|
if @campaign.loaded
|
|
|
|
@onCampaignSync()
|
|
|
|
else
|
|
|
|
@supermodel.loadModel @campaign, 'campaign'
|
2015-09-13 01:01:59 -04:00
|
|
|
@render?()
|
|
|
|
|
|
|
|
onCampaignSync: ->
|
|
|
|
# console.log 'onCampaignSync'
|
2015-09-03 14:04:40 -04:00
|
|
|
if @courseInstanceID
|
2015-09-13 01:01:59 -04:00
|
|
|
@loadCourseInstance(@courseInstanceID)
|
2015-09-03 14:04:40 -04:00
|
|
|
else if !me.isAnonymous()
|
|
|
|
@courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance})
|
2015-09-13 01:01:59 -04:00
|
|
|
@listenToOnce @courseInstances, 'sync', @onCourseInstancesSync
|
2015-09-03 14:04:40 -04:00
|
|
|
@supermodel.loadCollection(@courseInstances, 'course_instances')
|
2015-09-13 01:01:59 -04:00
|
|
|
@levelConceptMap = {}
|
|
|
|
for levelID, level of @campaign.get('levels')
|
|
|
|
@levelConceptMap[levelID] ?= {}
|
|
|
|
for concept in level.concepts
|
|
|
|
@levelConceptMap[levelID][concept] = true
|
|
|
|
@render?()
|
2015-09-03 14:04:40 -04:00
|
|
|
|
2015-09-13 01:01:59 -04:00
|
|
|
loadCourseInstance: (courseInstanceID) ->
|
|
|
|
# console.log 'loadCourseInstance'
|
|
|
|
return if @courseInstance?
|
2015-09-24 20:12:18 -04:00
|
|
|
@courseInstanceID = courseInstanceID
|
|
|
|
@courseInstance = @supermodel.getModel(CourseInstance, @courseInstanceID) or new CourseInstance _id: @courseInstanceID
|
2015-09-13 01:01:59 -04:00
|
|
|
@listenTo @courseInstance, 'sync', @onCourseInstanceSync
|
2015-09-23 19:27:45 -04:00
|
|
|
if @courseInstance.loaded
|
|
|
|
@onCourseInstanceSync()
|
|
|
|
else
|
|
|
|
@courseInstance = @supermodel.loadModel(@courseInstance, 'course_instance').model
|
2015-09-03 14:04:40 -04:00
|
|
|
|
2015-09-13 01:01:59 -04:00
|
|
|
onCourseInstancesSync: ->
|
|
|
|
# console.log 'onCourseInstancesSync'
|
2015-09-03 14:04:40 -04:00
|
|
|
if @courseInstances.models.length is 1
|
2015-09-13 01:01:59 -04:00
|
|
|
@loadCourseInstance(@courseInstances.models[0].id)
|
2015-09-23 19:27:45 -04:00
|
|
|
else
|
|
|
|
if @courseInstances.models.length is 0
|
|
|
|
@noCourseInstance = true
|
|
|
|
else
|
|
|
|
@noCourseInstanceSelected = true
|
|
|
|
@render?()
|
2015-09-13 01:01:59 -04:00
|
|
|
|
|
|
|
onCourseInstanceSync: ->
|
2015-09-23 19:27:45 -04:00
|
|
|
# console.log 'onCourseInstanceSync'
|
2015-10-13 11:11:55 -04:00
|
|
|
@adminMode = true if @courseInstance.get('ownerID') is me.id and @courseInstance.get('name') isnt 'Single Player'
|
2015-09-13 01:01:59 -04:00
|
|
|
@levelSessions = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/level_sessions", model: LevelSession, comparator:'_id' })
|
|
|
|
@listenToOnce @levelSessions, 'sync', @onLevelSessionsSync
|
|
|
|
@supermodel.loadCollection @levelSessions, 'level_sessions', cache: false
|
|
|
|
@members = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/members", model: User, comparator: 'nameLower' })
|
|
|
|
@listenToOnce @members, 'sync', @onMembersSync
|
|
|
|
@supermodel.loadCollection @members, 'members', cache: false
|
2015-10-05 19:00:47 -04:00
|
|
|
if @adminMode and prepaidID = @courseInstance.get('prepaidID')
|
2015-10-05 16:34:37 -04:00
|
|
|
@prepaid = @supermodel.getModel(Prepaid, prepaidID) or new Prepaid _id: prepaidID
|
|
|
|
@listenTo @prepaid, 'sync', @onPrepaidSync
|
|
|
|
if @prepaid.loaded
|
|
|
|
@onPrepaidSync()
|
|
|
|
else
|
|
|
|
@supermodel.loadModel @prepaid, 'prepaid'
|
|
|
|
@render?()
|
|
|
|
|
|
|
|
onPrepaidSync: ->
|
2015-09-13 01:01:59 -04:00
|
|
|
@render?()
|
|
|
|
|
|
|
|
onLevelSessionsSync: ->
|
|
|
|
# console.log 'onLevelSessionsSync'
|
2015-09-24 10:28:43 -04:00
|
|
|
@instanceStats = averageLevelsCompleted: 0, furthestLevelCompleted: '', totalLevelsCompleted: 0, totalPlayTime: 0
|
|
|
|
@memberStats = {}
|
2015-09-13 01:01:59 -04:00
|
|
|
@userConceptStateMap = {}
|
2015-09-24 17:48:54 -04:00
|
|
|
@userLevelSessionMap = {}
|
2015-09-13 01:01:59 -04:00
|
|
|
@userLevelStateMap = {}
|
2015-09-24 10:28:43 -04:00
|
|
|
levelStateMap = {}
|
2015-09-13 01:01:59 -04:00
|
|
|
for levelSession in @levelSessions.models
|
|
|
|
userID = levelSession.get('creator')
|
|
|
|
levelID = levelSession.get('level').original
|
|
|
|
state = if levelSession.get('state')?.complete then 'complete' else 'started'
|
2015-09-24 10:28:43 -04:00
|
|
|
levelStateMap[levelID] = state
|
|
|
|
|
|
|
|
@instanceStats.totalLevelsCompleted++ if state is 'complete'
|
2015-10-08 09:02:45 -04:00
|
|
|
@instanceStats.totalPlayTime += parseInt(levelSession.get('playtime') ? 0)
|
2015-09-24 10:28:43 -04:00
|
|
|
|
|
|
|
@memberStats[userID] ?= totalLevelsCompleted: 0, totalPlayTime: 0
|
|
|
|
@memberStats[userID].totalLevelsCompleted++ if state is 'complete'
|
2015-10-08 09:02:45 -04:00
|
|
|
@memberStats[userID].totalPlayTime += parseInt(levelSession.get('playtime') ? 0)
|
2015-09-24 10:28:43 -04:00
|
|
|
|
|
|
|
@userConceptStateMap[userID] ?= {}
|
2015-09-13 01:01:59 -04:00
|
|
|
for concept of @levelConceptMap[levelID]
|
|
|
|
@userConceptStateMap[userID][concept] = state
|
2015-09-24 10:28:43 -04:00
|
|
|
|
2015-09-24 17:48:54 -04:00
|
|
|
@userLevelSessionMap[userID] ?= {}
|
|
|
|
@userLevelSessionMap[userID][levelID] = levelSession
|
|
|
|
|
2015-09-24 10:28:43 -04:00
|
|
|
@userLevelStateMap[userID] ?= {}
|
|
|
|
@userLevelStateMap[userID][levelID] = state
|
|
|
|
|
|
|
|
if @courseInstance.get('members').length > 0
|
|
|
|
@instanceStats.averageLevelsCompleted = @instanceStats.totalLevelsCompleted / @courseInstance.get('members').length
|
2015-10-07 20:14:56 -04:00
|
|
|
@instanceStats.averageLevelPlaytime = @instanceStats.totalPlayTime / @courseInstance.get('members').length
|
2015-09-24 10:28:43 -04:00
|
|
|
for levelID, level of @campaign.get('levels')
|
|
|
|
@instanceStats.furthestLevelCompleted = level.name if levelStateMap[levelID] is 'complete'
|
|
|
|
|
2015-09-13 01:01:59 -04:00
|
|
|
@conceptsCompleted = {}
|
|
|
|
for userID, conceptStateMap of @userConceptStateMap
|
|
|
|
for concept, state of conceptStateMap
|
|
|
|
@conceptsCompleted[concept] ?= 0
|
|
|
|
@conceptsCompleted[concept]++
|
|
|
|
@render?()
|
|
|
|
|
2015-10-13 11:11:55 -04:00
|
|
|
# If we just joined a single-player course for Hour of Code, we automatically play.
|
|
|
|
if @instanceStats.totalLevelsCompleted is 0 and @instanceStats.totalPlayTime is 0 and @courseInstance.get('members').length is 1 and me.get('hourOfCode') and not @adminMode and not autoplayedOnce
|
|
|
|
autoplayedOnce = true
|
|
|
|
@$el.find('button.btn-play-level').click()
|
|
|
|
|
2015-09-13 01:01:59 -04:00
|
|
|
onMembersSync: ->
|
|
|
|
# console.log 'onMembersSync'
|
|
|
|
@memberUserMap = {}
|
|
|
|
for user in @members.models
|
|
|
|
@memberUserMap[user.id] = user
|
|
|
|
@sortMembers()
|
|
|
|
@render?()
|
|
|
|
|
|
|
|
onCheckExpandedProgress: (e) ->
|
|
|
|
@showExpandedProgress = $('.progress-expand-checkbox').prop('checked')
|
|
|
|
# TODO: why does render reset the checkbox to be unchecked?
|
|
|
|
@render?()
|
|
|
|
$('.progress-expand-checkbox').attr('checked', @showExpandedProgress)
|
|
|
|
|
|
|
|
onClickMemberHeader: (e) ->
|
|
|
|
@memberSort = if @memberSort is 'nameAsc' then 'nameDesc' else 'nameAsc'
|
|
|
|
@sortMembers()
|
|
|
|
@render?()
|
|
|
|
|
|
|
|
onClickProgressHeader: (e) ->
|
|
|
|
@memberSort = if @memberSort is 'progressAsc' then 'progressDesc' else 'progressAsc'
|
|
|
|
@sortMembers()
|
|
|
|
@render?()
|
|
|
|
|
|
|
|
onClickPlayLevel: (e) ->
|
|
|
|
levelSlug = $(e.target).data('level-slug')
|
|
|
|
Backbone.Mediator.publish 'router:navigate', {
|
|
|
|
route: "/play/level/#{levelSlug}"
|
|
|
|
viewClass: 'views/play/level/PlayLevelView'
|
2015-09-24 20:12:18 -04:00
|
|
|
viewArgs: [{courseID: @courseID, courseInstanceID: @courseInstanceID}, levelSlug]
|
2015-09-13 01:01:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
onClickSaveSettings: (e) ->
|
|
|
|
return unless @courseInstance
|
|
|
|
if name = $('.settings-name-input').val()
|
|
|
|
@courseInstance.set('name', name)
|
|
|
|
description = $('.settings-description-input').val()
|
|
|
|
console.log 'onClickSaveSettings', description
|
|
|
|
@courseInstance.set('description', description)
|
|
|
|
@courseInstance.patch()
|
|
|
|
$('#settingsModal').modal('hide')
|
|
|
|
|
2015-09-23 19:27:45 -04:00
|
|
|
onClickSelectInstance: (e) ->
|
|
|
|
courseInstanceID = $('.select-instance').val()
|
|
|
|
@noCourseInstanceSelected = false
|
|
|
|
@loadCourseInstance(courseInstanceID)
|
|
|
|
|
2015-09-24 17:48:54 -04:00
|
|
|
onClickProgressLevelCell: (e) ->
|
|
|
|
return unless @adminMode
|
|
|
|
levelID = $(e.currentTarget).data('level-id')
|
|
|
|
levelSlug = $(e.currentTarget).data('level-slug')
|
|
|
|
userID = $(e.currentTarget).data('user-id')
|
|
|
|
return unless levelID and levelSlug and userID
|
|
|
|
route = "/play/level/#{levelSlug}"
|
|
|
|
if @userLevelSessionMap[userID]?[levelID]
|
|
|
|
route += "?session=#{@userLevelSessionMap[userID][levelID].id}&observing=true"
|
|
|
|
Backbone.Mediator.publish 'router:navigate', {
|
|
|
|
route: route
|
|
|
|
viewClass: 'views/play/level/PlayLevelView'
|
|
|
|
viewArgs: [{}, levelSlug]
|
|
|
|
}
|
|
|
|
|
2015-10-05 19:00:47 -04:00
|
|
|
onClickInviteButton: (e) ->
|
|
|
|
emails = @$('#invite-emails-textarea').val()
|
|
|
|
emails = emails.split('\n')
|
|
|
|
emails = _.filter((_.string.trim(email) for email in emails))
|
|
|
|
if not emails.length
|
|
|
|
return
|
|
|
|
url = @courseInstance.url() + '/invite_students'
|
|
|
|
@$('#invite-btn, #invite-emails-textarea').addClass('hide')
|
|
|
|
@$('#invite-emails-sending-alert').removeClass('hide')
|
2015-10-05 16:34:37 -04:00
|
|
|
|
2015-10-05 19:00:47 -04:00
|
|
|
$.ajax({
|
|
|
|
url: url
|
|
|
|
data: {emails: emails}
|
|
|
|
method: 'POST'
|
|
|
|
context: @
|
|
|
|
success: ->
|
|
|
|
@$('#invite-emails-sending-alert').addClass('hide')
|
|
|
|
@$('#invite-emails-success-alert').removeClass('hide')
|
|
|
|
})
|
2015-10-05 16:34:37 -04:00
|
|
|
|
2015-09-13 01:01:59 -04:00
|
|
|
onMouseEnterPoint: (e) ->
|
2015-09-24 17:48:54 -04:00
|
|
|
$('.progress-popup-container').hide()
|
|
|
|
container = $(e.target).find('.progress-popup-container').show()
|
2015-09-13 01:01:59 -04:00
|
|
|
margin = 20
|
|
|
|
offset = $(e.target).offset()
|
|
|
|
scrollTop = $('#page-container').scrollTop()
|
|
|
|
height = container.outerHeight()
|
|
|
|
container.css('left', offset.left + e.offsetX)
|
|
|
|
container.css('top', offset.top + scrollTop - height - margin)
|
|
|
|
|
|
|
|
onMouseLeavePoint: (e) ->
|
2015-09-24 17:48:54 -04:00
|
|
|
$(e.target).find('.progress-popup-container').hide()
|
2015-09-13 01:01:59 -04:00
|
|
|
|
|
|
|
sortMembers: ->
|
|
|
|
# Progress sort precedence: most completed concepts, most started concepts, most levels, name sort
|
|
|
|
return unless @campaign and @courseInstance and @memberUserMap
|
|
|
|
@sortedMembers = @courseInstance.get('members')
|
|
|
|
switch @memberSort
|
|
|
|
when "nameDesc"
|
2015-10-09 12:27:30 -04:00
|
|
|
@sortedMembers.sort (a, b) =>
|
|
|
|
aName = @memberUserMap[a]?.get('name') ? 'Anoner'
|
|
|
|
bName = @memberUserMap[b]?.get('name') ? 'Anoner'
|
|
|
|
bName.localeCompare(aName)
|
2015-09-13 01:01:59 -04:00
|
|
|
when "progressAsc"
|
|
|
|
@sortedMembers.sort (a, b) =>
|
|
|
|
for levelID, level of @campaign.get('levels')
|
2015-10-06 14:20:53 -04:00
|
|
|
if @userLevelStateMap[a]?[levelID] isnt 'complete' and @userLevelStateMap[b]?[levelID] is 'complete'
|
2015-09-13 01:01:59 -04:00
|
|
|
return -1
|
2015-10-06 14:20:53 -04:00
|
|
|
else if @userLevelStateMap[a]?[levelID] is 'complete' and @userLevelStateMap[b]?[levelID] isnt 'complete'
|
2015-09-13 01:01:59 -04:00
|
|
|
return 1
|
|
|
|
0
|
|
|
|
when "progressDesc"
|
|
|
|
@sortedMembers.sort (a, b) =>
|
|
|
|
for levelID, level of @campaign.get('levels')
|
2015-10-06 14:20:53 -04:00
|
|
|
if @userLevelStateMap[a]?[levelID] isnt 'complete' and @userLevelStateMap[b]?[levelID] is 'complete'
|
2015-09-13 01:01:59 -04:00
|
|
|
return 1
|
2015-10-06 14:20:53 -04:00
|
|
|
else if @userLevelStateMap[a]?[levelID] is 'complete' and @userLevelStateMap[b]?[levelID] isnt 'complete'
|
2015-09-13 01:01:59 -04:00
|
|
|
return -1
|
|
|
|
0
|
|
|
|
else
|
2015-10-09 12:27:30 -04:00
|
|
|
@sortedMembers.sort (a, b) =>
|
|
|
|
aName = @memberUserMap[a]?.get('name') ? 'Anoner'
|
|
|
|
bName = @memberUserMap[b]?.get('name') ? 'Anoner'
|
|
|
|
aName.localeCompare(bName)
|