Add enrollments graph to analytics dashboard

This commit is contained in:
Matt Lott 2016-02-06 14:02:44 -08:00
parent 378ddccce7
commit bd9ca8ee5f
4 changed files with 194 additions and 9 deletions

View file

@ -30,6 +30,9 @@ module.exports.normalizeFunc = (func_thing, object) ->
func_thing = func
return func_thing
module.exports.objectIdToDate = (objectID) ->
new Date(parseInt(objectID.toString().slice(0,8), 16)*1000)
module.exports.hexToHSL = (hex) ->
rgbToHsl(hexToR(hex), hexToG(hex), hexToB(hex))

View file

@ -23,7 +23,7 @@ block content
h3 KPI 60 days
.kpi-recent-chart.line-chart-container
h3 KPI 300 days
h3 KPI 365 days
.kpi-chart.line-chart-container
h3 Active Classes 90 days
@ -41,6 +41,9 @@ block content
h3 Active Users 90 days
.active-users-chart.line-chart-container
h3 Enrollments Issued and Redeemed 90 days
.paid-courses-chart.line-chart-container
h3 Furthest Course
.small Teacher: owner of a course instance
.small Student: member of a course instance (assigned to course)
@ -119,3 +122,25 @@ block content
else
td
td
h1 Enrollments
table.table.table-striped.table-condensed
tr
th Day
th Paid Enrollments Issued
th Paid Enrollments Redeemed
th Trial Enrollments Issued
th Trial Enrollments Redeemed
each day in enrollmentDays
tr
td= day
if dayEnrollmentsMap[day]
td= dayEnrollmentsMap[day].paidIssued || 0
td= dayEnrollmentsMap[day].paidRedeemed || 0
td= dayEnrollmentsMap[day].trialIssued || 0
td= dayEnrollmentsMap[day].trialRedeemed || 0
else
td 0
td 0
td 0
td 0

View file

