mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
Add enrollments graph to analytics dashboard
This commit is contained in:
parent
378ddccce7
commit
bd9ca8ee5f
4 changed files with 194 additions and 9 deletions
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue