Merge remote-tracking branch 'refs/remotes/codecombat/master'

This commit is contained in:
joshualu4 2015-12-04 12:11:08 -05:00
commit da6d07bc7d
88 changed files with 1420 additions and 531 deletions

View file

@ -1,6 +1,7 @@
publishableKey = if application.isProduction() then 'pk_live_27jQZozjDGN1HSUTnSuM578g' else 'pk_test_zG5UwVu6Ww8YhtE9ZYh0JO6a'
module.exports = handler = StripeCheckout.configure({
if StripeCheckout?
module.exports = handler = StripeCheckout.configure({
key: publishableKey
name: 'CodeCombat'
email: me.get('email')
@ -10,6 +11,8 @@ module.exports = handler = StripeCheckout.configure({
handler.trigger 'received-token', { token: token }
Backbone.Mediator.publish 'stripe:received-token', { token: token }
locale: 'auto'
})
})
else
module.exports = {}
console.error "Failure loading StripeCheckout API, returning empty object."
_.extend(handler, Backbone.Events)

View file

@ -20,6 +20,7 @@ module.exports = class LevelBus extends Bus
'tome:spell-changed': 'onSpellChanged'
'tome:spell-created': 'onSpellCreated'
'tome:cast-spells': 'onCastSpells'
'tome:winnability-updated': 'onWinnabilityUpdated'
'application:idle-changed': 'onIdleChanged'
'goal-manager:new-goal-states': 'onNewGoalStates'
'god:new-world-created': 'onNewWorldCreated'
@ -119,6 +120,11 @@ module.exports = class LevelBus extends Bus
@changedSessionProperties.state = true
@saveSession()
onWinnabilityUpdated: (e) ->
return unless @onPoint() and e.winnable
return unless e.level.get('slug') in ['ace-of-coders'] # Mirror matches don't otherwise show victory, so we win here.
@onVictory()
onNewWorldCreated: (e) ->
return unless @onPoint()
# Record the flag history.

View file

@ -10,6 +10,7 @@ CocoClass = require 'core/CocoClass'
AudioPlayer = require 'lib/AudioPlayer'
app = require 'core/application'
World = require 'lib/world/world'
utils = require 'core/utils'
# This is an initial stab at unifying loading and setup into a single place which can
# monitor everything and keep a LoadingScreen visible overall progress.
@ -105,6 +106,8 @@ module.exports = class LevelLoader extends CocoClass
loadDependenciesForSession: (session) ->
if me.id isnt session.get 'creator'
session.patch = session.save = -> console.error "Not saving session, since we didn't create it."
else if codeLanguage = utils.getQueryVariable 'codeLanguage'
session.set 'codeLanguage', codeLanguage
@loadCodeLanguagesForSession session
if session is @session
@addSessionBrowserInfo session

View file

@ -100,7 +100,6 @@ module.exports = class LevelSetupManager extends CocoClass
@options.parent.openModalView(firstModal)
# @inventoryModal.onShown() # replace?
@playSound 'game-menu-open'
#- Modal events

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

@ -51,6 +51,7 @@
play:
play_as: "Play As" # Ladder page
compete: "Compete!" # Course details page
spectate: "Spectate" # Ladder page
players: "players" # Hover over a level on /play
hours_played: "hours played" # Hover over a level on /play
@ -246,7 +247,7 @@
victory_title_suffix: " Complete"
victory_sign_up: "Sign Up to Save Progress"
victory_sign_up_poke: "Want to save your code? Create a free account!"
victory_rate_the_level: "Rate the level: " # Only in old-style levels.
victory_rate_the_level: "How fun was this level?" # {change}
victory_return_to_ladder: "Return to Ladder"
victory_play_continue: "Continue"
victory_saving_progress: "Saving Progress"
@ -663,16 +664,20 @@
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..."
being_reviewed_1: "Your application for a free trial subscription is being"
being_reviewed_1: "Your application for a free trial is being" # {change}
being_reviewed_2: "reviewed."
approved_1: "Your application for a free trial subscription was"
approved_1: "Your application for a free trial was" # {change}
approved_2: "approved."
approved_3: "Further instructions have been sent to"
denied_1: "Your application for a free trial subscription has been"
approved_4: "Enroll your students on the"
approved_5: "courses"
approved_6: "page."
denied_1: "Your application for a free trial has been" # {change}
denied_2: "denied."
contact_1: "Please contact"
contact_2: "if you have further questions."
description_1: "We offer free subscriptions to teachers for evaluation purposes. You can find more information on our"
description_1: "We offer free trials to teachers. You will be given 2 free enrollments which can be used to enroll students in paid courses." # {change}
description_1b: "You can find more information on our"
description_2: "teachers"
description_3: "page."
description_4: "Please fill out this quick survey and well email you setup instructions."

View file

@ -80,7 +80,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
adjust_volume: "Regola il volume"
campaign_multiplayer: "Arene multigiocatore"
campaign_multiplayer_description: "... nelle quali programmi faccia a faccia contro altri giocatori."
# campaign_old_multiplayer: "(Deprecated) Old Multiplayer Arenas"
campaign_old_multiplayer: "(Deprecato) Vecchia Arena multiplayer"
campaign_old_multiplayer_description: "Reliquie di un'epoca più civilizzata. Nessuna simulazione viene eseguita per queste arene multi-giocatore più vecchie e senza eroi" #"Relics of a more civilized age. No simulations are run for these older, hero-less multiplayer arenas."
share_progress_modal:
@ -218,7 +218,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
play_level:
done: "Fatto"
next_game: "Prossimo gioco" #"Next game"
# show_menu: "Show game menu"
show_menu: "Visualizza menu gioco"
home: "Pagina iniziale" # Not used any more, will be removed soon.
level: "Livello" # Like "Level: Dungeons of Kithgard"
skip: "Salta"
@ -306,8 +306,8 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
tip_baby_coders: "Nel futuro, persino i neonati saranno Arcimaghi."
tip_morale_improves: "Il caricamento continuerà fino a che il morale migliora." #"Loading will continue until morale improves."
tip_all_species: "Crediamo che chiunque debba avere le stesse opportunità di imparare a programmare."
# tip_reticulating: "Reticulating spines."
# tip_harry: "Yer a Wizard, "
tip_reticulating: "Reticolazione spine"
tip_harry: "Yer il mago, "
tip_great_responsibility: "Da grandi abilità di programmazione derivano grandi responsabilità."
tip_munchkin: "Se non mangi la tua verdura, un munchkin verrà a cercarti mentre dormi."
tip_binary: "Ci sono solo 10 tipi di persone al mondo: quelli che capiscono il binario, e quelli che non lo capiscono." #"There are only 10 types of people in the world: those who understand binary, and those who don't."
@ -335,7 +335,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
tip_move_forward: "Qualsiasi cosa tu faccia, vai sempre avanti. - Martin Luther King Jr."
tip_google: "Hai un problema che non riesci a risolvere? Cerca su Google!"
tip_adding_evil: "Aggiungendo un pizzico di malvagità."
# tip_hate_computers: "That's the thing about people who think they hate computers. What they really hate is lousy programmers. - Larry Niven"
tip_hate_computers: "A proposito delle persone che pensano di odiare i computer. Cio' che odiano realmente sono i programmatori scarsi - Larry Niven"
tip_open_source_contribute: "Puoi aiutare CodeCombat a migliorare!"
tip_recurse: "Iterare e umano, usare la ricorsione è divino. - L. Peter Deutsch"
tip_free_your_mind: "Devi liberarti di tutto questo, Neo. Paura, dubbio, sfiducia. Libera la tua mente. - Morpheus"
@ -431,60 +431,60 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
parents: "Per i genitori"
parents_title: "Caro Genitore: Tuo figlio/a sta imparando a programmare. Vuoi continuare ad aiutarlo/a ? "
parents_blurb1: "Tuo figlio/a ha giocato a _nLevels__ livelli ed ha imparato le basi della programmazione. Aiutalo/a a coltivare i suoi interessi ed acquistagli unabbonamento così potrà continuare a giocare."
# 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: "For $9.99 USD/mo, your child will get new challenges every week and personal email support from professional programmers."
# parents_blurb3: "No Risk: 100% money back guarantee, easy 1-click unsubscribe."
parents_blurb1a: "La programmazione e' una capacita' che indubbiamente sara' utile a tuo figlio da adulto. Nel 2020 una capacita' basilare di programmazione sara' richiesta dal 77% dei lavori e la richiesta a livello mondiale di ingegneri del software e' in continua crescita. Lo sai che la laurea in Informatica e' la piu' pagata sul mercato?"
parents_blurb2: "Per $9.99 dollari/mese, tuo figlio ricevera' settimanalmente nuove avventure e un supporto personale via email fornito d aprogrammatori professionisti."
parents_blurb3: "Nessun rischio: 100% restituzione dei soldi con un semplice processo di deregistrazione"
payment_methods: "Metodi di Pagamento"
payment_methods_title: "Metodi di Pagamento Accetati"
payment_methods_blurb1: "Attualmente accettiamo come metodi di pagamento la carta di credito e Alipay."
payment_methods_blurb2: "Se necessiti di un forma di pagamento diverso.Per favore contattaci"
sale_already_subscribed: "Sei già abbonato!" #"You're already subscribed!"
sale_blurb1: "Risparmia il 35%"
# sale_blurb2: "off regular subscription price of $120 for a whole year!"
sale_blurb2: "rispetto all'abbinamento ordinario di 120$ per l'intero anno!"
sale_button: "Saldi!" #"Sale!"
sale_button_title: "Risparmi il 35% quando compri l'abbonamento per 1 anno"
sale_click_here: "Clicca qui"
# sale_ends: "Ends"
# sale_extended: "*Existing subscriptions will be extended by 1 year."
# sale_feature_here: "Here's what you'll get:"
# 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_ends: "Fine"
sale_extended: "*Gli abbonamenti in corso verranno estesi per 1 anno."
sale_feature_here: "Qui cio' che otterrai:"
sale_feature2: "Accesso a 9 nuovi potenti <strong>eroi</strong> con abilita' uniche!"
sale_feature4: "<strong>42,000 gemme premio</strong> riconosciute immediatamente!"
sale_continue: "Pronto per continuare l'avventura?"
sale_limited_time: "Offerta limitata!"
sale_new_heroes: "Nuovi eroi!"
sale_title: "Saldi di inizio scuola"
sale_view_button: "Compra un abbonamento annuale per "
stripe_description: "Sottoscrizione mensile"
# stripe_description_year_sale: "1 Year Subscription (35% discount)"
# subscription_required_to_play: "You'll need a subscription to play this level."
# unlock_help_videos: "Subscribe to unlock all video tutorials."
stripe_description_year_sale: "Abbonamneto annuale (sconto 35%)"
subscription_required_to_play: "Devi essere abbonato per giocare su questo livello."
unlock_help_videos: "Abbonati per accedere a tutti i tutorial video."
personal_sub: "Sottoscrizione Personale" # Accounts Subscription View below
# loading_info: "Loading subscription information..."
# managed_by: "Managed by"
# will_be_cancelled: "Will be cancelled on"
# currently_free: "You currently have a free subscription"
# currently_free_until: "You currently have a subscription until"
# was_free_until: "You had a free subscription until"
# managed_subs: "Managed Subscriptions"
# managed_subs_desc: "Add subscriptions for other players (students, children, etc.)"
# managed_subs_desc_2: "Recipients must have a CodeCombat account associated with the email address you provide."
# group_discounts: "Group discounts"
# group_discounts_1: "We also offer group discounts for bulk subscriptions."
loading_info: "Caricamento informazioni abbonamento..."
managed_by: "Gestito da"
will_be_cancelled: "Sara' rimosso il"
currently_free: "Al momento hai un abbonamento gratuito"
currently_free_until: "Hai un abbonamento valido fino al"
was_free_until: "Hai avuto un abbonamento gratuito fino al "
managed_subs: "Gestione Abbonamenti"
managed_subs_desc: "Aggiungi un abbonamento per altri giocatori (studenti, bambini, etc.)"
managed_subs_desc_2: "I beneficiari devono avere un abbonamento CodeCombat associato con l'email fornito."
group_discounts: "Sconto comitiva"
group_discounts_1: "Offriamo sconto comitiva anche per abbonamenti in grandi volumi."
group_discounts_1st: "Prima sottoscrizione"
group_discounts_full: "Prezzo completo"
group_discounts_2nd: "Sottoscrizione 2-11" #"Subscriptions 2-11"
group_discounts_20: "20% disconto"
group_discounts_12th: "Sottoscrizione 12+" #"Subscriptions 12+"
group_discounts_40: "40% di sconto"
# subscribing: "Subscribing..."
# recipient_emails_placeholder: "Enter email address to subscribe, one per line."
subscribing: "Abbonamento..."
recipient_emails_placeholder: "Inserisci l'indirizzo email per abbonarti, uno per linea."
subscribe_users: "Iscrivere Utenti"
users_subscribed: "Utenti iscritti:"
# no_users_subscribed: "No users subscribed, please double check your email addresses."
# current_recipients: "Current Recipients"
# unsubscribing: "Unsubscribing"
# subscribe_prepaid: "Click Subscribe to use prepaid code"
# using_prepaid: "Using prepaid code for monthly subscription"
no_users_subscribed: "Utente non abbonato, per favore verifica il tuo indirizzo di email."
current_recipients: "Destinatari attuali"
unsubscribing: "Deregistrazione"
subscribe_prepaid: "Clicca su Registrazione per usare un codice pre pagato"
using_prepaid: "Usa un codice pre pagato per un abbonamento mensile"
choose_hero:
choose_hero: "Scegli il tuo eroe"
@ -1612,7 +1612,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
# inactive_developers: "Inactive Developers"
admin:
# av_espionage: "Espionage" # Really not important to translate /admin controls.
av_espionage: "Spionaggio" # Really not important to translate /admin controls.
av_espionage_placeholder: "Email o nome utente"
av_usersearch: "Cerca utenti"
av_usersearch_placeholder: "Email, username, nome, qualsiasi cosa"
@ -1621,8 +1621,8 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t
av_entities_sub_title: "Entità"
av_entities_users_url: "Utenti"
av_entities_active_instances_url: "Istanze attive"
# av_entities_employer_list_url: "Employer List"
# av_entities_candidates_list_url: "Candidate List"
av_entities_employer_list_url: "Lista datore di lavoro"
av_entities_candidates_list_url: "Lista Candidati"
av_entities_user_code_problems_list_url: "Lista problemi codice utenti"
av_other_sub_title: "Altro"
av_other_debug_base_url: "Base (per il debug di base.jade)"

View file

@ -724,16 +724,16 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
wrong_password: "間違ったパスワード"
upload_picture: "画像をアップロード"
delete_this_account: "アカウントを完全削除する"
# reset_progress_tab: "Reset All Progress"
# reset_your_progress: "Clear all your progress and start over"
reset_progress_tab: "すべての進捗をリセットする"
reset_your_progress: "すべての進捗をリセットしやり直す"
god_mode: "ゴッドモード"
password_tab: "パスワード"
emails_tab: "メール"
admin: "管理者"
new_password: "新パスワード"
new_password_verify: "新パスワードを再入力"
type_in_email: "アカウントの削除を確認するために、メールアドレスを入力して下さい"
# type_in_email_progress: "Type in your email to confirm deleting your progress."
type_in_email: "アカウントの削除を確認するために、メールアドレスを入力して下さい"
type_in_email_progress: "進捗を消すために、メールアドレスを入力してください。"
type_in_password: "そして、パスワードを入力してください。"
email_subscriptions: "ニュースレターの購読"
email_subscriptions_none: "No Email Subscriptions."
@ -1147,22 +1147,22 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
summary_wins: " 勝利数, "
summary_losses: " 敗北数"
rank_no_code: "新しいコードがランクにありません"
# rank_my_game: "Rank My Game!"
rank_my_game: "試合をランキングに送信!"
rank_submitting: "送信中..."
rank_submitted: "ランキングに送信されました。"
# rank_failed: "Failed to Rank"
# rank_being_ranked: "Game Being Ranked"
rank_failed: "ランキングに送信できませんでした。"
rank_being_ranked: "ランキングにのっています"
# rank_last_submitted: "submitted "
# help_simulate: "Help simulate games?"
# code_being_simulated: "Your new code is being simulated by other players for ranking. This will refresh as new matches come in."
# no_ranked_matches_pre: "No ranked matches for the "
# no_ranked_matches_post: " team! Play against some competitors and then come back here to get your game ranked."
# choose_opponent: "Choose an Opponent"
choose_opponent: "相手を選んでください"
select_your_language: "使う言語を選んでください!"
tutorial_play: "チュートリアルで遊ぶ"
tutorial_recommended: "はじめて遊ぶ人におすすめ"
tutorial_skip: "チュートリアルをスキップする"
# tutorial_not_sure: "Not sure what's going on?"
tutorial_not_sure: "なにが起きているのかわかりませんか?"
tutorial_play_first: "はじめからチュートリアルを遊ぶ"
simple_ai: "単純なAI"
warmup: "ウォームアップ"

View file

@ -147,7 +147,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
unwatch: "Ngừng Quan Sát"
submit_patch: "Gửi Bản Cập Nhật"
submit_changes: "Gửi Những Thay Đổi"
# save_changes: "Save Changes"
save_changes: "Lưu Những Thay Đổi"
general:
and: ""
@ -251,7 +251,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
victory_saving_progress: "Đang lưu tiến trình"
victory_go_home: "Quay về màn hình chính"
victory_review: "Hãy cho chúng tôi biết thêm"
# victory_review_placeholder: "How was the level?"
victory_review_placeholder: "Bàn vừa rồi như thế nào ?"
victory_hour_of_code_done: "Bạn xong chưa?"
victory_hour_of_code_done_yes: "Đúng vậy, tôi đã hoàn tất thời gian lập trình!"
victory_experience_gained: "XP nhận được"
@ -336,7 +336,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
tip_google: "Có vấn đề mà bạn không thể giải quyết ? Hãy sử dụng Google để tìm ra phương án!"
# tip_adding_evil: "Adding a pinch of evil."
# tip_hate_computers: "That's the thing about people who think they hate computers. What they really hate is lousy programmers. - Larry Niven"
# tip_open_source_contribute: "You can help CodeCombat improve!"
tip_open_source_contribute: "Bạn có thể giúp CodeCombat trở nên tốt hơn!"
# tip_recurse: "To iterate is human, to recurse divine. - L. Peter Deutsch"
# tip_free_your_mind: "You have to let it all go, Neo. Fear, doubt, and disbelief. Free your mind. - Morpheus"
# tip_strong_opponents: "Even the strongest of opponents always has a weakness. - Itachi Uchiha"
@ -434,7 +434,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# 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: "Chỉ với $9.99 USD một tháng, con của bạn sẽ nhận được những thử thách mới mỗi tháng và sẽ nhận được sự hỗ trợ từ các lập trình viên chuyên nghiệp qua email." # {change}
parents_blurb3: "Không hề có rủi ro: Nếu bạn không hài lòng bạn có thể nhận lại 100% số tiền mình bỏ ra chỉ với 1 cú nhấp chuốt."
# payment_methods: "Payment Methods"
payment_methods: "Những phương thức thanh toán"
# 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"
@ -497,7 +497,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
coffeescript_blurb: "Thực ra là JavaScript nhưng với cú pháp tốt hơn."
clojure_blurb: "Lisp thời đại mới."
lua_blurb: "Ngôn ngữ hay dùng trong làm game."
# io_blurb: "Simple but obscure."
io_blurb: "Đơn giản nhưng ít người biết đến."
status: "Tình trạng"
hero_type: "Loại"
weapons: "Vũ khí"
@ -635,7 +635,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# 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_title: "CodeCombat dành cho ai ?"
# 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?"
@ -719,11 +719,11 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
autosave: "Tự động lưu thay đổi"
me_tab: "Tôi"
picture_tab: "Bức tranh"
# delete_account_tab: "Delete Your Account"
# wrong_email: "Wrong Email"
# wrong_password: "Wrong Password"
delete_account_tab: "Xóa tài khoản của bạn"
wrong_email: "Email không đúng"
wrong_password: "Mật khẩu không đúng"
upload_picture: "Tải ảnh lên"
# delete_this_account: "Delete this account permanently"
delete_this_account: "Xóa tài khoản này vĩnh viễn"
# reset_progress_tab: "Reset All Progress"
# reset_your_progress: "Clear all your progress and start over"
# god_mode: "God Mode"
@ -745,7 +745,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# email_any_notes_description: "Disable to stop all activity notification emails."
# email_news: "News"
email_recruit_notes: "Cơ hội việc làm"
# email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job."
email_recruit_notes_description: "Nếu bạn chơi trò này rất giỏi, chúng tôi có thể sẽ liên lạc với bạn về cơ hội nghề nghiệp."
# contributor_emails: "Contributor Class Emails"
contribute_prefix: "Chúng tôi đang tìm thêm người vào nhóm của chúng tôi! Hãy kiểm "
contribute_page: "trang đóng góp"
@ -765,7 +765,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
keyboard_shortcuts: "Các phím tắt"
space: "Phím Space"
enter: "Phím Enter"
# press_enter: "press enter"
press_enter: "Ấn Enter"
escape: "Phím Escape"
shift: "Phím Shift"
# run_code: "Run current code."
@ -801,19 +801,19 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# social_hipchat: "Chat with us in the public CodeCombat Slack channel"
# contribute_to_the_project: "Contribute to the project"
# clans:
# clan: "Clan"
clans:
clan: "Clan"
# clans: "Clans"
# new_name: "New clan name"
# new_description: "New clan description"
# make_private: "Make clan private"
# subs_only: "subscribers only"
# create_clan: "Create New Clan"
create_clan: "Tạo một clan mới"
# private_preview: "Preview"
# public_clans: "Public Clans"
# my_clans: "My Clans"
# clan_name: "Clan Name"
# name: "Name"
name: "Tên"
# chieftain: "Chieftain"
# type: "Type"
# edit_clan_name: "Edit Clan Name"
@ -829,7 +829,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# join_clan: "Join Clan"
# invite_1: "Invite:"
# invite_2: "*Invite players to this Clan by sending them this link."
# members: "Members"
members: "Những thành viên"
# progress: "Progress"
# not_started_1: "not started"
# started_1: "started"
@ -859,9 +859,9 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# track_concepts8: "to join"
# private_require_sub: "Private clans require a subscription to create or join."
# courses:
# course: "Course"
# courses: "courses"
courses:
course: "Khóa học"
courses: "Những khóa học"
# not_enrolled: "You are not enrolled in this course."
# visit_pref: "Please visit the"
# visit_suf: "page to enroll."
@ -872,11 +872,11 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# edit_settings: "edit class settings"
# edit_settings1: "Edit Class Settings"
# progress: "Class Progress"
# add_students: "Add Students"
# stats: "Statistics"
# total_students: "Total students:"
add_students: "Thêm học sinh"
stats: "Thống kê"
total_students: "Tổng số học sinh:"
# average_time: "Average level play time:"
# total_time: "Total play time:"
total_time: "Tổng thời gian chơi:"
# average_levels: "Average levels completed:"
# total_levels: "Total levels completed:"
# furthest_level: "Furthest level completed:"
@ -896,20 +896,20 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# capacity_used: "Course slots used:"
# enter_emails: "Enter student emails to invite, one per line"
# send_invites: "Send Invites"
# title: "Title"
title: "Tiêu đề"
# description: "Description"
# creating_class: "Creating class..."
# purchasing_course: "Purchasing course..."
# buy_course: "Buy Course"
# buy_course1: "Buy this course"
buy_course: "Mua khóa học"
buy_course1: "Mua khóa học này"
# create_class: "Create Class"
# select_all_courses: "Select 'All Courses' for a 50% discount!"
# all_courses: "All Courses"
# number_students: "Number of students"
# enter_number_students: "Enter the number of students you need for this class."
all_courses: "Tất cả những khóa học"
number_students: "Số lượng học sinh"
enter_number_students: "Hãy nhập vào số lượng học sinh bạn cần cho lớp học này."
# name_class: "Name your class"
# displayed_course_page: "This will be displayed on the course page for you and your students. It can be changed later."
# buy: "Buy"
buy: "Mua"
# purchasing_for: "You are purchasing a license for"
# creating_for: "You are creating a class for"
# for: "for" # Like in 'for 30 students'
@ -925,7 +925,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# answer2: "The single player game has items, gems, hero selection, leveling up, and in-app purchases. Courses have classroom management features and streamlined student-focused level pacing."
# teachers_click: "Teachers Click Here"
# students_click: "Students Click Here"
# courses_on_coco: "Courses on CodeCombat"
courses_on_coco: "Những khóa học trên CodeCombat"
# designed_to: "Courses are designed to introduce computer science concepts using CodeCombat's fun and engaging environment. CodeCombat levels are organized around key topics to encourage progressive learning, over the course of 5 hours."
# more_in_less: "Learn more in less time"
# no_experience: "No coding experience necesssary"
@ -938,7 +938,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# enter_code1: "Enter unlock code"
# enroll: "Enroll"
# pick_from_classes: "Pick from your current classes"
# enter: "Enter"
enter: "Enter"
# or: "Or"
# topics: "Topics"
# hours_content: "Hours of content:"
@ -979,7 +979,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
indoor: "Trong nhà"
desert: "Sa mạc"
# grassy: "Grassy"
# mountain: "Mountain"
mountain: "Đồi núi"
# glacier: "Glacier"
small: ""
large: "Lớn"
@ -1110,7 +1110,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# diplomat_i18n_page: "translations page"
# diplomat_i18n_page_suffix: ", or our interface and website on GitHub."
# diplomat_join_pref_github: "Find your language locale file "
# diplomat_github_url: "on GitHub"
diplomat_github_url: "ở trên GitHub"
# diplomat_join_suf_github: ", edit it online, and submit a pull request. Also, check this box below to keep up-to-date on new internationalization developments!"
# diplomat_subscribe_desc: "Get emails about i18n developments and levels to translate."
# ambassador_introduction: "This is a community we're building, and you are the connections. We've got forums, emails, and social networks with lots of people to talk with and help get acquainted with the game and learn from. If you want to help people get involved and have fun, and get a good feel of the pulse of CodeCombat and where we're going, then this class might be for you."
@ -1206,7 +1206,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# no_achievements: "No Achievements earned yet."
favorite_prefix: "Ngôn ngữ lập trình ưu thích là "
favorite_postfix: "."
# not_member_of_clans: "Not a member of any clans yet."
not_member_of_clans: "Không phải là thành viên của bất kì clan nào."
# achievements:
# last_earned: "Last Earned"
@ -1476,7 +1476,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
complete: "Hoàn tất"
next: "Tiếp"
next_city: "Thành phố?"
# next_country: "pick your country."
next_country: "Chọn đất nước của bạn."
# next_name: "name?"
# next_short_description: "write a short description."
# next_long_description: "describe your desired position."
@ -1487,7 +1487,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# next_links: "add any personal or social links."
# next_photo: "add an optional professional photo."
# next_active: "mark yourself open to offers to show up in searches."
# example_blog: "Blog"
example_blog: "Blog"
example_personal_site: "Trang cá nhân"
links_header: "Đường truyền cá nhân"
# links_blurb: "Link any other sites or profiles you want to highlight, like your GitHub, your LinkedIn, or your blog."
@ -1510,12 +1510,12 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# basics_looking_for_part_time: "Part-time"
# basics_looking_for_remote: "Remote"
# basics_looking_for_contracting: "Contracting"
# basics_looking_for_internship: "Internship"
basics_looking_for_internship: "Thực tập"
# basics_looking_for_help: "What kind of developer position do you want?"
name_header: "Điền tên của bạn"
# name_anonymous: "Anonymous Developer"
# name_help: "Name you want employers to see, like 'Nick Winter'."
# short_description_header: "Write a short description of yourself"
short_description_header: "Hãy viết một bản giới thiệu ngắn gọn về bạn"
# short_description_blurb: "Add a tagline to help an employer quickly learn more about you."
# short_description: "Tagline"
# short_description_help: "Who are you, and what are you looking for? 140 characters max."
@ -1577,9 +1577,9 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# filter_visa_no: "Not Authorized"
# filter_education_top: "Top School"
filter_education_other: "Khác"
# filter_role_web_developer: "Web Developer"
# filter_role_software_developer: "Software Developer"
# filter_role_mobile_developer: "Mobile Developer"
filter_role_web_developer: "Nhà phát triển web"
filter_role_software_developer: "Nhà phát triển phần mềm"
filter_role_mobile_developer: "Nhà phát triển phần ứng dụng di động"
filter_experience: "Kinh Nghiệm"
# filter_experience_senior: "Senior"
# filter_experience_junior: "Junior"

View file

@ -6,7 +6,8 @@ module.exports = class Prepaid extends CocoModel
urlRoot: '/db/prepaid'
openSpots: ->
@get('maxRedeemers') - @get('redeemers')?.length
return @get('maxRedeemers') - @get('redeemers')?.length if @get('redeemers')?
@get('maxRedeemers')
userHasRedeemed: (userID) ->
for redeemer in @get('redeemers')

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

@ -26,6 +26,7 @@ MongoFindQuerySchema =
{type: 'object'}
{type: 'boolean'}
]
properties: {}
additionalProperties: false
definitions: {}

View file

@ -116,6 +116,7 @@ _.extend CampaignSchema.properties, {
}}
campaign: c.shortString title: 'Campaign', description: 'Which campaign this level is part of (like "desert").', format: 'hidden' # Automatically set by campaign editor.
campaignIndex: c.int title: 'Campaign Index', description: 'The 0-based index of this level in its campaign.', format: 'hidden' # Automatically set by campaign editor.
tasks: c.array {title: 'Tasks', description: 'Tasks to be completed for this level.'}, c.task
concepts: c.array {title: 'Programming Concepts', description: 'Which programming concepts this level covers.'}, c.concept

View file

@ -8,6 +8,9 @@ _.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']}
c.extendBasicProperties ClassroomSchema, 'Classroom'

View file

@ -344,6 +344,7 @@ _.extend LevelSchema.properties,
type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference'
}}
campaign: c.shortString title: 'Campaign', description: 'Which campaign this level is part of (like "desert").', format: 'hidden' # Automatically set by campaign editor.
campaignIndex: c.int title: 'Campaign Index', description: 'The 0-based index of this level in its campaign.', format: 'hidden' # Automatically set by campaign editor.
scoreTypes: c.array {title: 'Score Types', description: 'What metric to show leaderboards for.', uniqueItems: true},
c.shortString(title: 'Score Type', 'enum': ['time', 'damage-taken', 'damage-dealt', 'gold-collected', 'difficulty']) # TODO: good version of LoC; total gear value.
concepts: c.array {title: 'Programming Concepts', description: 'Which programming concepts this level covers.', uniqueItems: true}, c.concept

View file

@ -169,6 +169,11 @@ _.extend LevelSessionSchema.properties,
chat:
type: 'array'
ladderAchievementDifficulty:
type: 'integer'
minimum: 0
description: 'What ogre AI difficulty, 0-4, this human session has beaten in a multiplayer arena.'
meanStrength:
type: 'number'

