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