Merge branch 'master' into production

This commit is contained in:
Nick Winter 2014-12-18 20:39:29 -08:00
commit 5492e0b62a
38 changed files with 391 additions and 268 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View file

@ -477,12 +477,13 @@
contact: contact:
contact_us: "Contact CodeCombat" contact_us: "Contact CodeCombat"
welcome: "Good to hear from you! Use this form to send us email. " welcome: "Good to hear from you! Use this form to send us email. "
contribute_prefix: "If you're interested in contributing, check out our "
contribute_page: "contribute page"
contribute_suffix: "!"
forum_prefix: "For anything public, please try " forum_prefix: "For anything public, please try "
forum_page: "our forum" forum_page: "our forum"
forum_suffix: " instead." forum_suffix: " instead."
subscribe_prefix: "If you need help figuring out a level, please"
subscribe: "buy a CodeCombat subscription"
subscribe_suffix: "and we'll be happy to help you with your code."
subscriber_support: "Since you're a CodeCombat subscriber, your email will get our priority support."
where_reply: "Where should we reply?" where_reply: "Where should we reply?"
send: "Send Feedback" send: "Send Feedback"
contact_candidate: "Contact Candidate" # Deprecated contact_candidate: "Contact Candidate" # Deprecated

View file

@ -0,0 +1,25 @@
#contribute-view
.class_tile
position: relative
width: 330px
padding: 5px
float: left
&:hover img
outline: 3px solid #161a9e
.class_text
position: absolute
bottom: 5px
width: 300px
padding: 12px
z-index: 1
background-color: rgba(255,255,255,.5)
p
color: black
h3
color: black
padding-top: 0px
margin-top: 0px

View file

@ -2,6 +2,13 @@
#homepage_screenshot #homepage_screenshot
margin: 20px 0px margin: 20px 0px
.class_detail
float: left
img
width: 360px
.signature .signature
text-align: right text-align: right
@ -13,21 +20,9 @@
width: 150px width: 150px
margin: 10px 10px 20px 20px margin: 10px 10px 20px 20px
#contribute-nav
float: left
max-width: 20%
width: 250px
box-sizing: border-box
margin-left: 20px
padding-top: 40px
li
float: none
width: 100%
.class-main .class-main
margin-left: 25% margin-left: 33%
padding: 40px padding: 0px 40px 40px 40px
box-sizing: border-box box-sizing: border-box
.header-scrolling-fix .header-scrolling-fix

View file

@ -206,16 +206,8 @@ $level-resize-transition-time: 0.5s
@include opacity(1) @include opacity(1)
@media screen and (min-aspect-ratio: 17/10) @media screen and (min-aspect-ratio: 17/10)
display: none &:not(.premium)
display: none
.hour-of-code-explanation
margin-top: 5px
color: white
font-size: 12px
a
color: white
text-decoration: underline
#fullscreen-editor-background-screen #fullscreen-editor-background-screen
background-color: black background-color: black

View file

@ -104,13 +104,6 @@
&.btn-#{nth($tuple, 1)} &.btn-#{nth($tuple, 1)}
@include banner-button(nth($tuple, 2), #FFF) @include banner-button(nth($tuple, 2), #FFF)
.footer .footer-link-text a
@include opacity(0.75)
@include transition(opacity .10s linear)
&:hover, &:active
@include opacity(1)
$GI: 0.5 // gradient intensity; can tweak this 0-1 $GI: 0.5 // gradient intensity; can tweak this 0-1
.gradient .gradient
@ -152,22 +145,3 @@
top: 0 top: 0
height: 100% height: 100%
width: 2% width: 2%
.footer
@media screen and (min-aspect-ratio: 17/10)
display: none
&:not(:hover)
@include opacity(0.6)
.hour-of-code-explanation
margin-top: 5px
color: white
font-size: 12px
&:not(:hover)
@include opacity(0.75)
a
color: white
text-decoration: underline

View file

@ -4,12 +4,11 @@ block content
div.contribute_class div.contribute_class
include /templates/contribute/contribute_nav .class_detail
img(src="/images/pages/contribute/class_detail_adventurer.png", alt="")
div.class-main#adventurer-main div.class-main#adventurer-main
.class_image
img.img-responsive(src="/images/pages/contribute/adventurer.png", alt="")
h2 h2
span(data-i18n="classes.adventurer_title") Adventurer span(data-i18n="classes.adventurer_title") Adventurer

View file

@ -4,12 +4,11 @@ block content
div.contribute_class div.contribute_class
include /templates/contribute/contribute_nav .class_detail
img(src="/images/pages/contribute/class_detail_ambassador.png", alt="")
div.class-main#ambassador-main div.class-main#ambassador-main
.class_image
img.img-responsive(src="/images/pages/contribute/ambassador.png", alt="")
h2 h2
span(data-i18n="classes.ambassador_title") Ambassador span(data-i18n="classes.ambassador_title") Ambassador

View file

@ -4,13 +4,12 @@ block content
div.contribute_class div.contribute_class
include /templates/contribute/contribute_nav .class_detail
img(src="/images/pages/contribute/class_detail_archmage.png", alt="")
div.class-main#archmage-main div.class-main#archmage-main
.class_image
img.img-responsive(src="/images/pages/contribute/archmage.png", alt="")
h2 h2
span(data-i18n="classes.archmage_title") Archmage span(data-i18n="classes.archmage_title") Archmage
span span

View file

@ -4,12 +4,11 @@ block content
div.contribute_class div.contribute_class
include /templates/contribute/contribute_nav .class_detail
img(src="/images/pages/contribute/class_detail_artisan.png", alt="")
div.class-main#artisan-main div.class-main#artisan-main
.class_image
img.img-responsive(src="/images/pages/contribute/artisan.png", alt="")
h2 h2
span(data-i18n="classes.artisan_title") Artisan span(data-i18n="classes.artisan_title") Artisan

View file

@ -2,181 +2,77 @@ extends /templates/base
block content block content
div.contribute_class h2 Contributing
p CodeCombat is 100% open source and hundreds of dedicated players have helped us build the games
| into what it is today. Join us and write the next chapter in CodeCombat's quest to teach the
| world to code!
include /templates/contribute/contribute_nav a(href="/contribute/archmage")
div.class_tile
img(src="/images/pages/contribute/tile_archmage.png", alt="")
div#contribute-main.class-main div.class_text
div#intro h3 Archmage
h2(data-i18n="contribute.page_title") Contributing p(data-i18n="contribute.short_archmage")
| If you are a developer interested in coding educational games, become an archmage
| to help us build CodeCombat!
#homepage_screenshot a(href="/contribute/artisan")
img.img-responsive(src="/images/pages/contribute/contribute_header.png", alt="") div.class_tile
img.tile-img(src="/images/pages/contribute/tile_artisan.png", alt="")
p div.class_text
strong(data-i18n="contribute.introduction_desc_intro") h3 Artisan
| We have high hopes for CodeCombat.
|
span(data-i18n="contribute.introduction_desc_pref")
| We want to be where programmers of all stripes come to learn and play together,
| introduce others to the wonderful world of coding,
| and reflect the best parts of the community.
| We can't and don't want to do that alone;
| what makes projects like GitHub, Stack Overflow and Linux great are the people who
| use them and build on them.
| To that end,
a(href="https://github.com/codecombat/codecombat", data-i18n="contribute.introduction_desc_github_url")
| CodeCombat is totally open source
span(data-i18n="contribute.introduction_desc_suf")
| , and we aim to provide as many ways as possible for you to take part and
| make this project as much yours as ours.
p(data-i18n="contribute.introduction_desc_ending")
| We hope you'll join our party!
p(data-i18n="contribute.introduction_desc_signature").signature
| - Nick, George, Scott, Michael, and Matt
hr
.contributor-signup-anonymous p(data-i18n="contribute.short_artisan")
| Build and share levels for you and your friends to play. Become an Artisan to learn
| the art of teaching others to program.
#archmage.header-scrolling-fix a(href="/contribute/adventurer")
.class_image div.class_tile
img.img-responsive(src="/images/pages/contribute/archmage.png", alt="") img.tile-img(src="/images/pages/contribute/tile_adventurer.png", alt="")
h3.header-scrolling-fix div.class_text
span(data-i18n="classes.archmage_title") Archmage h3 Adventurer
span
span(data-i18n="classes.archmage_title_description") (Coder)
p(data-i18n="contribute.archmage_summary")
| Interested in working on game graphics, user interface design, database and server organization,
| multiplayer networking, physics, sound, or game engine performance? Want to help build a game to
| help other people learn what you are good at? We have a lot to do and if you are an experienced
| programmer and want to develop for CodeCombat, this class is for you. We would love your help
| building the best programming game ever.
a(href="/contribute/archmage") p(data-i18n="contribute.short_adventurer")
p.lead(data-i18n="contribute.more_about_archmage") | Get our new levels (even our subscriber content) for free one week early and help us
| Learn More About Becoming an Archmage | work out bugs before our public release.
.contributor-signup(data-contributor-class-id="developer", data-contributor-class-name="archmage") a(href="/contribute/scribe")
div.class_tile
img.tile-img(src="/images/pages/contribute/tile_scribe.png", alt="")
#artisan.header-scrolling-fix div.class_text
h3 Scribe
.class_image p(data-i18n="contribute.short_scribe")
img.img-responsive(src="/images/pages/contribute/artisan.png", alt="") | Good code needs good documentation. Write,
| edit, and improve the docs read by millions of players across the globe.
h3.header-scrolling-fix
span(data-i18n="classes.artisan_title") Artisan
span
span(data-i18n="classes.artisan_title_description") (Level Builder)
p
span(data-i18n="contribute.artisan_summary_pref")
| Want to design levels and expand CodeCombat's arsenal? People are playing through our
| content at a pace faster than we can build! Right now, our level editor is barebone,
| so be wary. Making levels will be a little challenging and buggy. If you have visions
| of campaigns spanning for-loops to
span
a(href="http://stackoverflow.com/questions/758088/seeking-contrived-example-code-continuations/758105#758105")
| Mondo Bizzaro
span(data-i18n="contribute.artisan_summary_suf")
| , then this class is for you.
a(href="/contribute/artisan") a(href="/contribute/diplomat")
p.lead(data-i18n="contribute.more_about_artisan")
| Learn More About Becoming An Artisan
.contributor-signup(data-contributor-class-id="level_creator", data-contributor-class-name="artisan") div.class_tile
img.tile-img(src="/images/pages/contribute/tile_diplomat.png", alt="")
#adventurer.header-scrolling-fix div.class_text
h3 Diplomat
.class_image p(data-i18n="contribute.short_diplomat")
img.img-responsive(src="/images/pages/contribute/adventurer.png", alt="") | CodeCombat is localized in 39 languages by our Diplomats. Help them
| out and contribute translations.
h3.header-scrolling-fix a(href="/contribute/ambassador")
span(data-i18n="classes.adventurer_title") Adventurer div.class_tile
span img.tile-img(src="/images/pages/contribute/tile_ambassador.png", alt="")
span(data-i18n="classes.adventurer_title_description") (Level Playtester)
p(data-i18n="contribute.adventurer_summary")
| Let us be clear about your role: you are the tank. You are going to take heavy damage.
| We need people to try out brand-new levels and help identify how to make things better.
| The pain will be enormous; making good games is a long process and no one gets
| it right the first time.
| If you can endure and have a high constitution score, then this class is for you.
a(href="/contribute/adventurer")
p.lead(data-i18n="contribute.more_about_adventurer")
| Learn More About Becoming an Adventurer
.contributor-signup(data-contributor-class-id="tester", data-contributor-class-name="adventurer") div.class_text
h3 Ambassador
#scribe.header-scrolling-fix
.class_image p(data-i18n="contribute.short_ambassador")
img.img-responsive(src="/images/pages/contribute/scribe.png", alt="") | Tame our forum users and provide direction for those with questions. Our ambassadors
| represent CodeCombat to the world.
h3.header-scrolling-fix div.clearfix
span(data-i18n="classes.scribe_title") Scribe
span
span(data-i18n="classes.scribe_title_description") (Article Editor)
p
span(data-i18n="contribute.scribe_summary_pref")
| CodeCombat is not just going to be a bunch of levels. It will also be a resource of
| programming knowledge that players can hook into. That way, each Artisan can link
| to a detailed article that for the player's edification:
| documentation akin to what the
a(href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide", data-i18n="contribute.scribe_introduction_url_mozilla")
| Mozilla Developer Network
span(data-i18n="contribute.scribe_summary_suf")
| has built. If you enjoy explaining programming concepts, then this class is for you.
a(href="/contribute/scribe")
p.lead(data-i18n="contribute.more_about_scribe")
| Learn More About Becoming a Scribe
.contributor-signup(data-contributor-class-id="article_editor", data-contributor-class-name="scribe")
#diplomat.header-scrolling-fix
.class_image
img.img-responsive(src="/images/pages/contribute/diplomat.png", alt="")
h3.header-scrolling-fix
span(data-i18n="classes.diplomat_title") Diplomat
span
span(data-i18n="classes.diplomat_title_description") (Translator)
p
span(data-i18n="contribute.diplomat_summary")
| There is a large interest in CodeCombat in other countries that do not speak English!
| We are looking for translators who are willing to spend their time translating the
| site's corpus of words so that CodeCombat is accessible across the world as soon as
| possible. If you'd like to help getting CodeCombat international, then this class is
| for you.
a(href="/contribute/diplomat")
p.lead(data-i18n="contribute.more_about_diplomat")
| Learn More About Becoming a Diplomat
.contributor-signup(data-contributor-class-id="translator", data-contributor-class-name="diplomat")
#ambassador.header-scrolling-fix
.class_image
img.img-responsive(src="/images/pages/contribute/ambassador.png", alt="")
h3.header-scrolling-fix
span(data-i18n="classes.ambassador_title") Ambassador
span
span(data-i18n="classes.ambassador_title_description") (Support)
p(data-i18n="contribute.ambassador_summary")
| We are trying to build a community, and every community needs a support team when
| there are troubles. We have got chats, emails, and social networks so that our users
| can get acquainted with the game. If you want to help people get involved, have fun,
| and learn some programming, then this c lass is for you.
a(href="/contribute/ambassador")
p.lead(data-i18n="contribute.more_about_ambassador")
| Learn More About Becoming an Ambassador
.contributor-signup(data-contributor-class-id="support", data-contributor-class-name="ambassador")
div.clearfix

View file

@ -4,12 +4,11 @@ block content
div.contribute_class div.contribute_class
include /templates/contribute/contribute_nav .class_detail
img(src="/images/pages/contribute/class_detail_diplomat.png", alt="")
div.class-main#diplomat-main div.class-main#diplomat-main
.class_image
img.img-responsive(src="/images/pages/contribute/diplomat.png", alt="")
h2 h2
span(data-i18n="classes.diplomat_title") Diplomat span(data-i18n="classes.diplomat_title") Diplomat

View file

@ -4,13 +4,12 @@ block content
div.contribute_class div.contribute_class
include /templates/contribute/contribute_nav .class_detail
img(src="/images/pages/contribute/class_detail_scribe.png", alt="")
div.class-main#scribe-main div.class-main#scribe-main
.class_image
img.img-responsive(src="/images/pages/contribute/scribe.png", alt="")
h2 h2
span(data-i18n="classes.scribe_title") Scribe span(data-i18n="classes.scribe_title") Scribe
span span

View file

@ -6,13 +6,16 @@ block modal-header-content
block modal-body-content block modal-body-content
p p
span(data-i18n="contact.welcome") Good to hear from you! Use this form to send us email. span(data-i18n="contact.welcome") Good to hear from you! Use this form to send us email.
span(data-i18n="contact.contribute_prefix") If you're interested in contributing, check out our span.spl(data-i18n="contact.forum_prefix") For anything public, please try
a(href="/contribute", data-dismiss="modal", data-i18n="contact.contribute_page") contribute page
span(data-i18n="contact.contribute_suffix") !
p
span(data-i18n="contact.forum_prefix") For anything public, please try
a(href="http://discourse.codecombat.com/", data-i18n="contact.forum_page") our forum a(href="http://discourse.codecombat.com/", data-i18n="contact.forum_page") our forum
span(data-i18n="contact.forum_suffix") instead. span(data-i18n="contact.forum_suffix") instead.
if me.isPremium()
p(data-i18n="contact.subscriber_support") Since you're a CodeCombat subscriber, your email will get our priority support.
else
p
span(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
span(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

@ -34,6 +34,7 @@
button.btn.btn-lg.btn-warning.banner.header-font#stop-real-time-playback-button(title="Stop real-time playback", data-i18n="play_level.skip") Skip button.btn.btn-lg.btn-warning.banner.header-font#stop-real-time-playback-button(title="Stop real-time playback", data-i18n="play_level.skip") Skip
#play-footer if !me.get('anonymous')
p(class='footer-link-text') #play-footer(class=me.isPremium() ? "premium" : "")
a(title='Send CodeCombat a message', tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="nav.contact") Contact p(class='footer-link-text')
a(title='Send CodeCombat a message', tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="nav.contact") Contact

View file

@ -11,7 +11,3 @@
#level-chat-view #level-chat-view
#playback-view #playback-view
#thang-hud #thang-hud
.footer
.content
p(class='footer-link-text')
a(title='Send CodeCombat a message', tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="nav.contact") Contact

View file

@ -83,6 +83,9 @@
else else
span.player-name.spr= me.get('name') span.player-name.spr= me.get('name')
button#logout-button.btn.btn-illustrated.btn-warning(data-i18n="login.log_out") Log Out button#logout-button.btn.btn-illustrated.btn-warning(data-i18n="login.log_out") Log Out
if me.isPremium()
button.btn.btn-illustrated.btn-primary(data-i18n="nav.contact", data-toggle="coco-modal", data-target="core/ContactModal") Contact
button.btn.btn-lg.btn-inverse#volume-button(title="Adjust volume") button.btn.btn-lg.btn-inverse#volume-button(title="Adjust volume")
.glyphicon.glyphicon-volume-off .glyphicon.glyphicon-volume-off

View file

@ -1,7 +1,7 @@
RootView = require 'views/core/RootView' RootView = require 'views/core/RootView'
template = require 'templates/account/subscription-view' template = require 'templates/account/subscription-view'
CocoCollection = require 'collections/CocoCollection' CocoCollection = require 'collections/CocoCollection'
SubscribeModal = require 'views/play/modal/SubscribeModal' SubscribeModal = require 'views/core/SubscribeModal'
module.exports = class SubscriptionView extends RootView module.exports = class SubscriptionView extends RootView
id: "subscription-view" id: "subscription-view"
@ -33,7 +33,7 @@ module.exports = class SubscriptionView extends RootView
c.cost = "$#{(subscription.plan.amount/100).toFixed(2)}" c.cost = "$#{(subscription.plan.amount/100).toFixed(2)}"
if card = @stripeInfo.cards?.data?[0] if card = @stripeInfo.cards?.data?[0]
c.card = "#{card.brand}: x#{card.last4}" c.card = "#{card.brand}: x#{card.last4}"
c.stripeInfo = @stripeInfo c.stripeInfo = @stripeInfo
c.subscribed = me.get('stripe')?.planID c.subscribed = me.get('stripe')?.planID
c.active = me.isPremium() c.active = me.isPremium()

View file

@ -1,5 +1,5 @@
ModalView = require 'views/core/ModalView' ModalView = require 'views/core/ModalView'
template = require 'templates/play/modal/subscribe-modal' template = require 'templates/core/subscribe-modal'
stripeHandler = require 'core/services/stripe' stripeHandler = require 'core/services/stripe'
utils = require 'core/utils' utils = require 'core/utils'
AuthModal = require 'views/core/AuthModal' AuthModal = require 'views/core/AuthModal'

View file

@ -9,7 +9,7 @@ ThangType = require 'models/ThangType'
MusicPlayer = require 'lib/surface/MusicPlayer' MusicPlayer = require 'lib/surface/MusicPlayer'
storage = require 'core/storage' storage = require 'core/storage'
AuthModal = require 'views/core/AuthModal' AuthModal = require 'views/core/AuthModal'
SubscribeModal = require 'views/play/modal/SubscribeModal' SubscribeModal = require 'views/core/SubscribeModal'
Level = require 'models/Level' Level = require 'models/Level'
trackedHourOfCode = false trackedHourOfCode = false

View file

@ -1,7 +1,7 @@
CocoView = require 'views/core/CocoView' CocoView = require 'views/core/CocoView'
template = require 'templates/play/level/level_loading' template = require 'templates/play/level/level_loading'
utils = require 'core/utils' utils = require 'core/utils'
SubscribeModal = require 'views/play/modal/SubscribeModal' SubscribeModal = require 'views/core/SubscribeModal'
module.exports = class LevelLoadingView extends CocoView module.exports = class LevelLoadingView extends CocoView
id: 'level-loading-view' id: 'level-loading-view'

View file

@ -7,30 +7,33 @@
// For a given style: // For a given style:
// - Video completion rates (Not too interesting unless each level has all styles available) // - Video completion rates (Not too interesting unless each level has all styles available)
// - Video completion rates, per-level too // - Video completion rates, per-level too
// TODO: The rest of these.
// - Watched another video // - Watched another video
// - Level completion rates // - Level completion rates
// - Subscription coversion rates // - Subscription coversion totals
// TODO: The rest
// - How many people who start a level click the help button, and which one? // - How many people who start a level click the help button, and which one?
// - Need a hard start date when the help button presented // - Need a hard start date when the help button presented
// Intial production deploy completed at 12:42am 12/18/14 PST // 12:42am 12/18/14 PST - Intial production deploy completed
var testStartDate='2014-12-14T08:42:00.000Z'; var testStartDate = '2014-12-18T08:42:00.000Z';
// 12:29pm 12/18/14 PST - 2nd deploy w/ originals for dungeons-of-kithgard and second-kithmaze
// TODO: move this date up to avoid prod deploy transitional data messing with us.
// testStartDate = '2014-12-18T20:29:00.000Z';
testStartDate = '2014-12-18T22:29:00.000Z';
function printVideoCompletionRates() { function printVideoCompletionRates() {
print("Querying for help video events..."); print("Querying for help video events...");
var videosCursor = db['analytics.log.events'].find({ var videosCursor = db['analytics.log.events'].find({
$and: [ $and: [
{"created": { $gte: ISODate(testStartDate)}}, {"created": { $gte: ISODate(testStartDate)}},
{$or : [ {$or : [
{"event": "Start help video"}, {"event": "Start help video"},
{"event": "Finish help video"} {"event": "Finish help video"}
]} ]}
] ]
}); });
print("Building video progression data..."); print("Building video progression data...");
// Build: <style><level><userID><event> counts // Build: <style><level><userID><event> counts
var videoProgression = {}; var videoProgression = {};
@ -131,14 +134,242 @@ function printVideoCompletionRates() {
styleLevelCompletionRates.push(data); styleLevelCompletionRates.push(data);
} }
} }
styleLevelCompletionRates.sort(function(a,b) {return b['rate'] && a['rate'] ? b.rate - a.rate : 0;}); styleLevelCompletionRates.sort(function(a,b) {
if (a.level !== b.level) {
if (a.level < b.level) return -1;
else return 1;
}
return b['rate'] && a['rate'] ? b.rate - a.rate : 0;
});
print("Per-level style completion rates:"); print("Per-level style completion rates:");
for (var i = 0; i < styleLevelCompletionRates.length; i++) { for (var i = 0; i < styleLevelCompletionRates.length; i++) {
var item = styleLevelCompletionRates[i]; var item = styleLevelCompletionRates[i];
var msg = item.level + "\t" + item.style + "\t" + item.started + "\t" + item.finished; var msg = item.level + "\t" + item.style + (item.style === 'edited' ? "\t\t" : "\t") + item.started + "\t" + item.finished;
if (item['rate']) msg += "\t" + item.rate + "%"; if (item['rate']) msg += "\t" + item.rate + "%";
print(msg); print(msg);
} }
} }
printVideoCompletionRates();
function printWatchedAnotherVideoRates() {
// How useful is a style/level in yielding more video starts
// Algorithm:
// 1. Fetch all start/finish video events after test start date
// 2. Create a per-userID dictionary of user event history arrays
// 3. Sort each user event history array in ascending order. Now we have a video watching history, per-user.
// 4. Walk through each user's history
// a. Increment global count for level/style/event, for each level/style event in past history.
// b. Save current entry in the past history.
// 5. Sort by ascending level name, descending started count
// TODO: only attribute one start/finish per level to a user?
print("Querying for help video events...");
var videosCursor = db['analytics.log.events'].find({
$and: [
{"created": { $gte: ISODate(testStartDate)}},
{$or : [
{"event": "Start help video"},
{"event": "Finish help video"}
]}
]
});
print("Building per-user video progression data...");
// Find video progression per-user
// Build: <userID>[sorted style/event/level/date events]
var videoProgression = {};
while (videosCursor.hasNext()) {
var doc = videosCursor.next();
var event = doc.event;
var userID = doc.user.valueOf();
var created = doc.created
var levelID = doc.properties.level;
var style = doc.properties.style;
if (!videoProgression[userID]) videoProgression[userID] = [];
videoProgression[userID].push({
style: style,
level: levelID,
event: event,
created: created.toISOString()
})
}
// printjson(videoProgression);
print("Sorting per-user video progression data...");
for (userID in videoProgression) videoProgression[userID].sort(function (a,b) {return a.created < b.created ? -1 : 1});
print("Building per-level/style additional watched videos..");
var additionalWatchedVideos = {};
for (userID in videoProgression) {
// Walk user's history, and tally what preceded each historical entry
var userHistory = videoProgression[userID];
// printjson(userHistory);
var previouslyWatched = {};
for (var i = 0; i < userHistory.length; i++) {
// Walk previously watched events, and attribute to correct additionally watched entry
var item = userHistory[i];
var level = item.level;
var style = item.style;
var event = item.event;
var created = item.created;
for (previousLevel in previouslyWatched) {
for (previousStyle in previouslyWatched[previousLevel]) {
if (previousLevel === level) continue;
var previous = previouslyWatched[previousLevel];
// For previous level and style, 'event' followed it
if (!additionalWatchedVideos[previousLevel]) additionalWatchedVideos[previousLevel] = {};
if (!additionalWatchedVideos[previousLevel][previousStyle]) {
additionalWatchedVideos[previousLevel][previousStyle] = {};
}
// TODO: care which video watched next?
if (!additionalWatchedVideos[previousLevel][previousStyle][event]) {
additionalWatchedVideos[previousLevel][previousStyle][event] = 0;
}
additionalWatchedVideos[previousLevel][previousStyle][event]++;
if (previousLevel === 'the-second-kithmaze') {
print("Followed the-second-kithmaze " + userID + " " + level + " " + event + " " + created);
}
}
}
// Add level/style to previouslyWatched for this user
if (!previouslyWatched[level]) previouslyWatched[level] = {};
if (!previouslyWatched[level][style]) previouslyWatched[level][style] = true;
}
}
print("Sorting additional watched videos by started event counts...");
var additionalWatchedVideoByStarted = [];
for (levelID in additionalWatchedVideos) {
for (style in additionalWatchedVideos[levelID]) {
var started = 0;
var finished = 0;
for (event in additionalWatchedVideos[levelID][style]) {
if (event === "Start help video") started += additionalWatchedVideos[levelID][style][event];
else if (event === "Finish help video") finished += additionalWatchedVideos[levelID][style][event];
else throw new Error("Unknown event " + event);
}
var data = {
level: levelID,
style: style,
started: started,
finished: finished
};
if (finished > 0) data['rate'] = finished / started * 100;
additionalWatchedVideoByStarted.push(data);
}
}
additionalWatchedVideoByStarted.sort(function(a,b) {
if (a.level !== b.level) {
if (a.level < b.level) return -1;
else return 1;
}
return b.started - a.started;
});
print("Per-level additional videos watched:");
print("For a given level and style, this is how many more videos were started and finished.");
print("Columns: level, style, started, finished, additionals completion rate");
for (var i = 0; i < additionalWatchedVideoByStarted.length; i++) {
var item = additionalWatchedVideoByStarted[i];
var msg = item.level + "\t" + item.style + (item.style === 'edited' ? "\t\t" : "\t") + item.started + "\t" + item.finished;
if (item['rate']) msg += "\t" + item.rate + "%";
print(msg);
}
}
function printSubConversionTotals() {
// For a user, who started a video, did they subscribe afterwards?
// Find each started event, per user
print("Querying for help video start events...");
var eventsCursor = db['analytics.log.events'].find({
$and: [
{"created": { $gte: ISODate(testStartDate)}},
{$or : [
{"event": "Start help video"},
{"event": "Finished subscription purchase"}
]}
]
});
print("Building per-user events progression data...");
// Find event progression per-user
var eventsProgression = {};
while (eventsCursor.hasNext()) {
var doc = eventsCursor.next();
var event = doc.event;
var userID = doc.user.valueOf();
var created = doc.created
var levelID = doc.properties.level;
var style = doc.properties.style;
if (!eventsProgression[userID]) eventsProgression[userID] = [];
eventsProgression[userID].push({
style: style,
level: levelID,
event: event,
created: created.toISOString()
})
// if (event === 'Finished subscription purchase')
// printjson(eventsProgression[userID]);
}
// printjson(eventsProgression);
print("Sorting per-user events progression data...");
for (userID in eventsProgression) eventsProgression[userID].sort(function (a,b) {return a.created < b.created ? -1 : 1});
print("Building per-level/style sub purchases..");
// Build: <level><style><count>
var subPurchaseCounts = {};
for (userID in eventsProgression) {
var history = eventsProgression[userID];
for (var i = 0; i < history.length; i++) {
if (history[i].event === 'Finished subscription purchase') {
var item = i > 0 ? history[i - 1] : {level: 'unknown', style: 'unknown'};
// if (i === 0) {
// print(userID);
// printjson(history[i]);
// }
if (!subPurchaseCounts[item.level]) subPurchaseCounts[item.level] = {};
if (!subPurchaseCounts[item.level][item.style]) subPurchaseCounts[item.level][item.style] = 0;
subPurchaseCounts[item.level][item.style]++;
}
}
}
// printjson(subPurchaseCounts);
print("Sorting per-level/style sub purchase counts...");
var subPurchasesByTotal = [];
for (levelID in subPurchaseCounts) {
for (style in subPurchaseCounts[levelID]) {
subPurchasesByTotal.push({
level: levelID,
style: style,
total: subPurchaseCounts[levelID][style]
})
}
}
subPurchasesByTotal.sort(function (a,b) {
if (a.level !== b.level) return a.level < b.level ? -1 : 1;
return b.total - a.total;
});
print("Per-level/style following sub purchases:");
print("Columns: level, style, following sub purchases.");
print("'unknown' means no preceding start help video event.");
for (var i = 0; i < subPurchasesByTotal.length; i++) {
var item = subPurchasesByTotal[i];
print(item.level + "\t" + item.style + (item.style === 'edited' ? "\t\t" : "\t") + item.total);
}
}
printVideoCompletionRates();
printWatchedAnotherVideoRates();
printSubConversionTotals();

View file

@ -14,17 +14,25 @@ module.exports.setup = (app) ->
return res.end() return res.end()
createMailContext = (sender, message, user, recipientID, subject, done) -> createMailContext = (sender, message, user, recipientID, subject, done) ->
level = if user?.get('points') > 0 then Math.floor(5 * Math.log((1 / 100) * (xp + 100))) + 1 else 0
premium = user?.isPremium()
content = """
#{message}
#{user.get('name') or 'Anonymous'} - Level #{level}#{if premium then ' - Subscriber' else ''} - #{user._id}
"""
context = context =
email_id: sendwithus.templates.plain_text_email email_id: sendwithus.templates.plain_text_email
recipient: recipient:
address: config.mail.username address: if premium then config.mail.supportPremium else config.mail.supportPrimary
sender: sender:
address: config.mail.username address: config.mail.username
reply_to: sender reply_to: sender
name: user.get('name') name: user.get('name')
email_data: email_data:
subject: "[CodeCombat] #{subject ? ('Feedback - ' + sender)}" subject: "[CodeCombat] #{subject ? ('Feedback - ' + sender)}"
content: "#{message}\n\nUsername: #{user.get('name') or 'Anonymous'}\nID: #{user._id}" content: content
if recipientID and (user.isAdmin() or ('employer' in (user.get('permissions') ? []))) if recipientID and (user.isAdmin() or ('employer' in (user.get('permissions') ? [])))
User.findById(recipientID, 'email').exec (err, document) -> User.findById(recipientID, 'email').exec (err, document) ->

View file

@ -36,6 +36,9 @@ else
config.mongo.password = process.env.COCO_MONGO_PASSWORD or '' config.mongo.password = process.env.COCO_MONGO_PASSWORD or ''
config.mail = config.mail =
username: process.env.COCO_MAIL_SERVICE_USERNAME or ''
supportPrimary: process.env.COCO_MAIL_SUPPORT_PRIMARY or ''
supportPremium: process.env.COCO_MAIL_SUPPORT_PREMIUM or ''
username: process.env.COCO_MAIL_SERVICE_USERNAME or '' username: process.env.COCO_MAIL_SERVICE_USERNAME or ''
mailchimpAPIKey: process.env.COCO_MAILCHIMP_API_KEY or '' mailchimpAPIKey: process.env.COCO_MAILCHIMP_API_KEY or ''
mailchimpWebhook: process.env.COCO_MAILCHIMP_WEBHOOK or '/mail/webhook' mailchimpWebhook: process.env.COCO_MAILCHIMP_WEBHOOK or '/mail/webhook'

View file

@ -58,11 +58,12 @@ setupExpressMiddleware = (app) ->
express.logger.format('prod', productionLogging) express.logger.format('prod', productionLogging)
app.use(express.logger('prod')) app.use(express.logger('prod'))
app.use express.compress filter: (req, res) -> app.use express.compress filter: (req, res) ->
return false if req.headers.host is 'codecombat.com' # CloudFlare will gzip it for us on codecombat.com
compressible res.getHeader('Content-Type') compressible res.getHeader('Content-Type')
else else
express.logger.format('dev', developmentLogging) express.logger.format('dev', developmentLogging)
app.use(express.logger('dev')) app.use(express.logger('dev'))
app.use(express.static(path.join(__dirname, 'public'))) app.use(express.static(path.join(__dirname, 'public'), maxAge: 30 * 60 * 1000))
app.use(useragent.express()) app.use(useragent.express())
app.use(express.favicon()) app.use(express.favicon())