View file

@ -7,6 +7,7 @@ TrialRequestSchema = c.object {
_.extend TrialRequestSchema.properties,
applicant: c.objectId(links: [{rel: 'extra', href: '/db/user/{($)}'}])
created: c.date()
prepaidCode: c.objectId()
reviewDate: c.date({readOnly: true})
reviewer: c.objectId(links: [{rel: 'extra', href: '/db/user/{($)}'}])

View file

@ -316,6 +316,7 @@ _.extend UserSchema.properties,
country: { type: 'string', enum: ['brazil', 'china'] } # New, supports multiple countries for different versions--only set for specific countries where we have premium servers right now
clans: c.array {}, c.objectId()
courseInstances: c.array {}, c.objectId()
currentCourse: c.object {}, { # Old, can be removed after we deploy and delete it from all users
courseID: c.objectId({})
courseInstanceID: c.objectId({})

View file

@ -136,6 +136,7 @@ module.exports =
'tome:winnability-updated': c.object {title: 'Winnability Updated', description: 'When we think we can now win (or can no longer win), we may want to emphasize the submit button versus the run button (or vice versa), so this fires when we get new goal states (even preloaded goal states) suggesting success or failure change.', required: ['winnable']},
winnable: {type: 'boolean'}
level: {type: 'object'}
# Problem Alert
'tome:show-problem-alert': c.object {title: 'Show Problem Alert', description: 'A problem alert needs to be shown.', required: ['problem']},

View file

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

View file

@ -4,6 +4,11 @@
img.media-object
width: 300px
.edit-classroom-small
cursor: pointer
&:hover
color: grey
#fixed-area
position: fixed
bottom: 0

View file

@ -3,6 +3,9 @@
.name-col-cell
max-width: 170px
&.ai
color: #3f44bf
.histogram-display
height: 130px

View file

@ -6,6 +6,22 @@
padding-bottom: 30px
border-bottom: 1px solid #888
#course-header
background-color: black
font-size: 24px
padding: 6px 4px 8px
font-weight: bold
#course-details-link
position: absolute
background-color: white
padding: 2px 5px
a
color: black
#course-name
color: white
#level-column
padding-top: 14px
text-align: center
@ -14,6 +30,12 @@
margin-top: -14px
width: 100%
#course-h1
color: black
font-size: 72px
text-transform: capitalize
margin-top: 0
h1
text-align: center

View file

@ -190,7 +190,7 @@
// border-bottom: 1px dotted rgba(0, 51, 255, 0.25)
.ace_text-layer .ace_comment
color: rgb(78, 38, 226)
color: #1900AD
.ace_text-layer .ace_variable
// https://github.com/codecombat/codecombat/issues/6

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

@ -1,8 +1,5 @@
#teachers-free-trial-view
.input-email-address
width: 40%
.input-school
width: 40%
@ -18,6 +15,9 @@
.thanks-submit
display: none
.email-address
margin-right: 12px
.error-message
display: none
color: red

View file

@ -80,28 +80,26 @@ block content
span.spr(data-i18n="[title]account_prepaid.copy_link;general.code", title="You can copy the code's link and send it to someone.")
span.glyphicon.glyphicon-question-sign(aria-hidden="true")
th(data-i18n="account_prepaid.months")
th(data-i18n="account_prepaid.quantity")
th Remaining Users
th Total Users
th(data-i18n="user.status")
for code in codes.models
if code.get('type') === 'terminal_subscription'
- var owner = (code.get('creator') == me.id ? true : false)
- var properties = code.get('properties')
- var redeemers = code.get('redeemers')
if redeemers
- var redeemed = redeemers.length
else
- var redeemed = '0'
- var redeemed = redeemers ? redeemers.length : 0
tr
td
a(href="/account/prepaid?_ppc=#{code.get('code')}")= code.get('code')
td= properties.months || '-'
if owner
td= code.get('maxRedeemers') - redeemed
else
td -
if owner
td= code.get('maxRedeemers')
td(data-i18n="account.purchased")
else
td -
td -
td(data-i18n="account_prepaid.redeemed")
else
p(data-i18n="account_prepaid.no_codes")

View file

@ -31,7 +31,7 @@ block content
if numReviewed > maxReviewedShown
- break
tr
td.created= new Date(parseInt(trialRequest.get('_id').substring(0, 8), 16) * 1000).toISOString().substring(0, 10)
td.created= trialRequest.get('created').substring(0, 10)
td.reviewed
if trialRequest.get('reviewDate')
span= trialRequest.get('reviewDate').substring(0, 10)

View file

@ -0,0 +1,24 @@
extends /templates/core/modal-base
block modal-header-content
button.close(data-dismiss='modal')
span &times;
h3.modal-title(data-i18n="courses.edit_settings1")
block modal-body-content
.form
.form-group
label(data-i18n="courses.title")
input.form-control.settings-name-input(type='text', value="#{view.classroom.get('name') || ''}")
.form-group
label(data-i18n="courses.description")
textarea.form-control.settings-description-input(rows=2)= view.classroom.get('description')
.form-group
label(data-i18n="choose_hero.programming_language")
select.form-control#programming-language-select
- var aceConfig = view.classroom.get('aceConfig') || {};
option(value="python", selected=aceConfig.language==='python') Python
option(value="javascript", selected=aceConfig.language==='javascript') JavaScript
block modal-footer-content
button#save-settings-btn.btn(data-i18n="common.save_changes")

View file

@ -53,11 +53,6 @@ block content
if courseInstance.get('description')
each line in courseInstance.get('description').split('\n')
div= line
// TODO: migrate these settings to classrooms
//if adminMode && courseInstance
// +settings-dialog
// p
// button.btn.btn-xs(data-toggle='modal', data-target='#settingsModal', data-i18n="courses.edit_settings")
div.well.well-sm(role='tabpanel')
ul.nav.nav-pills(role='tablist')
@ -213,10 +208,10 @@ mixin progress-members-levels-expanded(memberID)
if userLevelStateMap[memberID][levelID] === 'complete'
span.progress-level-cell.progress-level-cell-complete(data-level-id=levelID, data-level-slug=level.slug, data-user-id=memberID) #{i + 1}
span.spl= level.name.replace('Course: ', '')
+progress-members-popup-completed(i, level)
+progress-members-popup-completed(i, level, (view.userLevelSessionMap[memberID] || {})[levelID])
else if userLevelStateMap[memberID][levelID] === 'started'
span.progress-level-cell.progress-level-cell-started(data-level-id=levelID, data-level-slug=level.slug, data-user-id=memberID) #{i + 1} #{level.name.replace('Course: ', '')}
+progress-members-popup-started(i, level)
+progress-members-popup-started(i, level, (view.userLevelSessionMap[memberID] || {})[levelID])
else
span.progress-level-cell #{i + 1} #{level.name.replace('Course: ', '')}
- i++
@ -231,35 +226,35 @@ mixin progress-members-levels-condensed(memberID)
each level, levelID in campaign.get('levels')
if userLevelStateMap[memberID][levelID] === 'complete'
span.progress-level-cell.progress-level-cell-complete(style="width:#{levelCellWidth}%;", data-level-id=levelID, data-level-slug=level.slug, data-user-id=memberID) #{i + 1}
+progress-members-popup-completed(i, level)
+progress-members-popup-completed(i, level, (view.userLevelSessionMap[memberID] || {})[levelID])
else if userLevelStateMap[memberID][levelID] === 'started'
span.progress-level-cell.progress-level-cell-started(style="width:#{levelCellWidth}%;", data-level-id=levelID, data-level-slug=level.slug, data-user-id=memberID) #{i + 1}
+progress-members-popup-started(i, level)
+progress-members-popup-started(i, level, (view.userLevelSessionMap[memberID] || {})[levelID])
else
break
- i++
mixin progress-members-popup-completed(i, level)
mixin progress-members-popup-completed(i, level, session)
.progress-popup-container
h3 #{i + 1}. #{level.name.replace('Course: ', '')}
p
span.spr(data-i18n="courses.play_time")
span #{moment.duration(level.playtime, "seconds").humanize()}
span #{moment.duration(session.get('playtime'), "seconds").humanize()}
p
span.spr(data-i18n="courses.completed")
span #{moment(level.changed).format('MMMM Do YYYY, h:mm:ss a')}
span #{moment(session.get('changed')).format('MMMM Do YYYY, h:mm:ss a')}
if adminMode
strong(data-i18n="clans.view_solution")
mixin progress-members-popup-started(i, level)
mixin progress-members-popup-started(i, level, session)
.progress-popup-container
h3 #{i + 1}. #{level.name.replace('Course: ', '')}
p
span.spr(data-i18n="courses.play_time")
span #{moment.duration(level.playtime, "seconds").humanize()}
span #{moment.duration(session.get('playtime'), "seconds").humanize()}
p
span.spr(data-i18n="clans.last_played")
span #{moment(level.changed).format('MMMM Do YYYY, h:mm:ss a')}
span #{moment(session.get('changed')).format('MMMM Do YYYY, h:mm:ss a')}
if adminMode
strong(data-i18n="clans.view_solution")
@ -274,45 +269,23 @@ mixin levels-tab
tbody
if campaign
- var lastLevelCompleted = true;
- var levelCount = 0;
each level, levelID in campaign.get('levels')
tr
td
if lastLevelCompleted || adminMode
button.btn.btn-success.btn-play-level(data-level-slug=level.slug, data-i18n="home.play")
- var i18n = level.type === 'course-ladder' ? 'play.compete' : 'home.play';
button.btn.btn-success.btn-play-level(data-level-slug=level.slug, data-i18n=i18n, data-level-id=levelID)
td
if userLevelStateMap[me.id]
div= userLevelStateMap[me.id][levelID]
- lastLevelCompleted = userLevelStateMap[me.id][levelID] === 'complete'
else
- lastLevelCompleted = false
td= level.name.replace('Course: ', '')
td= ++levelCount + '. ' + level.name.replace('Course: ', '')
td
if levelConceptMap[levelID]
each concept in course.get('concepts')
if levelConceptMap[levelID][concept]
span.spr.progress-level-cell.progress-level-cell-not-started(data-i18n="concepts." + concept)
mixin settings-dialog
.modal#settingsModal
.modal-dialog
.modal-header
button.close(data-dismiss='modal')
span &times;
h3.modal-title(data-i18n="courses.edit_settings1")
.modal-body
.form
.form-group
label(data-i18n="courses.title")
input.form-control.settings-name-input(type='text', value="#{courseInstance.get('name') || ''}")
.form-group
label(data-i18n="courses.description")
textarea.form-control.settings-description-input(rows=2)= courseInstance.get('description')
.form-group
label(data-i18n="choose_hero.programming_language")
select.form-control#programming-language-select
- var aceConfig = view.courseInstance.get('aceConfig') || {};
option(value="python", selected=aceConfig.language==='python') Python
option(value="javascript", selected=aceConfig.language==='javascript') JavaScript
.modal-footer
button.btn.btn-save-settings(data-i18n="common.save_changes")

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

@ -102,7 +102,15 @@ block content
a.btn.btn-default.btn-xs(href="/courses/purchase") Add
for classroom in view.classrooms.models
h2= classroom.get('name')
h2
span.spr= classroom.get('name')
- var language = (classroom.get('aceConfig') || {}).language || 'python';
if language === 'python'
img(src="/images/common/code_languages/python_icon.png")
if language === 'javascript'
img(src="/images/common/code_languages/javascript_icon.png")
small.spl.edit-classroom-small(data-classroom-id=classroom.id)
span.glyphicon.glyphicon-pencil
- var courseInstances = view.courseInstances.where({classroomID: classroom.id})
@ -111,6 +119,9 @@ block content
.progress-bar(style="width: 100%")
else
- var description = classroom.get('description');
if description
p= description
table.table
tr
th Student

View file

@ -31,7 +31,7 @@
td.hero-portrait-cell(style="background-image: url(/file/db/thang.type/#{(session.get('heroConfig') || {}).thangType || '529ffbf1cf1818f2be000001'}/portrait.png)")
td.rank-cell= rank + 1
td.score-cell= Math.round(sessionStats.totalScore * 100)
td.name-col-cell= session.get('creatorName') || "Anonymous"
td(class='name-col-cell' + ((new RegExp('(Simple|Shaman|Brawler|Chieftain|Thoktar) AI')).test(session.get('creatorName')) ? ' ai' : ''))= session.get('creatorName') || "Anonymous"
td.age-cell= moment(session.get('submitDate')).fromNow().replace('a few ', '')
td.fight-cell
a(href="/play/level/#{level.get('slug') || level.id}?team=#{team.otherTeam}&opponent=#{session.id}" + (league ? "&league=" + league.id : ""))
@ -51,7 +51,7 @@
td.hero-portrait-cell(style="background-image: url(/file/db/thang.type/#{(session.get('heroConfig') || {}).thangType || '529ffbf1cf1818f2be000001'}/portrait.png)")
td.rank-cell= session.rank
td.score-cell= Math.round(sessionStats.totalScore * 100)
td.name-col-cell= session.get('creatorName') || "Anonymous"
td(class='name-col-cell' + ((new RegExp('(Simple|Shaman|Brawler|Chieftain|Thoktar) AI')).test(session.get('creatorName')) ? ' ai' : ''))= session.get('creatorName') || "Anonymous"
td.age-cell= moment(session.get('submitDate')).fromNow().replace('a few ', '')
td.fight-cell
a(href="/play/level/#{level.get('slug') || level.id}?team=#{team.otherTeam}&opponent=#{session.id}" + (league ? "&league=" + league.id : ""))

View file

@ -4,15 +4,29 @@ block content
div#ladder-top
if leagueType == 'course'
#course-header
#course-details-link
a(href="/courses/"+view.course.id+"/"+view.league.id)
span.glyphicon.glyphicon-arrow-left
span.spl Levels
.text-center
span#course-name
span= view.course.get('name')
span.spl - Arena
div#level-column
if leagueType === 'course'
h1#course-h1= (level.get('name') || '').toUpperCase()
if levelDescription
div!= levelDescription
else
h1= level.get('name')
if league
if leagueType === 'clan'
h1.league-header
a(href="/#{leagueType == 'clan' ? 'clans' : leagueType}/#{league.id}")= league.get('name')
a(href="/clans/#{league.id}")= league.get('name')
span.spl(data-i18n="ladder.league") League
if level.get('name') == 'Greed'
@ -132,6 +146,7 @@ block content
span= team.displayName
div.column.col-md-2
if leagueType !== 'course'
.spectate-button-container
a(href="/play/spectate/#{level.get('slug')}" + (league ? "?league=" + league.id : "")).spectate-button.btn.btn-illustrated.btn-info.center
span(data-i18n="play.spectate") Spectate
@ -142,6 +157,7 @@ block content
if !me.get('anonymous')
li
a(href="#my-matches", data-toggle="tab", data-i18n="ladder.my_matches") My Matches
if leagueType !== 'course'
li
a(href="#simulate", data-toggle="tab", data-i18n="ladder.simulate") Simulate
if level.get('name') == 'Greed'

View file

@ -22,7 +22,7 @@ else
.level-name-area
.level-label(data-i18n="play_level.level")
.level-name(title=difficultyTitle || "")
span= worldName.replace('Course: ', '')
span= (campaignIndex ? (campaignIndex + 1) + '. ' : '') + worldName.replace('Course: ', '')
if levelDifficulty
sup.level-difficulty= levelDifficulty

View file

@ -26,6 +26,7 @@ block modal-body-content
textarea(data-i18n="[placeholder]play_level.victory_review_placeholder")
.clearfix
if level.get('type', true) === 'hero' || level.get('type') == 'hero-ladder'
for achievement in achievements
- var animate = achievement.completed && !achievement.completedAWhileAgo
.achievement-panel(class=achievement.completedAWhileAgo ? 'earned' : '' data-achievement-id=achievement.id data-animate=animate)

View file

@ -20,6 +20,10 @@ block content
strong.spr(data-i18n="teachers_survey.approved_2")
span.spr(data-i18n="teachers_survey.approved_3")
strong= existingRequest.get('properties').email
p
span.spr(data-i18n="teachers_survey.approved_4")
a(href='/courses', data-i18n="teachers_survey.approved_5")
span.spl(data-i18n="teachers_survey.approved_6")
else
p
span.spr(data-i18n="teachers_survey.denied_1")
@ -29,15 +33,20 @@ block content
a(href='mailto:team@codecombat.com') team@codecombat.com
span.spl(data-i18n="teachers_survey.contact_2")
else
p(data-i18n="teachers_survey.description_1")
p
span.spr(data-i18n="teachers_survey.description_1")
strong.spr Hour of Code Special!
span Complete the survey by December 31st and enroll all your students in the paid courses for 2 months.
p
span.spr(data-i18n="teachers_survey.description_1b")
a(href='/teachers', data-i18n="teachers_survey.description_2")
span.spl(data-i18n="teachers_survey.description_3")
p(data-i18n="teachers_survey.description_4")
p.container-email-address
label.control-label(data-i18n="teachers_survey.email")
br
input.control-label.input-email-address(type='text', value=view.email)
span.email-address= view.email
a(href='/account/settings') Change
p.container-school
label.control-label(data-i18n="teachers_survey.school")
br

View file

@ -30,7 +30,6 @@ module.exports = class TeachersFreeTrialView extends RootView
$('.radio-other').prop("checked", true)
onClickSubmit: (e) ->
email = $('.input-email-address').val()
school = $('.input-school').val()
location = $('.input-location').val()
age = $('input[name=age]:checked').val()
@ -46,10 +45,6 @@ module.exports = class TeachersFreeTrialView extends RootView
$('.container-num-students').removeClass('has-error')
$('.container-heard-about').removeClass('has-error')
$('.error-message').hide()
unless email
$('.container-email-address').addClass('has-error')
$('.error-message').show()
return
unless school
$('.container-school').addClass('has-error')
$('.error-message').show()
@ -75,7 +70,7 @@ module.exports = class TeachersFreeTrialView extends RootView
trialRequest = new TrialRequest
type: 'subscription'
properties:
email: email
email: @email
school: school
location: location
age: age

View file

@ -7,6 +7,6 @@ module.exports = class TeachersView extends RootView
constructor: ->
super()
_.defer ->
# Redirect to HoC version of /courses/teachers until we update the /teachers landing page
application.router.navigate "/courses/teachers?hoc=true", trigger: true

View file

@ -37,10 +37,8 @@ module.exports = class AccountSettingsView extends CocoView
#- Form input callbacks
onChangePanelInput: (e) ->
return if $(e.target).closest('.form').attr('id') in ['reset-progress-form', 'delete-account-form']
$(e.target).addClass 'changed'
if (JSON.stringify(document.getElementById('email1').className)).indexOf("changed") > -1 or (JSON.stringify(document.getElementById('password1').className)).indexOf("changed") > -1
$(e.target).removeClass 'changed'
else
@trigger 'input-changed'
onClickToggleAllButton: ->

View file

@ -311,7 +311,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
getAnalyticsInvoices = (done) =>
@updateFetchDataState "Fetching invoices #{Object.keys(invoices).length}..."
options =
url: '/db/analytics_stripe_invoice/-/all'
url: '/db/analytics.stripe.invoice/-/all'
method: 'GET'
options.error = (model, response, options) =>
return if @destroyed

View file

@ -102,7 +102,7 @@ module.exports = class PatchesView extends RootView
@render()
modelNamesRequest = @supermodel.addRequestResource 'patches', {
url: "/db/#{collection}/names"
url: "/db/#{collection.replace('_', '.')}/names"
data: {ids: ids}
method: 'POST'
success: success

View file

@ -18,7 +18,7 @@ module.exports = class TrialRequestsView extends RootView
statusA = a.get('status')
statusB = b.get('status')
if statusA is 'submitted' and statusB is 'submitted'
if a.get('_id') < b.get('_id')
if a.get('created') < b.get('created')
-1
else
1
@ -30,7 +30,7 @@ module.exports = class TrialRequestsView extends RootView
-1
else
1
@trialRequests = new CocoCollection([], { url: '/db/trial.request?conditions[sort]=-_id&conditions[limit]=500', model: TrialRequest, comparator: sortRequests })
@trialRequests = new CocoCollection([], { url: '/db/trial.request?conditions[sort]=-created&conditions[limit]=500', model: TrialRequest, comparator: sortRequests })
@supermodel.loadCollection(@trialRequests, 'trial-requests', {cache: false})
getRenderData: ->

View file

@ -196,7 +196,7 @@ module.exports = class ClanDetailsView extends RootView
if level.concepts?
for concept in level.concepts
@conceptsProgression.push concept unless concept in @conceptsProgression
if level.type is 'hero-ladder'
if level.type is 'hero-ladder' and level.slug not in ['capture-their-flag']
@arenas.push level
@campaignLevelProgressions.push campaignLevelProgression
@render?()

View file

@ -38,7 +38,7 @@ module.exports = class RootView extends CocoView
showNewAchievement: (achievement, earnedAchievement) ->
earnedAchievement.set('notified', true)
earnedAchievement.patch()
return if achievement.get('collection') is 'level.sessions'
return if achievement.get('collection') is 'level.sessions' and not achievement.get('query')?.team
#return if @isIE() # Some bugs in IE right now, TODO fix soon! # Maybe working now with not caching achievement fetches in CocoModel?
new AchievementPopup achievement: achievement, earnedAchievement: earnedAchievement

View file

@ -0,0 +1,26 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/courses/classroom-settings-modal'
module.exports = class AddLevelSystemModal extends ModalView
id: 'classroom-settings-modal'
template: template
events:
'click #save-settings-btn': 'onClickSaveSettingsButton'
initialize: (options) ->
@classroom = options.classroom
onClickSaveSettingsButton: ->
return unless @classroom
if name = $('.settings-name-input').val()
@classroom.set('name', name)
description = $('.settings-description-input').val()
@classroom.set('description', description)
@classroom.set('aceConfig', {
language: @$('#programming-language-select').val()
})
@classroom.patch()
@hide()

View file

@ -19,7 +19,6 @@ module.exports = class CourseDetailsView extends RootView
events:
'change .progress-expand-checkbox': 'onCheckExpandedProgress'
'click .btn-play-level': 'onClickPlayLevel'
'click .btn-save-settings': 'onClickSaveSettings'
'click .btn-select-instance': 'onClickSelectInstance'
'click .progress-member-header': 'onClickMemberHeader'
'click .progress-header': 'onClickProgressHeader'
@ -219,6 +218,13 @@ module.exports = class CourseDetailsView extends RootView
onClickPlayLevel: (e) ->
levelSlug = $(e.target).data('level-slug')
levelID = $(e.target).data('level-id')
level = @campaign.get('levels')[levelID]
if level.type is 'course-ladder'
route = '/play/ladder/' + levelSlug
route += '/course/' + @courseInstance.id if @courseInstance.get('members').length > 1 # No league for solo courses
Backbone.Mediator.publish 'router:navigate', route: route
else
Backbone.Mediator.publish 'router:navigate', {
route: @getLevelURL levelSlug
viewClass: 'views/play/level/PlayLevelView'
@ -228,19 +234,6 @@ module.exports = class CourseDetailsView extends RootView
getLevelURL: (levelSlug) ->
"/play/level/#{levelSlug}?course=#{@courseID}&course-instance=#{@courseInstanceID}"
onClickSaveSettings: (e) ->
return unless @courseInstance
if name = $('.settings-name-input').val()
@courseInstance.set('name', name)
description = $('.settings-description-input').val()
console.log 'onClickSaveSettings', description
@courseInstance.set('description', description)
@courseInstance.set('aceConfig', {
language: @$('#programming-language-select').val()
})
@courseInstance.patch()
$('#settingsModal').modal('hide')
onClickSelectInstance: (e) ->
courseInstanceID = $('.select-instance').val()
@noCourseInstanceSelected = false

View file

@ -11,6 +11,7 @@ RootView = require 'views/core/RootView'
template = require 'templates/courses/teacher-courses-view'
utils = require 'core/utils'
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
module.exports = class TeacherCoursesView extends RootView
id: 'teacher-courses-view'
@ -22,6 +23,7 @@ module.exports = class TeacherCoursesView extends RootView
'click .course-instance-membership-checkbox': 'onClickCourseInstanceMembershipCheckbox'
'click #save-changes-btn': 'onClickSaveChangesButton'
'click #manage-tab-link': 'onClickManageTabLink'
'click .edit-classroom-small': 'onClickEditClassroomSmall'
constructor: (options) ->
super(options)
@ -71,6 +73,13 @@ module.exports = class TeacherCoursesView extends RootView
@renderSelectors('#manage-tab-pane')
@$('#manage-tab-pane').toggleClass('active', isActive)
onClickEditClassroomSmall: (e) ->
classroomID = $(e.target).closest('small').data('classroom-id')
classroom = @classrooms.get(classroomID)
modal = new ClassroomSettingsModal({classroom: classroom})
@openModalView(modal)
@listenToOnce modal, 'hide', @renderManageTab
onClickAddStudentsButton: (e) ->
classroomID = $(e.target).data('classroom-id')
classroom = @classrooms.get(classroomID)
@ -152,7 +161,9 @@ module.exports = class TeacherCoursesView extends RootView
return
user = @usersToRedeem.first()
prepaid = @prepaids.find (prepaid) -> prepaid.openSpots()
prepaid = @prepaids.find((prepaid) -> prepaid.get('properties').endDate? and prepaid.openSpots())
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots()) unless prepaid
$.ajax({
method: 'POST'
url: _.result(prepaid, 'url') + '/redeemers'

View file

@ -92,7 +92,7 @@ module.exports = class CampaignEditorView extends RootView
onLoaded: ->
@toSave.add @campaign if @campaign.hasLocalChanges()
campaignLevels = $.extend({}, @campaign.get('levels'))
for level in @levels.models
for level, levelIndex in @levels.models
levelOriginal = level.get('original')
campaignLevel = campaignLevels[levelOriginal]
continue if not campaignLevel
@ -129,6 +129,8 @@ module.exports = class CampaignEditorView extends RootView
delete campaignLevel.unlocks
# Save campaign to level, unless it's a course campaign, since we reuse hero levels for course levels.
campaignLevel.campaign = @campaign.get 'slug' if @campaign.get('type', true) isnt 'course'
# Save campaign index to level if it's a course campaign, since we show linear level order numbers for course levels.
campaignLevel.campaignIndex = (@levels.models.length - levelIndex - 1) if @campaign.get('type', true) is 'course'
campaignLevels[levelOriginal] = campaignLevel
@campaign.set('levels', campaignLevels)

View file

@ -81,6 +81,7 @@ module.exports = class LadderPlayModal extends ModalView
# PART 4: Render
finishRendering: ->
return if @destroyed
@checkTutorialLevelExists (exists) =>
@tutorialLevelExists = exists
@render()

View file

@ -150,7 +150,7 @@ module.exports = class LadderTabView extends CocoView
refreshLadder: ->
@supermodel.resetProgress()
@ladderLimit ?= parseInt @getQueryVariable('top_players', 20)
@ladderLimit ?= parseInt @getQueryVariable('top_players', if @options.league then 100 else 20)
for team in @teams
if oldLeaderboard = @leaderboards[team.id]
@supermodel.removeModelResource oldLeaderboard

View file

@ -13,7 +13,8 @@ LadderPlayModal = require './LadderPlayModal'
CocoClass = require 'core/CocoClass'
Clan = require 'models/Clan'
#CourseInstance = require 'models/CourseInstance'
CourseInstance = require 'models/CourseInstance'
Course = require 'models/Course'
HIGHEST_SCORE = 1000000
@ -44,13 +45,20 @@ module.exports = class LadderView extends RootView
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(@levelID), 'your_sessions', {cache: false}).model
@teams = []
@loadLeague()
@course = new Course()
loadLeague: ->
@leagueID = @leagueType = null unless @leagueType in ['clan'] #, 'course']
@leagueID = @leagueType = null unless @leagueType in ['clan', 'course']
return unless @leagueID
modelClass = if @leagueType is 'clan' then Clan else null# else CourseInstance
resourceString = if @leagueType is 'clan' then 'clans.clan' else null# else 'courses.course'
modelClass = if @leagueType is 'clan' then Clan else CourseInstance
resourceString = if @leagueType is 'clan' then 'clans.clan' else 'courses.course'
@league = @supermodel.loadModel(new modelClass(_id: @leagueID), resourceString).model
if @leagueType is 'course'
@listenToOnce @league, 'sync', @onCourseInstanceLoaded
onCourseInstanceLoaded: (courseInstance) ->
course = new Course({_id: courseInstance.get('courseID')})
@course = @supermodel.loadModel(course, 'courses.course').model
onLoaded: ->
@teams = teamDataFromLevel @level

View file

@ -11,10 +11,14 @@ module.exports.teamDataFromLevel = (level) ->
color = teamConfigs[team].color
bgColor = hslToHex([color.hue, color.saturation, color.lightness + (1 - color.lightness) * 0.5])
primaryColor = hslToHex([color.hue, 0.5, 0.5])
if level.get('slug') in ['wakka-maul']
displayName = _.string.titleize(team)
else
displayName = $.i18n.t("ladder.#{team}") # Use Red/Blue instead of Humans/Ogres
teams.push({
id: team
name: _.string.titleize(team)
displayName: $.i18n.t("ladder.#{team}") # Use Red/Blue instead of Humans/Ogres
displayName: displayName
otherTeam: otherTeam
otherTeamDisplayName: $.i18n.t("ladder.#{otherTeam}")
bgColor: bgColor

View file

@ -33,9 +33,9 @@ class LevelSessionsCollection extends CocoCollection
@url = "/db/user/#{me.id}/level.sessions?project=state.complete,levelID,state.difficulty,playtime"
class CampaignsCollection extends CocoCollection
url: '/db/campaign'
# We don't send all of levels, just the parts needed in countLevels
url: '/db/campaign/-/overworld?project=slug,adjacentCampaigns,name,fullName,description,i18n,color,levels'
model: Campaign
project: ['name', 'fullName', 'description', 'i18n']
module.exports = class CampaignView extends RootView
id: 'campaign-view'
@ -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)
@ -427,6 +423,7 @@ module.exports = class CampaignView extends RootView
onSessionsLoaded: (e) ->
return if @editorMode
for session in @sessions.models
unless @levelStatusMap[session.get('levelID')] is 'complete' # Don't overwrite a complete session with an incomplete one
@levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started'
@levelDifficultyMap[session.get('levelID')] = session.get('state').difficulty if session.get('state')?.difficulty
@render()

View file

@ -268,6 +268,7 @@ module.exports = class SpectateLevelView extends RootView
onNextGamePressed: (e) ->
@fetchRandomSessionPair (err, data) =>
return if @destroyed
if err? then return console.log "There was an error fetching the random session pair: #{data}"
@sessionOne = data[0]._id
@sessionTwo = data[1]._id

View file

@ -61,6 +61,7 @@ module.exports = class ControlBarView extends CocoView
getRenderData: (c={}) ->
super c
c.worldName = @worldName
c.campaignIndex = @level.get('campaignIndex') if @level.get('type') is 'course'
c.multiplayerEnabled = @session.get('multiplayer')
c.ladderGame = @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder']
if c.isMultiplayerLevel = @isMultiplayerLevel

View file

@ -12,12 +12,12 @@ module.exports = class LevelGoalsView extends CocoView
template: template
className: 'secret expanded'
playbackEnded: false
mouseEntered: false
subscriptions:
'goal-manager:new-goal-states': 'onNewGoalStates'
'tome:cast-spells': 'onTomeCast'
'level:set-letterbox': 'onSetLetterbox'
'level:set-playing': 'onSetPlaying'
'surface:playback-restarted': 'onSurfacePlaybackRestarted'
'surface:playback-ended': 'onSurfacePlaybackEnded'
@ -84,6 +84,13 @@ module.exports = class LevelGoalsView extends CocoView
@$el.find('.goal-status').addClass('secret')
@$el.find('.goal-status.running').removeClass('secret')
onSetPlaying: (e) ->
return unless e.playing
# Automatically hide it while we replay
@mouseEntered = false
@expanded = true
@updatePlacement()
onSurfacePlaybackRestarted: ->
@playbackEnded = false
@$el.removeClass 'brighter'
@ -105,11 +112,17 @@ module.exports = class LevelGoalsView extends CocoView
@normalHeight = @$el.outerHeight()
updatePlacement: ->
expand = @playbackEnded or @mouseEntered
# Expand it if it's at the end. Mousing over reverses this.
expand = @playbackEnded isnt @mouseEntered
return if expand is @expanded
@updateHeight()
sound = if expand then 'goals-expand' else 'goals-collapse'
top = if expand then -5 else 41 - (@normalHeight ? @$el.outerHeight())
if expand
top = -5
else
height = @normalHeight
height = @$el.outerHeight() if not height or @playbackEnded
top = 41 - height
@$el.css 'top', top
if @soundTimeout
# Don't play the sound we were going to play after all; the transition has reversed.

View file

@ -48,7 +48,7 @@ module.exports = class HeroVictoryModal extends ModalView
@session = options.session
@level = options.level
@thangTypes = {}
if @level.get('type', true) is 'hero'
if @level.get('type', true) in ['hero', 'hero-ladder', 'course', 'course-ladder']
achievements = new CocoCollection([], {
url: "/db/achievement?related=#{@session.get('level').original}"
model: Achievement
@ -212,7 +212,7 @@ module.exports = class HeroVictoryModal extends ModalView
afterRender: ->
super()
@$el.toggleClass 'with-achievements', @level.get('type', true) is 'hero'
@$el.toggleClass 'with-achievements', @level.get('type', true) in ['hero', 'hero-ladder']
return unless @supermodel.finished()
@playSelectionSound hero, true for original, hero of @thangTypes # Preload them
@updateSavingProgressStatus()
@ -222,7 +222,7 @@ module.exports = class HeroVictoryModal extends ModalView
@insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view')
initializeAnimations: ->
if @level.get('type', true) is 'hero'
return @endSequentialAnimations() unless @level.get('type', true) in ['hero', 'hero-ladder']
@updateXPBars 0
#playVictorySound = => @playSound 'victory-title-appear' # TODO: actually add this
@$el.find('#victory-header').delay(250).queue(->
@ -253,7 +253,7 @@ module.exports = class HeroVictoryModal extends ModalView
beginSequentialAnimations: ->
return if @destroyed
return unless @level.get('type', true) is 'hero'
return unless @level.get('type', true) in ['hero', 'hero-ladder']
@sequentialAnimatedPanels = _.map(@animatedPanels.find('.reward-panel'), (panel) -> {
number: $(panel).data('number')
previousNumber: $(panel).data('previous-number')
@ -381,12 +381,13 @@ module.exports = class HeroVictoryModal extends ModalView
returnToLadder: ->
# Preserve the supermodel as we navigate back to the ladder.
viewArgs = [{supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel}, @level.get('slug')]
ladderURL = "/play/ladder/#{@level.get('slug') || @level.id}#my-matches"
ladderURL = "/play/ladder/#{@level.get('slug') || @level.id}"
if leagueID = @getQueryVariable 'league'
leagueType = if @level.get('type') is 'course-ladder' then 'course' else 'clan'
viewArgs.push leagueType
viewArgs.push leagueID
ladderURL += "/#{leagueType}/#{leagueID}"
ladderURL += '#my-matches'
Backbone.Mediator.publish 'router:navigate', route: ladderURL, viewClass: 'views/ladder/LadderView', viewArgs: viewArgs
playSelectionSound: (hero, preload=false) ->
@ -441,6 +442,12 @@ module.exports = class HeroVictoryModal extends ModalView
viewClass = require 'views/courses/CourseDetailsView'
viewArgs.push @courseID
viewArgs.push @courseInstanceID if @courseInstanceID
else if @level.get('type', true) is 'course-ladder'
leagueID = @getQueryVariable 'league'
link = "/play/ladder/#{@level.get('slug')}"
link += "/course/#{leagueID}" if leagueID
Backbone.Mediator.publish 'router:navigate', route: link
return
else
viewClass = require 'views/play/CampaignView'
viewArgs = [options, @getNextLevelCampaign()]

View file

@ -56,7 +56,7 @@ module.exports = class CastButtonView extends CocoView
@$el.find('.submit-button').hide() # Hide Submit for the first few until they run it once.
if @options.session.get('state')?.complete and @options.level.get 'hidesRealTimePlayback'
@$el.find('.done-button').show()
if @options.level.get('slug') is 'thornbush-farm'# and not @options.session.get('state')?.complete
if @options.level.get('slug') in ['course-thornbush-farm', 'thornbush-farm']
@$el.find('.submit-button').hide() # Hide submit until first win so that script can explain it.
@updateReplayability()
@updateLadderSubmissionViews()
@ -114,17 +114,17 @@ module.exports = class CastButtonView extends CocoView
return if @winnable is winnable
@winnable = winnable
@$el.toggleClass 'winnable', @winnable
Backbone.Mediator.publish 'tome:winnability-updated', winnable: @winnable
Backbone.Mediator.publish 'tome:winnability-updated', winnable: @winnable, level: @options.level
if @options.level.get 'hidesRealTimePlayback'
@$el.find('.done-button').toggle @winnable
else if @winnable and @options.level.get('slug') is 'thornbush-farm'
else if @winnable and @options.level.get('slug') in ['course-thornbush-farm', 'thornbush-farm']
@$el.find('.submit-button').show() # Hide submit until first win so that script can explain it.
onGoalsCalculated: (e) ->
# When preloading, with real-time playback enabled, we highlight the submit button when we think they'll win.
return unless e.preload
return if @options.level.get 'hidesRealTimePlayback'
return if @options.level.get('slug') is 'thornbush-farm' # Don't show it until they actually win for this first one.
return if @options.level.get('slug') in ['course-thornbush-farm', 'thornbush-farm'] # Don't show it until they actually win for this first one.
@onNewGoalStates e
updateCastButton: ->

View file

@ -74,12 +74,13 @@ module.exports = class MultiplayerView extends CocoView
onGameSubmitted: (e) ->
# Preserve the supermodel as we navigate back to the ladder.
viewArgs = [{supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel}, @levelID]
ladderURL = "/play/ladder/#{@levelID}#my-matches"
ladderURL = "/play/ladder/#{@levelID}"
if leagueID = @getQueryVariable 'league'
leagueType = if @level?.get('type') is 'course-ladder' then 'course' else 'clan'
viewArgs.push leagueType
viewArgs.push leagueID
ladderURL += "/#{leagueType}/#{leagueID}"
ladderURL += '#my-matches'
Backbone.Mediator.publish 'router:navigate', route: ladderURL, viewClass: 'views/ladder/LadderView', viewArgs: viewArgs
updateLinkSection: ->

View file

@ -31,6 +31,7 @@ module.exports = class PlayAchievementsModal extends ModalView
'rewards'
'collection'
'function'
'query'
])
earnedAchievementsFetcher = new CocoCollection([], {url: '/db/earned_achievement', model: EarnedAchievement})
@ -72,7 +73,7 @@ module.exports = class PlayAchievementsModal extends ModalView
@onEverythingLoaded()
onEverythingLoaded: =>
@achievements.set(@achievements.filter((m) -> m.get('collection') isnt 'level.sessions'))
@achievements.set(@achievements.filter((m) -> m.get('collection') isnt 'level.sessions' or m.get('query')?.team))
for achievement in @achievements.models
if earned = @earnedMap[achievement.id]
achievement.earned = earned

View file

@ -72,6 +72,7 @@
"mongoose-cache": "~0.1.4",
"node-force-domain": "~0.1.0",
"node-gyp": "~0.13.0",
"node-statsd": "^0.1.1",
"passport": "0.1.x",
"passport-local": "0.1.x",
"redis": "",

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,18 +105,35 @@ function getFunnelData(startDay, eventFunnel, testGroupFn, levelSlugs) {
log("Fetching users..");
var userGroupMap = {};
cursor = db['users'].find({_id : {$in: users}});
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] = testGroupFn(doc.testGroupNumber);
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}});
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();
@ -129,6 +150,8 @@ function getFunnelData(startDay, eventFunnel, testGroupFn, levelSlugs) {
userBrowserMap[user] = browserInfo;
}
}
log("Fetched", Math.min(sessionOffset, levelSessions.length), "sessions");
}
// printjson(lsBrowserMap);
log("Mapping data..");
@ -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

@ -0,0 +1,44 @@
// Print out classrooms ordered by size
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
var userClassroomMap = {};
var cursor = db.classrooms.find({$where: 'this.members.length > 40'}, {ownerID: 1, name: 1, members: 1});
while (cursor.hasNext()) {
var doc = cursor.next();
var userID = doc.ownerID.valueOf();
if (!userClassroomMap[userID]) userClassroomMap[userID] = [];
userClassroomMap[userID].push({
classroomID: doc._id,
className: doc.name,
count: doc.members.length
});
}
var userIDs = [];
for (var userID in userClassroomMap) {
userIDs.push(new ObjectId(userID));
}
var classrooms = [];
cursor = db.users.find({_id: {$in: userIDs}}, {email: 1});
while (cursor.hasNext()) {
var doc = cursor.next();
var userID = doc._id.valueOf();
for (var i = 0; i < userClassroomMap[userID].length; i++) {
classrooms.push({
ownerID: userID,
email: doc.email,
classroomID: userClassroomMap[userID][i].classroomID,
className: userClassroomMap[userID][i].className,
count: userClassroomMap[userID][i].count
});
}
}
classrooms.sort(function(a, b) { return a.count > b.count ? -1 : 1;});
for (var i = 0; i < classrooms.length; i++) {
print(classrooms[i].count, classrooms[i].className, classrooms[i].email, classrooms[i].classroomID.valueOf());
}
print("Total classes:", classrooms.length);

View file

@ -0,0 +1,78 @@
// Add user.courseInstances properties and then add those to session leagues
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
addCourseInstancesToUsers();
function uniq(array) {
var u = {}, a = [];
for(var i = 0, l = array.length; i < l; ++i){
if(u.hasOwnProperty(array[i])) {
continue;
}
a.push(array[i]);
u[array[i]] = 1;
}
return a;
}
function addCourseInstancesToUsers() {
print("Adding courseInstances to users...");
var cursor = db.course.instances.find({$where: "this.members && this.members.length > 1"}, {members: 1});
print("CourseInstances with users found: " + cursor.count());
var courseInstances = cursor.toArray();
var userIDList = [];
courseInstances.forEach(function (courseInstance, courseInstanceIndex) {
userIDList = userIDList.concat(courseInstance.members);
var conditions = {_id: {$in: courseInstance.members}};
var operations = {$addToSet: {courseInstances: courseInstance._id}};
//print('Fetching', JSON.stringify(conditions), 'with operations', JSON.stringify(operations));
//print("... Have this many:", db.users.count(conditions));
var result = db.users.update(conditions, operations, {multi: true});
if (courseInstanceIndex % 100 === 0)
print("Done", courseInstanceIndex, "\tof", courseInstances.length, "course instances.");
});
print("Done adding course instances to users; now going to add them to sessions for leagues.");
addCourseInstancesToSessions(userIDList);
}
function addCourseInstancesToSessions(userIDList) {
userIDList = uniq(userIDList);
print("Adding courseInstance leagues to sessions for", userIDList.length, "users...");
var cursor = db.users.find({_id: {$in: userIDList}, courseInstances: {$exists: true}}, {courseInstances: 1, name: 1, leagues: 1});
print("Users with courseInstances found: " + cursor.count(), '-- supposed to have:', userIDList.length);
var users = cursor.toArray();
var arenas = [
"5442ba0e1e835500007eb1c7",
"550363b4ec31df9c691ab629",
"5469643c37600b40e0e09c5b",
"54b83c2629843994803c838e",
"544437e0645c0c0000c3291d",
"5630eab0c0fcbd86057cc2f8",
"55de80407a57948705777e89"
];
users.forEach(function (user, userIndex) {
var sessions = db.level.sessions.find({creator: user._id + '', 'level.original': {$in: arenas}, submitted: true}).toArray();
//print("Found sessions", sessions, "for user", user._id, user.name, 'who has courseInstances', user.courseInstances.join(', '));
sessions.forEach(function(session, sessionIndex) {
var leagues = session.leagues || [];
for (var i = 0; i < user.courseInstances.length; ++i) {
var alreadyHave = false;
for (var j = 0; j < leagues.length; ++j)
if (leagues[j].leagueID == user.courseInstances[i])
alreadyHave = true;
if (!alreadyHave)
leagues.push({leagueID: user.courseInstances[i] + '', stats: {standardDeviation: 25 / 3, numberOfWinsAndTies: 0, numberOfLosses: 0, totalScore: 10, meanStrength: 25}});
}
//print(" Setting leagues to...");
//printjson(leagues);
session.leagues = leagues;
db.level.sessions.save(session);
});
if (userIndex % 100 === 0)
print("Done", userIndex, "\tof", users.length, "users.");
});
print("Done.");
}

View file

@ -0,0 +1,155 @@
// Add 2 course headcount to older approved teacher surveys
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
// Who needs 2 course headcount added?
// Approved trial.requests but no prepaid with properties.trialRequestID set
// Priority userID selection
// 1. UserID that redeemed approved prepaid
// 2. UserID that applied for trial request
// 3. User email that applied for trial request
// NOTE: May give course headcount to multiple accounts if they applied and redeemed with different users
addHeadcount();
function addHeadcount() {
print("Finding approved trial requests..");
var approvedUserIDMap = {};
var approvedUserEmails = [];
var codeRequestMap = {};
var userIDRequestMap = {};
var userEmailRequestMap = {};
var cursor = db['trial.requests'].find({status: 'approved', type: 'subscription'}, {});
while (cursor.hasNext()) {
var doc = cursor.next();
if (doc.applicant) {
approvedUserIDMap[doc.applicant.valueOf()] = false;
userIDRequestMap[doc.applicant.valueOf()] = doc._id;
}
if (doc.prepaidCode) {
codeRequestMap[doc.prepaidCode] = doc._id;
}
approvedUserEmails.push(doc.properties.email.toLowerCase());
userEmailRequestMap[doc.properties.email.toLowerCase()] = doc._id;
}
print("Finding users via redeemed prepaids..");
cursor = db.prepaids.find({code: {$in: Object.keys(codeRequestMap)}});
while (cursor.hasNext()) {
var doc = cursor.next();
if (doc.redeemers && doc.redeemers.length > 0) {
for (var i = 0; i < doc.redeemers.length; i++) {
approvedUserIDMap[doc.redeemers[i].userID.valueOf()] = false;
userIDRequestMap[doc.redeemers[i].userID.valueOf()] = codeRequestMap[doc.code];
}
}
}
print("Finding users via approved emails..");
cursor = db.users.find({emailLower: {$in: approvedUserEmails}});
while (cursor.hasNext()) {
var doc = cursor.next();
approvedUserIDMap[doc._id.valueOf()] = false;
if (userEmailRequestMap[doc.emailLower]) {
// Trial request had a known email, but not an applicant field set
userIDRequestMap[doc._id.valueOf()] = userEmailRequestMap[doc.emailLower];
}
}
var approvedUserIDs = [];
for (var userID in approvedUserIDMap) {
approvedUserIDs.push(new ObjectId(userID));
}
print("Approved user IDs:", approvedUserIDs.length);
print("Finding approved users with trial request headcount..");
cursor = db.prepaids.find({$and: [{creator: {$in: approvedUserIDs}}, {'properties.trialRequestID': {$exists: true}}]});
while (cursor.hasNext()) {
var doc = cursor.next();
approvedUserIDMap[doc.creator.valueOf()] = true;
}
var needsHeadcount = [];
for (var userID in approvedUserIDMap) {
if (approvedUserIDMap[userID] === false) {
needsHeadcount.push(ObjectId(userID));
}
}
print("Needs headcount:", needsHeadcount.length);
var updateCount = 0;
function insertHeadCount(userID) {
if (!userIDRequestMap[userID.valueOf()]) {
print('ERROR: No trial request ID', userID);
print('Trial course headcount prepaids inserted:', updateCount);
return;
}
generateNewCode(function(code) {
if (!code) {
print("ERROR: no code");
return;
}
criteria = {
creator: userID,
type: 'course',
maxRedeemers: NumberInt(2),
properties: {
trialRequestID: userIDRequestMap[userID.valueOf()]
},
exhausted: false,
__v: NumberInt(0)
};
if (!db.prepaids.findOne(criteria)) {
// print('Adding trial request prepaid for', userID, code);
criteria.code = code;
var writeResult = db.prepaids.insert(criteria);
updateCount += writeResult.nInserted;
}
else {
print('ERROR: Already has trial request headcount', userID, criteria.properties.trialRequestID);
print('Trial course headcount prepaids inserted:', updateCount);
return;
}
if (updateCount < 500 && needsHeadcount.length > 0) {
insertHeadCount(needsHeadcount.pop());
}
else {
print('Trial course headcount prepaids inserted:', updateCount);
}
});
}
if (needsHeadcount.length > 0) {
insertHeadCount(needsHeadcount.pop());
}
}
function generateNewCode(done)
{
function tryCode() {
code = createCode(8);
criteria = {code: code};
if (db.prepaids.findOne(criteria)) {
return tryCode();
}
return done(code);
}
tryCode();
}
function createCode(length)
{
var text = "";
var possible = "abcdefghijklmnopqrstuvwxyz0123456789";
for( var i=0; i < length; i++ ) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}

View file

@ -0,0 +1,113 @@
// Set created field if necessary, and migrate previous teacher trial requests to database collection trial.requests
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password> --eval "var csvFilePath = '/Users/blah/Downloads/externalRequestData.csv'"
// NOTES
// Do not have prepaid IDs for these approved requests
// Do not have applicant user IDs
// Adding special properties.fromGoogleForm = true flag
if (typeof csvFilePath === 'undefined') {
print("ERROR: no csvFilePath specified");
}
else {
addCreated();
insertExternalRequests();
}
function addCreated() {
// Set created property if it's missing
var cursor = db['trial.requests'].find({created: {$exists: false}});
var updateCount = 0;
while (cursor.hasNext()) {
var doc = cursor.next();
var writeResult = db['trial.requests'].update(
{_id: doc._id},
{$set: {created: doc._id.getTimestamp()}}
);
// printjson(writeResult);
updateCount += writeResult.nModified;
if (updateCount > 500) break;
}
print("Num created field added: ", updateCount);
}
function insertExternalRequests() {
var file = cat(csvFilePath);
var lines = file.split('\n');
var lineRegExp = /(?!\s*$)\s*(?:'([^'\\]*(?:\\[\S\s][^'\\]*)*)'|"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*))\s*(?:,|$)/g;
updateCount = 0;
for (var i = 1; i < lines.length; i++) {
var matches = lines[i].match(lineRegExp);
if (!matches || matches.length < 6) {
print(i, lines[i]);
continue;
}
// Instead of fixing the line regex
for (var j = 0; j < matches.length; j++) {
matches[j] = matches[j].substring(0, matches[j].length - 1);
matches[j] = matches[j].replace(/"/ig, '');
}
// Build date
var dateMatches = matches[0].split(/[\/: ]{1}/ig);
if (!dateMatches || dateMatches.length !== 6) {
print(matches[0], dateMatches.length);
break;
}
var year = parseInt(dateMatches[2]);
var month = parseInt(dateMatches[0]);
var day = parseInt(dateMatches[1]);
var hours = parseInt(dateMatches[3]);
var minutes = parseInt(dateMatches[4]);
var seconds = parseInt(dateMatches[5]);
if (month < 10) month = "0" + month;
if (day < 10) day = "0" + day;
if (hours < 10) hours = "0" + hours;
if (minutes < 10) minutes = "0" + minutes;
if (seconds < 10) seconds = "0" + seconds;
var isoDate = year + "-" + month + "-" + day + "T" + hours + ":" + minutes + ":" + seconds + ".000Z";
var created = new Date(isoDate);
// print(created.toISOString());
var reviewDate = created;
var reviewer = new ObjectId('52f94443fcb334581466a992');
var properties = {
heardAbout: matches[5],
numStudents: matches[4],
age: matches[3],
location: matches[2],
school: matches[2],
email: matches[1],
fromGoogleForm: true
};
var status = 'approved';
var type = 'subscription';
// print(created, reviewDate, reviewer, status, type);
// printjson(properties);
// Insert based on email
writeResult = db['trial.requests'].update(
{$and: [{'properties.fromGoogleForm': true}, {'properties.email': properties.email}]},
{
$set: {
created: created,
type: 'subscription',
properties: properties,
status: 'approved',
reviewDate: reviewDate,
reviewer: reviewer
}
},
{ upsert: true}
);
updateCount += writeResult.nModified + writeResult.nUpserted;
if (updateCount >= 500) break;
}
print("Num external trial requests inserted:", updateCount);
}

View file

@ -52,8 +52,27 @@ CampaignHandler = class CampaignHandler extends Handler
documents = (@formatEntity(req, doc) for doc in documents)
@sendSuccess(res, documents)
getOverworld: (req, res) ->
return @sendForbiddenError(res) if not @hasAccess(req)
projection = {}
if req.query.project
projection[field] = 1 for field in req.query.project.split(',')
q = @modelClass.find {type: 'hero'}, projection
q.exec (err, documents) =>
return @sendDatabaseError(res, err) if err
formatCampaign = (doc) =>
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', 'slug']
obj
documents = (formatCampaign(doc) for doc in documents)
@sendSuccess(res, documents)
getByRelationship: (req, res, args...) ->
relationship = args[1]
return @getOverworld(req,res) if args[0] is '-' and relationship is 'overworld'
if relationship in ['levels', 'achievements']
projection = {}
if req.query.project

View file

@ -11,14 +11,19 @@ ClassroomSchema.statics.privateProperties = []
ClassroomSchema.statics.editableProperties = [
'description'
'name'
'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()
@ -26,8 +31,9 @@ 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()
)

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'))
@ -67,7 +68,7 @@ ClassroomHandler = class ClassroomHandler extends Handler
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
@ -86,7 +87,7 @@ 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, {})

View file

@ -0,0 +1,43 @@
fs = require 'fs'
path = require 'path'
config = require '../../server_config'
StatsD = require 'node-statsd'
if config.statsd
realClient = new StatsD(config.statsd)
else
mock = new StatsD(mock: true)
exports.client = realClient or mock
exports.middleware = (req, res, next) ->
req.statsd = exports.client
if realClient
time = process.hrtime();
cleanup = ->
res.removeListener 'finish', recordMetrics
res.removeListener 'error', cleanup
res.removeListener 'close', cleanup
recordMetrics = ->
diff = process.hrtime(time);
ms = (diff[0] * 1000 + diff[1] / 1e6);
path = req.route?.path or '/*'
stat = req.method + "." + path.replace /[^A-Za-z0-9]+/g, '_'
realClient.timing stat, ms
res.once 'finish', recordMetrics
res.once 'error', cleanup
res.once 'close', cleanup
else
req.statsd = mock
next() unless not next
exports.trace = (name, callback) ->
return callback unless realClient
time = process.hrtime()
(args...) ->
realClient.timing name, ms
return callback.apply(this, args)

View file

@ -87,6 +87,8 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
members.push(userID)
courseInstance.set('members', members)
courseInstance.save (err, courseInstance) =>
return @sendDatabaseError(res, err) if err
User.update {_id: mongoose.Types.ObjectId(userID)}, {$addToSet: {courseInstances: courseInstance.get('_id')}}, (err) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, courseInstance))

View file

@ -11,6 +11,7 @@ log = require 'winston'
Campaign = require '../campaigns/Campaign'
Course = require '../courses/Course'
CourseInstance = require '../courses/CourseInstance'
Classroom = require '../classrooms/Classroom'
LevelHandler = class LevelHandler extends Handler
modelClass: Level
@ -60,6 +61,7 @@ LevelHandler = class LevelHandler extends Handler
'tasks'
'helpVideos'
'campaign'
'campaignIndex'
'replayable'
'buildTime'
'scoreTypes'
@ -129,10 +131,16 @@ LevelHandler = class LevelHandler extends Handler
courses = _.filter(courses, (course) -> course.get('campaignID').toString() in campaignStrings)
courseStrings = (course.id.toString() for course in courses)
courseInstances = _.filter(courseInstances, (courseInstance) -> courseInstance.get('courseID').toString() in courseStrings)
aceConfigs = (ci.get('aceConfig') for ci in courseInstances)
classroomIDs = (courseInstance.get('classroomID') for courseInstance in courseInstances)
classroomIDs = _.filter _.uniq classroomIDs, false, (objectID='') -> objectID.toString()
if classroomIDs.length
Classroom.find({ _id: { $in: classroomIDs }}).exec (err, classrooms) =>
aceConfigs = (c.get('aceConfig') for c in classrooms)
aceConfig = _.filter(aceConfigs)[0] or {}
req.codeLanguage = aceConfig.language
@createAndSaveNewSession(sessionQuery, req, res)
else
@createAndSaveNewSession(sessionQuery, req, res)
else
return @sendPaymentRequiredError(res, 'You must be in a course which includes this level to play it')

View file

@ -74,20 +74,19 @@ PrepaidHandler = class PrepaidHandler extends Handler
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) if not prepaid
return @sendForbiddenError(res) if prepaid.get('creator').toString() isnt req.user.id
return @sendForbiddenError(res) if _.size(prepaid.get('redeemers')) >= prepaid.get('maxRedeemers')
return @sendForbiddenError(res) if prepaid.get('redeemers')? and _.size(prepaid.get('redeemers')) >= prepaid.get('maxRedeemers')
return @sendForbiddenError(res) unless prepaid.get('type') is 'course'
return @sendForbiddenError(res) if prepaid.get('properties')?.endDate < new Date()
User.findById(req.body.userID).exec (err, user) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'User for given ID not found') if not user
userID = user.get('_id')
# Prepaid.count {'redeemers.userID': userID}, (err, count) =>
# return @sendDatabaseError(res, err) if err
# return @sendSuccess(res, @formatEntity(req, prepaid)) if count
return @sendSuccess(res, @formatEntity(req, prepaid)) if user.get('coursePrepaidID')
query =
_id: prepaid.get('_id')
'redeemers.userID': { $ne: req.user.get('_id') }
$where: "this.redeemers.length < #{prepaid.get('maxRedeemers')}"
'redeemers.userID': { $ne: user.get('_id') }
$where: "this.maxRedeemers > 0 && (!this.redeemers || this.redeemers.length < #{prepaid.get('maxRedeemers')})"
update = { $push: { redeemers : { date: new Date(), userID: userID } }}
Prepaid.update query, update, (err, nMatched) =>
return @sendDatabaseError(res, err) if err
@ -243,12 +242,15 @@ PrepaidHandler = class PrepaidHandler extends Handler
return @sendBadInputError(res, 'Bad creator') unless utils.isID creator
q = {
_id: {$gt: cutoffID}
creator: mongoose.Types.ObjectId(creator),
creator: mongoose.Types.ObjectId(creator)
type: 'course'
}
Prepaid.find q, (err, prepaids) =>
return @sendDatabaseError(res, err) if err
return @sendSuccess(res, (@formatEntity(req, prepaid) for prepaid in prepaids))
documents = []
for prepaid in prepaids
documents.push(@formatEntity(req, prepaid)) unless prepaid.get('properties')?.endDate < new Date()
return @sendSuccess(res, documents)
else
super(arguments...)

View file

@ -67,7 +67,7 @@ updateSessionToSubmit = (transpiledCode, user, sessionToUpdate, callback) ->
# Reset all league stats as well, and enter the session into any leagues the user is currently part of (not retroactive when joining new leagues)
leagueIDs = user.get('clans') or []
#leagueIDs = leagueIDs.concat user.get('courseInstances') or []
leagueIDs = leagueIDs.concat user.get('courseInstances') or []
leagueIDs = (leagueID + '' for leagueID in leagueIDs) # Make sure to save them as strings.
newLeagues = []
for leagueID in leagueIDs

View file

@ -38,7 +38,7 @@ getRandomSessions = (user, callback) ->
# Only people in a league will end up simulating internal league matches (for leagues they're in) except by dumb chance.
# If we don't like that, we can rework sampleByLevel to have an opportunity to switch to internal leagues if the first session had a league affiliation.
leagueIDs = user?.get('clans') or []
#leagueIDs = leagueIDs.concat user?.get('courseInstances') or []
leagueIDs = leagueIDs.concat user?.get('courseInstances') or []
leagueIDs = (leagueID + '' for leagueID in leagueIDs) # Make sure to fetch them as strings.
return sampleByLevel callback unless leagueIDs.length and Math.random() > 1 / leagueIDs.length
leagueID = _.sample leagueIDs

View file

@ -58,6 +58,7 @@ retrieveOldSessionData = (sessionID, callback) ->
totalScore: session.totalScore ? (25 - 1.8*(25/3))
id: sessionID
submittedCodeLanguage: session.submittedCodeLanguage
ladderAchievementDifficulty: session.ladderAchievementDifficulty
if session.leagues?.length
_.find(@clientResponseObject.sessions, sessionID: sessionID).leagues = session.leagues
oldScoreObject.leagues = []
@ -74,7 +75,7 @@ retrieveOldSessionData = (sessionID, callback) ->
return formatOldScoreObject @levelSession if sessionID is @levelSession?._id # No need to fetch again
query = _id: sessionID
selection = 'standardDeviation meanStrength totalScore submittedCodeLanguage leagues'
selection = 'standardDeviation meanStrength totalScore submittedCodeLanguage leagues ladderAchievementDifficulty'
LevelSession.findOne(query).select(selection).lean().exec (err, session) ->
return callback err, {'error': 'There was an error retrieving the session.'} if err?
callback err, formatOldScoreObject session
@ -150,6 +151,13 @@ module.exports.addMatchToSessionsAndUpdate = (newScoreObject, callback) ->
async.each sessionIDs, updateMatchesInSession.bind(@, matchObject), (err) ->
callback err
ladderBenchmarkAIs =
'564ba6cea33967be1312ae59': 0
'564ba830a33967be1312ae61': 1
'564ba91aa33967be1312ae65': 2
'564ba95ca33967be1312ae69': 3
'564ba9b7a33967be1312ae6d': 4
updateMatchesInSession = (matchObject, sessionID, callback) ->
currentMatchObject = {}
currentMatchObject.date = matchObject.date
@ -163,6 +171,11 @@ updateMatchesInSession = (matchObject, sessionID, callback) ->
#currentMatchObject.randomSeed = parseInt(@clientResponseObject.randomSeed or 0, 10) # Uncomment when actively debugging simulation mismatches
sessionUpdateObject = @levelSessionUpdates[sessionID]
sessionUpdateObject.$push.matches = {$each: [currentMatchObject], $slice: -200}
if currentMatchObject.metrics.rank is 0 and defeatedAI = ladderBenchmarkAIs[currentMatchObject.opponents[0].userID]
mySession = _.find @clientResponseObject.sessions, sessionID: sessionID
newLadderAchievementDifficulty = Math.max defeatedAI, mySession.ladderAchievementDifficulty || 0
if newLadderAchievementDifficulty isnt mySession.ladderAchievementDifficulty
sessionUpdateObject.ladderAchievementDifficulty = newLadderAchievementDifficulty
myScoreObject = @newScoresObject[sessionID]
opponentSession = _.find @clientResponseObject.sessions, (session) -> session.sessionID isnt sessionID

View file

@ -11,7 +11,6 @@ if config.unittest
module.exports.api.send = ->
module.exports.templates =
parent_subscribe_email: 'tem_2APERafogvwKhmcnouigud'
setup_free_sub_email: 'tem_sqdvLCZRwoDQc6jAf5RrQE'
share_progress_email: 'tem_VHE3ihhGmVa3727qds9zY8'
welcome_email: 'utnGaBHuSU4Hmsi7qrAypU'
ladder_update_email: 'JzaZxf39A4cKMxpPZUfWy4'
@ -23,4 +22,5 @@ module.exports.templates =
plain_text_email: 'tem_85UvKDCCNPXsFckERTig6Y'
next_steps_email: 'tem_RDHhTG5inXQi8pthyqWr5D'
course_invite_email: 'tem_u6D2EFWYC5Ptk38bSykjsU'
teacher_free_trial: 'tem_sqdvLCZRwoDQc6jAf5RrQE'
teacher_free_trial_hoc: 'tem_4ZSY9wsA9Qwn4wBFmZgPdc'

View file

@ -11,24 +11,6 @@ TrialRequestSchema = new mongoose.Schema {}, {strict: false, minimize: false, re
TrialRequestSchema.pre 'save', (next) ->
return next() unless @get('status') is 'approved'
# Add subscription
Prepaid.generateNewCode (code) =>
unless code
log.error "Trial request pre save prepaid gen new code failure"
return next()
prepaid = new Prepaid
creator: @get('reviewer')
type: 'subscription'
maxRedeemers: 1
code: code
properties:
couponID: 'free'
prepaid.save (err) =>
if err
log.error "Trial request prepaid creation error: #{err}"
return next()
@set('prepaidCode', code)
# Add 2 course headcount
prepaid = new Prepaid
creator: @get('applicant')
@ -36,6 +18,20 @@ TrialRequestSchema.pre 'save', (next) ->
maxRedeemers: 2
properties:
trialRequestID: @get('_id')
prepaid.save (err) =>
if err
log.error "Trial request prepaid creation error: #{err}"
# Special HoC trial: Add 500 course headcount with end date
endDate = new Date()
endDate.setUTCMonth(endDate.getUTCMonth() + 2)
prepaid = new Prepaid
creator: @get('applicant')
type: 'course'
maxRedeemers: 500
properties:
endDate: endDate
trialRequestID: @get('_id')
prepaid.save (err) =>
if err
log.error "Trial request prepaid creation error: #{err}"
@ -43,24 +39,19 @@ TrialRequestSchema.pre 'save', (next) ->
TrialRequestSchema.post 'save', (doc) ->
if doc.get('status') is 'submitted'
msg = "<a href=\"http://codecombat.com/admin/trial-requests\">Trial Request</a> submitted by #{doc.get('properties').email}"
msg = "<a href=\"http://codecombat.com/admin/trial-requests\">Trial Request</a> submitted by #{doc.get('properties')?.email}"
hipchat.sendHipChatMessage msg, ['tower']
else if doc.get('status') is 'approved'
ppc = doc.get('prepaidCode')
unless ppc
log.error 'Trial request post save no ppc'
return
emailParams =
recipient:
address: doc.get('properties')?.email
email_id: sendwithus.templates.setup_free_sub_email
email_data:
url: "https://codecombat.com/account/subscription?_ppc=#{ppc}";
email_id: sendwithus.templates.teacher_free_trial_hoc
sendwithus.api.send emailParams, (err, result) =>
log.error "sendwithus trial request approved error: #{err}, result: #{result}" if err
TrialRequestSchema.statics.privateProperties = []
TrialRequestSchema.statics.editableProperties = [
'created'
'prepaidCode'
'properties'
'reviewDate'

View file

@ -19,6 +19,7 @@ TrialRequestHandler = class TrialRequestHandler extends Handler
makeNewInstance: (req) ->
instance = super(req)
instance.set 'applicant', req.user._id
instance.set 'created', new Date()
instance.set 'status', 'submitted'
instance

View file

@ -459,6 +459,7 @@ UserHandler = class UserHandler extends Handler
sendMail emailParams
getPrepaidCodes: (req, res) ->
return @sendSuccess(res, []) unless req.user?
orQuery = [{ creator: req.user._id }, { 'redeemers.userID' : req.user._id }]
Prepaid.find({}).or(orQuery).exec (err, documents) =>
@sendSuccess(res, documents)

View file

@ -84,5 +84,10 @@ if not config.unittest and not config.isProduction
# change artificially slow down non-static requests for testing
config.slow_down = false
if process.env.COCO_STATSD_HOST
config.statsd =
host: process.env.COCO_STATSD_HOST
port: process.env.COCO_STATSD_PORT or 8125
prefix: process.env.COCO_STATSD_PREFIX or ''
module.exports = config

View file

@ -8,6 +8,7 @@ compressible = require 'compressible'
geoip = require 'geoip-lite'
database = require './server/commons/database'
perfmon = require './server/commons/perfmon'
baseRoute = require './server/routes/base'
user = require './server/users/user_handler'
logging = require './server/commons/logging'
@ -127,8 +128,11 @@ setupRedirectMiddleware = (app) ->
nameOrID = req.path.split('/')[3]
res.redirect 301, "/user/#{nameOrID}/profile"
setupPerfMonMiddleware = (app) ->
app.use perfmon.middleware
exports.setupMiddleware = (app) ->
setupPerfMonMiddleware app
setupCountryRedirectMiddleware app, "china", "CN", "zh", "tokyo"
setupCountryRedirectMiddleware app, "brazil", "BR", "pt-BR", "saoPaulo"
setupMiddlewareToSendOldBrowserWarningWhenPlayersViewLevelDirectly app

View file

@ -71,12 +71,23 @@ describe 'GET /db/level/<id>/session', ->
loginJoe (joe) ->
classroom = new Classroom({
name: 'Test Classroom'
members: [ joe.get('_id') ]
aceConfig: { language: 'javascript' }
})
classroom.save (err, classroom) ->
expect(err).toBeNull()
courseInstance = new CourseInstance({
name: 'Course Instance'
members: [
joe.get('_id')
]
courseID: ObjectId(course.id)
classroomID: ObjectId(classroom.id)
})
courseInstance.save (err) ->
@ -89,8 +100,9 @@ describe 'GET /db/level/<id>/session', ->
url = getURL("/db/level/#{levelID}/session")
request.get { uri: url }, (err, res, body) ->
request.get { uri: url, json: true }, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.codeLanguage).toBe('javascript')
done()
it 'does not create a new session if the user is not in a course with that level', (done) ->

View file

@ -43,6 +43,7 @@ describe '/db/prepaid', ->
redeemers: [],
creator: user1.get('_id')
code: 0
type: 'course'
})
prepaid.save (err, prepaid) ->
otherUser = new User()
@ -67,6 +68,7 @@ describe '/db/prepaid', ->
redeemers: [],
creator: user1.get('_id')
code: 1
type: 'course'
})
prepaid.save (err, prepaid) ->
otherUser = new User()
@ -84,6 +86,7 @@ describe '/db/prepaid', ->
redeemers: [],
creator: user1.get('_id')
code: 2
type: 'course'
})
prepaid.save (err, prepaid) ->
loginNewUser (user2) ->
@ -97,11 +100,14 @@ describe '/db/prepaid', ->
it 'is idempotent across prepaids collection', (done) ->
loginNewUser (user1) ->
otherUser = new User()
otherUser = new User({
'coursePrepaidID': new ObjectId()
})
otherUser.save (err, otherUser) ->
prepaid1 = new Prepaid({
redeemers: [{userID: otherUser.get('_id')}],
code: 3
type: 'course'
})
prepaid1.save (err, prepaid1) ->
prepaid2 = new Prepaid({
@ -109,6 +115,7 @@ describe '/db/prepaid', ->
redeemers: [],
creator: user1.get('_id')
code: 4
type: 'course'
})
prepaid2.save (err, prepaid2) ->
url = getURL("/db/prepaid/#{prepaid2.id}/redeemers")
@ -119,6 +126,159 @@ describe '/db/prepaid', ->
expect(body.redeemers.length).toBe(0)
done()
it 'is idempotent to itself for a user other than the creator', (done) ->
loginNewUser (user1) ->
prepaid = new Prepaid({
maxRedeemers: 2,
redeemers: [],
creator: user1.get('_id')
code: 0
type: 'course'
})
prepaid.save (err, prepaid) ->
otherUser = new User()
otherUser.save (err, otherUser) ->
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
redeemer = { userID: otherUser.id }
request.post {uri: url, json: redeemer }, (err, res, body) ->
expect(body.redeemers?.length).toBe(1)
expect(res.statusCode).toBe(200)
request.post {uri: url, json: redeemer }, (err, res, body) ->
expect(body.redeemers?.length).toBe(1)
expect(res.statusCode).toBe(200)
prepaid = Prepaid.findById body._id, (err, prepaid) ->
expect(err).toBeNull()
expect(prepaid.get('redeemers').length).toBe(1)
User.findById otherUser.id, (err, user) ->
expect(user.get('coursePrepaidID').equals(prepaid.get('_id'))).toBe(true)
done()
it 'is idempotent to itself for the creator', (done) ->
loginNewUser (user1) ->
prepaid = new Prepaid({
maxRedeemers: 2,
redeemers: [],
creator: user1.get('_id')
code: 0
type: 'course'
})
prepaid.save (err, prepaid) ->
otherUser = new User()
otherUser.save (err, otherUser) ->
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
redeemer = { userID: user1.id }
request.post {uri: url, json: redeemer }, (err, res, body) ->
expect(body.redeemers?.length).toBe(1)
expect(res.statusCode).toBe(200)
request.post {uri: url, json: redeemer }, (err, res, body) ->
expect(body.redeemers?.length).toBe(1)
expect(res.statusCode).toBe(200)
prepaid = Prepaid.findById body._id, (err, prepaid) ->
expect(err).toBeNull()
expect(prepaid.get('redeemers').length).toBe(1)
User.findById user1.id, (err, user) ->
expect(user.get('coursePrepaidID').equals(prepaid.get('_id'))).toBe(true)
redeemer = { userID: otherUser.id }
request.post {uri: url, json: redeemer }, (err, res, body) ->
expect(body.redeemers?.length).toBe(2)
expect(res.statusCode).toBe(200)
done()
it 'return terminal prepaids', (done) ->
endDate = new Date()
endDate.setUTCMonth(endDate.getUTCMonth() + 2)
loginNewUser (user1) ->
prepaid = new Prepaid({
maxRedeemers: 500,
redeemers: [],
creator: user1.get('_id')
type: 'course'
properties:
endDate: endDate
})
prepaid.save (err, prepaid) ->
expect(err).toBeNull()
url = getURL("/db/prepaid?creator=#{user1.id}")
request.get {uri: url}, (err, res, body) ->
expect(res.statusCode).toBe(200)
documents = JSON.parse(body)
expect(documents.length).toEqual(1)
return done() unless documents.length is 1
expect(documents[0]?.properties?.endDate).toEqual(endDate.toISOString())
done()
it 'do not return expired terminal prepaids', (done) ->
endDate = new Date()
endDate.setUTCMonth(endDate.getUTCMonth() - 1)
loginNewUser (user1) ->
prepaid = new Prepaid({
maxRedeemers: 500,
redeemers: [],
creator: user1.get('_id')
type: 'course'
properties:
endDate: endDate
})
prepaid.save (err, prepaid) ->
expect(err).toBeNull()
url = getURL("/db/prepaid?creator=#{user1.id}")
request.get {uri: url}, (err, res, body) ->
expect(res.statusCode).toBe(200)
documents = JSON.parse(body)
expect(documents.length).toEqual(0)
done()
it 'redeem terminal prepaids', (done) ->
endDate = new Date()
endDate.setUTCMonth(endDate.getUTCMonth() + 2)
loginNewUser (user1) ->
prepaid = new Prepaid({
maxRedeemers: 500,
redeemers: [],
creator: user1.get('_id')
type: 'course'
properties:
endDate: endDate
})
prepaid.save (err, prepaid) ->
expect(err).toBeNull()
otherUser = new User()
otherUser.save (err, otherUser) ->
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
redeemer = { userID: otherUser.id }
request.post {uri: url, json: redeemer }, (err, res, body) ->
expect(body.redeemers?.length).toBe(1)
expect(res.statusCode).toBe(200)
return done() unless res.statusCode is 200
prepaid = Prepaid.findById body._id, (err, prepaid) ->
expect(err).toBeNull()
expect(prepaid.get('redeemers').length).toBe(1)
User.findById otherUser.id, (err, user) ->
expect(user.get('coursePrepaidID').equals(prepaid.get('_id'))).toBe(true)
done()
it 'do not redeem expired terminal prepaids', (done) ->
endDate = new Date()
endDate.setUTCMonth(endDate.getUTCMonth() - 1)
loginNewUser (user1) ->
prepaid = new Prepaid({
maxRedeemers: 500,
redeemers: [],
creator: user1.get('_id')
type: 'course'
properties:
endDate: endDate
})
prepaid.save (err, prepaid) ->
expect(err).toBeNull()
otherUser = new User()
otherUser.save (err, otherUser) ->
url = getURL("/db/prepaid/#{prepaid.id}/redeemers")
redeemer = { userID: otherUser.id }
request.post {uri: url, json: redeemer }, (err, res, body) ->
expect(res.statusCode).toBe(403)
done()
it 'Clear database', (done) ->
clearModels [Course, CourseInstance, Payment, Prepaid, User], (err) ->
throw err if err
@ -502,16 +662,16 @@ describe '/db/prepaid', ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginJoe (joe) ->
loginNewUser (user) ->
purchasePrepaid 'terminal_subscription', months: 1, 3, token.id, (err, res, prepaid) ->
request.get "#{getURL('/db/user')}/#{joe.id}/prepaid_codes", (err, res) ->
request.get "#{getURL('/db/user')}/#{user.id}/prepaid_codes", (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(200);
codes = JSON.parse res.body
expect(codes.length).toEqual(2)
expect(codes.length).toEqual(1)
expect(codes[0].maxRedeemers).toEqual(3)
expect(codes[0].properties).toBeDefined()
expect(codes[0].properties.months).toEqual(3)
expect(codes[0].properties.months).toEqual(1)
done()
it 'Test for injection', (done) ->

View file

@ -120,20 +120,24 @@ describe 'Trial Requests', ->
expect(body.reviewDate).toBeDefined()
expect(new Date(body.reviewDate)).toBeLessThan(new Date())
expect(body.reviewer).toEqual(admin.id)
expect(body.prepaidCode).toBeDefined()
TrialRequest.findById body._id, (err, doc) ->
expect(err).toBeNull()
expect(doc.get('status')).toEqual('approved')
expect(doc.get('reviewDate')).toBeDefined()
expect(new Date(doc.get('reviewDate'))).toBeLessThan(new Date())
expect(doc.get('reviewer')).toEqual(admin._id)
expect(doc.get('prepaidCode')).toBeDefined()
Prepaid.findOne {'properties.trialRequestID': doc.get('_id')}, (err, doc) ->
Prepaid.find {'properties.trialRequestID': doc.get('_id')}, (err, prepaids) ->
expect(err).toBeNull()
return done(err) if err
expect(doc.get('type')).toEqual('course')
expect(doc.get('creator')).toEqual(user.get('_id'))
expect(doc.get('maxRedeemers')).toEqual(2)
expect(prepaids.length).toEqual(2)
for prepaid in prepaids
expect(prepaid.get('type')).toEqual('course')
expect(prepaid.get('creator')).toEqual(user.get('_id'))
if prepaid.get('properties').endDate
expect(prepaid.get('maxRedeemers')).toEqual(500)
expect(prepaid.get('properties').endDate).toBeGreaterThan(new Date())
else
expect(prepaid.get('maxRedeemers')).toEqual(2)
done()
it 'Deny trial request', (done) ->