Merge branch 'master' into production

This commit is contained in:
Nick Winter 2015-04-08 16:36:56 -07:00
commit f2f008e01a
12 changed files with 83 additions and 30 deletions

View file

@ -72,10 +72,12 @@ module.exports = class LevelLoader extends CocoClass
url += "?team=#{@team}" if @team url += "?team=#{@team}" if @team
session = new LevelSession().setURL url session = new LevelSession().setURL url
session.project = ['creator', 'team', 'heroConfig', 'codeLanguage', 'submittedCodeLanguage', 'state'] if @headless
@sessionResource = @supermodel.loadModel(session, 'level_session', {cache: false}) @sessionResource = @supermodel.loadModel(session, 'level_session', {cache: false})
@session = @sessionResource.model @session = @sessionResource.model
if @opponentSessionID if @opponentSessionID
opponentSession = new LevelSession().setURL "/db/level.session/#{@opponentSessionID}" opponentSession = new LevelSession().setURL "/db/level.session/#{@opponentSessionID}"
opponentSession.project = session.project if @headless
@opponentSessionResource = @supermodel.loadModel(opponentSession, 'opponent_session', {cache: false}) @opponentSessionResource = @supermodel.loadModel(opponentSession, 'opponent_session', {cache: false})
@opponentSession = @opponentSessionResource.model @opponentSession = @opponentSessionResource.model

View file

@ -447,6 +447,7 @@
managed_subs: "Managed Subscriptions" managed_subs: "Managed Subscriptions"
managed_subs_desc: "Add subscriptions for other players (students, children, etc.)" managed_subs_desc: "Add subscriptions for other players (students, children, etc.)"
group_discounts: "Group discounts" group_discounts: "Group discounts"
group_discounts_1: "We also offer group discounts for bulk subscriptions."
group_discounts_1st: "1st subscription" group_discounts_1st: "1st subscription"
group_discounts_full: "Full price" group_discounts_full: "Full price"
group_discounts_2nd: "Subscriptions 2-11" group_discounts_2nd: "Subscriptions 2-11"
@ -611,7 +612,6 @@
how_much_2: "monthly subscription" how_much_2: "monthly subscription"
how_much_3: "costs $9.99, and can be cancelled anytime." how_much_3: "costs $9.99, and can be cancelled anytime."
how_much_4: "Additionally, we provide discounts for larger groups:" how_much_4: "Additionally, we provide discounts for larger groups:"
group_discounts_1: "We also offer group discounts for bulk subscriptions."
sys_requirements_title: "System Requirements" sys_requirements_title: "System Requirements"
sys_requirements_1: "A modern web browser. Newer versions of Chrome, Firefox, or Safari. Internet Explorer 9 or later." sys_requirements_1: "A modern web browser. Newer versions of Chrome, Firefox, or Safari. Internet Explorer 9 or later."
sys_requirements_2: "CodeCombat is not supported on iPad yet." sys_requirements_2: "CodeCombat is not supported on iPad yet."

View file

@ -27,6 +27,7 @@ class CocoModel extends Backbone.Model
# if fixed, RevertModal will also need the fix # if fixed, RevertModal will also need the fix
setProjection: (project) -> setProjection: (project) ->
# TODO: ends up getting done twice, since the URL is modified and the @project is modified. So don't do this, just set project directly... (?)
return if project is @project return if project is @project
url = @getURL() url = @getURL()
url += '&project=' unless /project=/.test url url += '&project=' unless /project=/.test url

View file

@ -88,7 +88,7 @@ _.extend LevelSessionSchema.properties,
type: 'boolean' # Not tracked any more type: 'boolean' # Not tracked any more
frame: frame:
type: 'number' # Not tracked any more type: 'number' # Not tracked any more
thangs: thangs: # ... what is this? Is this used?
type: 'object' type: 'object'
additionalProperties: additionalProperties:
title: 'Thang' title: 'Thang'
@ -241,7 +241,7 @@ _.extend LevelSessionSchema.properties,
description: 'The date a match was computed.' description: 'The date a match was computed.'
playtime: playtime:
title: 'Playtime so far' title: 'Playtime so far'
description: 'The total seconds of playtime on this session when the match was computed.' description: 'The total seconds of playtime on this session when the match was computed. Not currently tracked.'
type: 'number' type: 'number'
metrics: metrics:
type: 'object' type: 'object'

View file

@ -47,6 +47,9 @@ block content
tr tr
th User Start th User Start
th Sub Start th Sub Start
if subscriberCancelled
th Cancelled
else
th th
th th
//- th Name //- th Name

View file

