Merge branch 'master' into production

This commit is contained in:
Nick Winter 2016-07-11 10:44:13 -07:00
commit dbc99ba52b
8 changed files with 275 additions and 251 deletions

View file

@ -267,7 +267,7 @@ LevelSchema = c.object {
victory: {}
type: 'hero'
goals: [
{id: 'ogres-die', name: 'Ogres must die.', killThangs: ['ogres'], worldEndsAfter: 3}
{id: 'ogres-die', name: 'Defeat the ogres.', killThangs: ['ogres'], worldEndsAfter: 3}
{id: 'humans-survive', name: 'Your hero must survive.', saveThangs: ['Hero Placeholder'], howMany: 1, worldEndsAfter: 3, hiddenGoal: true}
]
concepts: ['basic_syntax']

View file

@ -18,7 +18,7 @@ block content
.col-md-5.big-stat.classroom-active-users
div.description Classroom Monthly Active Users
if view.activeUsers.length > 0
- var classroomBigMAU = 0;
- var classroomBigMAU = 0;
each count, event in view.activeUsers[0].events
if event.indexOf('MAU classroom') >= 0
- classroomBigMAU += count;
@ -26,24 +26,24 @@ block content
.col-md-5.big-stat.campaign-active-users
div.description Campaign Monthly Active Users
if view.activeUsers.length > 0
- var campaignBigMAU = 0;
- var campaignBigMAU = 0;
each count, event in view.activeUsers[0].events
if event.indexOf('MAU campaign') >= 0
- campaignBigMAU += count;
div.count= campaignBigMAU
ul.nav.nav-tabs
li.active
li.active
a(data-target="#tab_kpis", data-toggle="tab") KPIs
li
li
a(data-target="#tab_active_classes", data-toggle="tab") Active Classes
li
li
a(data-target="#tab_revenue", data-toggle="tab") Revenue
li
li
a(data-target="#tab_classroom", data-toggle="tab") Classroom
li
li
a(data-target="#tab_campaign", data-toggle="tab") Campaign
li
li
a(data-target="#tab_campaign_vs_classroom", data-toggle="tab") Campaign vs Classroom
.tab-content
@ -165,7 +165,7 @@ block content
.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
.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
@ -267,9 +267,9 @@ block content
h1 Active Users
if view.activeUsers.length > 0
- var eventNames = [];
each count, event in view.activeUsers[0].events
each event in view.activeUserEventNames
if event.indexOf('classroom') >= 0
- eventNames.push(event)
- eventNames.push(event);
- eventNames.sort(function (a, b) {return a.localeCompare(b);});
table.table.table-striped.table-condensed
tr
@ -293,7 +293,7 @@ block content
each day in view.enrollmentDays
tr
td= day
if view.dayEnrollmentsMap[day]
if view.dayEnrollmentsMap[day]
td= view.dayEnrollmentsMap[day].paidIssued || 0
td= view.dayEnrollmentsMap[day].paidRedeemed || 0
td= view.dayEnrollmentsMap[day].trialIssued || 0
@ -322,9 +322,9 @@ block content
h1 Active Users
if view.activeUsers.length > 0
- var eventNames = [];
each count, event in view.activeUsers[0].events
each event in view.activeUserEventNames
if event.indexOf('campaign') >= 0
- eventNames.push(event)
- eventNames.push(event);
- eventNames.sort(function (a, b) {return a.localeCompare(b);});
table.table.table-striped.table-condensed
tr
@ -346,27 +346,13 @@ block content
h1 Active Users
if view.activeUsers.length > 0
- var eventNames = [];
each count, event in view.activeUsers[0].events
- eventNames.push(event)
- eventNames.sort(function (a, b) {
- if (a.indexOf('campaign') == b.indexOf('campaign') || a.indexOf('classroom') == b.indexOf('classroom')) {
- return a.localeCompare(b);
- }
- else if (a.indexOf('campaign') > b.indexOf('campaign')) {
- return 1;
- }
- else {
- return -1;
- }
- });
table.table.table-striped.table-condensed
tr
th(style='min-width:85px;') Day
each eventName in eventNames
each eventName in view.activeUserEventNames
th= eventName
each activeUser in view.activeUsers
tr
td= activeUser.day
each eventName in eventNames
each eventName in view.activeUserEventNames
td= activeUser.events[eventName] || 0

View file

@ -73,6 +73,7 @@ module.exports = class AnalyticsView extends RootView
# Add campaign/classroom DAU 30-day averages and daily totals
campaignDauTotals = []
classroomDauTotals = []
eventMap = {}
for entry in @activeUsers
day = entry.day
campaignDauTotal = 0
@ -82,18 +83,31 @@ module.exports = class AnalyticsView extends RootView
campaignDauTotal += count
else if event.indexOf('DAU classroom') >= 0
classroomDauTotal += count
eventMap[event] = true;
entry.events['DAU campaign total'] = campaignDauTotal
eventMap['DAU campaign total'] = true;
campaignDauTotals.unshift(campaignDauTotal)
campaignDauTotals.pop() while campaignDauTotals.length > 30
if campaignDauTotals.length is 30
entry.events['DAU campaign 30-day average'] = Math.round(_.reduce(campaignDauTotals, (a, b) -> a + b) / 30)
eventMap['DAU campaign 30-day average'] = true;
entry.events['DAU classroom total'] = classroomDauTotal
eventMap['DAU classroom total'] = true;
classroomDauTotals.unshift(classroomDauTotal)
classroomDauTotals.pop() while classroomDauTotals.length > 30
if classroomDauTotals.length is 30
entry.events['DAU classroom 30-day average'] = Math.round(_.reduce(classroomDauTotals, (a, b) -> a + b) / 30)
eventMap['DAU classroom 30-day average'] = true;
@activeUsers.sort (a, b) -> b.day.localeCompare(a.day)
@activeUserEventNames = Object.keys(eventMap)
@activeUserEventNames.sort (a, b) ->
if a.indexOf('campaign') is b.indexOf('campaign') or a.indexOf('classroom') is b.indexOf('classroom')
a.localeCompare(b)
else if a.indexOf('campaign') > b.indexOf('campaign')
1
else
-1
@updateAllKPIChartData()
@updateActiveUsersChartData()
@ -134,13 +148,13 @@ module.exports = class AnalyticsView extends RootView
return unless @revenue.length > 0
# Add monthly recurring revenue values
# For each daily group, add up monthly values walking forward through time, and add to revenue groups
monthlyDailyGroupMap = {}
dailyGroupIndexMap = {}
for group, i in @revenueGroups
monthlyDailyGroupMap[group.replace('DRR', 'MRR')] = group
dailyGroupIndexMap[group] = i
dailyGroupIndexMap[group] = i
for monthlyGroup, dailyGroup of monthlyDailyGroupMap
monthlyValues = []
for i in [@revenue.length-1..0]
@ -183,7 +197,7 @@ module.exports = class AnalyticsView extends RootView
@supermodel.addRequestResource({
url: '/db/prepaid/-/courses'
method: 'POST'
data: {project: {maxRedeemers: 1, properties: 1, redeemers: 1}}
data: {project: {endDate: 1, maxRedeemers: 1, properties: 1, redeemers: 1}}
success: (prepaids) =>
paidDayMaxMap = {}
paidDayRedeemedMap = {}
@ -201,14 +215,13 @@ module.exports = class AnalyticsView extends RootView
redeemDay = redeemer.date.substring(0, 10)
trialDayRedeemedMap[redeemDay] ?= 0
trialDayRedeemedMap[redeemDay]++
else
else if not prepaid.endDate? or new Date(prepaid.endDate) > new Date()
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
@ -218,7 +231,7 @@ module.exports = class AnalyticsView extends RootView
@paidCourseTotalEnrollments.sort (a, b) -> a.day.localeCompare(b.day)
@paidCourseRedeemedEnrollments = []
for day, count of paidDayRedeemedMap
@paidCourseRedeemedEnrollments.push({day: day, count: count})
@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)
@ -239,7 +252,7 @@ module.exports = class AnalyticsView extends RootView
}, 0).load()
@courses = new CocoCollection([], { url: "/db/course", model: Course})
@courses.comparator = "_id"
@courses.comparator = "_id"
@listenToOnce @courses, 'sync', @onCoursesSync
@supermodel.loadCollection(@courses)
@ -276,7 +289,7 @@ module.exports = class AnalyticsView extends RootView
studentFurthestCourseMap = {}
studentPaidStatusMap = {}
for courseInstance in data.courseInstances
continue if utils.objectIdToDate(courseInstance._id) < startDate
continue if utils.objectIdToDate(courseInstance._id) < startDate
courseID = courseInstance.courseID
teacherID = courseInstance.ownerID
for studentID in courseInstance.members
@ -306,7 +319,7 @@ module.exports = class AnalyticsView extends RootView
# 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
# Teacher furthest course is furthest course of highest paid status student
teacherFurthestCourseMap = {}
teacherPaidStatusMap = {}
for teacher, students of teacherStudentsMap
@ -368,14 +381,17 @@ module.exports = class AnalyticsView extends RootView
# Trim points preceding days
if points.length and days.length and points[0].day.localeCompare(days[0]) < 0
for point, i in points
if point.day.localeCompare(days[0]) >= 0
points.splice(0, i)
break
if points[points.length - 1].day.localeCompare(days[0]) < 0
points = []
else
for point, i in points
if point.day.localeCompare(days[0]) >= 0
points.splice(0, i)
break
# Ensure points for each day
for day, i in days
if points.length <= i or points[i].day isnt day
if points.length <= i or points[i]?.day isnt day
prevY = if i > 0 then points[i - 1].y else 0.0
points.splice i, 0,
day: day
@ -534,7 +550,7 @@ module.exports = class AnalyticsView extends RootView
day = entry.day
for event, count of entry.events
eventDataMap[event] ?= []
eventDataMap[event].push
eventDataMap[event].push
day: entry.day
value: count
@ -550,7 +566,7 @@ module.exports = class AnalyticsView extends RootView
lines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: showYScale
@ -577,7 +593,7 @@ module.exports = class AnalyticsView extends RootView
day = entry.day
for event, count of entry.events
eventDataMap[event] ?= []
eventDataMap[event].push
eventDataMap[event].push
day: entry.day
value: count
@ -591,7 +607,7 @@ module.exports = class AnalyticsView extends RootView
@campaignVsClassroomMonthlyActiveUsersRecentChartLines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: true
@ -601,7 +617,7 @@ module.exports = class AnalyticsView extends RootView
@campaignVsClassroomMonthlyActiveUsersRecentChartLines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: false
@ -619,7 +635,7 @@ module.exports = class AnalyticsView extends RootView
@campaignVsClassroomMonthlyActiveUsersChartLines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: true
@ -629,7 +645,7 @@ module.exports = class AnalyticsView extends RootView
@campaignVsClassroomMonthlyActiveUsersChartLines.push
points: points
description: event
lineColor: @lineColors[colorIndex++ % @lineColors.length]
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
showYScale: false
@ -648,16 +664,14 @@ module.exports = class AnalyticsView extends RootView
dailyMax = 0
data = []
total = 0
for entry in @paidCourseTotalEnrollments
total += entry.count
data.push
day: entry.day
value: total
value: entry.count
points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push
points: points
description: 'Total paid enrollments issued'
description: 'Paid enrollments issued'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
@ -666,16 +680,14 @@ module.exports = class AnalyticsView extends RootView
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
data = []
total = 0
for entry in @paidCourseRedeemedEnrollments
total += entry.count
data.push
day: entry.day
value: total
value: entry.count
points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push
points: points
description: 'Total paid enrollments redeemed'
description: 'Paid enrollments redeemed'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
@ -684,16 +696,14 @@ module.exports = class AnalyticsView extends RootView
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)
value: entry.count
points = @createLineChartPoints(days, data, true)
@enrollmentsChartLines.push
points: points
description: 'Total trial enrollments issued'
description: 'Trial enrollments issued'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0
@ -702,16 +712,14 @@ module.exports = class AnalyticsView extends RootView
dailyMax = _.max([dailyMax, _.max(points, 'y').y])
data = []
total = 0
for entry in @trialCourseRedeemedEnrollments
total += entry.count
data.push
day: entry.day
value: total
value: entry.count
points = @createLineChartPoints(days, data)
@enrollmentsChartLines.push
points: points
description: 'Total trial enrollments redeemed'
description: 'Trial enrollments redeemed'
lineColor: @lineColors[colorIndex++ % @lineColors.length]
strokeWidth: 1
min: 0

