mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-25 00:28:31 -05:00
d72e4eb750
Update classroom and gameplay Ux to surface practice levels as 3a, 3b, etc. Update next level logic to leverage practice levels based on per level completion playtime thresholds. Patrol buster and patrol buster A are live for testing. Fix a few classroom Ux progress hover bubble info bugs. Closes #3767
246 lines
10 KiB
CoffeeScript
246 lines
10 KiB
CoffeeScript
Campaign = require 'models/Campaign'
|
|
CocoCollection = require 'collections/CocoCollection'
|
|
Course = require 'models/Course'
|
|
CourseInstance = require 'models/CourseInstance'
|
|
Classroom = require 'models/Classroom'
|
|
Classrooms = require 'collections/Classrooms'
|
|
LevelSession = require 'models/LevelSession'
|
|
Prepaids = require 'collections/Prepaids'
|
|
Levels = require 'collections/Levels'
|
|
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'
|
|
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
|
|
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
|
|
RemoveStudentModal = require 'views/courses/RemoveStudentModal'
|
|
popoverTemplate = require 'templates/courses/classroom-level-popover'
|
|
|
|
module.exports = class ClassroomView extends RootView
|
|
id: 'classroom-view'
|
|
template: template
|
|
teacherMode: false
|
|
|
|
events:
|
|
'click #edit-class-details-link': 'onClickEditClassDetailsLink'
|
|
'click #activate-licenses-btn': 'onClickActivateLicensesButton'
|
|
'click .activate-single-license-btn': 'onClickActivateSingleLicenseButton'
|
|
'click #add-students-btn': 'onClickAddStudentsButton'
|
|
'click .enable-btn': 'onClickEnableButton'
|
|
'click .remove-student-link': 'onClickRemoveStudentLink'
|
|
|
|
initialize: (options, classroomID) ->
|
|
return if me.isAnonymous()
|
|
@classroom = new Classroom({_id: classroomID})
|
|
@supermodel.loadModel @classroom
|
|
@courses = new CocoCollection([], { url: "/db/course", model: Course})
|
|
@courses.comparator = '_id'
|
|
@supermodel.loadCollection(@courses)
|
|
@courses.comparator = '_id'
|
|
@courseInstances = new CocoCollection([], { url: "/db/course_instance", model: CourseInstance})
|
|
@courseInstances.comparator = 'courseID'
|
|
@supermodel.loadCollection(@courseInstances, { data: { classroomID: classroomID } })
|
|
@prepaids = new Prepaids()
|
|
@prepaids.comparator = '_id'
|
|
@prepaids.fetchByCreator(me.id)
|
|
@supermodel.loadCollection(@prepaids)
|
|
@users = new CocoCollection([], { url: "/db/classroom/#{classroomID}/members?memberLimit=100", model: User })
|
|
@users.comparator = (user) => user.broadName().toLowerCase()
|
|
@supermodel.loadCollection(@users)
|
|
@listenToOnce @courseInstances, 'sync', @onCourseInstancesSync
|
|
@sessions = new CocoCollection([], { model: LevelSession })
|
|
@ownedClassrooms = new Classrooms()
|
|
@ownedClassrooms.fetchMine({data: {project: '_id'}})
|
|
@supermodel.trackCollection(@ownedClassrooms)
|
|
@levels = new Levels()
|
|
@levels.fetchForClassroom(classroomID, {data: {project: 'name,original,practice,slug'}})
|
|
@levels.on 'add', (model) -> @_byId[model.get('original')] = model # so you can 'get' them
|
|
@supermodel.trackCollection(@levels)
|
|
window.tracker?.trackEvent 'Students Class Loaded', category: 'Students', classroomID: classroomID, ['Mixpanel']
|
|
|
|
onCourseInstancesSync: ->
|
|
@sessions = new CocoCollection([], { model: LevelSession })
|
|
for courseInstance in @courseInstances.models
|
|
sessions = new CocoCollection([], { url: "/db/course_instance/#{courseInstance.id}/level_sessions", model: LevelSession })
|
|
@supermodel.loadCollection(sessions, { data: { project: ['level', 'playtime', 'creator', 'changed', 'state.complete'].join(' ') } })
|
|
courseInstance.sessions = sessions
|
|
sessions.courseInstance = courseInstance
|
|
courseInstance.sessionsByUser = {}
|
|
@listenToOnce sessions, 'sync', (sessions) ->
|
|
@sessions.add(sessions.slice())
|
|
for courseInstance in @courseInstances.models
|
|
courseInstance.sessionsByUser = courseInstance.sessions.groupBy('creator')
|
|
|
|
# Generate course instance JIT, in the meantime have models w/out equivalents in the db
|
|
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 = {}
|
|
|
|
onLoaded: ->
|
|
@teacherMode = me.isAdmin() or @classroom.get('ownerID') is me.id
|
|
userSessions = @sessions.groupBy('creator')
|
|
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)
|
|
courseInstance.sessions.course = course
|
|
super()
|
|
|
|
afterRender: ->
|
|
@$('[data-toggle="popover"]').popover({
|
|
html: true
|
|
trigger: 'hover'
|
|
placement: 'top'
|
|
})
|
|
super()
|
|
|
|
onClickActivateLicensesButton: ->
|
|
modal = new ActivateLicensesModal({
|
|
classroom: @classroom
|
|
users: @users
|
|
})
|
|
@openModalView(modal)
|
|
modal.once 'redeem-users', -> document.location.reload()
|
|
application.tracker?.trackEvent 'Classroom started enroll students', category: 'Courses'
|
|
|
|
onClickActivateSingleLicenseButton: (e) ->
|
|
userID = $(e.target).closest('.btn').data('user-id')
|
|
if @prepaids.totalMaxRedeemers() - @prepaids.totalRedeemers() > 0
|
|
# Have an unused enrollment, enroll student immediately instead of opening the enroll modal
|
|
prepaid = @prepaids.find((prepaid) -> prepaid.status() is 'available')
|
|
$.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
|
|
|
|
onClickEditClassDetailsLink: ->
|
|
modal = new ClassroomSettingsModal({classroom: @classroom})
|
|
@openModalView(modal)
|
|
@listenToOnce modal, 'hidden', @render
|
|
|
|
userLastPlayedString: (user) ->
|
|
return '' unless user.sessions?
|
|
session = user.sessions.last()
|
|
return '' unless session
|
|
course = session.collection.course
|
|
levelOriginal = session.get('level').original
|
|
level = @levels.findWhere({original: levelOriginal})
|
|
return "#{course.get('name')}, #{level.get('name')}"
|
|
|
|
userPlaytimeString: (user) ->
|
|
return '' unless user.sessions?
|
|
playtime = _.reduce user.sessions.pluck('playtime'), (s1, s2) -> (s1 or 0) + (s2 or 0)
|
|
return '' unless playtime
|
|
return moment.duration(playtime, 'seconds').humanize()
|
|
|
|
classStats: ->
|
|
stats = {}
|
|
|
|
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
|
|
|
|
levelPracticeMap = {}
|
|
levelPracticeMap[level.id] = level.get('practice') ? false for level in @levels.models
|
|
completeSessions = @sessions.filter (s) -> s.get('state')?.complete and not levelPracticeMap[s.get('levelID')]
|
|
stats.averageLevelsComplete = if @users.size() then (_.size(completeSessions) / @users.size()).toFixed(1) else 'N/A' # '
|
|
stats.totalLevelsComplete = _.size(completeSessions)
|
|
|
|
enrolledUsers = @users.filter (user) -> user.isEnrolled()
|
|
stats.enrolledUsers = _.size(enrolledUsers)
|
|
return stats
|
|
|
|
onClickAddStudentsButton: (e) ->
|
|
modal = new InviteToClassroomModal({classroom: @classroom})
|
|
@openModalView(modal)
|
|
application.tracker?.trackEvent 'Classroom started add students', category: 'Courses', classroomID: @classroom.id
|
|
|
|
onClickEnableButton: (e) ->
|
|
$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)
|
|
application.tracker?.trackEvent 'Course assign student', category: 'Courses', courseInstanceID: courseInstance.id, userID: userID
|
|
|
|
onCourseInstanceCreated = =>
|
|
courseInstance.addMember(userID)
|
|
@listenToOnce courseInstance, 'sync', @render
|
|
|
|
if courseInstance.isNew()
|
|
# adding the first student to this course, so generate the course instance for it
|
|
if not courseInstance.saving
|
|
courseInstance.save(null, {validate: false})
|
|
courseInstance.saving = true
|
|
courseInstance.once 'sync', onCourseInstanceCreated
|
|
else
|
|
onCourseInstanceCreated()
|
|
|
|
# TODO: update newly visible level progress bar (currently all white)
|
|
|
|
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, @
|
|
|
|
onStudentRemoved: (e) ->
|
|
@users.remove(e.user)
|
|
@render()
|
|
application.tracker?.trackEvent 'Classroom removed student', category: 'Courses', classroomID: @classroom.id, userID: e.user.id
|
|
|
|
levelPopoverContent: (level, session, i) ->
|
|
return null unless level
|
|
context = {
|
|
moment: moment
|
|
level: level
|
|
session: session
|
|
i: i
|
|
canViewSolution: @teacherMode
|
|
}
|
|
return popoverTemplate(context)
|
|
|
|
getLevelURL: (level, course, courseInstance, session) ->
|
|
return null unless @teacherMode and _.all(arguments)
|
|
"/play/level/#{level.get('slug')}?course=#{course.id}&course-instance=#{courseInstance.id}&session=#{session.id}&observing=true"
|