@ -16,9 +16,9 @@ block modal-body-content
p(data-i18n="contact.subscriber_support") Since you're a CodeCombat subscriber, your email will get our priority support. p(data-i18n="contact.subscriber_support") Since you're a CodeCombat subscriber, your email will get our priority support.
else else
p p
span(data-i18n="contact.subscribe_prefix") If you need help figuring out a level, please span.spr(data-i18n="contact.subscribe_prefix") If you need help figuring out a level, please
a.spl.spr(data-toggle="coco-modal", data-target="core/SubscribeModal", data-i18n="contact.subscribe") buy a CodeCombat subscription a(data-toggle="coco-modal", data-target="core/SubscribeModal", data-i18n="contact.subscribe") buy a CodeCombat subscription
span(data-i18n="contact.subscribe_suffix") and we'll be happy to help you with your code. span.spl(data-i18n="contact.subscribe_suffix") and we'll be happy to help you with your code.
.form .form
.form-group .form-group
label.control-label(for="contact-email", data-i18n="general.email") Email label.control-label(for="contact-email", data-i18n="general.email") Email

View file

@ -3,8 +3,6 @@ template = require 'templates/admin/analytics-subscriptions'
ThangType = require 'models/ThangType' ThangType = require 'models/ThangType'
User = require 'models/User' User = require 'models/User'
# TODO: Add last N subscribers table
# TODO: Add revenue line
# TODO: Graphing code copied/mangled from campaign editor level view. OMG, DRY. # TODO: Graphing code copied/mangled from campaign editor level view. OMG, DRY.
require 'vendor/d3' require 'vendor/d3'
@ -12,10 +10,11 @@ require 'vendor/d3'
module.exports = class AnalyticsSubscriptionsView extends RootView module.exports = class AnalyticsSubscriptionsView extends RootView
id: 'admin-analytics-subscriptions-view' id: 'admin-analytics-subscriptions-view'
template: template template: template
targetSubCount: 2000
constructor: (options) -> constructor: (options) ->
super options super options
@resetData() @resetSubscriptionsData()
if me.isAdmin() if me.isAdmin()
@refreshData() @refreshData()
_.delay (=> @refreshData()), 30 * 60 * 1000 _.delay (=> @refreshData()), 30 * 60 * 1000
@ -25,6 +24,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
context.analytics = @analytics ? graphs: [] context.analytics = @analytics ? graphs: []
context.subs = _.cloneDeep(@subs ? []).reverse() context.subs = _.cloneDeep(@subs ? []).reverse()
context.subscribers = @subscribers ? [] context.subscribers = @subscribers ? []
context.subscriberCancelled = _.find context.subscribers, (subscriber) -> subscriber.cancel
context.total = @total ? 0 context.total = @total ? 0
context.cancelled = @cancelled ? 0 context.cancelled = @cancelled ? 0
context.monthlyChurn = @monthlyChurn ? 0.0 context.monthlyChurn = @monthlyChurn ? 0.0
@ -35,16 +35,17 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
super() super()
@updateAnalyticsGraphs() @updateAnalyticsGraphs()
resetData: -> resetSubscriptionsData: ->
@analytics = graphs: [] @analytics = graphs: []
@subs = [] @subs = []
@total = 0 @total = 0
@cancelled = 0 @cancelled = 0
@monthlyChurn = 0.0 @monthlyChurn = 0.0
@monthlyGrowth = 0.0
refreshData: -> refreshData: ->
return unless me.isAdmin() return unless me.isAdmin()
@resetData() @resetSubscriptionsData()
@getSubscribers() @getSubscribers()
@getSubscriptions() @getSubscriptions()
@ -75,7 +76,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
console.error 'Failed to get subscriptions', response console.error 'Failed to get subscriptions', response
options.success = (subs, response, options) => options.success = (subs, response, options) =>
return if @destroyed return if @destroyed
@resetData() @resetSubscriptionsData()
subDayMap = {} subDayMap = {}
for sub in subs for sub in subs
startDay = sub.start.substring(0, 10) startDay = sub.start.substring(0, 10)
@ -121,6 +122,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
# TODO: Where should this metadata live? # TODO: Where should this metadata live?
# TODO: lineIDs assumed to be unique across graphs # TODO: lineIDs assumed to be unique across graphs
totalSubsID = 'total-subs' totalSubsID = 'total-subs'
targetSubsID = 'target-subs'
startedSubsID = 'started-subs' startedSubsID = 'started-subs'
cancelledSubsID = 'cancelled-subs' cancelledSubsID = 'cancelled-subs'
netSubsID = 'net-subs' netSubsID = 'net-subs'
@ -129,6 +131,11 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
description: 'Total Active Subscriptions' description: 'Total Active Subscriptions'
color: 'green' color: 'green'
strokeWidth: 1 strokeWidth: 1
lineMetadata[targetSubsID] =
description: 'Target Total Subscriptions'
color: 'gold'
strokeWidth: 4
opacity: 1.0
lineMetadata[startedSubsID] = lineMetadata[startedSubsID] =
description: 'New Subscriptions' description: 'New Subscriptions'
color: 'blue' color: 'blue'
@ -185,8 +192,9 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
points: levelPoints points: levelPoints
description: lineMetadata[totalSubsID].description description: lineMetadata[totalSubsID].description
lineColor: lineMetadata[totalSubsID].color lineColor: lineMetadata[totalSubsID].color
strokeWidth: lineMetadata[totalSubsID].strokeWidth
min: 0 min: 0
max: d3.max(@subs, (d) -> d.total) max: Math.max(@targetSubCount, d3.max(@subs, (d) -> d.total))
## Started ## Started
@ -219,9 +227,34 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
points: levelPoints points: levelPoints
description: lineMetadata[startedSubsID].description description: lineMetadata[startedSubsID].description
lineColor: lineMetadata[startedSubsID].color lineColor: lineMetadata[startedSubsID].color
strokeWidth: lineMetadata[startedSubsID].strokeWidth
min: 0 min: 0
max: d3.max(@subs, (d) -> d.started) max: d3.max(@subs, (d) -> d.started)
## Total subs target
# Build line data
levelPoints = []
for sub, i in @subs
levelPoints.push
x: i
y: @targetSubCount
day: sub.day
pointID: "#{targetSubsID}#{i}"
values: []
levelPoints.splice(0, levelPoints.length - timeframeDays) if levelPoints.length > timeframeDays
@analytics.graphs[0].lines.push
lineID: targetSubsID
enabled: true
points: levelPoints
description: lineMetadata[targetSubsID].description
lineColor: lineMetadata[targetSubsID].color
strokeWidth: lineMetadata[targetSubsID].strokeWidth
min: 0
max: @targetSubCount
## Cancelled ## Cancelled
averageCancelled = 0 averageCancelled = 0
@ -258,6 +291,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
points: levelPoints points: levelPoints
description: lineMetadata[cancelledSubsID].description description: lineMetadata[cancelledSubsID].description
lineColor: lineMetadata[cancelledSubsID].color lineColor: lineMetadata[cancelledSubsID].color
strokeWidth: lineMetadata[cancelledSubsID].strokeWidth
min: 0 min: 0
max: d3.max(@subs, (d) -> d.started) max: d3.max(@subs, (d) -> d.started)
@ -333,7 +367,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
xRange = d3.scale.linear().range([0, width]).domain([d3.min(line.points, (d) -> d.x), d3.max(line.points, (d) -> d.x)]) xRange = d3.scale.linear().range([0, width]).domain([d3.min(line.points, (d) -> d.x), d3.max(line.points, (d) -> d.x)])
yRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max]) yRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])
# x-Axis and guideline once # x-Axis
if currentLine is 0 if currentLine is 0
startDay = new Date(line.points[0].day) startDay = new Date(line.points[0].day)
endDay = new Date(line.points[line.points.length - 1].day) endDay = new Date(line.points[line.points.length - 1].day)
@ -392,7 +426,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
svg.append("text") svg.append("text")
.attr("x", margin + 40 + 10) .attr("x", margin + 40 + 10)
.attr("y", margin + height + xAxisHeight + keyHeight * currentLine + (keyHeight + 10) / 2) .attr("y", margin + height + xAxisHeight + keyHeight * currentLine + (keyHeight + 10) / 2)
.attr("fill", line.lineColor) .attr("fill", if line.lineColor is 'gold' then 'orange' else line.lineColor)
.attr("class", "key-text") .attr("class", "key-text")
.text(line.description) .text(line.description)

View file

@ -299,7 +299,12 @@ module.exports = class CampaignView extends RootView
unless foundNext unless foundNext
for nextLevelOriginal in level.nextLevels for nextLevelOriginal in level.nextLevels
nextLevel = _.find levels, original: nextLevelOriginal nextLevel = _.find levels, original: nextLevelOriginal
if nextLevel and not nextLevel.locked and @levelStatusMap[nextLevel.slug] isnt 'complete' and (me.isPremium() or not nextLevel.requiresSubscription or nextLevel.slug is 'apocalypse') if nextLevel and not nextLevel.locked and @levelStatusMap[nextLevel.slug] isnt 'complete' and (
me.isPremium() or
not nextLevel.requiresSubscription or
nextLevel.slug is 'apocalypse' or
(nextLevel.slug is 'favorable-odds' and not @levelStatusMap['the-raised-sword'])
)
nextLevel.next = true nextLevel.next = true
foundNext = true foundNext = true
break break

View file

@ -19,7 +19,9 @@ module.exports = class LeaderboardModal extends ModalView
constructor: (options) -> constructor: (options) ->
super options super options
@levelSlug = @options.levelSlug @levelSlug = @options.levelSlug
@level = @supermodel.loadModel(new Level({_id: @levelSlug}, {project: ['name', 'i18n', 'scoreType', 'original']}), 'level').model level = new Level({_id: @levelSlug})
level.project = ['name', 'i18n', 'scoreType', 'original']
@level = @supermodel.loadModel(level, 'level').model
getRenderData: (c) -> getRenderData: (c) ->
c = super c c = super c

