Merge branch 'master' into production

This commit is contained in:
Nick Winter 2015-11-22 08:02:51 -08:00
commit 3b1f21eff5
13 changed files with 223 additions and 146 deletions

View file

@ -38,7 +38,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
okay: "Okay"
not_found:
page_not_found: "Tut uns leid ! Wir haben die Seite nicht gefunden"
page_not_found: "Tut uns leid! Wir haben die Seite nicht gefunden"
diplomat_suggestion:
title: "Hilf CodeCombat zu übersetzen!" # This shows up when a player switches to a non-English language using the language selector.
@ -62,11 +62,11 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
available: "Verfügbar"
skills_granted: "Verfügbare Fähigkeiten" # Property documentation details
heroes: "Helden" # Tooltip on hero shop button from /play
achievements: "Medaillen" # Tooltip on achievement list button from /play
achievements: "Errungenschaften" # Tooltip on achievement list button from /play
account: "Account" # Tooltip on account button from /play
settings: "Einstellungen" # Tooltip on settings button from /play
poll: "Umfrage" # Tooltip on poll button from /play
next: "Nächster" # Go from choose hero to choose inventory before playing a level
next: "Weiter" # Go from choose hero to choose inventory before playing a level
change_hero: "Held wechseln" # Go back from choose inventory to choose hero
choose_inventory: "Inventar"
buy_gems: "Edelsteine kaufen"
@ -138,7 +138,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
publish: "Veröffentlichen"
create: "Erstellen"
fork: "Kopieren"
play: "Nächstes Level starten" # When used as an action verb, like "Play next level"
play: "Spielen" # When used as an action verb, like "Play next level"
retry: "Erneut versuchen"
actions: "Aktionen"
info: "Informationen"
@ -263,10 +263,10 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
tome_minion_spells: "Die Zaubersprüche Deiner Knechte" # Only in old-style levels.
tome_read_only_spells: "Nur-lesen Zaubersprüche" # Only in old-style levels.
tome_other_units: "Andere Einheiten" # Only in old-style levels.
tome_cast_button_run: "Zaubern"
tome_cast_button_running: "Wird gezaubert"
tome_cast_button_ran: "Wurde gezaubert"
tome_submit_button: "Senden"
tome_cast_button_run: "Ausführen"
tome_cast_button_running: "Wird ausgeführt"
tome_cast_button_ran: "Wurde ausgeführt"
tome_submit_button: "Absenden"
tome_reload_method: "Original Code für diese Methode neu laden" # Title text for individual method reload button.
tome_select_method: "Methode auswählen"
tome_see_all_methods: "Alle bearbeitbaren Methoden anzeigen" # Title text for method list selector (shown when there are multiple programmable methods).
@ -281,11 +281,11 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
loading_ready: "Bereit!"
loading_start: "Starte Level"
problem_alert_title: "Repariere deinen Code"
time_current: "Aktuell"
time_total: "Total"
time_goto: "Gehe zu"
# non_user_code_problem_title: "Unable to Load Level"
# infinite_loop_title: "Infinite Loop Detected"
time_current: "Aktuell:"
time_total: "Gesamt:"
time_goto: "Gehe zu:"
non_user_code_problem_title: "Level konnte nicht geladen werden"
infinite_loop_title: "Unendliche Schleife entdeckt"
# infinite_loop_description: "The initial code to build the world never finished running. It's probably either really slow or has an infinite loop. Or there might be a bug. You can either try running this code again or reset the code to the default state. If that doesn't fix it, please let us know."
# check_dev_console: "You can also open the developer console to see what might be going wrong."
# check_dev_console_link: "(instructions)"
@ -361,16 +361,16 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
auth_caption: "Fortschritt speichern."
leaderboard:
view_other_solutions: "Andere Lösungen" # {change}
scores: "Punktzahl"
top_players: "Die besten Spieler von"
view_other_solutions: "Zur Bestenliste"
scores: "Bestenliste"
top_players: "Die besten Spieler nach"
day: "Heute"
week: "dieser Woche"
week: "diese Woche"
all: "insgesamt"
time: "Zeit"
damage_taken: "Erhaltener Schaden"
damage_dealt: "Ausgeteilter Schaden"
difficulty: "Schwierigkeit"
damage_taken: "Schaden (ausgeteilt)"
damage_dealt: "Schaden (erhalten)"
difficulty: "Schwierigkeitsgrad"
gold_collected: "Gold gesammelt"
inventory:
@ -379,10 +379,10 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
required_purchase_title: "Benötigt"
available_item: "Verfügbar"
restricted_title: "Eingeschränkt"
should_equip: "(Doppelklick zum Hinzufügen)"
equipped: "(hinzugefügt)"
should_equip: "(Doppelklicken zum Ausrüsten)"
equipped: "(ausgerüstet)"
locked: "(gesperrt)"
restricted: "(benötigt für dieses Level)"
restricted: "(In diesem Level nicht verfügbar)"
equip: "Ausrüsten"
unequip: "Ablegen"
@ -429,33 +429,33 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
parent_email_sent: "Email gesendet!"
parent_email_title: "Wie lautet die Emailadresse deiner Eltern?"
parents: "Für Eltern"
parents_title: "Dein Kind lernt zu programmieren." # {change}
parents_blurb1: "Mit CodeCombat, lernt dein Kind richtige Programme zu schreiben. Es fängt mit einfachen Befehlen an, und schreitet ganz unmerklich zu schwierigeren Themen fort."
# parents_blurb1a: "Computer programming is an essential skill that your child will undoubtedly use as an adult. By 2020, basic software skills will be needed by 77% of jobs, and software engineers are in high demand across the world. Did you know that Computer Science is the highest-paid university degree?"
parents_blurb2: "Für 9.99 im Monat, bekommt es jede Woche neue Herausforderungen sowie persönlichen Email Support von professionellen Programmierern." # {change}
parents_title: "Liebe Eltern: Ihr kind erlernt das Programmieren. Wollen Sie es nicht dabei unterstützen?"
parents_blurb1: "Ihr Kind hat __nLevels__ Level gemeistert und dabei Grundkenntnisse des Programmierens erlangt. Fördern Sie ihr Kind weiterhin, indem sie mit einem Abonnement weitere Herausforderungen freischalten."
parents_blurb1a: "Das Programmieren entwickelt sich zunehmend zu einem Grundwerkzeug, dass in immer mehr Berufen benötigt wird und ohne Zweifel sich im Leben als eine sehr nützliche Fähigkeit erweisen wird."
parents_blurb2: "Für 9.99 im Monat, bekommt Ihr Kind jede Woche neue Herausforderungen sowie persönlichen Email Support von erfahrenen Programmierern."
parents_blurb3: "Kein Risiko: 100% Geld zurück Garantie, und 1-Klick Abokündigung."
# payment_methods: "Payment Methods"
# payment_methods_title: "Accepted Payment Methods"
# payment_methods_blurb1: "We currently accept credit cards and Alipay."
# payment_methods_blurb2: "If you require an alternate form of payment, please contact"
# sale_already_subscribed: "You're already subscribed!"
# sale_blurb1: "Save 35%"
# sale_blurb2: "off regular subscription price of $120 for a whole year!"
# sale_button: "Sale!"
# sale_button_title: "Save 35% when you purchase a 1 year subscription"
# sale_click_here: "Click Here"
payment_methods: "Zahlungsarten"
payment_methods_title: "Akzeptierte Zahlungsarten"
payment_methods_blurb1: "Momentan akzeptieren wir nur Kreditkarten und Alipay."
payment_methods_blurb2: "Wenn Sie auf eine andere Art zahlen wollen, bitte kontaktieren Sie"
sale_already_subscribed: "Sie haben bereits ein Abonnement!"
sale_blurb1: "Spare 35%"
sale_blurb2: "vom regelmäßigen Abonnementpreis von $120 für ein ganzes Jahr!"
sale_button: "Angebot!"
sale_button_title: "Spare 35% beim Kauf eines Jahresabonnements"
sale_click_here: "Hier klicken"
# sale_ends: "Ends"
# sale_extended: "*Existing subscriptions will be extended by 1 year."
# sale_feature_here: "Here's what you'll get:"
sale_extended: "*Bestehende Abonnements werden um 1 Jahr verlängert."
sale_feature_here: "Dies ist im Abonnement enthalten:"
# sale_feature2: "Access to 9 powerful <strong>new heroes</strong> with unique skills!"
# sale_feature4: "<strong>42,000 bonus gems</strong> awarded immediately!"
# sale_continue: "Ready to continue adventuring?"
# sale_limited_time: "Limited time offer!"
# sale_new_heroes: "New heroes!"
# sale_title: "Back to School Sale"
# sale_view_button: "Buy 1 year subscription for"
sale_continue: "Bereit das Abenteuer fortzusetzen?"
sale_limited_time: "Nur für beschränkte Zeit!"
sale_new_heroes: "Neue Helden!"
sale_title: "Angebot zum neuen Schuljahr"
sale_view_button: "Kaufe ein Jahresabonnement für"
stripe_description: "Monatsabo"
# stripe_description_year_sale: "1 Year Subscription (35% discount)"
stripe_description_year_sale: "Jahresabonnement (35% Angebot)"
subscription_required_to_play: "Leider musst du ein Abo haben, um dieses Level spielen zu können."
unlock_help_videos: "Abonniere, um alle Videoanleitungen freizuschalten."
personal_sub: "Persönliches Abonnement" # Accounts Subscription View below
@ -482,7 +482,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
users_subscribed: "Abonnement für Spieler übernommen:"
no_users_subscribed: "Abonnement für keine Spieler übernommen, bitte prüfe deine E-Mail-Adressen."
current_recipients: "Aktuelle Empfänger"
unsubscribing: "Abmelden..." # {change}
unsubscribing: "Abonnement wird gekündigt..."
# subscribe_prepaid: "Click Subscribe to use prepaid code"
# using_prepaid: "Using prepaid code for monthly subscription"
@ -501,9 +501,9 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
status: "Status"
hero_type: "Typ"
weapons: "Waffen"
weapons_warrior: "Schwert - Kurze Reichweite, Keine Zauber"
weapons_ranger: "Armbrust, Geschütz - Hohe Reichweite, Keine Zauber"
weapons_wizard: "Stäbe, Stäbe - Lange Reichweite, Zauber"
weapons_warrior: "Schwerter - Kurze Reichweite, Keine Zauber"
weapons_ranger: "Schusswaffen - Hohe Reichweite, Keine Zauber"
weapons_wizard: "Stäbe - Lange Reichweite, Zauber"
attack: "Schaden" # Can also translate as "Attack"
health: "Gesundheit"
speed: "Geschwindigkeit"
@ -518,7 +518,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
health_1: "Erhält"
health_2: "der genannten"
health_3: "Rüstungspunkte."
speed_1: "Gehe zu"
speed_1: "Bewegt sich mit"
speed_2: "Meter pro Sekunde."
available_for_purchase: "Zum Kauf verfügbar" # Shows up when you have unlocked, but not purchased, a hero in the hero store
level_to_unlock: "Level zum Freischalten:" # Label for which level you have to beat to unlock a particular hero (click a locked hero in the store to see)
@ -607,26 +607,26 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
teachers:
more_info: "Info für Lehrer"
# intro_1: "CodeCombat is an online game that teaches programming. Students write code in real programming languages."
# intro_2: "No experience required!"
# free_title: "How much does it cost?"
intro_1: "CodeCombat ist ein Onlinespiel, dass Programmieren lehrt, indem es Schüler Code in gängigen Programmiersprachen schreiben lässt."
intro_2: "Keine Vorkenntnisse nötig!"
free_title: "Wie viel kostet es?"
# cost_premium_server: "CodeCombat is free for the first five levels, after which it costs $9.99 USD per month for access to our other 190+ levels on our exclusive country-specific servers."
# free_1: "There are 110+ FREE levels which cover every concept."
# free_2: "A monthly subscription provides access to video tutorials and extra practice levels."
# teacher_subs_title: "Teachers get free subscriptions!"
# teacher_subs_0: "We offer free subscriptions to teachers for evaluation purposes."
free_1: "Es gibt 110+ kostenlose Level, die alle Konzepte abedecken."
free_2: "Ein monatliches Abonnement verschafft Zugang zu Video-Tutorien and zusätzlichen Übungsleveln."
teacher_subs_title: "Für Lehrer ist das Abonnement kostenlos!"
teacher_subs_0: "Wir bieten Lehrern ein kostenloses Abonnement zu evaluirungszwecken."
# teacher_subs_1: "Please fill out our"
# teacher_subs_2: "Teacher Survey"
# teacher_subs_3: "to set up your subscription."
# sub_includes_title: "What is included in the subscription?"
sub_includes_title: "Was beinhaltet ein Abonnement?"
# sub_includes_1: "In addition to the 110+ basic levels, students with a monthly subscription get access to these additional features:"
# sub_includes_2: "80+ practice levels"
# sub_includes_3: "Video tutorials"
# sub_includes_4: "Premium email support"
# sub_includes_5: "10 new heroes with unique skills to master"
# sub_includes_6: "3500 bonus gems every month"
# sub_includes_7: "Private Clans"
# monitor_progress_title: "How do I monitor student progress?"
sub_includes_2: "80+ Übungslevel"
sub_includes_3: "Video-Tutorien"
sub_includes_4: "Email-Support"
sub_includes_5: "10 neue Helden zum Meistern"
sub_includes_6: "3500 Juwelen jeden Monat"
sub_includes_7: "Private Clans"
monitor_progress_title: "Wie verfolge ich die Fortschritte der Schüler?"
# monitor_progress_1: "Student progress can be monitored by creating a"
# monitor_progress_2: "for your class."
# monitor_progress_3: "To add a student, send them the invite link for your Clan, which is on the"
@ -635,32 +635,32 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
# private_clans_1: "Private Clans provide increased privacy and detailed progress information for each student."
# private_clans_2: "To create a private Clan, check the 'Make clan private' checkbox when creating a"
# private_clans_3: "."
# who_for_title: "Who is CodeCombat for?"
# who_for_1: "We recommend CodeCombat for students aged 9 and up. No prior programming experience is needed."
# who_for_2: "We've designed CodeCombat to appeal to both boys and girls."
# material_title: "How much material is there?"
# material_premium_server: "Approximately 50 hours of gameplay spread over 190+ subscriber-only levels so far."
# material_1: "Approximately 25 hours of free content and an additional 15 hours of subscriber content."
# concepts_title: "What concepts are covered?"
# how_much_title: "How much does a monthly subscription cost?"
who_for_title: "An wen richtet sich CodeCombat?"
who_for_1: "Wir empfehlen CodeCombat Schülern im Alter von 9 Jahren und älter. Es werden keine Vorkenntnisse vorausgesetzt."
who_for_2: "Wir haben CodeCombat so gestalltet, dass es sowohl Jungen als auch Mädchen ansprechend finden."
material_title: "Wie viel Lernstoff gibt es?"
material_premium_server: "Bislang gibt es ungefähr 50 Spielstunden verteilt über 190+ Level (mit Abonnement)."
material_1: "Ungefähr 25 Spielstunden kostenlosen Inhalts und zusätzliche 15 Spielstunden die mit einem Abonnement freigeschaltet werden."
concepts_title: "Welche Konzepte werde abgedeckt?"
how_much_title: "Wie viel kostet ein monatliches Abonnement?"
# how_much_1: "A"
# how_much_2: "monthly subscription"
# how_much_3: "costs $9.99, and can be cancelled anytime."
# how_much_4: "Additionally, we provide discounts for larger groups:"
# how_much_5: "We accept discounted one-time purchases and yearly subscription purchases for groups, such as a class or school. Please contact"
# how_much_6: "for more details."
# more_info_title: "Where can I find more information?"
more_info_title: "Wo kann ich mehr Information finden?"
# more_info_1: "Our"
# more_info_2: "teachers forum"
# more_info_3: "is a good place to connect with fellow educators who are using CodeCombat."
sys_requirements_title: "System Voraussetzungen"
# sys_requirements_1: "A modern web browser. Newer versions of Chrome, Firefox, or Safari. Internet Explorer 9 or later."
sys_requirements_2: "Nutzen Sie die neuesten Versionen von Google Chrome oder Firefox." # {change}
sys_requirements_1: "Ein aktueller Webbrowser. Neuere Versionen von Chrome, Firefox, oder Safari. Internet Explorer 9 oder aktueller."
sys_requirements_2: "CodeCombat wird auf dem iPad noch nicht unterstützt." # {change}
# teachers_survey:
# title: "Teacher Survey"
# must_be_logged: "You must be logged in first. Please create an account or log in from the menu above."
# retrieving: "Retrieving information..."
teachers_survey:
title: "Lehrerumfrage"
must_be_logged: "Sie müssen sich zunächst einloggen. Bitte erstellen Sie ein Account oder loggen Sie sich im oberen Menü ein."
retrieving: "Information abrufen..."
# being_reviewed_1: "Your application for a free trial subscription is being"
# being_reviewed_2: "reviewed."
# approved_1: "Your application for a free trial subscription was"
@ -720,21 +720,21 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
me_tab: "Ich"
picture_tab: "Bild"
delete_account_tab: "Account löschen"
wrong_email: "Falsche Email Adresse"
# wrong_password: "Wrong Password"
wrong_email: "Die Emailadresse ist falsch"
wrong_password: "Das Passwort ist falsch"
upload_picture: "Ein Bild hochladen"
delete_this_account: "Das Löschen deines Accounts kann nicht rückgängig gemacht werden!"
# reset_progress_tab: "Reset All Progress"
# reset_your_progress: "Clear all your progress and start over"
delete_this_account: "Den Account unwiderruflich löschen!"
reset_progress_tab: "Spielfortschritt zurücksetzen"
reset_your_progress: "Gesamten Fortschritt zurücksetzen und Spiel von vorn beginnen"
god_mode: "Gottmodus"
password_tab: "Passwort"
emails_tab: "Emails"
admin: "Admin"
new_password: "Neues Passwort"
new_password_verify: "Passwort verifizieren"
type_in_email: "Email eingeben, um Löschung zu bestätigen" # {change}
# type_in_email_progress: "Type in your email to confirm deleting your progress."
# type_in_password: "Also, type in your password."
type_in_email: "Email eingeben um das Löschen des Accounts zu bestätigen"
type_in_email_progress: "Gib zum Bestätigen Deine Email ein."
type_in_password: "Gib Dein Passwort ebenfalls ein."
email_subscriptions: "Email Abonnements"
email_subscriptions_none: "Keine Email Abonnements."
email_announcements: "Ankündigungen"
@ -979,8 +979,8 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
indoor: "Indoor"
desert: "Wüste"
grassy: "Gräsern"
# mountain: "Mountain"
# glacier: "Glacier"
mountain: "Berg"
glacier: "Gletscher"
small: "Klein"
large: "Groß"
fork_title: "Forke neue Version"

View file

@ -131,17 +131,6 @@ module.exports = class User extends CocoModel
application.tracker.identify fourthLevelGroup: @fourthLevelGroup unless me.isAdmin()
@fourthLevelGroup
getSubscriptionPromptGroup: ->
return @subscriptionPromptGroup if @subscriptionPromptGroup
group = me.get('testGroupNumber') % 3
@subscriptionPromptGroup = switch group
when 0 then 'favorable-odds'
when 1 then 'tactical-strike'
when 2 then 'boom-and-bust'
@subscriptionPromptGroup = 'favorable-odds' if me.isAdmin()
application.tracker.identify subscriptionPromptGroup: @subscriptionPromptGroup unless me.isAdmin()
@subscriptionPromptGroup
getVideoTutorialStylesIndex: (numVideos=0)->
# A/B Testing video tutorial styles
# Not a constant number of videos available (e.g. could be 0, 1, 3, or 4 currently)

View file

@ -8,6 +8,7 @@ _.extend ClassroomSchema.properties,
ownerID: c.objectId()
description: {type: 'string'}
code: c.shortString(title: "Unique code to redeem")
codeCamel: c.shortString(title: "UpperCamelCase version of code for display purposes")
aceConfig:
language: {type: 'string', 'enum': ['python', 'javascript']}

View file

@ -4,6 +4,7 @@
width: 330px
padding: 5px
float: left
z-index: 0
&:hover img
outline: 3px solid #161a9e

View file

@ -82,7 +82,8 @@
left: 219px
top: 21px
width: 571px
height: 514px
height: 495px
margin-top: 15px
padding: 50px
overflow-y: scroll

View file

@ -10,7 +10,7 @@ block modal-body-content
p(data-i18n="courses.invite_link_p_1")
.alert.alert-info
strong= document.location.origin + "/courses/students?_cc=" + view.classroom.get('code')
strong= document.location.origin + "/courses/students?_cc=" + (view.classroom.get('codeCamel') || view.classroom.get('code'))
p(data-i18n="courses.invite_link_p_2")
.form
.form-group

View file

@ -28,7 +28,7 @@ module.exports = class StudentCoursesView extends RootView
@supermodel.loadCollection(@classrooms, 'classrooms', { data: {memberID: me.id} })
@courses = new CocoCollection([], { url: "/db/course", model: Course})
@supermodel.loadCollection(@courses, 'courses')
onLoaded: ->
if (@classCode = utils.getQueryVariable('_cc', false)) and not me.isAnonymous()
@joinClass()
@ -63,17 +63,17 @@ module.exports = class StudentCoursesView extends RootView
onJoinClassroomSuccess: (data, textStatus, jqxhr) ->
classroom = new Classroom(data)
application.tracker?.trackEvent 'Joined classroom', {
classroomID: classroom.id,
classroomID: classroom.id,
classroomName: classroom.get('name')
ownerID: classroom.get('ownerID')
}
@classrooms.add(classroom)
@render()
classroomCourseInstances = new CocoCollection([], { url: "/db/course_instance", model: CourseInstance })
classroomCourseInstances.fetch({ data: {classroomID: classroom.id} })
@listenToOnce classroomCourseInstances, 'sync', ->
# join any course instances in the classroom which are free to join
jqxhrs = []
for courseInstance in classroomCourseInstances.models

View file

@ -315,10 +315,6 @@ module.exports = class CampaignView extends RootView
foundNext = false
dontPointTo = ['lost-viking', 'kithgard-mastery'] # Challenge levels we don't want most players bashing heads against
subscriptionPrompts = [{slug: 'boom-and-bust', unless: 'defense-of-plainswood'}]
if me.getSubscriptionPromptGroup() is 'favorable-odds'
subscriptionPrompts.push slug: 'favorable-odds', unless: 'the-raised-sword'
if me.getSubscriptionPromptGroup() is 'tactical-strike'
subscriptionPrompts.push slug: 'tactical-strike', unless: 'a-mayhem-of-munchkins'
for level in levels
# Iterate through all levels in order and look to find the first unlocked one that meets all our criteria for being pointed out as the next level.
level.nextLevels = (reward.level for reward in level.rewards ? [] when reward.level)

View file

@ -0,0 +1,58 @@
// subscriptionPromptGroup A/B Results
// Test started 2015-09-18, ended 2015-11-22
// Final results:
// Subscribers by group: {
// "tactical-strike": 246,
// "boom-and-bust": 255,
// "favorable-odds": 303
// }
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
// Except actually now you run these scripts on the analytics server itself.
// https://docs.google.com/document/d/1d5mOsTjioX2KRNAqhWXdGyBevuhxSPH1xX7UWlYPpwk/edit#
load('abTestHelpers.js');
var scriptStartTime = new Date();
try {
var logDB = new Mongo("localhost").getDB("analytics");
var startDay = '2015-09-18';
log("Today is " + new Date().toISOString().substr(0, 10));
log("Start day is " + startDay);
var eventFunnel = ['Started Level', 'Saw Victory'];
var levelSlugs = ['dungeons-of-kithgard', 'gems-in-the-deep', 'shadow-guard', 'forgetful-gemsmith', 'true-names', 'favorable-odds', 'the-raised-sword', 'lowly-kithmen', 'closing-the-distance', 'tactical-strike', 'a-mayhem-of-munchkins', 'kithgard-gates', 'boom-and-bust', 'defense-of-plainswood'];
// getSubscriptionPromptGroup
var testGroupFn = function (testGroupNumber) {
var group = testGroupNumber % 3;
if (group === 0) return 'favorable-odds';
if (group === 1) return 'tactical-strike';
if (group === 2) return 'boom-and-bust';
};
var funnelData = getFunnelData(startDay, eventFunnel, testGroupFn, levelSlugs, logDB);
printFunnelData(funnelData, function (day, level, browser, group, started, finished, rate) {
if (day && level && browser && group) {
log(day + "\t" + group + "\t" + started + "\t" + finished + "\t" + rate.toFixed(2));
}
else if (level && browser && group) {
log(level + "\t" + browser + "\t" + (browser.length < 8 ? "\t": "") + group + "\t" + started + "\t" + finished + "\t" + rate.toFixed(2));
}
else if (level && group) {
log(level + "\t" + group + "\t" + started + "\t" + finished + "\t" + rate.toFixed(2));
}
else if (group) {
log(group + "\t" + started + "\t" + finished + "\t" + rate.toFixed(2));
}
});
}
catch(err) {
log("ERROR: " + err);
printjson(err);
}
finally {
log("Script runtime: " + (new Date() - scriptStartTime));
}

