Merge branch 'master' into production

This commit is contained in:
Matt Lott 2015-09-25 15:23:56 -07:00
commit e37c5645ef
49 changed files with 1413 additions and 343 deletions

View file

@ -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')

View file

@ -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

View file

@ -868,6 +868,7 @@ module.exports.thangNames = thangNames =
'Nordex'
'Satish'
'Vera'
'Charlotte'
]
'Equestrian': [
# Male

View file

@ -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: "Скриптен език за игри."

View file

@ -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"

View file

@ -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."

View file

@ -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."

View file

@ -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, . - 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 well 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
View 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

View file

@ -0,0 +1,3 @@
#users, #months
max-width: 100px

View file

@ -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

View 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

View 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!

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View 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'

View 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()

View file

@ -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()

View file

@ -48,7 +48,6 @@ module.exports = class SubscribeModal extends ModalView
popoverTitle = $.i18n.t 'subscribe.parent_email_title'
popoverTitle += '<button type="button" class="close" onclick="$(&#39;.parent-button&#39;).popover(&#39;hide&#39;);">&times;</button>'
popoverContent = ->
console.log 'found html', $('.parent-button-popover-content').html()
$('.parent-button-popover-content').html()
@$el.find('.parent-button').popover(
animation: true

View file

@ -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

View file

@ -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) ->

View file

@ -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

View file

@ -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'

View file

@ -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>')

View file

@ -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) ->

View file

@ -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)

View file

@ -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
)

View file

@ -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)

View file

@ -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()]

View file

@ -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?()

View file

@ -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

View file

@ -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

View file

@ -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) =>

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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})

View file

@ -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('')

View file

@ -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()

View file

@ -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) ->

View file

@ -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 =

View file

@ -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

View file

@ -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

View file

@ -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) ->