@ -25,6 +25,8 @@ module.exports = class AnalyticsView extends RootView
context.activeUsers = @activeUsers ? []
context.revenue = @revenue ? []
context.revenueGroups = @revenueGroups ? {}
context.dayEnrollmentsMap = @dayEnrollmentsMap ? {}
context.enrollmentDays = @enrollmentDays ? []
context
afterRender: ->
@ -32,7 +34,7 @@ module.exports = class AnalyticsView extends RootView
@createLineCharts()
loadData: ->
@supermodel.addRequestResource('active_classes', {
@supermodel.addRequestResource({
url: '/db/analytics_perday/-/active_classes'
method: 'POST'
success: (data) =>
@ -63,7 +65,7 @@ module.exports = class AnalyticsView extends RootView
@render?()
}, 0).load()
@supermodel.addRequestResource('active_users', {
@supermodel.addRequestResource({
url: '/db/analytics_perday/-/active_users'
method: 'POST'
success: (data) =>
@ -77,7 +79,7 @@ module.exports = class AnalyticsView extends RootView
@render?()
}, 0).load()
@supermodel.addRequestResource('recurring_revenue', {
@supermodel.addRequestResource({
url: '/db/analytics_perday/-/recurring_revenue'
method: 'POST'
success: (data) =>
@ -120,7 +122,7 @@ module.exports = class AnalyticsView extends RootView
@render?()
}, 0).load()
@supermodel.addRequestResource('school_counts', {
@supermodel.addRequestResource({
url: '/db/user/-/school_counts'
method: 'POST'
data: {minCount: @minSchoolCount}
@ -132,10 +134,68 @@ module.exports = class AnalyticsView extends RootView
@render?()
}, 0).load()
@supermodel.addRequestResource({
url: '/db/prepaid/-/courses'
method: 'POST'
data: {project: {maxRedeemers: 1, properties: 1, redeemers: 1}}
success: (prepaids) =>
paidDayMaxMap = {}
paidDayRedeemedMap = {}
trialDayMaxMap = {}
trialDayRedeemedMap = {}
for prepaid in prepaids
day = utils.objectIdToDate(prepaid._id).toISOString().substring(0, 10)
if prepaid.properties?.trialRequestID? or prepaid.properties?.endDate?
trialDayMaxMap[day] ?= 0
if prepaid.properties?.endDate?
trialDayMaxMap[day] += prepaid.redeemers?.length ? 0
else
trialDayMaxMap[day] += prepaid.maxRedeemers
for redeemer in (prepaid.redeemers ? [])
redeemDay = redeemer.date.substring(0, 10)
trialDayRedeemedMap[redeemDay] ?= 0
trialDayRedeemedMap[redeemDay]++
else
paidDayMaxMap[day] ?= 0
paidDayMaxMap[day] += prepaid.maxRedeemers
for redeemer in prepaid.redeemers
redeemDay = redeemer.date.substring(0, 10)
paidDayRedeemedMap[redeemDay] ?= 0
paidDayRedeemedMap[redeemDay]++
@dayEnrollmentsMap = {}
@paidCourseTotalEnrollments = []
for day, count of paidDayMaxMap
@paidCourseTotalEnrollments.push({day: day, count: count})
@dayEnrollmentsMap[day] ?= {paidIssued: 0, paidRedeemed: 0, trialIssued: 0, trialRedeemed: 0}
@dayEnrollmentsMap[day].paidIssued += count
@paidCourseTotalEnrollments.sort (a, b) -> a.day.localeCompare(b.day)
@paidCourseRedeemedEnrollments = []
for day, count of paidDayRedeemedMap
@paidCourseRedeemedEnrollments.push({day: day, count: count})
@dayEnrollmentsMap[day] ?= {paidIssued: 0, paidRedeemed: 0, trialIssued: 0, trialRedeemed: 0}
@dayEnrollmentsMap[day].paidRedeemed += count
@paidCourseRedeemedEnrollments.sort (a, b) -> a.day.localeCompare(b.day)
@trialCourseTotalEnrollments = []
for day, count of trialDayMaxMap
@trialCourseTotalEnrollments.push({day: day, count: count})
@dayEnrollmentsMap[day] ?= {paidIssued: 0, paidRedeemed: 0, trialIssued: 0, trialRedeemed: 0}
@dayEnrollmentsMap[day].trialIssued += count
@trialCourseTotalEnrollments.sort (a, b) -> a.day.localeCompare(b.day)
@trialCourseRedeemedEnrollments = []
for day, count of trialDayRedeemedMap
@trialCourseRedeemedEnrollments.push({day: day, count: count})
@dayEnrollmentsMap[day] ?= {paidIssued: 0, paidRedeemed: 0, trialIssued: 0, trialRedeemed: 0}
@dayEnrollmentsMap[day].trialRedeemed += count
@trialCourseRedeemedEnrollments.sort (a, b) -> a.day.localeCompare(b.day)
@updateEnrollmentsChartData()
@render?()
}, 0).load()
@courses = new CocoCollection([], { url: "/db/course", model: Course})
@courses.comparator = "_id"
@listenToOnce @courses, 'sync', @onCoursesSync
@supermodel.loadCollection(@courses, 'courses')
@supermodel.loadCollection(@courses)
onCoursesSync: ->
# Assumes courses retrieved in order
@ -156,7 +216,7 @@ module.exports = class AnalyticsView extends RootView
@courseInstances = models ? []
@onCourseInstancesSync()
@render?()
@supermodel.addRequestResource('get_recent_course_instances', options, 0).load()
@supermodel.addRequestResource(options, 0).load()
onCourseInstancesSync: ->
return unless @courseInstances
@ -212,13 +272,14 @@ module.exports = class AnalyticsView extends RootView
d3Utils.createLineChart('.kpi-chart', @kpiChartLines)
d3Utils.createLineChart('.active-classes-chart', @activeClassesChartLines)
d3Utils.createLineChart('.active-users-chart', @activeUsersChartLines)
d3Utils.createLineChart('.paid-courses-chart', @enrollmentsChartLines)
d3Utils.createLineChart('.recurring-revenue-chart', @revenueChartLines)
updateAllKPIChartData: ->
@kpiRecentChartLines = []
@kpiChartLines = []
@updateKPIChartData(60, @kpiRecentChartLines)
@updateKPIChartData(300, @kpiChartLines)
@updateKPIChartData(365, @kpiChartLines)
updateKPIChartData: (timeframeDays, chartLines) ->
days = d3Utils.createContiguousDays(timeframeDays)
@ -359,6 +420,90 @@ module.exports = class AnalyticsView extends RootView
max: _.max(dausmausPoints, 'y').y
showYScale: true
updateEnrollmentsChartData: ->
@enrollmentsChartLines = []
return unless @paidCourseTotalEnrollments?.length and @trialCourseTotalEnrollments?.length
days = d3Utils.createContiguousDays(90, false)
@enrollmentDays = _.cloneDeep(days)
@enrollmentDays.reverse()
colorIndex = 0
dailyMax = 0
data = []
total = 0
for entry in @paidCourseTotalEnrollments
total += entry.count
data.push
day: entry.day
value: total
points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push
points: points
description: 'Total paid enrollments issued'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: true
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
data = []
total = 0
for entry in @paidCourseRedeemedEnrollments
total += entry.count
data.push
day: entry.day
value: total
points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push
points: points
description: 'Total paid enrollments redeemed'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: false
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
data = []
total = 0
for entry in @trialCourseTotalEnrollments
total += entry.count
data.push
day: entry.day
value: total
points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push
points: points
description: 'Total trial enrollments issued'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: false
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
data = []
total = 0
for entry in @trialCourseRedeemedEnrollments
total += entry.count
data.push
day: entry.day
value: total
points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push
points: points
description: 'Total trial enrollments redeemed'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
max: _.max(points, 'y').y
showYScale: false
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
line.max = dailyMax for line in @enrollmentsChartLines
updateRevenueChartData: ->
@revenueChartLines = []
return unless @revenue?.length
@ -371,7 +516,6 @@ module.exports = class AnalyticsView extends RootView
groupDayMap[@revenueGroups[i]][entry.day] ?= 0
groupDayMap[@revenueGroups[i]][entry.day] += count
lines = []
colorIndex = 0
dailyMax = 0
for group, entries of groupDayMap

View file

@ -26,12 +26,25 @@ PrepaidHandler = class PrepaidHandler extends Handler
getByRelationship: (req, res, args...) ->
relationship = args[1]
return @getCoursePrepaidsAPI(req, res) if relationship is 'courses'
return @getPrepaidAPI(req, res, args[2]) if relationship is 'code'
return @createPrepaidAPI(req, res) if relationship is 'create'
return @purchasePrepaidAPI(req, res) if relationship is 'purchase'
return @postRedeemerAPI(req, res, args[0]) if relationship is 'redeemers'
super arguments...
getCoursePrepaidsAPI: (req, res, code) ->
return @sendSuccess(res, []) unless req.user?.isAdmin()
query = {$and: [
{type: 'course'},
{maxRedeemers: {$ne: "9999"}},
{'properties.courseIDs': {$exists: false}},
{_id: {$gt: cutoffID}}
]}
Prepaid.find(query, req.body.project).exec (err, documents) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, documents)
getPrepaidAPI: (req, res, code) ->
return @sendForbiddenError(res) unless req.user?
return @sendNotFoundError(res, "You must specify a code") unless code