From 332cf728d1299f82ed55e50f37f5e71e1a347c9c Mon Sep 17 00:00:00 2001 From: rohmunhoz Date: Wed, 27 May 2015 16:21:46 -0300 Subject: [PATCH 01/26] Update pt-BR.coffee Translated many strings --- app/locale/pt-BR.coffee | 102 ++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/app/locale/pt-BR.coffee b/app/locale/pt-BR.coffee index 4a7a709f3..5bfd274dc 100644 --- a/app/locale/pt-BR.coffee +++ b/app/locale/pt-BR.coffee @@ -138,7 +138,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: common: back: "Voltar" # When used as an action verb, like "Navigate backward" - continue: "Continue" # When used as an action verb, like "Continue forward" + continue: "Continuar" # When used as an action verb, like "Continue forward" loading: "Carregando..." saving: "Salvando..." sending: "Enviando..." @@ -158,7 +158,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: unwatch: "Não Observar" submit_patch: "Enviar arranjo" submit_changes: "Enviar mudanças" -# save_changes: "Save Changes" + save_changes: "Salvar Alterações" general: and: "e" @@ -262,9 +262,9 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: victory_hour_of_code_done_yes: "Sim, eu terminei minha Hora da Programação!" victory_experience_gained: "XP ganho" victory_gems_gained: "Gems ganhas" -# victory_new_item: "New Item" -# victory_viking_code_school: "Holy smokes, that was a hard level you just beat! If you aren't already a software developer, you should be. You just got fast-tracked for acceptance with Viking Code School, where you can take your skills to the next level and become a professional web developer in 14 weeks." -# victory_become_a_viking: "Become a Viking" + victory_new_item: "Novo Item" + victory_viking_code_school: "Santa fumaça, você acaba de vencer uma fasedifícil! Se você ainda não é um programador, deveria ser. Você acaba de conseguir aceitação rápida para a escola de programação Viking, onde pode aprimorar suas habilidades e se tornar um programador web profissional em 14 semanas." + victory_become_a_viking: "Se tornar um Viking" guide_title: "Guia" tome_minion_spells: "Magias dos seus subordinados" # Only in old-style levels. tome_read_only_spells: "Magias não editáveis" # Only in old-style levels. @@ -292,11 +292,11 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: time_current: "Agora:" time_total: "Máximo:" time_goto: "Ir para:" -# non_user_code_problem_title: "Unable to Load Level" -# infinite_loop_title: "Infinite Loop Detected" -# 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)" + non_user_code_problem_title: "Não foi possível carregar a fase" + infinite_loop_title: "Loop infinito detectado" + infinite_loop_description: "O código inicial para construir o mundo nunca foi executado completamente. Ele é, provavelmente, muito lento ou tem um loop infinito. Ou talvez haja um bug. Você pode tentar rodar esse código novamente ou resetá-lo ao estado original. Se isso não der certo, por favor nos avise." + check_dev_console: "Você também pode abrir o console de desenvolvedor para ver o que está dando errado." + check_dev_console_link: "(instruções)" infinite_loop_try_again: "Tentar novamente" infinite_loop_reset_level: "Resetar nível" infinite_loop_comment_out: "Comentar Meu Código" @@ -306,7 +306,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: tip_open_source: "CodeCombat é 100% código aberto!" tip_beta_launch: "CodeCombat lançou sua versão beta em outubro de 2013." tip_think_solution: "Pense na solução, não no problema." - tip_theory_practice: "Na teoria, não existe diferença entre teoria e prática. Mas, na prática, há. - Yogi Berra" + tip_theory_practice: "Na teoria, não existe diferença entre teoria e prática. Mas, na prática, existe. - Yogi Berra" tip_error_free: "Existem duas formas de escrever programas sem erros; apenas a terceira funciona. - Alan Perlis" tip_debugging_program: "Se depurar é o processo de remover erros, então programar deve ser o processo de adicioná-los. - Edsger W. Dijkstra" tip_forums: "Vá aos fóruns e diga-nos o que você pensa!" @@ -345,8 +345,8 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: tip_hate_computers: "As pessoas realmente pensam porque odeiam computadores. O que eles realmente odeiam são programadores ruins. - Larry Niven" tip_open_source_contribute: "Você pode ajudar CodeCombat a melhorar!" tip_recurse: "Para iterar é humano, para recursão, é divino. - 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" + tip_free_your_mind: "Você precisa abandonar tudo, Neo. Dor, dúvida, e descrença. Liberte sua mente. - Morpheus" + tip_strong_opponents: "Mesmo o mais forte dos oponentes ainda tem uma fraqueza. - Itachi Uchiha" game_menu: inventory_tab: "Inventário" @@ -366,7 +366,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: auth_caption: "Salve seu progresso." leaderboard: -# leaderboard: "Leaderboard" + leaderboard: "Ranking" view_other_solutions: "Ver Outras Soluções" # {change} scores: "Pontuação" top_players: "Top Jogadores por" @@ -413,7 +413,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: feature4: "3500 gemas bônus todo mês!" feature5: "Vídeo tutorials" feature6: "Suporte via e-mail Premium" -# feature7: "Private Clans" + feature7: "Clans Privado" free: "Grátis" month: "mês" subscribe_title: "Inscrever-se" @@ -436,43 +436,43 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: parents: "Para os pais" parents_title: "Seus filhos estão aprendendo a programar." # {change} parents_blurb1: "Com o CodeCombat, seus filhos aprendem a programar de verdade. Eles começam a aprender comandos simples, e progridem para tópicos avançados." -# 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_blurb1a: "Programação de computadores é uma habilidade essencial que seus filhos vão indubitávelmente usar quando adultos. Em 2020, habilidades básicas de software vão ser necessárias a 77% dos empregos, e a demanda por engenheiros de software é alta ao redor do mundo. Você sabia que Ciência da Computação é o diploma universitário mais bem pago?" parents_blurb2: "Apenas $9.99 USD/mês, eles recebem novos desafios todo mês e suporte no email pessoal de programadores profissionais." # {change} parents_blurb3: "Sem risco: 100% devolução do dinheiro garantida, basta um simples clique em desinscrever-se." -# 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" + payment_methods: "Métodos de pagamento" + payment_methods_title: "Métodos de pagamento aceitos" + payment_methods_blurb1: "Atualmente, nós aceitamos cartões de crédito e Alipay." + payment_methods_blurb2: "Se você precisar de uma forma alternativa de pagamento, por favor contate-nos" stripe_description: "Inscrição Mensal" subscription_required_to_play: "Você precisará se inscrever para jogar este nível." unlock_help_videos: "Inscreva-se para desbloquear todos os vídeos tutoriais." -# personal_sub: "Personal Subscription" # 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 free 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." -# group_discounts_1st: "1st subscription" -# group_discounts_full: "Full price" -# group_discounts_2nd: "Subscriptions 2-11" -# group_discounts_20: "20% off" -# group_discounts_12th: "Subscriptions 12+" -# group_discounts_40: "40% off" -# subscribing: "Subscribing..." -# recipient_emails_placeholder: "Enter email address to subscribe, one per line." -# subscribe_users: "Subscribe Users" -# users_subscribed: "Users subscribed:" -# 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" + personal_sub: "Assinatura pessoal" # Accounts Subscription View below + loading_info: "Carregando informações de assinatura..." + managed_by: "Gerenciado por" + will_be_cancelled: "Será cancelada em" + currently_free: "Atualmente, você possui uma assinatura gratuita" + currently_free_until: "Atualmente, você possui uma assinatura gratuita até" + was_free_until: "Você teve uma assinatura gratuita até" + managed_subs: "Assinaturas gerenciadas" + managed_subs_desc: "Adicione assinaturas para outros jogadores (alunos, filhos, etc)" + managed_subs_desc_2: "Destinatários precisam ter uma conta CodeCombat associada ao email que você escolheu." + group_discounts: "Descontos de grupo" + group_discounts_1: "Nós também oferecemos descontos de grupo para assinaturas." + group_discounts_1st: "1ª Assinatura" + group_discounts_full: "Preço total" + group_discounts_2nd: "Assinaturas 2-11" + group_discounts_20: "20% de desconto " + group_discounts_12th: "Assinaturas 12+" + group_discounts_40: "40% de desconto" + subscribing: "Assinando..." + recipient_emails_placeholder: "Insira emails para assinatura, um por linha." + subscribe_users: "Inscrever usuários" + users_subscribed: "Usuários inscritos:" + no_users_subscribed: "Nenhum usuário inscrito, por favor, verifique os emails." + current_recipients: "Destinatários atuais" + unsubscribing: "Desinscrevendo..." + subscribe_prepaid: "Clique em assinar para usar um código pré pago" + using_prepaid: "Utilizando código pré pago para assinatura mensal" choose_hero: choose_hero: "Escolha seu Herói" @@ -592,10 +592,10 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: teachers: title: "CodeCombat para Professores" # {change} -# 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?" -# cost_china: "CodeCombat in China is free for the first five levels, after which it costs $9.99 USD per month for access to our other 140+ levels on our exclusive China servers." + intro_1: "CodeCombat é um jogo online que ensina programação. Alunos escrevem código em linguagens de programação reais." + intro_2: "Sem necessidade de experiência!" + free_title: "Quanto custa?" + cost_china: "CodeCombat na China é gratuito para os primeiro cinco níveis, depois dos quais ele custa $9.99 USD por mês pelo acesso aos nossos outros mais de 140 níveis nos nossos exclusivos servidores na China." # free_1: "CodeCombat Basic is FREE! There are 80+ 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!" From bd4063ed3a92fa84a847b7050df58b4d03101132 Mon Sep 17 00:00:00 2001 From: Imperadeiro98 Date: Wed, 9 Sep 2015 22:36:05 +0100 Subject: [PATCH 02/26] Use playSound across the code --- app/views/ladder/MyMatchesTabView.coffee | 4 ++-- app/views/play/level/LevelChatView.coffee | 2 +- app/views/play/level/LevelGoalsView.coffee | 4 ++-- app/views/play/level/LevelLoadingView.coffee | 4 ++-- app/views/play/level/LevelPlaybackView.coffee | 12 ++++++------ app/views/play/level/modal/HeroVictoryModal.coffee | 10 +++++----- app/views/play/level/modal/VictoryModal.coffee | 2 +- app/views/play/level/tome/ProblemAlertView.coffee | 4 ++-- .../play/level/tome/SpellListTabEntryView.coffee | 4 ++-- .../play/level/tome/SpellPaletteEntryView.coffee | 6 +++--- app/views/play/modal/PollModal.coffee | 2 +- 11 files changed, 27 insertions(+), 27 deletions(-) diff --git a/app/views/ladder/MyMatchesTabView.coffee b/app/views/ladder/MyMatchesTabView.coffee index 43ed6d5c8..d93747e64 100644 --- a/app/views/ladder/MyMatchesTabView.coffee +++ b/app/views/ladder/MyMatchesTabView.coffee @@ -76,7 +76,7 @@ module.exports = class MyMatchesTabView extends CocoView state = 'tie' if match.metrics.rank is opponent.metrics.rank fresh = match.date > (new Date(new Date() - 20 * 1000)).toISOString() if fresh - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'chat_received' + @playSound 'chat_received' { state: state opponentName: @nameMap[opponent.userID] @@ -105,7 +105,7 @@ module.exports = class MyMatchesTabView extends CocoView team.scoreHistory = scoreHistory if not team.isRanking and @previouslyRankingTeams[team.id] - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'cast-end' + @playSound 'cast-end' @previouslyRankingTeams[team.id] = team.isRanking ctx diff --git a/app/views/play/level/LevelChatView.coffee b/app/views/play/level/LevelChatView.coffee index 952f9938c..06c218294 100644 --- a/app/views/play/level/LevelChatView.coffee +++ b/app/views/play/level/LevelChatView.coffee @@ -54,7 +54,7 @@ module.exports = class LevelChatView extends CocoView @playNoise() if e.message.authorID isnt me.id playNoise: -> - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'chat_received' + @playSound 'chat_received' messageObjectToJQuery: (message) -> td = $('') diff --git a/app/views/play/level/LevelGoalsView.coffee b/app/views/play/level/LevelGoalsView.coffee index d28f9657d..f570ec481 100644 --- a/app/views/play/level/LevelGoalsView.coffee +++ b/app/views/play/level/LevelGoalsView.coffee @@ -97,7 +97,7 @@ module.exports = class LevelGoalsView extends CocoView @lastSizeTweenTime = new Date() @updatePlacement() if @soundToPlayWhenPlaybackEnded - Backbone.Mediator.publish 'audio-player:play-sound', trigger: @soundToPlayWhenPlaybackEnded, volume: 1 + @playSound @soundToPlayWhenPlaybackEnded updateHeight: -> return if @$el.hasClass('brighter') or @$el.hasClass('secret') @@ -122,7 +122,7 @@ module.exports = class LevelGoalsView extends CocoView playToggleSound: (sound) => return if @destroyed - Backbone.Mediator.publish 'audio-player:play-sound', trigger: sound, volume: 1 + @playSound sound @soundTimeout = null onSetLetterbox: (e) -> diff --git a/app/views/play/level/LevelLoadingView.coffee b/app/views/play/level/LevelLoadingView.coffee index e39339f37..f95791231 100644 --- a/app/views/play/level/LevelLoadingView.coffee +++ b/app/views/play/level/LevelLoadingView.coffee @@ -71,7 +71,7 @@ module.exports = class LevelLoadingView extends CocoView @startUnveiling() @unveil() else - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'level_loaded', volume: 0.75 # old: loading_ready + @playSound 'level_loaded', 0.75 # old: loading_ready @$el.find('.progress').hide() @$el.find('.start-level-button').show() @@ -97,7 +97,7 @@ module.exports = class LevelLoadingView extends CocoView loadingDetails.css 'top', -loadingDetails.outerHeight(true) @$el.find('.left-wing').css left: '-100%', backgroundPosition: 'right -400px top 0' @$el.find('.right-wing').css right: '-100%', backgroundPosition: 'left -400px top 0' - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'loading-view-unveil', volume: 0.5 + @playSound 'loading-view-unveil', 0.5 _.delay @onUnveilEnded, duration * 1000 $('#level-footer-background').detach().appendTo('#page-container').slideDown(duration * 1000) diff --git a/app/views/play/level/LevelPlaybackView.coffee b/app/views/play/level/LevelPlaybackView.coffee index dcc1340bb..6547a683b 100644 --- a/app/views/play/level/LevelPlaybackView.coffee +++ b/app/views/play/level/LevelPlaybackView.coffee @@ -108,7 +108,7 @@ module.exports = class LevelPlaybackView extends CocoView @realTime = true @togglePlaybackControls false Backbone.Mediator.publish 'playback:real-time-playback-started', {} - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'real-time-playback-start', volume: 1 + @playSound 'real-time-playback-start' onRealTimeMultiplayerCast: (e) -> @realTime = true @@ -160,7 +160,7 @@ module.exports = class LevelPlaybackView extends CocoView ended = button.hasClass 'ended' changed = button.hasClass('playing') isnt @playing button.toggleClass('playing', @playing and not ended).toggleClass('paused', not @playing and not ended) - Backbone.Mediator.publish 'audio-player:play-sound', trigger: (if @playing then 'playback-play' else 'playback-pause'), volume: 1 + @playSound (if @playing then 'playback-play' else 'playback-pause') return # don't stripe the bar bar = @$el.find '.scrubber .progress' bar.toggleClass('progress-striped', @playing and not ended).toggleClass('active', @playing and not ended) @@ -266,7 +266,7 @@ module.exports = class LevelPlaybackView extends CocoView return unless @realTime @realTime = false @togglePlaybackControls true - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'real-time-playback-end', volume: 1 + @playSound 'real-time-playback-end' onStopRealTimePlayback: (e) -> Backbone.Mediator.publish 'level:set-letterbox', on: false @@ -287,14 +287,14 @@ module.exports = class LevelPlaybackView extends CocoView if ratioChange = @getScrubRatio() - oldRatio sound = "playback-scrub-slide-#{if ratioChange > 0 then 'forward' else 'back'}-#{@slideCount % 3}" unless /back/.test sound # We don't have the back sounds in yet: http://discourse.codecombat.com/t/bug-some-mp3-lost/4830 - Backbone.Mediator.publish 'audio-player:play-sound', trigger: sound, volume: Math.min 1, Math.abs ratioChange * 50 + @playSound sound, (Math.min 1, Math.abs ratioChange * 50) start: (event, ui) => return if @shouldIgnore() @slideCount = 0 @wasPlaying = @playing and not $('#play-button').hasClass('ended') Backbone.Mediator.publish 'level:set-playing', {playing: false} - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'playback-scrub-start', volume: 0.5 + @playSound 'playback-scrub-start', 0.5 stop: (event, ui) => @@ -307,7 +307,7 @@ module.exports = class LevelPlaybackView extends CocoView Backbone.Mediator.publish 'level:set-playing', {playing: false} @$el.find('.scrubber-handle').effect('bounce', {times: 2}) else - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'playback-scrub-end', volume: 0.5 + @playSound 'playback-scrub-end', 0.5 ) diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee index 8a7f7b15f..a75f7ca40 100644 --- a/app/views/play/level/modal/HeroVictoryModal.coffee +++ b/app/views/play/level/modal/HeroVictoryModal.coffee @@ -57,7 +57,7 @@ module.exports = class HeroVictoryModal extends ModalView @previousLevel = me.level() else @readyToContinue = true - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'victory' + @playSound 'victory' if @level.get('type', true) is 'course' and nextLevel = @level.get('nextLevel') @nextLevel = new Level().setURL "/db/level/#{nextLevel.original}/version/#{nextLevel.majorVersion}" @nextLevel = @supermodel.loadModel(@nextLevel, 'level').model @@ -220,7 +220,7 @@ module.exports = class HeroVictoryModal extends ModalView @updateXPBars 0 @$el.find('#victory-header').delay(250).queue(-> $(@).removeClass('out').dequeue() - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'victory-title-appear' # TODO: actually add this + @playSound 'victory-title-appear' # TODO: actually add this ) complete = _.once(_.bind(@beginSequentialAnimations, @)) @animatedPanels = $() @@ -283,7 +283,7 @@ module.exports = class HeroVictoryModal extends ModalView @XPEl.text(totalXP) @updateXPBars(totalXP) xpTrigger = 'xp-' + (totalXP % 6) # 6 xp sounds - Backbone.Mediator.publish 'audio-player:play-sound', trigger: xpTrigger, volume: 0.5 + ratio / 2 + @playSound xpTrigger, (0.5 + ratio / 2) @XPEl.addClass 'four-digits' if totalXP >= 1000 and @lastTotalXP < 1000 @XPEl.addClass 'five-digits' if totalXP >= 10000 and @lastTotalXP < 10000 @lastTotalXP = totalXP @@ -294,14 +294,14 @@ module.exports = class HeroVictoryModal extends ModalView panel.textEl.text('+' + newGems) @gemEl.text(totalGems) gemTrigger = 'gem-' + (parseInt(panel.number * ratio) % 4) # 4 gem sounds - Backbone.Mediator.publish 'audio-player:play-sound', trigger: gemTrigger, volume: 0.5 + ratio / 2 + @playSound gemTrigger, (0.5 + ratio / 2) @gemEl.addClass 'four-digits' if totalGems >= 1000 and @lastTotalGems < 1000 @gemEl.addClass 'five-digits' if totalGems >= 10000 and @lastTotalGems < 10000 @lastTotalGems = totalGems else if panel.item thangType = @thangTypes[panel.item] panel.textEl.text utils.i18n(thangType.attributes, 'name') - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'item-unlocked', volume: 1 if 0.5 < ratio < 0.6 + @playSound 'item-unlocked' if 0.5 < ratio < 0.6 else if panel.hero thangType = @thangTypes[panel.hero] panel.textEl.text(thangType.get('name')) diff --git a/app/views/play/level/modal/VictoryModal.coffee b/app/views/play/level/modal/VictoryModal.coffee index 51a0e34ab..a77287b1b 100644 --- a/app/views/play/level/modal/VictoryModal.coffee +++ b/app/views/play/level/modal/VictoryModal.coffee @@ -82,7 +82,7 @@ module.exports = class VictoryModal extends ModalView afterInsert: -> super() - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'victory' + @playSound 'victory' gapi?.plusone?.go? @$el[0] FB?.XFBML?.parse? @$el[0] twttr?.widgets?.load?() diff --git a/app/views/play/level/tome/ProblemAlertView.coffee b/app/views/play/level/tome/ProblemAlertView.coffee index 942319568..46fb22093 100644 --- a/app/views/play/level/tome/ProblemAlertView.coffee +++ b/app/views/play/level/tome/ProblemAlertView.coffee @@ -59,7 +59,7 @@ module.exports = class ProblemAlertView extends CocoView if @problem? @$el.addClass('alert').addClass("alert-#{@problem.aetherProblem.level}").hide().fadeIn('slow') @$el.addClass('no-hint') unless @problem.aetherProblem.hint - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'error_appear', volume: 1.0 + @playSound 'error_appear' onShowProblemAlert: (data) -> return unless $('#code-area').is(":visible") @@ -80,7 +80,7 @@ module.exports = class ProblemAlertView extends CocoView return unless @problem? @$el.show() unless @$el.is(":visible") @$el.addClass 'jiggling' - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'error_appear', volume: 1.0 + @playSound 'error_appear' pauseJiggle = => @$el?.removeClass 'jiggling' _.delay pauseJiggle, 1000 diff --git a/app/views/play/level/tome/SpellListTabEntryView.coffee b/app/views/play/level/tome/SpellListTabEntryView.coffee index 65113cd1e..23a012565 100644 --- a/app/views/play/level/tome/SpellListTabEntryView.coffee +++ b/app/views/play/level/tome/SpellListTabEntryView.coffee @@ -83,7 +83,7 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView content: docFormatter.formatPopover() container: @$el.parent() ).on 'show.bs.popover', => - Backbone.Mediator.publish 'audio-player:play-sound', trigger: "spell-tab-entry-open", volume: 0.75 + @playSound 'spell-tab-entry-open', 0.75 onMouseEnterAvatar: (e) -> # Don't call super onMouseLeaveAvatar: (e) -> # Don't call super @@ -94,7 +94,7 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView onDropdownClick: (e) -> return unless @controlsEnabled Backbone.Mediator.publish 'tome:toggle-spell-list', {} - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'spell-list-open', volume: 1 + @playSound 'spell-list-open' onCodeReload: (e) -> #return unless @controlsEnabled diff --git a/app/views/play/level/tome/SpellPaletteEntryView.coffee b/app/views/play/level/tome/SpellPaletteEntryView.coffee index 7c532a54e..14cadf0cd 100644 --- a/app/views/play/level/tome/SpellPaletteEntryView.coffee +++ b/app/views/play/level/tome/SpellPaletteEntryView.coffee @@ -51,7 +51,7 @@ module.exports = class SpellPaletteEntryView extends CocoView ).on 'shown.bs.popover', => Backbone.Mediator.publish 'tome:palette-hovered', thang: @thang, prop: @doc.name, entry: @ soundIndex = Math.floor(Math.random() * 4) - Backbone.Mediator.publish 'audio-player:play-sound', trigger: "spell-palette-entry-open-#{soundIndex}", volume: 0.75 + @playSound 'spell-palette-entry-open-#{soundIndex}', 0.75 popover = @$el.data('bs.popover') popover?.$tip?.i18n() codeLanguage = @options.language @@ -95,7 +95,7 @@ module.exports = class SpellPaletteEntryView extends CocoView @$el.add('.spell-palette-popover.popover').removeClass 'pinned' $('.spell-palette-popover.popover .close').remove() @$el.popover 'hide' - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'spell-palette-entry-unpin', volume: 1 + @playSound 'spell-palette-entry-unpin' else @popoverPinned = true @$el.popover 'show' @@ -103,7 +103,7 @@ module.exports = class SpellPaletteEntryView extends CocoView x = $('') $('.spell-palette-popover.popover').append x x.on 'click', @onClick - Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'spell-palette-entry-pin', volume: 1 + @playSound 'spell-palette-entry-pin' Backbone.Mediator.publish 'tome:palette-pin-toggled', entry: @, pinned: @popoverPinned onClick: (e) => diff --git a/app/views/play/modal/PollModal.coffee b/app/views/play/modal/PollModal.coffee index 3682e2d0e..bec1914d0 100644 --- a/app/views/play/modal/PollModal.coffee +++ b/app/views/play/modal/PollModal.coffee @@ -103,7 +103,7 @@ module.exports = class PollModal extends ModalView if Math.random() < randomGems / 40 gemTrigger = 'gem-' + (gemNoisesPlayed % 4) # 4 gem sounds ++gemNoisesPlayed - Backbone.Mediator.publish 'audio-player:play-sound', trigger: gemTrigger, volume: 0.475 + i / 2000 + @playSound gemTrigger, (0.475 + i / 2000) @$randomNumber.delay 25 @$randomGems.delay(1100).queue -> utils.replaceText $(@), commentStart + randomGems From c5ac99f4c9adb14b0ad404aff7df998384676547 Mon Sep 17 00:00:00 2001 From: Tuomas Tanner Date: Fri, 18 Sep 2015 17:02:18 +0300 Subject: [PATCH 03/26] A few tip translations --- app/locale/fi.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/locale/fi.coffee b/app/locale/fi.coffee index 4ecdf63c3..e453acb8c 100644 --- a/app/locale/fi.coffee +++ b/app/locale/fi.coffee @@ -299,11 +299,11 @@ module.exports = nativeDescription: "suomi", englishDescription: "Finnish", tran tip_tell_friends: "Pidätkö CodeCombatista? Kerro meistä myös ystävillesi!" tip_beta_launch: "CodeCombat avattiin betatestaukseen lokakuussa 2013." tip_think_solution: "Mieti ratkaisua, älä ongelmaa." -# tip_theory_practice: "In theory, there is no difference between theory and practice. But in practice, there is. - Yogi Berra" -# tip_error_free: "There are two ways to write error-free programs; only the third one works. - Alan Perlis" -# tip_debugging_program: "If debugging is the process of removing bugs, then programming must be the process of putting them in. - Edsger W. Dijkstra" -# tip_forums: "Head over to the forums and tell us what you think!" -# tip_baby_coders: "In the future, even babies will be Archmages." + tip_theory_practice: "Teoriassa teoria ja käytäntö ovat sama asia. Käytännössä näin ei ole. - Yogi Berra" + tip_error_free: "Virheettömiä ohjelmia voi kirjoittaa kahdella eri tavalla. Ikävä kyllä kolmas tapa on ainoa toimiva. - Alan Perlis" + tip_debugging_program: "Jos debuggaus tarkoittaa virheiden poistamista ohjelmasta, niin ohjelmoinnin on tarkoitettava niiden lisäämistä. - Edsger W. Dijkstra" + tip_forums: "Tulepa keskustelupalstalle kertomaan mielipiteesi!" + tip_baby_coders: "Tulevaisuudessa jopa vauvoista tulee Arkkimaageja." # tip_morale_improves: "Loading will continue until morale improves." # tip_all_species: "We believe in equal opportunities to learn programming for all species." # tip_reticulating: "Reticulating spines." From ca629e383f8ab7b9321cc89671093a012582a683 Mon Sep 17 00:00:00 2001 From: mads232 Date: Tue, 22 Sep 2015 17:16:45 +0200 Subject: [PATCH 04/26] Update da.coffee --- app/locale/da.coffee | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/app/locale/da.coffee b/app/locale/da.coffee index 18f7111f4..5ee890895 100644 --- a/app/locale/da.coffee +++ b/app/locale/da.coffee @@ -1,6 +1,6 @@ module.exports = nativeDescription: "dansk", englishDescription: "Danish", translation: home: - slogan: "Lær at Kode ved at Spille et Spil" + slogan: "Lær at kode ved at spille et spil" no_ie: "CodeCombat kan desværre ikke køre i Internet Explorer 8 eller ældre. Beklager!" # Warning that only shows up in IE8 and older no_mobile: "CodeCombat er ikke designet til mobile enheder og vil måske ikke virke!" # Warning that shows up on mobile devices play: "Spil" # The big play button that opens up the campaign view. @@ -98,7 +98,7 @@ module.exports = nativeDescription: "dansk", englishDescription: "Danish", trans log_out: "Log Ud" forgot_password: "Glemt dit kodeord?" authenticate_gplus: "Forbind med G+" - load_profile: "Load G+ Profil" + load_profile: "Indlæs G+ Profil" finishing: "Færdiggører" sign_in_with_facebook: "Log ind med Facebook" sign_in_with_gplus: "Sign in with G+" @@ -310,8 +310,8 @@ module.exports = nativeDescription: "dansk", englishDescription: "Danish", trans # tip_reticulating: "Reticulating spines." tip_harry: "Du' en troldmand, " tip_great_responsibility: "Med store kodeevner kommer stort fejlfindingsansvnar." -# tip_munchkin: "If you don't eat your vegetables, a munchkin will come after you while you're asleep." -# tip_binary: "There are only 10 types of people in the world: those who understand binary, and those who don't." + tip_munchkin: "Hvis du ikke spiser dine grøntsager, kommer der en Munchkin efter dig mens du sover." + tip_binary: "Der findes kun 10 slags mennesker i verdenen: Dem som forstår binært, og dem som ikke gør." # tip_commitment_yoda: "A programmer must have the deepest commitment, the most serious mind. ~ Yoda" # tip_no_try: "Do. Or do not. There is no try. - Yoda" # tip_patience: "Patience you must have, young Padawan. - Yoda" @@ -334,31 +334,31 @@ module.exports = nativeDescription: "dansk", englishDescription: "Danish", trans # tip_source_code: "I want to change the world but they would not give me the source code." # tip_javascript_java: "Java is to JavaScript what Car is to Carpet. - Chris Heilmann" # tip_move_forward: "Whatever you do, keep moving forward. - Martin Luther King Jr." -# tip_google: "Have a problem you can't solve? Google it!" -# tip_adding_evil: "Adding a pinch of evil." + tip_google: "Har du et problem du ikke kan løse? Google det!" + tip_adding_evil: "Tilføjer et strejf af ondskab.." # 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_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" -# tip_paper_and_pen: "Before you start coding, you can always plan with a sheet of paper and a pen." + tip_paper_and_pen: "Før du starter med at programmere, kan du altid sætte dig ned med et stykke papir og blyant." game_menu: -# inventory_tab: "Inventory" -# save_load_tab: "Save/Load" -# options_tab: "Options" -# guide_tab: "Guide" + inventory_tab: "Dine ting" + save_load_tab: "Gem/Indlæs" + options_tab: "Indstillinger" + guide_tab: "Guide" # guide_video_tutorial: "Video Tutorial" -# guide_tips: "Tips" + guide_tips: "Råd" multiplayer_tab: "Flere spillere" -# auth_tab: "Sign Up" -# inventory_caption: "Equip your hero" -# choose_hero_caption: "Choose hero, language" -# save_load_caption: "... and view history" -# options_caption: "Configure settings" + auth_tab: "Tilmeld dig" + inventory_caption: "Udrust din helt" + choose_hero_caption: "Vælg helt, sprog" + save_load_caption: "... og se historie" + options_caption: "Ændre indstillinger" # guide_caption: "Docs and tips" -# multiplayer_caption: "Play with friends!" -# auth_caption: "Save your progress." + multiplayer_caption: "Spil med venner!" + auth_caption: "Gem dit spil." # leaderboard: # leaderboard: "Leaderboard" @@ -371,7 +371,7 @@ module.exports = nativeDescription: "dansk", englishDescription: "Danish", trans # time: "Time" # damage_taken: "Damage Taken" # damage_dealt: "Damage Dealt" -# difficulty: "Difficulty" + difficulty: "Sværhedsgrad" # gold_collected: "Gold Collected" # inventory: @@ -699,7 +699,7 @@ module.exports = nativeDescription: "dansk", englishDescription: "Danish", trans contact_us: "Kontakt CodeCombat" welcome: "Godt at høre fra dig! Brug denne formular til at sende os en email. " forum_prefix: "For noget offentligt, prøv venligst " - forum_page: "vores forum" + forum_page: "Vores forum" forum_suffix: " istedet." # faq_prefix: "There's also a" # faq: "FAQ" From 1a08730ae76f6ee6ef41b0617daba8a099b8b195 Mon Sep 17 00:00:00 2001 From: Imperadeiro98 Date: Wed, 23 Sep 2015 14:05:09 +0100 Subject: [PATCH 05/26] Uncommented an header --- app/locale/da.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/locale/da.coffee b/app/locale/da.coffee index 5ee890895..5f6042148 100644 --- a/app/locale/da.coffee +++ b/app/locale/da.coffee @@ -360,7 +360,7 @@ module.exports = nativeDescription: "dansk", englishDescription: "Danish", trans multiplayer_caption: "Spil med venner!" auth_caption: "Gem dit spil." -# leaderboard: + leaderboard: # leaderboard: "Leaderboard" # view_other_solutions: "View Leaderboards" # scores: "Scores" From 3253ae456ece02f1e7c19e47996fc51d1e80d81f Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Wed, 23 Sep 2015 08:24:11 -0700 Subject: [PATCH 06/26] Fix #3050: undefined simulation debugging tooltips --- app/views/ladder/MyMatchesTabView.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/ladder/MyMatchesTabView.coffee b/app/views/ladder/MyMatchesTabView.coffee index 43ed6d5c8..51bc71854 100644 --- a/app/views/ladder/MyMatchesTabView.coffee +++ b/app/views/ladder/MyMatchesTabView.coffee @@ -86,7 +86,7 @@ module.exports = class MyMatchesTabView extends CocoView stale: match.date < submitDate fresh: fresh codeLanguage: match.codeLanguage - simulator: JSON.stringify(match.simulator) + ' | seed ' + match.randomSeed + simulator: if match.simulator then JSON.stringify(match.simulator) + ' | seed ' + match.randomSeed else '' } for team in @teams From dcfc5726e98918db42e6a894ce0a3e2225f9e473 Mon Sep 17 00:00:00 2001 From: Cat Sync Date: Wed, 23 Sep 2015 13:56:25 -0400 Subject: [PATCH 07/26] Added Librarian name --- app/lib/world/names.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/lib/world/names.coffee b/app/lib/world/names.coffee index 08f2863e6..d17ab48f7 100644 --- a/app/lib/world/names.coffee +++ b/app/lib/world/names.coffee @@ -868,6 +868,7 @@ module.exports.thangNames = thangNames = 'Nordex' 'Satish' 'Vera' + 'Charlotte' ] 'Equestrian': [ # Male From 0e149e84e19ee736297071612e746b0cf2d53fc4 Mon Sep 17 00:00:00 2001 From: rohmunhoz Date: Wed, 23 Sep 2015 15:49:56 -0300 Subject: [PATCH 08/26] Translate and correct Translate some strings and correted the translation of some other ones --- app/locale/pt-BR.coffee | 130 ++++++++++++++++++++-------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/app/locale/pt-BR.coffee b/app/locale/pt-BR.coffee index 56b0c0720..8f038fb78 100644 --- a/app/locale/pt-BR.coffee +++ b/app/locale/pt-BR.coffee @@ -1,12 +1,12 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: "Portuguese (Brazil)", translation: home: slogan: "Aprenda a programar enquanto se diverte jogando." - no_ie: "CodeCombat não roda em versões mais antigas que o Internet Explorer 10. Desculpe!" # Warning that only shows up in IE8 and older - no_mobile: "CodeCombat não foi projetado para dispositivos móveis e pode não funcionar!" # Warning that shows up on mobile devices + no_ie: "O CodeCombat não roda em versões mais antigas que o Internet Explorer 10. Desculpe!" # Warning that only shows up in IE8 and older + no_mobile: "O CodeCombat não foi projetado para dispositivos móveis e pode não funcionar!" # Warning that shows up on mobile devices play: "Jogar" # The big play button that opens up the campaign view. old_browser: "Ops, seu navegador é muito antigo para rodar o CodeCombat. Desculpe!" # Warning that shows up on really old Firefox/Chrome/Safari old_browser_suffix: "Você pode tentar de qualquer forma, mas provavelmente não irá funcionar." - ipad_browser: "Más notícias:CodeCombat não é executado no navegador do iPad. Boas notícias: Nosso app nativo do iPad esta esperando a aprovação da Apple." + ipad_browser: "Más notícias: O CodeCombat não é executado no navegador do iPad. Boas notícias: Nosso app nativo do iPad está esperando a aprovação da Apple." campaign: "Campanha" for_beginners: "Para Iniciantes" multiplayer: "Multijogador" # Not currently shown on home page @@ -46,12 +46,12 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: pitch_body: "Desenvolvemos o CodeCombat em Inglês, mas já temos jogadores de todo o mundo. Muitos deles querem jogar em Português Brasileiro por não falar Inglês, por isso, se você conhece os dois idiomas, por favor, considere inscrever-se no programa para Diplomata e assim ajudar a traduzir tanto o site do CodeCombat quanto todos os estágios para o Português Brasileiro." missing_translations: "Até que possamos traduzir todo o conteúdo para o Português Brasileiro, você lerá o texto em Inglês quando a versão em Português Brasileiro não estiver disponível." learn_more: "Saiba mais sobre ser um Diplomata" - subscribe_as_diplomat: "Assinar como um Diplomata" + subscribe_as_diplomat: "Inscrever-se como um Diplomata" play: play_as: "Jogar Como " # Ladder page spectate: "Assistir" # Ladder page - players: "jogadores" # Hover over a level on /play + players: "Jogadores" # Hover over a level on /play hours_played: "Horas jogadas" # Hover over a level on /play items: "Itens" # Tooltip on item shop button from /play unlock: "Desbloquear" # For purchasing items and heroes @@ -422,7 +422,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: sorry_to_see_you_go: "É uma pena ver você indo embora! Por favor, diga o que podemos fazer para melhorar." unsubscribe_feedback_placeholder: "Oh, o que nós fizemos?" parent_button: "Pergunte aos seus pais" - parent_email_description: "Nós enviaremos um e-mail para eles adquirir para você uma assinatura do CodeCombat." + parent_email_description: "Nós enviaremos um e-mail para eles adquirirem uma assinatura do CodeCombat para você." parent_email_input_invalid: "Endereço de e-mail inválido." parent_email_input_label: "Endereço de e-mail dos pais" parent_email_input_placeholder: "Informe o e-mail dos pais" @@ -445,15 +445,15 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: sale_button: "Venda!" sale_button_title: "Economize 35% quando você adquirir a assinatura de 1 ano" sale_click_here: "Clique Aqui" -# sale_ends: "Ends" + sale_ends: "Termina" sale_extended: "*Assinaturas existentes serão extendidas por 1 ano." -# sale_feature_here: "Here's what you'll get:" + sale_feature_here: "O que está incluso:" sale_feature2: "Acesso a 9 poderosos novos heróis com atributos únicos!" -# sale_feature4: "42,000 bonus gems awarded immediately!" -# sale_continue: "Ready to continue adventuring?" -# sale_limited_time: "Limited time offer!" + sale_feature4: "42,000 gemas bônus entregues imediatamente!" + sale_continue: "Pronto para continuar a aventura?" + sale_limited_time: "Oferta por tempo limitado!" sale_new_heroes: "Novos heróis!" -# sale_title: "Back to School Sale" + sale_title: "Promoção de volta às aulas" # sale_view_button: "Buy 1 year subscription for" stripe_description: "Inscrição Mensal" stripe_description_year_sale: "Assinatura de 1 Ano (35% de desconto" @@ -610,7 +610,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: intro_1: "CodeCombat é um jogo online que ensina programação. Estudantes criam código em linguagens de programação usadas na vida real." intro_2: "Não é necessário ter experiência!" free_title: "Quanto custa?" - cost_china: "CodeCombat na China é gratuito para os primeiros cinco níveis, depois disso o valor mensal para ter acesso a mais de 140 níveis nos nossos servidores exclusivos na China é de $9.99 dólares americanos." + cost_china: "CodeCombat na China é gratuito para os primeiros cinco níveis, depois disso o valor mesal para ter acesso a mais de 140 níveis nos nossos servidores exclusivos na China é de $9.99 dólares americanos." free_1: "CodeCombat Basic é gratuito! Há mais de 80 níveis gratuitos que cobrem todos os conceitos." # {change} free_2: "Uma assinatura mensal dá acesso aos vídeos tutoriais e mais níveis para praticar." teacher_subs_title: "Professores recebem assinaturas gratuitas!" @@ -632,58 +632,58 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: monitor_progress_3: "Para adicionar estudantes, envie-os o link de convite para seu clã, que está na" monitor_progress_4: "página." monitor_progress_5: "Depois que eles se juntarem ao seu Clã, você verá um resumo do progresso dos estudantes na página do seu Clã." -# 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_1: "Clãs privados proporcionam mais pricacidade e informações de progresso detalhadas para cada estudante." + private_clans_2: "Para criar um Clã privado, marque a caixa 'Tornar Clã privado' ao criar um" private_clans_3: "." who_for_title: "Para quem é indicado o CodeCombat?" who_for_1: "Nós recomendamos CodeCombat para estudantes a partir de 9 anos de idade. Nenhuma experiência anterior em programação é necessária." -# who_for_2: "We've designed CodeCombat to appeal to both boys and girls." -# material_title: "How much material is there?" -# material_china: "Approximately 40 hours of gameplay spread over 180+ 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?" -# how_much_1: "A" + who_for_2: "Nós projetamos o CodeCombat para ser atraente à meninos e meninas." + material_title: "Quanto material existe?" + material_china: "Aproximadamente 40 horas de jogo espalhadas por mais de 180 níveis restritos até agora." + material_1: "Aproximadamente 25 horas de conteúdo livre e 15 horas adicionais de conteúdo restrito." + concepts_title: "Que conceitos são abordados?" + how_much_title: "Quanto custa uma assinatura mensal?" + how_much_1: "Uma" how_much_2: "assinatura mensal" -# how_much_3: "costs $9.99, and can be cancelled anytime." -# how_much_4: "Additionally, we provide discounts for larger groups:" + how_much_3: "custa $9.99, e pode ser cancelada a qualquer momento." + how_much_4: "Além disso, nós provemos descontos para grupos maiores:" # 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: "para mais detalhes." more_info_title: "Onde eu posso encontrar mais informações?" -# 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." + more_info_1: "Nosso" + more_info_2: "fórum de professores" + more_info_3: "é um bom lugar para se conectar com ótimos educadores que utilizam o CodeCombat." sys_requirements_title: "Requisitos de Sistema" sys_requirements_1: "Pelo motivo de CodeCombat ser um jogo, é mais intenso para ser processado em computadores do que tutoriais em vídeo ou escritos. Estamos otimizando para que rode rapidamente em todos navegadores modernos e também em computadores antigos, assim todos podem jogar. Sendo assim, aqui estão as nossas sugestões para tirar o máximo proveito da experiência de CodeCombat:" # {change} sys_requirements_2: "Use versões novas dos navegadores Chrome ou Firefox." # {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..." -# 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" -# approved_2: "approved." -# approved_3: "Further instructions have been sent to" -# denied_1: "Your application for a free trial subscription has been" -# 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" + title: "Pesquisa de professor" + must_be_logged: "Você precisa fazer login primeiro. Por favor, crie uma conta ou faça login no menu acima." + retrieving: "Recuperando informações..." + being_reviewed_1: "Sua solicitação de teste grátis de assinatura está sendo" + being_reviewed_2: "revisada." + approved_1: "Sua solicitação de teste grátis de assinatura foi" + approved_2: "aprovada." + approved_3: "Mais intruções foram enviadas para" + denied_1: "Sua solicitação de teste grátis de assinatura foi" + denied_2: "negada." + contact_1: "Por favor, entre em contato" + contact_2: "caso você tenha dúvidas no futuro." + description_1: "Nós oferecemos assinaturas grátis à professores para fins de avaliação. Você pode encontrar mais informações na nossa" description_2: "professores" description_3: "página." -# description_4: "Please fill out this quick survey and we’ll email you setup instructions." + description_4: "Por favor, preencha esta rápida pesquisa e nós o enviaremos as intruções de instalação por email." email: "Endereço de email" school: "Nome da Escola" location: "Nome da Cidade" -# age_students: "How old are your students?" -# under: "Under" + age_students: "Qual a idade dos seus alunos?" + under: "Embaixo" other: "Outro:" -# amount_students: "How many students do you teach?" -# hear_about: "How did you hear about CodeCombat?" -# fill_fields: "Please fill out all fields." -# thanks: "Thanks! We'll send you setup instructions shortly." + amount_students: "Quantos alunos você ensina?" + hear_about: "Como você ouviu falar do CodeCombat?" + fill_fields: "Por favor, preencha todos os campos." + thanks: "Obrigado! Enviaremos suas instruções de instalação em breve." versions: save_version_title: "Salvar nova versão" @@ -693,7 +693,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: cla_url: "CLA" cla_suffix: "." cla_agree: "EU CONCORDO" -# owner_approve: "An owner will need to approve it before your changes will become visible." + owner_approve: "Outro dono terá de aprovar isso antes de suas alterações se tornarem visíveis." contact: contact_us: "Contate-nos" @@ -948,8 +948,8 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: pop_i18n: "Popular I18N" tasks: "Tarefas" clear_storage: "Limpar suas alterações locais" -# add_system_title: "Add Systems to Level" -# done_adding: "Done Adding" + add_system_title: "Adicionar sistemas ao nível" + done_adding: "Concluir adição" article: edit_btn_preview: "Prever" @@ -1072,7 +1072,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: fight: "Lutem!" watch_victory: "Assista sua vitória" defeat_the: "Derrote" -# tournament_started: ", started" + tournament_started: ", iniciado" tournament_ends: "Fim do torneio" tournament_ended: "Torneio encerrado" tournament_rules: "Regras do Torneio" @@ -1084,11 +1084,11 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: rules: "Regras" winners: "Vencedores" league: "Liga" -# red_ai: "Red AI" # "Red AI Wins", at end of multiplayer match playback -# blue_ai: "Blue AI" -# wins: "Wins" # At end of multiplayer match playback -# humans: "Red" # Ladder page display team name -# ogres: "Blue" + red_ai: "AI Vermelho" # "Red AI Wins", at end of multiplayer match playback + blue_ai: "AI Azul" + wins: "Vence" # At end of multiplayer match playback + humans: "Vermelho" # Ladder page display team name + ogres: "Azul" user: stats: "Estatísticas" @@ -1223,21 +1223,21 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: # user_polls_record: "Poll Voting History" concepts: -# advanced_strings: "Advanced Strings" + advanced_strings: "Strings avançadas" algorithms: "Algorítmos" -# arguments: "Arguments" -# arithmetic: "Arithmetic" + arguments: "Argumentos" + arithmetic: "Aritmética" arrays: "Arrays" basic_syntax: "Sintaxe Básica" boolean_logic: "Lógica Booleana" -# break_statements: "Break Statements" + break_statements: "Comandos Break" classes: "Classes" -# continue_statements: "Continue Statements" + continue_statements: "Comandos continue" for_loops: "Laço For" functions: "Funções" -# graphics: "Graphics" -# if_statements: "If Statements" -# input_handling: "Input Handling" + graphics: "Gráficos" + if_statements: "Comandos se" + input_handling: "Tratamento de entrada" math_operations: "Operações Matemáticas" object_literals: "Objetos Literais" parameters: "Parâmetros" @@ -1261,7 +1261,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: temp: "Temp" # temp: -# ace_of_coders_tournament: "New: play in the Ace of Coders tournament now!" + ace_of_coders_tournament: "Novo: jogue no torneir Era dos Programadores agora!" multiplayer: multiplayer_title: "Configurações de Multijogador" # We'll be changing this around significantly soon. Until then, it's not important to translate. From 4cd872091e60830fd928a5e66d9ded88d0746262 Mon Sep 17 00:00:00 2001 From: Imperadeiro98 Date: Wed, 23 Sep 2015 20:00:38 +0100 Subject: [PATCH 09/26] Uncommented an header --- app/locale/pt-BR.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/locale/pt-BR.coffee b/app/locale/pt-BR.coffee index 8f038fb78..2dfc9ab01 100644 --- a/app/locale/pt-BR.coffee +++ b/app/locale/pt-BR.coffee @@ -1260,7 +1260,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: guide: temp: "Temp" -# temp: + temp: ace_of_coders_tournament: "Novo: jogue no torneir Era dos Programadores agora!" multiplayer: From cbaac9855671c61c3ba27c5844f0b856e8a2081f Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Wed, 23 Sep 2015 16:27:45 -0700 Subject: [PATCH 10/26] Course details page no instances selected UI --- app/templates/courses/course-details.jade | 23 +++++++++- app/views/courses/CourseDetailsView.coffee | 52 +++++++++++++++------- 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade index b6c42b524..87e3f958a 100644 --- a/app/templates/courses/course-details.jade +++ b/app/templates/courses/course-details.jade @@ -9,8 +9,27 @@ block content a.spl(href='mailto:team@codecombat.com') team@codecombat.com div(style='border-bottom: 1px solid black;') - if me.isAnonymous() - h1 TODO: logged out + if (noCourseInstance || noCourseInstanceSelected) && course + h1= course.get('name') + if noCourseInstance + p You are not enrolled in this course. + p + span.spr Please visit the + a.spr(href="/courses") courses + span page to enroll. + else if noCourseInstanceSelected + p Select one of your classes + .container-fluid + .row + .col-md-6 + select.form-control.select-instance + each courseInstance in courseInstances + if courseInstance.get('name') + option(value="#{courseInstance.id}")= courseInstance.get('name') + else + option(value="#{courseInstance.id}") *unnamed* + .col-md-6 + button.btn.btn-success.btn-select-instance Select else if !course || !courseInstance h1 Loading... else diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee index 1a1d80212..6d423e2d4 100644 --- a/app/views/courses/CourseDetailsView.coffee +++ b/app/views/courses/CourseDetailsView.coffee @@ -8,10 +8,6 @@ template = require 'templates/courses/course-details' User = require 'models/User' utils = require 'core/utils' -# TODO: logged out experience -# TODO: no course instances -# TODO: no course instance selected - module.exports = class CourseDetailsView extends RootView id: 'course-details-view' template: template @@ -20,6 +16,7 @@ module.exports = class CourseDetailsView extends RootView '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' 'mouseenter .progress-level-cell': 'onMouseEnterPoint' @@ -30,10 +27,12 @@ module.exports = class CourseDetailsView extends RootView @courseInstanceID = utils.getQueryVariable('ciid', false) or options.courseInstanceID @adminMode = me.isAdmin() @memberSort = 'nameAsc' - unless me.isAnonymous() - @course = new Course _id: @courseID - @listenTo @course, 'sync', @onCourseSync - @supermodel.loadModel @course, 'course', cache: false + @course = @supermodel.getModel(Course, @courseID) or new Course _id: @courseID + @listenTo @course, 'sync', @onCourseSync + if @course.loaded + @onCourseSync() + else + @supermodel.loadModel @course, 'course' getRenderData: -> context = super() @@ -42,9 +41,12 @@ module.exports = class CourseDetailsView extends RootView context.conceptsCompleted = @conceptsCompleted ? {} context.course = @course if @course?.loaded context.courseInstance = @courseInstance if @courseInstance?.loaded + context.courseInstances = @courseInstances?.models ? [] context.levelConceptMap = @levelConceptMap ? {} context.memberSort = @memberSort context.memberUserMap = @memberUserMap ? {} + context.noCourseInstance = @noCourseInstance + context.noCourseInstanceSelected = @noCourseInstanceSelected context.showExpandedProgress = @showExpandedProgress context.sortedMembers = @sortedMembers ? [] context.userConceptStateMap = @userConceptStateMap ? {} @@ -53,10 +55,18 @@ module.exports = class CourseDetailsView extends RootView onCourseSync: -> # console.log 'onCourseSync' + if me.isAnonymous() + @noCourseInstance = true + @render?() + return return if @campaign? - @campaign = new Campaign _id: @course.get('campaignID') + campaignID = @course.get('campaignID') + @campaign = @supermodel.getModel(Campaign, campaignID) or new Campaign _id: campaignID @listenTo @campaign, 'sync', @onCampaignSync - @supermodel.loadModel @campaign, 'campaign', cache: false + if @campaign.loaded + @onCampaignSync() + else + @supermodel.loadModel @campaign, 'campaign' @render?() onCampaignSync: -> @@ -77,19 +87,26 @@ module.exports = class CourseDetailsView extends RootView loadCourseInstance: (courseInstanceID) -> # console.log 'loadCourseInstance' return if @courseInstance? - @courseInstance = new CourseInstance _id: courseInstanceID + @courseInstance = @supermodel.getModel(CourseInstance, courseInstanceID) or new CourseInstance _id: courseInstanceID @listenTo @courseInstance, 'sync', @onCourseInstanceSync - @supermodel.loadModel @courseInstance, 'course_instance', cache: false + if @courseInstance.loaded + @onCourseInstanceSync() + else + @courseInstance = @supermodel.loadModel(@courseInstance, 'course_instance').model onCourseInstancesSync: -> # console.log 'onCourseInstancesSync' if @courseInstances.models.length is 1 @loadCourseInstance(@courseInstances.models[0].id) - else if @courseInstances.models.length > 0 - @loadCourseInstance(@courseInstances.models[0].id) + else + if @courseInstances.models.length is 0 + @noCourseInstance = true + else + @noCourseInstanceSelected = true + @render?() onCourseInstanceSync: -> - console.log 'onCourseInstanceSync', @courseInstance.get('description') + # console.log 'onCourseInstanceSync' @adminMode = true if @courseInstance.get('ownerID') is me.id @levelSessions = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/level_sessions", model: LevelSession, comparator:'_id' }) @listenToOnce @levelSessions, 'sync', @onLevelSessionsSync @@ -161,6 +178,11 @@ module.exports = class CourseDetailsView extends RootView @courseInstance.patch() $('#settingsModal').modal('hide') + onClickSelectInstance: (e) -> + courseInstanceID = $('.select-instance').val() + @noCourseInstanceSelected = false + @loadCourseInstance(courseInstanceID) + onMouseEnterPoint: (e) -> $('.level-popup-container').hide() container = $(e.target).find('.level-popup-container').show() From 238ea490903dddb13aa62a958fee27f9fe03b2d1 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 24 Sep 2015 07:28:43 -0700 Subject: [PATCH 11/26] Add stats to course details page --- app/templates/courses/course-details.jade | 43 +++++++++++++--------- app/views/courses/CourseDetailsView.coffee | 27 ++++++++++++-- 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade index 87e3f958a..be01acffc 100644 --- a/app/templates/courses/course-details.jade +++ b/app/templates/courses/course-details.jade @@ -80,21 +80,28 @@ mixin progress-summary-stats td if courseInstance div #{courseInstance.get('members').length} - tr - td Average level play time: - td TODO - tr - td Total play time: - td TODO - tr - td Average levels completed: - td TODO - tr - td Total levels completed: - td TODO - tr - td Furthest level completed: - td TODO + if instanceStats + tr + td Average level play time: + if instanceStats.averageLevelPlaytime > 0 + td= moment.duration(instanceStats.averageLevelPlaytime, "seconds").humanize() + else + td 0 + tr + td Total play time: + if instanceStats.totalPlayTime > 0 + td= moment.duration(instanceStats.totalPlayTime, "seconds").humanize() + else + td 0 + tr + td Average levels completed: + td #{instanceStats.averageLevelsCompleted.toFixed(2)} + tr + td Total levels completed: + td= instanceStats.totalLevelsCompleted + tr + td Furthest level completed: + td= instanceStats.furthestLevelCompleted.replace('Course: ', '') mixin progress-summary-concepts h3 Concepts Covered @@ -160,9 +167,9 @@ mixin progress-members mixin progress-members-individual(memberID) - var name = memberUserMap[memberID] ? memberUserMap[memberID].get('name') : 'Anoner' a(href="/user/#{memberID}")= name || 'Anoner' - div TODO: levels completed - div TODO: total time played - div TODO: last played + if memberStats && memberStats[memberID] + div #{memberStats[memberID].totalLevelsCompleted} levels + div Played #{moment.duration(memberStats[memberID].totalPlayTime, "seconds").humanize()} mixin progress-members-concepts(memberID) if course && userLevelStateMap[memberID] diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee index 6d423e2d4..93b3a246c 100644 --- a/app/views/courses/CourseDetailsView.coffee +++ b/app/views/courses/CourseDetailsView.coffee @@ -42,8 +42,10 @@ module.exports = class CourseDetailsView extends RootView context.course = @course if @course?.loaded context.courseInstance = @courseInstance if @courseInstance?.loaded context.courseInstances = @courseInstances?.models ? [] + context.instanceStats = @instanceStats context.levelConceptMap = @levelConceptMap ? {} context.memberSort = @memberSort + context.memberStats = @memberStats context.memberUserMap = @memberUserMap ? {} context.noCourseInstance = @noCourseInstance context.noCourseInstanceSelected = @noCourseInstanceSelected @@ -118,17 +120,36 @@ module.exports = class CourseDetailsView extends RootView onLevelSessionsSync: -> # console.log 'onLevelSessionsSync' + @instanceStats = averageLevelsCompleted: 0, furthestLevelCompleted: '', totalLevelsCompleted: 0, totalPlayTime: 0 + @memberStats = {} @userConceptStateMap = {} @userLevelStateMap = {} + levelStateMap = {} for levelSession in @levelSessions.models userID = levelSession.get('creator') levelID = levelSession.get('level').original - @userConceptStateMap[userID] ?= {} - @userLevelStateMap[userID] ?= {} state = if levelSession.get('state')?.complete then 'complete' else 'started' - @userLevelStateMap[userID][levelID] = state + levelStateMap[levelID] = state + + @instanceStats.totalLevelsCompleted++ if state is 'complete' + @instanceStats.totalPlayTime += levelSession.get('playtime') + + @memberStats[userID] ?= totalLevelsCompleted: 0, totalPlayTime: 0 + @memberStats[userID].totalLevelsCompleted++ if state is 'complete' + @memberStats[userID].totalPlayTime += levelSession.get('playtime') + + @userConceptStateMap[userID] ?= {} for concept of @levelConceptMap[levelID] @userConceptStateMap[userID][concept] = state + + @userLevelStateMap[userID] ?= {} + @userLevelStateMap[userID][levelID] = state + + if @courseInstance.get('members').length > 0 + @instanceStats.averageLevelsCompleted = @instanceStats.totalLevelsCompleted / @courseInstance.get('members').length + for levelID, level of @campaign.get('levels') + @instanceStats.furthestLevelCompleted = level.name if levelStateMap[levelID] is 'complete' + @conceptsCompleted = {} for userID, conceptStateMap of @userConceptStateMap for concept, state of conceptStateMap From b4d59ced3e0a95cbf50a68032c5e21f5c22d5b3f Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 24 Sep 2015 14:48:54 -0700 Subject: [PATCH 12/26] Update course details progress level cell popups --- app/templates/courses/course-details.jade | 21 +++++++++-------- app/views/courses/CourseDetailsView.coffee | 26 +++++++++++++++++++--- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade index be01acffc..4d9ae7dbc 100644 --- a/app/templates/courses/course-details.jade +++ b/app/templates/courses/course-details.jade @@ -186,11 +186,11 @@ mixin progress-members-levels-expanded(memberID) - var i = 0 each level, levelID in campaign.get('levels') if userLevelStateMap[memberID][levelID] === 'complete' - span.progress-level-cell.progress-level-cell-complete #{i + 1} + 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) else if userLevelStateMap[memberID][levelID] === 'started' - span.progress-level-cell.progress-level-cell-started #{i + 1} #{level.name.replace('Course: ', '')} + 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) else span.progress-level-cell #{i + 1} #{level.name.replace('Course: ', '')} @@ -205,10 +205,10 @@ mixin progress-members-levels-condensed(memberID) - var i = 0 each level, levelID in campaign.get('levels') if userLevelStateMap[memberID][levelID] === 'complete' - span.progress-level-cell.progress-level-cell-complete(style="width:#{levelCellWidth}%;") #{i + 1} + 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) else if userLevelStateMap[memberID][levelID] === 'started' - span.progress-level-cell.progress-level-cell-started(style="width:#{levelCellWidth}%;") #{i + 1} + 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) else break @@ -217,15 +217,18 @@ mixin progress-members-levels-condensed(memberID) mixin progress-members-popup-completed(i, level) .progress-popup-container h3 #{i + 1}. #{level.name.replace('Course: ', '')} - p TODO: Time to solve - p TODO: Completed on - strong Click to view solution. + p Play time: #{moment.duration(level.playtime, "seconds").humanize()} + p Completed: #{moment(level.changed).format('MMMM Do YYYY, h:mm:ss a')} + if adminMode + strong Click to view solution. mixin progress-members-popup-started(i, level) .progress-popup-container h3 #{i + 1}. #{level.name.replace('Course: ', '')} - p TODO: last played on - strong Click to view solution. + p Play time: #{moment.duration(level.playtime, "seconds").humanize()} + p Last played: #{moment(level.changed).format('MMMM Do YYYY, h:mm:ss a')} + if adminMode + strong Click to view solution. mixin invite-tab p Invite students to join this class. diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee index 93b3a246c..92e80b395 100644 --- a/app/views/courses/CourseDetailsView.coffee +++ b/app/views/courses/CourseDetailsView.coffee @@ -19,6 +19,7 @@ module.exports = class CourseDetailsView extends RootView 'click .btn-select-instance': 'onClickSelectInstance' 'click .progress-member-header': 'onClickMemberHeader' 'click .progress-header': 'onClickProgressHeader' + 'click .progress-level-cell': 'onClickProgressLevelCell' 'mouseenter .progress-level-cell': 'onMouseEnterPoint' 'mouseleave .progress-level-cell': 'onMouseLeavePoint' @@ -123,6 +124,7 @@ module.exports = class CourseDetailsView extends RootView @instanceStats = averageLevelsCompleted: 0, furthestLevelCompleted: '', totalLevelsCompleted: 0, totalPlayTime: 0 @memberStats = {} @userConceptStateMap = {} + @userLevelSessionMap = {} @userLevelStateMap = {} levelStateMap = {} for levelSession in @levelSessions.models @@ -142,6 +144,9 @@ module.exports = class CourseDetailsView extends RootView for concept of @levelConceptMap[levelID] @userConceptStateMap[userID][concept] = state + @userLevelSessionMap[userID] ?= {} + @userLevelSessionMap[userID][levelID] = levelSession + @userLevelStateMap[userID] ?= {} @userLevelStateMap[userID][levelID] = state @@ -204,9 +209,24 @@ module.exports = class CourseDetailsView extends RootView @noCourseInstanceSelected = false @loadCourseInstance(courseInstanceID) + onClickProgressLevelCell: (e) -> + return unless @adminMode + levelID = $(e.currentTarget).data('level-id') + levelSlug = $(e.currentTarget).data('level-slug') + userID = $(e.currentTarget).data('user-id') + return unless levelID and levelSlug and userID + route = "/play/level/#{levelSlug}" + if @userLevelSessionMap[userID]?[levelID] + route += "?session=#{@userLevelSessionMap[userID][levelID].id}&observing=true" + Backbone.Mediator.publish 'router:navigate', { + route: route + viewClass: 'views/play/level/PlayLevelView' + viewArgs: [{}, levelSlug] + } + onMouseEnterPoint: (e) -> - $('.level-popup-container').hide() - container = $(e.target).find('.level-popup-container').show() + $('.progress-popup-container').hide() + container = $(e.target).find('.progress-popup-container').show() margin = 20 offset = $(e.target).offset() scrollTop = $('#page-container').scrollTop() @@ -215,7 +235,7 @@ module.exports = class CourseDetailsView extends RootView container.css('top', offset.top + scrollTop - height - margin) onMouseLeavePoint: (e) -> - $(e.target).find('.level-popup-container').hide() + $(e.target).find('.progress-popup-container').hide() sortMembers: -> # Progress sort precedence: most completed concepts, most started concepts, most levels, name sort From d7d6694ee9fb5f04f7db7eb95d88ca478a364adb Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 24 Sep 2015 17:12:18 -0700 Subject: [PATCH 13/26] Update course level routing --- app/views/courses/CourseDetailsView.coffee | 9 ++--- app/views/play/level/ControlBarView.coffee | 19 ++++++---- app/views/play/level/PlayLevelView.coffee | 7 ++-- .../play/level/modal/HeroVictoryModal.coffee | 36 ++++++++++++++----- 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee index 92e80b395..f4eac6223 100644 --- a/app/views/courses/CourseDetailsView.coffee +++ b/app/views/courses/CourseDetailsView.coffee @@ -23,9 +23,9 @@ module.exports = class CourseDetailsView extends RootView 'mouseenter .progress-level-cell': 'onMouseEnterPoint' 'mouseleave .progress-level-cell': 'onMouseLeavePoint' - constructor: (options, @courseID) -> + constructor: (options, @courseID, @courseInstanceID) -> super options - @courseInstanceID = utils.getQueryVariable('ciid', false) or options.courseInstanceID + @courseInstanceID ?= utils.getQueryVariable('ciid', false) or options.courseInstanceID @adminMode = me.isAdmin() @memberSort = 'nameAsc' @course = @supermodel.getModel(Course, @courseID) or new Course _id: @courseID @@ -90,7 +90,8 @@ module.exports = class CourseDetailsView extends RootView loadCourseInstance: (courseInstanceID) -> # console.log 'loadCourseInstance' return if @courseInstance? - @courseInstance = @supermodel.getModel(CourseInstance, courseInstanceID) or new CourseInstance _id: courseInstanceID + @courseInstanceID = courseInstanceID + @courseInstance = @supermodel.getModel(CourseInstance, @courseInstanceID) or new CourseInstance _id: @courseInstanceID @listenTo @courseInstance, 'sync', @onCourseInstanceSync if @courseInstance.loaded @onCourseInstanceSync() @@ -191,7 +192,7 @@ module.exports = class CourseDetailsView extends RootView Backbone.Mediator.publish 'router:navigate', { route: "/play/level/#{levelSlug}" viewClass: 'views/play/level/PlayLevelView' - viewArgs: [{}, levelSlug] + viewArgs: [{courseID: @courseID, courseInstanceID: @courseInstanceID}, levelSlug] } onClickSaveSettings: (e) -> diff --git a/app/views/play/level/ControlBarView.coffee b/app/views/play/level/ControlBarView.coffee index d0a100bad..a8784fcd6 100644 --- a/app/views/play/level/ControlBarView.coffee +++ b/app/views/play/level/ControlBarView.coffee @@ -28,6 +28,9 @@ module.exports = class ControlBarView extends CocoView 'click #control-bar-sign-up-button': 'onClickSignupButton' constructor: (options) -> + @courseID = options.courseID + @courseInstanceID = options.courseInstanceID + @worldName = options.worldName @session = options.session @level = options.level @@ -88,13 +91,15 @@ module.exports = class ControlBarView extends CocoView @homeLink += '/' + campaign @homeViewArgs.push campaign else if @level.get('type', true) in ['course'] - @homeLink = '/courses/mock1' - @homeViewClass = 'views/courses/mock1/CourseDetailsView' - #campaign = @level.get 'campaign' - #@homeLink += '/' + campaign - #@homeViewArgs.push campaign - @homeLink += '/' + '0' - @homeViewArgs.push '0' + @homeLink = '/courses' + @homeViewClass = 'views/courses/CoursesView' + if @courseID + @homeLink += "/#{@courseID}" + @homeViewArgs.push @courseID + @homeViewClass = 'views/courses/CourseDetailsView' + if @courseInstanceID + @homeLink += "?ciid=#{@courseInstanceID}" + @homeViewArgs.push @courseInstanceID else @homeLink = '/' @homeViewClass = 'views/HomeView' diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index 27a86a92c..627245cb3 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -94,6 +94,9 @@ module.exports = class PlayLevelView extends RootView console.profile?() if PROFILE_ME super options + @courseID = options.courseID + @courseInstanceID = options.courseInstanceID + @isEditorPreview = @getQueryVariable 'dev' @sessionID = @getQueryVariable 'session' @observing = @getQueryVariable 'observing' @@ -248,7 +251,7 @@ module.exports = class PlayLevelView extends RootView @insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session @insertSubView new ProblemAlertView session: @session, level: @level, supermodel: @supermodel @insertSubView new DuelStatsView level: @level, session: @session, otherSession: @otherSession, supermodel: @supermodel, thangs: @world.thangs if @level.get('type') in ['hero-ladder', 'course-ladder'] - @insertSubView @controlBar = new ControlBarView {worldName: utils.i18n(@level.attributes, 'name'), session: @session, level: @level, supermodel: @supermodel} + @insertSubView @controlBar = new ControlBarView {worldName: utils.i18n(@level.attributes, 'name'), session: @session, level: @level, supermodel: @supermodel, courseID: @courseID, courseInstanceID: @courseInstanceID} #_.delay (=> Backbone.Mediator.publish('level:set-debug', debug: true)), 5000 if @isIPadApp() # if me.displayName() is 'Nick' initVolume: -> @@ -444,7 +447,7 @@ module.exports = class PlayLevelView extends RootView showVictory: -> return if @level.hasLocalChanges() # Don't award achievements when beating level changed in level editor @endHighlight() - options = {level: @level, supermodel: @supermodel, session: @session, hasReceivedMemoryWarning: @hasReceivedMemoryWarning} + options = {level: @level, supermodel: @supermodel, session: @session, hasReceivedMemoryWarning: @hasReceivedMemoryWarning, courseID: @courseID, courseInstanceID: @courseInstanceID} ModalClass = if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] then HeroVictoryModal else VictoryModal victoryModal = new ModalClass(options) @openModalView(victoryModal) diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee index e97613a17..74fb9cb00 100644 --- a/app/views/play/level/modal/HeroVictoryModal.coffee +++ b/app/views/play/level/modal/HeroVictoryModal.coffee @@ -42,6 +42,9 @@ module.exports = class HeroVictoryModal extends ModalView constructor: (options) -> super(options) + @courseID = options.courseID + @courseInstanceID = options.courseInstanceID + @session = options.session @level = options.level @thangTypes = {} @@ -399,13 +402,17 @@ module.exports = class HeroVictoryModal extends ModalView if @level.get('type', true) is 'course' and nextLevel = @level.get('nextLevel') and not returnToCourse # need to do something more complicated to load its slug console.log 'have @nextLevel', @nextLevel, 'from nextLevel', nextLevel - return "/play/level/#{@nextLevel.get('slug')}" + link = "/play/level/#{@nextLevel.get('slug')}" else if @level.get('type', true) is 'course' - # TODO: figure out which course it is - return '/courses/mock1/0' - link = '/play' - nextCampaign = @getNextLevelCampaign() - link += '/' + nextCampaign + link = "/courses" + if @courseID + link += "/#{@courseID}" + if @courseInstanceID + link += "?ciid=#{@courseInstanceID}" + else + link = '/play' + nextCampaign = @getNextLevelCampaign() + link += '/' + nextCampaign link onClickContinue: (e, extraOptions=null) -> @@ -418,11 +425,22 @@ module.exports = class HeroVictoryModal extends ModalView _.merge options, extraOptions if extraOptions if @level.get('type', true) is 'course' and @nextLevel and not options.returnToCourse viewClass = require 'views/play/level/PlayLevelView' + if @courseID + options.courseID = @courseID + if @courseInstanceID + options.courseInstanceID = @courseInstanceID viewArgs = [options, @nextLevel.get('slug')] else if @level.get('type', true) is 'course' - options.studentMode = true - viewClass = require 'views/courses/mock1/CourseDetailsView' - viewArgs = [options, '0'] + # TODO: shouldn't set viewClass and route in different places + viewClass = require 'views/courses/CoursesView' + viewArgs = [options] + if @courseID + viewClass = require 'views/courses/CourseDetailsView' + options.courseID = @courseID + viewArgs.push @courseID + if @courseInstanceID + options.courseInstanceID = @courseInstanceID + viewArgs.push @courseInstanceID else viewClass = require 'views/play/CampaignView' viewArgs = [options, @getNextLevelCampaign()] From feef9c0ac6300aab2a8c994d06a26a5bbef86a93 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 24 Sep 2015 17:52:00 -0700 Subject: [PATCH 14/26] :bug:Fix instance selection on /courses page --- app/core/Router.coffee | 2 +- app/templates/courses/courses.jade | 11 ++++++----- app/views/courses/CourseDetailsView.coffee | 3 ++- app/views/courses/CoursesView.coffee | 7 ++++--- app/views/play/level/ControlBarView.coffee | 2 +- app/views/play/level/modal/HeroVictoryModal.coffee | 4 +--- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/core/Router.coffee b/app/core/Router.coffee index 28ca9854a..462809e9a 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -65,7 +65,7 @@ module.exports = class CocoRouter extends Backbone.Router 'courses/mock1/:courseID': go('courses/mock1/CourseDetailsView') 'courses': go('courses/CoursesView') 'courses/enroll(/:courseID)': go('courses/CourseEnrollView') - 'courses/:courseID': go('courses/CourseDetailsView') + 'courses/:courseID(/:courseInstanceID)': go('courses/CourseDetailsView') 'db/*path': 'routeToServer' 'demo(/*subpath)': go('DemoView') diff --git a/app/templates/courses/courses.jade b/app/templates/courses/courses.jade index e9f62e2f6..29536fc58 100644 --- a/app/templates/courses/courses.jade +++ b/app/templates/courses/courses.jade @@ -92,12 +92,13 @@ mixin teacher-dialog(course) .container-fluid .row .col-md-8 - select.form-control.select-session + select.form-control.select-session(data-course-id="#{course.id}") each inst in instances - if inst.get('name') - option(value="#{inst.id}")= inst.get('name') - else - option(value="#{inst.id}") *unnamed* + if inst.get('courseID') == course.id + if inst.get('name') + option(value="#{inst.id}")= inst.get('name') + else + option(value="#{inst.id}") *unnamed* .col-md-4 button.btn.btn-success.btn-enter(data-course-id="#{course.id}") Enter .row.button-row.center.row-pick-class diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee index f4eac6223..ceb19dff9 100644 --- a/app/views/courses/CourseDetailsView.coffee +++ b/app/views/courses/CourseDetailsView.coffee @@ -25,7 +25,8 @@ module.exports = class CourseDetailsView extends RootView constructor: (options, @courseID, @courseInstanceID) -> super options - @courseInstanceID ?= utils.getQueryVariable('ciid', false) or options.courseInstanceID + @courseID ?= options.courseID + @courseInstanceID ?= options.courseInstanceID @adminMode = me.isAdmin() @memberSort = 'nameAsc' @course = @supermodel.getModel(Course, @courseID) or new Course _id: @courseID diff --git a/app/views/courses/CoursesView.coffee b/app/views/courses/CoursesView.coffee index fd8cc45a8..84a0942c3 100644 --- a/app/views/courses/CoursesView.coffee +++ b/app/views/courses/CoursesView.coffee @@ -74,10 +74,11 @@ module.exports = class CoursesView extends RootView onClickEnter: (e) -> $('.continue-dialog').modal('hide') courseID = $(e.target).data('course-id') - courseInstanceID = $('.select-session').val() + courseInstanceID = $(".select-session[data-course-id=#{courseID}]").val() + route = "/courses/#{courseID}/#{courseInstanceID}" viewClass = require 'views/courses/CourseDetailsView' - viewArgs = [{courseInstanceID:courseInstanceID}, courseID] - navigationEvent = route: "/courses/#{courseID}", viewClass: viewClass, viewArgs: viewArgs + viewArgs = [{}, courseID, courseInstanceID] + navigationEvent = route: route, viewClass: viewClass, viewArgs: viewArgs Backbone.Mediator.publish 'router:navigate', navigationEvent onClickStudent: (e) -> diff --git a/app/views/play/level/ControlBarView.coffee b/app/views/play/level/ControlBarView.coffee index a8784fcd6..418c1dcc8 100644 --- a/app/views/play/level/ControlBarView.coffee +++ b/app/views/play/level/ControlBarView.coffee @@ -98,7 +98,7 @@ module.exports = class ControlBarView extends CocoView @homeViewArgs.push @courseID @homeViewClass = 'views/courses/CourseDetailsView' if @courseInstanceID - @homeLink += "?ciid=#{@courseInstanceID}" + @homeLink += "/#{@courseInstanceID}" @homeViewArgs.push @courseInstanceID else @homeLink = '/' diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee index 74fb9cb00..bf41620ab 100644 --- a/app/views/play/level/modal/HeroVictoryModal.coffee +++ b/app/views/play/level/modal/HeroVictoryModal.coffee @@ -408,7 +408,7 @@ module.exports = class HeroVictoryModal extends ModalView if @courseID link += "/#{@courseID}" if @courseInstanceID - link += "?ciid=#{@courseInstanceID}" + link += "/#{@courseInstanceID}" else link = '/play' nextCampaign = @getNextLevelCampaign() @@ -436,10 +436,8 @@ module.exports = class HeroVictoryModal extends ModalView viewArgs = [options] if @courseID viewClass = require 'views/courses/CourseDetailsView' - options.courseID = @courseID viewArgs.push @courseID if @courseInstanceID - options.courseInstanceID = @courseInstanceID viewArgs.push @courseInstanceID else viewClass = require 'views/play/CampaignView' From 71538b0ac9bef2f99f86333bfb98261b4f4b768d Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 24 Sep 2015 17:56:12 -0700 Subject: [PATCH 15/26] Remove 'do not localize' comment from courses UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s ok to localize / i18n these now. --- app/templates/courses/course-details.jade | 2 -- app/templates/courses/course-enroll.jade | 2 -- app/templates/courses/courses.jade | 2 -- 3 files changed, 6 deletions(-) diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade index 4d9ae7dbc..2bdec233e 100644 --- a/app/templates/courses/course-details.jade +++ b/app/templates/courses/course-details.jade @@ -2,8 +2,6 @@ extends /templates/base block content - //- DO NOT localize / i18n - div span *UNDER CONSTRUCTION, send feedback to a.spl(href='mailto:team@codecombat.com') team@codecombat.com diff --git a/app/templates/courses/course-enroll.jade b/app/templates/courses/course-enroll.jade index 763520085..24fb8a1c9 100644 --- a/app/templates/courses/course-enroll.jade +++ b/app/templates/courses/course-enroll.jade @@ -2,8 +2,6 @@ extends /templates/base block content - //- DO NOT localize / i18n - div span *UNDER CONSTRUCTION, send feedback to a.spl(href='mailto:team@codecombat.com') team@codecombat.com diff --git a/app/templates/courses/courses.jade b/app/templates/courses/courses.jade index 29536fc58..ea8f4f8ea 100644 --- a/app/templates/courses/courses.jade +++ b/app/templates/courses/courses.jade @@ -2,8 +2,6 @@ extends /templates/base block content - //- DO NOT localize / i18n - div(style='border-bottom: 1px solid black') span *UNDER CONSTRUCTION, please send feedback to a.spl(href='mailto:team@codecombat.com') team@codecombat.com From 4cd8d95c72e20bead79a04adb27a35f127c57e55 Mon Sep 17 00:00:00 2001 From: Imperadeiro98 Date: Fri, 25 Sep 2015 13:12:06 +0100 Subject: [PATCH 16/26] Removed an unnecessary console log --- app/views/core/SubscribeModal.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/core/SubscribeModal.coffee b/app/views/core/SubscribeModal.coffee index 4f1c72f3f..903c9846a 100644 --- a/app/views/core/SubscribeModal.coffee +++ b/app/views/core/SubscribeModal.coffee @@ -48,7 +48,6 @@ module.exports = class SubscribeModal extends ModalView popoverTitle = $.i18n.t 'subscribe.parent_email_title' popoverTitle += '' popoverContent = -> - console.log 'found html', $('.parent-button-popover-content').html() $('.parent-button-popover-content').html() @$el.find('.parent-button').popover( animation: true From 4f054fa31ad030a57d17cd6371f46232292dcbba Mon Sep 17 00:00:00 2001 From: Imperadeiro98 Date: Fri, 25 Sep 2015 14:00:03 +0100 Subject: [PATCH 17/26] Fixed issue with string interpolation --- app/views/play/level/tome/SpellPaletteEntryView.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/play/level/tome/SpellPaletteEntryView.coffee b/app/views/play/level/tome/SpellPaletteEntryView.coffee index 14cadf0cd..27c505502 100644 --- a/app/views/play/level/tome/SpellPaletteEntryView.coffee +++ b/app/views/play/level/tome/SpellPaletteEntryView.coffee @@ -51,7 +51,7 @@ module.exports = class SpellPaletteEntryView extends CocoView ).on 'shown.bs.popover', => Backbone.Mediator.publish 'tome:palette-hovered', thang: @thang, prop: @doc.name, entry: @ soundIndex = Math.floor(Math.random() * 4) - @playSound 'spell-palette-entry-open-#{soundIndex}', 0.75 + @playSound "spell-palette-entry-open-#{soundIndex}", 0.75 popover = @$el.data('bs.popover') popover?.$tip?.i18n() codeLanguage = @options.language From 257f0f6b1e55585791e0174bb5293bf826eb083c Mon Sep 17 00:00:00 2001 From: eschuler21 Date: Fri, 25 Sep 2015 09:55:54 -0400 Subject: [PATCH 18/26] Update config.coffee --- config.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.coffee b/config.coffee index 03d7b4a1e..b855d17c2 100644 --- a/config.coffee +++ b/config.coffee @@ -3,7 +3,7 @@ _ = require 'lodash' _.str = require 'underscore.string' sysPath = require 'path' fs = require('fs') -commonjsHeader = fs.readFileSync('node_modules/brunch/node_modules/commonjs-require-definition/require.js', {encoding: 'utf8'}) +commonjsHeader = commonjsHeader = require('commonjs-require-definition') TRAVIS = process.env.COCO_TRAVIS_TEST From 9c7fe0c512c4c531c96d2d1e6b1b9922d8af9b12 Mon Sep 17 00:00:00 2001 From: eschuler21 Date: Fri, 25 Sep 2015 09:58:35 -0400 Subject: [PATCH 19/26] Update package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index b779bdb79..80ef9c0c1 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "coffee-script-brunch": "https://github.com/brunch/coffee-script-brunch/tarball/master", "coffeelint-brunch": "> 1.0 < 1.8", "compressible": "~1.0.1", + "commonjs-require-definition": "0.1.0", "css-brunch": "> 1.0 < 1.8", "jade": "0.33.x", "jade-brunch": "> 1.0 < 1.8", From ab97c1d5d48e46f988bb3204ca7bb8a506b32269 Mon Sep 17 00:00:00 2001 From: jpss256 Date: Fri, 25 Sep 2015 19:19:34 +0300 Subject: [PATCH 20/26] Bulgarian translation --- app/locale/bg.coffee | 140 +++++++++++++++++++++---------------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/app/locale/bg.coffee b/app/locale/bg.coffee index 5f9c6fbf5..2d8f3f9c3 100644 --- a/app/locale/bg.coffee +++ b/app/locale/bg.coffee @@ -31,7 +31,7 @@ module.exports = nativeDescription: "български език", englishDescri contact: "Контакти" twitter_follow: "Започни да следиш" teachers: "Учители" -# careers: "Careers" + careers: "Кариери" modal: close: "Затвори" @@ -75,7 +75,7 @@ module.exports = nativeDescription: "български език", englishDescri level_difficulty: "Трудност" campaign_beginner: "Кампания за начинаещи" awaiting_levels_adventurer_prefix: "5 нови нива всяка седмица" # {change} - awaiting_levels_adventurer: "Стани Приключенец" + awaiting_levels_adventurer: "Стани Търсач на приключения" awaiting_levels_adventurer_suffix: "за да бъдеш първият, който играе нови нива." adjust_volume: "Настрой звук" campaign_multiplayer: "Арени за мултиплейър" @@ -97,7 +97,7 @@ module.exports = nativeDescription: "български език", englishDescri logging_in: "Влизане..." log_out: "Изход" forgot_password: "Забравена парола?" - authenticate_gplus: "Автентикация чрез G+" + authenticate_gplus: "Аутентикация чрез G+" load_profile: "Зареди G+ профил" finishing: "Завършване" sign_in_with_facebook: "Вписване чрез Facebook" @@ -105,7 +105,7 @@ module.exports = nativeDescription: "български език", englishDescri signup_switch: "Създаване на нов акаунт?" signup: - email_announcements: "Получава анонси по имейл" + email_announcements: "Получавай анонси по имейл" creating: "Създаване на профил..." sign_up: "Регистриране" log_in: "Вход с парола" @@ -204,9 +204,9 @@ module.exports = nativeDescription: "български език", englishDescri minute: "минута" minutes: "минути" hour: "час" - hours: "часове" + hours: "часа" day: "ден" - days: "дни" + days: "дена" week: "седмица" weeks: "седмици" month: "месец" @@ -256,8 +256,8 @@ module.exports = nativeDescription: "български език", englishDescri victory_new_item: "Нов Предмет" victory_viking_code_school: "О да - това ниво беше наистина тежко! Ти или си програмист, или обезателно трябва да станеш такъв! Току що се доближи до приемането си във Викингското Училище по Програмиране, където ще научиш много нови неща и ще станеш професионален уеб програмист за 14 седмици." victory_become_a_viking: "Стани Викинг" -# victory_bloc: "Great work! Your skills are improving, and someone's taking notice. If you've considered becoming a software developer, this may be your lucky day. Bloc is an online bootcamp that pairs you 1-on-1 with an expert mentor who will help train you into a professional developer! By beating A Mayhem of Munchkins, you're now eligible for a $500 price reduction with the code: CCRULES" -# victory_bloc_cta: "Meet your mentor – learn about Bloc" + victory_bloc: "Чудесна работа! Твоите умения се подобряват и някой го е забелязал! Ако смяташ да ставаш програмист, може би това е щастливият ти ден. Bloc е online bootcamp, където ще можеш да тренираш насаме с наставник и да станеш професионален програмист! Тъй като победи в A Mayhem of Munchkins, получаваш купон за намаление от $500. Кодът е: CCRULES" + victory_bloc_cta: "Срещни се с наставника си - научи повече за Bloc" guide_title: "Упътване" tome_minion_spells: "Заклинания на вашите Миньони' Spells" # Only in old-style levels. tome_read_only_spells: "Read-Only Заклинания" # Only in old-style levels. @@ -401,17 +401,17 @@ module.exports = nativeDescription: "български език", englishDescri price: "x3500 / месец" subscribe: -# comparison_blurb: "Sharpen your skills with a CodeCombat subscription!" -# feature1: "110+ basic levels across 4 worlds" -# feature2: "10 powerful new heroes with unique skills!" -# feature3: "70+ bonus levels" -# feature4: "3500 bonus gems every month!" + comparison_blurb: "Изостри уменията си в CodeCombat с абонамент!" + feature1: "110+ основни нива в 4 свята" + feature2: "10 силни нови герои с уникални умения!" + feature3: "70+ бонус нива" + feature4: "3500 скъпоценни камъни бонус всеки месец!" feature5: "Видео уроци" -# feature6: "Premium email support" -# feature7: "Private Clans" -# free: "Free" -# month: "month" -# must_be_logged: "You must be logged in first. Please create an account or log in from the menu above." + feature6: "Премиум email поддръжка" + feature7: "Частни Кланове" + free: "Безплатно" + month: "месец" + must_be_logged: "Първо трябва да сте влезли. Моля, създайте си акаунт или влезте от менюто отгоре." subscribe_title: "Абонирай се" unsubscribe: "Прекрати абонамента" confirm_unsubscribe: "Подтвърди прекратяване на абонамента" @@ -431,61 +431,61 @@ module.exports = nativeDescription: "български език", englishDescri parent_email_title: "Въведете email на родител?" parents: "За Родители" parents_title: "Скъпи Родители: Вашето дете се учи да програмира. Ще му помогнете ли да продължи?" - parents_blurb1: "Вашето дете изигра __nLevels__ нива и научи основи на програмирането. Развийте интереса у тях като им купите абонамент - така те ще могат да продължат с игрите." - parents_blurb1a: "Програмирането е важно умение което без съмнение вашето дете ще използва когато порасне. До 2020-та, основни познания по програмиране ще са необходими за повече от 77% от работните места, а софтуерните инженери ще са много търсени по света. Знаете ли че диплома в сферата на информационните технологии е предпоставка за едни от най-високите заплати в индустрията?" - parents_blurb2: "За $9.99 USD/месец, вашето дете ще получава нови задачи всяка седмица, както и персонална помощ по електронната помощ от професионални програмисти." - parents_blurb3: "Без Риск: 100% гаранция за възстановяване на средствата, прекратяване на абонамаента с едно натискане на бутон." + parents_blurb1: "Вашето дете изигра __nLevels__ нива и получи основни познания в програмирането. Развийте интереса в него като му купите абонамент - така то ще може да продължи с игрите." + parents_blurb1a: "Програмирането е важно умение което без съмнение вашето дете ще използва когато порасне. До 2020-та, основни познания по програмиране ще са необходими за повече от 77% от работните места, а софтуерните инженери ще са много търсени по света. Знаете ли, че диплома в сферата на информационните технологии е предпоставка за едни от най-високите заплати в индустрията?" + parents_blurb2: "За $9.99 USD/месец, вашето дете ще получава нови задачи всяка седмица, както и персонална помощ по електронната поща от професионални програмисти." + parents_blurb3: "Без Риск: 100% гаранция за възстановяване на средствата, прекратяване на абонамента с едно натискане на бутон." payment_methods: "Начини на плащане" payment_methods_title: "Възможни начини на плащане" payment_methods_blurb1: "В момента приемаме кредитни карти и Alipay." payment_methods_blurb2: "Ако желаете алтернативна форма на плащане, свържете се с нас" -# sale_already_subscribed: "You're already subscribed!" -# sale_blurb1: "Save 35%" -# sale_blurb2: "off regular subscription price of $120 for a whole year!" # {changed} -# sale_button: "Sale!" -# sale_button_title: "Save 35% when you purchase a 1 year subscription" -# sale_click_here: "Click Here" -# 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 new heroes with unique skills!" -# sale_feature4: "42,000 bonus gems 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" -# stripe_description: "Monthly Subscription" -# 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." -# personal_sub: "Personal Subscription" # 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" # {changed} -# 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." -# group_discounts_1st: "1st subscription" -# group_discounts_full: "Full price" -# group_discounts_2nd: "Subscriptions 2-11" -# group_discounts_20: "20% off" -# group_discounts_12th: "Subscriptions 12+" -# group_discounts_40: "40% off" -# subscribing: "Subscribing..." -# recipient_emails_placeholder: "Enter email address to subscribe, one per line." -# subscribe_users: "Subscribe Users" -# users_subscribed: "Users subscribed:" -# 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" + sale_already_subscribed: "Вече имате абонамент!" + sale_blurb1: "Спестете 35%" + sale_blurb2: "от редовната абонаментна такса от $120 за цялата година!" # {changed} + sale_button: "Разпродажба!" + sale_button_title: "Спестете 35% като направите абонамент за 1 година" + sale_click_here: "Кликнете Тук" + sale_ends: "Завършва" + sale_extended: "*Съществуващият абонамент ще бъде продължен с 1 година." + sale_feature_here: "Ето какво получавате:" + sale_feature2: "Достъп до 9 силни нови герои с уникални умения!" + sale_feature4: "42,000 скъпоценни камъни бонус присъдени веднага!" + sale_continue: "Готови ли сте да продължите приключението?" + sale_limited_time: "Офертата е достъпна за ограничен период от време!" + sale_new_heroes: "Нови герои!" + sale_title: "Назад към Училищната Разпродажба" + sale_view_button: "Купи едногодишен абонамент за" + stripe_description: "Месечен Абонамент" + stripe_description_year_sale: "Едногодишен абонамент (35% намаление)" + subscription_required_to_play: "Необходим ви е абонамент за да играете това ниво." + unlock_help_videos: "Абонирайте се за да отключите всичките видео уроци." + personal_sub: "Персонален абонамент" # Accounts Subscription View below + loading_info: "Зареждане на информацията за абонамента..." + managed_by: "Управлявана от" + will_be_cancelled: "Ще бъде отменена" + currently_free: "В момента имате безплатен абонамент" + currently_free_until: "В момента имате абонамент до" # {changed} + was_free_until: "Имали сте безплатен абонамент до" + managed_subs: "Управлявани Абонаменти" + managed_subs_desc: "Добавете абонаменти на други играчи (студенти, деца и т.н.)" + managed_subs_desc_2: "Получателите трябва да имат CodeCombat акаунт, асоцииран с email адреса, който предоставяте." + group_discounts: "Групови намаления" + group_discounts_1: "Също предлагаме специални намаления за групово абониране." + group_discounts_1st: "Първи абонамент" + group_discounts_full: "Пълната цена" + group_discounts_2nd: "Абонаменти 2-11" + group_discounts_20: "20% намаление" + group_discounts_12th: "Абонаменти 12+" + group_discounts_40: "40% намаление" + subscribing: "Абониране..." + recipient_emails_placeholder: "Въведете email адресите на абонатите, по един на ред." + subscribe_users: "Абониране на потребители" + users_subscribed: "Абонирани потребители:" + no_users_subscribed: "Няма абонирани потребители, моля проверете email адресите отново." + current_recipients: "Текущи получатели" + unsubscribing: "Прекратяване на абонамента..." + subscribe_prepaid: "Кликнете 'Абонамент', за да използвате предплатен код" + using_prepaid: "Използване на предплатен код за месечен абонамент" choose_hero: choose_hero: "Избери си герой" @@ -494,7 +494,7 @@ module.exports = nativeDescription: "български език", englishDescri default: "По подразбиране" experimental: "Експериментално" python_blurb: "Прост,но мощен, идеален за начинаещи и експерти." - javascript_blurb: "Езикът на мрежата. (Не е същия като Java.)" + javascript_blurb: "Езикът на Мрежата. (Не е същия като Java.)" coffeescript_blurb: "По-добър синтаксис от JavaScript." clojure_blurb: "Модерен Lisp." lua_blurb: "Скриптен език за игри." From 928f72e2cf7e5e8eda9b9c88daeefaef5e1a44b0 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Fri, 25 Sep 2015 10:03:44 -0700 Subject: [PATCH 21/26] Merge branch 'prepaid-v2' into master --- app/core/Router.coffee | 1 + app/core/utils.coffee | 5 + app/locale/en.coffee | 9 + app/models/Prepaid.coffee | 14 + app/styles/account/account-prepaid-view.sass | 3 + app/templates/account/main-account-view.jade | 6 +- .../account/prepaid-redeem-modal.jade | 19 ++ app/templates/account/prepaid-view.jade | 99 +++++++ app/templates/account/subscription-view.jade | 9 + app/templates/admin.jade | 10 + app/templates/base.jade | 12 +- app/views/account/PrepaidRedeemModal.coffee | 27 ++ app/views/account/PrepaidView.coffee | 179 ++++++++++++ app/views/admin/MainAdminView.coffee | 25 ++ server/lib/utils.coffee | 4 + server/payments/subscription_handler.coffee | 120 +++++++- server/prepaids/Prepaid.coffee | 2 + server/prepaids/prepaid_handler.coffee | 94 +++++- server/routes/stripe.coffee | 20 +- server/users/user_handler.coffee | 7 + test/server/common.coffee | 24 +- test/server/functional/prepaid.spec.coffee | 270 +++++++++++++++++- .../functional/subscription.spec.coffee | 63 ++-- 23 files changed, 961 insertions(+), 61 deletions(-) create mode 100644 app/models/Prepaid.coffee create mode 100644 app/styles/account/account-prepaid-view.sass create mode 100644 app/templates/account/prepaid-redeem-modal.jade create mode 100644 app/templates/account/prepaid-view.jade create mode 100644 app/views/account/PrepaidRedeemModal.coffee create mode 100644 app/views/account/PrepaidView.coffee diff --git a/app/core/Router.coffee b/app/core/Router.coffee index 462809e9a..0bedf02de 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -26,6 +26,7 @@ module.exports = class CocoRouter extends Backbone.Router 'account/subscription': go('account/SubscriptionView') 'account/subscription/sale': go('account/SubscriptionSaleView') 'account/invoices': go('account/InvoicesView') + 'account/prepaid': go('account/PrepaidView') 'admin': go('admin/MainAdminView') 'admin/candidates': go('admin/CandidatesView') diff --git a/app/core/utils.coffee b/app/core/utils.coffee index e69808462..0c293279e 100644 --- a/app/core/utils.coffee +++ b/app/core/utils.coffee @@ -243,3 +243,8 @@ module.exports.getCoursePraise = getCoursePraise = -> } ] praise[_.random(0, praise.length - 1)] + +module.exports.getPrepaidCodeAmount = getPrepaidCodeAmount = (price=999, users=0, months=0) -> + return 0 unless users > 0 and months > 0 + total = price * users * months + total diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 517599ddf..54f82e278 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -1129,6 +1129,7 @@ recently_played: "Recently Played" no_recent_games: "No games played during the past two weeks." payments: "Payments" + prepaid: "Prepaid" purchased: "Purchased" sale: "Sale" subscription: "Subscription" @@ -1159,6 +1160,14 @@ retrying: "Server error, retrying." success: "Successfully paid. Thanks!" + account_prepaid: + purchase_code: "Purchase a Subscription Code" + purchase_amount: "Amount" + purchase_total: "Total" + purchase_button: "Submit Purchase" + your_codes: "Your Codes:" + redeem_codes: "Redeem a Subscription Code" + loading_error: could_not_load: "Error loading from server" connection_failure: "Connection failed." diff --git a/app/models/Prepaid.coffee b/app/models/Prepaid.coffee new file mode 100644 index 000000000..eea054326 --- /dev/null +++ b/app/models/Prepaid.coffee @@ -0,0 +1,14 @@ +CocoModel = require './CocoModel' +schema = require 'schemas/models/prepaid.schema' + +module.exports = class Prepaid extends CocoModel + @className: "Prepaid" + urlRoot: '/db/prepaid' + + openSpots: -> + @get('maxRedeemers') - @get('redeemers')?.length + + userHasRedeemed: (userID) -> + for redeemer in @get('redeemers') + return redeemer.date if redeemer.userID is userID + return null diff --git a/app/styles/account/account-prepaid-view.sass b/app/styles/account/account-prepaid-view.sass new file mode 100644 index 000000000..f942180d9 --- /dev/null +++ b/app/styles/account/account-prepaid-view.sass @@ -0,0 +1,3 @@ + +#users, #months + max-width: 100px \ No newline at end of file diff --git a/app/templates/account/main-account-view.jade b/app/templates/account/main-account-view.jade index b41c5016a..3f998f9ac 100644 --- a/app/templates/account/main-account-view.jade +++ b/app/templates/account/main-account-view.jade @@ -4,14 +4,14 @@ block content if me.get('anonymous') p(data-i18n="account_settings.not_logged_in") Log in or create an account to change your settings. - + else ol.breadcrumb li a(href="/") span.glyphicon.glyphicon-home li.active(data-i18n="nav.account") - + #account-links.panel.panel-default .panel-heading(data-i18n="nav.account") ul.list-group @@ -23,4 +23,6 @@ block content a.btn.btn-lg.btn-primary(href="/account/payments", data-i18n="account.payments") li.list-group-item a.btn.btn-lg.btn-primary(href="/account/subscription", data-i18n="account.subscription") + li.list-group-item + a.btn.btn-lg.btn-primary(href="/account/prepaid") Prepaid Codes diff --git a/app/templates/account/prepaid-redeem-modal.jade b/app/templates/account/prepaid-redeem-modal.jade new file mode 100644 index 000000000..43cedabcb --- /dev/null +++ b/app/templates/account/prepaid-redeem-modal.jade @@ -0,0 +1,19 @@ +extends /templates/core/modal-base + +block modal-header-content + h3 Prepaid Code Details (#{ppc.get('code')}) + +block modal-body-content + if redeemedOn + p You redeemed this code: #{redeemedOn} + else + if ppc.openSpots() + p: strong Adds #{ppc.get('properties').months} month(s) to your current subscription. + p You can redeem this code. + else + p You cannot redeem this code. + +block modal-footer-content + button#close.btn.btn-primary(type="button", data-dismiss="modal") Cancel + if !redeemedOn && ppc.openSpots() > 0 + button#redeem.btn.btn-primary(type="button", data-dismiss="modal") Redeem Code To My Account diff --git a/app/templates/account/prepaid-view.jade b/app/templates/account/prepaid-view.jade new file mode 100644 index 000000000..7db33fabf --- /dev/null +++ b/app/templates/account/prepaid-view.jade @@ -0,0 +1,99 @@ +extends /templates/base + +block content + + if me.get('anonymous') + p(data-i18n="account_settings.not_logged_in") Log in or create an account to change your settings. + + else + ol.breadcrumb + li + a(href="/") + span.glyphicon.glyphicon-home + li + a(href="/account", data-i18n="nav.account") + li.active(data-i18n="account.prepaid") + + .row + .col-md-12 + .panel.panel-default + .panel-heading + .panel-title + a(data-toggle="collapse" href="#purchasepanel") + span(data-i18n="account_prepaid.purchase_code") + .panel-collapse.collapse(class=ppc ? "": "in")#purchasepanel + .panel-body + p Subscription Codes can be redeemed to add premium subscription time to one or more Code Combat accounts. + p Each Code Combat account can only redeem a particular Subscription Code once. + p Subscription Code months will be added to the end of any existing subscription on the account. + .form-horizontal + .form-group + label.control-label.col-md-2(for="users") Users + .col-md-2 + input#users.form-control(name="users", type="number", value="#{purchase.users}", min=1) + .form-group + label.control-label.col-md-2(for="months") Months + .col-md-2 + input#months.form-control(name="months", type="number", value="#{purchase.months}", min=1) + .form-group + label.control-label.col-md-2(data-i18n="account_prepaid.purchase_total") + .col-md-10 + p.form-control-static $ + span#total #{purchase.total} + button#purchase-button.btn.btn-success.pull-right(data-i18n="account_prepaid.purchase_button") + .row + .col-md-12 + .panel.panel-default + .panel-heading + .panel-title + a(data-toggle="collapse" href="#redeempanel") + span(data-i18n="account_prepaid.redeem_codes") + .panel-collapse.collapse.in#redeempanel + .panel-body + .form-horizontal + .form-group + label.control-label.col-md-2(for="ppc") Code: + .col-md-10 + input#ppc.form-control(name="ppc", type="text", value="#{ppc}" required) + button#redeem-button.btn.btn-success.pull-right View Code Details + .row + .col-md-12 + .panel.panel-default + .panel-heading + .panel-title + a(data-toggle="collapse" href="#codeslist") + span(data-i18n="account_prepaid.your_codes") + .panel-collapse.collapse.in#codeslist + .panel-body + if codes && codes.length + table.table.table-striped + tr + th + span(title="You can copy the code's link and send it to someone.") Code + span.glyphicon.glyphicon-question-sign(aria-hidden="true") + th Months + th Quantity + th 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' + 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 Purchased + else + td Redeemed + else + p No codes yet! \ No newline at end of file diff --git a/app/templates/account/subscription-view.jade b/app/templates/account/subscription-view.jade index 79d71708f..13480978a 100644 --- a/app/templates/account/subscription-view.jade +++ b/app/templates/account/subscription-view.jade @@ -119,6 +119,15 @@ block content else button.start-subscription-button.btn.btn-lg.btn-success(data-i18n="subscribe.subscribe_title") Subscribe + // - Prepaid Codes + .panel.panel-default + .panel-heading + h3 Prepaid Codes + .panel-body + p + span You can + a(href="/account/prepaid") purchase a prepaid code + span that can be applied to your own account or given to others. //- Sponsored Subscriptions .panel.panel-default diff --git a/app/templates/admin.jade b/app/templates/admin.jade index f5817aa9a..4ea19bc19 100644 --- a/app/templates/admin.jade +++ b/app/templates/admin.jade @@ -63,6 +63,16 @@ block content if freeSubLink input#free-sub-input(type="text", readonly, value="#{freeSubLink}") + .form-inline + .form-group + label(for="users") Users + input#users.form-control(name="users", type="number", min=1) + .form-group + label(for="months") Months + input#months.form-control(name="months", type="number", min=1) + a#terminal-create.btn.btn-default Create Terminal Subscription Code + + hr h3 Achievements diff --git a/app/templates/base.jade b/app/templates/base.jade index a9079b1ec..2c9286913 100644 --- a/app/templates/base.jade +++ b/app/templates/base.jade @@ -37,6 +37,8 @@ block header a(href="/account/payments", data-i18n="account.payments") li a(href="/account/subscription", data-i18n="account.subscription") + li + a(href="/account/prepaid") Prepaid Codes li a#logout-button(data-i18n="login.log_out") @@ -44,7 +46,7 @@ block header button.btn.btn-sm.btn-primary.header-font.signup-button(data-i18n="login.sign_up") button.btn.btn-sm.btn-default.header-font.login-button(data-i18n="login.log_in") select.language-dropdown.form-control - + block outer_content #site-content-area @@ -75,15 +77,15 @@ block footer if !isIE a.twitter-follow-button(href="https://twitter.com/CodeCombat", data-show-count="true", data-show-screen-name="false", data-dnt="true", data-align="right", data-i18n="nav.twitter_follow") Follow iframe.github-star-button(src="https://ghbtns.com/github-btn.html?user=codecombat&repo=codecombat&type=watch&count=true", allowtransparency="true", frameborder="0", scrolling="0", width="110", height="20") - + #footer-credits - span + span span © All Rights Reserved br span CodeCombat 2015 img#footer-logo(src="/images/pages/base/logo.png", alt="CodeCombat") - span - span Site Design by + span + span Site Design by br a(href="http://www.fullyillustrated.com/") Fully Illustrated //a.firebase-bade(href="https://www.firebase.com/") // Not using right now diff --git a/app/views/account/PrepaidRedeemModal.coffee b/app/views/account/PrepaidRedeemModal.coffee new file mode 100644 index 000000000..e8fb1959f --- /dev/null +++ b/app/views/account/PrepaidRedeemModal.coffee @@ -0,0 +1,27 @@ +ModalView = require 'views/core/ModalView' +template = require 'templates/account/prepaid-redeem-modal' +{me} = require 'core/auth' + + +module.exports = class PrepaidRedeemModal extends ModalView + id: 'prepaid-redeem-modal' + template: template + closeButton: true + + events: + 'click #redeem' : 'onRedeemClicked' + + constructor: (options) -> + super options + @ppc = options.ppc + hasRedeemed = @ppc.userHasRedeemed(me.get('_id')) + @redeemedOn = new moment(hasRedeemed).calendar() if hasRedeemed + + getRenderData: -> + c = super() + c.ppc = @ppc + c.redeemedOn = @redeemedOn if @redeemedOn + c + + onRedeemClicked: -> + @trigger 'confirm-redeem' diff --git a/app/views/account/PrepaidView.coffee b/app/views/account/PrepaidView.coffee new file mode 100644 index 000000000..893e15a74 --- /dev/null +++ b/app/views/account/PrepaidView.coffee @@ -0,0 +1,179 @@ +RootView = require 'views/core/RootView' +template = require 'templates/account/prepaid-view' +stripeHandler = require 'core/services/stripe' +{getPrepaidCodeAmount} = require '../../core/utils' +CocoCollection = require 'collections/CocoCollection' +Prepaid = require '../../models/Prepaid' +utils = require 'core/utils' +RedeemModal = require 'views/account/PrepaidRedeemModal' +forms = require 'core/forms' + + +module.exports = class PrepaidView extends RootView + id: 'prepaid-view' + template: template + className: 'container-fluid' + + events: + 'change #users': 'onUsersChanged' + 'change #months': 'onMonthsChanged' + 'click #purchase-button': 'onPurchaseClicked' + 'click #redeem-button': 'onRedeemClicked' + + subscriptions: + 'stripe:received-token': 'onStripeReceivedToken' + + baseAmount: 9.99 + + constructor: (options) -> + super(options) + @purchase = + total: @baseAmount + users: 3 + months: 3 + @updateTotal() + + @codes = new CocoCollection([], { url: '/db/user/'+me.id+'/prepaid_codes', model: Prepaid }) + @codes.on 'add', (code) => + @render?() + @codes.on 'sync', (code) => + @render?() + + @supermodel.loadCollection(@codes, 'prepaid', {cache: false}) + @ppc = utils.getQueryVariable('_ppc') ? '' + + getRenderData: -> + c = super() + c.purchase = @purchase + c.codes = @codes + c.ppc = @ppc + c + + afterRender: -> + super() + @$el.find("span[title]").tooltip() + + statusMessage: (message, type='alert') -> + noty text: message, layout: 'topCenter', type: type, killer: false, timeout: 5000, dismissQueue: true, maxVisible: 3 + + updateTotal: -> + @purchase.total = getPrepaidCodeAmount(@baseAmount, @purchase.users, @purchase.months) + @renderSelectors("#total", "#users", "#months") + + + # Form Input Callbacks + onUsersChanged: (e) -> + newAmount = $(e.target).val() + newAmount = 1 if newAmount < 1 + @purchase.users = newAmount + el = $('#purchasepanel') + if newAmount < 3 and @purchase.months < 3 + message = "Either Users or Months must be greater than 2" + err = [message: message, property: 'users', formatted: true] + forms.clearFormAlerts(el) + forms.applyErrorsToForm(el, err) + else + forms.clearFormAlerts(el) + + @updateTotal() + + onMonthsChanged: (e) -> + newAmount = $(e.target).val() + newAmount = 1 if newAmount < 1 + @purchase.months = newAmount + el = $('#purchasepanel') + if newAmount < 3 and @purchase.users < 3 + message = "Either Users or Months must be greater than 2" + err = [message: message, property: 'months', formatted: true] + forms.clearFormAlerts(el) + forms.applyErrorsToForm(el, err) + else + forms.clearFormAlerts(el) + + @updateTotal() + + onPurchaseClicked: (e) -> + return unless $("#users").val() >= 3 or $("#months").val() >= 3 + @purchaseTimestamp = new Date().getTime() + @stripeAmount = @purchase.total * 100 + @description = "Prepaid Code for " + @purchase.users + " users / " + @purchase.months + " months" + + stripeHandler.open + amount: @stripeAmount + description: @description + bitcoin: true + alipay: if me.get('chinaVersion') or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto' + + onRedeemClicked: (e) -> + @ppc = $('#ppc').val() + + unless @ppc + @statusMessage "You must enter a code.", "error" + return + options = + url: '/db/prepaid/-/code/'+ @ppc + method: 'GET' + + options.success = (model, res, options) => + redeemModal = new RedeemModal ppc: model + redeemModal.on 'confirm-redeem', @confirmRedeem + @openModalView redeemModal + + options.error = (model, res, options) => + console.warn 'Error getting Prepaid Code' + + prepaid = new Prepaid() + prepaid.fetch(options) + # @supermodel.addRequestResource('get_prepaid', options, 0).load() + + + confirmRedeem: => + + options = + url: '/db/subscription/-/subscribe_prepaid' + method: 'POST' + data: { ppc: @ppc } + + options.error = (model, res, options, foo) => + console.error 'FAILED redeeming prepaid code' + msg = model.responseText ? '' + @statusMessage "Error: Could not redeem prepaid code. #{msg}", "error" + + options.success = (model, res, options) => + console.log 'SUCCESS redeeming prepaid code' + @statusMessage "Prepaid Code Redeemed!", "success" + @supermodel.loadCollection(@codes, 'prepaid', {cache: false}) + @codes.fetch() + + @supermodel.addRequestResource('subscribe_prepaid', options, 0).load() + + + onStripeReceivedToken: (e) -> + # TODO: show that something is happening in the UI + options = + url: '/db/prepaid/-/purchase' + method: 'POST' + + options.data = + amount: @stripeAmount + description: @description + stripe: + token: e.token.id + timestamp: @purchaseTimestamp + type: 'terminal_subscription' + maxRedeemers: @purchase.users + months: @purchase.months + + options.error = (model, response, options) => + console.error 'FAILED: Prepaid purchase', response + console.error options + @statusMessage "Error purchasing prepaid code", "error" + # Not sure when this will happen. Stripe popup seems to give appropriate error messages. + + options.success = (model, response, options) => + console.log 'SUCCESS: Prepaid purchase', model.code + @statusMessage "Successfully purchased Prepaid Code!", "success" + @codes.add(model) + + @statusMessage "Finalizing purchase...", "information" + @supermodel.addRequestResource('purchase_prepaid', options, 0).load() diff --git a/app/views/admin/MainAdminView.coffee b/app/views/admin/MainAdminView.coffee index 8f5687d3b..5e86c93cc 100644 --- a/app/views/admin/MainAdminView.coffee +++ b/app/views/admin/MainAdminView.coffee @@ -15,6 +15,7 @@ module.exports = class MainAdminView extends RootView 'click #increment-button': 'incrementUserAttribute' 'click #user-search-result': 'onClickUserSearchResult' 'click #create-free-sub-btn': 'onClickFreeSubLink' + 'click #terminal-create': 'onClickTerminalSubLink' getRenderData: -> context = super() @@ -89,3 +90,27 @@ module.exports = class MainAdminView extends RootView options.error = (model, response, options) => console.error 'Failed to create prepaid', response @supermodel.addRequestResource('create_prepaid', options, 0).load() + + onClickTerminalSubLink: (e) => + @freeSubLink = '' + return unless me.isAdmin() + + options = + url: '/db/prepaid/-/create' + method: 'POST' + data: + type: 'terminal_subscription' + maxRedeemers: parseInt($("#users").val()) + months: parseInt($("#months").val()) + + options.success = (model, response, options) => + # TODO: Don't hardcode domain. + if application.isProduction() + @freeSubLink = "https://codecombat.com/account/prepaid?_ppc=#{model.code}" + else + @freeSubLink = "http://localhost:3000/account/prepaid?_ppc=#{model.code}" + @render?() + options.error = (model, response, options) => + console.error 'Failed to create prepaid', response + @supermodel.addRequestResource('create_prepaid', options, 0).load() + diff --git a/server/lib/utils.coffee b/server/lib/utils.coffee index 20474bc5f..2bae86c09 100644 --- a/server/lib/utils.coffee +++ b/server/lib/utils.coffee @@ -1,6 +1,7 @@ AnalyticsString = require '../analytics/AnalyticsString' log = require 'winston' mongoose = require 'mongoose' +config = require '../../server_config' module.exports = isID: (id) -> _.isString(id) and id.length is 24 and id.match(/[a-f0-9]/gi)?.length is 24 @@ -21,6 +22,9 @@ module.exports = # Grabs latest subscription (e.g. in case of a resubscribe) return done() unless customerID? return done() unless options.subscriptionID? or options.userID? + # Some prepaid tests were calling this in such a way that stripe wasn't defined. + stripe = require('stripe')(config.stripe.secretKey) unless stripe + subscriptionID = options.subscriptionID userID = options.userID diff --git a/server/payments/subscription_handler.coffee b/server/payments/subscription_handler.coffee index 7fba15a48..0c9fcf82a 100644 --- a/server/payments/subscription_handler.coffee +++ b/server/payments/subscription_handler.coffee @@ -14,6 +14,7 @@ User = require '../users/User' {findStripeSubscription} = require '../lib/utils' {getSponsoredSubsAmount} = require '../../app/core/utils' StripeUtils = require '../lib/stripe_utils' +moment = require 'moment' recipientCouponID = 'free' @@ -38,6 +39,7 @@ class SubscriptionHandler extends Handler return @getStripeSubscriptions(req, res) if args[1] is 'stripe_subscriptions' return @getSubscribers(req, res) if args[1] is 'subscribers' return @purchaseYearSale(req, res) if args[1] is 'year_sale' + return @subscribeWithPrepaidCode(req, res) if args[1] is 'subscribe_prepaid' super(arguments...) getStripeEvents: (req, res) -> @@ -117,23 +119,24 @@ class SubscriptionHandler extends Handler log.debug 'Analytics error:\n' + err @sendSuccess(res, userMap) + cancelSubscriptionImmediately: (user, subscription, done) => + return done() unless user and subscription + stripe.customers.cancelSubscription subscription.customer, subscription.id, (err) => + return done(err) if err + stripeInfo = _.cloneDeep(user.get('stripe') ? {}) + delete stripeInfo.planID + delete stripeInfo.prepaidCode + delete stripeInfo.subscriptionID + user.set('stripe', stripeInfo) + user.save (err) => + return done(err) if err + done() + + purchaseYearSale: (req, res) -> return @sendForbiddenError(res) unless req.user? return @sendForbiddenError(res) if req.user?.get('stripe')?.sponsorID - cancelSubscriptionImmediately = (user, subscription, done) => - return done() unless user and subscription - stripe.customers.cancelSubscription subscription.customer, subscription.id, (err) => - return done(err) if err - stripeInfo = _.cloneDeep(user.get('stripe') ? {}) - delete stripeInfo.planID - delete stripeInfo.prepaidCode - delete stripeInfo.subscriptionID - user.set('stripe', stripeInfo) - user.save (err) => - return done(err) if err - done() - StripeUtils.getCustomer req.user, req.body.stripe?.token, (err, customer) => if err @logSubscriptionError(req.user, "Purchase year sale get customer: #{JSON.stringify(err)}") @@ -142,7 +145,7 @@ class SubscriptionHandler extends Handler findStripeSubscription customer.id, subscriptionID: req.user.get('stripe')?.subscriptionID, (subscription) => stripeSubscriptionPeriodEndDate = new Date(subscription.current_period_end * 1000) if subscription - cancelSubscriptionImmediately req.user, subscription, (err) => + @cancelSubscriptionImmediately req.user, subscription, (err) => if err @logSubscriptionError(user, "Purchase year sale Stripe cancel subscription error: #{JSON.stringify(err)}") return @sendDatabaseError(res, err) @@ -192,6 +195,95 @@ class SubscriptionHandler extends Handler @logSubscriptionError(req.user, "Year sub sale HipChat tower msg error: #{JSON.stringify(error)}") @sendSuccess(res, user) + subscribeWithPrepaidCode: (req, res) -> + return @sendForbiddenError(res) unless req.user? + return @sendBadInputError(res,"You must provide a valid prepaid code") unless req.body?.ppc + + # Check if code exists and has room for more redeemers + Prepaid.findOne({ code: req.body.ppc?.toString() }).exec (err, prepaid) => + if err + @logSubscriptionError(req.user, "Redeem Prepaid Code find: #{JSON.stringify(err)}") + return @sendDatabaseError(res, err) + + return @sendForbiddenError(res) if prepaid is null + + oldRedeemers = prepaid.get('redeemers') ? [] + return @sendForbiddenError(res) if oldRedeemers.length >= prepaid.get('maxRedeemers') + + months = parseInt(prepaid.get('properties')?.months) + return @sendForbiddenError(res) if isNaN(months) or months < 1 + + for redeemer in oldRedeemers + return @sendForbiddenError(res) if redeemer.userID.equals(req.user._id) + + customerID = req.user.get('stripe')?.customerID + + unless customerID + @redeemCode(req, res, oldRedeemers, months) + else + stripe.customers.retrieve customerID, (err, customer) => + if err + @logSubscriptionError(req.user, "Redeem Prepaid Code get customer: #{JSON.stringify(err)}") + return @sendDatabaseError(res, err) + + findStripeSubscription customer.id, subscriptionID: req.user.get('stripe')?.subscriptionID, (subscription) => + stripeSubscriptionPeriodEndDate = null + if subscription + stripeSubscriptionPeriodEndDate = new Date(subscription.current_period_end * 1000) + + + @cancelSubscriptionImmediately req.user, subscription, (err) => + if err + @logSubscriptionError(user, "Redeem Prepaid Code Stripe cancel subscription error: #{JSON.stringify(err)}") + return @sendDatabaseError(res, err) + + @redeemCode(req, res, oldRedeemers, months, stripeSubscriptionPeriodEndDate) + + redeemCode: (req, res, oldRedeemers, months, startDate=null) => + return @sendForbiddenError(res) unless req.user? + return @sendForbiddenError(res) unless req.body?.ppc + return @sendForbiddenError(res) unless oldRedeemers + return @sendForbiddenError(res) if isNaN(months) or months < 1 + + newRedeemerPush = { $push: { redeemers : { date: new Date().toISOString(), userID: req.user._id } }} + + # Only update the prepaid document if the length of the redeemers array hasn't changed in the db. + # This will probably fail if redeemers isn't defined. new terminal_subscriptions created should be sure to set the redeemers array + # TODO: find a better way? + Prepaid.update { 'code': req.body.ppc, 'redeemers': { $size: oldRedeemers.length }}, newRedeemerPush, (err, num, info) => + if err + @logSubscriptionError(req.user, "Subscribe with Prepaid Code update: #{JSON.stringify(err)}") + return @sendDatabaseError(res, err) + + return @sendNotFoundError(res, "Error while updating prepaid redeemer") if num isnt 1 + + + # Add terminal subscription to User, extending existing subscriptions + # TODO: refactor this into some form useable by both this and purchaseYearSale? + stripeInfo = _.cloneDeep(req.user.get('stripe') ? {}) + endDate = new moment() + if startDate + endDate = new moment(startDate) + else if _.isString(stripeInfo.free) and new moment().isBefore(new moment(stripeInfo.free)) + endDate = new moment(stripeInfo.free) + + endDate = endDate.add(months, 'months') + stripeInfo.free = endDate.toISOString().substring(0, 10) + req.user.set('stripe', stripeInfo) + + # Add gems to User + purchased = _.clone(req.user.get('purchased')) + purchased ?= {} + purchased.gems ?= 0 + purchased.gems += subscriptions.basic.gems * months + req.user.set('purchased', purchased) + + req.user.save (err, user) => + if err + @logSubscriptionError(req.user, "User save error: #{JSON.stringify(err)}") + return @sendDatabaseError(res, err) + @sendSuccess(res, user) + subscribeUser: (req, user, done) -> if (not req.user) or req.user.isAnonymous() or user.isAnonymous() return done({res: 'You must be signed in to subscribe.', code: 403}) diff --git a/server/prepaids/Prepaid.coffee b/server/prepaids/Prepaid.coffee index bccc9ac60..3461dc32c 100644 --- a/server/prepaids/Prepaid.coffee +++ b/server/prepaids/Prepaid.coffee @@ -2,6 +2,8 @@ mongoose = require 'mongoose' config = require '../../server_config' PrepaidSchema = new mongoose.Schema {}, {strict: false, minimize: false,read:config.mongo.readpref} +PrepaidSchema.index({code: 1}, { unique: true }) + PrepaidSchema.statics.generateNewCode = (done) -> tryCode = -> code = _.sample("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 8).join('') diff --git a/server/prepaids/prepaid_handler.coffee b/server/prepaids/prepaid_handler.coffee index 1851fa7e5..9060ece17 100644 --- a/server/prepaids/prepaid_handler.coffee +++ b/server/prepaids/prepaid_handler.coffee @@ -1,5 +1,7 @@ Handler = require '../commons/Handler' Prepaid = require './Prepaid' +StripeUtils = require '../lib/stripe_utils' +{getPrepaidCodeAmount} = require '../../app/core/utils' # TODO: Should this happen on a save() call instead of a prepaid/-/create post? # TODO: Probably a better way to create a unique 8 charactor string property using db voodoo @@ -7,31 +9,113 @@ Prepaid = require './Prepaid' PrepaidHandler = class PrepaidHandler extends Handler modelClass: Prepaid jsonSchema: require '../../app/schemas/models/prepaid.schema' - allowedMethods: ['POST'] + allowedMethods: ['GET','POST'] + + baseAmount: 999 + + logPurchaseError: (user, msg) -> + console.warn "Prepaid Purchase Error: [#{user.get('slug')} (#{user._id})] '#{msg}'" hasAccess: (req) -> req.user?.isAdmin() getByRelationship: (req, res, args...) -> relationship = args[1] + return @getPrepaid(req, res, args[2]) if relationship is 'code' return @createPrepaid(req, res) if relationship is 'create' + return @purchasePrepaid(req, res) if relationship is 'purchase' super arguments... + getPrepaid: (req, res, code) -> + return @sendForbiddenError(res) unless req.user? + return @sendNotFoundError(res, "You must specify a code") unless code + + Prepaid.findOne({ code: code.toString() }).exec (err, prepaid) => + if err + console.warn "Get Prepaid Code Error [#{req.user.get('slug')} (#{req.user.id})]: #{JSON.stringify(err)}" + return @sendDatabaseError(res, err) + + return @sendNotFoundError(res, "Code not found") unless prepaid + + @sendSuccess(res, prepaid.toObject()) + createPrepaid: (req, res) -> return @sendForbiddenError(res) unless @hasAccess(req) - return @sendForbiddenError(res) unless req.body.type is 'subscription' + return @sendForbiddenError(res) unless req.body.type in ['subscription','terminal_subscription'] return @sendForbiddenError(res) unless req.body.maxRedeemers > 0 + Prepaid.generateNewCode (code) => return @sendDatabaseError(res, 'Database error.') unless code - prepaid = new Prepaid + # TODO: change the creator to use ObjectID like with the terminal_subscription + options = creator: req.user.id type: req.body.type code: code maxRedeemers: req.body.maxRedeemers - properties: - couponID: 'free' + properties: {} + redeemers: [] + + if req.body.type is 'subscription' + options.properties.couponID = 'free' + + if req.body.type is 'terminal_subscription' + options.properties.months = req.body.months + options.creator = req.user._id + + prepaid = new Prepaid options prepaid.save (err) => return @sendDatabaseError(res, err) if err @sendSuccess(res, prepaid.toObject()) + purchasePrepaid: (req, res) -> + return @sendForbiddenError(res) unless req.user? + return @sendForbiddenError(res) unless req.body.type is 'terminal_subscription' + + maxRedeemers = parseInt(req.body.maxRedeemers) + months = parseInt(req.body.months) + + return @sendForbiddenError(res) unless isNaN(maxRedeemers) is false and maxRedeemers > 0 + return @sendForbiddenError(res) unless isNaN(months) is false and months > 0 + return @sendError(res, 403, "Users or Months must be greater than 3") if maxRedeemers < 3 and months < 3 + + StripeUtils.getCustomer req.user, req.body.stripe?.token, (err, customer) => + if err + @logPurchaseError(req.user, "getCustomer error: #{JSON.stringify(err)}") + return @sendDatabaseError(res, err) + + metadata = + type: req.body.type + userID: req.user._id + '' + timestamp: parseInt(req.body.stripe?.timestamp) + description: req.body.description + maxRedeemers: maxRedeemers + months: months + productID: 'prepaid ' + req.body.type + + amount = getPrepaidCodeAmount(@baseAmount, maxRedeemers, months) + + StripeUtils.createCharge req.user, amount, metadata, (err, charge) => + if err + @logPurchaseError(req.user, "createCharge error: #{JSON.stringify(err)}") + return @sendDatabaseError(res, err) + + StripeUtils.createPayment req.user, charge, (err, payment) => + if err + @logPurchaseError(req.user, "createPayment error: #{JSON.stringify(err)}") + return @sendDatabaseError(res, err) + + Prepaid.generateNewCode (code) => + return @sendDatabaseError(res, 'Database error.') unless code + prepaid = new Prepaid + creator: req.user._id + type: req.body.type + code: code + maxRedeemers: req.body.maxRedeemers + redeemers: [] + properties: + months: req.body.months + prepaid.save (err) => + return @sendDatabaseError(res, err) if err + @sendSuccess(res, prepaid.toObject()) + module.exports = new PrepaidHandler() diff --git a/server/routes/stripe.coffee b/server/routes/stripe.coffee index 0e46de9c6..27330411e 100644 --- a/server/routes/stripe.coffee +++ b/server/routes/stripe.coffee @@ -46,14 +46,18 @@ module.exports.setup = (app) -> invoiceID = req.body.data.object.id stripe.invoices.retrieve invoiceID, (err, invoice) => - return res.send(500, '') if err + if err + logStripeWebhookError("Retrieve invoice error: #{JSON.stringify(err)}") + return res.send(500, '') unless invoice.total or invoice.discount?.coupon?.id is 'free' # invoices made when trialing, probably given for people who resubscribe after unsubscribing return res.send(200, '') return res.send(200, '') unless invoice.lines?.data?.length > 0 getUserID invoice.customer, (err, userID) => - return res.send(500, '') if err + if err + logStripeWebhookError("Get user ID error: #{JSON.stringify(err)}") + return res.send(500, '') # User is recipient if no metadata.id recipientID = invoice.lines.data[0].metadata?.id or userID @@ -62,7 +66,9 @@ module.exports.setup = (app) -> subscriptionID = invoice.lines.data[0].subscription or invoice.lines.data[0].id User.findById recipientID, (err, recipient) => - return res.send(500, '') if err + if err + logStripeWebhookError("Find recipient user error: #{JSON.stringify(err)}") + return res.send(500, '') return res.send(200) unless recipient # just for the sake of testing... Payment.findOne {'stripe.invoiceID': invoiceID}, (err, payment) => @@ -82,7 +88,9 @@ module.exports.setup = (app) -> payment.set 'gems', 3500 if invoice.lines.data[0].plan?.id is 'basic' payment.save (err) => - return res.send(500, '') if err + if err + logStripeWebhookError("Save payment error: #{JSON.stringify(err)}") + return res.send(500, '') return res.send(201, '') if invoice.lines.data[0].plan?.id isnt 'basic' # Update purchased gems @@ -94,7 +102,9 @@ module.exports.setup = (app) -> purchased.gems = gems recipient.set('purchased', purchased) recipient.save (err) -> - return res.send(500, '') if err + if err + logStripeWebhookError("Save recipient user error: #{JSON.stringify(err)}") + return res.send(500, '') return res.send(201, '') handleSubscriptionDeleted = (req, res) -> diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index fd040d204..79d176441 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -23,6 +23,7 @@ UserRemark = require './remarks/UserRemark' {isID} = require '../lib/utils' hipchat = require '../hipchat' sendwithus = require '../sendwithus' +Prepaid = require '../prepaids/Prepaid' serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset', 'lastIP'] candidateProperties = [ @@ -305,6 +306,7 @@ UserHandler = class UserHandler extends Handler return @avatar(req, res, args[0]) if args[1] is 'avatar' return @getByIDs(req, res) if args[1] is 'users' return @getNamesByIDs(req, res) if args[1] is 'names' + return @getPrepaidCodes(req, res) if args[1] is 'prepaid_codes' return @nameToID(req, res, args[0]) if args[1] is 'nameToID' return @getLevelSessionsForEmployer(req, res, args[0]) if args[1] is 'level.sessions' and args[2] is 'employer' return @getLevelSessions(req, res, args[0]) if args[1] is 'level.sessions' @@ -453,6 +455,11 @@ UserHandler = class UserHandler extends Handler sendMail emailParams + getPrepaidCodes: (req, res) -> + orQuery = [{ creator: req.user._id }, { 'redeemers.userID' : req.user._id }] + Prepaid.find({}).or(orQuery).exec (err, documents) => + @sendSuccess(res, documents) + agreeToCLA: (req, res) -> return @sendForbiddenError(res) unless req.user doc = diff --git a/test/server/common.coffee b/test/server/common.coffee index 821bcec2a..71d9ae58f 100644 --- a/test/server/common.coffee +++ b/test/server/common.coffee @@ -120,11 +120,33 @@ wrapUpGetUser = (email, user, done) -> GLOBAL.getURL = (path) -> return 'http://localhost:3001' + path -GLOBAL.createPrepaid = (type, maxRedeemers, done) -> +GLOBAL.createPrepaid = (type, maxRedeemers, months, done) -> options = uri: GLOBAL.getURL('/db/prepaid/-/create') options.json = type: type maxRedeemers: maxRedeemers + if months + options.json.months = months + request.post options, done + +GLOBAL.fetchPrepaid = (ppc, done) -> + options = uri: GLOBAL.getURL('/db/prepaid/-/code/'+ppc) + request.get options, done + +GLOBAL.purchasePrepaid = (type, maxRedeemers, months, done) -> + options = uri: GLOBAL.getURL('/db/prepaid/-/purchase') + options.json = + type: type + maxRedeemers: maxRedeemers + months: months + stripe: + timestamp: new Date().getTime() + request.post options, done + +GLOBAL.subscribeWithPrepaid = (ppc, done) => + options = url: GLOBAL.getURL('/db/subscription/-/subscribe_prepaid') + options.json = + ppc: ppc request.post options, done newUserCount = 0 diff --git a/test/server/functional/prepaid.spec.coffee b/test/server/functional/prepaid.spec.coffee index ae1bb0dbd..e2dfd6bc4 100644 --- a/test/server/functional/prepaid.spec.coffee +++ b/test/server/functional/prepaid.spec.coffee @@ -1,9 +1,18 @@ require '../common' +config = require '../../../server_config' +moment = require 'moment' +{findStripeSubscription} = require '../../../server/lib/utils' describe '/db/prepaid', -> prepaidURL = getURL('/db/prepaid') prepaidCreateURL = getURL('/db/prepaid/-/create') + headers = {'X-Change-Plan': 'true'} + + joeData = null + stripe = require('stripe')(config.stripe.secretKey) + joeCode = null + verifyPrepaid = (user, prepaid, done) -> expect(prepaid.creator).toEqual(user.id) expect(prepaid.type).toEqual('subscription') @@ -13,12 +22,12 @@ describe '/db/prepaid', -> done() it 'Clear database users and prepaids', (done) -> - clearModels [User, Prepaid], (err) -> + clearModels [User, Prepaid, Payment], (err) -> throw err if err done() it 'Anonymous creates prepaid code', (done) -> - createPrepaid 'subscription', 1, (err, res, body) -> + createPrepaid 'subscription', 1, 0, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(401) done() @@ -26,7 +35,7 @@ describe '/db/prepaid', -> it 'Non-admin creates prepaid code', (done) -> loginNewUser (user1) -> expect(user1.isAdmin()).toEqual(false) - createPrepaid 'subscription', 4, (err, res, body) -> + createPrepaid 'subscription', 4, 0, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(403) done() @@ -37,18 +46,35 @@ describe '/db/prepaid', -> user1.save (err, user1) -> expect(err).toBeNull() expect(user1.isAdmin()).toEqual(true) - createPrepaid 'subscription', 1, (err, res, body) -> + createPrepaid 'subscription', 1, 0, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(200) verifyPrepaid user1, body, done + it 'Admin creates prepaid code with type terminal_subscription', (done) -> + loginNewUser (user1) -> + user1.set('permissions', ['admin']) + user1.save (err, user1) -> + expect(err).toBeNull() + expect(user1.isAdmin()).toEqual(true) + createPrepaid 'terminal_subscription', 2, 3, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + expect(body.creator).toEqual(user1.id) + expect(body.type).toEqual('terminal_subscription') + expect(body.maxRedeemers).toEqual(2) + expect(body.properties?.months).toEqual(3) + expect(body.code).toMatch(/^\w{8}$/) + done() + + it 'Admin creates prepaid code with invalid type', (done) -> loginNewUser (user1) -> user1.set('permissions', ['admin']) user1.save (err, user1) -> expect(err).toBeNull() expect(user1.isAdmin()).toEqual(true) - createPrepaid 'bulldozer', 1, (err, res, body) -> + createPrepaid 'bulldozer', 1, 0, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(403) done() @@ -59,7 +85,7 @@ describe '/db/prepaid', -> user1.save (err, user1) -> expect(err).toBeNull() expect(user1.isAdmin()).toEqual(true) - createPrepaid null, 1, (err, res, body) -> + createPrepaid null, 1, 0, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(403) done() @@ -70,7 +96,7 @@ describe '/db/prepaid', -> user1.save (err, user1) -> expect(err).toBeNull() expect(user1.isAdmin()).toEqual(true) - createPrepaid 'subscription', 0, (err, res, body) -> + createPrepaid 'subscription', 0, 0, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(403) done() @@ -89,7 +115,7 @@ describe '/db/prepaid', -> user1.save (err, user1) -> expect(err).toBeNull() expect(user1.isAdmin()).toEqual(true) - createPrepaid 'subscription', 1, (err, res, prepaid) -> + createPrepaid 'subscription', 1, 0, (err, res, prepaid) -> expect(err).toBeNull() expect(res.statusCode).toBe(200) request.get {uri: prepaidURL}, (err, res, body) -> @@ -104,3 +130,231 @@ describe '/db/prepaid', -> break expect(found).toEqual(true) done() unless found + + # *** Purchase Prepaid Codes *** # + it 'Anonymous submits a prepaid purchase', (done) -> + logoutUser () -> + purchasePrepaid 'terminal_subscription', 3, 3, (err, res, prepaid) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(401) + done() + + it 'Should error if type isnt terminal_subscription', (done) -> + loginNewUser (user1) -> + purchasePrepaid 'subscription', 3, 3, (err, res, prepaid) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(403) + done() + + it 'Should error if maxRedeemers is invalid', (done) -> + loginNewUser (user1) -> + purchasePrepaid 'terminal_subscription', -1, 3, (err, res, prepaid) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(403) + done() + purchasePrepaid 'terminal_subscription', 'foo', 3, (err, res, prepaid) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(403) + done() + + it 'Should error if months is invalid', (done) -> + loginNewUser (user1) -> + purchasePrepaid 'terminal_subscription', 3, -1, (err, res, prepaid) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(403) + done() + purchasePrepaid 'terminal_subscription', 3, 'foo', (err, res, prepaid) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(403) + done() + + it 'Should error if maxRedeemers and months are less than 3', (done) -> + loginNewUser (user1) -> + purchasePrepaid 'terminal_subscription', 1, 1, (err, res, prepaid) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(403) + done() + + it 'User submits valid prepaid code purchase', (done) -> + stripe.tokens.create { + card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } + }, (err, token) -> + stripeTokenID = token.id + loginJoe (joe) -> + joeData = joe.toObject() + joeData.stripe = { + token: stripeTokenID + planID: 'basic' + } + request.put {uri: getURL('/db/user'), json: joeData, headers: headers }, (err, res, body) -> + joeData = body + expect(res.statusCode).toBe(200) + expect(joeData.stripe.customerID).toBeDefined() + expect(firstSubscriptionID = joeData.stripe.subscriptionID).toBeDefined() + expect(joeData.stripe.planID).toBe('basic') + expect(joeData.stripe.token).toBeUndefined() + purchasePrepaid 'terminal_subscription', 3, 3, (err, res, prepaid) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + expect(prepaid.type).toEqual('terminal_subscription') + expect(prepaid.code).toBeDefined() + # Saving this code for later tests + joeCode = prepaid.code + expect(prepaid.creator).toBeDefined() + expect(prepaid.maxRedeemers).toEqual(3) + expect(prepaid.properties).toBeDefined() + expect(prepaid.properties.months).toEqual(3) + done() + + it 'Should have logged a Payment with the correct amount', (done) -> + loginJoe (joe) -> + query = + purchaser: joe._id + Payment.find query, (err, payments) -> + expect(err).toBeNull() + expect(payments).not.toBeNull() + expect(payments.length).toEqual(1) + expect(payments[0].get('amount')).toEqual(8991) + done() + + + it 'Anonymous cant redeem a prepaid code', (done) -> + logoutUser () -> + subscribeWithPrepaid joeCode, (err, res) -> + expect(err).toBeNull() + expect(res?.statusCode).toEqual(401) + done() + + + it 'User cant redeem a nonexistant prepaid code', (done) -> + loginJoe (joe) -> + subscribeWithPrepaid 'abc123', (err, res) -> + expect(err).toBeNull() + expect(res.statusCode).toEqual(403) + done() + + it 'User cant redeem empty code', (done) -> + loginJoe (joe) -> + subscribeWithPrepaid '', (err, res) -> + expect(err).toBeNull() + expect(res.statusCode).toEqual(422) + done() + + it 'Anonymous cant fetch a prepaid code', (done) -> + expect(joeCode).not.toBeNull() + logoutUser () -> + fetchPrepaid joeCode, (err, res) -> + expect(err).toBeNull() + expect(res.statusCode).toEqual(403) + done() + + it 'User can fetch a prepaid code', (done) -> + expect(joeCode).not.toBeNull() + loginJoe (joe) -> + fetchPrepaid joeCode, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toEqual(200) + + expect(body).toBeDefined() + return done() unless body + + prepaid = JSON.parse(body) + expect(prepaid.code).toEqual(joeCode) + expect(prepaid.maxRedeemers).toEqual(3) + expect(prepaid.properties?.months).toEqual(3) + done() + + it 'Creator can redeeem a prepaid code', (done) -> + loginJoe (joe) -> + expect(joeCode).not.toBeNull() + expect(joeData.stripe?.customerID).toBeDefined() + expect(joeData.stripe?.subscriptionID).toBeDefined() + return done() unless joeData.stripe?.customerID + + # joe has a stripe subscription, so test if the months are added to the end of it. + stripe.customers.retrieve joeData.stripe.customerID, (err, customer) => + expect(err).toBeNull() + + findStripeSubscription customer.id, subscriptionID: joeData.stripe?.subscriptionID, (subscription) => + if subscription + stripeSubscriptionPeriodEndDate = new moment(subscription.current_period_end * 1000) + else + expect(stripeSubscriptionPeriodEndDate).toBeDefined() + return done() + + subscribeWithPrepaid joeCode, (err, res, result) => + expect(err).toBeNull() + expect(res.statusCode).toEqual(200) + endDate = stripeSubscriptionPeriodEndDate.add(3, 'months').toISOString().substring(0, 10) + expect(result?.stripe?.free).toEqual(endDate) + expect(result?.purchased?.gems).toEqual(14000) + findStripeSubscription customer.id, subscriptionID: joeData.stripe?.subscriptionID, (subscription) => + expect(subscription).toBeNull() + done() + + it 'User can redeem a prepaid code', (done) -> + loginSam (sam) -> + subscribeWithPrepaid joeCode, (err, res, result) -> + expect(err).toBeNull() + expect(res.statusCode).toEqual(200) + endDate = new moment().add(3, 'months').toISOString().substring(0, 10) + expect(result?.stripe?.free).toEqual(endDate) + expect(result?.purchased?.gems).toEqual(10500) + done() + + it 'Wont allow the same person to redeem twice', (done) -> + loginSam (sam) -> + subscribeWithPrepaid joeCode, (err, res, result) -> + expect(err).toBeNull() + expect(res.statusCode).toEqual(403) + done() + + it 'Will return redeemed code as part of codes list', (done) -> + loginSam (sam) -> + request.get "#{getURL('/db/user')}/#{sam.id}/prepaid_codes", (err, res) -> + expect(err).toBeNull() + expect(res.statusCode).toEqual(200) + codes = JSON.parse res.body + expect(codes.length).toEqual(1) + done() + + it 'Third user can redeem a prepaid code', (done) -> + loginNewUser (user) -> + subscribeWithPrepaid joeCode, (err, res, result) -> + expect(err).toBeNull() + expect(res.statusCode).toEqual(200) + endDate = new moment().add(3, 'months').toISOString().substring(0, 10) + expect(result?.stripe?.free).toEqual(endDate) + expect(result?.purchased?.gems).toEqual(10500) + done() + + it 'Fourth user cannot redeem code', (done) -> + loginNewUser (user) -> + subscribeWithPrepaid joeCode, (err, res, result) -> + expect(err).toBeNull() + expect(res.statusCode).toEqual(403) + done() + + + it 'Can fetch a list of purchased and redeemed prepaid codes', (done) -> + loginJoe (joe) -> + purchasePrepaid 'terminal_subscription', 3, 1, (err, res, prepaid) -> + request.get "#{getURL('/db/user')}/#{joe.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[0].maxRedeemers).toEqual(3) + expect(codes[0].properties).toBeDefined() + expect(codes[0].properties.months).toEqual(3) + done() + + it 'Test for injection', (done) -> + loginNewUser (user) -> + code = { $exists: true } + subscribeWithPrepaid code, (err, res, result) -> + expect(err).toBeNull() + expect(res.statusCode).not.toEqual(200) + done() + # TODO: add a bunch of parallel tests trying to redeem a code with a high maxRedeemers (50?) to see what happens + diff --git a/test/server/functional/subscription.spec.coffee b/test/server/functional/subscription.spec.coffee index f95aeea3f..88e16935e 100644 --- a/test/server/functional/subscription.spec.coffee +++ b/test/server/functional/subscription.spec.coffee @@ -297,6 +297,8 @@ describe 'Subscriptions', -> return done() unless sponsorCustomerID and sponsorStripe.sponsorSubscriptionID stripe.customers.retrieveSubscription sponsorCustomerID, sponsorStripe.sponsorSubscriptionID, (err, subscription) -> expect(err).toBeNull() + expect(subscription?).toBe(true) + return done() unless subscription? expect(subscription.plan.amount).toEqual(1) expect(subscription.customer).toEqual(sponsorCustomerID) expect(subscription.quantity).toEqual(utils.getSponsoredSubsAmount(subPrice, numSponsored, sponsorStripe.subscriptionID?)) @@ -361,6 +363,7 @@ describe 'Subscriptions', -> Payment.findOne paymentQuery, (err, payment) -> expect(err).toBeNull() expect(payment).not.toBeNull() + return done() if payment is null expect(payment.get('amount')).toEqual(0) expect(payment.get('gems')).toBeGreaterThan(subGems - 1) done() @@ -406,6 +409,8 @@ describe 'Subscriptions', -> expect(user.get('stripe').customerID).toBeDefined() expect(user.get('stripe').planID).toBeUndefined() expect(user.get('stripe').token).toBeUndefined() + expect(user.get('stripe').subscriptionID).toBeDefined() + return done() unless user.get('stripe').subscriptionID stripe.customers.retrieveSubscription user.get('stripe').customerID, user.get('stripe').subscriptionID, (err, subscription) -> expect(err).toBeNull() expect(subscription).not.toBeNull() @@ -422,11 +427,11 @@ describe 'Subscriptions', -> expect(err).toBeNull() return done() if err expect(res.statusCode).toBe(200) - expect(body.stripe.customerID).toBeDefined() + expect(body.stripe?.customerID).toBeDefined() updatedUser = body # Call webhooks for invoices - options = customer: body.stripe.customerID, limit: 100 + options = customer: body.stripe?.customerID, limit: 100 stripe.invoices.list options, (err, invoices) -> expect(err).toBeNull() expect(invoices).not.toBeNull() @@ -569,7 +574,7 @@ describe 'Subscriptions', -> user1.save (err, user1) -> expect(err).toBeNull() expect(user1.isAdmin()).toEqual(true) - createPrepaid 'subscription', 1, (err, res, prepaid) -> + createPrepaid 'subscription', 1, 0, (err, res, prepaid) -> expect(err).toBeNull() subscribeUser user1, null, prepaid.code, -> Prepaid.findById prepaid._id, (err, prepaid) -> @@ -584,6 +589,8 @@ describe 'Subscriptions', -> expect(err).toBeNull() stripeInfo = user1.get('stripe') expect(stripeInfo.prepaidCode).toEqual(prepaid.get('code')) + expect(stripeInfo.subscriptionID).toBeDefined() + return done() unless stripeInfo.subscriptionID # Delete subscription stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.subscriptionID, (err, subscription) -> @@ -612,7 +619,7 @@ describe 'Subscriptions', -> subscribeUser user1, token, null, -> User.findById user1.id, (err, user1) -> expect(err).toBeNull() - createPrepaid 'subscription', 1, (err, res, prepaid) -> + createPrepaid 'subscription', 1, 0, (err, res, prepaid) -> expect(err).toBeNull() subscribeUser user1, null, prepaid.code, -> Prepaid.findById prepaid._id, (err, prepaid) -> @@ -627,6 +634,7 @@ describe 'Subscriptions', -> stripe.customers.retrieveSubscription customerID, subscriptionID, (err, subscription) -> expect(err).toBeNull() expect(subscription).not.toBeNull() + return done() unless subscription expect(subscription.discount?.coupon?.id).toEqual('free') done() @@ -646,7 +654,7 @@ describe 'Subscriptions', -> request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, updatedUser) -> expect(err).toBeNull() expect(res.statusCode).toBe(200) - createPrepaid 'subscription', 1, (err, res, prepaid) -> + createPrepaid 'subscription', 1, 0, (err, res, prepaid) -> subscribeUser user1, null, prepaid.code, -> Prepaid.findById prepaid._id, (err, prepaid) -> expect(err).toBeNull() @@ -660,7 +668,7 @@ describe 'Subscriptions', -> stripe.customers.retrieveSubscription customerID, subscriptionID, (err, subscription) -> expect(err).toBeNull() expect(subscription).not.toBeNull() - expect(subscription.discount?.coupon?.id).toEqual('free') + expect(subscription?.discount?.coupon?.id).toEqual('free') done() it 'Subscribe with prepaid, then cancel', (done) -> @@ -669,7 +677,7 @@ describe 'Subscriptions', -> user1.save (err, user1) -> expect(err).toBeNull() expect(user1.isAdmin()).toEqual(true) - createPrepaid 'subscription', 1, (err, res, prepaid) -> + createPrepaid 'subscription', 1, 0, (err, res, prepaid) -> expect(err).toBeNull() subscribeUser user1, null, prepaid.code, -> Prepaid.findById prepaid._id, (err, prepaid) -> @@ -691,7 +699,7 @@ describe 'Subscriptions', -> user1.save (err, user1) -> expect(err).toBeNull() expect(user1.isAdmin()).toEqual(true) - createPrepaid 'subscription', 1, (err, res, prepaid) -> + createPrepaid 'subscription', 1, 0, (err, res, prepaid) -> expect(err).toBeNull() subscribeUser user1, null, prepaid.code, -> loginNewUser (user2) -> @@ -714,7 +722,7 @@ describe 'Subscriptions', -> user1.save (err, user1) -> expect(err).toBeNull() expect(user1.isAdmin()).toEqual(true) - createPrepaid 'subscription', 2, (err, res, prepaid) -> + createPrepaid 'subscription', 2, 0, (err, res, prepaid) -> expect(err).toBeNull() subscribeUser user1, null, prepaid.code, -> loginNewUser (user2) -> @@ -730,7 +738,7 @@ describe 'Subscriptions', -> user1.save (err, user1) -> expect(err).toBeNull() expect(user1.isAdmin()).toEqual(true) - createPrepaid 'subscription', 1, (err, res, prepaid) -> + createPrepaid 'subscription', 1, 0, (err, res, prepaid) -> expect(err).toBeNull() subscribeUser user1, null, prepaid.code, -> Prepaid.findById prepaid._id, (err, prepaid) -> @@ -746,7 +754,7 @@ describe 'Subscriptions', -> user1.save (err, user1) -> expect(err).toBeNull() expect(user1.isAdmin()).toEqual(true) - createPrepaid 'subscription', 2, (err, res, prepaid) -> + createPrepaid 'subscription', 2, 0, (err, res, prepaid) -> expect(err).toBeNull() Prepaid.findById prepaid._id, (err, prepaid) -> expect(err).toBeNull() @@ -795,8 +803,13 @@ describe 'Subscriptions', -> createNewUser (user2) -> loginNewUser (user1) -> subscribeRecipients user1, [user2], token, (updatedUser) -> - customerID = updatedUser.stripe.customerID - subscriptionID = updatedUser.stripe.recipients[0].subscriptionID + expect(updatedUser).not.toBeNull() + return done() unless updatedUser + customerID = updatedUser.stripe?.customerID + expect(customerID).toBeDefined() + subscriptionID = updatedUser.stripe?.recipients[0]?.subscriptionID + expect(subscriptionID).toBeDefined() + return done() unless customerID and subscriptionID loginUser user2, (user2) -> request.del {uri: "#{userURL}/#{user2.id}"}, (err, res) -> expect(err).toBeNull() @@ -987,7 +1000,7 @@ describe 'Subscriptions', -> user2.save (err, user1) -> expect(err).toBeNull() expect(user2.isAdmin()).toEqual(true) - createPrepaid 'subscription', 1, (err, res, prepaid) -> + createPrepaid 'subscription', 1, 0, (err, res, prepaid) -> expect(err).toBeNull() requestBody = user2.toObject() requestBody.stripe = @@ -1022,8 +1035,11 @@ describe 'Subscriptions', -> createNewUser (user2) -> loginNewUser (user1) -> subscribeRecipients user1, [user2, user3], token, (updatedUser) -> - customerID = updatedUser.stripe.customerID - subscriptionID = updatedUser.stripe.sponsorSubscriptionID + customerID = updatedUser?.stripe?.customerID + subscriptionID = updatedUser?.stripe?.sponsorSubscriptionID + expect(customerID).toBeDefined() + expect(subscriptionID).toBeDefined() + return done() unless customerID and subscriptionID # Find Stripe sponsor subscription stripe.customers.retrieveSubscription customerID, subscriptionID, (err, subscription) -> @@ -1041,6 +1057,7 @@ describe 'Subscriptions', -> expect(err).toBeNull() # Should have 2 cancelled recipient subs with cancel_at_period_end = true + # TODO: is this correct, or do we terminate recipient subs immediately now? User.findById user1.id, (err, user1) -> expect(err).toBeNull() stripeInfo = user1.get('stripe') @@ -1159,7 +1176,7 @@ describe 'Subscriptions', -> user1.save (err, user1) -> expect(err).toBeNull() expect(user1.isAdmin()).toEqual(true) - createPrepaid 'subscription', 1, (err, res, prepaid) -> + createPrepaid 'subscription', 1, 0, (err, res, prepaid) -> expect(err).toBeNull() subscribeUser user1, null, prepaid.code, -> stripe.tokens.create { @@ -1233,7 +1250,7 @@ describe 'Subscriptions', -> stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> expect(err).toBeNull() expect(subscription).not.toBeNull() - expect(subscription.quantity).toEqual(getSubscribedQuantity(1)) + expect(subscription?.quantity).toEqual(getSubscribedQuantity(1)) # Unsubscribe recipient1 unsubscribeRecipient user1, recipients.get(1), -> @@ -1245,6 +1262,7 @@ describe 'Subscriptions', -> stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> expect(err).toBeNull() expect(subscription).not.toBeNull() + return done() unless subscription expect(subscription.quantity).toEqual(0) done() @@ -1274,13 +1292,16 @@ describe 'Subscriptions', -> unsubscribeRecipient user1, recipients.get(0), -> User.findById user1.id, (err, user1) -> stripeInfo = user1.get('stripe') + expect(stripeInfo.customerID).toBeDefined() + expect(stripeInfo.sponsorSubscriptionID).toBeDefined() + return done() unless stripeInfo.customerID and stripeInfo.sponsorSubscriptionID expect(stripeInfo.recipients.length).toEqual(recipientCount - 1) verifyNotSponsoring user1.id, recipients.get(0).id, -> verifyNotRecipient recipients.get(0).id, -> stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> expect(err).toBeNull() expect(subscription).not.toBeNull() - expect(subscription.quantity).toEqual(getSubscribedQuantity(recipientCount - 1)) + expect(subscription?.quantity).toEqual(getSubscribedQuantity(recipientCount - 1)) # Unsubscribe second recipient unsubscribeRecipient user1, recipients.get(1), -> @@ -1292,7 +1313,7 @@ describe 'Subscriptions', -> stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> expect(err).toBeNull() expect(subscription).not.toBeNull() - expect(subscription.quantity).toEqual(getSubscribedQuantity(recipientCount - 2)) + expect(subscription?.quantity).toEqual(getSubscribedQuantity(recipientCount - 2)) # Unsubscribe self User.findById user1.id, (err, user1) -> @@ -1312,7 +1333,7 @@ describe 'Subscriptions', -> stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> expect(err).toBeNull() expect(subscription).not.toBeNull() - expect(subscription.quantity).toEqual(getSubscribedQuantity(recipientCount - 3)) + expect(subscription?.quantity).toEqual(getSubscribedQuantity(recipientCount - 3)) done() xit 'Unsubscribed user1 subscribes 13 users, unsubcribes 2', (done) -> From 1f08867f798c46856a0bf63b2484f71e5f40b361 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Fri, 25 Sep 2015 10:28:13 -0700 Subject: [PATCH 22/26] Update subscription server tests to use async.series --- test/server/functional/subscription.spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/server/functional/subscription.spec.coffee b/test/server/functional/subscription.spec.coffee index 88e16935e..8010c324a 100644 --- a/test/server/functional/subscription.spec.coffee +++ b/test/server/functional/subscription.spec.coffee @@ -449,7 +449,7 @@ describe 'Subscriptions', -> unless invoice.id of invoicesWebHooked invoicesWebHooked[invoice.id] = true webhookTasks.push makeWebhookCall(invoice) - async.parallel webhookTasks, (err, results) -> + async.series webhookTasks, (err, results) -> expect(err?).toEqual(false) done(updatedUser) From 11c03b3905009286928686d8b971dc39d2027055 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Fri, 25 Sep 2015 13:58:32 -0700 Subject: [PATCH 23/26] Think I fixed commonJSHeader regardless of npm version. --- config.coffee | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.coffee b/config.coffee index b855d17c2..32e6fa1c6 100644 --- a/config.coffee +++ b/config.coffee @@ -3,7 +3,7 @@ _ = require 'lodash' _.str = require 'underscore.string' sysPath = require 'path' fs = require('fs') -commonjsHeader = commonjsHeader = require('commonjs-require-definition') +commonjsHeader = require('commonjs-require-definition') TRAVIS = process.env.COCO_TRAVIS_TEST diff --git a/package.json b/package.json index 80ef9c0c1..f9259c5dc 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "coffee-script-brunch": "https://github.com/brunch/coffee-script-brunch/tarball/master", "coffeelint-brunch": "> 1.0 < 1.8", "compressible": "~1.0.1", - "commonjs-require-definition": "0.1.0", + "commonjs-require-definition": "~0.2.0", "css-brunch": "> 1.0 < 1.8", "jade": "0.33.x", "jade-brunch": "> 1.0 < 1.8", From c6caafb7cdc09bec62ecf524bd192c47721b26c1 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Fri, 25 Sep 2015 13:58:17 -0700 Subject: [PATCH 24/26] Remove extra Stripe call in sub prepaid redeem --- server/payments/subscription_handler.coffee | 36 +++++++-------------- test/server/functional/prepaid.spec.coffee | 4 ++- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/server/payments/subscription_handler.coffee b/server/payments/subscription_handler.coffee index 0c9fcf82a..096b0bf9c 100644 --- a/server/payments/subscription_handler.coffee +++ b/server/payments/subscription_handler.coffee @@ -204,42 +204,29 @@ class SubscriptionHandler extends Handler if err @logSubscriptionError(req.user, "Redeem Prepaid Code find: #{JSON.stringify(err)}") return @sendDatabaseError(res, err) - - return @sendForbiddenError(res) if prepaid is null + unless prepaid + @logSubscriptionError(req.user, "Could not find prepaid code #{req.body.ppc}") + return @sendForbiddenError(res) oldRedeemers = prepaid.get('redeemers') ? [] return @sendForbiddenError(res) if oldRedeemers.length >= prepaid.get('maxRedeemers') - months = parseInt(prepaid.get('properties')?.months) return @sendForbiddenError(res) if isNaN(months) or months < 1 - for redeemer in oldRedeemers return @sendForbiddenError(res) if redeemer.userID.equals(req.user._id) customerID = req.user.get('stripe')?.customerID + subscriptionID = req.user.get('stripe')?.subscriptionID + findStripeSubscription customerID, subscriptionID: subscriptionID, (subscription) => + stripeSubscriptionPeriodEndDate = new Date(subscription.current_period_end * 1000) if subscription - unless customerID - @redeemCode(req, res, oldRedeemers, months) - else - stripe.customers.retrieve customerID, (err, customer) => + @cancelSubscriptionImmediately req.user, subscription, (err) => if err - @logSubscriptionError(req.user, "Redeem Prepaid Code get customer: #{JSON.stringify(err)}") + @logSubscriptionError(user, "Redeem Prepaid Code Stripe cancel subscription error: #{JSON.stringify(err)}") return @sendDatabaseError(res, err) + @redeemPrepaidCode(req, res, oldRedeemers, months, stripeSubscriptionPeriodEndDate) - findStripeSubscription customer.id, subscriptionID: req.user.get('stripe')?.subscriptionID, (subscription) => - stripeSubscriptionPeriodEndDate = null - if subscription - stripeSubscriptionPeriodEndDate = new Date(subscription.current_period_end * 1000) - - - @cancelSubscriptionImmediately req.user, subscription, (err) => - if err - @logSubscriptionError(user, "Redeem Prepaid Code Stripe cancel subscription error: #{JSON.stringify(err)}") - return @sendDatabaseError(res, err) - - @redeemCode(req, res, oldRedeemers, months, stripeSubscriptionPeriodEndDate) - - redeemCode: (req, res, oldRedeemers, months, startDate=null) => + redeemPrepaidCode: (req, res, oldRedeemers, months, startDate=null) => return @sendForbiddenError(res) unless req.user? return @sendForbiddenError(res) unless req.body?.ppc return @sendForbiddenError(res) unless oldRedeemers @@ -257,9 +244,8 @@ class SubscriptionHandler extends Handler return @sendNotFoundError(res, "Error while updating prepaid redeemer") if num isnt 1 - # Add terminal subscription to User, extending existing subscriptions - # TODO: refactor this into some form useable by both this and purchaseYearSale? + # TODO: refactor this into some form useable by both this and purchaseYearSale stripeInfo = _.cloneDeep(req.user.get('stripe') ? {}) endDate = new moment() if startDate diff --git a/test/server/functional/prepaid.spec.coffee b/test/server/functional/prepaid.spec.coffee index e2dfd6bc4..a29600ade 100644 --- a/test/server/functional/prepaid.spec.coffee +++ b/test/server/functional/prepaid.spec.coffee @@ -199,6 +199,7 @@ describe '/db/prepaid', -> expect(prepaid.type).toEqual('terminal_subscription') expect(prepaid.code).toBeDefined() # Saving this code for later tests + # TODO: don't make tests dependent on each other joeCode = prepaid.code expect(prepaid.creator).toBeDefined() expect(prepaid.maxRedeemers).toEqual(3) @@ -264,6 +265,8 @@ describe '/db/prepaid', -> expect(prepaid.properties?.months).toEqual(3) done() + # TODO: Move redeem subscription prepaid code tests to subscription tests file + it 'Creator can redeeem a prepaid code', (done) -> loginJoe (joe) -> expect(joeCode).not.toBeNull() @@ -357,4 +360,3 @@ describe '/db/prepaid', -> expect(res.statusCode).not.toEqual(200) done() # TODO: add a bunch of parallel tests trying to redeem a code with a high maxRedeemers (50?) to see what happens - From f9b9ac8f8636a9ff96b82212411e58de0dae46d0 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Fri, 25 Sep 2015 15:19:44 -0700 Subject: [PATCH 25/26] Fix error in playing victory sound --- app/views/play/level/modal/HeroVictoryModal.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee index cb88577e3..b3615fc45 100644 --- a/app/views/play/level/modal/HeroVictoryModal.coffee +++ b/app/views/play/level/modal/HeroVictoryModal.coffee @@ -222,9 +222,10 @@ module.exports = class HeroVictoryModal extends ModalView initializeAnimations: -> if @level.get('type', true) is 'hero' @updateXPBars 0 + #playVictorySound = => @playSound 'victory-title-appear' # TODO: actually add this @$el.find('#victory-header').delay(250).queue(-> $(@).removeClass('out').dequeue() - @playSound 'victory-title-appear' # TODO: actually add this + #playVictorySound() ) complete = _.once(_.bind(@beginSequentialAnimations, @)) @animatedPanels = $() From 5eadd926b364de4f2522426edcd25302682ce4c6 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Fri, 25 Sep 2015 15:21:48 -0700 Subject: [PATCH 26/26] Move redeem prepaid code UI out of modal --- app/templates/account/prepaid-view.jade | 23 ++++++--- app/views/account/PrepaidView.coffee | 69 +++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/app/templates/account/prepaid-view.jade b/app/templates/account/prepaid-view.jade index 7db33fabf..bdc15cbe3 100644 --- a/app/templates/account/prepaid-view.jade +++ b/app/templates/account/prepaid-view.jade @@ -21,7 +21,7 @@ block content .panel-title a(data-toggle="collapse" href="#purchasepanel") span(data-i18n="account_prepaid.purchase_code") - .panel-collapse.collapse(class=ppc ? "": "in")#purchasepanel + .panel-collapse.collapse(class=ppcQuery ? "": "in")#purchasepanel .panel-body p Subscription Codes can be redeemed to add premium subscription time to one or more Code Combat accounts. p Each Code Combat account can only redeem a particular Subscription Code once. @@ -50,12 +50,19 @@ block content span(data-i18n="account_prepaid.redeem_codes") .panel-collapse.collapse.in#redeempanel .panel-body - .form-horizontal - .form-group - label.control-label.col-md-2(for="ppc") Code: - .col-md-10 - input#ppc.form-control(name="ppc", type="text", value="#{ppc}" required) - button#redeem-button.btn.btn-success.pull-right View Code Details + p + span.spr Prepaid Code: + input.input-ppc(name="ppc", type="text", value="#{ppc}", required) + if ppcInfo && ppcInfo.length > 0 + p + each info in ppcInfo + div + != info + p + span.spr + button.btn.btn-info.btn-check-code Lookup prepaid code + span + button.btn.btn-success.btn-redeem-code Apply to your account .row .col-md-12 .panel.panel-default @@ -96,4 +103,4 @@ block content else td Redeemed else - p No codes yet! \ No newline at end of file + p No codes yet! diff --git a/app/views/account/PrepaidView.coffee b/app/views/account/PrepaidView.coffee index 893e15a74..f18bc2e99 100644 --- a/app/views/account/PrepaidView.coffee +++ b/app/views/account/PrepaidView.coffee @@ -8,6 +8,7 @@ utils = require 'core/utils' RedeemModal = require 'views/account/PrepaidRedeemModal' forms = require 'core/forms' +# TODO: remove redeem code modal module.exports = class PrepaidView extends RootView id: 'prepaid-view' @@ -19,6 +20,8 @@ module.exports = class PrepaidView extends RootView 'change #months': 'onMonthsChanged' 'click #purchase-button': 'onPurchaseClicked' 'click #redeem-button': 'onRedeemClicked' + 'click .btn-check-code': 'onClickCheckCode' + 'click .btn-redeem-code': 'onClickRedeemCode' subscriptions: 'stripe:received-token': 'onStripeReceivedToken' @@ -38,15 +41,20 @@ module.exports = class PrepaidView extends RootView @render?() @codes.on 'sync', (code) => @render?() - @supermodel.loadCollection(@codes, 'prepaid', {cache: false}) + @ppc = utils.getQueryVariable('_ppc') ? '' + unless _.isEmpty(@ppc) + @ppcQuery = true + @loadPrepaid(@ppc) getRenderData: -> c = super() c.purchase = @purchase c.codes = @codes c.ppc = @ppc + c.ppcInfo = @ppcInfo ? [] + c.ppcQuery = @ppcQuery ? false c afterRender: -> @@ -60,7 +68,6 @@ module.exports = class PrepaidView extends RootView @purchase.total = getPrepaidCodeAmount(@baseAmount, @purchase.users, @purchase.months) @renderSelectors("#total", "#users", "#months") - # Form Input Callbacks onUsersChanged: (e) -> newAmount = $(e.target).val() @@ -135,15 +142,16 @@ module.exports = class PrepaidView extends RootView data: { ppc: @ppc } options.error = (model, res, options, foo) => - console.error 'FAILED redeeming prepaid code' + # console.error 'FAILED redeeming prepaid code' msg = model.responseText ? '' @statusMessage "Error: Could not redeem prepaid code. #{msg}", "error" options.success = (model, res, options) => - console.log 'SUCCESS redeeming prepaid code' + # console.log 'SUCCESS redeeming prepaid code' @statusMessage "Prepaid Code Redeemed!", "success" @supermodel.loadCollection(@codes, 'prepaid', {cache: false}) @codes.fetch() + me.fetch cache: false @supermodel.addRequestResource('subscribe_prepaid', options, 0).load() @@ -171,9 +179,60 @@ module.exports = class PrepaidView extends RootView # Not sure when this will happen. Stripe popup seems to give appropriate error messages. options.success = (model, response, options) => - console.log 'SUCCESS: Prepaid purchase', model.code + # console.log 'SUCCESS: Prepaid purchase', model.code @statusMessage "Successfully purchased Prepaid Code!", "success" @codes.add(model) @statusMessage "Finalizing purchase...", "information" @supermodel.addRequestResource('purchase_prepaid', options, 0).load() + + loadPrepaid: (ppc) -> + return unless ppc + options = + cache: false + method: 'GET' + url: "/db/prepaid/-/code/#{ppc}" + + options.success = (model, res, options) => + @ppcInfo = [] + if model.get('type') is 'terminal_subscription' + months = model.get('properties')?.months ? 0 + maxRedeemers = model.get('maxRedeemers') ? 0 + redeemers = model.get('redeemers') ? [] + unlocksLeft = maxRedeemers - redeemers.length + @ppcInfo.push "This prepaid code adds #{months} months of subscription to your account." + @ppcInfo.push "It can be used #{unlocksLeft} more times." + # TODO: user needs to know they can't apply it more than once to their account + else + @ppcInfo.push "Type: #{model.get('type')}" + @render?() + options.error = (model, res, options) => + @statusMessage "Unable to retrieve code.", "error" + + @prepaid = new Prepaid() + @prepaid.fetch(options) + + onClickCheckCode: (e) -> + @ppc = $('.input-ppc').val() + unless @ppc + @statusMessage "You must enter a code.", "error" + return + @ppcInfo = [] + @render?() + @loadPrepaid(@ppc) + + onClickRedeemCode: (e) -> + @ppc = $('.input-ppc').val() + options = + url: '/db/subscription/-/subscribe_prepaid' + method: 'POST' + data: { ppc: @ppc } + options.error = (model, res, options, foo) => + msg = model.responseText ? '' + @statusMessage "Error: Could not redeem prepaid code. #{msg}", "error" + options.success = (model, res, options) => + @statusMessage "Prepaid applied to your account!", "success" + @codes.fetch cache: false + me.fetch cache: false + @loadPrepaid(@ppc) + @supermodel.addRequestResource('subscribe_prepaid', options, 0).load()