mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-04-26 05:53:39 -04:00
Update campaign editor analytics
Add date range picker to level view.
This commit is contained in:
parent
e49c74259b
commit
3f40364616
3 changed files with 137 additions and 101 deletions
app
templates/editor/campaign
views/editor/campaign
|
@ -5,8 +5,15 @@
|
|||
a(href="/editor/level/#{level.get('slug')}", target="_blank") (edit)
|
||||
p= level.get('description')
|
||||
|
||||
if analytics.startDay && analytics.endDay
|
||||
.input-group.input-group-sm
|
||||
input.form-control#input-startday(type='text', style='width:100px;', value=analytics.startDay)
|
||||
input.form-control#input-endday(type='text', style='width:100px;', value=analytics.endDay)
|
||||
button.btn.btn-default.btn-sm#reload-button(style='margin-left:10px;') Reload
|
||||
h4 Completion Rates
|
||||
if levelCompletions
|
||||
if analytics.levelCompletions.loading
|
||||
div Loading...
|
||||
else
|
||||
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
|
||||
thead
|
||||
tr
|
||||
|
@ -14,48 +21,43 @@
|
|||
td Started
|
||||
td Finished
|
||||
td Completion %
|
||||
if levelHelps && levelHelps.length === levelCompletions.length
|
||||
if analytics.levelHelps.levels.length === analytics.levelCompletions.levels.length
|
||||
td Helps Clicked
|
||||
td Helps / Started
|
||||
td Help Videos
|
||||
td Videos / Started
|
||||
tbody
|
||||
- for (var i = 0; i < levelCompletions.length; i++)
|
||||
- for (var i = 0; i < analytics.levelCompletions.levels.length; i++)
|
||||
tr
|
||||
td= levelCompletions[i].created
|
||||
td= levelCompletions[i].started
|
||||
td= levelCompletions[i].finished
|
||||
td= levelCompletions[i].rate
|
||||
if levelHelps && levelHelps.length === levelCompletions.length
|
||||
td= levelHelps[i].alertHelps + levelHelps[i].paletteHelps
|
||||
td= ((levelHelps[i].alertHelps + levelHelps[i].paletteHelps) / levelCompletions[i].started).toFixed(2)
|
||||
td= levelHelps[i].videoStarts
|
||||
td= (levelHelps[i].videoStarts / levelCompletions[i].started).toFixed(2)
|
||||
else
|
||||
div Loading...
|
||||
td= analytics.levelCompletions.levels[i].created
|
||||
td= analytics.levelCompletions.levels[i].started
|
||||
td= analytics.levelCompletions.levels[i].finished
|
||||
td= analytics.levelCompletions.levels[i].rate
|
||||
if analytics.levelHelps.levels.length === analytics.levelCompletions.levels.length && analytics.levelCompletions.levels[i].created == analytics.levelHelps.levels[i].day
|
||||
td= analytics.levelHelps.levels[i].alertHelps + analytics.levelHelps.levels[i].paletteHelps
|
||||
td= ((analytics.levelHelps.levels[i].alertHelps + analytics.levelHelps.levels[i].paletteHelps) / analytics.levelCompletions.levels[i].started).toFixed(2)
|
||||
td= analytics.levelHelps.levels[i].videoStarts
|
||||
td= (analytics.levelHelps.levels[i].videoStarts / analytics.levelCompletions.levels[i].started).toFixed(2)
|
||||
|
||||
h4 Average Playtimes
|
||||
if levelPlaytimes
|
||||
if analytics.levelPlaytimes.loading
|
||||
div Loading...
|
||||
else
|
||||
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
|
||||
thead
|
||||
tr
|
||||
td Date
|
||||
td Average (s)
|
||||
tbody
|
||||
- for (var i = 0; i < levelPlaytimes.length; i++)
|
||||
- for (var i = 0; i < analytics.levelPlaytimes.levels.length; i++)
|
||||
tr
|
||||
td= levelPlaytimes[i].created
|
||||
td= levelPlaytimes[i].average.toFixed(2)
|
||||
else
|
||||
div Loading...
|
||||
td= analytics.levelPlaytimes.levels[i].created
|
||||
td= analytics.levelPlaytimes.levels[i].average.toFixed(2)
|
||||
|
||||
h4 Common Problems
|
||||
if commonProblems
|
||||
if commonProblems.startDay
|
||||
if commonProblems.endDay
|
||||
div(style='font-size:10pt') #{commonProblems.startDay} to #{commonProblems.endDay}
|
||||
else
|
||||
div(style='font-size:10pt') #{commonProblems.startDay} to today
|
||||
if analytics.commonProblems.loading
|
||||
div Loading...
|
||||
else
|
||||
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
|
||||
thead
|
||||
tr
|
||||
|
@ -64,18 +66,18 @@
|
|||
td Error Hint
|
||||
td Count
|
||||
tbody
|
||||
- for (var i = 0; i < commonProblems.length && i < 20; i++)
|
||||
- for (var i = 0; i < analytics.commonProblems.levels.length && i < 20; i++)
|
||||
tr
|
||||
td= commonProblems[i].language
|
||||
td= commonProblems[i].message
|
||||
td= commonProblems[i].hint
|
||||
td= commonProblems[i].count
|
||||
else
|
||||
div Loading...
|
||||
td= analytics.commonProblems.levels[i].language
|
||||
td= analytics.commonProblems.levels[i].message
|
||||
td= analytics.commonProblems.levels[i].hint
|
||||
td= analytics.commonProblems.levels[i].count
|
||||
|
||||
h4 Recent Sessions
|
||||
if recentSessions
|
||||
div(style='font-size:10pt') Latest #{recentSessions.length} sessions for this level
|
||||
if analytics.recentSessions.loading
|
||||
div Loading...
|
||||
else
|
||||
div(style='font-size:10pt') Latest #{analytics.recentSessions.levels.length} sessions for this level
|
||||
div(style='font-size:10pt') Double-click row to open player and session
|
||||
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
|
||||
thead
|
||||
|
@ -87,19 +89,17 @@
|
|||
td Complete
|
||||
td Changed
|
||||
tbody
|
||||
- for (var i = 0; i < recentSessions.length; i++)
|
||||
tr.recent-session(data-player-id=recentSessions[i].creator, data-session-id=recentSessions[i]._id)
|
||||
td= recentSessions[i]._id
|
||||
td= recentSessions[i].creatorName || recentSessions[i].creator
|
||||
td= recentSessions[i].codeLanguage
|
||||
td= recentSessions[i].playtime
|
||||
if recentSessions[i].state && recentSessions[i].state.complete
|
||||
td= recentSessions[i].state.complete
|
||||
- for (var i = 0; i < analytics.recentSessions.levels.length; i++)
|
||||
tr.recent-session(data-player-id=analytics.recentSessions.levels[i].creator, data-session-id=analytics.recentSessions.levels[i]._id)
|
||||
td= analytics.recentSessions.levels[i]._id
|
||||
td= analytics.recentSessions.levels[i].creatorName || analytics.recentSessions.levels[i].creator
|
||||
td= analytics.recentSessions.levels[i].codeLanguage
|
||||
td= analytics.recentSessions.levels[i].playtime
|
||||
if analytics.recentSessions.levels[i].state && analytics.recentSessions.levels[i].state.complete
|
||||
td= analytics.recentSessions.levels[i].state.complete
|
||||
else
|
||||
td false
|
||||
td= recentSessions[i].changed
|
||||
else
|
||||
div Loading...
|
||||
td= analytics.recentSessions.levels[i].changed
|
||||
|
||||
if level.get('tasks')
|
||||
.tasks
|
||||
|
|
|
@ -25,9 +25,9 @@ module.exports = class CampaignAnalyticsModal extends ModalView
|
|||
|
||||
getRenderData: ->
|
||||
c = super()
|
||||
c.campaignCompletions = @campaignCompletions
|
||||
c.showLeftGame = @showLeftGame
|
||||
c.showSubscriptions = @showSubscriptions
|
||||
c.campaignCompletions = @campaignCompletions
|
||||
c
|
||||
|
||||
afterRender: ->
|
||||
|
@ -124,13 +124,13 @@ module.exports = class CampaignAnalyticsModal extends ModalView
|
|||
|
||||
# Chain these together so we can calculate relative metrics (e.g. left game per second)
|
||||
@getCampaignLevelCompletions startDay, endDay, () =>
|
||||
@render()
|
||||
@render?()
|
||||
@getCompaignLevelDrops startDay, endDay, () =>
|
||||
@render()
|
||||
@render?()
|
||||
@getCampaignAveragePlaytimes startDayDashed, endDayDashed, () =>
|
||||
@render()
|
||||
@render?()
|
||||
@getCampaignLevelSubscriptions startDay, endDay, () =>
|
||||
@render()
|
||||
@render?()
|
||||
|
||||
getCampaignAveragePlaytimes: (startDay, endDay, doneCallback) =>
|
||||
# Fetch level average playtimes
|
||||
|
|
|
@ -11,6 +11,7 @@ module.exports = class CampaignLevelView extends CocoView
|
|||
|
||||
events:
|
||||
'click .close': 'onClickClose'
|
||||
'click #reload-button': 'onClickReloadButton'
|
||||
'dblclick .recent-session': 'onDblClickRecentSession'
|
||||
|
||||
constructor: (options, @level) ->
|
||||
|
@ -20,26 +21,28 @@ module.exports = class CampaignLevelView extends CocoView
|
|||
@listenToOnce @fullLevel, 'sync', => @render?()
|
||||
|
||||
@levelSlug = @level.get('slug')
|
||||
@getCommonLevelProblems()
|
||||
@getLevelCompletions()
|
||||
@getLevelHelps()
|
||||
@getLevelPlaytimes()
|
||||
@getRecentSessions()
|
||||
@getAnalytics()
|
||||
|
||||
getRenderData: ->
|
||||
c = super()
|
||||
c.level = if @fullLevel.loaded then @fullLevel else @level
|
||||
c.commonProblems = @commonProblems
|
||||
c.levelCompletions = @levelCompletions
|
||||
c.levelHelps = @levelHelps
|
||||
c.levelPlaytimes = @levelPlaytimes
|
||||
c.recentSessions = @recentSessions
|
||||
c.analytics = @analytics
|
||||
c
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
$("#input-startday").datepicker dateFormat: "yy-mm-dd"
|
||||
$("#input-endday").datepicker dateFormat: "yy-mm-dd"
|
||||
|
||||
onClickClose: ->
|
||||
@$el.addClass('hidden')
|
||||
@trigger 'hidden'
|
||||
|
||||
onClickReloadButton: () =>
|
||||
startDay = $('#input-startday').val()
|
||||
endDay = $('#input-endday').val()
|
||||
@getAnalytics startDay, endDay
|
||||
|
||||
onDblClickRecentSession: (e) ->
|
||||
# Admin view of players' code
|
||||
return unless me.isAdmin()
|
||||
|
@ -48,91 +51,124 @@ module.exports = class CampaignLevelView extends CocoView
|
|||
session = new LevelSession _id: row.data 'session-id'
|
||||
@openModalView new ModelModal models: [session, player]
|
||||
|
||||
getCommonLevelProblems: ->
|
||||
# Fetch last 30 days of common level problems
|
||||
startDay = utils.getUTCDay -29
|
||||
startDay = "#{startDay[0..3]}-#{startDay[4..5]}-#{startDay[6..7]}"
|
||||
getAnalytics: (startDay, endDay) =>
|
||||
if startDay?
|
||||
startDayDashed = startDay
|
||||
startDay = startDay.replace(/-/g, '')
|
||||
else
|
||||
startDay = utils.getUTCDay -14
|
||||
startDayDashed = "#{startDay[0..3]}-#{startDay[4..5]}-#{startDay[6..7]}"
|
||||
if endDay?
|
||||
endDayDashed = endDay
|
||||
endDay = endDay.replace(/-/g, '')
|
||||
else
|
||||
endDay = utils.getUTCDay -1
|
||||
endDayDashed = "#{endDay[0..3]}-#{endDay[4..5]}-#{endDay[6..7]}"
|
||||
|
||||
success = (data) =>
|
||||
return if @destroyed
|
||||
@commonProblems = data
|
||||
@commonProblems.startDay = startDay
|
||||
@analytics =
|
||||
startDay: startDayDashed
|
||||
endDay: endDayDashed
|
||||
commonProblems:
|
||||
levels: []
|
||||
loading: true
|
||||
levelCompletions:
|
||||
levels: []
|
||||
loading: true
|
||||
levelHelps:
|
||||
levels: []
|
||||
loading: true
|
||||
levelPlaytimes:
|
||||
levels: []
|
||||
loading: true
|
||||
recentSessions:
|
||||
levels: []
|
||||
loading: true
|
||||
@render()
|
||||
|
||||
@getCommonLevelProblems startDayDashed, endDayDashed, () =>
|
||||
@analytics.commonProblems.loading = false
|
||||
@render()
|
||||
@getLevelCompletions startDay, endDay, () =>
|
||||
@analytics.levelCompletions.loading = false
|
||||
@render()
|
||||
@getLevelHelps startDay, endDay, () =>
|
||||
@analytics.levelHelps.loading = false
|
||||
@render()
|
||||
@getLevelPlaytimes startDayDashed, endDayDashed, () =>
|
||||
@analytics.levelPlaytimes.loading = false
|
||||
@render()
|
||||
@getRecentSessions () =>
|
||||
@analytics.recentSessions.loading = false
|
||||
@render()
|
||||
|
||||
getCommonLevelProblems: (startDay, endDay, doneCallback) ->
|
||||
success = (data) =>
|
||||
return doneCallback() if @destroyed
|
||||
@analytics.commonProblems.levels = data
|
||||
doneCallback()
|
||||
|
||||
# TODO: Why do we need this url dash?
|
||||
request = @supermodel.addRequestResource 'common_problems', {
|
||||
url: '/db/user_code_problem/-/common_problems'
|
||||
data: {startDay: startDay, slug: @levelSlug}
|
||||
data: {startDay: startDay, endDay: endDay, slug: @levelSlug}
|
||||
method: 'POST'
|
||||
success: success
|
||||
}, 0
|
||||
request.load()
|
||||
|
||||
getLevelCompletions: ->
|
||||
# Fetch last 14 days of level completion counts
|
||||
getLevelCompletions: (startDay, endDay, doneCallback) ->
|
||||
success = (data) =>
|
||||
return if @destroyed
|
||||
return doneCallback() if @destroyed
|
||||
data.sort (a, b) -> if a.created < b.created then 1 else -1
|
||||
mapFn = (item) ->
|
||||
item.rate = (item.finished / item.started * 100).toFixed(2)
|
||||
item
|
||||
@levelCompletions = _.map data, mapFn, @
|
||||
@render()
|
||||
@analytics.levelCompletions.levels = _.map data, mapFn, @
|
||||
doneCallback()
|
||||
|
||||
startDay = utils.getUTCDay -14
|
||||
|
||||
# TODO: Why do we need this url dash?
|
||||
request = @supermodel.addRequestResource 'level_completions', {
|
||||
url: '/db/analytics_perday/-/level_completions'
|
||||
data: {startDay: startDay, slug: @levelSlug}
|
||||
data: {startDay: startDay, endDay: endDay, slug: @levelSlug}
|
||||
method: 'POST'
|
||||
success: success
|
||||
}, 0
|
||||
request.load()
|
||||
|
||||
getLevelHelps: ->
|
||||
# Fetch last 14 days of level completion counts
|
||||
getLevelHelps: (startDay, endDay, doneCallback) ->
|
||||
success = (data) =>
|
||||
return if @destroyed
|
||||
@levelHelps = data.sort (a, b) -> if a.created < b.created then 1 else -1
|
||||
@render()
|
||||
|
||||
startDay = utils.getUTCDay -14
|
||||
return doneCallback() if @destroyed
|
||||
@analytics.levelHelps.levels = data.sort (a, b) -> if a.day < b.day then 1 else -1
|
||||
doneCallback()
|
||||
|
||||
request = @supermodel.addRequestResource 'level_helps', {
|
||||
url: '/db/analytics_perday/-/level_helps'
|
||||
data: {startDay: startDay, slugs: [@levelSlug]}
|
||||
data: {startDay: startDay, endDay: endDay, slugs: [@levelSlug]}
|
||||
method: 'POST'
|
||||
success: success
|
||||
}, 0
|
||||
request.load()
|
||||
|
||||
getLevelPlaytimes: ->
|
||||
# Fetch last 14 days of level average playtimes
|
||||
getLevelPlaytimes: (startDay, endDay, doneCallback) ->
|
||||
success = (data) =>
|
||||
return if @destroyed
|
||||
@levelPlaytimes = data.sort (a, b) -> if a.created < b.created then 1 else -1
|
||||
@render()
|
||||
return doneCallback() if @destroyed
|
||||
@analytics.levelPlaytimes.levels = data.sort (a, b) -> if a.created < b.created then 1 else -1
|
||||
doneCallback()
|
||||
|
||||
startDay = utils.getUTCDay -13
|
||||
startDay = "#{startDay[0..3]}-#{startDay[4..5]}-#{startDay[6..7]}"
|
||||
|
||||
# TODO: Why do we need this url dash?
|
||||
request = @supermodel.addRequestResource 'playtime_averages', {
|
||||
url: '/db/level/-/playtime_averages'
|
||||
data: {startDay: startDay, slugs: [@levelSlug]}
|
||||
data: {startDay: startDay, endDay: endDay, slugs: [@levelSlug]}
|
||||
method: 'POST'
|
||||
success: success
|
||||
}, 0
|
||||
request.load()
|
||||
|
||||
getRecentSessions: ->
|
||||
getRecentSessions: (doneCallback) ->
|
||||
limit = 100
|
||||
|
||||
success = (data) =>
|
||||
return if @destroyed
|
||||
@recentSessions = data
|
||||
@render()
|
||||
return doneCallback() if @destroyed
|
||||
@analytics.recentSessions.levels = data
|
||||
doneCallback()
|
||||
|
||||
# TODO: Why do we need this url dash?
|
||||
request = @supermodel.addRequestResource 'level_sessions_recent', {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue