codecombat/app/views/editor/campaign/CampaignEditorView.coffee
Matt Lott c54fea929e Update campaign editor analytics
Optimize analytics.log.event user event data find() to use stream()
instead of exec(), which is better for large result sets
Fix startDay formatting bug
Per-level recent sessions to 100
2015-01-07 14:49:24 -08:00

353 lines
13 KiB
CoffeeScript

RootView = require 'views/core/RootView'
Campaign = require 'models/Campaign'
Level = require 'models/Level'
Achievement = require 'models/Achievement'
ThangType = require 'models/ThangType'
CampaignView = require 'views/play/CampaignView'
CocoCollection = require 'collections/CocoCollection'
treemaExt = require 'core/treema-ext'
utils = require 'core/utils'
SaveCampaignModal = require './SaveCampaignModal'
RelatedAchievementsCollection = require 'collections/RelatedAchievementsCollection'
CampaignLevelView = require './CampaignLevelView'
achievementProject = ['related', 'rewards', 'name', 'slug']
thangTypeProject = ['name', 'original']
module.exports = class CampaignEditorView extends RootView
id: "campaign-editor-view"
template: require 'templates/editor/campaign/campaign-editor-view'
className: 'editor'
events:
'click #save-button': 'onClickSaveButton'
constructor: (options, @campaignHandle) ->
super(options)
@campaign = new Campaign({_id:@campaignHandle})
@supermodel.loadModel(@campaign, 'campaign')
@levels = new CocoCollection([], {
model: Level
url: "/db/campaign/#{@campaignHandle}/levels"
project: Campaign.denormalizedLevelProperties
})
@supermodel.loadCollection(@levels, 'levels')
@achievements = new CocoCollection([], {
model: Achievement
url: "/db/campaign/#{@campaignHandle}/achievements"
project: achievementProject
})
@supermodel.loadCollection(@achievements, 'achievements')
@toSave = new Backbone.Collection()
@listenToOnce @campaign ,'sync', @loadThangTypeNames
@listenToOnce @campaign, 'sync', @onFundamentalLoaded
@listenToOnce @levels, 'sync', @onFundamentalLoaded
@listenToOnce @achievements, 'sync', @onFundamentalLoaded
_.delay @getCampaignCompletions, 1000
loadThangTypeNames: ->
# Load the names of the ThangTypes that this level's Treema nodes might want to display.
originals = []
for level in _.values(@campaign.get('levels'))
originals = originals.concat(_.values(level.requiredGear)) if level.requiredGear
originals = originals.concat(_.values(level.restrictedGear)) if level.restrictedGear
originals = originals.concat(level.allowedHeroes) if level.allowedHeroes
originals = _.uniq _.flatten originals
for original in originals
thangType = new ThangType()
thangType.setProjection(thangTypeProject)
thangType.setURL("/db/thang.type/#{original}/version")
@supermodel.loadModel(thangType, 'thang')
onFundamentalLoaded: ->
# Load any levels which haven't been denormalized into our campaign.
return unless @campaign.loaded and @levels.loaded and @achievements.loaded
for level in _.values(@campaign.get('levels'))
continue if model = @levels.findWhere(original: level.original)
model = new Level({})
model.setProjection Campaign.denormalizedLevelProperties
model.setURL("/db/level/#{level.original}/version")
@levels.add @supermodel.loadModel(model, 'level').model
achievements = new RelatedAchievementsCollection level.original
achievements.setProjection achievementProject
@supermodel.loadCollection achievements, 'achievements'
@listenToOnce achievements, 'sync', ->
@achievements.add(achievements.models)
onLoaded: ->
@toSave.add @campaign if @campaign.hasLocalChanges()
campaignLevels = $.extend({}, @campaign.get('levels'))
for level in @levels.models
levelOriginal = level.get('original')
campaignLevel = campaignLevels[levelOriginal]
continue if not campaignLevel
$.extend campaignLevel, _.omit(level.attributes, '_id')
achievements = @achievements.where {'related': levelOriginal}
rewards = []
for achievement in achievements
for rewardType, rewardArray of achievement.get('rewards')
for reward in rewardArray
rewardObject = { achievement: achievement.id }
if rewardType is 'heroes'
rewardObject.hero = reward
thangType = new ThangType({}, {project: thangTypeProject})
thangType.setURL("/db/thang.type/#{reward}/version")
@supermodel.loadModel(thangType, 'thang')
if rewardType is 'levels'
rewardObject.level = reward
if not @levels.findWhere({original: reward})
level = new Level({}, {project: Campaign.denormalizedLevelProperties})
level.setURL("/db/level/#{reward}/version")
@supermodel.loadModel(level, 'level')
if rewardType is 'items'
rewardObject.item = reward
thangType = new ThangType({}, {project: thangTypeProject})
thangType.setURL("/db/thang.type/#{reward}/version")
@supermodel.loadModel(thangType, 'thang')
rewards.push rewardObject
campaignLevel.rewards = rewards
delete campaignLevel.unlocks
campaignLevel.campaign = @campaign.get 'slug'
campaignLevels[levelOriginal] = campaignLevel
@campaign.set('levels', campaignLevels)
for level in _.values campaignLevels
model = @levels.findWhere {original: level.original}
model.set key, level[key] for key in Campaign.denormalizedLevelProperties
@toSave.add model if model.hasLocalChanges()
@updateRewardsForLevel model, level.rewards
super()
getRenderData: ->
c = super()
c.campaign = @campaign
c.campaignDropOffs = @campaignDropOffs
c
onClickSaveButton: ->
@toSave.set @toSave.filter (m) -> m.hasLocalChanges()
@openModalView new SaveCampaignModal({}, @toSave)
afterRender: ->
super()
treemaOptions =
schema: Campaign.schema
data: $.extend({}, @campaign.attributes)
filePath: "db/campaign/#{@campaign.get('_id')}"
callbacks:
change: @onTreemaChanged
select: @onTreemaSelectionChanged
dblclick: @onTreemaDoubleClicked
nodeClasses:
levels: LevelsNode
level: LevelNode
campaigns: CampaignsNode
campaign: CampaignNode
achievement: AchievementNode
supermodel: @supermodel
@treema = @$el.find('#campaign-treema').treema treemaOptions
@treema.build()
@treema.open()
@treema.childrenTreemas.levels?.open()
@campaignView = new CampaignView({editorMode: true, supermodel: @supermodel}, @campaignHandle)
@campaignView.highlightElement = _.noop # make it stop
@listenTo @campaignView, 'level-moved', @onCampaignLevelMoved
@listenTo @campaignView, 'adjacent-campaign-moved', @onAdjacentCampaignMoved
@listenTo @campaignView, 'level-clicked', @onCampaignLevelClicked
@listenTo @campaignView, 'level-double-clicked', @onCampaignLevelDoubleClicked
@insertSubView @campaignView
onTreemaChanged: (e, nodes) =>
for node in nodes
path = node.getPath()
if _.string.startsWith path, '/levels/'
parts = path.split('/')
original = parts[2]
level = @supermodel.getModelByOriginal Level, original
campaignLevel = @treema.get "/levels/#{original}"
@updateRewardsForLevel level, campaignLevel.rewards
level.set key, campaignLevel[key] for key in Campaign.denormalizedLevelProperties
@toSave.add level if level.hasLocalChanges()
@toSave.add @campaign
@campaign.set key, value for key, value of @treema.data
@campaignView.setCampaign(@campaign)
onTreemaDoubleClicked: (e, node) =>
path = node.getPath()
return unless _.string.startsWith path, '/levels/'
original = path.split('/')[2]
@openCampaignLevelView @supermodel.getModelByOriginal Level, original
onCampaignLevelMoved: (e) ->
path = "levels/#{e.levelOriginal}/position"
@treema.set path, e.position
onAdjacentCampaignMoved: (e) ->
path = "adjacentCampaigns/#{e.campaignID}/position"
@treema.set path, e.position
onCampaignLevelClicked: (levelOriginal) ->
return unless levelTreema = @treema.childrenTreemas?.levels?.childrenTreemas?[levelOriginal]
if key.ctrl or key.command
url = "/editor/level/#{levelTreema.data.slug}"
window.open url, '_blank'
levelTreema.select()
#levelTreema.open()
onCampaignLevelDoubleClicked: (levelOriginal) ->
@openCampaignLevelView @supermodel.getModelByOriginal Level, levelOriginal
openCampaignLevelView: (level) ->
@insertSubView campaignLevelView = new CampaignLevelView({}, level)
@listenToOnce campaignLevelView, 'hidden', => @$el.find('#campaign-view').show()
@$el.find('#campaign-view').hide()
updateRewardsForLevel: (level, rewards) ->
achievements = @supermodel.getModels(Achievement)
achievements = (a for a in achievements when a.get('related') is level.get('original'))
for achievement in achievements
rewardSubset = (r for r in rewards when r.achievement is achievement.id)
oldRewards = achievement.get 'rewards'
newRewards = {}
heroes = _.compact((r.hero for r in rewardSubset))
newRewards.heroes = heroes if heroes.length
items = _.compact((r.item for r in rewardSubset))
newRewards.items = items if items.length
levels = _.compact((r.level for r in rewardSubset))
newRewards.levels = levels if levels.length
newRewards.gems = oldRewards.gems if oldRewards.gems
achievement.set 'rewards', newRewards
if achievement.hasLocalChanges()
@toSave.add achievement
getCampaignCompletions: =>
# Fetch last 7 days of campaign drop-off rates
startDay = utils.getUTCDay -6
success = (data) =>
return if @destroyed
# API returns all the campaign data currently
@campaignDropOffs = data[@campaignHandle]
mapFn = (item) ->
item.startDropRate = (item.startDropped / item.started * 100).toFixed(2)
item.finishDropRate = (item.finishDropped / item.finished * 100).toFixed(2)
item.completionRate = (item.finished / item.started * 100).toFixed(2)
item
@campaignDropOffs.levels = _.map @campaignDropOffs.levels, mapFn, @
@campaignDropOffs.startDay = startDay
@render()
# TODO: Why do we need this url dash?
request = @supermodel.addRequestResource 'campaign_completions', {
url: '/db/analytics_log_event/-/campaign_completions'
data: {startDay: startDay, slug: @campaignHandle}
method: 'POST'
success: success
}, 0
request.load()
class LevelsNode extends TreemaObjectNode
valueClass: 'treema-levels'
@levels: {}
buildValueForDisplay: (valEl, data) ->
@buildValueForDisplaySimply valEl, ''+_.size(data)
childPropertiesAvailable: -> @childSource
childSource: (req, res) =>
s = new Backbone.Collection([], {model:Level})
s.url = '/db/level'
s.fetch({data: {term:req.term, project: Campaign.denormalizedLevelProperties.join(',')}})
s.once 'sync', (collection) =>
for level in collection.models
LevelsNode.levels[level.get('original')] = level
@settings.supermodel.registerModel level
mapped = ({label: r.get('name'), value: r.get('original')} for r in collection.models)
res(mapped)
class LevelNode extends TreemaObjectNode
valueClass: 'treema-level'
buildValueForDisplay: (valEl, data) ->
name = data.name
if data.requiresSubscription
name = "[P] " + name
status = ''
el = 'strong'
if data.adminOnly
status += " (disabled)"
el = 'span'
else if data.adventurer
status += " (adventurer)"
completion = ''
if data.tasks
completion = "#{(t for t in data.tasks when t.complete).length} / #{data.tasks.length}"
valEl.append $("<a href='/editor/level/#{_.string.slugify(data.name)}' class='spr'>(e)</a>")
valEl.append $("<#{el}></#{el}>").addClass('treema-shortened').text name
if status
valEl.append $('<em class="spl"></em>').text status
if completion
valEl.append $('<span class="completion"></span>').text completion
populateData: ->
return if @data.name?
data = _.pick LevelsNode.levels[@keyForParent].attributes, Campaign.denormalizedLevelProperties
_.extend @data, data
class CampaignsNode extends TreemaObjectNode
valueClass: 'treema-campaigns'
@campaigns: {}
buildValueForDisplay: (valEl, data) ->
@buildValueForDisplaySimply valEl, ''+_.size(data)
childPropertiesAvailable: -> @childSource
childSource: (req, res) =>
s = new Backbone.Collection([], {model:Campaign})
s.url = '/db/campaign'
s.fetch({data: {term:req.term, project: Campaign.denormalizedCampaignProperties}})
s.once 'sync', (collection) ->
CampaignsNode.campaigns[campaign.id] = campaign for campaign in collection.models
mapped = ({label: r.get('name'), value: r.id} for r in collection.models)
res(mapped)
class CampaignNode extends TreemaObjectNode
valueClass: 'treema-campaign'
buildValueForDisplay: (valEl, data) ->
@buildValueForDisplaySimply valEl, data.name
populateData: ->
return if @data.name?
data = _.pick CampaignsNode.campaigns[@keyForParent].attributes, Campaign.denormalizedCampaignProperties
_.extend @data, data
class AchievementNode extends treemaExt.IDReferenceNode
buildSearchURL: (term) -> "#{@url}?term=#{term}&project=#{achievementProject.join(',')}"