Merge branch 'master' into production

This commit is contained in:
Matt Lott 2015-08-23 17:14:04 -07:00
commit 36e337495d
25 changed files with 657 additions and 203 deletions

View file

@ -24,6 +24,7 @@ module.exports = class CocoRouter extends Backbone.Router
'account/profile': go('EmployersView') # Show the not-recruiting-now screen
'account/payments': go('account/PaymentsView')
'account/subscription': go('account/SubscriptionView')
'account/subscription/sale': go('account/SubscriptionSaleView')
'account/invoices': go('account/InvoicesView')
'admin': go('admin/MainAdminView')
@ -59,7 +60,7 @@ module.exports = class CocoRouter extends Backbone.Router
'courses': go('courses/mock1/CoursesView')
'courses/mock1': go('courses/mock1/CoursesView')
'courses/mock1/enroll': go('courses/mock1/CourseEnrollView')
'courses/mock1/enroll/:courseID': go('courses/mock1/CourseEnrollView')
'courses/mock1/:courseID': go('courses/mock1/CourseDetailsView')
'courses/mock1/:courseID/info': go('courses/mock1/CourseInfoView')

View file

@ -5,7 +5,7 @@ module.exports = nativeDescription: "български език", englishDescri
no_mobile: "CodeCombat не е направен за мобилни устройства и може да не работи с тях!" # Warning that shows up on mobile devices
play: "Играй" # The big play button that opens up the campaign view.
old_browser: "О, не! Браузърът ти е твърде стар за CodeCombat. Съжалявам!" # Warning that shows up on really old Firefox/Chrome/Safari
old_browser_suffix: "Все пак можеш да опиваш, но най-вероятно няма да проработи."
old_browser_suffix: "Все пак можеш да опиташ, но най-вероятно няма да проработи."
ipad_browser: "Лошa новинa: CodeCombat не работи в браузъра на iPad. Добра новина: Приложението ни за iPad изчаква одобрение от Apple."
campaign: "Кампания"
for_beginners: "За начинаещи"
@ -42,8 +42,8 @@ module.exports = nativeDescription: "български език", englishDescri
diplomat_suggestion:
title: "Помогни да преведем CodeCombat!" # This shows up when a player switches to a non-English language using the language selector.
sub_heading: "Имаме нужда от твоите езикови познания!"
pitch_body: "We develop CodeCombat in English, but we already have players all over the world. Many of them want to play in Bulgarian but don't speak English, so if you can speak both, please consider signing up to be a Diplomat and help translate both the CodeCombat website and all the levels into Bulgarian."
missing_translations: "Until we can translate everything into Bulgarian, you'll see English when Bulgarian isn't available."
pitch_body: "Ние разработихме CodeCombat на английски, но имаме играчи от целият свят. Много от тях искат да играят на български, защото не знаят английски, така че ако знаете и двата езика можете да се регистрирате като Дипломат и да помогнете в българският превод както на сайта на CodeCombat, така и на всички нива."
missing_translations: "До момента, в който всичко бъде преведено на български, някои неща ще са на английски."
learn_more: "Научи повече за това как да станеш Дипломат"
subscribe_as_diplomat: "Стани дипломат"
@ -317,28 +317,28 @@ module.exports = nativeDescription: "български език", englishDescri
tip_talk_is_cheap: "Приказките са вятър и мъгла. Покажи ми кода. - Линус Торвалдс"
tip_first_language: "Най-пагубното нещо, което можеш да научиш е първият ти език за програмиране. - Alan Kay"
tip_hardware_problem: "Въпр.: Колко програмиста са нужни, за да сменят електрическа крушка? Отг.: Николко, това е хардуерен проблем."
# tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law."
# tip_premature_optimization: "Premature optimization is the root of all evil. - Donald Knuth"
# tip_brute_force: "When in doubt, use brute force. - Ken Thompson"
# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..."
# tip_superpower: "Coding is the closest thing we have to a superpower."
# tip_control_destiny: "In real open source, you have the right to control your own destiny. - Linus Torvalds"
# tip_no_code: "No code is faster than no code."
# tip_code_never_lies: "Code never lies, comments sometimes do. — Ron Jeffries"
# tip_reusable_software: "Before software can be reusable it first has to be usable."
# tip_optimization_operator: "Every language has an optimization operator. In most languages that operator is //"
# tip_lines_of_code: "Measuring programming progress by lines of code is like measuring aircraft building progress by weight. — Bill Gates"
# 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_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_hofstadters_law: "Закон на Хофщадтер: Всяка работа продължава повече от колкото се очаква, дори когато законът на Хофщадтер е взет в предвид."
tip_premature_optimization: "Прибързаната оптимизиция е коренът на всяко зло. - Donald Knuth"
tip_brute_force: "Когато се съмняваш, използвай груба сила. - Ken Thompson"
tip_extrapolation: "Има само два типа хора: тези, които могат да екстраполират от непълни данни..."
tip_superpower: "Програмирането е най-голямото ни доближаване до суперсилата."
tip_control_destiny: "При истинския отворен код всеки има правото да контролира собствената си съдба. - Линус Торвалдс"
tip_no_code: "Няма по-бърз код от отсъстващия."
tip_code_never_lies: "Кодът никога не лъже, коментарите - понякога. — Ron Jeffries"
tip_reusable_software: "Преди кодът да стане преизползваем, той трябва да е използваем."
tip_optimization_operator: "Всеки език има оптимизационен оператор. В повечето езици операторът е //"
tip_lines_of_code: "Да мериш работата на програмиста по брой редове е все едно да мериш построяването на самолет по теглото му. — Бил Гейтс"
tip_source_code: "Искам да променя света, но едва ли ще ми дадат сорсовете."
tip_javascript_java: "Java е за JavaScript това, което са компютрите за компотите. - Chris Heilmann (перифразирано)"
tip_move_forward: "Каквото и да правиш, продължавай напред. - Мартин Лутър Кинг Мл."
tip_google: "Имаш проблем, който не можеш да решиш? Гугълни го!"
tip_adding_evil: "Добавяме щипка зло."
tip_hate_computers: "Какво да ви кажа за хората, които си мислят, че мразят компютрите. Всъщност те мразят скапаните програмисти. - Лари Нивън"
tip_open_source_contribute: "И Вие можете да помогнете за подобряването на CodeCombat!"
tip_recurse: "Итерациите са човещина, рекурсиите са божествени. - L. Peter Deutsch"
tip_free_your_mind: "Трябва да им позволиш да си отидат Нео. Страхът, съмнението и липсата на вяра. Освободи съзнанието си. - Морфеус"
tip_strong_opponents: "Дори най-силният противник си има слабости. Винаги. - Itachi Uchiha"
tip_paper_and_pen: "Преди да почнеш с програмирането, винаги започни с планиране на хартия."
game_menu:
inventory_tab: "Инвентар"
@ -347,19 +347,19 @@ module.exports = nativeDescription: "български език", englishDescri
guide_tab: "Упътване"
guide_video_tutorial: "Видео Упътване"
guide_tips: "Съвети"
# multiplayer_tab: "Multiplayer"
multiplayer_tab: "Мултиплейър"
auth_tab: "Записване"
# inventory_caption: "Equip your hero"
inventory_caption: "Екипирай героя си"
choose_hero_caption: "Избери герой, език"
# save_load_caption: "... and view history"
save_load_caption: "... и вижте историята"
options_caption: "Промени настройките"
# guide_caption: "Docs and tips"
guide_caption: "Документи и съвети"
multiplayer_caption: "Играй с приятели!"
auth_caption: "Запиши напредъка си."
leaderboard:
# leaderboard: "Leaderboard"
view_other_solutions: "Виж други решения" # {change}
leaderboard: "Таблица на лидерите"
view_other_solutions: "Виж решенията на лидерите"
scores: "Точки"
top_players: "ТОП играчи според"
day: "Днес"
@ -371,18 +371,18 @@ module.exports = nativeDescription: "български език", englishDescri
difficulty: "Трудност"
gold_collected: "Събрано Злато"
# inventory:
# choose_inventory: "Equip Items"
# equipped_item: "Equipped"
# required_purchase_title: "Required"
# available_item: "Available"
# restricted_title: "Restricted"
# should_equip: "(double-click to equip)"
# equipped: "(equipped)"
# locked: "(locked)"
# restricted: "(restricted in this level)"
# equip: "Equip"
# unequip: "Unequip"
inventory:
choose_inventory: "Избери предмети"
equipped_item: "Избрано"
required_purchase_title: "Задължително"
available_item: "Достъпно"
restricted_title: "Недостъпно"
should_equip: "(двоен клик за обличане)"
equipped: "(облечено)"
locked: "(заключено)"
restricted: "(забранено за това ниво)"
equip: "Облечи"
unequip: "Съблечи"
buy_gems:
few_gems: "Няколко скъпоценни камъни"
@ -393,9 +393,9 @@ module.exports = nativeDescription: "български език", englishDescri
retrying: "Грешка в сървъра, пробвам отново."
prompt_title: "Недостатъчно скъпоценни камъни"
prompt_body: "Искате ли още?"
# prompt_button: "Enter Shop"
# recovered: "Previous gems purchase recovered. Please refresh the page."
# price: "x3500 / mo"
prompt_button: "Влез в Магазина"
recovered: "Предишните покупки на скъпоценни камъни са възстановени. Моля опреснете страницата."
price: "x3500 / месец"
subscribe:
# comparison_blurb: "Sharpen your skills with a CodeCombat subscription!"

