mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-24 08:08:15 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
af3c6560e9
18 changed files with 413 additions and 152 deletions
BIN
app/assets/images/pages/play/modal/subscribe-heroes.png
Normal file
BIN
app/assets/images/pages/play/modal/subscribe-heroes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
|
@ -32,6 +32,7 @@ module.exports = class LevelLoader extends CocoClass
|
|||
@team = options.team
|
||||
@headless = options.headless
|
||||
@spectateMode = options.spectateMode ? false
|
||||
@observing = options.observing
|
||||
|
||||
@worldNecessities = []
|
||||
@listenTo @supermodel, 'resource-loaded', @onWorldNecessityLoaded
|
||||
|
@ -389,6 +390,8 @@ module.exports = class LevelLoader extends CocoClass
|
|||
@world.submissionCount = @session?.get('state')?.submissionCount ? 0
|
||||
@world.flagHistory = @session?.get('state')?.flagHistory ? []
|
||||
@world.difficulty = @session?.get('state')?.difficulty ? 0
|
||||
if @observing
|
||||
@world.difficulty = Math.max 0, @world.difficulty - 1 # Show the difficulty they won, not the next one.
|
||||
serializedLevel = @level.serialize(@supermodel, @session, @opponentSession)
|
||||
@world.loadFromLevel serializedLevel, false
|
||||
console.log 'World has been initialized from level loader.'
|
||||
|
|
|
@ -381,6 +381,15 @@
|
|||
recovered: "Previous gems purchase recovered. Please refresh the page."
|
||||
|
||||
subscribe:
|
||||
comparison_blurb: "Sharpen your skills with a CodeCombat subscription!"
|
||||
feature1: "60+ basic levels across 4 worlds"
|
||||
feature2: "7 powerful <strong>new heroes</strong> with unique skills!"
|
||||
feature3: "30+ bonus levels"
|
||||
feature4: "<strong>3500 bonus gems</strong> every month!"
|
||||
feature5: " Video tutorials"
|
||||
feature6: "Premium email support"
|
||||
free: "Free"
|
||||
month: "month"
|
||||
subscribe_title: "Subscribe"
|
||||
unsubscribe: "Unsubscribe"
|
||||
confirm_unsubscribe: "Confirm Unsubscribe"
|
||||
|
@ -394,12 +403,20 @@
|
|||
heroes: "More powerful heroes!"
|
||||
gems: "3500 bonus gems every month!"
|
||||
items: "Over 250 bonus items!"
|
||||
parent_button: "Ask your parent"
|
||||
parent_email_description: "We'll email them so they can buy you a CodeCombat subscription."
|
||||
parent_email_input_invalid: "Email address invalid."
|
||||
parent_email_input_label: "Parent email address"
|
||||
parent_email_input_placeholder: "Enter parent email"
|
||||
parent_email_send: "Send Email"
|
||||
parent_email_sent: "Email sent!"
|
||||
parent_email_title: "What's your parent's email?"
|
||||
parents: "For Parents"
|
||||
parents_title: "Your child will learn to code."
|
||||
parents_blurb1: "With CodeCombat, your child learns by writing real code. They start by learning simple commands, and progress to more advanced topics."
|
||||
parents_blurb2: "For $9.99 USD/mo, they get new challenges every week and personal email support from professional programmers."
|
||||
parents_blurb3: "No Risk: 100% money back guarantee, easy 1-click unsubscribe."
|
||||
subscribe_button: "Subscribe Now"
|
||||
subscribe_button: "Subscribe"
|
||||
stripe_description: "Monthly Subscription"
|
||||
subscription_required_to_play: "You'll need a subscription to play this level."
|
||||
unlock_help_videos: "Subscribe to unlock all video tutorials."
|
||||
|
|
|
@ -85,6 +85,12 @@ _.extend UserSchema.properties,
|
|||
recruitNotes: {$ref: '#/definitions/emailSubscription'}
|
||||
employerNotes: {$ref: '#/definitions/emailSubscription'}
|
||||
|
||||
oneTimes: c.array {title: 'One-time emails'},
|
||||
c.object {title: 'One-time email', required: ['type', 'targetEmail']},
|
||||
type: c.shortString() # E.g 'subscribe modal parent'
|
||||
targetEmail: c.shortString()
|
||||
sent: c.date() # Set when sent
|
||||
|
||||
# server controlled
|
||||
permissions: c.array {}, c.shortString()
|
||||
dateCreated: c.date({title: 'Date Joined'})
|
||||
|
@ -272,7 +278,7 @@ _.extend UserSchema.properties,
|
|||
purchased: c.RewardSchema 'purchased with gems or money'
|
||||
spent: {type: 'number'}
|
||||
stripeCustomerID: { type: 'string' } # TODO: Migrate away from this property
|
||||
|
||||
|
||||
stripe: c.object {}, {
|
||||
customerID: { type: 'string' }
|
||||
planID: { enum: ['basic'] }
|
||||
|
|
|
@ -51,36 +51,11 @@
|
|||
&:hover
|
||||
color: yellow
|
||||
|
||||
|
||||
//- Selling points
|
||||
|
||||
#selling-points
|
||||
position: absolute
|
||||
left: 65px
|
||||
top: 335px
|
||||
width: 650px
|
||||
font-weight: bold
|
||||
line-height: 18px
|
||||
color: black
|
||||
font-family: $headings-font-family
|
||||
font-size: 18px
|
||||
|
||||
.point
|
||||
width: 150px
|
||||
overflow: none
|
||||
float: left
|
||||
text-align: center
|
||||
margin-right: 10px
|
||||
|
||||
#parents-info
|
||||
position: absolute
|
||||
right: 7px
|
||||
top: 56px
|
||||
text-decoration: underline
|
||||
cursor: pointer
|
||||
//- Popovers
|
||||
|
||||
.popover
|
||||
z-index: 1050
|
||||
min-width: 400px
|
||||
|
||||
h3
|
||||
background: transparent
|
||||
|
@ -88,13 +63,67 @@
|
|||
font-size: 30px
|
||||
color: black
|
||||
|
||||
//- Sales image
|
||||
|
||||
.subscribe-image
|
||||
position: absolute
|
||||
top: 114px
|
||||
right: 65px
|
||||
|
||||
//- Feature comparison table
|
||||
|
||||
.comparison-blurb
|
||||
position: absolute
|
||||
left: 10%
|
||||
top: 132px
|
||||
width: 450px
|
||||
background: rgba(0, 0, 0, 0.0)
|
||||
font-weight: normal
|
||||
line-height: 18px
|
||||
color: black
|
||||
font-family: $headings-font-family
|
||||
font-size: 18px
|
||||
|
||||
.comparison-table
|
||||
position: absolute
|
||||
left: 10%
|
||||
top: 160px
|
||||
width: 450px
|
||||
background: rgba(0, 0, 0, 0.0)
|
||||
thead
|
||||
tr
|
||||
th
|
||||
font-size: 24px
|
||||
font-variant: small-caps
|
||||
font-family: "Open Sans Condensed", "Helvetica Neue", Helvetica, Arial, sans-serif
|
||||
font-weight: 700
|
||||
line-height: 1.1
|
||||
color: #317EAC
|
||||
tbody
|
||||
font-size: 14px
|
||||
.center-ok
|
||||
text-align: center
|
||||
|
||||
//- Parent info popover link
|
||||
|
||||
#parents-info
|
||||
position: absolute
|
||||
left: 38px
|
||||
top: 389px
|
||||
text-decoration: underline
|
||||
cursor: pointer
|
||||
font-weight: bold
|
||||
line-height: 18px
|
||||
color: black
|
||||
font-family: $headings-font-family
|
||||
font-size: 18px
|
||||
|
||||
//- Purchase button
|
||||
|
||||
.purchase-button
|
||||
position: absolute
|
||||
left: 73px
|
||||
width: 600px
|
||||
right: 24px
|
||||
width: 400px
|
||||
height: 70px
|
||||
top: 430px
|
||||
font-size: 32px
|
||||
|
@ -116,6 +145,28 @@
|
|||
padding: 2px 0 0 2px
|
||||
color: white
|
||||
|
||||
//- Parent button
|
||||
//- TODO: Add hover and active effects
|
||||
|
||||
.parent-button
|
||||
position: absolute
|
||||
left: 24px
|
||||
width: 250px
|
||||
height: 70px
|
||||
top: 430px
|
||||
font-size: 28px
|
||||
line-height: 38px
|
||||
border-style: solid
|
||||
border-image: url(/images/common/button-background-warning-disabled.png) 14 20 20 20 fill round
|
||||
border-width: 14px 20px 20px 20px
|
||||
color: darken(white, 5%)
|
||||
|
||||
#email-parent-form
|
||||
.email_invalid
|
||||
color: red
|
||||
display: none
|
||||
#email-parent-complete
|
||||
display: none
|
||||
|
||||
//- Errors
|
||||
|
||||
|
|
|
@ -243,6 +243,21 @@ $gameControlMargin: 30px
|
|||
min-width: 200px
|
||||
display: block
|
||||
margin: 10px auto 0 auto
|
||||
position: relative
|
||||
|
||||
.badge
|
||||
position: absolute
|
||||
top: initial
|
||||
left: initial
|
||||
right: -25px
|
||||
bottom: -25px
|
||||
font-size: 20px
|
||||
color: black
|
||||
border: 1px solid black
|
||||
background-color: rgb(232, 217, 87)
|
||||
border-radius: 50%
|
||||
opacity: 1
|
||||
padding: 3px 9px
|
||||
|
||||
&.complete
|
||||
.start-level, .view-solutions
|
||||
|
|
|
@ -7,27 +7,67 @@
|
|||
#retrying-alert.alert.alert-danger(data-i18n="buy_gems.retrying")
|
||||
|
||||
else
|
||||
img(src="/images/pages/play/modal/subscribe-background.png")#subscribe-background
|
||||
img#subscribe-background(src="/images/pages/play/modal/subscribe-background-blank.png")
|
||||
img.subscribe-image(src="/images/pages/play/modal/subscribe-heroes.png")
|
||||
|
||||
h1(data-i18n="subscribe.subscribe_title") Subscribe
|
||||
|
||||
div#close-modal
|
||||
span.glyphicon.glyphicon-remove
|
||||
|
||||
#selling-points
|
||||
#point-levels.point
|
||||
.blurb(data-i18n="subscribe.levels")
|
||||
#point-heroes.point
|
||||
.blurb(data-i18n="subscribe.heroes")
|
||||
#point-gems.point
|
||||
.blurb(data-i18n="subscribe.gems")
|
||||
#point-items.point
|
||||
.blurb(data-i18n="subscribe.items")
|
||||
|
||||
#parents-info(data-i18n="subscribe.parents")
|
||||
div.comparison-blurb(data-i18n="subscribe.comparison_blurb")
|
||||
table.table.table-condensed.table-bordered.comparison-table
|
||||
thead
|
||||
tr
|
||||
th
|
||||
th(data-i18n="subscribe.free")
|
||||
th
|
||||
//- TODO: find a better way to localize '$9.99/month'
|
||||
span $#{price}/
|
||||
span(data-i18n="subscribe.month")
|
||||
tbody
|
||||
tr
|
||||
td.feature-description
|
||||
span(data-i18n="subscribe.feature1")
|
||||
td.center-ok
|
||||
span.glyphicon.glyphicon-ok
|
||||
td.center-ok
|
||||
span.glyphicon.glyphicon-ok
|
||||
tr
|
||||
td.feature-description
|
||||
span(data-i18n="[html]subscribe.feature2")
|
||||
td
|
||||
td.center-ok
|
||||
span.glyphicon.glyphicon-ok
|
||||
tr
|
||||
td.feature-description
|
||||
span(data-i18n="subscribe.feature3")
|
||||
td
|
||||
td.center-ok
|
||||
span.glyphicon.glyphicon-ok
|
||||
tr
|
||||
td.feature-description
|
||||
span(data-i18n="[html]subscribe.feature4")
|
||||
td
|
||||
td.center-ok
|
||||
span.glyphicon.glyphicon-ok
|
||||
tr
|
||||
td.feature-description
|
||||
span(data-i18n="subscribe.feature5")
|
||||
td
|
||||
td.center-ok
|
||||
span.glyphicon.glyphicon-ok
|
||||
tr
|
||||
td.feature-description
|
||||
span(data-i18n="subscribe.feature6")
|
||||
td
|
||||
td.center-ok
|
||||
span.glyphicon.glyphicon-ok
|
||||
#parents-info(data-i18n="subscribe.parents")
|
||||
|
||||
button.btn.btn-lg.btn-illustrated.purchase-button(data-i18n="subscribe.subscribe_button")
|
||||
|
||||
button.btn.btn-lg.btn-illustrated.parent-button(data-i18n="subscribe.parent_button")
|
||||
|
||||
if state === 'declined'
|
||||
#declined-alert.alert.alert-danger.alert-dismissible
|
||||
span(data-i18n="buy_gems.declined")
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
td Playtime
|
||||
td Complete
|
||||
td Changed
|
||||
td Replay
|
||||
tbody
|
||||
- for (var i = 0; i < analytics.recentSessions.data.length; i++)
|
||||
tr.recent-session(data-player-id=analytics.recentSessions.data[i].creator, data-session-id=analytics.recentSessions.data[i]._id)
|
||||
|
@ -71,6 +72,9 @@
|
|||
else
|
||||
td false
|
||||
td= analytics.recentSessions.data[i].changed
|
||||
td
|
||||
button.btn.replay-button.btn-xs
|
||||
.glyphicon.glyphicon-eye-open
|
||||
|
||||
h4 Completion Rates
|
||||
if analytics.levelCompletions.loading
|
||||
|
|
|
@ -17,8 +17,9 @@ module.exports = class SubscribeModal extends ModalView
|
|||
'stripe:received-token': 'onStripeReceivedToken'
|
||||
|
||||
events:
|
||||
'click .purchase-button': 'onClickPurchaseButton'
|
||||
'click #close-modal': 'hide'
|
||||
'click #parent-send': 'onClickParentSendButton'
|
||||
'click .purchase-button': 'onClickPurchaseButton'
|
||||
|
||||
constructor: (options) ->
|
||||
super(options)
|
||||
|
@ -34,6 +35,40 @@ module.exports = class SubscribeModal extends ModalView
|
|||
|
||||
afterRender: ->
|
||||
super()
|
||||
@setupParentButtonPopover()
|
||||
@setupParentInfoPopover()
|
||||
|
||||
setupParentButtonPopover: ->
|
||||
popoverTitle = $.i18n.t 'subscribe.parent_email_title'
|
||||
popoverTitle += '<button type="button" class="close" onclick="$('.parent-button').popover('hide');">×</button>'
|
||||
popoverContent = "<div id='email-parent-form'>"
|
||||
popoverContent += "<p>#{$.i18n.t('subscribe.parent_email_description')}</p>"
|
||||
popoverContent += "<form>"
|
||||
popoverContent += " <div class='form-group'>"
|
||||
popoverContent += " <label>#{$.i18n.t('subscribe.parent_email_input_label')}</label>"
|
||||
popoverContent += " <input id='parent-input' type='email' class='form-control' placeholder='#{$.i18n.t('subscribe.parent_email_input_placeholder')}'/>"
|
||||
popoverContent += " <div id='parent-email-validator' class='email_invalid'>#{$.i18n.t('subscribe.parent_email_input_invalid')}</div>"
|
||||
popoverContent += " </div>"
|
||||
popoverContent += " <button id='parent-send' type='submit' class='btn btn-default'>#{$.i18n.t('subscribe.parent_email_send')}</button>"
|
||||
popoverContent += "</form>"
|
||||
popoverContent += "</div>"
|
||||
popoverContent += "<div id='email-parent-complete'>"
|
||||
popoverContent += " <p>#{$.i18n.t('subscribe.parent_email_sent')}</p>"
|
||||
popoverContent += " <button type='button' onclick='$('.parent-button').popover('hide');'>#{$.i18n.t('modal.close')}</button>"
|
||||
popoverContent += "</div>"
|
||||
|
||||
@$el.find('.parent-button').popover(
|
||||
animation: true
|
||||
html: true
|
||||
placement: 'top'
|
||||
trigger: 'click'
|
||||
title: popoverTitle
|
||||
content: popoverContent
|
||||
container: @$el
|
||||
).on 'shown.bs.popover', =>
|
||||
application.tracker?.trackEvent 'Subscription ask parent button click', {}
|
||||
|
||||
setupParentInfoPopover: ->
|
||||
popoverTitle = $.i18n.t 'subscribe.parents_title'
|
||||
popoverContent = "<p>" + $.i18n.t('subscribe.parents_blurb1') + "</p>"
|
||||
popoverContent += "<p>" + $.i18n.t('subscribe.parents_blurb2') + "</p>"
|
||||
|
@ -50,6 +85,26 @@ module.exports = class SubscribeModal extends ModalView
|
|||
).on 'shown.bs.popover', =>
|
||||
application.tracker?.trackEvent 'Subscription parent hover', {}
|
||||
|
||||
onClickParentSendButton: (e) ->
|
||||
# TODO: Popover sometimes dismisses immediately after send
|
||||
|
||||
email = $('#parent-input').val()
|
||||
unless /[\w\.]+@\w+\.\w+/.test email
|
||||
$('#parent-input').parent().addClass('has-error')
|
||||
$('#parent-email-validator').show()
|
||||
return false
|
||||
|
||||
request = @supermodel.addRequestResource 'send_one_time_email', {
|
||||
url: '/db/user/-/send_one_time_email'
|
||||
data: {email: email, type: 'subscribe modal parent'}
|
||||
method: 'POST'
|
||||
}, 0
|
||||
request.load()
|
||||
|
||||
$('#email-parent-form').hide()
|
||||
$('#email-parent-complete').show()
|
||||
false
|
||||
|
||||
onClickPurchaseButton: (e) ->
|
||||
@playSound 'menu-button-click'
|
||||
return @openModalView new AuthModal() if me.get('anonymous')
|
||||
|
|
|
@ -16,6 +16,7 @@ module.exports = class CampaignLevelView extends CocoView
|
|||
'dblclick .recent-session': 'onDblClickRecentSession'
|
||||
'mouseenter .graph-point': 'onMouseEnterPoint'
|
||||
'mouseleave .graph-point': 'onMouseLeavePoint'
|
||||
'click .replay-button': 'onClickReplay'
|
||||
|
||||
constructor: (options, @level) ->
|
||||
super(options)
|
||||
|
@ -77,6 +78,11 @@ module.exports = class CampaignLevelView extends CocoView
|
|||
pointID = $(e.target).data('pointid')
|
||||
@$el.find(".graph-point-info-container[data-pointid=#{pointID}]").hide()
|
||||
|
||||
onClickReplay: (e) ->
|
||||
sessionID = $(e.target).closest('tr').data 'session-id'
|
||||
url = "/play/level/#{@level.get('slug')}?session=#{sessionID}&observing=true"
|
||||
window.open url, '_blank'
|
||||
|
||||
updateAnalyticsGraphData: ->
|
||||
# console.log 'updateAnalyticsGraphData'
|
||||
# Build graphs based on available @analytics data
|
||||
|
|
|
@ -91,7 +91,7 @@ module.exports = class CampaignView extends RootView
|
|||
|
||||
destroy: ->
|
||||
@setupManager?.destroy()
|
||||
@$el.find('.ui-draggable').draggable 'destroy'
|
||||
@$el.find('.ui-draggable').off().draggable 'destroy'
|
||||
$(window).off 'resize', @onWindowResize
|
||||
if ambientSound = @ambientSound
|
||||
# Doesn't seem to work; stops immediately.
|
||||
|
@ -124,6 +124,7 @@ module.exports = class CampaignView extends RootView
|
|||
@fullyRendered = true
|
||||
@render()
|
||||
@preloadTopHeroes() unless me.get('heroConfig')?.thangType
|
||||
@$el.find('#campaign-status').delay(4000).animate({top: "-=58"}, 1000) unless @terrain is 'dungeon'
|
||||
|
||||
setCampaign: (@campaign) ->
|
||||
@render()
|
||||
|
@ -189,7 +190,7 @@ module.exports = class CampaignView extends RootView
|
|||
_.defer => @$el?.find('.game-controls .btn').addClass('has-tooltip').tooltip() # Have to defer or i18n doesn't take effect.
|
||||
view = @
|
||||
@$el.find('.level, .campaign-switch').addClass('has-tooltip').tooltip().each ->
|
||||
return unless me.isAdmin()
|
||||
return unless me.isAdmin() and view.editorMode
|
||||
$(@).draggable().on 'dragstop', ->
|
||||
bg = $('.map-background')
|
||||
x = ($(@).offset().left - bg.offset().left + $(@).outerWidth() / 2) / bg.width()
|
||||
|
@ -285,12 +286,26 @@ module.exports = class CampaignView extends RootView
|
|||
@levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started'
|
||||
@render()
|
||||
|
||||
preloadLevel: (levelSlug) ->
|
||||
levelURL = "/db/level/#{levelSlug}"
|
||||
level = new Level().setURL levelURL
|
||||
level = @supermodel.loadModel(level, 'level', null, 0).model
|
||||
sessionURL = "/db/level/#{levelSlug}/session"
|
||||
@preloadedSession = new LevelSession().setURL sessionURL
|
||||
@preloadedSession.levelSlug = levelSlug
|
||||
@preloadedSession.fetch()
|
||||
@listenToOnce @preloadedSession, 'sync', @onSessionPreloaded
|
||||
|
||||
onSessionPreloaded: (session) ->
|
||||
levelElement = @$el.find('.level-info-container:visible')
|
||||
return unless session.levelSlug is levelElement.data 'level-slug'
|
||||
return unless difficulty = session.get('state')?.difficulty
|
||||
badge = $("<span class='badge'>#{difficulty}</span>")
|
||||
levelElement.find('.start-level .badge').remove()
|
||||
levelElement.find('.start-level').append badge
|
||||
|
||||
onClickMap: (e) ->
|
||||
@$levelInfo?.hide()
|
||||
# Easy-ish way of figuring out coordinates for placing level dots.
|
||||
x = e.offsetX / @$el.find('.map-background').width()
|
||||
y = (1 - e.offsetY / @$el.find('.map-background').height())
|
||||
console.log " x: #{(100 * x).toFixed(2)}\n y: #{(100 * y).toFixed(2)}\n"
|
||||
|
||||
onClickLevel: (e) ->
|
||||
e.preventDefault()
|
||||
|
@ -304,6 +319,7 @@ module.exports = class CampaignView extends RootView
|
|||
@$levelInfo = @$el.find(".level-info-container[data-level-slug=#{levelSlug}]").show()
|
||||
@adjustLevelInfoPosition e
|
||||
@endHighlight()
|
||||
@preloadLevel levelSlug
|
||||
|
||||
onDoubleClickLevel: (e) ->
|
||||
return unless @editorMode
|
||||
|
@ -326,7 +342,9 @@ module.exports = class CampaignView extends RootView
|
|||
|
||||
startLevel: (levelElement) ->
|
||||
@setupManager?.destroy()
|
||||
@setupManager = new LevelSetupManager supermodel: @supermodel, levelID: levelElement.data('level-slug'), levelPath: levelElement.data('level-path'), levelName: levelElement.data('level-name'), hadEverChosenHero: @hadEverChosenHero, parent: @
|
||||
levelSlug = levelElement.data 'level-slug'
|
||||
session = @preloadedSession if @preloadedSession?.loaded and @preloadedSession.levelSlug is levelSlug
|
||||
@setupManager = new LevelSetupManager supermodel: @supermodel, levelID: levelSlug, levelPath: levelElement.data('level-path'), levelName: levelElement.data('level-name'), hadEverChosenHero: @hadEverChosenHero, parent: @, session: session
|
||||
@setupManager.open()
|
||||
@$levelInfo?.hide()
|
||||
|
||||
|
|
|
@ -64,6 +64,8 @@ module.exports = class ControlBarView extends CocoView
|
|||
c.multiplayerStatus = @multiplayerStatusManager?.status
|
||||
if @level.get 'replayable'
|
||||
c.levelDifficulty = @session.get('state')?.difficulty ? 0
|
||||
if @observing
|
||||
c.levelDifficulty = Math.max 0, c.levelDifficulty - 1 # Show the difficulty they won, not the next one.
|
||||
c.difficultyTitle = "#{$.i18n.t 'play.level_difficulty'}#{c.levelDifficulty}"
|
||||
@lastDifficulty = c.levelDifficulty
|
||||
c.spectateGame = @spectateGame
|
||||
|
|
|
@ -127,7 +127,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
load: ->
|
||||
@loadStartTime = new Date()
|
||||
@god = new God debugWorker: true
|
||||
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @opponentSessionID, team: @getQueryVariable('team')
|
||||
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @opponentSessionID, team: @getQueryVariable('team'), observing: @observing
|
||||
@listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded
|
||||
|
||||
trackLevelLoadEnd: ->
|
||||
|
|
|
@ -166,7 +166,10 @@ module.exports = class TomeView extends CocoView
|
|||
sessionState.flagHistory = _.filter sessionState.flagHistory ? [], (event) => event.team isnt (@options.session.get('team') ? 'humans')
|
||||
sessionState.lastUnsuccessfulSubmissionTime = new Date() if @options.level.get 'replayable'
|
||||
@options.session.set 'state', sessionState
|
||||
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload, realTime: realTime, submissionCount: sessionState.submissionCount ? 0, flagHistory: sessionState.flagHistory ? [], difficulty: sessionState.difficulty ? 0
|
||||
difficulty = sessionState.difficulty ? 0
|
||||
if @options.observing
|
||||
difficulty = Math.max 0, difficulty - 1 # Show the difficulty they won, not the next one.
|
||||
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload, realTime: realTime, submissionCount: sessionState.submissionCount ? 0, flagHistory: sessionState.flagHistory ? [], difficulty: difficulty
|
||||
|
||||
onToggleSpellList: (e) ->
|
||||
@spellList.rerenderEntries()
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
log = require 'winston'
|
||||
mongoose = require 'mongoose'
|
||||
plugins = require '../plugins/plugins'
|
||||
utils = require '../lib/utils'
|
||||
|
||||
AnalyticsLogEventSchema = new mongoose.Schema({
|
||||
u: mongoose.Schema.Types.ObjectId
|
||||
|
@ -14,4 +16,102 @@ AnalyticsLogEventSchema = new mongoose.Schema({
|
|||
|
||||
AnalyticsLogEventSchema.index({event: 1, _id: 1})
|
||||
|
||||
AnalyticsLogEventSchema.statics.logEvent = (user, event, properties) ->
|
||||
unless user?
|
||||
log.warn 'No user given to analytics logEvent.'
|
||||
return
|
||||
|
||||
saveDoc = (eventID, slimProperties) ->
|
||||
doc = new AnalyticsLogEvent
|
||||
u: user
|
||||
e: eventID
|
||||
p: slimProperties
|
||||
# TODO: Remove these legacy properties after we stop querying for them (probably 30 days, ~2/16/15)
|
||||
user: user
|
||||
event: event
|
||||
properties: properties
|
||||
doc.save()
|
||||
|
||||
utils.getAnalyticsStringID event, (eventID) ->
|
||||
if eventID > 0
|
||||
# TODO: properties slimming is pretty ugly
|
||||
slimProperties = _.cloneDeep properties
|
||||
if event in ['Clicked Level', 'Show problem alert', 'Started Level', 'Saw Victory', 'Problem alert help clicked', 'Spell palette help clicked']
|
||||
delete slimProperties.level if event is 'Saw Victory'
|
||||
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
||||
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
||||
if slimProperties.levelID?
|
||||
# levelID: string => l: string ID
|
||||
utils.getAnalyticsStringID slimProperties.levelID, (levelStringID) ->
|
||||
if levelStringID > 0
|
||||
delete slimProperties.levelID
|
||||
slimProperties.l = levelStringID
|
||||
saveDoc eventID, slimProperties
|
||||
return
|
||||
else if event in ['Script Started', 'Script Ended']
|
||||
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
||||
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
||||
if slimProperties.levelID? and slimProperties.label?
|
||||
# levelID: string => l: string ID
|
||||
# label: string => lb: string ID
|
||||
utils.getAnalyticsStringID slimProperties.levelID, (levelStringID) ->
|
||||
if levelStringID > 0
|
||||
delete slimProperties.levelID
|
||||
slimProperties.l = levelStringID
|
||||
utils.getAnalyticsStringID slimProperties.label, (labelStringID) ->
|
||||
if labelStringID > 0
|
||||
delete slimProperties.label
|
||||
slimProperties.lb = labelStringID
|
||||
saveDoc eventID, slimProperties
|
||||
return
|
||||
else if event is 'Heard Sprite'
|
||||
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
||||
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
||||
if slimProperties.message?
|
||||
# message: string => m: string ID
|
||||
utils.getAnalyticsStringID slimProperties.message, (messageStringID) ->
|
||||
if messageStringID > 0
|
||||
delete slimProperties.message
|
||||
slimProperties.m = messageStringID
|
||||
saveDoc eventID, slimProperties
|
||||
return
|
||||
else if event in ['Start help video', 'Finish help video']
|
||||
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
||||
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
||||
if slimProperties.level and slimProperties.style?
|
||||
# level: string => l: string ID
|
||||
# style: string => s: string ID
|
||||
utils.getAnalyticsStringID slimProperties.level, (levelStringID) ->
|
||||
if levelStringID > 0
|
||||
delete slimProperties.level
|
||||
slimProperties.l = levelStringID
|
||||
utils.getAnalyticsStringID slimProperties.style, (styleStringID) ->
|
||||
if styleStringID > 0
|
||||
delete slimProperties.style
|
||||
slimProperties.s = styleStringID
|
||||
saveDoc eventID, slimProperties
|
||||
return
|
||||
else if event is 'Show subscription modal'
|
||||
delete properties.category
|
||||
delete slimProperties.category
|
||||
if slimProperties.label?
|
||||
# label: string => lb: string ID
|
||||
utils.getAnalyticsStringID slimProperties.label, (labelStringID) ->
|
||||
if labelStringID > 0
|
||||
delete slimProperties.label
|
||||
slimProperties.lb = labelStringID
|
||||
if slimProperties.level?
|
||||
# level: string => l: string ID
|
||||
utils.getAnalyticsStringID slimProperties.level, (levelStringID) ->
|
||||
if levelStringID > 0
|
||||
delete slimProperties.level
|
||||
slimProperties.l = levelStringID
|
||||
saveDoc eventID, slimProperties
|
||||
return
|
||||
saveDoc eventID, slimProperties
|
||||
return
|
||||
saveDoc eventID, slimProperties
|
||||
else
|
||||
log.warn "Unable to get analytics string ID for " + event
|
||||
|
||||
module.exports = AnalyticsLogEvent = mongoose.model('analytics.log.event', AnalyticsLogEventSchema)
|
||||
|
|
|
@ -39,102 +39,7 @@ class AnalyticsLogEventHandler extends Handler
|
|||
event = req.query.event or req.body.event
|
||||
properties = req.query.properties or req.body.properties
|
||||
@sendSuccess res # Return request immediately
|
||||
unless user?
|
||||
log.warn 'No user given to analytics logEvent.'
|
||||
return
|
||||
|
||||
saveDoc = (eventID, slimProperties) ->
|
||||
doc = new AnalyticsLogEvent
|
||||
u: user
|
||||
e: eventID
|
||||
p: slimProperties
|
||||
# TODO: Remove these legacy properties after we stop querying for them (probably 30 days, ~2/16/15)
|
||||
user: user
|
||||
event: event
|
||||
properties: properties
|
||||
doc.save()
|
||||
|
||||
utils.getAnalyticsStringID event, (eventID) ->
|
||||
if eventID > 0
|
||||
# TODO: properties slimming is pretty ugly
|
||||
slimProperties = _.cloneDeep properties
|
||||
if event in ['Clicked Level', 'Show problem alert', 'Started Level', 'Saw Victory', 'Problem alert help clicked', 'Spell palette help clicked']
|
||||
delete slimProperties.level if event is 'Saw Victory'
|
||||
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
||||
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
||||
if slimProperties.levelID?
|
||||
# levelID: string => l: string ID
|
||||
utils.getAnalyticsStringID slimProperties.levelID, (levelStringID) ->
|
||||
if levelStringID > 0
|
||||
delete slimProperties.levelID
|
||||
slimProperties.l = levelStringID
|
||||
saveDoc eventID, slimProperties
|
||||
return
|
||||
else if event in ['Script Started', 'Script Ended']
|
||||
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
||||
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
||||
if slimProperties.levelID? and slimProperties.label?
|
||||
# levelID: string => l: string ID
|
||||
# label: string => lb: string ID
|
||||
utils.getAnalyticsStringID slimProperties.levelID, (levelStringID) ->
|
||||
if levelStringID > 0
|
||||
delete slimProperties.levelID
|
||||
slimProperties.l = levelStringID
|
||||
utils.getAnalyticsStringID slimProperties.label, (labelStringID) ->
|
||||
if labelStringID > 0
|
||||
delete slimProperties.label
|
||||
slimProperties.lb = labelStringID
|
||||
saveDoc eventID, slimProperties
|
||||
return
|
||||
else if event is 'Heard Sprite'
|
||||
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
||||
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
||||
if slimProperties.message?
|
||||
# message: string => m: string ID
|
||||
utils.getAnalyticsStringID slimProperties.message, (messageStringID) ->
|
||||
if messageStringID > 0
|
||||
delete slimProperties.message
|
||||
slimProperties.m = messageStringID
|
||||
saveDoc eventID, slimProperties
|
||||
return
|
||||
else if event in ['Start help video', 'Finish help video']
|
||||
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
||||
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
||||
if slimProperties.level and slimProperties.style?
|
||||
# level: string => l: string ID
|
||||
# style: string => s: string ID
|
||||
utils.getAnalyticsStringID slimProperties.level, (levelStringID) ->
|
||||
if levelStringID > 0
|
||||
delete slimProperties.level
|
||||
slimProperties.l = levelStringID
|
||||
utils.getAnalyticsStringID slimProperties.style, (styleStringID) ->
|
||||
if styleStringID > 0
|
||||
delete slimProperties.style
|
||||
slimProperties.s = styleStringID
|
||||
saveDoc eventID, slimProperties
|
||||
return
|
||||
else if event is 'Show subscription modal'
|
||||
delete properties.category
|
||||
delete slimProperties.category
|
||||
if slimProperties.label?
|
||||
# label: string => lb: string ID
|
||||
utils.getAnalyticsStringID slimProperties.label, (labelStringID) ->
|
||||
if labelStringID > 0
|
||||
delete slimProperties.label
|
||||
slimProperties.lb = labelStringID
|
||||
if slimProperties.level?
|
||||
# level: string => l: string ID
|
||||
utils.getAnalyticsStringID slimProperties.level, (levelStringID) ->
|
||||
if levelStringID > 0
|
||||
delete slimProperties.level
|
||||
slimProperties.l = levelStringID
|
||||
saveDoc eventID, slimProperties
|
||||
return
|
||||
saveDoc eventID, slimProperties
|
||||
return
|
||||
saveDoc eventID, slimProperties
|
||||
else
|
||||
log.warn "Unable to get analytics string ID for " + event
|
||||
AnalyticsLogEvent.logEvent user, event, properties
|
||||
|
||||
getLevelCompletionsBySlug: (req, res) ->
|
||||
# Returns an array of per-day level starts and finishes
|
||||
|
@ -204,7 +109,7 @@ class AnalyticsLogEventHandler extends Handler
|
|||
for level of levelDateMap
|
||||
completions[level] = []
|
||||
for created, item of levelDateMap[level]
|
||||
completions[level].push
|
||||
completions[level].push
|
||||
level: level
|
||||
created: created
|
||||
started: Object.keys(item.started).length
|
||||
|
@ -382,7 +287,7 @@ class AnalyticsLogEventHandler extends Handler
|
|||
getUserEventData campaigns
|
||||
|
||||
getCampaignData = () =>
|
||||
# Get campaign data
|
||||
# Get campaign data
|
||||
# Output:
|
||||
# campaigns - per-campaign dictionary of ordered levelIDs
|
||||
# campaignLevelIDs - dictionary of all campaign levelIDs
|
||||
|
|
|
@ -10,6 +10,7 @@ module.exports.api = new sendwithusAPI swuAPIKey, debug
|
|||
if config.unittest
|
||||
module.exports.api.send = ->
|
||||
module.exports.templates =
|
||||
parent_subscribe_email: 'tem_2APERafogvwKhmcnouigud'
|
||||
welcome_email: 'utnGaBHuSU4Hmsi7qrAypU'
|
||||
ladder_update_email: 'JzaZxf39A4cKMxpPZUfWy4'
|
||||
patch_created: 'tem_xhxuNosLALsizTNojBjNcL'
|
||||
|
|
|
@ -9,6 +9,7 @@ errors = require '../commons/errors'
|
|||
async = require 'async'
|
||||
log = require 'winston'
|
||||
moment = require 'moment'
|
||||
AnalyticsLogEvent = require '../analytics/AnalyticsLogEvent'
|
||||
LevelSession = require '../levels/sessions/LevelSession'
|
||||
LevelSessionHandler = require '../levels/sessions/level_session_handler'
|
||||
SubscriptionHandler = require '../payments/subscription_handler'
|
||||
|
@ -17,6 +18,7 @@ EarnedAchievement = require '../achievements/EarnedAchievement'
|
|||
UserRemark = require './remarks/UserRemark'
|
||||
{isID} = require '../lib/utils'
|
||||
hipchat = require '../hipchat'
|
||||
sendwithus = require '../sendwithus'
|
||||
|
||||
serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset', 'lastIP']
|
||||
candidateProperties = [
|
||||
|
@ -232,6 +234,7 @@ UserHandler = class UserHandler extends Handler
|
|||
return @getRemark(req, res, args[0]) if args[1] is 'remark'
|
||||
return @searchForUser(req, res) if args[1] is 'admin_search'
|
||||
return @getStripeInfo(req, res, args[0]) if args[1] is 'stripe'
|
||||
return @sendOneTimeEmail(req, res, args[0]) if args[1] is 'send_one_time_email'
|
||||
return @sendNotFoundError(res)
|
||||
super(arguments...)
|
||||
|
||||
|
@ -244,6 +247,38 @@ UserHandler = class UserHandler extends Handler
|
|||
return @sendDatabaseError(res, err) if err
|
||||
@sendSuccess(res, JSON.stringify(customer, null, '\t'))
|
||||
|
||||
sendOneTimeEmail: (req, res) ->
|
||||
# TODO: should this API be somewhere else?
|
||||
# TODO: hipchat tower success message shows up as a misleading PaperTrail error message
|
||||
return @sendForbiddenError(res) unless req.user
|
||||
email = req.query.email or req.body.email
|
||||
type = req.query.type or req.body.type
|
||||
return @sendBadInputError res, 'No email given.' unless email?
|
||||
return @sendBadInputError res, 'No type given.' unless type?
|
||||
|
||||
return @sendBadInputError res, "Unknown one-time email type #{type}" unless type is 'subscribe modal parent'
|
||||
|
||||
emailParams =
|
||||
email_id: sendwithus.templates.parent_subscribe_email
|
||||
recipient:
|
||||
address: email
|
||||
email_data:
|
||||
name: req.user.get('name') or ''
|
||||
if codeLanguage = req.user.get('aceConfig.language')
|
||||
codeLanguage = codeLanguage[0].toUpperCase() + codeLanguage.slice(1)
|
||||
emailParams['email_data']['codeLanguage'] = codeLanguage
|
||||
sendwithus.api.send emailParams, (err, result) =>
|
||||
if err
|
||||
log.error "sendwithus one-time email error: #{err}, result: #{result}"
|
||||
return @sendError res, 500, 'send mail failed.'
|
||||
req.user.update {$push: {"emails.oneTimes": {type: type, email: email, sent: new Date()}}}, (err) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
req.user.save (err) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
@sendSuccess(res, {result: 'success'})
|
||||
hipchat.sendTowerHipChatMessage "#{req.user.get('name') or req.user.get('email')} just submitted subscribe modal parent email #{email}."
|
||||
AnalyticsLogEvent.logEvent req.user, 'Sent one time email', email: email, type: type
|
||||
|
||||
agreeToCLA: (req, res) ->
|
||||
return @sendForbiddenError(res) unless req.user
|
||||
doc =
|
||||
|
|
Loading…
Reference in a new issue