View file

@ -14,6 +14,7 @@ var analyticsStringIDCache = {};
// *** Helper functions ***
function log(str) {
str = Array.prototype.slice.call(arguments).join(' ');
print(new Date().toISOString() + " " + str);
}
@ -24,7 +25,7 @@ function objectIdWithTimestamp(timestamp) {
var hexSeconds = Math.floor(timestamp/1000).toString(16);
// Create an ObjectId with that hex timestamp
var constructedObjectId = ObjectId(hexSeconds + "0000000000000000");
return constructedObjectId
return constructedObjectId;
}
function getAnalyticsString(strID) {
@ -47,7 +48,7 @@ function getAnalyticsStringID(str) {
throw new Error("ERROR: Did not find analytics.strings insert for: " + str);
}
function getFunnelData(startDay, eventFunnel, testGroupFn, levelSlugs) {
function getFunnelData(startDay, eventFunnel, testGroupFn, levelSlugs, logDB) {
if (!startDay || !eventFunnel || eventFunnel.length === 0 || !testGroupFn) return {};
// log('getFunnelData:');
@ -56,13 +57,14 @@ function getFunnelData(startDay, eventFunnel, testGroupFn, levelSlugs) {
var startObj = objectIdWithTimestamp(ISODate(startDay + "T00:00:00.000Z"));
var queryParams = {$and: [{_id: {$gte: startObj}},{"event": {$in: eventFunnel}}]};
var cursor = db['analytics.log.events'].find(queryParams);
var cursor = (logDB.log || db['analytics.log.events']).find(queryParams);
log("Fetching events..");
// Map ordering: level, user, event, day
var levelUserEventMap = {};
var levelSessions = [];
var users = [];
var eventsCounted = 0;
while (cursor.hasNext()) {
var doc = cursor.next();
var created = doc._id.getTimestamp().toISOString();
@ -72,10 +74,12 @@ function getFunnelData(startDay, eventFunnel, testGroupFn, levelSlugs) {
var user = doc.user.valueOf();
var level = 'n/a';
var ls = null;
if (eventsCounted++ % 10000 == 0)
log("Counted", eventsCounted, "events, up to", created);
// TODO: Switch to properties.levelID for 'Saw Victory'
if (event === 'Saw Victory' && properties.level) level = properties.level.toLowerCase().replace(/ /g, '-');
else if (properties.levelID) level = properties.levelID
else if (properties.levelID) level = properties.levelID;
if (levelSlugs && levelSlugs.indexOf(level) < 0) continue;
@ -101,33 +105,52 @@ function getFunnelData(startDay, eventFunnel, testGroupFn, levelSlugs) {
log("Fetching users..");
var userGroupMap = {};
cursor = db['users'].find({_id : {$in: users}});
while (cursor.hasNext()) {
var doc = cursor.next();
var user = doc._id.valueOf();
userGroupMap[user] = testGroupFn(doc.testGroupNumber);
var groupSubscribedMap = {};
var countedSubscriberMap = {};
for (var userOffset = 0; userOffset < users.length; userOffset += 1000) {
cursor = db['users'].find({_id : {$in: users.slice(userOffset, userOffset + 1000)}});
while (cursor.hasNext()) {
var doc = cursor.next();
var user = doc._id.valueOf();
userGroupMap[user] = group = testGroupFn(doc.testGroupNumber);
if (!countedSubscriberMap[doc._id + ''] &&
doc.created >= ISODate(startDay + "T00:00:00.000Z") &&
doc.stripe &&
doc.stripe.customerID &&
doc.purchased &&
doc.purchased.gems &&
!doc.stripe.free
) {
countedSubscriberMap[doc._id + ''] = true;
groupSubscribedMap[group] = (groupSubscribedMap[group] || 0) + 1;
}
}
log("Fetched", Math.min(userOffset, users.length), "users");
}
// printjson(userGroupMap);
log("Fetching level sessions..");
var lsBrowserMap = {};
var userBrowserMap = {};
cursor = db['level.sessions'].find({_id : {$in: levelSessions}});
while (cursor.hasNext()) {
var doc = cursor.next();
var user = doc._id.valueOf();
var browser = doc.browser;
var browserInfo = '';
if (browser && browser.platform) {
browserInfo += browser.platform;
}
if (browser && browser.name) {
browserInfo += browser.name;
}
if (browserInfo.length > 0) {
lsBrowserMap[doc._id.valueOf()] = browserInfo;
userBrowserMap[user] = browserInfo;
for (var sessionOffset = 0; sessionOffset < levelSessions.length; sessionOffset += 1000) {
cursor = db['level.sessions'].find({_id : {$in: levelSessions.slice(sessionOffset, sessionOffset + 1000)}});
while (cursor.hasNext()) {
var doc = cursor.next();
var user = doc._id.valueOf();
var browser = doc.browser;
var browserInfo = '';
if (browser && browser.platform) {
browserInfo += browser.platform;
}
if (browser && browser.name) {
browserInfo += browser.name;
}
if (browserInfo.length > 0) {
lsBrowserMap[doc._id.valueOf()] = browserInfo;
userBrowserMap[user] = browserInfo;
}
}
log("Fetched", Math.min(sessionOffset, levelSessions.length), "sessions");
}
// printjson(lsBrowserMap);
@ -238,6 +261,8 @@ function getFunnelData(startDay, eventFunnel, testGroupFn, levelSlugs) {
return a.group < b.group ? -1 : 1;
});
log("Subscribers by group:", JSON.stringify(groupSubscribedMap, null, 2));
return funnelData;
}

View file

@ -64,7 +64,7 @@ CampaignHandler = class CampaignHandler extends Handler
obj = @formatEntity(req, doc)
obj.adjacentCampaigns = _.mapValues(obj.adjacentCampaigns, (a) -> _.pick(a, ['showIfUnlocked', 'color', 'name', 'description' ]))
for original, level of obj.levels
obj.levels[original] = _.pick level, ['locked', 'disabled', 'original', 'rewards']
obj.levels[original] = _.pick level, ['locked', 'disabled', 'original', 'rewards', 'slug']
obj
documents = (formatCampaign(doc) for doc in documents)
@sendSuccess(res, documents)

View file

@ -14,12 +14,16 @@ ClassroomSchema.statics.editableProperties = [
'aceConfig'
]
# 250 words; will want to use 4 code words once we get past 10M classrooms.
words = 'angry apple arm army art baby back bad bag ball bath bean bear bed bell best big bird bite blue boat book box boy bread burn bus cake car cat chair city class clock cloud coat coin cold cook cool corn crash cup dark day deep desk dish dog door down draw dream drink drop dry duck dust east eat egg enemy eye face false farm fast fear fight find fire flag floor fly food foot fork fox free fruit full fun funny game gate gift glass goat gold good green hair half hand happy heart heavy help hide hill home horse house ice idea iron jelly job jump key king lamp large last late lazy leaf left leg life light lion lock long luck map mean milk mix moon more most mouth music name neck net new next nice night north nose old only open page paint pan paper park party path pig pin pink place plane plant plate play point pool power pull push queen rain ready red rest rice ride right ring road rock room run sad safe salt same sand sell shake shape share sharp sheep shelf ship shirt shoe shop short show sick side silly sing sink sit size sky sleep slow small snow sock soft soup south space speed spell spoon star start step stone stop sweet swim sword table team thick thin thing think today tooth top town tree true turn type under want warm watch water west wide win word yes zoo'.split(' ')
ClassroomSchema.statics.generateNewCode = (done) ->
tryCode = ->
code = _.sample("abcdefghijklmnopqrstuvwxyz0123456789", 8).join('')
codeCamel = _.map(_.sample(words, 3), (s) -> s[0].toUpperCase() + s.slice(1)).join('')
code = codeCamel.toLowerCase()
Classroom.findOne code: code, (err, classroom) ->
return done() if err
return done(code) unless classroom
return done(code, codeCamel) unless classroom
tryCode()
tryCode()
@ -27,14 +31,15 @@ ClassroomSchema.statics.generateNewCode = (done) ->
ClassroomSchema.pre('save', (next) ->
return next() if @get('code')
Classroom.generateNewCode (code) =>
Classroom.generateNewCode (code, codeCamel) =>
@set 'code', code
@set 'codeCamel', codeCamel
next()
)
ClassroomSchema.methods.isOwner = (userID) ->
return userID.equals(@get('ownerID'))
ClassroomSchema.methods.isMember = (userID) ->
return _.any @get('members') or [], (memberID) -> userID.equals(memberID)

View file

@ -51,7 +51,8 @@ ClassroomHandler = class ClassroomHandler extends Handler
joinClassroomAPI: (req, res, classroomID) ->
return @sendBadInputError(res, 'Need an object with a code') unless req.body?.code
Classroom.findOne {code: req.body.code}, (err, classroom) =>
code = req.body.code.toLowerCase()
Classroom.findOne {code: code}, (err, classroom) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) if not classroom
members = _.clone(classroom.get('members'))
@ -63,16 +64,16 @@ ClassroomHandler = class ClassroomHandler extends Handler
members.push req.user.get('_id')
classroom.set('members', members)
return @sendSuccess(res, @formatEntity(req, classroom))
formatEntity: (req, doc) ->
if req.user?.isAdmin() or req.user?.get('_id').equals(doc.get('ownerID'))
return doc.toObject()
return _.omit(doc.toObject(), 'code')
return _.omit(doc.toObject(), 'code', 'codeCamel')
inviteStudents: (req, res, classroomID) ->
if not req.body.emails
return @sendBadInputError(res, 'Emails not included')
Classroom.findById classroomID, (err, classroom) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless classroom
@ -86,10 +87,10 @@ ClassroomHandler = class ClassroomHandler extends Handler
email_data:
class_name: classroom.get('name')
# TODO: join_link
join_link: "https://codecombat.com/courses/students?_cc=" + classroom.get('code')
join_link: "https://codecombat.com/courses/students?_cc=" + (classroom.get('codeCamel') or classroom.get('code'))
sendwithus.api.send context, _.noop
return @sendSuccess(res, {})
get: (req, res) ->
if ownerID = req.query.ownerID
return @sendForbiddenError(res) unless req.user and (req.user.isAdmin() or ownerID is req.user.id)