View file

@ -206,10 +206,11 @@ module.exports = class Handler
return @sendForbiddenError(res) return @sendForbiddenError(res)
getById: (req, res, id) -> getById: (req, res, id) ->
# return @sendNotFoundError(res) # for testing
return @sendForbiddenError(res) unless @hasAccess(req) return @sendForbiddenError(res) unless @hasAccess(req)
if req.query.project
@getDocumentForIdOrSlug id, (err, document) => projection = {}
projection[field] = 1 for field in req.query.project.split(',')
@getDocumentForIdOrSlug id, projection, (err, document) =>
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless document? return @sendNotFoundError(res) unless document?
return @sendForbiddenError(res) unless @hasAccessToDocument(req, document) return @sendForbiddenError(res) unless @hasAccessToDocument(req, document)
@ -491,13 +492,17 @@ module.exports = class Handler
@isID: (id) -> _.isString(id) and id.length is 24 and id.match(/[a-f0-9]/gi)?.length is 24 @isID: (id) -> _.isString(id) and id.length is 24 and id.match(/[a-f0-9]/gi)?.length is 24
getDocumentForIdOrSlug: (idOrSlug, done) -> getDocumentForIdOrSlug: (idOrSlug, projection, done) ->
unless done
done = projection # projection is optional argument
projection = null
idOrSlug = idOrSlug+'' idOrSlug = idOrSlug+''
if Handler.isID(idOrSlug) if Handler.isID(idOrSlug)
@modelClass.findById(idOrSlug).exec (err, document) -> query = @modelClass.findById(idOrSlug)
done(err, document)
else else
@modelClass.findOne {slug: idOrSlug}, (err, document) -> query = @modelClass.findOne {slug: idOrSlug}
query.select projection if projection
query.exec (err, document) ->
done(err, document) done(err, document)
doWaterfallChecks: (req, document, done) -> doWaterfallChecks: (req, document, done) ->

View file

@ -158,7 +158,8 @@ LevelHandler = class LevelHandler extends Handler
majorVersion: level.version.major majorVersion: level.version.major
creator: req.user._id+'' creator: req.user._id+''
query = Session.find(sessionQuery).select('-screenshot') query = Session.find(sessionQuery).select('-screenshot -transpiledCode')
# TODO: take out "code" as well, since that can get huge containing the transpiled code for the lat hero, and find another way of having the LadderSubmissionViews in the MyMatchesTab determine rankin readiness
query.exec (err, results) => query.exec (err, results) =>
if err then @sendDatabaseError(res, err) else @sendSuccess res, results if err then @sendDatabaseError(res, err) else @sendSuccess res, results

View file

@ -527,11 +527,11 @@ updateMatchesInSession = (matchObject, sessionID, callback) ->
opponentsArray = _.toArray opponentsClone opponentsArray = _.toArray opponentsClone
currentMatchObject.opponents = opponentsArray currentMatchObject.opponents = opponentsArray
currentMatchObject.codeLanguage = matchObject.opponents[opponentsArray[0].sessionID].codeLanguage currentMatchObject.codeLanguage = matchObject.opponents[opponentsArray[0].sessionID].codeLanguage
currentMatchObject.simulator = @clientResponseObject.simulator #currentMatchObject.simulator = @clientResponseObject.simulator # Uncomment when actively debugging simulation mismatches
currentMatchObject.randomSeed = parseInt(@clientResponseObject.randomSeed or 0, 10) #currentMatchObject.randomSeed = parseInt(@clientResponseObject.randomSeed or 0, 10) # Uncomment when actively debugging simulation mismatches
LevelSession.findOne {'_id': sessionID}, (err, session) -> LevelSession.findOne {'_id': sessionID}, (err, session) ->
session = session.toObject() session = session.toObject()
currentMatchObject.playtime = session.playtime ? 0 #currentMatchObject.playtime = session.playtime ? 0 # Not useful if we can only see last 200
sessionUpdateObject = sessionUpdateObject =
$push: {matches: {$each: [currentMatchObject], $slice: -200}} $push: {matches: {$each: [currentMatchObject], $slice: -200}}
#log.info "Updating session #{sessionID}" #log.info "Updating session #{sessionID}"