View file

@ -437,7 +437,18 @@
payment_methods_title: "Accepted Payment Methods"
payment_methods_blurb1: "We currently accept credit cards and Alipay."
payment_methods_blurb2: "If you require an alternate form of payment, please contact"
sale_already_subscribed: "You're already subscribed!"
sale_blurb1: "Save 35%"
sale_blurb2: "off regular subscription price!"
sale_button: "Sale!"
sale_button_title: "Save 35% when you purchase a 1 year subscription"
sale_click_here: "Click Here"
sale_continue: "Ready to continue adventuring?"
sale_paid: "Payment received. Thanks!"
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."
@ -446,7 +457,7 @@
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"
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.)"
@ -1102,6 +1113,7 @@
no_recent_games: "No games played during the past two weeks."
payments: "Payments"
purchased: "Purchased"
sale: "Sale"
subscription: "Subscription"
invoices: "Invoices"
service_apple: "Apple"

View file

@ -247,7 +247,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
victory_saving_progress: "保存进度"
victory_go_home: "返回主页"
victory_review: "给我们反馈!"
# victory_review_placeholder: "How was the level?"
victory_review_placeholder: "关卡如何?"
victory_hour_of_code_done: "你完成了吗?"
victory_hour_of_code_done_yes: "是的, 完成了!"
victory_experience_gained: "获得经验"
@ -294,7 +294,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
tip_scrub_shortcut: "用 Ctrl+[ 和 Ctrl+] 来倒退和快进。" # {change}
tip_guide_exists: "点击页面上方的指南, 可以获得更多有用信息。"
tip_open_source: "「CodeCombat」是100%开源的!"
# tip_tell_friends: "Enjoying CodeCombat? Tell your friends about us!"
tip_tell_friends: "喜欢Codecombat那就赶快把它安利给朋友"
tip_beta_launch: "CodeCombat开始于2013的10月份。"
tip_think_solution: "思考如何解决, 而不是思考问题。"
tip_theory_practice: "在理论上,理论和实践之间是没有区别的。但在实践上,它们是有区别的。 - Yogi Berra"
@ -336,7 +336,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
tip_hate_computers: "那些认为他们讨厌电脑的人,其实他们讨厌的是垃圾程序编写员。- Larry Niven"
tip_open_source_contribute: "你可以帮助「CodeCombat」提高"
tip_recurse: "迭代为人,递归为神 - L. Peter Deutsch"
tip_free_your_mind: "丢掉一切私心杂念,丢掉害怕、疑问和拒信,解放你的思想。 - Morpheus"
tip_free_your_mind: "丢掉一切私心杂念,丢掉害怕、疑问和拒信,解放你的思想。 - Morpheus黑客帝国"
tip_strong_opponents: "即使是最强大的对手也是有弱点的. - Itachi Uchiha"
# tip_paper_and_pen: "Before you start coding, you can always plan with a sheet of paper and a pen."
@ -960,7 +960,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
artisan_join_step1: "阅读文档。"
artisan_join_step2: "创建一个新关卡 以及探索已经存在的关卡。"
artisan_join_step3: "来我们的HipChat聊天室寻求帮助。"
artisan_join_step4: "你的关卡发到论坛让别人给你评价。"
artisan_join_step4: "你的关卡发到论坛让别人给你评价。"
artisan_subscribe_desc: "通过电子邮件获得关卡编辑器更新和公告。"
adventurer_introduction: "现在就来了解你的角色吧:你是一辆战车,并将要承担沉重的攻击。我们需要人来尝试下我们新开的关卡以了解怎么样才能使每一样东西更美好。一开始虽然会非常辛苦;可是制造出一个最好的游戏是一个很长的过程,在这个过程中,没有人可以第一次就成功。如果你挺过去了并且自我感觉良好,那么这个位置就是为你而准备的。"
adventurer_attribute_1: "学习的冲动!你想要学好怎么编程,与此同时我们也想要教你怎么编程。虽然你可能会觉得你大多数时间你反而都在教导。这,就是学习。"

View file

