mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-04-27 06:23:41 -04:00
Merge branch 'master' into production
This commit is contained in:
commit
36e337495d
25 changed files with 657 additions and 203 deletions
app
core
locale
models
styles
account
courses/mock1
modal
templates
account
admin
core
courses/mock1
views
server
test/server/functional
|
@ -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')
|
||||
|
||||
|
|
|
@ -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!"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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: "学习的冲动!你想要学好怎么编程,与此同时我们也想要教你怎么编程。虽然你可能会觉得你大多数时间你反而都在教导。这,就是学习。"
|
||||
|
|
|
@ -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."
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
7
app/styles/account/subscription-sale-view.sass
Normal file
7
app/styles/account/subscription-sale-view.sass
Normal file
|
@ -0,0 +1,7 @@
|
|||
#subscription-sale-view
|
||||
.center
|
||||
text-align: center
|
||||
.sale-blurb
|
||||
font-size: 22px
|
||||
#pay-button
|
||||
font-size: 18px
|
|
@ -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%
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
#courses-view
|
||||
|
||||
.btn-continue
|
||||
margin-top: 40px
|
||||
|
||||
.center
|
||||
text-align: center
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
54
app/templates/account/subscription-sale-view.jade
Normal file
54
app/templates/account/subscription-sale-view.jade
Normal 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") ×
|
||||
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") ×
|
||||
if state === 'unknown_error'
|
||||
#error-alert.alert.alert-danger.alert-dismissible
|
||||
button.close(type="button" data-dismiss="alert")
|
||||
span(aria-hidden="true") ×
|
||||
p(data-i18n="loading_error.unknown")
|
||||
p= stateMessage
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -20,10 +20,12 @@ block content
|
|||
span ×
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
69
app/views/account/SubscriptionSaleView.coffee
Normal file
69
app/views/account/SubscriptionSaleView.coffee
Normal 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?()
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
79
server/lib/stripe_utils.coffee
Normal file
79
server/lib/stripe_utils.coffee
Normal 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)
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 = ->
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue