Update furthest course dashboard tables

Adding a second table, setting range to be 60 and 365 days.
Updating teacher columns to prioritize student paid status over
furthest student course.
This commit is contained in:
Matt Lott 2016-02-24 06:24:58 -08:00
parent dd603a0436
commit 609884eb51
5 changed files with 185 additions and 48 deletions

View file

@ -124,26 +124,72 @@ block content
h3#enrollments-graph Enrollments Issued and Redeemed 90 days
.paid-courses-chart.line-chart-container
h3#furthest-courses-table Furthest Course
.small Teacher: owner of a course instance
.small Student: member of a course instance (assigned to course)
.small For course instances created in last #{view.furthestCourseDayRange} days, not Single Player, hourOfCode != true
.small Counts are not summed. I.e. a student or teacher only contributes to the count of one course.
if view.teacherCourseDistribution
table.table.table-striped.table-condensed
tr
th Course
th Teachers
th Students
th Avg students per teacher
each count, courseIndex in view.teacherCourseDistribution
#furthest-course
h3 Furthest Course in last #{view.furthestCourseDayRangeRecent} days
.small Restricted to courses instances from last #{view.furthestCourseDayRangeRecent} days
.small Teacher: owner of a course instance
.small Student: member of a course instance (assigned to course)
.small For course instances != Single Player, hourOfCode != true
.small Counts are not summed. I.e. a student or teacher only contributes to the count of one course
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set
.small Free student: not paid, not trial
.small Paid teacher: at least one paid student in course instance
.small Trial teacher: at least one trial student in course instance, and no paid students
.small Free teacher: no paid students, no trial students
.small Paid status takes precedent over furthest course, so teacher furthest course is furthest course of highest paid status student
if view.courseDistributionsRecent
table.table.table-striped.table-condensed
tr
td= view.courses.models[courseIndex].get('name')
td= count
td= view.studentCourseDistribution[courseIndex] || 0
td= Math.round((view.studentCourseDistribution[courseIndex] || 0) / count)
else
div Loading ...
th Course
th Paid Teachers
th Trial Teachers
th Free Teachers
th Total Teachers
th Paid Students
th Trial Students
th Free Students
th Total Students
each row in view.courseDistributionsRecent
tr
td= row.courseName
td= row.totals['Paid Teachers'] || 0
td= row.totals['Trial Teachers'] || 0
td= row.totals['Free Teachers'] || 0
td= row.totals['Total Teachers'] || 0
td= row.totals['Paid Students'] || 0
td= row.totals['Trial Students'] || 0
td= row.totals['Free Students'] || 0
td= row.totals['Total Students'] || 0
else
div Loading ...
h3 Furthest Course in last #{view.furthestCourseDayRange} days
if view.courseDistributions
table.table.table-striped.table-condensed
tr
th Course
th Paid Teachers
th Trial Teachers
th Free Teachers
th Total Teachers
th Paid Students
th Trial Students
th Free Students
th Total Students
each row in view.courseDistributions
tr
td= row.courseName
td= row.totals['Paid Teachers'] || 0
td= row.totals['Trial Teachers'] || 0
td= row.totals['Free Teachers'] || 0
td= row.totals['Total Teachers'] || 0
td= row.totals['Paid Students'] || 0
td= row.totals['Trial Students'] || 0
td= row.totals['Free Students'] || 0
td= row.totals['Total Students'] || 0
else
div Loading ...
#school-sales
h3 School Sales

View file

@ -11,7 +11,8 @@ utils = require 'core/utils'
module.exports = class AnalyticsView extends RootView
id: 'admin-analytics-view'
template: template
furthestCourseDayRange: 30
furthestCourseDayRangeRecent: 60
furthestCourseDayRange: 365
lineColors: ['red', 'blue', 'green', 'purple', 'goldenrod', 'brown', 'darkcyan']
minSchoolCount: 20
@ -238,35 +239,106 @@ module.exports = class AnalyticsView extends RootView
options.error = (models, response, options) =>
return if @destroyed
console.error 'Failed to get recent course instances', response
options.success = (models) =>
@courseInstances = models ? []
@onCourseInstancesSync()
@render?()
options.success = (data) =>
@onCourseInstancesSync(data)
@renderSelectors?('#furthest-course')
@supermodel.addRequestResource(options, 0).load()
onCourseInstancesSync: ->
return unless @courseInstances
onCourseInstancesSync: (data) ->
@courseDistributionsRecent = []
@courseDistributions = []
return unless data.courseInstances and data.students and data.prepaids
# Find highest course for teachers and students
@teacherFurthestCourseMap = {}
@studentFurthestCourseMap = {}
for courseInstance in @courseInstances
courseID = courseInstance.courseID
teacherID = courseInstance.ownerID
if not @teacherFurthestCourseMap[teacherID] or @teacherFurthestCourseMap[teacherID] < @courseOrderMap[courseID]
@teacherFurthestCourseMap[teacherID] = @courseOrderMap[courseID]
for studentID in courseInstance.members
if not @studentFurthestCourseMap[studentID] or @studentFurthestCourseMap[studentID] < @courseOrderMap[courseID]
@studentFurthestCourseMap[studentID] = @courseOrderMap[courseID]
createCourseDistributions = (numDays) =>
# Find student furthest course
startDate = new Date()
startDate.setUTCDate(startDate.getUTCDate() - numDays)
teacherStudentsMap = {}
studentFurthestCourseMap = {}
studentPaidStatusMap = {}
for courseInstance in data.courseInstances
continue if utils.objectIdToDate(courseInstance._id) < startDate
courseID = courseInstance.courseID
teacherID = courseInstance.ownerID
for studentID in courseInstance.members
studentPaidStatusMap[studentID] = 'free'
if not studentFurthestCourseMap[studentID] or studentFurthestCourseMap[studentID] < @courseOrderMap[courseID]
studentFurthestCourseMap[studentID] = @courseOrderMap[courseID]
teacherStudentsMap[teacherID] ?= []
teacherStudentsMap[teacherID].push(studentID)
@teacherCourseDistribution = {}
for teacherID, courseIndex of @teacherFurthestCourseMap
@teacherCourseDistribution[courseIndex] ?= 0
@teacherCourseDistribution[courseIndex]++
@studentCourseDistribution = {}
for studentID, courseIndex of @studentFurthestCourseMap
@studentCourseDistribution[courseIndex] ?= 0
@studentCourseDistribution[courseIndex]++
# Find paid students
prepaidUserMap = {}
for user in data.students
continue unless studentPaidStatusMap[user._id]
if prepaidID = user.coursePrepaidID
studentPaidStatusMap[user._id] = 'paid'
prepaidUserMap[prepaidID] ?= []
prepaidUserMap[prepaidID].push(user._id)
# Find trial students
for prepaid in data.prepaids
continue unless prepaidUserMap[prepaid._id]
if prepaid.properties?.trialRequestID
for userID in prepaidUserMap[prepaid._id]
studentPaidStatusMap[userID] = 'trial'
# Find teacher furthest course and paid status based on their students
# Paid teacher: at least one paid student
# Trial teacher: at least one trial student in course instance, and no paid students
# Free teacher: no paid students, no trial students
# Teacher furthest course is furthest course of highest paid status student
teacherFurthestCourseMap = {}
teacherPaidStatusMap = {}
for teacher, students of teacherStudentsMap
for student in students
if not teacherPaidStatusMap[teacher]
teacherPaidStatusMap[teacher] = studentPaidStatusMap[student]
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
else if teacherPaidStatusMap[teacher] is 'trial' and studentPaidStatusMap[student] is 'paid'
teacherPaidStatusMap[teacher] = studentPaidStatusMap[student]
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
else if teacherPaidStatusMap[teacher] is 'free' and studentPaidStatusMap[student] in ['paid', 'trial']
teacherPaidStatusMap[teacher] = studentPaidStatusMap[student]
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
else if teacherFurthestCourseMap[teacher] < studentFurthestCourseMap[student]
teacherFurthestCourseMap[teacher] = studentFurthestCourseMap[student]
# Build table of student/teacher paid/trial/free totals
updateCourseTotalsMap = (courseTotalsMap, furthestCourseMap, paidStatusMap, columnSuffix) =>
for user, courseIndex of furthestCourseMap
courseName = @courses.models[courseIndex].get('name')
courseTotalsMap[courseName] ?= {}
columnName = switch paidStatusMap[user]
when 'paid' then 'Paid ' + columnSuffix
when 'trial' then 'Trial ' + columnSuffix
when 'free' then 'Free ' + columnSuffix
courseTotalsMap[courseName][columnName] ?= 0
courseTotalsMap[courseName][columnName]++
courseTotalsMap[courseName]['Total ' + columnSuffix] ?= 0
courseTotalsMap[courseName]['Total ' + columnSuffix]++
courseTotalsMap['All Courses']['Total ' + columnSuffix] ?= 0
courseTotalsMap['All Courses']['Total ' + columnSuffix]++
courseTotalsMap['All Courses'][columnName] ?= 0
courseTotalsMap['All Courses'][columnName]++
courseTotalsMap = {'All Courses': {}}
updateCourseTotalsMap(courseTotalsMap, teacherFurthestCourseMap, teacherPaidStatusMap, 'Teachers')
updateCourseTotalsMap(courseTotalsMap, studentFurthestCourseMap, studentPaidStatusMap, 'Students')
courseDistributions = []
for courseName, totals of courseTotalsMap
courseDistributions.push({courseName: courseName, totals: totals})
courseDistributions.sort (a, b) ->
if a.courseName.indexOf('Introduction') >= 0 and b.courseName.indexOf('Introduction') < 0 then return -1
else if b.courseName.indexOf('Introduction') >= 0 and a.courseName.indexOf('Introduction') < 0 then return 1
else if a.courseName.indexOf('All Courses') >= 0 and b.courseName.indexOf('All Courses') < 0 then return 1
else if b.courseName.indexOf('All Courses') >= 0 and a.courseName.indexOf('All Courses') < 0 then return -1
a.courseName.localeCompare(b.courseName)
courseDistributions
@courseDistributionsRecent = createCourseDistributions(@furthestCourseDayRangeRecent)
@courseDistributions = createCourseDistributions(@furthestCourseDayRange)
createLineChartPoints: (days, data) ->
points = []