@ -79,8 +79,8 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
adjust_volume: "調整音量"
campaign_multiplayer: "多人競技場"
campaign_multiplayer_description: "...在這裡您可以和其他玩家進行對戰。"
# campaign_old_multiplayer: "(Deprecated) Old Multiplayer Arenas"
# campaign_old_multiplayer_description: "Relics of a more civilized age. No simulations are run for these older, hero-less multiplayer arenas."
campaign_old_multiplayer: "(過時的)舊多人競技場"
campaign_old_multiplayer_description: "多個文明時代的遺跡。已沒有模擬運行這些陳舊、蕪絕英雄多人競技場。"
share_progress_modal:
blurb: "您正在建立優秀的進度! 告訴別人您已經從CodeCombat學習到多少東西." # {change}
@ -147,7 +147,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
unwatch: "取消關注"
submit_patch: "送出修補"
submit_changes: "送出修改"
# save_changes: "Save Changes"
save_changes: "保存更改"
general:
and: ""
@ -284,9 +284,9 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
time_goto: "前往:"
non_user_code_problem_title: "無法加載關卡"
infinite_loop_title: "檢測到無限循環"
# 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)"
infinite_loop_description: "建立世界的初始代碼還沒有運行完畢。這可能是真的很慢或出現無限循環或者存在一個bug。你可以嘗試再次運行這段代碼或重置代碼為默認狀態。如果還是解決不了問題請聯繫我們。."
check_dev_console: "你也可以打開開發者界面看一下有什麼可能出錯了。"
check_dev_console_link: "(說明)"
infinite_loop_try_again: "再試一次"
infinite_loop_reset_level: "重置關卡"
infinite_loop_comment_out: "在我的程式碼中加入注解"
@ -294,7 +294,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
tip_scrub_shortcut: "Ctrl+[ 快退; Ctrl+] 快進." # {change}
tip_guide_exists: "點擊頁面上方的指南,可獲得更多有用的訊息."
tip_open_source: "「CodeCombat」100% 開源!"
# tip_tell_friends: "Enjoying CodeCombat? Tell your friends about us!"
tip_tell_friends: "喜歡Codecombat那就把它介紹給朋友!"
tip_beta_launch: "「CodeCombat」在2013年10月進入 BETA 測試。"
tip_think_solution: "思考解決方法而不是問題."
tip_theory_practice: "理論上, 理論和實作之間是沒有區別. 但是實作上, 這兩者是有區別的. - Yogi Berra"
@ -335,8 +335,8 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
tip_adding_evil: "增加一個邪惡之捏."
tip_hate_computers: "關於自我覺得恨透電腦的那群人. 其實他們真正應該恨的事情是糟糕的程序員. - Larry Niven"
tip_open_source_contribute: "你可以幫助「CodeCombat」提高"
# 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_recurse: "迭代者人也,遞歸者神也 - L. Peter Deutsch"
tip_free_your_mind: "放下一切私心雜念,丟棄害怕、疑問和拒信,解放你的思維。 - 莫菲斯(駭客任務)"
# 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."

View file