View file

@ -31,31 +31,10 @@ module.exports = class MainAdminView extends RootView
getTitle: -> return $.i18n.t('account_settings.admin')
initialize: ->
@campaigns = new Campaigns()
@courses = new CocoCollection([], { url: "/db/course", model: Course})
if window.amActually
@amActually = new User({_id: window.amActually})
@amActually.fetch()
@supermodel.trackModel(@amActually)
if me.isAdmin()
@supermodel.trackRequest @campaigns.fetchByType('course', { data: { project: 'levels' } })
@supermodel.loadCollection(@courses, 'courses')
super()
onLoaded: ->
campaignCourseIndexMap = {}
for course, index in @courses.models
campaignCourseIndexMap[course.get('campaignID')] = index + 1
@courseLevels = []
for campaign in @campaigns.models
continue unless campaignCourseIndexMap[campaign.id]
for levelID, level of campaign.get('levels')
@courseLevels.push({
levelID
slug: level.slug
courseIndex: campaignCourseIndexMap[campaign.id]
})
super()
onClickStopSpyingButton: ->
@ -157,42 +136,31 @@ module.exports = class MainAdminView extends RootView
@supermodel.addRequestResource('create_prepaid', options, 0).load()
onClickExportProgress: ->
return unless @courseLevels?.length > 0
$('.classroom-progress-csv').prop('disabled', true)
classCode = $('.classroom-progress-class-code').val()
classroom = null
courseLevels = []
sessions = null
users = null
userMap = {}
new Promise((resolve, reject) =>
new Classroom().fetchByCode(classCode, {
success: resolve
error: (model, response, options) => reject(response)
})
)
.then (classroom) =>
new Promise((resolve, reject) =>
new Classroom({ _id: classroom.id }).fetch({
success: resolve
error: (model, response, options) => reject(response)
})
)
.then (classroom) =>
new Promise((resolve, reject) =>
new Users().fetchForClassroom(classroom, {
success: (models, response, options) =>
resolve([classroom, models]) if models?.loaded
error: (models, response, options) => reject(response)
})
)
.then ([classroom, users]) =>
Promise.resolve(new Classroom().fetchByCode(classCode))
.then (model) =>
classroom = new Classroom({ _id: model.data._id })
Promise.resolve(classroom.fetch())
.then (model) =>
for course, index in classroom.get('courses')
for level in course.levels
courseLevels.push
courseIndex: index + 1
levelID: level.original
slug: level.slug
users = new Users()
Promise.resolve($.when(users.fetchForClassroom(classroom)...))
.then (models) =>
userMap[user.id] = user for user in users.models
new Promise((resolve, reject) =>
new LevelSessions().fetchForAllClassroomMembers(classroom, {
success: (models, response, options) =>
resolve(models) if models?.loaded
error: (models, response, options) => reject(response)
})
)
.then (sessions) =>
sessions = new LevelSessions()
Promise.resolve($.when(sessions.fetchForAllClassroomMembers(classroom)...))
.then (models) =>
userLevelPlaytimeMap = {}
for session in sessions.models
continue unless session.get('state')?.complete
@ -205,7 +173,7 @@ module.exports = class MainAdminView extends RootView
userPlaytimes = []
for userID, user of userMap
playtimes = [user.get('name') ? 'Anonymous']
for level in @courseLevels
for level in courseLevels
if userLevelPlaytimeMap[userID]?[level.levelID]?
rawSeconds = parseInt(userLevelPlaytimeMap[userID][level.levelID])
hours = Math.floor(rawSeconds / 60 / 60)
@ -222,7 +190,7 @@ module.exports = class MainAdminView extends RootView
columnLabels = "Username"
currentLevel = 1
lastCourseIndex = 1
for level in @courseLevels
for level in courseLevels
unless level.courseIndex is lastCourseIndex
currentLevel = 1
lastCourseIndex = level.courseIndex
@ -238,3 +206,4 @@ module.exports = class MainAdminView extends RootView
.catch (error) ->
$('.classroom-progress-csv').prop('disabled', false)
console.error error
throw error

View file

@ -11,7 +11,7 @@ module.exports = class SegmentCheckView extends CocoView
events:
'click .back-to-account-type': -> @trigger 'nav-back'
'input .class-code-input': 'onInputClassCode'
'input .birthday-form-group': 'onInputBirthday'
'change .birthday-form-group': 'onInputBirthday'
'submit form.segment-check': 'onSubmitSegmentCheck'
'click .individual-path-button': -> @trigger 'choose-path', 'individual'

View file

@ -275,78 +275,77 @@ module.exports = class SpellView extends CocoView
e.editor.execCommand 'gotolineend'
return true
if me.level() < 20 or aceConfig.indentGuides
# Add visual ident guides
language = @spell.language
ensureLineStartsBlock = (line) ->
return false unless language is "python"
match = /^\s*([^#]+)/.exec(line)
return false if not match?
return /:\s*$/.test(match[1])
# Add visual indent guides
language = @spell.language
ensureLineStartsBlock = (line) ->
return false unless language is "python"
match = /^\s*([^#]+)/.exec(line)
return false if not match?
return /:\s*$/.test(match[1])
@aceSession.addDynamicMarker
update: (html, markerLayer, session, config) =>
Range = ace.require('ace/range').Range
@aceSession.addDynamicMarker
update: (html, markerLayer, session, config) =>
Range = ace.require('ace/range').Range
foldWidgets = @aceSession.foldWidgets
return if not foldWidgets?
foldWidgets = @aceSession.foldWidgets
return if not foldWidgets?
lines = @aceDoc.getAllLines()
startOfRow = (r) ->
str = lines[r]
ar = str.match(/^\s*/)
ar.pop().length
lines = @aceDoc.getAllLines()
startOfRow = (r) ->
str = lines[r]
ar = str.match(/^\s*/)
ar.pop().length
colors = [{border: '74,144,226', fill: '108,162,226'}, {border: '132,180,235', fill: '230,237,245'}]
colors = [{border: '74,144,226', fill: '108,162,226'}, {border: '132,180,235', fill: '230,237,245'}]
for row in [0..@aceSession.getLength()]
foldWidgets[row] = @aceSession.getFoldWidget(row) unless foldWidgets[row]?
continue unless foldWidgets? and foldWidgets[row] is "start"
try
docRange = @aceSession.getFoldWidgetRange(row)
catch error
console.warn "Couldn't find fold widget docRange for row #{row}:", error
if not docRange?
guess = startOfRow(row)
docRange = new Range(row,guess,row,guess+4)
for row in [0..@aceSession.getLength()]
foldWidgets[row] = @aceSession.getFoldWidget(row) unless foldWidgets[row]?
continue unless foldWidgets? and foldWidgets[row] is "start"
try
docRange = @aceSession.getFoldWidgetRange(row)
catch error
console.warn "Couldn't find fold widget docRange for row #{row}:", error
if not docRange?
guess = startOfRow(row)
docRange = new Range(row,guess,row,guess+4)
continue unless ensureLineStartsBlock(lines[row])
continue unless ensureLineStartsBlock(lines[row])
if /^\s+$/.test lines[docRange.end.row+1]
docRange.end.row += 1
if /^\s+$/.test lines[docRange.end.row+1]
docRange.end.row += 1
xstart = startOfRow(row)
if language is 'python'
requiredIndent = new RegExp '^' + new Array(Math.floor(xstart / 4 + 1)).join('( |\t)') + '( |\t)+(\\S|\\s*$)'
for crow in [docRange.start.row+1..docRange.end.row]
unless requiredIndent.test lines[crow]
docRange.end.row = crow - 1
break
xstart = startOfRow(row)
if language is 'python'
requiredIndent = new RegExp '^' + new Array(Math.floor(xstart / 4 + 1)).join('( |\t)') + '( |\t)+(\\S|\\s*$)'
for crow in [docRange.start.row+1..docRange.end.row]
unless requiredIndent.test lines[crow]
docRange.end.row = crow - 1
break
rstart = @aceSession.documentToScreenPosition docRange.start.row, docRange.start.column
rend = @aceSession.documentToScreenPosition docRange.end.row, docRange.end.column
range = new Range rstart.row, rstart.column, rend.row, rend.column
level = Math.floor(xstart / 4)
color = colors[level % colors.length]
bw = 3
to = markerLayer.$getTop(range.start.row, config)
t = markerLayer.$getTop(range.start.row + 1, config)
h = config.lineHeight * (range.end.row - range.start.row)
l = markerLayer.$padding + xstart * config.characterWidth
# w = (data.i - data.b) * config.characterWidth
w = 4 * config.characterWidth
fw = config.characterWidth * ( @aceSession.getScreenLastRowColumn(range.start.row) - xstart )
rstart = @aceSession.documentToScreenPosition docRange.start.row, docRange.start.column
rend = @aceSession.documentToScreenPosition docRange.end.row, docRange.end.column
range = new Range rstart.row, rstart.column, rend.row, rend.column
level = Math.floor(xstart / 4)
color = colors[level % colors.length]
bw = 3
to = markerLayer.$getTop(range.start.row, config)
t = markerLayer.$getTop(range.start.row + 1, config)
h = config.lineHeight * (range.end.row - range.start.row)
l = markerLayer.$padding + xstart * config.characterWidth
# w = (data.i - data.b) * config.characterWidth
w = 4 * config.characterWidth
fw = config.characterWidth * ( @aceSession.getScreenLastRowColumn(range.start.row) - xstart )
html.push """
<div style=
"position: absolute; top: #{to}px; left: #{l}px; width: #{fw+bw}px; height: #{config.lineHeight}px;
border: #{bw}px solid rgba(#{color.border},1); border-left: none;"
></div>
<div style=
"position: absolute; top: #{t}px; left: #{l}px; width: #{w}px; height: #{h}px; background-color: rgba(#{color.fill},0.5);
border-right: #{bw}px solid rgba(#{color.border},1); border-bottom: #{bw}px solid rgba(#{color.border},1);"
></div>
"""
html.push """
<div style=
"position: absolute; top: #{to}px; left: #{l}px; width: #{fw+bw}px; height: #{config.lineHeight}px;
border: #{bw}px solid rgba(#{color.border},1); border-left: none;"
></div>
<div style=
"position: absolute; top: #{t}px; left: #{l}px; width: #{w}px; height: #{h}px; background-color: rgba(#{color.fill},0.5);
border-right: #{bw}px solid rgba(#{color.border},1); border-bottom: #{bw}px solid rgba(#{color.border},1);"
></div>
"""
fillACE: ->
@ace.setValue @spell.source

View file

@ -6,51 +6,49 @@
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
try {
var logDB = new Mongo("localhost").getDB("analytics")
var scriptStartTime = new Date();
var analyticsStringCache = {};
// TODO: Does not handle course prepaid updates on a user
// TODO: Does not handle class membership changes
var minClassSize = 12;
var minActiveCount = 6;
// TODO: Investigate abrupt trial drop off at 4/1/16. Showed up when fixing coursePrepaid.
var eventNamePaid = 'Active classes paid';
var eventNameTrial = 'Active classes trial';
var eventNameFree = 'Active classes free';
var analyticsDB = new Mongo("localhost").getDB("analytics")
var scriptStartTime = new Date();
var analyticsStringCache = {};
var numDays = 40;
var daysInMonth = 30;
var minClassSize = 12;
var minActiveCount = 6;
var startDay = new Date();
var today = startDay.toISOString().substr(0, 10);
startDay.setUTCDate(startDay.getUTCDate() - numDays);
startDay = startDay.toISOString().substr(0, 10);
var eventNamePaid = 'Active classes paid';
var eventNameTrial = 'Active classes trial';
var eventNameFree = 'Active classes free';
log("Today is " + today);
log("Start day is " + startDay);
var numDays = 40;
var daysInMonth = 30;
log("Getting active class counts..");
var activeClassCounts = getActiveClassCounts(startDay);
// printjson(activeClassCounts);
log("Inserting active class counts..");
for (var event in activeClassCounts) {
for (var day in activeClassCounts[event]) {
if (today === day) continue; // Never save data for today because it's incomplete
// print(event, day, activeClassCounts[event][day]);
insertEventCount(event, day, activeClassCounts[event][day]);
}
var startDay = new Date();
var today = startDay.toISOString().substr(0, 10);
startDay.setUTCDate(startDay.getUTCDate() - numDays);
startDay = startDay.toISOString().substr(0, 10);
log("Today is " + today);
log("Start day is " + startDay);
log("Getting active class counts..");
var activeClassCounts = getActiveClassCounts(startDay);
// printjson(activeClassCounts);
// log("Inserting active class counts..");
for (var event in activeClassCounts) {
for (var day in activeClassCounts[event]) {
if (today === day) continue; // Never save data for today because it's incomplete
// print(event, day, activeClassCounts[event][day]);
insertEventCount(event, day, activeClassCounts[event][day]);
}
}
log("Script runtime: " + (new Date() - scriptStartTime));
}
catch(err) {
log("ERROR: " + err);
printjson(err);
}
log("Script runtime: " + (new Date() - scriptStartTime));
function getActiveClassCounts(startDay) {
// Tally active classes per day, for paid, trial, and free
// TODO: does not handle class membership changes
if (!startDay) return {};
@ -60,7 +58,7 @@ function getActiveClassCounts(startDay) {
// paid: at least one paid member
// trial: not paid, at least one trial member
// free: not paid, not free trial
// user.coursePrepaidID set means access to paid courses
// user.coursePrepaidID or user.coursePrepaid set means access to paid courses
// prepaid.properties.trialRequestID means access was via trial
// Find classroom users
@ -86,30 +84,46 @@ function getActiveClassCounts(startDay) {
}
}
}
// log("DEBUG: Classroom users: " + classroomUserIDs.length);
log("Find user types..");
var userEventMap = {};
var userEventEndDateMap = {};
var prepaidUsersMap = {};
var prepaidIDs = [];
cursor = db.users.find({_id: {$in: classroomUserObjectIds}}, {coursePrepaidID: 1});
cursor = db.users.find({_id: {$in: classroomUserObjectIds}}, {coursePrepaid: 1, coursePrepaidID: 1});
while (cursor.hasNext()) {
doc = cursor.next();
userEventEndDateMap[doc._id.valueOf()] = {};
userEventEndDateMap[doc._id.valueOf()][eventNameFree] = new Date();
if (doc.coursePrepaid) {
if (!doc.coursePrepaid.endDate) throw new Error("No endDate for new prepaid " + doc._id.valuOf());
userEventEndDateMap[doc._id.valueOf()][eventNamePaid] = new Date(doc.coursePrepaid.endDate);
if (!prepaidUsersMap[doc.coursePrepaid._id.valueOf()]) prepaidUsersMap[doc.coursePrepaid._id.valueOf()] = [];
prepaidUsersMap[doc.coursePrepaid._id.valueOf()].push(doc._id.valueOf());
prepaidIDs.push(doc.coursePrepaid._id);
}
if (doc.coursePrepaidID) {
userEventMap[doc._id.valueOf()] = eventNamePaid;
if (!userEventEndDateMap[doc._id.valueOf()][eventNamePaid]) {
userEventEndDateMap[doc._id.valueOf()][eventNamePaid] = new Date();
}
if (!prepaidUsersMap[doc.coursePrepaidID.valueOf()]) prepaidUsersMap[doc.coursePrepaidID.valueOf()] = [];
prepaidUsersMap[doc.coursePrepaidID.valueOf()].push(doc._id.valueOf());
prepaidIDs.push(doc.coursePrepaidID);
}
else {
userEventMap[doc._id.valueOf()] = eventNameFree;
}
}
cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {properties: 1});
cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {endDate: 1, properties: 1});
while (cursor.hasNext()) {
doc = cursor.next();
if (doc.properties && doc.properties.trialRequestID) {
var endDate = new Date();
if (doc.endDate) {
endDate = new Date(doc.endDate);
}
else if (doc.properties.endDate) {
endDate = new Date(doc.properties.endDate);
}
for (var i = 0; i < prepaidUsersMap[doc._id.valueOf()].length; i++) {
userEventMap[prepaidUsersMap[doc._id.valueOf()][i]] = eventNameTrial;
userEventEndDateMap[prepaidUsersMap[doc._id.valueOf()][i]][eventNameTrial] = endDate;
}
}
}
@ -121,18 +135,22 @@ function getActiveClassCounts(startDay) {
var endDate = ISODate(startDay + "T00:00:00.000Z");
var todayDate = new Date(new Date().toISOString().substring(0, 10));
var startObj = objectIdWithTimestamp(startDate);
var queryParams = {$and: [
{_id: {$gte: startObj}},
{user: {$in: classroomUserIDs}},
{event: 'Started Level'}
]};
cursor = logDB['log'].find(queryParams, {user: 1});
while (cursor.hasNext()) {
doc = cursor.next();
if (!userPlayedMap[doc.user]) userPlayedMap[doc.user] = [];
userPlayedMap[doc.user].push(doc._id.getTimestamp());
// Batch size test times: 10k 427005, 5k 361361, 1k 799068, 2k 791521
var batchSize = 5000;
for (var j = 0; j < classroomUserIDs.length / batchSize + 1; j++) {
// log("DEBUG: Fetching classroom events batch " + (j * batchSize) + " " + (j * batchSize + batchSize));
var queryParams = {$and: [
{_id: {$gte: startObj}},
{user: {$in: classroomUserIDs.slice(j * batchSize, j * batchSize + batchSize)}},
{event: 'Started Level'}
]};
cursor = analyticsDB['log'].find(queryParams, {user: 1});
while (cursor.hasNext()) {
doc = cursor.next();
if (!userPlayedMap[doc.user]) userPlayedMap[doc.user] = [];
userPlayedMap[doc.user].push(doc._id.getTimestamp());
}
}
// printjson(userPlayedMap);
log("Calculate number of active members per classroom per day per event type..");
var classDayTypeMap = {};
@ -159,7 +177,19 @@ function getActiveClassCounts(startDay) {
if (userPlayedMap[member]) {
for (var k = 0; k < userPlayedMap[member].length; k++) {
if (userPlayedMap[member][k] > startDate && userPlayedMap[member][k] <= endDate) {
classDayTypeMap[classroom][endDay][userEventMap[member]]++;
if (userEventEndDateMap[member][eventNameTrial] > endDate) {
classDayTypeMap[classroom][endDay][eventNameTrial]++;
}
else if (userEventEndDateMap[member][eventNamePaid] > endDate) {
classDayTypeMap[classroom][endDay][eventNamePaid]++;
}
else if (userEventEndDateMap[member][eventNameFree] > endDate) {
classDayTypeMap[classroom][endDay][eventNameFree]++;
}
else {
print("ERROR: no event for " + member);
printjson(userEventEndDateMap[member]);
}
break;
}
}

View file

@ -8,6 +8,8 @@
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
// TODO: classroom paid active users before 4/13/16 not correct
try {
var logDB = new Mongo("localhost").getDB("analytics")
var scriptStartTime = new Date();
@ -129,32 +131,47 @@ function getActiveUserCounts(startDay, endDay, activeUserEvents) {
log("Classroom user count: " + classroomUserObjectIds.length);
// Classrooms free/trial/paid
// Paid user: user.coursePrepaidID set means access to paid courses
// Paid user: user.coursePrepaid or user.coursePrepaidID set means access to paid courses
// Trial user: prepaid.properties.trialRequestID means access was via trial
// Free: not paid, not trial
log("Finding classroom users free/trial/paid status..");
var classroomUserEventMap = {};
var classroomUserEventEndDateMap = {};
var prepaidUsersMap = {};
var prepaidIDs = [];
cursor = db.users.find({_id: {$in: classroomUserObjectIds}}, {coursePrepaidID: 1});
cursor = db.users.find({_id: {$in: classroomUserObjectIds}}, {coursePrepaid: 1, coursePrepaidID: 1});
while (cursor.hasNext()) {
doc = cursor.next();
classroomUserEventEndDateMap[doc._id.valueOf()] = {};
classroomUserEventEndDateMap[doc._id.valueOf()]['DAU classroom free'] = new Date();
if (doc.coursePrepaid) {
if (!doc.coursePrepaid.endDate) throw new Error("No endDate for new prepaid " + doc._id.valuOf());
classroomUserEventEndDateMap[doc._id.valueOf()]['DAU classroom paid'] = new Date(doc.coursePrepaid.endDate);
if (!prepaidUsersMap[doc.coursePrepaid._id.valueOf()]) prepaidUsersMap[doc.coursePrepaid._id.valueOf()] = [];
prepaidUsersMap[doc.coursePrepaid._id.valueOf()].push(doc._id.valueOf());
prepaidIDs.push(doc.coursePrepaid._id);
}
if (doc.coursePrepaidID) {
classroomUserEventMap[doc._id.valueOf()] = 'DAU classroom paid';
if (!classroomUserEventEndDateMap[doc._id.valueOf()]['DAU classroom paid']) {
classroomUserEventEndDateMap[doc._id.valueOf()]['DAU classroom paid'] = new Date();
}
if (!prepaidUsersMap[doc.coursePrepaidID.valueOf()]) prepaidUsersMap[doc.coursePrepaidID.valueOf()] = [];
prepaidUsersMap[doc.coursePrepaidID.valueOf()].push(doc._id.valueOf());
prepaidIDs.push(doc.coursePrepaidID);
}
else {
classroomUserEventMap[doc._id.valueOf()] = 'DAU classroom free';
}
}
cursor = db.prepaids.find({_id: {$in: prepaidIDs}}, {properties: 1});
while (cursor.hasNext()) {
doc = cursor.next();
if (doc.properties && doc.properties.trialRequestID) {
var endDate = new Date();
if (doc.endDate) {
endDate = new Date(doc.endDate);
}
else if (doc.properties.endDate) {
endDate = new Date(doc.properties.endDate);
}
for (var i = 0; i < prepaidUsersMap[doc._id.valueOf()].length; i++) {
classroomUserEventMap[prepaidUsersMap[doc._id.valueOf()][i]] = 'DAU classroom trial';
classroomUserEventEndDateMap[prepaidUsersMap[doc._id.valueOf()][i]]['DAU classroom trial'] = endDate;
}
}
}
@ -199,7 +216,22 @@ function getActiveUserCounts(startDay, endDay, activeUserEvents) {
var userDayEventMap = {}
for (day in dayUserActiveMap) {
for (var user in dayUserActiveMap[day]) {
var event = classroomUserEventMap[user] || (dayCampaignUserPaidMap[day] && dayCampaignUserPaidMap[day][user] ? 'DAU campaign paid' : 'DAU campaign free');
var event = null;
var endDate = new Date(day + "T00:00:00.000Z");
if (classroomUserEventEndDateMap[user]) {
if (classroomUserEventEndDateMap[user]['DAU classroom trial'] > endDate) {
event = 'DAU classroom trial';
}
else if (classroomUserEventEndDateMap[user]['DAU classroom paid'] > endDate) {
event = 'DAU classroom paid';
}
else if (classroomUserEventEndDateMap[user]['DAU classroom free'] > endDate) {
event = 'DAU classroom free';
}
}
if (!event) {
event = dayCampaignUserPaidMap[day] && dayCampaignUserPaidMap[day][user] ? 'DAU campaign paid' : 'DAU campaign free';
}
dailyEventNames[event] = true;
if (!activeUsersCounts[day]) activeUsersCounts[day] = {};
if (!activeUsersCounts[day][event]) activeUsersCounts[day][event] = 0;