View file

@ -60,6 +60,7 @@ function getRecurringRevenueCounts(startDay) {
}
if (doc.service === 'ios' || doc.service === 'bitcoin') continue;
if (!doc.amount || doc.amount <= 0) continue;
if (doc.prepaidID) {
if (prepaidDayAmountMap[doc.prepaidID.valueOf()]) {

View file

@ -207,9 +207,27 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
query = {$and: [{name: {$ne: 'Single Player'}}, {hourOfCode: {$ne: true}}]}
query["$and"].push(_id: {$gte: objectIdFromTimestamp(req.body.startDay + "T00:00:00.000Z")}) if req.body.startDay?
query["$and"].push(_id: {$lt: objectIdFromTimestamp(req.body.endDay + "T00:00:00.000Z")}) if req.body.endDay?
CourseInstance.find query, (err, courseInstances) =>
CourseInstance.find query, {courseID: 1, members: 1, ownerID: 1}, (err, courseInstances) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, courseInstances)
userIDs = []
for courseInstance in courseInstances
if members = courseInstance.get('members')
userIDs.push(userID) for userID in members
User.find {_id: {$in: userIDs}}, {coursePrepaidID: 1}, (err, users) =>
return @sendDatabaseError(res, err) if err
prepaidIDs = []
for user in users
if prepaidID = user.get('coursePrepaidID')
prepaidIDs.push(prepaidID)
Prepaid.find {_id: {$in: prepaidIDs}}, {properties: 1}, (err, prepaids) =>
return @sendDatabaseError(res, err) if err
data =
courseInstances: (@formatEntity(req, courseInstance) for courseInstance in courseInstances)
students: (@formatEntity(req, user) for user in users)
prepaids: (@formatEntity(req, prepaid) for prepaid in prepaids)
@sendSuccess(res, data)
inviteStudents: (req, res, courseInstanceID) ->
return @sendUnauthorizedError(res) if not req.user?

View file

@ -44,7 +44,7 @@ PaymentHandler = class PaymentHandler extends Handler
payment
getSchoolSalesAPI: (req, res, code) ->
return @sendSuccess(res, []) unless req.user?.isAdmin()
return @sendUnauthorizedError(res) unless req.user?.isAdmin()
userIDs = [];
Payment.find({}, {amount: 1, created: 1, description: 1, prepaidID: 1, productID: 1, purchaser: 1, service: 1}).exec (err, payments) =>
return @sendDatabaseError(res, err) if err