@ -137,14 +137,17 @@ module.exports = class User extends CocoModel
return 0 unless numVideos > 0
return me.get('testGroupNumber') % numVideos
isPremium: ->
return true if me.isInGodMode()
return true if me.isAdmin()
hasSubscription: ->
return false unless stripe = @get('stripe')
return true if stripe.sponsorID
return true if stripe.subscriptionID
return true if stripe.free is true
return true if _.isString(stripe.free) and new Date() < new Date(stripe.free)
isPremium: ->
return true if me.isInGodMode()
return true if me.isAdmin()
return true if me.hasSubscription()
return false
tiersByLevel = [-1, 0, 0.05, 0.14, 0.18, 0.32, 0.41, 0.5, 0.64, 0.82, 0.91, 1.04, 1.22, 1.35, 1.48, 1.65, 1.78, 1.96, 2.1, 2.24, 2.38, 2.55, 2.69, 2.86, 3.03, 3.16, 3.29, 3.42, 3.58, 3.74, 3.89, 4.04, 4.19, 4.32, 4.47, 4.64, 4.79, 4.96,

View file

@ -0,0 +1,7 @@
#subscription-sale-view
.center
text-align: center
.sale-blurb
font-size: 22px
#pay-button
font-size: 18px

View file

@ -23,7 +23,6 @@
font-size: 9pt
font-weight: normal
border: 1px solid gray
border-radius: 5px
margin: 0px
padding: 2px
background-color: white
@ -62,10 +61,10 @@
cursor: default
display: inline-block
white-space: nowrap
font-size: 9pt
font-size: 12px
line-height: 12px
font-weight: normal
border: 1px solid gray
border-radius: 5px
margin: 0px
padding: 2px
@ -82,6 +81,9 @@
font-weight: normal
font-size: 14px
.student-cell
width: 150px
.progress-cell
padding: 2px
padding-bottom: 10px
@ -111,9 +113,9 @@
.progress-level-cell
display: inline-block
white-space: nowrap
font-size: 9pt
font-size: 12px
line-height: 12px
border: 1px solid gray
border-radius: 5px
margin: 0px
padding: 2px
@ -128,9 +130,9 @@
.progress-concept-cell
display: inline-block
white-space: nowrap
font-size: 9pt
font-size: 12px
line-height: 12px
border: 1px solid gray
border-radius: 5px
margin: 0px
padding: 2px
@ -139,3 +141,6 @@
.progress-concept-cell-complete
background-color: lightgray
.condense-progress
width: 100%

View file

@ -1,8 +1,5 @@
#courses-view
.btn-continue
margin-top: 40px
.center
text-align: center

View file

@ -152,7 +152,7 @@
.purchase-button
position: absolute
right: 24px
width: 400px
width: 300px
height: 70px
top: 430px
font-size: 32px
@ -190,6 +190,24 @@
border-width: 14px 20px 20px 20px
color: darken(white, 5%)
//- Sale button
.sale-button
position: absolute
left: 290px
width: 115px
height: 70px
top: 430px
font-size: 32px
line-height: 42px
border-style: solid
border-image: url(/images/common/button-background-primary-active.png) 14 20 20 20 fill round
border-width: 14px 20px 20px 20px
color: darken(white, 5%)
span
pointer-events: none
.email-parent-form
.email_invalid
color: red

View file

@ -0,0 +1,54 @@
extends /templates/base
block content
ol.breadcrumb
li
a(href="/")
span.glyphicon.glyphicon-home
li
a(href="/account", data-i18n="nav.account")
li
a(href="/account/subscription", data-i18n="account.subscription")
li.active(data-i18n="account.sale") Sale
if me.get('anonymous')
p(data-i18n="account_settings.not_logged_in")
else if me.hasSubscription()
h1(data-i18n="subscribe.sale_already_subscribed")
span.spr(data-i18n="subscribe.sale_continue")
a(href="/play", data-i18n="subscribe.sale_click_here")
else
if state === 'purchasing'
.alert.alert-info(data-i18n="account_invoices.purchasing")
else
h1.center(data-i18n="subscribe.sale_title")
br
p.center.sale-blurb
strong.spr(data-i18n="subscribe.sale_blurb1")
span(data-i18n="subscribe.sale_blurb2")
br
if !state || state !== 'invoice_paid'
p.center
button.btn.btn-success#pay-button #{payButtonText}
br
else if state === 'invoice_paid'
div.center
span.spr(data-i18n="subscribe.sale_continue")
a(href="/play", data-i18n="subscribe.sale_click_here")
br
#declined-alert.alert.alert-success.alert-dismissible
button.close(type="button" data-dismiss="alert")
span(aria-hidden="true") &times;
p(data-i18n="subscribe.sale_paid")
if state === 'declined'
#declined-alert.alert.alert-danger.alert-dismissible
span(data-i18n="account_invoices.declined")
button.close(type="button" data-dismiss="alert")
span(aria-hidden="true") &times;
if state === 'unknown_error'
#error-alert.alert.alert-danger.alert-dismissible
button.close(type="button" data-dismiss="alert")
span(aria-hidden="true") &times;
p(data-i18n="loading_error.unknown")
p= stateMessage

View file

@ -106,7 +106,6 @@ block content
td= personalSub.card
else
button.start-subscription-button.btn.btn-lg.btn-success(data-i18n="subscribe.subscribe_title") Subscribe
if personalSub.free === true
div(data-i18n="subscribe.currently_free")
else if typeof personalSub.free === 'string'
@ -117,6 +116,8 @@ block content
else
span(data-i18n="subscribe.was_free_until")
span.spl.spr= moment(new Date(personalSub.free)).format('l')
else
button.start-subscription-button.btn.btn-lg.btn-success(data-i18n="subscribe.subscribe_title") Subscribe
//- Sponsored Subscriptions

View file

@ -132,6 +132,8 @@ block content
td
td
td
if !showMoreCancellations
button.btn.btn-sm.btn-show-more-cancellations Show More Cancellations
h2 Subscriptions
if !subs || subs.length < 1

View file

@ -80,8 +80,9 @@
#parents-info(data-i18n="subscribe.parents")
#payment-methods-info(data-i18n="subscribe.payment_methods")
button.btn.btn-lg.btn-illustrated.purchase-button(data-i18n="subscribe.subscribe_title")
button.btn.btn-lg.btn-illustrated.parent-button(data-i18n="subscribe.parent_button")
button.btn.btn-lg.btn-illustrated.sale-button(title="#{saleButtonTitle}", data-i18n="subscribe.sale_button")
button.btn.btn-lg.btn-illustrated.purchase-button(data-i18n="subscribe.subscribe_title")
if state === 'declined'
#declined-alert.alert.alert-danger.alert-dismissible

View file

@ -20,10 +20,12 @@ block content
span &times;
h3.modal-title Edit Class Settings
.modal-body
p This title will be displayed to everyone in the class.
p
strong Title
p
input.edit-name-input(type='text', value="#{instance.name}")
p This description will be displayed to everyone in the class.
p
strong Description
p
textarea.edit-description-input(rows=2)= instance.description
p Select programming languages available to the class:
@ -98,10 +100,10 @@ mixin progress-tab
td #{instance.students.length}
tr
td Average level play time:
td #{stats.averageLevelPlaytime} seconds
td= moment.duration(stats.averageLevelPlaytime, "seconds").humanize()
tr
td Total play time:
td #{stats.totalPlayTime} seconds
td= moment.duration(stats.totalPlayTime, "seconds").humanize()
tr
td Average levels completed:
td #{stats.averageLevelsCompleted}
@ -109,7 +111,7 @@ mixin progress-tab
td Total levels completed:
td #{stats.totalLevelsCompleted}
tr
td Last level completed:
td Furthest level completed:
td #{stats.lastLevelCompleted}
.col-md-6
h3 Concepts Covered
@ -145,51 +147,107 @@ mixin progress-tab
span.progress-key.progress-key-complete complete
span.progress-key.progress-key-started started
span.progress-key not started
if maxLastStartedIndex > 30
input.expand-progress-checkbox(type='checkbox')
span.spl.expand-progress-label(data-i18n="clans.exp_levels") Expand levels
input.expand-progress-checkbox(type='checkbox')
span.spl.expand-progress-label Expand details
tbody
each student in instance.students
tr
td
td.student-cell
a= student
div #{stats[student].levelsCompleted} levels completed
div #{moment.duration(stats[student].secondsPlayed, 'seconds').humanize()} played
div Played #{moment().subtract(stats[student].secondsLastPlayed, 'seconds').fromNow()}
td.progress-cell
.level-progression-concepts Concepts
each concept in courseConcepts
if userConceptsMap[student] && userConceptsMap[student][concept] === 'complete'
span.spr.progress-concept-cell.progress-concept-cell-complete(data-i18n="concepts." + concept)
else if userConceptsMap[student] && userConceptsMap[student][concept] === 'started'
span.spr.progress-concept-cell.progress-concept-cell-started(data-i18n="concepts." + concept)
else
span.spr.progress-concept-cell.progress-concept-cell-not-started(data-i18n="concepts." + concept)
if showExpandedProgress
.level-progression-concepts Concepts
each concept in courseConcepts
if userConceptsMap[student] && userConceptsMap[student][concept] === 'complete'
span.spr.progress-concept-cell.progress-concept-cell-complete(data-i18n="concepts." + concept)
else if userConceptsMap[student] && userConceptsMap[student][concept] === 'started'
span.spr.progress-concept-cell.progress-concept-cell-started(data-i18n="concepts." + concept)
else
span.spr.progress-concept-cell.progress-concept-cell-not-started(data-i18n="concepts." + concept)
.level-progression-levels Levels
- var i = 0
each level in course.levels
if userLevelStateMap[student][level] === 'complete'
span.progress-level-cell.progress-level-cell-complete #{i + 1}
if showExpandedProgress || i === 0 || i === course.levels.length - 1
.level-progression-levels Levels
- var i = 0
each level in course.levels
if userLevelStateMap[student][level] === 'complete'
span.progress-level-cell.progress-level-cell-complete #{i + 1}
span.spl= level.replace('Course: ', '')
.level-popup-container
h3 #{i + 1}. #{level.replace('Course: ', '')}
p
div
.level-popup-container
h3 #{i + 1}. #{level.replace('Course: ', '')}
p
- var playTime = Math.round(Math.random() * 600)
span Time to solve
span : #{playTime} seconds
div
span : #{moment.duration(playTime, "seconds").humanize()}
p
- var completionDate = new Date()
- completionDate.setUTCDate(completionDate.getUTCDate() - Math.round(Math.random() * 60))
span Completed on
span : #{moment(completionDate).format('MMMM Do YYYY, h:mm:ss a')}
strong(data-i18n="clans.view_solution") Click to view solution.
else if userLevelStateMap[student][level] === 'started'
span.progress-level-cell.progress-level-cell-started #{i + 1} #{level.replace('Course: ', '')}
else
span.progress-level-cell.level-progression-level-not-started #{i + 1}
if showExpandedProgress || i === 0
span.spl= level.replace('Course: ', '')
- i++
strong(data-i18n="clans.view_solution") Click to view solution.
else if userLevelStateMap[student][level] === 'started'
span.progress-level-cell.progress-level-cell-started #{i + 1} #{level.replace('Course: ', '')}
.level-popup-container
h3 #{i + 1}. #{level.replace('Course: ', '')}
p
- var completionDate = new Date()
- completionDate.setUTCDate(completionDate.getUTCDate() - Math.round(Math.random() * 60))
span Last played on
span : #{moment(completionDate).format('MMMM Do YYYY, h:mm:ss a')}
strong(data-i18n="clans.view_solution") Click to view solution.
else
span.progress-level-cell.level-progression-level-not-started #{i + 1} #{level.replace('Course: ', '')}
- i++
else
//- Condensed view
table
tbody
tr
td
.level-progression-concepts(style='margin:0px;') Concepts
td.condense-progress
each concept in courseConcepts
if userConceptsMap[student] && userConceptsMap[student][concept] === 'complete'
span.spr.progress-concept-cell.progress-concept-cell-complete(data-i18n="concepts." + concept)
else if userConceptsMap[student] && userConceptsMap[student][concept] === 'started'
span.spr.progress-concept-cell.progress-concept-cell-started(data-i18n="concepts." + concept)
else
break
tr
td
.level-progression-levels(style='margin:0px;') Levels
td.condense-progress
- var levelCellWidth = 100.00 / course.levels.length
- var i = 0
each level in course.levels
if userLevelStateMap[student][level] === 'complete'
span.progress-level-cell.progress-level-cell-complete(style="width:#{levelCellWidth}%;") #{i + 1}
.level-popup-container
h3 #{i + 1}. #{level.replace('Course: ', '')}
p
- var playTime = Math.round(Math.random() * 600)
span Time to solve
span : #{moment.duration(playTime, "seconds").humanize()}
p
- var completionDate = new Date()
- completionDate.setUTCDate(completionDate.getUTCDate() - Math.round(Math.random() * 60))
span Completed on
span : #{moment(completionDate).format('MMMM Do YYYY, h:mm:ss a')}
strong(data-i18n="clans.view_solution") Click to view solution.
else if userLevelStateMap[student][level] === 'started'
span.progress-level-cell.progress-level-cell-started(style="width:#{levelCellWidth}%;") #{i + 1}
.level-popup-container
h3 #{i + 1}. #{level.replace('Course: ', '')}
p
- var completionDate = new Date()
- completionDate.setUTCDate(completionDate.getUTCDate() - Math.round(Math.random() * 60))
span Last played on
span : #{moment(completionDate).format('MMMM Do YYYY, h:mm:ss a')}
strong(data-i18n="clans.view_solution") Click to view solution.
else
break
- i++
mixin levels-tab
table.table.table-striped.table-condensed

View file

@ -21,7 +21,7 @@ block content
.col-md-12
.well.well-sm
p
div.instruction-label Pick your class
div.instruction-label Pick from your current classes
.container-fluid
.row
.col-md-8
@ -33,44 +33,48 @@ block content
.row.button-row.center.row-pick-class
.col-md-12
div.or Or
.row.button-row
.col-md-12
.well.well-sm
p
div.instruction-label Enter an unlock code
.container-fluid
.row
.col-md-8
input.code-input(type='text', placeholder="Enter unlock code")
.col-md-4
button.btn.btn-success.btn-enroll Enroll
.row.button-row.center
.col-md-12
div.or Or
.row.button-row.center
.col-md-12
button.btn.btn-success.btn-lg.btn-buy Buy this course
if studentMode
.row.button-row
.col-md-12
.well.well-sm
p
div.instruction-label Enter an unlock code
.container-fluid
.row
.col-md-8
input.code-input(type='text', placeholder="Enter unlock code")
.col-md-4
button.btn.btn-success.btn-enroll Enroll
else
.row.button-row.center
.col-md-12
button.btn.btn-success.btn-lg.btn-buy Buy this course
if !studentMode
br
button.btn.btn-warning.btn-student Students Click Here
h1.center Courses on CodeCombat
.info-container
p Courses are designed to introduce computer science concepts using CodeCombat's fun and engaging environment. CodeCombat levels are organized around key topics to encourage progressive learning, over the course of 5 hours.
if !studentMode
.info-container
p Courses are designed to introduce computer science concepts using CodeCombat's fun and engaging environment. CodeCombat levels are organized around key topics to encourage progressive learning, over the course of 5 hours.
.container-fluid
.row
.col-md-6
ul
li Learn more in less time
li No coding experience necesssary
li Easily monitor student progress
div Purchase a course for your entire class. It's easy to sign up your students!
.col-md-6
.well.well-sm
div.praise-quote "#{praise.quote}"
div.caption-text - #{praise.source}
.container-fluid
.row
.col-md-6
ul
li Learn more in less time
li No coding experience necesssary
li Easily monitor student progress
div Purchase a course for your entire class. It's easy to sign up your students!
.col-md-6
.well.well-sm
div.praise-quote "#{praise.quote}"
div.caption-text - #{praise.source}
h2.center Choose Your Course:
h2.center Choose Your Course:
.container-fluid
- var i = 0
@ -98,5 +102,16 @@ mixin course-block(course, courseID)
each topic in course.topics
li= topic
strong Hours of content: #{course.duration}
.col-md-6.center
button.btn.btn-lg.btn-success.btn-continue(data-toggle='modal', data-target="#continueModal", data-course-title="#{course.title}", data-course-id="#{courseID}") #{course.unlocked ? 'Continue' : 'Enter'}
.col-md-6.center(style='margin-top: 40px;')
if !studentMode
if course.unlocked
button.btn.btn-lg.btn-success.btn-continue(data-toggle='modal', data-target="#continueModal", data-course-title="#{course.title}", data-course-id="#{courseID}") #{course.unlocked ? 'Continue' : 'Enter'}
else if course.title === 'Introduction to Computer Science'
button.btn.btn-lg.btn-success.btn-buy(data-course-id="#{courseID}") Get FREE course
else
button.btn.btn-lg.btn-success.btn-buy(data-course-id="#{courseID}") Buy course
else
if course.unlocked
a.btn.btn-lg.btn-success.btn-continue(href="/courses/mock1/#{courseID}?student=true") Continue
else
button.btn.btn-lg.btn-success.btn-continue(data-toggle='modal', data-target="#continueModal", data-course-title="#{course.title}", data-course-id="#{courseID}") Enter

View file

@ -0,0 +1,69 @@
RootView = require 'views/core/RootView'
template = require 'templates/account/subscription-sale-view'
stripeHandler = require 'core/services/stripe'
utils = require 'core/utils'
module.exports = class SubscriptionSaleView extends RootView
id: "subscription-sale-view"
template: template
yearSaleAmount: 7900
events:
'click #pay-button': 'onPayButton'
subscriptions:
'stripe:received-token': 'onStripeReceivedToken'
constructor: (options) ->
super(options)
@description = $.i18n.t('subscribe.stripe_description_year_sale')
displayAmount = (@yearSaleAmount / 100).toFixed(2)
@payButtonText = "#{$.i18n.t('subscribe.sale_view_button')} $#{displayAmount}"
getRenderData: ->
c = super()
c.payButtonText = @payButtonText
c.state = @state
c.stateMessage = @stateMessage
c
onPayButton: ->
@state = undefined
@stateMessage = undefined
@render()
# Show Stripe handler
application.tracker?.trackEvent 'Started sale landing page subscription purchase'
@timestampForPurchase = new Date().getTime()
stripeHandler.open
amount: @yearSaleAmount
description: @description
bitcoin: true
alipay: if me.get('chinaVersion') or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
onStripeReceivedToken: (e) ->
@state = 'purchasing'
@render?()
# Call year sale API
data =
stripe:
token: e.token.id
timestamp: @timestampForPurchase
jqxhr = $.post('/db/subscription/-/year_sale', data)
jqxhr.done (data, textStatus, jqXHR) =>
application.tracker?.trackEvent 'Finished sale landing page subscription purchase', value: @yearSaleAmount
me.set 'stripe', data?.stripe if data?.stripe?
@state = 'invoice_paid'
@stateMessage = undefined
@render?()
jqxhr.fail (xhr, textStatus, errorThrown) =>
console.error 'We got an error subscribing with Stripe from our server:', textStatus, errorThrown
application.tracker?.trackEvent 'Failed to finish 1 year subscription purchase', status: textStatus
if xhr.status is 402
@state = 'declined'
@stateMessage = arguments[2]
else
@state = 'unknown_error'
@stateMessage = "#{xhr.status}: #{xhr.responseText}"
@render?()

View file

@ -12,8 +12,12 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
template: template
targetSubCount: 1200
events:
'click .btn-show-more-cancellations': 'onClickShowMoreCancellations'
constructor: (options) ->
super options
@showMoreCancellations = false
@resetSubscriptionsData()
if me.isAdmin()
@refreshData()
@ -22,7 +26,8 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
getRenderData: ->
context = super()
context.analytics = @analytics ? graphs: []
context.cancellations = @cancellations ? []
context.cancellations = if @showMoreCancellations then @cancellations else (@cancellations ? []).slice(0, 40)
context.showMoreCancellations = @showMoreCancellations
context.subs = _.cloneDeep(@subs ? []).reverse()
context.subscribers = @subscribers ? []
context.subscriberCancelled = _.find context.subscribers, (subscriber) -> subscriber.cancel
@ -39,6 +44,10 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
super()
@updateAnalyticsGraphs()
onClickShowMoreCancellations: (e) ->
@showMoreCancellations = true
@render?()
resetSubscriptionsData: ->
@analytics = graphs: []
@subs = []
@ -93,7 +102,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
getCancellationEvents: (done) ->
cancellationEvents = []
earliestEventDate = new Date()
earliestEventDate.setUTCMonth(earliestEventDate.getUTCMonth() - 1)
earliestEventDate.setUTCMonth(earliestEventDate.getUTCMonth() - 2)
earliestEventDate.setUTCDate(earliestEventDate.getUTCDate() - 8)
nextBatch = (starting_after, done) =>
@updateFetchDataState "Fetching cancellations #{cancellationEvents.length}..."
@ -555,28 +564,15 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
## Cancelled
# TODO: move this average cancelled stuff up the chain
averageCancelled = 0
# Build line data
levelPoints = []
cancelled = []
for sub, i in @subs[@subs.length - 30...]
cancelled.push sub.cancelled
for sub, i in @subs
levelPoints.push
x: @subs.length - 30 + i
y: sub.cancelled
day: sub.day
pointID: "#{cancelledSubsID}#{@subs.length - 30 + i}"
values: []
averageCancelled = cancelled.reduce((a, b) -> a + b) / cancelled.length
for sub, i in @subs[0...-30]
levelPoints.splice i, 0,
x: i
y: averageCancelled
day: sub.day
pointID: "#{cancelledSubsID}#{i}"
values: []
# Ensure points for each day
for day, i in days
@ -608,10 +604,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
sevenNets = []
for sub, i in @subs
net = 0
if i >= @subs.length - 30
sevenNets.push sub.started - sub.cancelled
else
sevenNets.push sub.started - averageCancelled
sevenNets.push sub.started - sub.cancelled
if sevenNets.length > 7
sevenNets.shift()
if sevenNets.length is 7

View file

@ -12,6 +12,7 @@ module.exports = class SubscribeModal extends ModalView
product:
amount: 999
planID: 'basic'
yearAmount: 7900
subscriptions:
'stripe:received-token': 'onStripeReceivedToken'
@ -21,13 +22,16 @@ module.exports = class SubscribeModal extends ModalView
'click .popover-content .parent-send': 'onClickParentSendButton'
'click .email-parent-complete button': 'onClickParentEmailCompleteButton'
'click .purchase-button': 'onClickPurchaseButton'
'click .sale-button': 'onClickSaleButton'
constructor: (options) ->
super(options)
@state = 'standby'
@saleButtonTitle = $.i18n.t('subscribe.sale_button_title')
getRenderData: ->
c = super()
c.saleButtonTitle = @saleButtonTitle
c.state = @state
c.stateMessage = @stateMessage
c.price = @product.amount / 100
@ -137,21 +141,61 @@ module.exports = class SubscribeModal extends ModalView
#}
@purchasedAmount = options.amount
stripeHandler.open(options)
onClickSaleButton: (e) ->
@playSound 'menu-button-click'
return @openModalView new AuthModal() if me.get('anonymous')
application.tracker?.trackEvent 'Started 1 year subscription purchase'
options =
description: $.i18n.t('subscribe.stripe_description_year_sale')
amount: @product.yearAmount
alipay: if me.get('chinaVersion') or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
alipayReusable: true
@purchasedAmount = options.amount
stripeHandler.open(options)
onStripeReceivedToken: (e) ->
@state = 'purchasing'
@render()
stripe = _.clone(me.get('stripe') ? {})
stripe.planID = @product.planID
stripe.token = e.token.id
me.set 'stripe', stripe
@listenToOnce me, 'sync', @onSubscriptionSuccess
@listenToOnce me, 'error', @onSubscriptionError
me.patch({headers: {'X-Change-Plan': 'true'}})
if @purchasedAmount is @product.amount
stripe = _.clone(me.get('stripe') ? {})
stripe.planID = @product.planID
stripe.token = e.token.id
me.set 'stripe', stripe
@listenToOnce me, 'sync', @onSubscriptionSuccess
@listenToOnce me, 'error', @onSubscriptionError
me.patch({headers: {'X-Change-Plan': 'true'}})
else if @purchasedAmount is @product.yearAmount
# Purchasing a year
data =
stripe:
token: e.token.id
timestamp: new Date().getTime()
jqxhr = $.post('/db/subscription/-/year_sale', data)
jqxhr.done (data, textStatus, jqXHR) =>
application.tracker?.trackEvent 'Finished 1 year subscription purchase', value: @purchasedAmount
me.set 'stripe', data?.stripe if data?.stripe?
Backbone.Mediator.publish 'subscribe-modal:subscribed', {}
@playSound 'victory'
@hide()
jqxhr.fail (xhr, textStatus, errorThrown) =>
console.error 'We got an error subscribing with Stripe from our server:', textStatus, errorThrown
application.tracker?.trackEvent 'Failed to finish 1 year subscription purchase', status: textStatus, value: @purchasedAmount
stripe = me.get('stripe') ? {}
delete stripe.token
delete stripe.planID
if xhr.status is 402
@state = 'declined'
else
@state = 'unknown_error'
@stateMessage = "#{xhr.status}: #{xhr.responseText}"
@render()
else
console.error "Unexpected purchase amount received", @purchasedAmount, e
@state = 'unknown_error'
@stateMessage = "Uknown problem occurred while processing Stripe request"
onSubscriptionSuccess: ->
application.tracker?.trackEvent 'Finished subscription purchase', value: @purchasedAmount
@ -161,6 +205,7 @@ module.exports = class SubscribeModal extends ModalView
onSubscriptionError: (user, response, options) ->
console.error 'We got an error subscribing with Stripe from our server:', response
application.tracker?.trackEvent 'Failed to finish subscription purchase', status: options.xhr?.status, value: @purchasedAmount
stripe = me.get('stripe') ? {}
delete stripe.token
delete stripe.planID

View file

@ -1,4 +1,5 @@
app = require 'core/application'
utils = require 'core/utils'
RootView = require 'views/core/RootView'
template = require 'templates/courses/mock1/course-details'
CocoCollection = require 'collections/CocoCollection'
@ -20,6 +21,7 @@ module.exports = class CourseDetailsView extends RootView
constructor: (options, @courseID=0, @instanceID=0) ->
super options
@studentMode = utils.getQueryVariable('student', false) or options.studentMode
@initData()
destroy: ->
@ -37,8 +39,9 @@ module.exports = class CourseDetailsView extends RootView
context.memberSort = @memberSort
context.userConceptsMap = @userConceptsMap ? {}
context.userLevelStateMap = @userLevelStateMap ? {}
context.showExpandedProgress = @course.levels.length <= 30 or @showExpandedProgress
context.studentMode = @options.studentMode ? false
context.showExpandedProgress = @showExpandedProgress
context.stats = @stats
context.studentMode = @studentMode ? false
conceptsCompleted = {}
for user of context.userConceptsMap
@ -46,15 +49,6 @@ module.exports = class CourseDetailsView extends RootView
conceptsCompleted[concept] ?= 0
conceptsCompleted[concept]++
context.conceptsCompleted = conceptsCompleted
stats =
averageLevelPlaytime: _.random(30, 240)
averageLevelsCompleted: _.random(1, @course.levels.length)
stats.totalPlayTime = context.instance.students?.length * stats.averageLevelPlaytime ? 0
stats.totalLevelsCompleted = context.instance.students?.length * stats.averageLevelsCompleted ? 0
stats.lastLevelCompleted = @course.levels[@maxLastStartedIndex] ? @course.levels[@course.levels.length - 1]
context.stats = stats
context
initData: ->
@ -73,6 +67,10 @@ module.exports = class CourseDetailsView extends RootView
@levelMap = {}
@levelMap[level] = true for level in @course.levels
@userLevelStateMap = {}
@stats =
averageLevelPlaytime: _.random(30, 240)
averageLevelsCompleted: _.random(1, @course.levels.length)
students: {}
@maxLastStartedIndex = -1
for student in @instances?[@currentInstanceIndex].students
@userLevelStateMap[student] = {}
@ -82,7 +80,17 @@ module.exports = class CourseDetailsView extends RootView
lastStartedIndex = lastCompletedIndex + 1
@userLevelStateMap[student][@course.levels[lastStartedIndex]] = 'started'
@maxLastStartedIndex = lastStartedIndex if lastStartedIndex > @maxLastStartedIndex
@stats[student] ?= {}
@stats[student].levelsCompleted = 0
@stats[student].levelsCompleted++ for level in @course.levels when @userLevelStateMap[student][level] is 'complete'
@stats[student].secondsPlayed = Math.round(Math.random() * 1000 * (@stats[student].levelsCompleted + 1))
@stats[student].secondsLastPlayed = Math.round(Math.random() * 100000)
@sortMembers()
@stats.totalPlayTime = @instances?[@currentInstanceIndex].students?.length * @stats.averageLevelPlaytime ? 0
@stats.totalLevelsCompleted = @instances?[@currentInstanceIndex].students?.length * @stats.averageLevelsCompleted ? 0
@stats.totalPlayTime = @instances?[@currentInstanceIndex].students?.length * @stats.averageLevelPlaytime ? 0
@stats.lastLevelCompleted = @course.levels[0] ? @course.levels[@course.levels.length - 1]
sortMembers: ->
# Progress sort precedence: most completed concepts, most started concepts, most levels, name sort
@ -146,9 +154,9 @@ module.exports = class CourseDetailsView extends RootView
@render?()
onChangeStudent: (e) ->
@options.studentMode = $('.student-mode-checkbox').prop('checked')
@studentMode = $('.student-mode-checkbox').prop('checked')
@render?()
$('.student-mode-checkbox').attr('checked', @options.studentMode)
$('.student-mode-checkbox').attr('checked', @studentMode)
onExpandedProgressCheckbox: (e) ->
@showExpandedProgress = $('.expand-progress-checkbox').prop('checked')
@ -188,7 +196,7 @@ module.exports = class CourseDetailsView extends RootView
container = $(e.target).find('.level-popup-container').show()
margin = 20
offset = $(e.target).offset()
scrollTop = $(e.target).offsetParent().scrollTop()
scrollTop = $('#page-container').scrollTop()
height = container.outerHeight()
container.css('left', offset.left + e.offsetX)
container.css('top', offset.top + scrollTop - height - margin)

View file

@ -1,4 +1,5 @@
app = require 'core/application'
utils = require 'core/utils'
RootView = require 'views/core/RootView'
template = require 'templates/courses/mock1/courses'
@ -11,10 +12,12 @@ module.exports = class CoursesView extends RootView
'click .btn-continue': 'onClickContinue'
'click .btn-enroll': 'onClickEnroll'
'click .btn-enter': 'onClickEnter'
'click .btn-student': 'onClickStudent'
'hidden.bs.modal #continueModal': 'onHideContinueModal'
constructor: (options) ->
super options
@studentMode = utils.getQueryVariable('student', false) or options.studentMode
@initData()
getRenderData: ->
@ -22,6 +25,7 @@ module.exports = class CoursesView extends RootView
context.courses = @courses ? []
context.instances = @instances ? []
context.praise = @praise
context.studentMode = @studentMode
context
initData: ->
@ -36,12 +40,9 @@ module.exports = class CoursesView extends RootView
@praise = mockData.praise[_.random(0, mockData.praise.length - 1)]
onClickBuy: (e) ->
$('#continueModal').modal('hide')
courseID = $(e.target).data('course-id') ? 0
viewClass = require 'views/courses/mock1/CourseEnrollView'
viewArgs = [{}, courseID]
navigationEvent = route: "/courses/mock1/enroll", viewClass: viewClass, viewArgs: viewArgs
Backbone.Mediator.publish 'router:navigate', navigationEvent
app.router.navigate("/courses/mock1/enroll/#{courseID}")
window.location.reload()
onClickContinue: (e) ->
courseID = $(e.target).data('course-id')
@ -51,7 +52,9 @@ module.exports = class CoursesView extends RootView
$('#continueModal').find('.btn-enroll').data('course-id', courseID)
$('#continueModal').find('.btn-enter').data('course-id', courseID)
$('#continueModal .row-pick-class').show() if @courses[courseID]?.unlocked
if courseTitle is 'Introduction to Computer Science'
if @courses[courseID]?.unlocked
$('#continueModal .btn-buy').prop('innerText', 'Start new class')
else if courseTitle is 'Introduction to Computer Science'
$('#continueModal .btn-buy').prop('innerText', 'Get this FREE course!')
else
$('#continueModal .btn-buy').prop('innerText', 'Buy this course')
@ -61,7 +64,7 @@ module.exports = class CoursesView extends RootView
courseID = $(e.target).data('course-id')
instanceID = _.random(0, @instances.length - 1)
viewClass = require 'views/courses/mock1/CourseDetailsView'
viewArgs = [{}, courseID, instanceID]
viewArgs = [{studentMode: @studentMode}, courseID, instanceID]
navigationEvent = route: "/courses/mock1/#{courseID}", viewClass: viewClass, viewArgs: viewArgs
Backbone.Mediator.publish 'router:navigate', navigationEvent
@ -76,5 +79,12 @@ module.exports = class CoursesView extends RootView
navigationEvent = route: "/courses/mock1/#{courseID}", viewClass: viewClass, viewArgs: viewArgs
Backbone.Mediator.publish 'router:navigate', navigationEvent
onClickStudent: (e) ->
route = "/courses/mock1?student=true"
viewClass = require 'views/courses/mock1/CoursesView'
viewArgs = [studentMode: true]
navigationEvent = route: route, viewClass: viewClass, viewArgs: viewArgs
Backbone.Mediator.publish 'router:navigate', navigationEvent
onHideContinueModal: (e) ->
$('#continueModal .row-pick-class').hide()

View file

@ -0,0 +1,79 @@
log = require 'winston'
Payment = require '../payments/Payment'
PaymentHandler = require '../payments/payment_handler'
module.exports =
logError: (user, msg) ->
log.error "Stripe Utils Error: #{user.get('slug')} (#{user._id}): '#{msg}'"
createCharge: (user, amount, metadata, done) ->
options =
amount: amount
currency: 'usd'
customer: user.get('stripe')?.customerID
metadata: metadata
receipt_email: user.get('email')
statement_descriptor: 'CODECOMBAT.COM'
stripe.charges.create options, (err, charge) =>
if err
@logError(user, "Charge create error: #{JSON.stringify(err)}")
return done(err)
done(err, charge)
createPayment: (user, stripeCharge, done) ->
payment = new Payment
purchaser: user._id
recipient: user._id
created: new Date().toISOString()
service: 'stripe'
amount: parseInt(stripeCharge.amount)
payment.set 'description', stripeCharge.metadata.description if stripeCharge.metadata.description
payment.set 'stripe',
customerID: stripeCharge.customer
timestamp: parseInt(stripeCharge.metadata.timestamp)
chargeID: stripeCharge.id
validation = PaymentHandler.validateDocumentInput(payment.toObject())
if validation.valid is false
@logError(user, 'Invalid stripe payment object.')
return done(validation.errors)
payment.save (err) =>
if err
@logError(user, "Payment save error: #{JSON.stringify(err)}")
return done(err)
done(err, payment)
getCustomer: (user, token, done) ->
# If necessary, creates new Stripe customer and saves to user
customerID = user.get('stripe')?.customerID
if customerID
if token
# old customer, new token. Save it.
stripe.customers.update customerID, { card: token }, (err, customer) =>
if err
@logError(user, "Customer update error: #{JSON.stringify(err)}")
return done(err)
done(err, customer)
else
stripe.customers.retrieve customerID, (err, customer) =>
if err
@logError(user, "Customer retrieve error: #{JSON.stringify(err)}")
return done(err)
done(err, customer)
else
newCustomer = {
card: token
email: user.get('email')
metadata: { id: user._id + '', slug: user.get('slug') }
}
stripe.customers.create newCustomer, (err, customer) =>
if err
@logError(user, "Customer creation error: #{JSON.stringify(err)}")
return done(err)
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
stripeInfo.customerID = customer.id
user.set('stripe', stripeInfo)
user.save (err) =>
if err
@logError(user, 'Stripe customer id save db error. '+err)
return done(err)
done(err, customer)

View file

@ -12,6 +12,7 @@ Prepaid = require '../prepaids/Prepaid'
User = require '../users/User'
{findStripeSubscription} = require '../lib/utils'
{getSponsoredSubsAmount} = require '../../app/core/utils'
StripeUtils = require '../lib/stripe_utils'
recipientCouponID = 'free'
@ -21,6 +22,9 @@ subscriptions = {
gems: 3500
amount: 999 # For calculating incremental quantity before sub creation
}
year_sale: {
amount: 7900
}
}
class SubscriptionHandler extends Handler
@ -32,6 +36,7 @@ class SubscriptionHandler extends Handler
return @getStripeInvoices(req, res) if args[1] is 'stripe_invoices'
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'
super(arguments...)
getStripeEvents: (req, res) ->
@ -111,6 +116,42 @@ class SubscriptionHandler extends Handler
log.debug 'Analytics error:\n' + err
@sendSuccess(res, userMap)
purchaseYearSale: (req, res) ->
return @sendForbiddenError(res) unless req.user?
return @sendForbiddenError(res) if req.user?.hasSubscription()
StripeUtils.getCustomer req.user, req.body.stripe?.token, (err, customer) =>
if err
@logSubscriptionError(req.user, "Purchase year sale get customer: #{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
StripeUtils.createCharge req.user, subscriptions.year_sale.amount, metadata, (err, charge) =>
if err
@logSubscriptionError(req.user, "Purchase year sale create charge: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
StripeUtils.createPayment req.user, charge, (err, payment) =>
if err
@logSubscriptionError(req.user, "Purchase year sale create payment: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
# Add terminal subscription to User
endDate = new Date()
endDate.setUTCFullYear(endDate.getUTCFullYear() + 1)
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
stripeInfo.free = endDate.toISOString().substring(0, 10)
req.user.set('stripe', stripeInfo)
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})
@ -234,7 +275,7 @@ class SubscriptionHandler extends Handler
options.coupon = couponID if couponID
stripe.customers.createSubscription customer.id, options, (err, subscription) =>
if err
@logSubscriptionError(user, 'Stripe customer plan setting error. ' + err)
@logSubscriptionError(user, 'Stripe customer plan resetting error. ' + err)
return done({res: 'Database error.', code: 500})
@updateUser(req, user, customer, subscription, false, done)

View file

@ -217,14 +217,17 @@ UserSchema.methods.register = (done) ->
delighted.addDelightedUser @
@saveActiveUser 'register'
UserSchema.methods.isPremium = ->
return true if @isInGodMode()
return true if @isAdmin()
UserSchema.methods.hasSubscription = ->
return false unless stripeObject = @get('stripe')
return true if stripeObject.sponsorID
return true if stripeObject.subscriptionID
return true if stripeObject.free is true
return true if _.isString(stripeObject.free) and new Date() < new Date(stripeObject.free)
UserSchema.methods.isPremium = ->
return true if @isInGodMode()
return true if @isAdmin()
return true if @hasSubscription()
return false
UserSchema.methods.level = ->

View file

@ -375,6 +375,8 @@ describe 'Subscriptions', ->
expect(err).toBeNull()
return done() if err
expect(res.statusCode).toBe(200)
expect(body.stripe).toBeDefined()
return done() unless body.stripe
expect(body.stripe.customerID).toBeDefined()
expect(body.stripe.planID).toBe('basic')
expect(body.stripe.token).toBeUndefined()
@ -1366,3 +1368,33 @@ describe 'Subscriptions', ->
else
expect(subscription.quantity).toEqual(subPrice + 10 * subPrice * 0.8 + (numSponsored - 11) * subPrice * 0.6)
done()
describe 'APIs', ->
subscriptionURL = getURL('/db/subscription')
it 'year_sale', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
expect(user1.get('stripe')?.free).toBeUndefined()
requestBody =
stripe:
token: token.id
timestamp: new Date()
request.put {uri: "#{subscriptionURL}/-/year_sale", json: requestBody, headers: headers }, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
User.findById user1.id, (err, user1) ->
expect(err).toBeNull()
stripeInfo = user1.get('stripe')
expect(stripeInfo).toBeDefined()
return done() unless stripeInfo
endDate = new Date()
endDate.setUTCFullYear(endDate.getUTCFullYear() + 1)
expect(stripeInfo.free).toEqual(endDate.toISOString().substring(0, 10))
expect(stripeInfo.customerID).toBeDefined()
Payment.findOne 'stripe.customerID': stripeInfo.customerID, (err, payment) ->
expect(err).toBeNull()
expect(payment).toBeDefined()
done()