codecombat/app/views/teachers/TeacherStudentView.coffee
2016-09-22 18:01:58 -07:00

370 lines
15 KiB
CoffeeScript

RootView = require 'views/core/RootView'
Campaigns = require 'collections/Campaigns'
Classroom = require 'models/Classroom'
Courses = require 'collections/Courses'
Levels = require 'collections/Levels'
LevelSession = require 'models/LevelSession'
LevelSessions = require 'collections/LevelSessions'
User = require 'models/User'
Users = require 'collections/Users'
CourseInstances = require 'collections/CourseInstances'
require 'vendor/d3'
module.exports = class TeacherStudentView extends RootView
id: 'teacher-student-view'
template: require 'templates/teachers/teacher-student-view'
# helper: helper
events:
# 'click .assign-student-button': 'onClickAssignStudentButton' # TODO: make this work
# 'click .enroll-student-button': 'onClickEnrollStudentButton' # TODO: make this work
'change #course-dropdown': 'onChangeCourseChart'
getTitle: -> return @user?.broadName()
initialize: (options, classroomID, @studentID) ->
@classroom = new Classroom({_id: classroomID})
@listenToOnce @classroom, 'sync', @onClassroomSync
@supermodel.trackRequest(@classroom.fetch())
@courses = new Courses()
@supermodel.trackRequest(@courses.fetch({data: { project: 'name' }}))
@courseInstances = new CourseInstances()
@supermodel.trackRequest @courseInstances.fetchForClassroom(classroomID)
@levels = new Levels()
@supermodel.trackRequest(@levels.fetchForClassroom(classroomID, {data: {project: 'name,original'}}))
@urls = require('core/urls')
@singleStudentLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-level'
@levelProgressMap = {}
super(options)
onLoaded: ->
if @students.loaded and not @destroyed
@user = _.find(@students.models, (s)=> s.id is @studentID)
@updateLastPlayedString()
@updateLevelProgressMap()
@updateLevelDataMap()
@calculateStandardDev()
@render()
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')
@drawBarGraph()
@onChangeCourseChart()
onChangeCourseChart: (e)->
if (e)
selected = ('#visualisation-'+((e.currentTarget).value))
$("[id|='visualisation']").hide()
$(selected).show()
calculateStandardDev: ->
return unless @courses.loaded and @levels.loaded and @sessions?.loaded and @levelData
@courseComparisonMap = []
for versionedCourse in @classroom.get('courses') or []
# course = _.find @courses.models, (c) => c.id is versionedCourse._id
course = @courses.get(versionedCourse._id)
numbers = []
studentCourseTotal = 0
members = 0 #this is the COUNT for our standard deviation, number of members who have played all of the levels this student has played.
for member in @classroom.get('members')
number = 0
memberPlayed = 0 # number of levels a member has played that this student has also played
for versionedLevel in versionedCourse.levels
for session in @sessions.models
if session.get('level').original is versionedLevel.original and session.get('creator') is member
playedLevel = _.findWhere(@levelData, {levelID: session.get('level').original})
if playedLevel.levelProgress is 'complete' or playedLevel.levelProgress is 'started'
number += session.get('playtime') or 0
memberPlayed += 1
if session.get('creator') is @studentID
studentCourseTotal += session.get('playtime') or 0
if memberPlayed > 0 then members += 1
numbers.push number
# add all numbers[]
sum = numbers.reduce (a,b) -> a + b
# divide by members to get MEAN, remember MEAN is only an average of the members' performance on levels THIS student has done.
mean = sum/members
# # for each number in numbers[], subtract MEAN then SQUARE, add all, then divide by COUNT to get VARIANCE
diffSum = numbers.map((num) -> (num-mean)**2).reduce (a,b) -> a+b
variance = (diffSum / members)
# square root of VARIANCE is standardDev
StandardDev = Math.sqrt(variance)
perf = -(studentCourseTotal - mean) / StandardDev
perf = if perf > 0 then Math.ceil(perf) else Math.floor(perf)
@courseComparisonMap.push {
courseID: course.get('_id')
studentCourseTotal: studentCourseTotal
standardDev: StandardDev
mean: mean
performance: perf
}
# console.log (@courseComparisonMap)
drawBarGraph: ->
return unless @courses.loaded and @levels.loaded and @sessions?.loaded and @levelData and @courseComparisonMap
WIDTH = 1142
HEIGHT = 600
MARGINS = {
top: 50
right: 20
bottom: 50
left: 70
}
for versionedCourse in @classroom.get('courses') or []
# this does all of the courses, logic for whether student was assigned is in corresponding jade file
vis = d3.select('#visualisation-'+versionedCourse._id)
# TODO: continue if selector isn't found.
courseLevelData = []
for level in @levelData when level.courseID is versionedCourse._id
courseLevelData.push level
course = @courses.get(versionedCourse._id)
levels = @classroom.getLevels({courseID: course.id}).models
xRange = d3.scale.ordinal().rangeRoundBands([MARGINS.left, WIDTH - MARGINS.right], 0.1).domain(courseLevelData.map( (d) -> d.levelIndex))
yRange = d3.scale.linear().range([HEIGHT - (MARGINS.top), MARGINS.bottom]).domain([0, d3.max(courseLevelData, (d) -> if d.classAvg > d.studentTime then d.classAvg else d.studentTime)])
xAxis = d3.svg.axis().scale(xRange).tickSize(1).tickSubdivide(true)
yAxis = d3.svg.axis().scale(yRange).tickSize(1).orient('left').tickSubdivide(true)
vis.append('svg:g').attr('class', 'x axis').attr('transform', 'translate(0,' + (HEIGHT - (MARGINS.bottom)) + ')').call xAxis
vis.append('svg:g').attr('class', 'y axis').attr('transform', 'translate(' + MARGINS.left + ',0)').call yAxis
chart = vis.selectAll('rect')
.data(courseLevelData)
.enter()
# draw classroom average bars
chart.append('rect')
.attr('class', 'classroom-bar')
.attr('x', ((d) -> xRange(d.levelIndex) + (xRange.rangeBand())/2))
.attr('y', (d) -> yRange(d.classAvg))
.attr('width', (xRange.rangeBand())/2)
.attr('height', (d) -> HEIGHT - (MARGINS.bottom) - yRange(d.classAvg))
.attr('fill', '#5CB4D0')
# add classroom average values
chart.append('text')
.attr('x', ((d) -> xRange(d.levelIndex) + (xRange.rangeBand())/2))
.attr('y', ((d) -> yRange(d.classAvg) - 3 ))
.text((d)-> if d.classAvg isnt 0 then d.classAvg)
.attr('class', 'label')
# draw student playtime bars
chart.append('rect')
.attr('class', 'student-bar')
.attr('x', ((d) -> xRange(d.levelIndex)))
.attr('y', (d) -> yRange(d.studentTime))
.attr('width', (xRange.rangeBand())/2)
.attr('height', (d) -> HEIGHT - (MARGINS.bottom) - yRange(d.studentTime))
.attr('fill', (d) -> if d.levelProgress is 'complete' then '#20572B' else '#F2BE19')
# add student playtime value
chart.append('text')
.attr('x', ((d) -> xRange(d.levelIndex)) )
.attr('y', ((d) -> yRange(d.studentTime) - 3 ))
.text((d)-> if d.studentTime isnt 0 then d.studentTime)
.attr('class', 'label')
labels = vis.append("g").attr("class", "labels")
# add Playtime axis label
labels.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 20)
.attr("x", - HEIGHT/2)
.attr("dy", ".71em")
.style("text-anchor", "middle")
.text($.i18n.t("teacher.playtime_axis"))
# add levels axis label
labels.append("text")
.attr("x", WIDTH/2)
.attr("y", HEIGHT - 10)
.text("Levels in " + (course.get('name')))
.style("text-anchor", "middle")
onClassroomSync: ->
# Now that we have the classroom from db, can request all level sessions for this classroom
@sessions = new LevelSessions()
@sessions.comparator = 'changed' # Sort level sessions by changed field, ascending
@listenTo @sessions, 'sync', @onSessionsSync
@supermodel.trackRequests(@sessions.fetchForAllClassroomMembers(@classroom))
@students = new Users()
jqxhrs = @students.fetchForClassroom(@classroom, removeDeleted: true)
# @listenTo @students, ->
@supermodel.trackRequests jqxhrs
onSessionsSync: ->
# Now we have some level sessions, and enough data to calculate last played string
# This may be called multiple times due to paged server API calls via fetchForAllClassroomMembers
return if @destroyed # Don't do anything if page was destroyed after db request
@updateLastPlayedString()
@updateLevelProgressMap()
@updateLevelDataMap()
updateLastPlayedString: ->
# Make sure all our data is loaded, @sessions may not even be intialized yet
return unless @courses.loaded and @levels.loaded and @sessions?.loaded and @user?.loaded
# Use lodash to find the last session for our user, @sessions already sorted by changed date
session = _.findLast @sessions.models, (s) => s.get('creator') is @user.id
return unless session
# Find course for this level session, for it's name
# Level.original is the original id, used for level versioning, and connects levels to level sessions
for versionedCourse in @classroom.get('courses') or []
for level in versionedCourse.levels
if level.original is session.get('level').original
# Found the level for our level session in the classroom versioned courses
# Find the full course so we can get it's name
course = @courses.get(versionedCourse._id)
break
# Find level for this level session, for it's name
level = @levels.findWhere({original: session.get('level').original})
# Update last played string based on what we found
@lastPlayedString = ""
@lastPlayedString += course.get('name') if course
@lastPlayedString += ": " if course and level
@lastPlayedString += level.get('name') if level
@lastPlayedString += ", on " if course or level
@lastPlayedString += moment(session.get('changed')).format("LLLL")
# console.log (moment(session.get('changed')).format("LLLL"))
# Rerun template/jade file to display new last played string
@render()
updateLevelProgressMap: ->
return unless @courses.loaded and @levels.loaded and @sessions?.loaded and @user?.loaded
# Map levels to sessions once, so we don't have to search entire session list multiple times below
@levelSessionMap = {}
for session in @sessions.models when session.get('creator') is @studentID
@levelSessionMap[session.get('level').original] = session
# Create mapping of level to student progress
@levelProgressMap = {}
for versionedCourse in @classroom.get('courses') or []
for versionedLevel in versionedCourse.levels
session = @levelSessionMap[versionedLevel.original]
if session
if session.get('state')?.complete
@levelProgressMap[versionedLevel.original] = 'complete'
else
@levelProgressMap[versionedLevel.original] = 'started'
else
@levelProgressMap[versionedLevel.original] = 'not started'
updateLevelDataMap: ->
return unless @courses.loaded and @levels.loaded and @sessions?.loaded
@levelData = []
for versionedCourse in @classroom.get('courses') or []
course = @courses.get(versionedCourse._id)
for versionedLevel in versionedCourse.levels
playTime = 0 # TODO: this and timesPlayed should probably only count when the levels are completed
timesPlayed = 0
studentTime = 0
levelProgress = 'not started'
for session in @sessions.models
if session.get('level').original is versionedLevel.original
# if @levelProgressMap[versionedLevel.original] == 'complete' # ideally, don't log sessions that aren't completed in the class
playTime += session.get('playtime') or 0
timesPlayed += 1
if session.get('creator') is @studentID
studentTime = session.get('playtime') or 0
if @levelProgressMap[versionedLevel.original] is 'complete'
levelProgress = 'complete'
else if @levelProgressMap[versionedLevel.original] is 'started'
levelProgress = 'started'
classAvg = if timesPlayed > 0 then Math.round(playTime / timesPlayed) else 0 # only when someone other than the user has played
# console.log (timesPlayed)
@levelData.push {
levelID: versionedLevel.original
levelIndex: @classroom.getLevelNumber(versionedLevel.original)
levelName: versionedLevel.name
courseName: course.get('name')
courseID: course.get('_id')
classAvg: classAvg
studentTime: if studentTime then studentTime else 0
levelProgress: levelProgress
# required:
}
studentStatusString: () ->
status = @user.prepaidStatus()
return "" unless @user.get('coursePrepaid')
expires = @user.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')
if expires
return string.replace('{{date}}', moment(expires).utc().format('l'))
else
# this probably doesn't happen
return string.replace('{{date}}', "Never")
# TODO: Hookup enroll/assign functionality
# onClickEnrollStudentButton: (e) ->
# userID = $(e.currentTarget).data('user-id')
# user = @user.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: @user }
# @openModalView(modal)
# modal.once 'redeem-users', (enrolledUsers) =>
# enrolledUsers.each (newUser) =>
# user = @user.get(newUser.id)
# if user
# user.set(newUser.attributes)
# null
# 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"