Merge branch 'master' into production
BIN
app/assets/images/pages/base/codecombat_logo_circle_250.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
app/assets/images/pages/home/student_jumbotron.jpg
Normal file
After Width: | Height: | Size: 182 KiB |
Before Width: | Height: | Size: 1.6 MiB |
BIN
app/assets/images/pages/modal/auth/facebook_small.png
Executable file
After Width: | Height: | Size: 730 B |
BIN
app/assets/images/pages/modal/auth/facebook_sso_button.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
app/assets/images/pages/modal/auth/gplus_small.png
Executable file
After Width: | Height: | Size: 1.5 KiB |
BIN
app/assets/images/pages/modal/auth/gplus_sso_button.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
|
@ -16,4 +16,11 @@ module.exports = {
|
|||
options.url = '/contact'
|
||||
$.ajax(options)
|
||||
|
||||
|
||||
sendParentSignupInstructions: (parentEmail) ->
|
||||
jqxhr = $.ajax('/contact/send-parent-signup-instructions', {
|
||||
method: 'POST'
|
||||
data: {parentEmail}
|
||||
})
|
||||
return new Promise(jqxhr.then)
|
||||
}
|
||||
|
|
|
@ -57,6 +57,14 @@ module.exports.applyErrorsToForm = (el, errors, warning=false) ->
|
|||
message = error.message if error.formatted
|
||||
prop = error.property
|
||||
|
||||
if error.code is tv4.errorCodes.FORMAT_CUSTOM
|
||||
originalMessage = /Format validation failed \(([^\(\)]+)\)/.exec(message)[1]
|
||||
unless _.isEmpty(originalMessage)
|
||||
message = originalMessage
|
||||
|
||||
if error.code is 409 and error.property is 'email'
|
||||
message += ' <a class="login-link">Log in?</a>'
|
||||
|
||||
missingErrors.push error unless setErrorToProperty el, prop, message, warning
|
||||
missingErrors
|
||||
|
||||
|
|
|
@ -256,8 +256,13 @@
|
|||
signup_switch: "Want to create an account?"
|
||||
|
||||
signup:
|
||||
email_announcements: "Receive announcements by email"
|
||||
create_student_header: "Create Student Account"
|
||||
create_teacher_header: "Create Teacher Account"
|
||||
create_individual_header: "Create Individual Account"
|
||||
create_header: "Create Account"
|
||||
email_announcements: "Receive announcements about new CodeCombat levels and features!" # {change}
|
||||
creating: "Creating Account..."
|
||||
create_account: "Create Account"
|
||||
sign_up: "Sign Up"
|
||||
log_in: "log in with password"
|
||||
required: "You need to log in before you can go that way."
|
||||
|
@ -265,7 +270,7 @@
|
|||
school_name: "School Name and City"
|
||||
optional: "optional"
|
||||
school_name_placeholder: "Example High School, Springfield, IL"
|
||||
or_sign_up_with: "or sign up with"
|
||||
connect_with: "Connect with:"
|
||||
connected_gplus_header: "You've successfully connected with Google+!"
|
||||
connected_gplus_p: "Finish signing up so you can log in with your Google+ account."
|
||||
gplus_exists: "You already have an account associated with Google+!"
|
||||
|
@ -274,6 +279,45 @@
|
|||
facebook_exists: "You already have an account associated with Facebook!"
|
||||
hey_students: "Students, enter the class code from your teacher."
|
||||
birthday: "Birthday"
|
||||
parent_email_blurb: "We know you can't wait to learn programming — we're excited too! Your parents will receive an email with further instructions on how to create an account for you. Email {{email_link}} if you have any questions."
|
||||
classroom_not_found: "No classes exist with this Class Code. Check your spelling or ask your teacher for help."
|
||||
checking: "Checking..."
|
||||
account_exists: "This email is already in use:" # {change}
|
||||
sign_in: "Sign in"
|
||||
email_good: "Email looks good!"
|
||||
name_taken: "Username already taken! Try {{suggestedName}}?"
|
||||
name_available: "Username available!"
|
||||
choose_type: "Choose your account type:"
|
||||
teacher_type_1: "Teach programming using CodeCombat!"
|
||||
teacher_type_2: "Set up your class"
|
||||
teacher_type_3: "Access Course Guides"
|
||||
teacher_type_4: "View student progress"
|
||||
signup_as_teacher: "Sign up as a Teacher"
|
||||
student_type_1: "Learn to program while playing an engaging game!"
|
||||
student_type_2: "Play with your class"
|
||||
student_type_3: "Compete in arenas"
|
||||
student_type_4: "Choose your hero!"
|
||||
student_type_5: "Have your Class Code ready!"
|
||||
signup_as_student: "Sign up as a Student"
|
||||
individuals_or_parents: "Individuals & Parents"
|
||||
individual_type: "For players learning to code outside of a class. Parents should sign up for an account here."
|
||||
signup_as_individual: "Sign up as an Individual"
|
||||
enter_class_code: "Enter your Class Code"
|
||||
enter_birthdate: "Enter your birthdate:"
|
||||
ask_teacher_1: "Ask your teacher for your Class Code."
|
||||
ask_teacher_2: "Not part of a class? Create an "
|
||||
ask_teacher_3: "Individual Account"
|
||||
ask_teacher_4: " instead."
|
||||
about_to_join: "You're about to join:"
|
||||
enter_parent_email: "Enter your parent’s email address:"
|
||||
parent_email_error: "Something went wrong when trying to send the email. Check the email address and try again."
|
||||
parent_email_sent: "We’ve sent an email with further instructions on how to create an account. Ask your parent to check their inbox."
|
||||
account_created: "Account Created!"
|
||||
confirm_student_blurb: "Write down your information so that you don't forget it. Your teacher can also help you reset your password at any time."
|
||||
confirm_individual_blurb: "Write down your login information in case you need it later. Verify your email so you can recover your account if you ever forget your password - check your inbox!"
|
||||
write_this_down: "Write this down:"
|
||||
start_playing: "Start Playing!"
|
||||
sso_connected: "Successfully connected with:"
|
||||
|
||||
recover:
|
||||
recover_account_title: "Recover Account"
|
||||
|
@ -295,6 +339,7 @@
|
|||
saving: "Saving..."
|
||||
sending: "Sending..."
|
||||
send: "Send"
|
||||
sent: "Sent"
|
||||
type: "Type"
|
||||
cancel: "Cancel"
|
||||
save: "Save"
|
||||
|
@ -365,6 +410,7 @@
|
|||
wizard: "Wizard"
|
||||
first_name: "First Name"
|
||||
last_name: "Last Name"
|
||||
last_initial: "Last Initial"
|
||||
username: "Username"
|
||||
|
||||
units:
|
||||
|
|
|
@ -5,7 +5,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
no_mobile: "¡CodeCombat no fue diseñado para dispositivos móviles y puede que no funcione!" # Warning that shows up on mobile devices
|
||||
play: "Jugar" # The big play button that opens up the campaign view.
|
||||
play_campaign_version: "Juega a la versión camapaña." # Shows up under big play button if you only play /courses
|
||||
old_browser: "Ay, su navegador es demasiado viejo para ejecutar CodeCombat. ¡Lo sentimos!" # Warning that shows up on really old Firefox/Chrome/Safari
|
||||
old_browser: "Ay, tu navegador es demasiado viejo para ejecutar CodeCombat. ¡Lo sentimos!" # Warning that shows up on really old Firefox/Chrome/Safari
|
||||
old_browser_suffix: "Lo puede intentar de todos modos, pero probablemente no va a funcionar."
|
||||
ipad_browser: "Malas noticias: CodeCombat no corre en el navegador de iPad. Buenas noticias: nuestra aplicación para iPad está esperando la aprobación de Apple."
|
||||
campaign: "Campaña"
|
||||
|
@ -27,16 +27,16 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
# classroom_in_a_box: "A classroom in-a-box for teaching computer science."
|
||||
codecombat_is: "CodeCombat es una plataforma <strong>para que los estudiantes</strong> aprendan ciencia de la computación mientras juegan a un juego real."
|
||||
# our_courses: "Our courses have been specifically playtested to <strong>excel in the classroom</strong>, even by teachers with little to no prior programming experience."
|
||||
top_screenshots_hint: "Los estudiantes escriben código y ven sus cambios en tiempo real"
|
||||
top_screenshots_hint: "Los estudiantes escriben código y ven sus cambios en tiempo real."
|
||||
designed_with: "Diseñado pensando en los profesores"
|
||||
real_code: "Real, escribe código"
|
||||
from_the_first_level: "desde el primer nivel"
|
||||
from_the_first_level: "desde el primer nivel."
|
||||
getting_students: "Involucrar a los alumnos en la programación por sentencias tan rápido como sea posible es fundamental para aprender la sintaxis de la programación con una estructura apropiada."
|
||||
educator_resources: "Recursos para educadores"
|
||||
course_guides: "y guías de cursos"
|
||||
teaching_computer_science: "Enseñar ciencias computación no requiere de un costoso título, porque nosotros proveemos las herramientas para apoyar educadores con cualquier nivel de conocimientos."
|
||||
course_guides: "y guías de cursos."
|
||||
teaching_computer_science: "Enseñar ciencias de la computación no requiere de un costoso título, porque nosotros proveemos las herramientas para apoyar educadores con cualquier nivel de conocimientos."
|
||||
accessible_to: "Accesible para"
|
||||
everyone: "todo el mundo"
|
||||
everyone: "todo el mundo."
|
||||
democratizing: "La democratización del proceso de aprendizaje es el nucleo de nuestra filosofía. Todo mundo debe ser capaz de aprender a programar."
|
||||
forgot_learning: "En realidad creo que que ellos olvidaron que en realidad están aprendiendo algo."
|
||||
wanted_to_do: " Programar es algo que siempre he querido hacer, nunca pensé que sería capáz de aprenderlo en la escuela."
|
||||
|
@ -49,10 +49,10 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
motivating: "motivante"
|
||||
not_tedious: "no tedioso."
|
||||
gaming_is_good: "Estudios sugieren que el jugar es bueno para el cerebro de los niños. (¡Es verdad!)"
|
||||
# game_based: "When game-based learning systems are"
|
||||
# compared: "compared"
|
||||
# conventional: "against conventional assessment methods, the difference is clear: games are better at helping students retain knowledge, concentrate and"
|
||||
# perform_at_higher_level: "perform at a higher level of achievement"
|
||||
game_based: "Cuando los sistemas de aprendizaje basados en juegos son"
|
||||
compared: "comparados"
|
||||
conventional: "contra los métodos convencionales de evaluación, la diferencia es clara: los juegos son mejores ayudando a los alumnos a retener conocimiento, concentrarse y"
|
||||
perform_at_higher_level: "desempeñarse a un nivel mas alto de ejecución."
|
||||
# feedback: "Games also provide real-time feedback that allows students to adjust their solution path and understand concepts more holistically, instead of being limited to just “correct” or “incorrect” answers."
|
||||
# real_game: "A real game, played with real coding."
|
||||
# great_game: "A great game is more than just badges and achievements - it’s about a player’s journey, well-designed puzzles, and the ability to tackle challenges with agency and confidence."
|
||||
|
@ -76,11 +76,11 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
# boast: "Boasts riddles that are complex enough to fascinate gamers and coders alike."
|
||||
# winning: "A winning combination of RPG gameplay and programming homework that pulls off making kid-friendly education legitimately enjoyable."
|
||||
# run_class: "Everything you need to run a computer science class in your school today, no CS background required."
|
||||
# teachers: "Teachers!"
|
||||
# teachers_and_educators: "Teachers & Educators"
|
||||
teachers: "Profesores"
|
||||
teachers_and_educators: "Profesores y Educadores"
|
||||
# class_in_box: "Learn how our classroom-in-a-box platform fits into your curriculum."
|
||||
# get_started: "Get Started"
|
||||
# students: "Students:"
|
||||
students: "Alumnos:"
|
||||
# join_class: "Join Class"
|
||||
# role: "Your role:"
|
||||
# student_count: "Number of students:"
|
||||
|
@ -102,10 +102,10 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
blog: "Blog"
|
||||
forum: "Foro"
|
||||
account: "Cuenta"
|
||||
# my_account: "My Account"
|
||||
my_account: "Mi Cuenta"
|
||||
profile: "Perfil"
|
||||
stats: "Estadisticas"
|
||||
code: "Codigo"
|
||||
code: "Código"
|
||||
home: "Inicio"
|
||||
contribute: "Colaborar"
|
||||
legal: "Legalidad"
|
||||
|
@ -148,7 +148,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
|
||||
play:
|
||||
play_as: "Jugar como" # Ladder page
|
||||
# compete: "Compete!" # Course details page
|
||||
compete: "¡Compite!" # Course details page
|
||||
spectate: "Observar" # Ladder page
|
||||
players: "jugadores" # Hover over a level on /play
|
||||
hours_played: "horas jugadas" # Hover over a level on /play
|
||||
|
@ -323,7 +323,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
accept: "Aceptar"
|
||||
reject: "Rechazar"
|
||||
withdraw: "Retirar"
|
||||
submitter: "Submitter"
|
||||
#submitter: "Submitter"
|
||||
submitted: "Enviado"
|
||||
commit_msg: "Mensaje de Asignación o Commit"
|
||||
version_history: "Historial de versión"
|
||||
|
@ -597,7 +597,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
thank_you_months_suffix: "meses."
|
||||
thank_you: "Gracias por apoyar a CodeCombat."
|
||||
sorry_to_see_you_go: "¡Lamentamos verte marchar! Por favor, haznos saber que pudimos haacer mejor."
|
||||
# unsubscribe_feedback_placeholder: "O, what have we done?"
|
||||
unsubscribe_feedback_placeholder: "Oh, ¿Qué hemos hecho?"
|
||||
parent_button: "Pregunta a tus padres"
|
||||
parent_email_description: "Le escribiremos para que puedan comprarte una suscripción para CodeCombat."
|
||||
parent_email_input_invalid: "Correo electrónico inválido."
|
||||
|
@ -611,7 +611,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
parents_blurb1: "Su hijo ha jugado __nLevels__ niveles y ha aprendido conceptos básicos de programación. Ayudadle a mejorar su eficiencia adquiriendo una suscripción para poder seguir programando."
|
||||
parents_blurb1a: "La programación informática es una habilidad fundamental que su hijo usará indudablemente cuando sea un adulto. Aproximadamente en 2020, se necesitarán conceptos de programación en el 77% de los empleos, y los ingenieros programadores están muy demandados en todo el mundo. ¿Sabían que la Ingeniería Informática es la titulación que más dinero paga?"
|
||||
parents_blurb2: "Por tan sólo ${{price}} USD/mes, su hijo podrá afrontar nuevos retos cada semana, y recibirá soporte por correo electrónico de programadores profesionales."
|
||||
parents_blurb3: "Sin riesgo: 100% garantía de devoluación de dinero, desuscripción con un simple click."
|
||||
parents_blurb3: "Sin riesgo: 100% garantía de devolución de dinero, cancela tu suscripción con un simple click."
|
||||
payment_methods: "Métodos de pago"
|
||||
payment_methods_title: "Métodos de pago permitidos"
|
||||
payment_methods_blurb1: "Actualmente aceptamos pagos a través de tarjetas de crédito / débito y Alipay." # {change}
|
||||
|
@ -975,7 +975,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
social_facebook: "Dale a Me Gusta a CodeCombat en Facebook"
|
||||
social_twitter: "Sigue a CodeCombat en Twitter"
|
||||
social_gplus: "Unete a CodeCombat en Google+"
|
||||
# social_slack: "Chat with us in the public CodeCombat Slack channel"
|
||||
social_slack: "Chatea con nosotros en el canal público de CodeCombat"
|
||||
contribute_to_the_project: "Contribuye al proyecto"
|
||||
|
||||
clans:
|
||||
|
|
|
@ -233,9 +233,9 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
|
|||
None: "無值"
|
||||
|
||||
share_progress_modal:
|
||||
blurb: "您正在建立優秀的進度!告訴您的家長,您從CodeCombat學到了什麼!" # {change}
|
||||
blurb: "您正在創造偉大的旅程!告訴您的家長,您從CodeCombat學到了什麼!" # {change}
|
||||
email_invalid: "郵件地址無效"
|
||||
form_blurb: "在底下輸入他們的郵件並且我們將秀給他們!"
|
||||
form_blurb: "在底下輸入家長的郵件,讓我們展示給他們!"
|
||||
form_label: "郵件地址"
|
||||
placeholder: "郵件地址"
|
||||
title: "出色的作品,學徒"
|
||||
|
@ -760,28 +760,28 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
|
|||
retrostyle_blurb: "復古風格的遊戲"
|
||||
jose_title: "音樂"
|
||||
jose_blurb: "放輕鬆"
|
||||
# community_title: "...and our open-source community"
|
||||
# community_subtitle: "Over 450 contributors have helped build CodeCombat, with more joining every week!"
|
||||
# community_description_1: "CodeCombat is a community project, with hundreds of players volunteering to create levels, contribute to our code to add features, fix bugs, playtest, and even translate the game into 50 languages so far. Employees, contributors and the site gain by sharing ideas and pooling effort, as does the open source community in general. The site is built on numerous open source projects, and we are open sourced to give back to the community and provide code-curious players a familiar project to explore and experiment with. Anyone can join the CodeCombat community! Check out our"
|
||||
# community_description_link: "contribute page"
|
||||
# community_description_2: "for more info."
|
||||
# number_contributors: "Over 450 contributors have lent their support and time to this project."
|
||||
# story_title: "Our story so far"
|
||||
# story_subtitle: "Since 2013, CodeCombat has grown from a mere set of sketches to a living, thriving game."
|
||||
# story_statistic_1a: "5,000,000+"
|
||||
# story_statistic_1b: "total players"
|
||||
# story_statistic_1c: "have started their programming journey through CodeCombat"
|
||||
# story_statistic_2a: "We’ve been translated into over 50 languages — our players hail from"
|
||||
# story_statistic_2b: "200+ countries"
|
||||
# story_statistic_3a: "Together, they have written"
|
||||
# story_statistic_3b: "1 billion lines of code and counting"
|
||||
# story_statistic_3c: "across many different programming languages"
|
||||
# story_long_way_1: "Though we've come a long way..."
|
||||
# story_sketch_caption: "Nick's very first sketch depicting a programming game in action."
|
||||
# story_long_way_2: "we still have much to do before we complete our quest, so..."
|
||||
# jobs_title: "Come work with us and help write CodeCombat history!"
|
||||
# jobs_subtitle: "Don't see a good fit but interested in keeping in touch? See our \"Create Your Own\" listing."
|
||||
# jobs_benefits: "Employee Benefits"
|
||||
community_title: "...以及我們的開放原始碼社群" #"...and our open-source community"
|
||||
community_subtitle: "超過 450 位貢獻者協助建立 CodeCombat , 而且每週持續增加中!" #"Over 450 contributors have helped build CodeCombat, with more joining every week!"
|
||||
community_description_1: "CodeCombat 是一個社群專案,由有數以百計的玩家志願來建立遊戲關卡,建構我們的程式碼來添加功能、修正 Bugs、執行測試、甚至翻譯此遊戲至超過 50 種語言。如同多數的開源社群一般,所有的員工、貢獻者們及 CodeCombat 都獲益於持續的互相分享靈感和彙整努力。CodeCombat 是建立於無數的開源專案之上,因此我們開源回饋給社群朋友,提供一個友善專案給對於原始碼有興趣的玩家來探索和實驗。任何人都可以加入 CodeCombat 社群!請查看我們的" #"CodeCombat is a community project, with hundreds of players volunteering to create levels, contribute to our code to add features, fix bugs, playtest, and even translate the game into 50 languages so far. Employees, contributors and the site gain by sharing ideas and pooling effort, as does the open source community in general. The site is built on numerous open source projects, and we are open sourced to give back to the community and provide code-curious players a familiar project to explore and experiment with. Anyone can join the CodeCombat community! Check out our"
|
||||
community_description_link: "貢獻者頁" #"contribute page"
|
||||
community_description_2: "來獲得更多資訊。" #"for more info."
|
||||
number_contributors: "超過 450 位貢獻者奉獻他們的時間來協助本專案。" #"Over 450 contributors have lent their support and time to this project."
|
||||
story_title: "我們的故事..." #"Our story so far"
|
||||
story_subtitle: "從 2013 年起,CodeCombat 從簡單的草圖成長為一個有生命且生氣蓬勃的遊戲。" #"Since 2013, CodeCombat has grown from a mere set of sketches to a living, thriving game."
|
||||
story_statistic_1a: "總計超過 5,000,000+ " #"5,000,000+"
|
||||
story_statistic_1b: "位玩家" #"total players"
|
||||
story_statistic_1c: "藉由 CodeCombat 開啟他們的程式之旅" #"have started their programming journey through CodeCombat"
|
||||
story_statistic_2a: "我們已翻譯至超過 50 種語言,我們的玩家來自" #"We’ve been translated into over 50 languages — our players hail from"
|
||||
story_statistic_2b: "超過 200+ 個國家" #"200+ countries"
|
||||
story_statistic_3a: "他們一起撰寫完成" #"Together, they have written"
|
||||
story_statistic_3b: "一百萬行程式及計算" #"1 billion lines of code and counting"
|
||||
story_statistic_3c: "跨越數種不同的程式語言" #"across many different programming languages"
|
||||
story_long_way_1: "雖然我們已經走過很長的一段路..." #"Though we've come a long way..."
|
||||
story_sketch_caption: "Nick 著手完成了一個初版的藍圖,描述一個撰寫程式的遊戲。" #"Nick's very first sketch depicting a programming game in action."
|
||||
story_long_way_2: "然而在完成任務之前,我們仍然還有許多事情要做。所以..." #"we still have much to do before we complete our quest, so..."
|
||||
jobs_title: "與我們一同工作,來協助撰寫 CodeCombat 的歷史吧!" #"Come work with us and help write CodeCombat history!"
|
||||
jobs_subtitle: "找不到符合的位置,但有興趣與我們保持連繫?查看『建立我的』清單吧。" #"Don't see a good fit but interested in keeping in touch? See our \"Create Your Own\" listing."
|
||||
jobs_benefits: "員工福利" #"Employee Benefits"
|
||||
# jobs_benefit_4: "Unlimited vacation"
|
||||
# jobs_benefit_5: "Professional development and continuing education support – free books and games!"
|
||||
# jobs_benefit_6: "Medical (gold), dental, vision"
|
||||
|
@ -789,29 +789,29 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
|
|||
# jobs_benefit_9: "10-year option exercise window"
|
||||
# jobs_benefit_10: "Maternity leave: 10 weeks paid, next 6 @ 55% salary"
|
||||
# jobs_benefit_11: "Paternity leave: 10 weeks paid"
|
||||
# learn_more: "Learn More"
|
||||
# jobs_custom_title: "Create Your Own"
|
||||
learn_more: "了解更多" #"Learn More"
|
||||
jobs_custom_title: "建立我的" #"Create Your Own"
|
||||
# jobs_custom_description: "Are you passionate about CodeCombat but don't see a job listed that matches your qualifications? Write us and show how you think you can contribute to our team. We'd love to hear from you!"
|
||||
# jobs_custom_contact_1: "Send us a note at"
|
||||
# jobs_custom_contact_2: "introducing yourself and we might get in touch in the future!"
|
||||
# contact_title: "Press & Contact"
|
||||
# contact_subtitle: "Need more information? Get in touch with us at"
|
||||
# screenshots_title: "Game Screenshots"
|
||||
# screenshots_hint: "(click to view full size)"
|
||||
# downloads_title: "Download Assets & Information"
|
||||
# about_codecombat: "About CodeCombat"
|
||||
contact_title: "點擊 & 連繫" #"Press & Contact"
|
||||
contact_subtitle: "需要更多資訊?透過以下與我們連繫" #"Need more information? Get in touch with us at"
|
||||
screenshots_title: "遊戲螢幕截圖" #"Game Screenshots"
|
||||
screenshots_hint: "(點擊查看完整尺寸)" #"(click to view full size)"
|
||||
downloads_title: "下載資源 & 資訊" #"Download Assets & Information"
|
||||
about_codecombat: "關於 CodeCombat" #"About CodeCombat"
|
||||
# logo: "Logo"
|
||||
# screenshots: "Screenshots"
|
||||
# character_art: "Character Art"
|
||||
# download_all: "Download All"
|
||||
# previous: "Previous"
|
||||
# next: "Next"
|
||||
# location_title: "We're located in downtown SF:"
|
||||
screenshots: "螢幕截圖" #"Screenshots"
|
||||
character_art: "角色美術作品" #"Character Art"
|
||||
download_all: "下載全部" #"Download All"
|
||||
previous: "上一個" #"Previous"
|
||||
next: "下一個" #"Next"
|
||||
location_title: "我們位於城鎮 SF:" #"We're located in downtown SF:"
|
||||
|
||||
teachers:
|
||||
who_for_title: "誰是CodeCombat的使用對象呢?"
|
||||
who_for_1: "我們建議讓9歲及以上的學生使用CodeCombat。無需任何編程經驗。" # {change}
|
||||
who_for_2: "我們設計CodeCombat來吸引男生女生。" # {change}
|
||||
who_for_title: "誰是 CodeCombat 的使用對象呢?"
|
||||
who_for_1: "我們建議讓 9 歲及以上的學生使用 CodeCombat ,無需任何程式撰寫經驗。我們設計 CodeCombat 來吸引不分男女老幼的孩子們。" #"We recommend CodeCombat for students aged 9 and up. No prior programming experience is needed. We've designed CodeCombat to appeal to both boys and girls."
|
||||
who_for_2: "我們的課程系統允許教師們藉由專屬的介面來設定課堂,追蹤學習進度及指派額外的內容給學生們" #"Our Courses system allows teachers to set up classrooms, track progress and assign additional content to students through a dedicated interface."
|
||||
more_info_title: "我可以在哪裡找到更多訊息?"
|
||||
more_info_1: "我們的"
|
||||
more_info_2: "教師論壇"
|
||||
|
|
|
@ -47,12 +47,24 @@ module.exports = class User extends CocoModel
|
|||
super arguments...
|
||||
|
||||
@getUnconflictedName: (name, done) ->
|
||||
# deprecate in favor of @checkNameConflicts, which uses Promises and returns the whole response
|
||||
$.ajax "/auth/name/#{encodeURIComponent(name)}",
|
||||
cache: false
|
||||
success: (data) -> done data.name
|
||||
statusCode: 409: (data) ->
|
||||
response = JSON.parse data.responseText
|
||||
done response.name
|
||||
success: (data) -> done(data.suggestedName)
|
||||
|
||||
@checkNameConflicts: (name) ->
|
||||
new Promise (resolve, reject) ->
|
||||
$.ajax "/auth/name/#{encodeURIComponent(name)}",
|
||||
cache: false
|
||||
success: resolve
|
||||
error: (jqxhr) -> reject(jqxhr.responseJSON)
|
||||
|
||||
@checkEmailExists: (email) ->
|
||||
new Promise (resolve, reject) ->
|
||||
$.ajax "/auth/email/#{encodeURIComponent(email)}",
|
||||
cache: false
|
||||
success: resolve
|
||||
error: (jqxhr) -> reject(jqxhr.responseJSON)
|
||||
|
||||
getEnabledEmails: ->
|
||||
(emailName for emailName, emailDoc of @get('emails', true) when emailDoc.enabled)
|
||||
|
@ -259,6 +271,38 @@ module.exports = class User extends CocoModel
|
|||
window.location.reload()
|
||||
@fetch(options)
|
||||
|
||||
signupWithPassword: (email, password, options={}) ->
|
||||
options.url = _.result(@, 'url') + '/signup-with-password'
|
||||
options.type = 'POST'
|
||||
options.data ?= {}
|
||||
_.extend(options.data, {email, password})
|
||||
jqxhr = @fetch(options)
|
||||
jqxhr.then ->
|
||||
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'CodeCombat'
|
||||
return jqxhr
|
||||
|
||||
signupWithFacebook: (email, facebookID, options={}) ->
|
||||
options.url = _.result(@, 'url') + '/signup-with-facebook'
|
||||
options.type = 'POST'
|
||||
options.data ?= {}
|
||||
_.extend(options.data, {email, facebookID, facebookAccessToken: application.facebookHandler.token()})
|
||||
jqxhr = @fetch(options)
|
||||
jqxhr.then ->
|
||||
window.tracker?.trackEvent 'Facebook Login', category: "Signup", label: 'Facebook'
|
||||
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'Facebook'
|
||||
return jqxhr
|
||||
|
||||
signupWithGPlus: (email, gplusID, options={}) ->
|
||||
options.url = _.result(@, 'url') + '/signup-with-gplus'
|
||||
options.type = 'POST'
|
||||
options.data ?= {}
|
||||
_.extend(options.data, {email, gplusID, gplusAccessToken: application.gplusHandler.token()})
|
||||
jqxhr = @fetch(options)
|
||||
jqxhr.then ->
|
||||
window.tracker?.trackEvent 'Google Login', category: "Signup", label: 'GPlus'
|
||||
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'GPlus'
|
||||
return jqxhr
|
||||
|
||||
fetchGPlusUser: (gplusID, options={}) ->
|
||||
options.data ?= {}
|
||||
options.data.gplusID = gplusID
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
margin: 0 50px 50px
|
||||
margin: 0 48px 50px
|
||||
|
||||
.hero-avatar
|
||||
margin: 6px
|
||||
|
|
|
@ -1,249 +0,0 @@
|
|||
@import "app/styles/mixins"
|
||||
@import "app/styles/bootstrap/variables"
|
||||
|
||||
#create-account-modal
|
||||
|
||||
//- Clear modal defaults
|
||||
|
||||
.modal-dialog
|
||||
padding: 0
|
||||
width: 666px
|
||||
height: 694px
|
||||
|
||||
|
||||
//- Background
|
||||
|
||||
.auth-modal-background
|
||||
position: absolute
|
||||
top: -90px
|
||||
left: -40px
|
||||
|
||||
|
||||
//- Header
|
||||
|
||||
h1
|
||||
position: absolute
|
||||
left: 183px
|
||||
top: 0px
|
||||
margin: 0
|
||||
width: 255px
|
||||
text-align: center
|
||||
color: rgb(254,188,68)
|
||||
font-size: 32px
|
||||
text-shadow: black 2px 2px 0, black -2px -2px 0, black 2px -2px 0, black -2px 2px 0, black 2px 0px 0, black 0px -2px 0, black -2px 0px 0, black 0px 2px 0
|
||||
|
||||
&.long-title
|
||||
top: -14px
|
||||
|
||||
|
||||
//- Close modal button
|
||||
|
||||
#close-modal
|
||||
position: absolute
|
||||
left: 442px
|
||||
top: -15px
|
||||
width: 60px
|
||||
height: 60px
|
||||
color: white
|
||||
text-align: center
|
||||
font-size: 30px
|
||||
padding-top: 15px
|
||||
cursor: pointer
|
||||
@include rotate(-3deg)
|
||||
|
||||
&:hover
|
||||
color: yellow
|
||||
|
||||
|
||||
//- Modal body content
|
||||
|
||||
#gplus-account-exists-row, #facebook-account-exists-row
|
||||
margin-bottom: 20px
|
||||
|
||||
#facebook-signup-btn
|
||||
margin-bottom: 5px
|
||||
|
||||
.auth-form-content
|
||||
position: absolute
|
||||
top: 100px
|
||||
left: 40px
|
||||
width: 588px
|
||||
|
||||
.help-block
|
||||
margin: 0
|
||||
|
||||
.alert
|
||||
margin-top: -25px
|
||||
margin-bottom: 0
|
||||
padding: 10px 15px
|
||||
|
||||
.form-group
|
||||
color: rgb(51,51,51)
|
||||
padding: 0
|
||||
margin: 0
|
||||
|
||||
.input-border
|
||||
border: 2px solid rgb(233, 221, 194)
|
||||
border-radius: 4px
|
||||
margin-bottom: 7px
|
||||
|
||||
input, select
|
||||
background-color: rgb(239, 232, 216)
|
||||
border: 2px solid rgb(26, 21, 18)
|
||||
border-radius: 4px
|
||||
|
||||
select
|
||||
margin-right: 2px
|
||||
|
||||
label
|
||||
font-size: 20px
|
||||
text-transform: uppercase
|
||||
font-family: $headings-font-family
|
||||
margin-bottom: 0
|
||||
|
||||
.optional-note
|
||||
font-size: 14px
|
||||
|
||||
.well
|
||||
font-size: 18px
|
||||
position: absolute
|
||||
right: 0
|
||||
bottom: 0
|
||||
width: 278px
|
||||
margin-bottom: 0
|
||||
|
||||
//- Check boxes
|
||||
|
||||
.form-group.checkbox
|
||||
margin: 10px 0
|
||||
|
||||
label
|
||||
position: relative
|
||||
line-height: 34px
|
||||
|
||||
span:not(.custom-checkbox)
|
||||
margin-left: 40px
|
||||
|
||||
input
|
||||
display: none
|
||||
|
||||
& + .custom-checkbox
|
||||
.glyphicon
|
||||
display: none
|
||||
|
||||
&:checked + .custom-checkbox .glyphicon
|
||||
display: inline
|
||||
color: rgb(248,169,67)
|
||||
text-align: center
|
||||
text-shadow: 0 0 3px black, 0 0 3px black, 0 0 3px black
|
||||
font-size: 20px
|
||||
position: relative
|
||||
top: -2px
|
||||
|
||||
.input-border
|
||||
border-radius: 4px
|
||||
height: 34px
|
||||
width: 34px
|
||||
position: absolute
|
||||
|
||||
.custom-checkbox
|
||||
border-radius: 4px
|
||||
position: absolute
|
||||
height: 30px
|
||||
width: 30px
|
||||
border: 2px solid rgb(26,21,18)
|
||||
background: rgb(228,217,196)
|
||||
text-align: center
|
||||
|
||||
//- Primary auth button
|
||||
|
||||
#signup-button
|
||||
position: absolute
|
||||
top: 405px
|
||||
height: 70px
|
||||
font-size: 32px
|
||||
line-height: 42px
|
||||
|
||||
|
||||
//- Footer area
|
||||
|
||||
.auth-network-logins
|
||||
|
||||
.btn.btn-lg.network-login
|
||||
width: 251px
|
||||
height: 60px
|
||||
text-align: center
|
||||
position: relative
|
||||
|
||||
.network-logo
|
||||
height: 30px
|
||||
position: absolute
|
||||
left: -10px
|
||||
top: 2px
|
||||
|
||||
.sign-in-blurb
|
||||
line-height: 34px
|
||||
margin-left: 12px
|
||||
|
||||
.fb-login-button
|
||||
$scaleX: 251 / 64
|
||||
$scaleY: 60 / 23
|
||||
transform: scale($scaleX, $scaleY)
|
||||
position: absolute
|
||||
top: 4px
|
||||
left: 74px
|
||||
@include opacity(0.01)
|
||||
|
||||
.gplus-login-wrapper
|
||||
position: absolute
|
||||
left: 65px
|
||||
top: -6px
|
||||
$scaleX: 251 / 84
|
||||
$scaleY: 60 / 24
|
||||
transform: scale($scaleX, $scaleY)
|
||||
@include opacity(0.01)
|
||||
|
||||
#github-login-button
|
||||
position: relative
|
||||
top: -1px
|
||||
border-radius: 5px
|
||||
img
|
||||
width: 16px
|
||||
margin: 0 5px 0 -5px
|
||||
|
||||
#gplus-login-button
|
||||
position: relative
|
||||
top: 8px
|
||||
|
||||
|
||||
//- Extra bottom pane area
|
||||
.extra-pane
|
||||
background-image: url(/images/pages/modal/auth/extra-pane.png)
|
||||
width: 633px
|
||||
height: 139px
|
||||
padding: 23px 23px 23px 168px
|
||||
position: absolute
|
||||
top: 580px
|
||||
|
||||
.switch-explanation
|
||||
margin: 25px 10px 0 0
|
||||
width: 200px
|
||||
color: rgb(254,188,68)
|
||||
font-size: 20px
|
||||
font-family: $headings-font-family
|
||||
font-weight: bold
|
||||
text-transform: uppercase
|
||||
text-shadow: black 1px 1px 0, black -1px -1px 0, black 1px -1px 0, black -1px 1px 0, black 1px 0px 0, black 0px -1px 0, black -1px 0px 0, black 0px 1px 0
|
||||
float: left
|
||||
|
||||
.btn
|
||||
float: right
|
||||
margin-top: 20px
|
||||
width: 230px
|
||||
height: 70px
|
||||
line-height: 40px
|
||||
|
||||
|
||||
.ie10 #create-account-modal .auth-network-logins .btn.btn-lg.network-login .network-logo, .lt-ie10 #create-account-modal .auth-network-logins .btn.btn-lg.network-login .network-logo
|
||||
left: 15px
|
||||
top: 15px
|
30
app/styles/modal/create-account-modal/basic-info-view.sass
Normal file
|
@ -0,0 +1,30 @@
|
|||
#basic-info-view
|
||||
.network-login
|
||||
width: 175px
|
||||
height: 40px
|
||||
border: solid 0.5px darkgray
|
||||
|
||||
span
|
||||
visibility: hidden
|
||||
|
||||
// Forms
|
||||
|
||||
.basic-info
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
.form-group
|
||||
text-align: left
|
||||
|
||||
.btn-illustrated img
|
||||
// Undo previous opacity-toggling hover behavior
|
||||
opacity: 1
|
||||
|
||||
label
|
||||
margin-bottom: 0
|
||||
|
||||
.help-block
|
||||
margin: 0
|
||||
|
||||
.form-container
|
||||
width: 800px
|
|
@ -0,0 +1,68 @@
|
|||
@import "app/styles/style-flat-variables"
|
||||
|
||||
#choose-account-type-view
|
||||
.path-cards
|
||||
margin-top: 15px
|
||||
display: flex
|
||||
|
||||
.path-card ~ .path-card
|
||||
margin-left: 6.5px
|
||||
|
||||
.path-card
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: space-between
|
||||
width: 235px
|
||||
min-height: 340px
|
||||
border-style: solid
|
||||
border-width: thin
|
||||
border-radius: 5px
|
||||
|
||||
&.navy
|
||||
border-color: $navy
|
||||
.card-title
|
||||
background-color: $navy
|
||||
|
||||
&.forest
|
||||
border-color: $forest
|
||||
.card-title
|
||||
background-color: $forest
|
||||
|
||||
|
||||
.card-title
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
justify-content: center
|
||||
height: 50px
|
||||
color: white
|
||||
font-weight: bold
|
||||
text-align: center
|
||||
|
||||
.card-content
|
||||
flex-grow: 1
|
||||
display: flex
|
||||
flex-direction: column
|
||||
margin: 50px 20px 0
|
||||
|
||||
ul
|
||||
align-self: center
|
||||
text-align: left
|
||||
padding-left: 20px
|
||||
li
|
||||
span
|
||||
position: relative
|
||||
left: -5px
|
||||
|
||||
.card-footer
|
||||
margin: 20px
|
||||
|
||||
.individual-section
|
||||
margin-top: 50px
|
||||
max-width: 425px
|
||||
|
||||
.individual-title
|
||||
font-weight: bold
|
||||
|
||||
.text-h6
|
||||
color: white
|
15
app/styles/modal/create-account-modal/confirmation-view.sass
Normal file
|
@ -0,0 +1,15 @@
|
|||
@import "app/styles/style-flat-variables"
|
||||
|
||||
#confirmation-view
|
||||
text-align: left
|
||||
|
||||
.signup-info-box-wrapper
|
||||
width: 100%
|
||||
|
||||
.signup-info-box
|
||||
padding: 10px 20px
|
||||
border: 2px dashed $burgandy
|
||||
|
||||
.modal-body-content
|
||||
width: 80%
|
||||
margin-left: 10%
|
17
app/styles/modal/create-account-modal/coppa-deny-view.sass
Normal file
|
@ -0,0 +1,17 @@
|
|||
#coppa-deny-view
|
||||
.parent-email-blurb
|
||||
width: 500px
|
||||
|
||||
.parent-email-input-group
|
||||
display: flex
|
||||
flex-direction: row
|
||||
align-items: center
|
||||
text-align: center
|
||||
|
||||
.glyphicon
|
||||
width: 0
|
||||
line-height: 40px
|
||||
font-size: 30px
|
||||
|
||||
.error
|
||||
color: red
|
168
app/styles/modal/create-account-modal/create-account-modal.sass
Normal file
|
@ -0,0 +1,168 @@
|
|||
//
|
||||
Layout for CreateAccountModal is done largely with flexboxes.
|
||||
Rules in this file should be ones that apply to all screens/subviews, but this
|
||||
separation may not be perfect.
|
||||
|
||||
Currently it uses .modal-dialog, .modal-content, etc, for some parts of the modal.
|
||||
Unfortunately those preexesting classes don't line up perfectly with the needs of this modal,
|
||||
so many of the other styles for those classes don't apply or are overridden.
|
||||
|
||||
@import "app/styles/style-flat-variables"
|
||||
|
||||
#create-account-modal
|
||||
.modal-dialog
|
||||
width: 850px
|
||||
|
||||
.modal-content
|
||||
display: flex
|
||||
flex-direction: column
|
||||
height: 850px
|
||||
width: 850px
|
||||
text-align: center
|
||||
padding: 0
|
||||
border: none
|
||||
|
||||
// General modal stuff
|
||||
|
||||
.close
|
||||
color: white
|
||||
opacity: 0.5
|
||||
right: 7px
|
||||
&:hover
|
||||
opacity: 0.9
|
||||
|
||||
.modal-header, .modal-footer
|
||||
&.teacher
|
||||
background-color: $burgandy
|
||||
|
||||
&.student
|
||||
background-color: $forest
|
||||
|
||||
.modal-header
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
justify-content: flex-end
|
||||
height: 100px
|
||||
padding: 0
|
||||
|
||||
background-color: $navy
|
||||
|
||||
h3
|
||||
color: white
|
||||
|
||||
.modal-footer
|
||||
padding: 0
|
||||
margin: 0
|
||||
height: 50px
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
|
||||
background-color: $navy
|
||||
|
||||
span
|
||||
color: white
|
||||
|
||||
a span
|
||||
text-decoration: underline
|
||||
|
||||
#choose-account-type-view, #segment-check-view, #basic-info-view, #coppa-deny-view, #single-sign-on-already-exists-view, #single-sign-on-confirm-view, #confirmation-view
|
||||
display: flex
|
||||
flex-direction: column
|
||||
flex-grow: 1
|
||||
|
||||
.modal-body
|
||||
display: flex
|
||||
flex-direction: column
|
||||
flex-grow: 1
|
||||
|
||||
.modal-body-content
|
||||
flex-grow: 2
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
justify-content: center
|
||||
|
||||
// Back/forward buttons
|
||||
|
||||
.history-nav-buttons
|
||||
width: 100%
|
||||
display: flex
|
||||
flex-direction: row-reverse
|
||||
flex-grow: 1
|
||||
align-items: flex-end
|
||||
justify-content: space-between
|
||||
|
||||
.btn
|
||||
// Undo .style-flat's .btn ~ .btn margin
|
||||
margin: 0
|
||||
|
||||
&.just-one
|
||||
flex-direction: row
|
||||
|
||||
// Forms
|
||||
|
||||
.form-container
|
||||
width: 800px
|
||||
|
||||
.form-group
|
||||
text-align: left
|
||||
|
||||
.full-name
|
||||
display: flex
|
||||
flex-direction: row
|
||||
|
||||
.form-group
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-content: flex-start
|
||||
|
||||
&.text-center
|
||||
text-align: center
|
||||
|
||||
input
|
||||
height: 40px
|
||||
|
||||
&.row
|
||||
display: block
|
||||
|
||||
&.last-initial
|
||||
margin-left: 30px
|
||||
width: auto
|
||||
|
||||
input
|
||||
width: 70px
|
||||
|
||||
&.subscribe
|
||||
width: 100%
|
||||
|
||||
// Fancy text inside horizontal rules
|
||||
|
||||
.hr-text
|
||||
position: relative
|
||||
hr
|
||||
width: 430px
|
||||
padding: 0
|
||||
border: none
|
||||
border-top: thin solid #444
|
||||
color: #444
|
||||
span
|
||||
position: absolute
|
||||
left: 50%
|
||||
top: 0.45em
|
||||
transform: translateX(-50%)
|
||||
padding: 0 0.75em
|
||||
font-weight: bold
|
||||
font-size: 10pt
|
||||
background: white
|
||||
|
||||
|
||||
// Glyphicon colors
|
||||
|
||||
.glyphicon-ok-circle
|
||||
color: green
|
||||
|
||||
.glyphicon-remove-circle
|
||||
color: red
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
#segment-check-view
|
||||
.class-code-input-group
|
||||
display: flex
|
||||
flex-direction: row
|
||||
align-items: center
|
||||
.class-code-input
|
||||
text-align: center
|
||||
|
||||
.glyphicon
|
||||
width: 0
|
||||
line-height: 40px
|
||||
font-size: 30px
|
||||
|
||||
.classroom-name
|
||||
font-size: 26pt
|
||||
|
||||
.teacher-name
|
||||
font-size: 14pt
|
|
@ -0,0 +1,5 @@
|
|||
#single-sign-on-already-exists-view
|
||||
.modal-body
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
|
@ -0,0 +1,5 @@
|
|||
#single-sign-on-confirm-view
|
||||
.modal-body
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
|
@ -68,7 +68,7 @@
|
|||
margin-top: 10px
|
||||
|
||||
&.alt-image
|
||||
background-image: url("/images/pages/home/student_jumbotron.png")
|
||||
background-image: url("/images/pages/home/student_jumbotron.jpg")
|
||||
background-position: 60% 35%
|
||||
|
||||
.well
|
||||
|
|
|
@ -393,6 +393,12 @@ body[lang='ru'], body[lang='uk'], body[lang='bg'], body[lang^='mk'], body[lang='
|
|||
.text-navy
|
||||
color: $navy
|
||||
|
||||
.text-burgandy
|
||||
color: $burgandy
|
||||
|
||||
.text-forest
|
||||
color: $forest
|
||||
|
||||
.bg-navy
|
||||
background-color: $navy
|
||||
color: white
|
||||
|
@ -490,4 +496,4 @@ body[lang='ru'], body[lang='uk'], body[lang='bg'], body[lang^='mk'], body[lang='
|
|||
.button.close
|
||||
position: absolute
|
||||
top: 10px
|
||||
left: 10px
|
||||
right: 10px
|
||||
|
|
|
@ -44,6 +44,9 @@ block content
|
|||
ul
|
||||
li
|
||||
a(href="/admin/classroom-levels") Classroom Levels
|
||||
li
|
||||
button.classroom-progress-csv.btn.btn-sm.btn-success Classroom Progress CSV
|
||||
input.classroom-progress-class-code(type=text value="<class code>")
|
||||
li
|
||||
a(href="/admin/analytics") Dashboard
|
||||
li
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
extends /templates/core/modal-base-flat
|
||||
|
||||
block modal-header-content
|
||||
|
||||
block modal-body-content
|
||||
p
|
||||
h2(data-i18n="coppa_deny.text1")
|
||||
div(data-i18n="coppa_deny.text2")
|
||||
|
||||
block modal-footer-content
|
||||
a.btn.btn-primary(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="coppa_deny.close").btn
|
|
@ -1,118 +0,0 @@
|
|||
.modal-dialog
|
||||
.modal-content
|
||||
img(src="/images/pages/modal/auth/signup-background.png", draggable="false").auth-modal-background
|
||||
h1(data-i18n="login.sign_up")
|
||||
|
||||
div#close-modal
|
||||
span.glyphicon.glyphicon-remove
|
||||
|
||||
form.auth-form-content
|
||||
if view.options.showRequiredError
|
||||
#required-error-alert.alert.alert-success
|
||||
span(data-i18n="signup.required")
|
||||
if view.options.showSignupRationale
|
||||
#signup-rationale-alert.alert.alert-info
|
||||
span(data-i18n="play_level.victory_sign_up_poke")
|
||||
|
||||
#email-password-row.row
|
||||
.col-md-6
|
||||
.form-group
|
||||
label.control-label(for="email")
|
||||
span(data-i18n="general.email")
|
||||
| :
|
||||
.input-border
|
||||
input#email.input-large.form-control(name="email", type="email", value=view.previousFormInputs.email, tabindex=1)
|
||||
|
||||
.form-group
|
||||
label.control-label(for="password")
|
||||
span(data-i18n="general.password")
|
||||
| :
|
||||
.input-border
|
||||
input#password.input-large.form-control(name="password", type="password", value=view.previousFormInputs.password, tabindex=2)
|
||||
|
||||
.col-md-6
|
||||
.auth-network-logins.text-center
|
||||
strong(data-i18n="signup.or_sign_up_with")
|
||||
button#facebook-signup-btn.btn.btn-primary.btn-lg.btn-illustrated.network-login(type="button", disabled=true)
|
||||
img.network-logo(src="/images/pages/community/logo_facebook.png", draggable="false")
|
||||
span.sign-in-blurb(data-i18n="login.sign_in_with_facebook")
|
||||
button#gplus-signup-btn.btn.btn-danger.btn-lg.btn-illustrated.network-login(type="button", disabled=true)
|
||||
img.network-logo(src="/images/pages/community/logo_g+.png", draggable="false")
|
||||
span.sign-in-blurb(data-i18n="login.sign_in_with_gplus")
|
||||
.gplus-login-wrapper
|
||||
.gplus-login-button
|
||||
|
||||
#gplus-logged-in-row.row.text-center.hide
|
||||
h2(data-i18n="signup.connected_gplus_header")
|
||||
p(data-i18n="signup.connected_gplus_p")
|
||||
|
||||
#gplus-account-exists-row.row.text-center.hide
|
||||
h2(data-i18n="signup.gplus_exists")
|
||||
a.btn.btn-primary#gplus-login-btn(data-i18n="login.log_in")
|
||||
|
||||
#facebook-logged-in-row.row.text-center.hide
|
||||
h2(data-i18n="signup.connected_facebook_header")
|
||||
p(data-i18n="signup.connected_facebook_p")
|
||||
|
||||
#facebook-account-exists-row.row.text-center.hide
|
||||
h2(data-i18n="signup.facebook_exists")
|
||||
a.btn.btn-primary#facebook-login-btn(data-i18n="login.log_in")
|
||||
|
||||
.row
|
||||
.col-md-6
|
||||
.form-group
|
||||
label.control-label(for="name")
|
||||
span(data-i18n="general.username")
|
||||
| :
|
||||
.input-border
|
||||
if me.get('name')
|
||||
input#name.input-large.form-control(name="name", type="text", value=me.get('name'), tabindex=3)
|
||||
else
|
||||
input#name.input-large.form-control(name="name", type="text", value="", placeholder="e.g. Alex W the Skater", tabindex=3)
|
||||
|
||||
.form-group
|
||||
label.control-label(for="birthday-input")
|
||||
span(data-i18n="signup.birthday")
|
||||
| :
|
||||
.input-border
|
||||
select#birthday-month-input.input-large.form-control(name="birthdayMonth", value=view.previousFormInputs.birthdayMonth || '', tabindex=4, style="width: 106px; float: left")
|
||||
option(value='',data-i18n="calendar.month")
|
||||
for name, val in ['january','february','march','april','may','june','july','august','september','october','november','december']
|
||||
option(data-i18n="calendar.#{name}" value=val+1)
|
||||
select#birthday-day-input.input-large.form-control(name="birthdayDay", value=view.previousFormInputs.birthdayDay || '', tabindex=5, style="width: 75px; float: left")
|
||||
option(value='',data-i18n="calendar.day")
|
||||
for dummy, val in new Array(31)
|
||||
option #{val+1}
|
||||
select#birthday-year-input.input-large.form-control(name="birthdayYear", value=view.previousFormInputs.birthdayMonth || '', tabindex=6, style="width: 90px;")
|
||||
option(value='',data-i18n="calendar.year")
|
||||
- var startYear = new Date().getFullYear() - 1
|
||||
for dummy, val in new Array(100)
|
||||
option #{startYear-val}
|
||||
|
||||
.form-group
|
||||
label.control-label(for="school-input")
|
||||
span.spr(data-i18n="courses.class_code")
|
||||
em.optional-note
|
||||
| (
|
||||
span(data-i18n="signup.optional")
|
||||
| ):
|
||||
.input-border
|
||||
input#class-code-input.input-large.form-control(name="classCode", value=view.previousFormInputs.classCode || '', tabindex=7)
|
||||
|
||||
.col-md-6
|
||||
.form-group.checkbox
|
||||
label.control-label(for="subscribe")
|
||||
.input-border
|
||||
input#subscribe(type="checkbox", checked='checked')
|
||||
span.custom-checkbox
|
||||
.glyphicon.glyphicon-ok
|
||||
span(data-i18n="signup.email_announcements")
|
||||
|
||||
.well(data-i18n="signup.hey_students")
|
||||
|
||||
|
||||
input#signup-button.btn.btn-lg.btn-illustrated.btn-block.btn-success(value=translate("signup.sign_up"), type="submit")
|
||||
|
||||
.extra-pane
|
||||
.switch-explanation(data-i18n="signup.login_switch")
|
||||
.btn.btn-lg.btn-illustrated.btn-warning#switch-to-login-btn(data-i18n="login.log_in")
|
105
app/templates/core/create-account-modal/basic-info-view.jade
Normal file
|
@ -0,0 +1,105 @@
|
|||
form#basic-info-form.modal-body.basic-info
|
||||
.modal-body-content
|
||||
.auth-network-logins.text-center
|
||||
h4
|
||||
span(data-i18n="signup.connect_with")
|
||||
a#facebook-signup-btn.btn.btn-primary.btn-lg.btn-illustrated.network-login(disabled=!view.signupState.get('facebookEnabled'), data-sso-used="facebook")
|
||||
img.network-logo(src="/images/pages/modal/auth/facebook_sso_button.png", draggable="false", width="175", height="40")
|
||||
span.sign-in-blurb(data-i18n="login.sign_in_with_facebook")
|
||||
a#gplus-signup-btn.btn.btn-danger.btn-lg.btn-illustrated.network-login(disabled=!view.signupState.get('gplusEnabled'), data-sso-used="gplus")
|
||||
img.network-logo(src="/images/pages/modal/auth/gplus_sso_button.png", draggable="false", width="175", height="40")
|
||||
span.sign-in-blurb(data-i18n="login.sign_in_with_gplus")
|
||||
.gplus-login-wrapper
|
||||
.gplus-login-button
|
||||
|
||||
.hr-text
|
||||
hr
|
||||
span(data-i18n="general.or")
|
||||
|
||||
div.form-container
|
||||
if ['student', 'teacher'].indexOf(view.signupState.get('path')) !== -1
|
||||
.row.full-name
|
||||
.col-xs-offset-3.col-xs-5
|
||||
.form-group
|
||||
label.control-label(for="first-name-input")
|
||||
span(data-i18n="general.first_name")
|
||||
input#first-name-input.form-control.input-lg(name="firstName")
|
||||
.col-xs-4
|
||||
.last-initial.form-group
|
||||
label.control-label(for="last-name-input")
|
||||
span(data-i18n="general.last_initial")
|
||||
input#last-name-input.form-control.input-lg(name="lastName" maxlength="1")
|
||||
.form-group
|
||||
.row
|
||||
.col-xs-5.col-xs-offset-3
|
||||
label.control-label(for="email-input")
|
||||
span(data-i18n="share_progress_modal.form_label")
|
||||
.col-xs-5.col-xs-offset-3
|
||||
input.form-control.input-lg#email-input(name="email" type="email")
|
||||
.col-xs-4.email-check
|
||||
- var checkEmailState = view.state.get('checkEmailState');
|
||||
if checkEmailState === 'checking'
|
||||
span.small(data-i18n="signup.checking")
|
||||
if checkEmailState === 'exists'
|
||||
span.small
|
||||
span.text-burgandy.glyphicon.glyphicon-remove-circle
|
||||
=" "
|
||||
span(data-i18n="signup.account_exists")
|
||||
=" "
|
||||
a.login-link(data-i18n="signup.sign_in")
|
||||
|
||||
if checkEmailState === 'available'
|
||||
span.small
|
||||
span.text-forest.glyphicon.glyphicon-ok-circle
|
||||
=" "
|
||||
span(data-i18n="signup.email_good")
|
||||
.form-group
|
||||
.row
|
||||
.col-xs-7.col-xs-offset-3
|
||||
label.control-label(for="username-input")
|
||||
span(data-i18n="general.username")
|
||||
.col-xs-5.col-xs-offset-3
|
||||
input.form-control.input-lg#username-input(name="name")
|
||||
.col-xs-4.name-check
|
||||
- var checkNameState = view.state.get('checkNameState');
|
||||
if checkNameState === 'checking'
|
||||
span.small(data-i18n="signup.checking")
|
||||
if checkNameState === 'exists'
|
||||
span.small
|
||||
span.text-burgandy.glyphicon.glyphicon-remove-circle
|
||||
=" "
|
||||
span= view.state.get('suggestedNameText')
|
||||
if checkNameState === 'available'
|
||||
span.small
|
||||
span.text-forest.glyphicon.glyphicon-ok-circle
|
||||
=" "
|
||||
span(data-i18n="signup.name_available")
|
||||
.form-group
|
||||
.row
|
||||
.col-xs-7.col-xs-offset-3
|
||||
label.control-label(for="password-input")
|
||||
span(data-i18n="general.password")
|
||||
.col-xs-5.col-xs-offset-3
|
||||
input.form-control.input-lg#password-input(name="password" type="password")
|
||||
.form-group.checkbox.subscribe
|
||||
.row
|
||||
.col-xs-7.col-xs-offset-3
|
||||
.checkbox
|
||||
label
|
||||
input#subscribe-input(type="checkbox" checked="checked" name="subscribe")
|
||||
span(data-i18n="signup.email_announcements")
|
||||
|
||||
.error-area
|
||||
- var error = view.state.get('error');
|
||||
if error
|
||||
.row
|
||||
.col-xs-7.col-xs-offset-3
|
||||
.alert.alert-danger= error
|
||||
|
||||
// In reverse order for tabbing purposes
|
||||
.history-nav-buttons
|
||||
button#create-account-btn.next-button.btn.btn-lg.btn-navy(type='submit')
|
||||
span(data-i18n="signup.create_account")
|
||||
|
||||
button.back-button.btn.btn-lg.btn-navy-alt(type='button')
|
||||
span(data-i18n="common.back")
|
|
@ -0,0 +1,50 @@
|
|||
.modal-body-content
|
||||
h4
|
||||
span(data-i18n="signup.choose_type")
|
||||
.path-cards
|
||||
.path-card.navy
|
||||
.card-title
|
||||
span(data-i18n="courses.teacher")
|
||||
.card-content
|
||||
h6.card-description
|
||||
span(data-i18n="signup.teacher_type_1")
|
||||
ul.small.m-t-1
|
||||
li
|
||||
span(data-i18n="signup.teacher_type_2")
|
||||
li
|
||||
span(data-i18n="signup.teacher_type_3")
|
||||
li
|
||||
span(data-i18n="signup.teacher_type_4")
|
||||
.card-footer
|
||||
button.btn.btn-lg.btn-navy.teacher-path-button
|
||||
.text-h6
|
||||
span(data-i18n="signup.signup_as_teacher")
|
||||
|
||||
.path-card.forest
|
||||
.card-title
|
||||
span(data-i18n="courses.student")
|
||||
.card-content
|
||||
h6.card-description
|
||||
span(data-i18n="signup.student_type_1")
|
||||
ul.small.m-t-1
|
||||
li
|
||||
span(data-i18n="signup.student_type_2")
|
||||
li
|
||||
span(data-i18n="signup.student_type_3")
|
||||
li
|
||||
span(data-i18n="signup.student_type_4")
|
||||
.card-footer
|
||||
i.small
|
||||
span(data-i18n="signup.student_type_5")
|
||||
button.btn.btn-lg.btn-forest.student-path-button
|
||||
.text-h6
|
||||
span(data-i18n="signup.signup_as_student")
|
||||
|
||||
.individual-section
|
||||
.individual-title
|
||||
span(data-i18n="signup.individuals_or_parents")
|
||||
p.individual-description.small
|
||||
span(data-i18n="signup.individual_type")
|
||||
button.btn.btn-lg.btn-navy.individual-path-button
|
||||
.text-h6
|
||||
span(data-i18n="signup.signup_as_individual")
|
|
@ -0,0 +1,35 @@
|
|||
.modal-body
|
||||
.modal-body-content
|
||||
|
||||
h4.m-y-1(data-i18n="signup.account_created")
|
||||
|
||||
.text-center.m-y-1
|
||||
if view.signupState.get('path') === 'student'
|
||||
p(data-i18n="signup.confirm_student_blurb")
|
||||
|
||||
else
|
||||
p(data-i18n="signup.confirm_individual_blurb")
|
||||
|
||||
.signup-info-box-wrapper.m-y-3
|
||||
.text-burgandy(data-i18n="signup.write_this_down")
|
||||
.signup-info-box.text-center
|
||||
if me.get('name')
|
||||
h4
|
||||
b
|
||||
span(data-i18n="general.username")
|
||||
| : #{me.get('name')}
|
||||
if me.get('email')
|
||||
h5
|
||||
b
|
||||
- var ssoUsed = view.signupState.get('ssoUsed');
|
||||
if ssoUsed === 'facebook'
|
||||
img.m-r-1(src="/images/pages/modal/auth/facebook_small.png")
|
||||
= me.get('email')
|
||||
else if ssoUsed === 'gplus'
|
||||
img.m-r-1(src="/images/pages/modal/auth/gplus_small.png")
|
||||
= me.get('email')
|
||||
else
|
||||
span(data-i18n="general.email")
|
||||
| : #{me.get('email')}
|
||||
|
||||
button#start-btn.btn.btn-navy.btn-lg.m-y-3(data-i18n="signup.start_playing")
|
33
app/templates/core/create-account-modal/coppa-deny-view.jade
Normal file
|
@ -0,0 +1,33 @@
|
|||
form.modal-body.coppa-deny
|
||||
.modal-body-content
|
||||
.parent-email-input-group.form-group
|
||||
if !view.state.get('parentEmailSent')
|
||||
label.control-label.text-h4(for="parent-email-input")
|
||||
span(data-i18n="signup.enter_parent_email")
|
||||
input#parent-email-input(type="email" name="parentEmail" value=state.get('parentEmail'))
|
||||
|
||||
if state.get('error')
|
||||
p.small.error
|
||||
span(data-i18n="signup.parent_email_error")
|
||||
|
||||
p.small.parent-email-blurb.render
|
||||
span
|
||||
!= translate('signup.parent_email_blurb').replace('{{email_link}}', '<a href="mailto:team@codecombat.com">team@codecombat.com</a>')
|
||||
|
||||
|
||||
if view.state.get('parentEmailSent')
|
||||
p.small.parent-email-blurb
|
||||
span(data-i18n="signup.parent_email_sent")
|
||||
|
||||
a.btn.btn-navy.btn-lg(href="/play" data-dismiss="modal") Play without saving
|
||||
|
||||
// In reverse order for tabbing purposes
|
||||
.history-nav-buttons
|
||||
button.send-parent-email-button.btn.btn-lg.btn-navy(type='submit', disabled=state.get('parentEmailSent') || state.get('parentEmailSending'))
|
||||
if state.get('parentEmailSent')
|
||||
span(data-i18n="common.sent")
|
||||
else
|
||||
span(data-i18n="common.send")
|
||||
|
||||
button.back-btn.btn.btn-lg.btn-navy-alt(type='button')
|
||||
span(data-i18n="common.back")
|
|
@ -0,0 +1,60 @@
|
|||
extends /templates/core/modal-base-flat
|
||||
|
||||
block modal-header
|
||||
//-
|
||||
This allows for the header color to switch without the subview templates
|
||||
needing to contain the header
|
||||
.modal-header(class=view.signupState.get('path'))
|
||||
span.glyphicon.glyphicon-remove.button.close(data-dismiss="modal", aria-hidden="true")
|
||||
+modal-header-content
|
||||
|
||||
mixin modal-header-content
|
||||
h3
|
||||
case view.signupState.get('path')
|
||||
when 'student'
|
||||
span(data-i18n="signup.create_student_header")
|
||||
when 'teacher'
|
||||
span(data-i18n="signup.create_teacher_header")
|
||||
when 'individual'
|
||||
span(data-i18n="signup.create_individual_header")
|
||||
default
|
||||
span(data-i18n="signup.create_header")
|
||||
|
||||
//-
|
||||
This is where the subviews (screens) are hooked up.
|
||||
Most subview templates have a .modal-body at their root, but this is inconsistent and needs organization.
|
||||
block modal-body
|
||||
case view.signupState.get('screen')
|
||||
when 'choose-account-type'
|
||||
#choose-account-type-view
|
||||
when 'segment-check'
|
||||
#segment-check-view
|
||||
when 'basic-info'
|
||||
#basic-info-view
|
||||
when 'coppa-deny'
|
||||
#coppa-deny-view
|
||||
when 'sso-already-exists'
|
||||
#single-sign-on-already-exists-view
|
||||
when 'sso-confirm'
|
||||
#single-sign-on-confirm-view
|
||||
when 'confirmation'
|
||||
#confirmation-view
|
||||
//- This is not yet implemented
|
||||
//- when 'extras'
|
||||
//- #extras-view
|
||||
|
||||
|
||||
block modal-footer
|
||||
//-
|
||||
This allows for the footer color to switch without the subview templates
|
||||
needing to contain the footer
|
||||
.modal-footer(class=view.signupState.get('path'))
|
||||
+modal-footer-content
|
||||
|
||||
mixin modal-footer-content
|
||||
if view.signupState.get('screen') !== 'confirmation'
|
||||
.modal-footer-content
|
||||
.small-details
|
||||
span.spr(data-i18n="signup.login_switch")
|
||||
a.login-link
|
||||
span(data-i18n="signup.sign_in")
|
|
@ -0,0 +1,69 @@
|
|||
form.modal-body.segment-check
|
||||
.modal-body-content
|
||||
case view.signupState.get('path')
|
||||
when 'student'
|
||||
span(data-i18n="signup.enter_class_code")
|
||||
.class-code-input-group.form-group
|
||||
input.class-code-input(name="classCode" value=view.signupState.get('classCode'))
|
||||
.render
|
||||
unless _.isEmpty(view.signupState.get('classCode'))
|
||||
if state.get('classCodeValid')
|
||||
span.glyphicon.glyphicon-ok-circle.class-code-valid-icon
|
||||
else
|
||||
span.glyphicon.glyphicon-remove-circle.class-code-valid-icon
|
||||
|
||||
p.render
|
||||
if _.isEmpty(view.signupState.get('classCode'))
|
||||
span(data-i18n="signup.ask_teacher_1")
|
||||
else if state.get('classCodeValid')
|
||||
span.small(data-i18n="signup.about_to_join")
|
||||
br
|
||||
span.classroom-name= view.classroom.get('name')
|
||||
br
|
||||
span.teacher-name= view.classroom.owner.get('name')
|
||||
else
|
||||
span(data-i18n="signup.classroom_not_found")
|
||||
if _.isEmpty(view.signupState.get('classCode')) || !state.get('classCodeValid')
|
||||
br
|
||||
span.spr(data-i18n="signup.ask_teacher_2")
|
||||
a.individual-path-button
|
||||
span(data-i18n="signup.ask_teacher_3")
|
||||
span.spl(data-i18n="signup.ask_teacher_4")
|
||||
|
||||
when 'teacher'
|
||||
// TODO
|
||||
when 'individual'
|
||||
.birthday-form-group.form-group
|
||||
span(data-i18n="signup.enter_birthdate")
|
||||
.input-border
|
||||
select#birthday-month-input.input-large.form-control(name="birthdayMonth", style="width: 106px; float: left")
|
||||
option(value='',data-i18n="calendar.month")
|
||||
for name, index in ['january','february','march','april','may','june','july','august','september','october','november','december']
|
||||
- var month = index + 1
|
||||
option(data-i18n="calendar.#{name}" value=month, selected=(month == view.signupState.get('birthdayMonth')))
|
||||
select#birthday-day-input.input-large.form-control(name="birthdayDay", style="width: 75px; float: left")
|
||||
option(value='',data-i18n="calendar.day")
|
||||
for day in _.range(1,32)
|
||||
option(selected=(day == view.signupState.get('birthdayDay'))) #{day}
|
||||
select#birthday-year-input.input-large.form-control(name="birthdayYear", style="width: 90px;")
|
||||
option(value='',data-i18n="calendar.year")
|
||||
- var thisYear = new Date().getFullYear()
|
||||
for year in _.range(thisYear, thisYear - 100, -1)
|
||||
option(selected=(year == view.signupState.get('birthdayYear'))) #{year}
|
||||
|
||||
default
|
||||
p
|
||||
span Sign-up error, please contact
|
||||
=" "
|
||||
a(href="mailto:support@codecombat.com") support@codecombat.com
|
||||
| .
|
||||
|
||||
// In reverse order for tabbing purposes
|
||||
.history-nav-buttons
|
||||
//- disabled=!view.signupState.get('segmentCheckValid')
|
||||
button.next-button.btn.btn-lg.btn-navy(type='submit')
|
||||
span(data-i18n="about.next")
|
||||
|
||||
button.back-to-account-type.btn.btn-lg.btn-navy-alt(type='button')
|
||||
span(data-i18n="common.back")
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
.modal-body
|
||||
.modal-body-content
|
||||
if view.signupState.get('ssoUsed')
|
||||
h4
|
||||
span(data-i18n="signup.account_exists")
|
||||
div.small
|
||||
b
|
||||
span= view.signupState.get('email')
|
||||
|
||||
.hr-text
|
||||
hr
|
||||
span(data-i18n="common.continue")
|
||||
|
||||
button.login-link.btn.btn-lg.btn-navy
|
||||
span(data-i18n="login.log_in")
|
||||
|
||||
.history-nav-buttons.just-one
|
||||
button.back-button.btn.btn-lg.btn-navy-alt(type='button')
|
||||
span(data-i18n="common.back")
|
|
@ -0,0 +1,57 @@
|
|||
form#basic-info-form.modal-body
|
||||
.modal-body-content
|
||||
h4
|
||||
span(data-i18n="signup.sso_connected")
|
||||
div.small.m-y-1
|
||||
- var ssoUsed = view.signupState.get('ssoUsed');
|
||||
if ssoUsed === 'facebook'
|
||||
img(src="/images/pages/modal/auth/facebook_small.png")
|
||||
if ssoUsed === 'gplus'
|
||||
img(src="/images/pages/modal/auth/gplus_small.png")
|
||||
b.m-x-1
|
||||
span= view.signupState.get('email')
|
||||
span.glyphicon.glyphicon-ok-circle.class-code-valid-icon
|
||||
|
||||
.hr-text.m-y-3
|
||||
hr
|
||||
span(data-i18n="common.continue")
|
||||
|
||||
.form-container
|
||||
input.hidden(name="email" value=view.signupState.get('email'))
|
||||
.form-group
|
||||
.row
|
||||
.col-xs-7.col-xs-offset-3
|
||||
label.control-label(for="username-input")
|
||||
span(data-i18n="general.username")
|
||||
.col-xs-5.col-xs-offset-3
|
||||
input.form-control.input-lg#username-input(name="name")
|
||||
.col-xs-4.name-check
|
||||
- var checkNameState = view.state.get('checkNameState');
|
||||
if checkNameState === 'checking'
|
||||
span.small(data-i18n="signup.checking")
|
||||
if checkNameState === 'exists'
|
||||
span.small
|
||||
span.text-burgandy.glyphicon.glyphicon-remove-circle
|
||||
=" "
|
||||
span= view.state.get('suggestedNameText')
|
||||
if checkNameState === 'available'
|
||||
span.small
|
||||
span.text-forest.glyphicon.glyphicon-ok-circle
|
||||
=" "
|
||||
span(data-i18n="signup.name_available")
|
||||
|
||||
.form-group.subscribe
|
||||
.row
|
||||
.col-xs-7.col-xs-offset-3
|
||||
.checkbox
|
||||
label
|
||||
input#subscribe-input(type="checkbox" checked="checked" name="subscribe")
|
||||
span(data-i18n="signup.email_announcements")
|
||||
|
||||
// In reverse order for tabbing purposes
|
||||
.history-nav-buttons
|
||||
button.next-button.btn.btn-lg.btn-navy(type='submit')
|
||||
span(data-i18n="signup.create_account")
|
||||
|
||||
button.back-button.btn.btn-lg.btn-navy-alt(type='button')
|
||||
span(data-i18n="common.back")
|
|
@ -36,6 +36,9 @@ block modal-footer-content
|
|||
button.btn.btn-sm#agreement-button(data-i18n="versions.cla_agree") I AGREE
|
||||
if view.isPatch
|
||||
.alert.alert-info(data-i18n="versions.owner_approve") An owner will need to approve it before your changes will become visible.
|
||||
.save-error-area
|
||||
if view.savingPatchError
|
||||
.alert.alert-danger Unable to save patch: #{view.savingPatchError}
|
||||
|
||||
.buttons
|
||||
button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel
|
||||
|
|
|
@ -7,7 +7,7 @@ block content
|
|||
if view.leagueType === 'course' && view.course
|
||||
#course-header
|
||||
#course-details-link
|
||||
a(href="/courses/{#view.course.id}/{#view.league.id}")
|
||||
a(href="/courses/#{view.course.id}/#{view.league.id}")
|
||||
span.glyphicon.glyphicon-arrow-left
|
||||
span.spl Levels
|
||||
|
||||
|
|
|
@ -110,6 +110,14 @@ module.exports = class NewHomeView extends RootView
|
|||
$(window).on 'resize', @fitToPage
|
||||
@fitToPage()
|
||||
setTimeout(@fitToPage, 0)
|
||||
if me.isAnonymous()
|
||||
CreateAccountModal = require 'views/core/CreateAccountModal/CreateAccountModal'
|
||||
if document.location.hash is '#create-account'
|
||||
@openModalView(new CreateAccountModal())
|
||||
if document.location.hash is '#create-account-individual'
|
||||
@openModalView(new CreateAccountModal({startOnPath: 'individual'}))
|
||||
if document.location.hash is '#create-account-student'
|
||||
@openModalView(new CreateAccountModal({startOnPath: 'student'}))
|
||||
super()
|
||||
|
||||
destroy: ->
|
||||
|
|
|
@ -68,14 +68,13 @@ module.exports = TestView = class TestView extends RootView
|
|||
|
||||
specDone: (result) ->
|
||||
if result.status is 'failed'
|
||||
console.log 'result', result
|
||||
report = {
|
||||
suiteDescriptions: _.clone(@suiteStack)
|
||||
failMessages: (fe.message for fe in result.failedExpectations)
|
||||
testDescription: result.description
|
||||
}
|
||||
view.failureReports.push(report)
|
||||
view.renderSelectors('#failure-reports')
|
||||
view?.failureReports.push(report)
|
||||
view?.renderSelectors('#failure-reports')
|
||||
|
||||
suiteStarted: (result) ->
|
||||
@suiteStack.push(result.description)
|
||||
|
|
|
@ -4,7 +4,14 @@ RootView = require 'views/core/RootView'
|
|||
template = require 'templates/admin'
|
||||
AdministerUserModal = require 'views/admin/AdministerUserModal'
|
||||
forms = require 'core/forms'
|
||||
|
||||
Campaigns = require 'collections/Campaigns'
|
||||
Classroom = require 'models/Classroom'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
Course = require 'models/Course'
|
||||
LevelSessions = require 'collections/LevelSessions'
|
||||
User = require 'models/User'
|
||||
Users = require 'collections/Users'
|
||||
|
||||
module.exports = class MainAdminView extends RootView
|
||||
id: 'admin-view'
|
||||
|
@ -19,14 +26,37 @@ module.exports = class MainAdminView extends RootView
|
|||
'click #user-search-result': 'onClickUserSearchResult'
|
||||
'click #create-free-sub-btn': 'onClickFreeSubLink'
|
||||
'click #terminal-create': 'onClickTerminalSubLink'
|
||||
'click .classroom-progress-csv': 'onClickExportProgress'
|
||||
|
||||
getTitle: -> return $.i18n.t('account_settings.admin')
|
||||
|
||||
initialize: ->
|
||||
@campaigns = new Campaigns()
|
||||
@courses = new CocoCollection([], { url: "/db/course", model: Course})
|
||||
|
||||
if window.amActually
|
||||
@amActually = new User({_id: window.amActually})
|
||||
@amActually.fetch()
|
||||
@supermodel.trackModel(@amActually)
|
||||
if me.isAdmin()
|
||||
@supermodel.trackRequest @campaigns.fetchByType('course', { data: { project: 'levels' } })
|
||||
@supermodel.loadCollection(@courses, 'courses')
|
||||
super()
|
||||
|
||||
onLoaded: ->
|
||||
campaignCourseIndexMap = {}
|
||||
for course, index in @courses.models
|
||||
campaignCourseIndexMap[course.get('campaignID')] = index + 1
|
||||
@courseLevels = []
|
||||
for campaign in @campaigns.models
|
||||
continue unless campaignCourseIndexMap[campaign.id]
|
||||
for levelID, level of campaign.get('levels')
|
||||
@courseLevels.push({
|
||||
levelID
|
||||
slug: level.slug
|
||||
courseIndex: campaignCourseIndexMap[campaign.id]
|
||||
})
|
||||
super()
|
||||
|
||||
onClickStopSpyingButton: ->
|
||||
button = @$('#stop-spying-btn')
|
||||
|
@ -126,3 +156,85 @@ module.exports = class MainAdminView extends RootView
|
|||
console.error 'Failed to create prepaid', response
|
||||
@supermodel.addRequestResource('create_prepaid', options, 0).load()
|
||||
|
||||
onClickExportProgress: ->
|
||||
return unless @courseLevels?.length > 0
|
||||
$('.classroom-progress-csv').prop('disabled', true)
|
||||
|
||||
classCode = $('.classroom-progress-class-code').val()
|
||||
userMap = {}
|
||||
new Promise((resolve, reject) =>
|
||||
new Classroom().fetchByCode(classCode, {
|
||||
success: resolve
|
||||
error: (model, response, options) => reject(response)
|
||||
})
|
||||
)
|
||||
.then (classroom) =>
|
||||
new Promise((resolve, reject) =>
|
||||
new Classroom({ _id: classroom.id }).fetch({
|
||||
success: resolve
|
||||
error: (model, response, options) => reject(response)
|
||||
})
|
||||
)
|
||||
.then (classroom) =>
|
||||
new Promise((resolve, reject) =>
|
||||
new Users().fetchForClassroom(classroom, {
|
||||
success: (models, response, options) =>
|
||||
resolve([classroom, models]) if models?.loaded
|
||||
error: (models, response, options) => reject(response)
|
||||
})
|
||||
)
|
||||
.then ([classroom, users]) =>
|
||||
userMap[user.id] = user for user in users.models
|
||||
new Promise((resolve, reject) =>
|
||||
new LevelSessions().fetchForAllClassroomMembers(classroom, {
|
||||
success: (models, response, options) =>
|
||||
resolve(models) if models?.loaded
|
||||
error: (models, response, options) => reject(response)
|
||||
})
|
||||
)
|
||||
.then (sessions) =>
|
||||
userLevelPlaytimeMap = {}
|
||||
for session in sessions.models
|
||||
continue unless session.get('state')?.complete
|
||||
levelID = session.get('level').original
|
||||
userID = session.get('creator')
|
||||
userLevelPlaytimeMap[userID] ?= {}
|
||||
userLevelPlaytimeMap[userID][levelID] ?= {}
|
||||
userLevelPlaytimeMap[userID][levelID] = session.get('playtime')
|
||||
|
||||
userPlaytimes = []
|
||||
for userID, user of userMap
|
||||
playtimes = [user.get('name') ? 'Anonymous']
|
||||
for level in @courseLevels
|
||||
if userLevelPlaytimeMap[userID]?[level.levelID]?
|
||||
rawSeconds = parseInt(userLevelPlaytimeMap[userID][level.levelID])
|
||||
hours = Math.floor(rawSeconds / 60 / 60)
|
||||
minutes = Math.floor(rawSeconds / 60 - hours * 60)
|
||||
seconds = Math.round(rawSeconds - hours * 60 - minutes * 60)
|
||||
hours = "0#{hours}" if hours < 10
|
||||
minutes = "0#{minutes}" if minutes < 10
|
||||
seconds = "0#{seconds}" if seconds < 10
|
||||
playtimes.push "#{hours}:#{minutes}:#{seconds}"
|
||||
else
|
||||
playtimes.push 'Incomplete'
|
||||
userPlaytimes.push(playtimes)
|
||||
|
||||
columnLabels = "Username"
|
||||
currentLevel = 1
|
||||
lastCourseIndex = 1
|
||||
for level in @courseLevels
|
||||
unless level.courseIndex is lastCourseIndex
|
||||
currentLevel = 1
|
||||
lastCourseIndex = level.courseIndex
|
||||
columnLabels += ",CS#{level.courseIndex}.#{currentLevel++} #{level.slug}"
|
||||
csvContent = "data:text/csv;charset=utf-8,#{columnLabels}\n"
|
||||
for studentRow in userPlaytimes
|
||||
csvContent += studentRow.join(',') + "\n"
|
||||
csvContent = csvContent.substring(0, csvContent.length - 1)
|
||||
encodedUri = encodeURI(csvContent)
|
||||
window.open(encodedUri)
|
||||
$('.classroom-progress-csv').prop('disabled', false)
|
||||
|
||||
.catch (error) ->
|
||||
$('.classroom-progress-csv').prop('disabled', false)
|
||||
console.error error
|
||||
|
|
|
@ -22,6 +22,7 @@ module.exports = class AuthModal extends ModalView
|
|||
|
||||
initialize: (options={}) ->
|
||||
@previousFormInputs = options.initialValues or {}
|
||||
@previousFormInputs.emailOrUsername ?= @previousFormInputs.email or @previousFormInputs.username
|
||||
|
||||
# TODO: Switch to promises and state, rather than using defer to hackily enable buttons after render
|
||||
application.gplusHandler.loadAPI({ success: => _.defer => @$('#gplus-login-btn').attr('disabled', false) })
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
ModalView = require 'views/core/ModalView'
|
||||
template = require 'templates/core/coppa-deny'
|
||||
|
||||
|
||||
module.exports = class COPPADenyModal extends ModalView
|
||||
id: 'coppa-deny-modal'
|
||||
template: template
|
||||
closeButton: true
|
||||
|
||||
constructor: ->
|
||||
super()
|
||||
window.tracker?.trackEvent 'COPPA Message Shown', category: 'Homepage'
|
|
@ -14,6 +14,7 @@ doNothing = ->
|
|||
|
||||
module.exports = class CocoView extends Backbone.View
|
||||
cache: false # signals to the router to keep this view around
|
||||
retainSubviews: false # set to true if you don't want subviews to be destroyed whenever the view renders
|
||||
template: -> ''
|
||||
|
||||
events:
|
||||
|
@ -108,12 +109,19 @@ module.exports = class CocoView extends Backbone.View
|
|||
|
||||
render: ->
|
||||
return @ unless me
|
||||
view.destroy() for id, view of @subviews
|
||||
if @retainSubviews
|
||||
oldSubviews = _.values(@subviews)
|
||||
else
|
||||
view.destroy() for id, view of @subviews
|
||||
@subviews = {}
|
||||
super()
|
||||
return @template if _.isString(@template)
|
||||
@$el.html @template(@getRenderData())
|
||||
|
||||
if @retainSubviews
|
||||
for view in oldSubviews
|
||||
@insertSubView(view)
|
||||
|
||||
if not @supermodel.finished()
|
||||
@showLoading()
|
||||
else
|
||||
|
@ -306,11 +314,20 @@ module.exports = class CocoView extends Backbone.View
|
|||
key = @makeSubViewKey(view)
|
||||
@subviews[key].destroy() if key of @subviews
|
||||
elToReplace ?= @$el.find('#'+view.id)
|
||||
elToReplace.after(view.el).remove()
|
||||
@registerSubView(view, key)
|
||||
view.render()
|
||||
view.afterInsert()
|
||||
view
|
||||
if @retainSubviews
|
||||
@registerSubView(view, key)
|
||||
if elToReplace[0]
|
||||
view.setElement(elToReplace[0])
|
||||
view.render()
|
||||
view.afterInsert()
|
||||
return view
|
||||
|
||||
else
|
||||
elToReplace.after(view.el).remove()
|
||||
@registerSubView(view, key)
|
||||
view.render()
|
||||
view.afterInsert()
|
||||
return view
|
||||
|
||||
registerSubView: (view, key) ->
|
||||
# used to register views which are custom inserted into the view,
|
||||
|
|
|
@ -1,259 +0,0 @@
|
|||
ModalView = require 'views/core/ModalView'
|
||||
template = require 'templates/core/create-account-modal'
|
||||
forms = require 'core/forms'
|
||||
User = require 'models/User'
|
||||
application = require 'core/application'
|
||||
Classroom = require 'models/Classroom'
|
||||
errors = require 'core/errors'
|
||||
COPPADenyModal = require 'views/core/COPPADenyModal'
|
||||
utils = require 'core/utils'
|
||||
|
||||
|
||||
module.exports = class CreateAccountModal extends ModalView
|
||||
id: 'create-account-modal'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'submit form': 'onSubmitForm'
|
||||
'keyup #name': 'onNameChange'
|
||||
'click #gplus-signup-btn': 'onClickGPlusSignupButton'
|
||||
'click #gplus-login-btn': 'onClickGPlusLoginButton'
|
||||
'click #facebook-signup-btn': 'onClickFacebookSignupButton'
|
||||
'click #facebook-login-btn': 'onClickFacebookLoginButton'
|
||||
'click #close-modal': 'hide'
|
||||
'click #switch-to-login-btn': 'onClickSwitchToLoginButton'
|
||||
|
||||
|
||||
# Initialization
|
||||
|
||||
initialize: (options={}) ->
|
||||
@onNameChange = _.debounce(_.bind(@checkNameExists, @), 500)
|
||||
options.initialValues ?= {}
|
||||
options.initialValues?.classCode ?= utils.getQueryVariable('_cc', "")
|
||||
@previousFormInputs = options.initialValues or {}
|
||||
|
||||
# TODO: Switch to promises and state, rather than using defer to hackily enable buttons after render
|
||||
application.gplusHandler.loadAPI({ success: => _.defer => @$('#gplus-signup-btn').attr('disabled', false) })
|
||||
application.facebookHandler.loadAPI({ success: => _.defer => @$('#facebook-signup-btn').attr('disabled', false) })
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
@playSound 'game-menu-open'
|
||||
|
||||
afterInsert: ->
|
||||
super()
|
||||
_.delay (=> $('input:visible:first', @$el).focus()), 500
|
||||
|
||||
|
||||
# User creation
|
||||
|
||||
onSubmitForm: (e) ->
|
||||
e.preventDefault()
|
||||
@playSound 'menu-button-click'
|
||||
|
||||
forms.clearFormAlerts(@$el)
|
||||
attrs = forms.formToObject @$el
|
||||
attrs.name = @suggestedName if @suggestedName
|
||||
_.defaults attrs, me.pick([
|
||||
'preferredLanguage', 'testGroupNumber', 'dateCreated', 'wizardColor1',
|
||||
'name', 'music', 'volume', 'emails', 'schoolName'
|
||||
])
|
||||
attrs.emails ?= {}
|
||||
attrs.emails.generalNews ?= {}
|
||||
attrs.emails.generalNews.enabled = @$el.find('#subscribe').prop('checked')
|
||||
@classCode = attrs.classCode
|
||||
delete attrs.classCode
|
||||
|
||||
error = false
|
||||
birthday = new Date Date.UTC attrs.birthdayYear, attrs.birthdayMonth - 1, attrs.birthdayDay
|
||||
if @classCode
|
||||
attrs.role = 'student'
|
||||
else if isNaN(birthday.getTime())
|
||||
forms.setErrorToProperty @$el, 'birthdayDay', 'Required'
|
||||
error = true
|
||||
else
|
||||
age = (new Date().getTime() - birthday.getTime()) / 365.4 / 24 / 60 / 60 / 1000
|
||||
attrs.birthday = birthday.toISOString()
|
||||
|
||||
delete attrs.birthdayYear
|
||||
delete attrs.birthdayMonth
|
||||
delete attrs.birthdayDay
|
||||
|
||||
_.assign attrs, @gplusAttrs if @gplusAttrs
|
||||
_.assign attrs, @facebookAttrs if @facebookAttrs
|
||||
res = tv4.validateMultiple attrs, User.schema
|
||||
|
||||
if not res.valid
|
||||
forms.applyErrorsToForm(@$el, res.errors)
|
||||
error = true
|
||||
if not _.any([attrs.password, @gplusAttrs, @facebookAttrs])
|
||||
forms.setErrorToProperty @$el, 'password', 'Required'
|
||||
error = true
|
||||
if not forms.validateEmail(attrs.email)
|
||||
forms.setErrorToProperty @$el, 'email', 'Please enter a valid email address'
|
||||
error = true
|
||||
return if error
|
||||
|
||||
@$('#signup-button').text($.i18n.t('signup.creating')).attr('disabled', true)
|
||||
@newUser = new User(attrs)
|
||||
if @classCode
|
||||
@signupClassroomPrecheck()
|
||||
else
|
||||
if age < 13
|
||||
@openModalView new COPPADenyModal
|
||||
else
|
||||
@createUser()
|
||||
|
||||
signupClassroomPrecheck: ->
|
||||
classroom = new Classroom()
|
||||
classroom.fetch({ data: { code: @classCode } })
|
||||
classroom.once 'sync', @createUser, @
|
||||
classroom.once 'error', @onClassroomFetchError, @
|
||||
|
||||
onClassroomFetchError: ->
|
||||
@$('#signup-button').text($.i18n.t('signup.sign_up')).attr('disabled', false)
|
||||
forms.setErrorToProperty(@$el, 'classCode', "#{@classCode} is not a valid code. Please verify the code is typed correctly.")
|
||||
@$('#class-code-input').val('')
|
||||
|
||||
createUser: ->
|
||||
options = {}
|
||||
window.tracker?.identify()
|
||||
if @gplusAttrs
|
||||
@newUser.set('_id', me.id)
|
||||
options.url = "/db/user?gplusID=#{@gplusAttrs.gplusID}&gplusAccessToken=#{application.gplusHandler.accessToken.access_token}"
|
||||
options.type = 'PUT'
|
||||
if @facebookAttrs
|
||||
@newUser.set('_id', me.id)
|
||||
options.url = "/db/user?facebookID=#{@facebookAttrs.facebookID}&facebookAccessToken=#{application.facebookHandler.authResponse.accessToken}"
|
||||
options.type = 'PUT'
|
||||
@newUser.save(null, options)
|
||||
@newUser.once 'sync', @onUserCreated, @
|
||||
@newUser.once 'error', @onUserSaveError, @
|
||||
|
||||
onUserSaveError: (user, jqxhr) ->
|
||||
@$('#signup-button').text($.i18n.t('signup.sign_up')).attr('disabled', false)
|
||||
if _.isObject(jqxhr.responseJSON) and jqxhr.responseJSON.property
|
||||
error = jqxhr.responseJSON
|
||||
if jqxhr.status is 409 and error.property is 'name'
|
||||
@newUser.unset 'name'
|
||||
return @createUser()
|
||||
return forms.applyErrorsToForm(@$el, [jqxhr.responseJSON])
|
||||
errors.showNotyNetworkError(jqxhr)
|
||||
|
||||
onUserCreated: ->
|
||||
Backbone.Mediator.publish "auth:signed-up", {}
|
||||
if @gplusAttrs
|
||||
window.tracker?.trackEvent 'Google Login', category: "Signup", label: 'GPlus'
|
||||
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'GPlus'
|
||||
else if @facebookAttrs
|
||||
window.tracker?.trackEvent 'Facebook Login', category: "Signup", label: 'Facebook'
|
||||
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'Facebook'
|
||||
else
|
||||
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'CodeCombat'
|
||||
if @classCode
|
||||
url = "/courses?_cc="+@classCode
|
||||
location.href = url
|
||||
else
|
||||
window.location.reload()
|
||||
|
||||
|
||||
# Google Plus
|
||||
|
||||
onClickGPlusSignupButton: ->
|
||||
btn = @$('#gplus-signup-btn')
|
||||
application.gplusHandler.connect({
|
||||
context: @
|
||||
success: ->
|
||||
btn.find('.sign-in-blurb').text($.i18n.t('signup.creating'))
|
||||
btn.attr('disabled', true)
|
||||
application.gplusHandler.loadPerson({
|
||||
context: @
|
||||
success: (@gplusAttrs) ->
|
||||
existingUser = new User()
|
||||
existingUser.fetchGPlusUser(@gplusAttrs.gplusID, {
|
||||
context: @
|
||||
complete: ->
|
||||
@$('#email-password-row').remove()
|
||||
success: =>
|
||||
@$('#gplus-account-exists-row').removeClass('hide')
|
||||
error: (user, jqxhr) =>
|
||||
if jqxhr.status is 404
|
||||
@$('#gplus-logged-in-row').toggleClass('hide')
|
||||
else
|
||||
errors.showNotyNetworkError(jqxhr)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
onClickGPlusLoginButton: ->
|
||||
me.loginGPlusUser(@gplusAttrs.gplusID, {
|
||||
context: @
|
||||
success: -> window.location.reload()
|
||||
error: ->
|
||||
@$('#gplus-login-btn').text($.i18n.t('login.log_in')).attr('disabled', false)
|
||||
errors.showNotyNetworkError(arguments...)
|
||||
})
|
||||
@$('#gplus-login-btn').text($.i18n.t('login.logging_in')).attr('disabled', true)
|
||||
|
||||
|
||||
|
||||
# Facebook
|
||||
|
||||
onClickFacebookSignupButton: ->
|
||||
btn = @$('#facebook-signup-btn')
|
||||
application.facebookHandler.connect({
|
||||
context: @
|
||||
success: ->
|
||||
btn.find('.sign-in-blurb').text($.i18n.t('signup.creating'))
|
||||
btn.attr('disabled', true)
|
||||
application.facebookHandler.loadPerson({
|
||||
context: @
|
||||
success: (@facebookAttrs) ->
|
||||
existingUser = new User()
|
||||
existingUser.fetchFacebookUser(@facebookAttrs.facebookID, {
|
||||
context: @
|
||||
complete: ->
|
||||
@$('#email-password-row').remove()
|
||||
success: =>
|
||||
@$('#facebook-account-exists-row').removeClass('hide')
|
||||
error: (user, jqxhr) =>
|
||||
if jqxhr.status is 404
|
||||
@$('#facebook-logged-in-row').toggleClass('hide')
|
||||
else
|
||||
errors.showNotyNetworkError(jqxhr)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
onClickFacebookLoginButton: ->
|
||||
me.loginFacebookUser(@facebookAttrs.facebookID, {
|
||||
context: @
|
||||
success: -> window.location.reload()
|
||||
error: =>
|
||||
@$('#facebook-login-btn').text($.i18n.t('login.log_in')).attr('disabled', false)
|
||||
errors.showNotyNetworkError(jqxhr)
|
||||
})
|
||||
@$('#facebook-login-btn').text($.i18n.t('login.logging_in')).attr('disabled', true)
|
||||
|
||||
|
||||
# Misc
|
||||
|
||||
onHidden: ->
|
||||
super()
|
||||
@playSound 'game-menu-close'
|
||||
|
||||
checkNameExists: ->
|
||||
name = $('#name', @$el).val()
|
||||
return forms.clearFormAlerts(@$el) if name is ''
|
||||
User.getUnconflictedName name, (newName) =>
|
||||
forms.clearFormAlerts(@$el)
|
||||
if name is newName
|
||||
@suggestedName = undefined
|
||||
else
|
||||
@suggestedName = newName
|
||||
forms.setErrorToProperty @$el, 'name', "That name is taken! How about #{newName}?"
|
||||
|
||||
onClickSwitchToLoginButton: ->
|
||||
AuthModal = require('./AuthModal')
|
||||
modal = new AuthModal({initialValues: forms.formToObject @$el})
|
||||
currentView.openModalView(modal)
|
253
app/views/core/CreateAccountModal/BasicInfoView.coffee
Normal file
|
@ -0,0 +1,253 @@
|
|||
CocoView = require 'views/core/CocoView'
|
||||
AuthModal = require 'views/core/AuthModal'
|
||||
template = require 'templates/core/create-account-modal/basic-info-view'
|
||||
forms = require 'core/forms'
|
||||
errors = require 'core/errors'
|
||||
User = require 'models/User'
|
||||
State = require 'models/State'
|
||||
|
||||
###
|
||||
This view handles the primary form for user details — name, email, password, etc,
|
||||
and the AJAX that actually creates the user.
|
||||
|
||||
It also handles facebook/g+ login, which if used, open one of two other screens:
|
||||
sso-already-exists: If the facebook/g+ connection is already associated with a user, they're given a log in button
|
||||
sso-confirm: If this is a new facebook/g+ connection, ask for a username, then allow creation of a user
|
||||
|
||||
The sso-confirm view *inherits from this view* in order to share its account-creation logic and events.
|
||||
This means the selectors used in these events must work in both templates.
|
||||
|
||||
This view currently uses the old form API instead of stateful render.
|
||||
It needs some work to make error UX and rendering better, but is functional.
|
||||
###
|
||||
|
||||
module.exports = class BasicInfoView extends CocoView
|
||||
id: 'basic-info-view'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'change input[name="email"]': 'onChangeEmail'
|
||||
'change input[name="name"]': 'onChangeName'
|
||||
'click .back-button': 'onClickBackButton'
|
||||
'submit form': 'onSubmitForm'
|
||||
'click .use-suggested-name-link': 'onClickUseSuggestedNameLink'
|
||||
'click #facebook-signup-btn': 'onClickSsoSignupButton'
|
||||
'click #gplus-signup-btn': 'onClickSsoSignupButton'
|
||||
|
||||
initialize: ({ @signupState } = {}) ->
|
||||
@state = new State {
|
||||
suggestedNameText: '...'
|
||||
checkEmailState: 'standby' # 'checking', 'exists', 'available'
|
||||
checkEmailValue: null
|
||||
checkEmailPromise: null
|
||||
checkNameState: 'standby' # same
|
||||
checkNameValue: null
|
||||
checkNamePromise: null
|
||||
error: ''
|
||||
}
|
||||
@listenTo @state, 'change:checkEmailState', -> @renderSelectors('.email-check')
|
||||
@listenTo @state, 'change:checkNameState', -> @renderSelectors('.name-check')
|
||||
@listenTo @state, 'change:error', -> @renderSelectors('.error-area')
|
||||
@listenTo @signupState, 'change:facebookEnabled', -> @renderSelectors('.auth-network-logins')
|
||||
@listenTo @signupState, 'change:gplusEnabled', -> @renderSelectors('.auth-network-logins')
|
||||
|
||||
onChangeEmail: ->
|
||||
@checkEmail()
|
||||
|
||||
checkEmail: ->
|
||||
email = @$('[name="email"]').val()
|
||||
if email is @state.get('checkEmailValue')
|
||||
return @state.get('checkEmailPromise')
|
||||
|
||||
if not (email and forms.validateEmail(email))
|
||||
@state.set({
|
||||
checkEmailState: 'standby'
|
||||
checkEmailValue: email
|
||||
checkEmailPromise: null
|
||||
})
|
||||
return Promise.resolve()
|
||||
|
||||
@state.set({
|
||||
checkEmailState: 'checking'
|
||||
checkEmailValue: email
|
||||
|
||||
checkEmailPromise: (User.checkEmailExists(email)
|
||||
.then ({exists}) =>
|
||||
return unless email is @$('[name="email"]').val()
|
||||
if exists
|
||||
@state.set('checkEmailState', 'exists')
|
||||
else
|
||||
@state.set('checkEmailState', 'available')
|
||||
.catch (e) =>
|
||||
@state.set('checkEmailState', 'standby')
|
||||
throw e
|
||||
)
|
||||
})
|
||||
return @state.get('checkEmailPromise')
|
||||
|
||||
onChangeName: ->
|
||||
@checkName()
|
||||
|
||||
checkName: ->
|
||||
name = @$('input[name="name"]').val()
|
||||
|
||||
if name is @state.get('checkNameValue')
|
||||
return @state.get('checkNamePromise')
|
||||
|
||||
if not name
|
||||
@state.set({
|
||||
checkNameState: 'standby'
|
||||
checkNameValue: name
|
||||
checkNamePromise: null
|
||||
})
|
||||
return Promise.resolve()
|
||||
|
||||
@state.set({
|
||||
checkNameState: 'checking'
|
||||
checkNameValue: name
|
||||
|
||||
checkNamePromise: (User.checkNameConflicts(name)
|
||||
.then ({ suggestedName, conflicts }) =>
|
||||
return unless name is @$('input[name="name"]').val()
|
||||
if conflicts
|
||||
suggestedNameText = $.i18n.t('signup.name_taken').replace('{{suggestedName}}', suggestedName)
|
||||
@state.set({ checkNameState: 'exists', suggestedNameText })
|
||||
else
|
||||
@state.set { checkNameState: 'available' }
|
||||
.catch (error) =>
|
||||
@state.set('checkNameState', 'standby')
|
||||
throw error
|
||||
)
|
||||
})
|
||||
|
||||
return @state.get('checkNamePromise')
|
||||
|
||||
checkBasicInfo: (data) ->
|
||||
# TODO: Move this to somewhere appropriate
|
||||
tv4.addFormat({
|
||||
'email': (email) ->
|
||||
if forms.validateEmail(email)
|
||||
return null
|
||||
else
|
||||
return {code: tv4.errorCodes.FORMAT_CUSTOM, message: "Please enter a valid email address."}
|
||||
})
|
||||
|
||||
forms.clearFormAlerts(@$el)
|
||||
res = tv4.validateMultiple data, @formSchema()
|
||||
forms.applyErrorsToForm(@$('form'), res.errors) unless res.valid
|
||||
return res.valid
|
||||
|
||||
formSchema: ->
|
||||
type: 'object'
|
||||
properties:
|
||||
email: User.schema.properties.email
|
||||
name: User.schema.properties.name
|
||||
password: User.schema.properties.password
|
||||
required: ['email', 'name', 'password'].concat (if @signupState.get('path') is 'student' then ['firstName', 'lastName'] else [])
|
||||
|
||||
onClickBackButton: -> @trigger 'nav-back'
|
||||
|
||||
onClickUseSuggestedNameLink: (e) ->
|
||||
@$('input[name="name"]').val(@state.get('suggestedName'))
|
||||
forms.clearFormAlerts(@$el.find('input[name="name"]').closest('.form-group').parent())
|
||||
|
||||
onSubmitForm: (e) ->
|
||||
@state.unset('error')
|
||||
e.preventDefault()
|
||||
data = forms.formToObject(e.currentTarget)
|
||||
valid = @checkBasicInfo(data)
|
||||
return unless valid
|
||||
|
||||
@displayFormSubmitting()
|
||||
AbortError = new Error()
|
||||
|
||||
@checkEmail()
|
||||
.then @checkName()
|
||||
.then =>
|
||||
if not (@state.get('checkEmailState') is 'available' and @state.get('checkNameState') is 'available')
|
||||
throw AbortError
|
||||
|
||||
# update User
|
||||
emails = _.assign({}, me.get('emails'))
|
||||
emails.generalNews ?= {}
|
||||
emails.generalNews.enabled = @$('#subscribe-input').is(':checked')
|
||||
me.set('emails', emails)
|
||||
|
||||
unless _.isNaN(@signupState.get('birthday').getTime())
|
||||
me.set('birthday', @signupState.get('birthday').toISOString())
|
||||
|
||||
me.set(_.omit(@signupState.get('ssoAttrs') or {}, 'email', 'facebookID', 'gplusID'))
|
||||
me.set('name', @$('input[name="name"]').val())
|
||||
jqxhr = me.save()
|
||||
if not jqxhr
|
||||
console.error(me.validationError)
|
||||
throw new Error('Could not save user')
|
||||
|
||||
return new Promise(jqxhr.then)
|
||||
|
||||
.then =>
|
||||
# Use signup method
|
||||
window.tracker?.identify()
|
||||
switch @signupState.get('ssoUsed')
|
||||
when 'gplus'
|
||||
{ email, gplusID } = @signupState.get('ssoAttrs')
|
||||
jqxhr = me.signupWithGPlus(email, gplusID)
|
||||
when 'facebook'
|
||||
{ email, facebookID } = @signupState.get('ssoAttrs')
|
||||
jqxhr = me.signupWithFacebook(email, facebookID)
|
||||
else
|
||||
{ email, password } = forms.formToObject(@$el)
|
||||
jqxhr = me.signupWithPassword(email, password)
|
||||
|
||||
return new Promise(jqxhr.then)
|
||||
|
||||
.then =>
|
||||
{ classCode, classroom } = @signupState.attributes
|
||||
if classCode and classroom
|
||||
return new Promise(classroom.joinWithCode(classCode).then)
|
||||
|
||||
.then =>
|
||||
@finishSignup()
|
||||
|
||||
.catch (e) =>
|
||||
@displayFormStandingBy()
|
||||
if e is AbortError
|
||||
return
|
||||
else
|
||||
console.error 'BasicInfoView form submission Promise error:', e
|
||||
@state.set('error', e.responseJSON?.message or 'Unknown Error')
|
||||
|
||||
finishSignup: ->
|
||||
@trigger 'signup'
|
||||
|
||||
displayFormSubmitting: ->
|
||||
@$('#create-account-btn').text($.i18n.t('signup.creating')).attr('disabled', true)
|
||||
@$('input').attr('disabled', true)
|
||||
|
||||
displayFormStandingBy: ->
|
||||
@$('#create-account-btn').text($.i18n.t('signup.create_account')).attr('disabled', false)
|
||||
@$('input').attr('disabled', false)
|
||||
|
||||
onClickSsoSignupButton: (e) ->
|
||||
e.preventDefault()
|
||||
ssoUsed = $(e.currentTarget).data('sso-used')
|
||||
handler = if ssoUsed is 'facebook' then application.facebookHandler else application.gplusHandler
|
||||
handler.connect({
|
||||
context: @
|
||||
success: ->
|
||||
handler.loadPerson({
|
||||
context: @
|
||||
success: (ssoAttrs) ->
|
||||
@signupState.set { ssoAttrs }
|
||||
{ email } = ssoAttrs
|
||||
User.checkEmailExists(email).then ({exists}) =>
|
||||
@signupState.set {
|
||||
ssoUsed
|
||||
email: ssoAttrs.email
|
||||
}
|
||||
if exists
|
||||
@trigger 'sso-connect:already-in-use'
|
||||
else
|
||||
@trigger 'sso-connect:new-user'
|
||||
})
|
||||
})
|
|
@ -0,0 +1,11 @@
|
|||
CocoView = require 'views/core/CocoView'
|
||||
template = require 'templates/core/create-account-modal/choose-account-type-view'
|
||||
|
||||
module.exports = class ChooseAccountTypeView extends CocoView
|
||||
id: 'choose-account-type-view'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click .teacher-path-button': -> @trigger 'choose-path', 'teacher'
|
||||
'click .student-path-button': -> @trigger 'choose-path', 'student'
|
||||
'click .individual-path-button': -> @trigger 'choose-path', 'individual'
|
23
app/views/core/CreateAccountModal/ConfirmationView.coffee
Normal file
|
@ -0,0 +1,23 @@
|
|||
CocoView = require 'views/core/CocoView'
|
||||
State = require 'models/State'
|
||||
template = require 'templates/core/create-account-modal/confirmation-view'
|
||||
forms = require 'core/forms'
|
||||
|
||||
module.exports = class ConfirmationView extends CocoView
|
||||
id: 'confirmation-view'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click #start-btn': 'onClickStartButton'
|
||||
|
||||
initialize: ({ @signupState } = {}) ->
|
||||
|
||||
onClickStartButton: ->
|
||||
classroom = @signupState.get('classroom')
|
||||
if @signupState.get('path') is 'student'
|
||||
# force clearing of _cc GET param from url if on /courses
|
||||
application.router.navigate('/', {replace: true})
|
||||
application.router.navigate('/courses')
|
||||
else
|
||||
application.router.navigate('/play')
|
||||
document.location.reload()
|
33
app/views/core/CreateAccountModal/CoppaDenyView.coffee
Normal file
|
@ -0,0 +1,33 @@
|
|||
CocoView = require 'views/core/CocoView'
|
||||
State = require 'models/State'
|
||||
template = require 'templates/core/create-account-modal/coppa-deny-view'
|
||||
forms = require 'core/forms'
|
||||
contact = require 'core/contact'
|
||||
|
||||
module.exports = class CoppaDenyView extends CocoView
|
||||
id: 'coppa-deny-view'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click .send-parent-email-button': 'onClickSendParentEmailButton'
|
||||
'change input[name="parentEmail"]': 'onChangeParentEmail'
|
||||
'click .back-btn': 'onClickBackButton'
|
||||
|
||||
initialize: ({ @signupState } = {}) ->
|
||||
@state = new State({ parentEmail: '' })
|
||||
@listenTo @state, 'all', _.debounce(@render)
|
||||
|
||||
onChangeParentEmail: (e) ->
|
||||
@state.set { parentEmail: $(e.currentTarget).val() }, { silent: true }
|
||||
|
||||
onClickSendParentEmailButton: (e) ->
|
||||
e.preventDefault()
|
||||
@state.set({ parentEmailSending: true })
|
||||
contact.sendParentSignupInstructions(@state.get('parentEmail'))
|
||||
.then =>
|
||||
@state.set({ error: false, parentEmailSent: true, parentEmailSending: false })
|
||||
.catch =>
|
||||
@state.set({ error: true, parentEmailSent: false, parentEmailSending: false })
|
||||
|
||||
onClickBackButton: ->
|
||||
@trigger 'nav-back'
|
116
app/views/core/CreateAccountModal/CreateAccountModal.coffee
Normal file
|
@ -0,0 +1,116 @@
|
|||
ModalView = require 'views/core/ModalView'
|
||||
AuthModal = require 'views/core/AuthModal'
|
||||
ChooseAccountTypeView = require './ChooseAccountTypeView'
|
||||
SegmentCheckView = require './SegmentCheckView'
|
||||
CoppaDenyView = require './CoppaDenyView'
|
||||
BasicInfoView = require './BasicInfoView'
|
||||
SingleSignOnAlreadyExistsView = require './SingleSignOnAlreadyExistsView'
|
||||
SingleSignOnConfirmView = require './SingleSignOnConfirmView'
|
||||
ConfirmationView = require './ConfirmationView'
|
||||
State = require 'models/State'
|
||||
template = require 'templates/core/create-account-modal/create-account-modal'
|
||||
forms = require 'core/forms'
|
||||
User = require 'models/User'
|
||||
application = require 'core/application'
|
||||
errors = require 'core/errors'
|
||||
utils = require 'core/utils'
|
||||
|
||||
###
|
||||
CreateAccountModal is a wizard-style modal with several subviews, one for each
|
||||
`screen` that the user navigates forward and back through.
|
||||
|
||||
There are three `path`s, one for each account type (individual, student).
|
||||
Teacher account path will be added later; for now it defers to /teachers/signup)
|
||||
Each subview handles only one `screen`, but all three `path` variants because
|
||||
their logic is largely the same.
|
||||
|
||||
They `screen`s are:
|
||||
choose-account-type: Sets the `path`.
|
||||
segment-check: Checks required info for the path (age, )
|
||||
coppa-deny: Seen if the indidual segment-check age is < 13 years old
|
||||
basic-info: This is the form for username/password/email/etc.
|
||||
It asks for whatever is needed for this type of user.
|
||||
It also handles the actual user creation.
|
||||
A user may create their account here, or connect with facebook/g+
|
||||
sso-confirm: Alternate version of basic-info for new facebook/g+ users
|
||||
sso-already-exists: When facebook/g+ user already exists, this prompts them to sign in.
|
||||
extras: Not yet implemented
|
||||
confirmation: When an account has been successfully created, this view shows them their info and
|
||||
links them to a landing page based on their account type.
|
||||
|
||||
NOTE: BasicInfoView's two children (SingleSignOn...View) inherit from it.
|
||||
This allows them to have the same form-handling logic, but different templates.
|
||||
###
|
||||
|
||||
module.exports = class CreateAccountModal extends ModalView
|
||||
id: 'create-account-modal'
|
||||
template: template
|
||||
closesOnClickOutside: false
|
||||
retainSubviews: true
|
||||
|
||||
events:
|
||||
'click .login-link': 'onClickLoginLink'
|
||||
|
||||
initialize: (options={}) ->
|
||||
classCode = utils.getQueryVariable('_cc', undefined)
|
||||
@signupState = new State {
|
||||
path: if classCode then 'student' else null
|
||||
screen: if classCode then 'segment-check' else 'choose-account-type'
|
||||
ssoUsed: null # or 'facebook', 'gplus'
|
||||
classroom: null # or Classroom instance
|
||||
facebookEnabled: application.facebookHandler.apiLoaded
|
||||
gplusEnabled: application.gplusHandler.apiLoaded
|
||||
classCode
|
||||
birthday: new Date('') # so that birthday.getTime() is NaN
|
||||
}
|
||||
|
||||
{ startOnPath } = options
|
||||
if startOnPath is 'student'
|
||||
@signupState.set({ path: 'student', screen: 'segment-check' })
|
||||
if startOnPath is 'individual'
|
||||
@signupState.set({ path: 'individual', screen: 'segment-check' })
|
||||
|
||||
@listenTo @signupState, 'all', _.debounce @render
|
||||
|
||||
@listenTo @insertSubView(new ChooseAccountTypeView()),
|
||||
'choose-path': (path) ->
|
||||
if path is 'teacher'
|
||||
application.router.navigate('/teachers/signup', trigger: true)
|
||||
else
|
||||
@signupState.set { path, screen: 'segment-check' }
|
||||
|
||||
@listenTo @insertSubView(new SegmentCheckView({ @signupState })),
|
||||
'choose-path': (path) -> @signupState.set { path, screen: 'segment-check' }
|
||||
'nav-back': -> @signupState.set { path: null, screen: 'choose-account-type' }
|
||||
'nav-forward': (screen) -> @signupState.set { screen: screen or 'basic-info' }
|
||||
|
||||
@listenTo @insertSubView(new CoppaDenyView({ @signupState })),
|
||||
'nav-back': -> @signupState.set { screen: 'segment-check' }
|
||||
|
||||
@listenTo @insertSubView(new BasicInfoView({ @signupState })),
|
||||
'sso-connect:already-in-use': -> @signupState.set { screen: 'sso-already-exists' }
|
||||
'sso-connect:new-user': -> @signupState.set {screen: 'sso-confirm'}
|
||||
'nav-back': -> @signupState.set { screen: 'segment-check' }
|
||||
'signup': -> @signupState.set { screen: 'confirmation' }
|
||||
|
||||
@listenTo @insertSubView(new SingleSignOnAlreadyExistsView({ @signupState })),
|
||||
'nav-back': -> @signupState.set { screen: 'basic-info' }
|
||||
|
||||
@listenTo @insertSubView(new SingleSignOnConfirmView({ @signupState })),
|
||||
'nav-back': -> @signupState.set { screen: 'basic-info' }
|
||||
'signup': -> @signupState.set { screen: 'confirmation' }
|
||||
|
||||
@insertSubView(new ConfirmationView({ @signupState }))
|
||||
|
||||
# TODO: Switch to promises and state, rather than using defer to hackily enable buttons after render
|
||||
application.facebookHandler.loadAPI({ success: => @signupState.set { facebookEnabled: true } unless @destroyed })
|
||||
application.gplusHandler.loadAPI({ success: => @signupState.set { gplusEnabled: true } unless @destroyed })
|
||||
|
||||
@once 'hidden', ->
|
||||
if @signupState.get('screen') is 'confirmation' and not application.testing
|
||||
# ensure logged in state propagates through the entire app
|
||||
document.location.reload()
|
||||
|
||||
onClickLoginLink: ->
|
||||
# TODO: Make sure the right information makes its way into the state.
|
||||
@openModalView(new AuthModal({ initialValues: @signupState.pick(['email', 'name', 'password']) }))
|
106
app/views/core/CreateAccountModal/SegmentCheckView.coffee
Normal file
|
@ -0,0 +1,106 @@
|
|||
CocoView = require 'views/core/CocoView'
|
||||
template = require 'templates/core/create-account-modal/segment-check-view'
|
||||
forms = require 'core/forms'
|
||||
Classroom = require 'models/Classroom'
|
||||
State = require 'models/State'
|
||||
|
||||
module.exports = class SegmentCheckView extends CocoView
|
||||
id: 'segment-check-view'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click .back-to-account-type': -> @trigger 'nav-back'
|
||||
'input .class-code-input': 'onInputClassCode'
|
||||
'input .birthday-form-group': 'onInputBirthday'
|
||||
'submit form.segment-check': 'onSubmitSegmentCheck'
|
||||
'click .individual-path-button': -> @trigger 'choose-path', 'individual'
|
||||
|
||||
initialize: ({ @signupState } = {}) ->
|
||||
@checkClassCodeDebounced = _.debounce @checkClassCode, 1000
|
||||
@fetchClassByCode = _.memoize(@fetchClassByCode)
|
||||
@classroom = new Classroom()
|
||||
@state = new State()
|
||||
if @signupState.get('classCode')
|
||||
@checkClassCode(@signupState.get('classCode'))
|
||||
@listenTo @state, 'all', _.debounce(->
|
||||
@renderSelectors('.render')
|
||||
@trigger 'special-render'
|
||||
)
|
||||
|
||||
getClassCode: -> @$('.class-code-input').val() or @signupState.get('classCode')
|
||||
|
||||
onInputClassCode: ->
|
||||
@classroom = new Classroom()
|
||||
forms.clearFormAlerts(@$el)
|
||||
classCode = @getClassCode()
|
||||
@signupState.set { classCode }, { silent: true }
|
||||
@checkClassCodeDebounced()
|
||||
|
||||
checkClassCode: ->
|
||||
return if @destroyed
|
||||
classCode = @getClassCode()
|
||||
|
||||
@fetchClassByCode(classCode)
|
||||
.then (classroom) =>
|
||||
return if @destroyed or @getClassCode() isnt classCode
|
||||
if classroom
|
||||
@classroom = classroom
|
||||
@state.set { classCodeValid: true, segmentCheckValid: true }
|
||||
else
|
||||
@classroom = new Classroom()
|
||||
@state.set { classCodeValid: false, segmentCheckValid: false }
|
||||
.catch (error) ->
|
||||
throw error
|
||||
|
||||
onInputBirthday: ->
|
||||
{ birthdayYear, birthdayMonth, birthdayDay } = forms.formToObject(@$('form'))
|
||||
birthday = new Date Date.UTC(birthdayYear, birthdayMonth - 1, birthdayDay)
|
||||
@signupState.set { birthdayYear, birthdayMonth, birthdayDay, birthday }, { silent: true }
|
||||
unless _.isNaN(birthday.getTime())
|
||||
forms.clearFormAlerts(@$el)
|
||||
|
||||
onSubmitSegmentCheck: (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
if @signupState.get('path') is 'student'
|
||||
@$('.class-code-input').attr('disabled', true)
|
||||
|
||||
@fetchClassByCode(@getClassCode())
|
||||
.then (classroom) =>
|
||||
return if @destroyed
|
||||
if classroom
|
||||
@signupState.set { classroom }
|
||||
@trigger 'nav-forward'
|
||||
else
|
||||
@$('.class-code-input').attr('disabled', false)
|
||||
@classroom = new Classroom()
|
||||
@state.set { classCodeValid: false, segmentCheckValid: false }
|
||||
.catch (error) ->
|
||||
throw error
|
||||
|
||||
else if @signupState.get('path') is 'individual'
|
||||
if _.isNaN(@signupState.get('birthday').getTime())
|
||||
forms.clearFormAlerts(@$el)
|
||||
forms.setErrorToProperty @$el, 'birthdayDay', 'Required'
|
||||
else
|
||||
age = (new Date().getTime() - @signupState.get('birthday').getTime()) / 365.4 / 24 / 60 / 60 / 1000
|
||||
if age > 13
|
||||
@trigger 'nav-forward'
|
||||
else
|
||||
@trigger 'nav-forward', 'coppa-deny'
|
||||
|
||||
fetchClassByCode: (classCode) ->
|
||||
if not classCode
|
||||
return Promise.resolve()
|
||||
|
||||
new Promise((resolve, reject) ->
|
||||
new Classroom().fetchByCode(classCode, {
|
||||
success: resolve
|
||||
error: (classroom, jqxhr) ->
|
||||
if jqxhr.status is 404
|
||||
resolve()
|
||||
else
|
||||
reject(jqxhr.responseJSON)
|
||||
})
|
||||
)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
CocoView = require 'views/core/CocoView'
|
||||
template = require 'templates/core/create-account-modal/single-sign-on-already-exists-view'
|
||||
forms = require 'core/forms'
|
||||
User = require 'models/User'
|
||||
|
||||
module.exports = class SingleSignOnAlreadyExistsView extends CocoView
|
||||
id: 'single-sign-on-already-exists-view'
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click .back-button': 'onClickBackButton'
|
||||
|
||||
initialize: ({ @signupState }) ->
|
||||
|
||||
onClickBackButton: ->
|
||||
@signupState.set {
|
||||
ssoUsed: undefined
|
||||
ssoAttrs: undefined
|
||||
}
|
||||
@trigger('nav-back')
|
|
@ -0,0 +1,30 @@
|
|||
CocoView = require 'views/core/CocoView'
|
||||
BasicInfoView = require 'views/core/CreateAccountModal/BasicInfoView'
|
||||
template = require 'templates/core/create-account-modal/single-sign-on-confirm-view'
|
||||
forms = require 'core/forms'
|
||||
User = require 'models/User'
|
||||
|
||||
module.exports = class SingleSignOnConfirmView extends BasicInfoView
|
||||
id: 'single-sign-on-confirm-view'
|
||||
template: template
|
||||
|
||||
events: _.extend {}, BasicInfoView.prototype.events, {
|
||||
'click .back-button': 'onClickBackButton'
|
||||
}
|
||||
|
||||
initialize: ({ @signupState } = {}) ->
|
||||
super(arguments...)
|
||||
|
||||
onClickBackButton: ->
|
||||
@signupState.set {
|
||||
ssoUsed: undefined
|
||||
ssoAttrs: undefined
|
||||
}
|
||||
@trigger 'nav-back'
|
||||
|
||||
|
||||
formSchema: ->
|
||||
type: 'object'
|
||||
properties:
|
||||
name: User.schema.properties.name
|
||||
required: ['name']
|
1
app/views/core/CreateAccountModal/index.coffee
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = require 'views/core/CreateAccountModal/CreateAccountModal'
|
|
@ -88,6 +88,8 @@ module.exports = class CoursesView extends RootView
|
|||
if @classCodeQueryVar and not me.isAnonymous()
|
||||
window.tracker?.trackEvent 'Students Join Class Link', category: 'Students', classCode: @classCodeQueryVar, ['Mixpanel']
|
||||
@joinClass()
|
||||
else if @classCodeQueryVar and me.isAnonymous()
|
||||
@openModalView(new CreateAccountModal())
|
||||
|
||||
onClickLogInButton: ->
|
||||
modal = new AuthModal()
|
||||
|
@ -160,7 +162,7 @@ module.exports = class CoursesView extends RootView
|
|||
if jqxhr.status is 422
|
||||
@errorMessage = 'Please enter a code.'
|
||||
else if jqxhr.status is 404
|
||||
@errorMessage = 'Code not found.'
|
||||
@errorMessage = $.t('signup.classroom_not_found')
|
||||
else
|
||||
@errorMessage = "#{jqxhr.responseText}"
|
||||
@renderSelectors '#join-class-form'
|
||||
|
|
|
@ -46,6 +46,7 @@ module.exports = class SaveVersionModal extends ModalView
|
|||
}
|
||||
|
||||
submitPatch: ->
|
||||
@savingPatchError = false
|
||||
forms.clearFormAlerts @$el
|
||||
patch = new Patch()
|
||||
patch.set 'delta', @model.getDelta()
|
||||
|
@ -60,8 +61,10 @@ module.exports = class SaveVersionModal extends ModalView
|
|||
return unless res
|
||||
@enableModalInProgress(@$el)
|
||||
|
||||
res.error =>
|
||||
res.error (jqxhr) =>
|
||||
@disableModalInProgress(@$el)
|
||||
@savingPatchError = jqxhr.responseJSON?.message or 'Unknown error.'
|
||||
@renderSelectors '.save-error-area'
|
||||
|
||||
res.success =>
|
||||
@hide()
|
||||
|
|
|
@ -16,7 +16,7 @@ const request = require('request');
|
|||
|
||||
const zpPageSize = 100;
|
||||
|
||||
getZPRepliedContacts((err, emailContactMap) => {
|
||||
getZPContacts((err, emailContactMap) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return;
|
||||
|
@ -68,6 +68,7 @@ function createCloseLead(zpContact, done) {
|
|||
}
|
||||
|
||||
function updateCloseLead(zpContact, existingLead, done) {
|
||||
// console.log(`DEBUG: updateCloseLead ${existingLead.id} ${zpContact.email}`);
|
||||
const putData = {
|
||||
status: 'Contacted',
|
||||
'custom.lastUpdated': new Date(),
|
||||
|
@ -89,6 +90,9 @@ function updateCloseLead(zpContact, existingLead, done) {
|
|||
title: zpContact.title,
|
||||
emails: [{email: zpContact.email}]
|
||||
};
|
||||
if (zpContact.phone) {
|
||||
postData.phones = [{phone: zpContact.phone}];
|
||||
}
|
||||
const options = {
|
||||
uri: `https://${closeIoApiKey}:X@app.close.io/api/v1/contact/`,
|
||||
body: JSON.stringify(postData)
|
||||
|
@ -132,10 +136,9 @@ function createUpsertCloseLeadFn(zpContact) {
|
|||
};
|
||||
}
|
||||
|
||||
function getZPRepliedContactsPage(contacts, page, done) {
|
||||
// console.log(`DEBUG: Fetching page ${page} ${zpPageSize}...`);
|
||||
function getZPContactsPage(contacts, searchQuery, done) {
|
||||
const options = {
|
||||
url: `https://www.zenprospect.com/api/v1/contacts/search?codecombat_special_auth_token=${zpAuthToken}&page=${page}&per_page=${zpPageSize}&contact_email_replied=true`,
|
||||
url: `https://www.zenprospect.com/api/v1/contacts/search?${searchQuery}`,
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
@ -144,47 +147,69 @@ function getZPRepliedContactsPage(contacts, page, done) {
|
|||
if (err) return done(err);
|
||||
const data = JSON.parse(body);
|
||||
for (let contact of data.contacts) {
|
||||
if (contact.email_replied) {
|
||||
contacts.push({
|
||||
organization: contact.organization_name,
|
||||
name: contact.name,
|
||||
title: contact.title,
|
||||
email: contact.email,
|
||||
phone: contact.phone,
|
||||
data: contact
|
||||
});
|
||||
}
|
||||
contacts.push({
|
||||
organization: contact.organization_name,
|
||||
name: contact.name,
|
||||
title: contact.title,
|
||||
email: contact.email,
|
||||
phone: contact.phone,
|
||||
data: contact
|
||||
});
|
||||
}
|
||||
return done(null, data.pipeline_total);
|
||||
});
|
||||
}
|
||||
|
||||
function getZPRepliedContacts(done) {
|
||||
// Get first page to get total contact count for parallized page fetches
|
||||
function createGetZPAutoResponderContactsPage(contacts, page) {
|
||||
return (done) => {
|
||||
// console.log(`DEBUG: Fetching autoresponder page ${page} ${zpPageSize}...`);
|
||||
let searchQuery = `codecombat_special_auth_token=${zpAuthToken}&page=${page}&per_page=${zpPageSize}&contact_email_autoresponder=true`;
|
||||
getZPContactsPage(contacts, searchQuery, done);
|
||||
};
|
||||
}
|
||||
|
||||
function createGetZPRepliedContactsPage(contacts, page) {
|
||||
return (done) => {
|
||||
// console.log(`DEBUG: Fetching email reply page ${page} ${zpPageSize}...`);
|
||||
let searchQuery = `codecombat_special_auth_token=${zpAuthToken}&page=${page}&per_page=${zpPageSize}&contact_email_replied=true`;
|
||||
getZPContactsPage(contacts, searchQuery, done);
|
||||
};
|
||||
}
|
||||
|
||||
function getZPContacts(done) {
|
||||
// Get first page to get total contact count for future parallized page fetches
|
||||
const contacts = [];
|
||||
getZPRepliedContactsPage(contacts, 0, (err, total) => {
|
||||
createGetZPAutoResponderContactsPage(contacts, 0)((err, autoResponderTotal) => {
|
||||
if (err) return done(err);
|
||||
const createGetZPLeadsPage = (leads, page) => {
|
||||
return (done) => {
|
||||
getZPRepliedContactsPage(leads, page, done);
|
||||
};
|
||||
}
|
||||
const tasks = [];
|
||||
for (let i = 1; (i - 1) * zpPageSize < total; i++) {
|
||||
tasks.push(createGetZPLeadsPage(contacts, i));
|
||||
}
|
||||
async.series(tasks, (err, results) => {
|
||||
createGetZPRepliedContactsPage(contacts, 0)((err, repliedTotal) => {
|
||||
if (err) return done(err);
|
||||
const emailContactMap = {};
|
||||
for (const contact of contacts) {
|
||||
if (!contact.organization || !contact.name || !contact.title || !contact.email) {
|
||||
console.log(JSON.stringify(contact, null, 2));
|
||||
return done(`DEBUG: missing data for zp contact:`);
|
||||
}
|
||||
if (!emailContactMap[contact.email]) emailContactMap[contact.email] = contact;
|
||||
|
||||
const tasks = [];
|
||||
for (let i = 1; (i - 1) * zpPageSize < autoResponderTotal; i++) {
|
||||
tasks.push(createGetZPAutoResponderContactsPage(contacts, i));
|
||||
}
|
||||
log(`${total} total ZP contacts, ${Object.keys(emailContactMap).length} with replies`);
|
||||
return done(null, emailContactMap);
|
||||
for (let i = 1; (i - 1) * zpPageSize < repliedTotal; i++) {
|
||||
tasks.push(createGetZPRepliedContactsPage(contacts, i));
|
||||
}
|
||||
|
||||
async.series(tasks, (err, results) => {
|
||||
if (err) return done(err);
|
||||
const emailContactMap = {};
|
||||
for (const contact of contacts) {
|
||||
if (!contact.organization || !contact.name || !contact.title || !contact.email) {
|
||||
console.log(JSON.stringify(contact, null, 2));
|
||||
return done(`DEBUG: missing data for zp contact:`);
|
||||
}
|
||||
if (!emailContactMap[contact.email]) {
|
||||
emailContactMap[contact.email] = contact;
|
||||
}
|
||||
// else {
|
||||
// console.log(`DEBUG: already have contact ${contact.email}`);
|
||||
// }
|
||||
}
|
||||
log(`(${autoResponderTotal + repliedTotal}) ${autoResponderTotal} autoresponder ZP contacts ${repliedTotal} ZP contacts ${Object.keys(emailContactMap).length} contacts mapped`);
|
||||
return done(null, emailContactMap);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ UserHandler = class UserHandler extends Handler
|
|||
return callback(res: 'Facebook user login error.', code: 500) if err
|
||||
return callback(null, req, otherUser)
|
||||
)
|
||||
r = {message: 'is already used by another account', property: 'email'}
|
||||
r = {message: 'is already used by another account', property: 'email', code: 409}
|
||||
return callback({res: r, code: 409}) if otherUser
|
||||
user.set('email', req.body.email)
|
||||
callback(null, req, user)
|
||||
|
@ -118,7 +118,7 @@ UserHandler = class UserHandler extends Handler
|
|||
User.findOne({nameLower: nameLower, anonymous: false}).exec (err, otherUser) ->
|
||||
log.error "Database error setting user name: #{err}" if err
|
||||
return callback(res: 'Database error.', code: 500) if err
|
||||
r = {message: 'is already used by another account', property: 'name'}
|
||||
r = {message: 'is already used by another account', property: 'name', code: 409}
|
||||
log.info 'Another user exists' if otherUser
|
||||
return callback({res: r, code: 409}) if otherUser
|
||||
user.set('name', req.body.name)
|
||||
|
|
|
@ -98,6 +98,7 @@ module.exports =
|
|||
|
||||
processLicenseRequest: (teacherEmail, userID, leadID, licensesRequested, amount, done) ->
|
||||
# Update lead with licenses requested
|
||||
licensesRequested = parseInt(licensesRequested)
|
||||
putData = 'custom.licensesRequested': licensesRequested
|
||||
options =
|
||||
uri: "https://#{apiKey}:X@app.close.io/api/v1/lead/#{leadID}/"
|
||||
|
@ -135,7 +136,7 @@ module.exports =
|
|||
date_won: dateWon.toISOString().substring(0, 10)
|
||||
lead_id: leadID
|
||||
status: 'Active'
|
||||
value: parseInt(licensesRequested) * amount
|
||||
value: licensesRequested * amount
|
||||
value_period: "annual"
|
||||
options =
|
||||
uri: "https://#{apiKey}:X@app.close.io/api/v1/opportunity/"
|
||||
|
|
11
server/lib/facebook.coffee
Normal file
|
@ -0,0 +1,11 @@
|
|||
request = require 'request'
|
||||
Promise = require 'bluebird'
|
||||
|
||||
module.exports.fetchMe = (facebookAccessToken) ->
|
||||
return new Promise (resolve, reject) ->
|
||||
url = "https://graph.facebook.com/me?access_token=#{facebookAccessToken}"
|
||||
request.get url, {json: true}, (err, res) ->
|
||||
if err
|
||||
reject(err)
|
||||
else
|
||||
resolve(res.body)
|
11
server/lib/gplus.coffee
Normal file
|
@ -0,0 +1,11 @@
|
|||
request = require 'request'
|
||||
Promise = require 'bluebird'
|
||||
|
||||
module.exports.fetchMe = (gplusAccessToken) ->
|
||||
return new Promise (resolve, reject) ->
|
||||
url = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=#{gplusAccessToken}"
|
||||
request.get url, {json: true}, (err, res) ->
|
||||
if err
|
||||
reject(err)
|
||||
else
|
||||
resolve(res.body)
|
|
@ -197,12 +197,21 @@ module.exports =
|
|||
name: wrap (req, res) ->
|
||||
if not req.params.name
|
||||
throw new errors.UnprocessableEntity 'No name provided.'
|
||||
originalName = req.params.name
|
||||
givenName = req.params.name
|
||||
|
||||
User.unconflictNameAsync = Promise.promisify(User.unconflictName)
|
||||
name = yield User.unconflictNameAsync originalName
|
||||
response = name: name
|
||||
if originalName is name
|
||||
res.send 200, response
|
||||
else
|
||||
throw new errors.Conflict('Name is taken', response)
|
||||
suggestedName = yield User.unconflictNameAsync givenName
|
||||
response = {
|
||||
givenName
|
||||
suggestedName
|
||||
conflicts: givenName isnt suggestedName
|
||||
}
|
||||
res.send 200, response
|
||||
|
||||
email: wrap (req, res) ->
|
||||
{ email } = req.params
|
||||
if not email
|
||||
throw new errors.UnprocessableEntity 'No email provided.'
|
||||
|
||||
user = yield User.findByEmail(email)
|
||||
res.send 200, { exists: user? }
|
||||
|
|
18
server/middleware/contact.coffee
Normal file
|
@ -0,0 +1,18 @@
|
|||
sendwithus = require '../sendwithus'
|
||||
utils = require '../lib/utils'
|
||||
errors = require '../commons/errors'
|
||||
wrap = require 'co-express'
|
||||
database = require '../commons/database'
|
||||
parse = require '../commons/parse'
|
||||
|
||||
module.exports =
|
||||
sendParentSignupInstructions: wrap (req, res, next) ->
|
||||
context =
|
||||
email_id: sendwithus.templates.coppa_deny_parent_signup
|
||||
recipient:
|
||||
address: req.body.parentEmail
|
||||
sendwithus.api.send context, (err, result) ->
|
||||
if err
|
||||
return next(new errors.InternalServerError("Error sending email. Check that it's valid and try again."))
|
||||
else
|
||||
res.status(200).send()
|
|
@ -4,6 +4,7 @@ module.exports =
|
|||
classrooms: require './classrooms'
|
||||
campaigns: require './campaigns'
|
||||
codelogs: require './codelogs'
|
||||
contact: require './contact'
|
||||
courseInstances: require './course-instances'
|
||||
courses: require './courses'
|
||||
files: require './files'
|
||||
|
|
|
@ -11,6 +11,8 @@ mongoose = require 'mongoose'
|
|||
sendwithus = require '../sendwithus'
|
||||
User = require '../models/User'
|
||||
Classroom = require '../models/Classroom'
|
||||
facebook = require '../lib/facebook'
|
||||
gplus = require '../lib/gplus'
|
||||
|
||||
module.exports =
|
||||
fetchByGPlusID: wrap (req, res, next) ->
|
||||
|
@ -18,12 +20,12 @@ module.exports =
|
|||
gpAT = req.query.gplusAccessToken
|
||||
return next() unless gpID and gpAT
|
||||
|
||||
googleResponse = yield gplus.fetchMe(gpAT)
|
||||
idsMatch = gpID is googleResponse.id
|
||||
throw new errors.UnprocessableEntity('Invalid G+ Access Token.') unless idsMatch
|
||||
|
||||
dbq = User.find()
|
||||
dbq.select(parse.getProjectFromReq(req))
|
||||
url = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=#{gpAT}"
|
||||
[googleRes, body] = yield request.getAsync(url, {json: true})
|
||||
idsMatch = gpID is body.id
|
||||
throw new errors.UnprocessableEntity('Invalid G+ Access Token.') unless idsMatch
|
||||
user = yield User.findOne({gplusID: gpID})
|
||||
throw new errors.NotFound('No user with that G+ ID') unless user
|
||||
res.status(200).send(user.toObject({req: req}))
|
||||
|
@ -33,12 +35,12 @@ module.exports =
|
|||
fbAT = req.query.facebookAccessToken
|
||||
return next() unless fbID and fbAT
|
||||
|
||||
facebookResponse = yield facebook.fetchMe(fbAT)
|
||||
idsMatch = fbID is facebookResponse.id
|
||||
throw new errors.UnprocessableEntity('Invalid Facebook Access Token.') unless idsMatch
|
||||
|
||||
dbq = User.find()
|
||||
dbq.select(parse.getProjectFromReq(req))
|
||||
url = "https://graph.facebook.com/me?access_token=#{fbAT}"
|
||||
[facebookRes, body] = yield request.getAsync(url, {json: true})
|
||||
idsMatch = fbID is body.id
|
||||
throw new errors.UnprocessableEntity('Invalid Facebook Access Token.') unless idsMatch
|
||||
user = yield User.findOne({facebookID: fbID})
|
||||
throw new errors.NotFound('No user with that Facebook ID') unless user
|
||||
res.status(200).send(user.toObject({req: req}))
|
||||
|
@ -117,3 +119,80 @@ module.exports =
|
|||
if country = user.geo?.country
|
||||
user.geo.countryName = countryList.getName(country)
|
||||
res.status(200).send(users)
|
||||
|
||||
|
||||
signupWithPassword: wrap (req, res) ->
|
||||
unless req.user.isAnonymous()
|
||||
throw new errors.Forbidden('You are already signed in.')
|
||||
|
||||
{ password, email } = req.body
|
||||
unless _.all([password, email])
|
||||
throw new errors.UnprocessableEntity('Requires password and email')
|
||||
|
||||
if yield User.findByEmail(email)
|
||||
throw new errors.Conflict('Email already taken')
|
||||
|
||||
req.user.set({ password, email, anonymous: false })
|
||||
try
|
||||
yield req.user.save()
|
||||
catch e
|
||||
if e.code is 11000 # Duplicate key error
|
||||
throw new errors.Conflict('Email already taken')
|
||||
else
|
||||
throw e
|
||||
|
||||
req.user.sendWelcomeEmail()
|
||||
res.status(200).send(req.user.toObject({req: req}))
|
||||
|
||||
signupWithFacebook: wrap (req, res) ->
|
||||
unless req.user.isAnonymous()
|
||||
throw new errors.Forbidden('You are already signed in.')
|
||||
|
||||
{ facebookID, facebookAccessToken, email } = req.body
|
||||
unless _.all([facebookID, facebookAccessToken, email])
|
||||
throw new errors.UnprocessableEntity('Requires facebookID, facebookAccessToken and email')
|
||||
|
||||
facebookResponse = yield facebook.fetchMe(facebookAccessToken)
|
||||
emailsMatch = email is facebookResponse.email
|
||||
idsMatch = facebookID is facebookResponse.id
|
||||
unless emailsMatch and idsMatch
|
||||
throw new errors.UnprocessableEntity('Invalid facebookAccessToken')
|
||||
|
||||
req.user.set({ facebookID, email, anonymous: false })
|
||||
try
|
||||
yield req.user.save()
|
||||
catch e
|
||||
if e.code is 11000 # Duplicate key error
|
||||
throw new errors.Conflict('Email already taken')
|
||||
else
|
||||
throw e
|
||||
|
||||
req.user.sendWelcomeEmail()
|
||||
res.status(200).send(req.user.toObject({req: req}))
|
||||
|
||||
signupWithGPlus: wrap (req, res) ->
|
||||
unless req.user.isAnonymous()
|
||||
throw new errors.Forbidden('You are already signed in.')
|
||||
|
||||
{ gplusID, gplusAccessToken, email } = req.body
|
||||
unless _.all([gplusID, gplusAccessToken, email])
|
||||
throw new errors.UnprocessableEntity('Requires gplusID, gplusAccessToken and email')
|
||||
|
||||
gplusResponse = yield gplus.fetchMe(gplusAccessToken)
|
||||
emailsMatch = email is gplusResponse.email
|
||||
idsMatch = gplusID is gplusResponse.id
|
||||
|
||||
unless emailsMatch and idsMatch
|
||||
throw new errors.UnprocessableEntity('Invalid gplusAccessToken')
|
||||
|
||||
req.user.set({ gplusID, email, anonymous: false })
|
||||
try
|
||||
yield req.user.save()
|
||||
catch e
|
||||
if e.code is 11000 # Duplicate key error
|
||||
throw new errors.Conflict('Email already taken')
|
||||
else
|
||||
throw e
|
||||
|
||||
req.user.sendWelcomeEmail()
|
||||
res.status(200).send(req.user.toObject({req: req}))
|
||||
|
|
|
@ -31,6 +31,6 @@ AnalyticsLogEventSchema.statics.logEvent = (user, event, properties={}) ->
|
|||
unless config.proxy
|
||||
analyticsMongoose = mongoose.createConnection()
|
||||
analyticsMongoose.open "mongodb://#{config.mongo.analytics_host}:#{config.mongo.analytics_port}/#{config.mongo.analytics_db}", (error) ->
|
||||
console.log "Couldnt connect to analytics", error
|
||||
log.warn "Couldnt connect to analytics", error
|
||||
|
||||
module.exports = AnalyticsLogEvent = analyticsMongoose.model('analytics.log.event', AnalyticsLogEventSchema, config.mongo.analytics_collection)
|
||||
|
|
|
@ -118,6 +118,10 @@ UserSchema.statics.search = (term, done) ->
|
|||
query = $or: [{nameLower: term}, {emailLower: term}]
|
||||
return User.findOne(query).exec(done)
|
||||
|
||||
UserSchema.statics.findByEmail = (email, done=_.noop) ->
|
||||
emailLower = email.toLowerCase()
|
||||
User.findOne({emailLower: emailLower}).exec(done)
|
||||
|
||||
emailNameMap =
|
||||
generalNews: 'announcement'
|
||||
adventurerNews: 'tester'
|
||||
|
@ -262,14 +266,7 @@ UserSchema.statics.unconflictName = unconflictName = (name, done) ->
|
|||
suffix = _.random(0, 9) + ''
|
||||
unconflictName name + suffix, done
|
||||
|
||||
UserSchema.methods.register = (done) ->
|
||||
@set('anonymous', false)
|
||||
if (name = @get 'name')? and name isnt ''
|
||||
unconflictName name, (err, uniqueName) =>
|
||||
return done err if err
|
||||
@set 'name', uniqueName
|
||||
done()
|
||||
else done()
|
||||
UserSchema.methods.sendWelcomeEmail = ->
|
||||
{ welcome_email_student, welcome_email_user } = sendwithus.templates
|
||||
timestamp = (new Date).getTime()
|
||||
data =
|
||||
|
@ -282,7 +279,6 @@ UserSchema.methods.register = (done) ->
|
|||
verify_link: "http://codecombat.com/user/#{@_id}/verify/#{@verificationCode(timestamp)}"
|
||||
sendwithus.api.send data, (err, result) ->
|
||||
log.error "sendwithus post-save error: #{err}, result: #{result}" if err
|
||||
@saveActiveUser 'register'
|
||||
|
||||
UserSchema.methods.hasSubscription = ->
|
||||
return false unless stripeObject = @get('stripe')
|
||||
|
@ -361,10 +357,7 @@ UserSchema.pre('save', (next) ->
|
|||
if @get('password')
|
||||
@set('passwordHash', User.hashPassword(pwd))
|
||||
@set('password', undefined)
|
||||
if @get('email') and @get('anonymous') # a user registers
|
||||
@register next
|
||||
else
|
||||
next()
|
||||
next()
|
||||
)
|
||||
|
||||
UserSchema.post 'save', (doc) ->
|
||||
|
|
|
@ -8,12 +8,15 @@ module.exports.setup = (app) ->
|
|||
app.post('/auth/login-gplus', mw.auth.loginByGPlus, mw.auth.afterLogin)
|
||||
app.post('/auth/logout', mw.auth.logout)
|
||||
app.get('/auth/name/?(:name)?', mw.auth.name)
|
||||
app.get('/auth/email/?(:email)?', mw.auth.email)
|
||||
app.post('/auth/reset', mw.auth.reset)
|
||||
app.post('/auth/spy', mw.auth.spy)
|
||||
app.post('/auth/stop-spying', mw.auth.stopSpying)
|
||||
app.get('/auth/unsubscribe', mw.auth.unsubscribe)
|
||||
app.get('/auth/whoami', mw.auth.whoAmI)
|
||||
|
||||
app.post('/contact/send-parent-signup-instructions', mw.contact.sendParentSignupInstructions)
|
||||
|
||||
app.delete('/db/*', mw.auth.checkHasUser())
|
||||
app.patch('/db/*', mw.auth.checkHasUser())
|
||||
app.post('/db/*', mw.auth.checkHasUser())
|
||||
|
@ -96,6 +99,9 @@ module.exports.setup = (app) ->
|
|||
app.get('/db/level/:handle/session', mw.auth.checkHasUser(), mw.levels.upsertSession)
|
||||
app.get('/db/user/-/students', mw.auth.checkHasPermission(['admin']), mw.users.getStudents)
|
||||
app.get('/db/user/-/teachers', mw.auth.checkHasPermission(['admin']), mw.users.getTeachers)
|
||||
app.post('/db/user/:handle/signup-with-facebook', mw.users.signupWithFacebook)
|
||||
app.post('/db/user/:handle/signup-with-gplus', mw.users.signupWithGPlus)
|
||||
app.post('/db/user/:handle/signup-with-password', mw.users.signupWithPassword)
|
||||
|
||||
app.get('/db/prepaid', mw.auth.checkLoggedIn(), mw.prepaids.fetchByCreator)
|
||||
app.post('/db/prepaid', mw.auth.checkHasPermission(['admin']), mw.prepaids.post)
|
||||
|
|
|
@ -17,6 +17,7 @@ if swuAPIKey
|
|||
|
||||
module.exports.templates =
|
||||
parent_subscribe_email: 'tem_2APERafogvwKhmcnouigud'
|
||||
coppa_deny_parent_signup: 'tem_d5fCpXS8V7jgff2sYKCinX'
|
||||
share_progress_email: 'tem_VHE3ihhGmVa3727qds9zY8'
|
||||
welcome_email_user: 'tem_z7Xvj3mtWYk6ec6aW7RwFk'
|
||||
welcome_email_student: 'tem_4WYPZNLzs5wawMF9qUJXUH'
|
||||
|
|
|
@ -73,6 +73,8 @@ setupErrorMiddleware = (app) ->
|
|||
res.status(err.status ? 500).send(error: "Something went wrong!")
|
||||
message = "Express error: #{req.method} #{req.path}: #{err.message}"
|
||||
log.error "#{message}, stack: #{err.stack}"
|
||||
if global.testing
|
||||
console.log "#{message}, stack: #{err.stack}"
|
||||
slack.sendSlackMessage(message, ['ops'], {papertrail: true})
|
||||
else
|
||||
next(err)
|
||||
|
|
|
@ -231,18 +231,20 @@ describe 'GET /auth/name', ->
|
|||
expect(res.statusCode).toBe 422
|
||||
done()
|
||||
|
||||
it 'returns the name given if there is no conflict', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {url: getURL(url + '/Gandalf'), json: {}}
|
||||
it 'returns an object with properties conflicts, givenName and suggestedName', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync {url: getURL(url + '/Gandalf'), json: true}
|
||||
expect(res.statusCode).toBe 200
|
||||
expect(res.body.name).toBe 'Gandalf'
|
||||
done()
|
||||
expect(res.body.givenName).toBe 'Gandalf'
|
||||
expect(res.body.conflicts).toBe false
|
||||
expect(res.body.suggestedName).toBe 'Gandalf'
|
||||
|
||||
it 'returns a new name in case of conflict', utils.wrap (done) ->
|
||||
yield utils.initUser({name: 'joe'})
|
||||
[res, body] = yield request.getAsync {url: getURL(url + '/joe'), json: {}}
|
||||
expect(res.statusCode).toBe 409
|
||||
expect(res.body.name).not.toBe 'joe'
|
||||
expect(/joe[0-9]/.test(res.body.name)).toBe(true)
|
||||
expect(res.statusCode).toBe 200
|
||||
expect(res.body.suggestedName).not.toBe 'joe'
|
||||
expect(res.body.conflicts).toBe true
|
||||
expect(/joe[0-9]/.test(res.body.suggestedName)).toBe(true)
|
||||
|
||||
done()
|
||||
|
||||
|
||||
|
|
|
@ -5,6 +5,10 @@ User = require '../../../server/models/User'
|
|||
Classroom = require '../../../server/models/Classroom'
|
||||
Prepaid = require '../../../server/models/Prepaid'
|
||||
request = require '../request'
|
||||
facebook = require '../../../server/lib/facebook'
|
||||
gplus = require '../../../server/lib/gplus'
|
||||
sendwithus = require '../../../server/sendwithus'
|
||||
Promise = require 'bluebird'
|
||||
|
||||
describe 'POST /db/user', ->
|
||||
|
||||
|
@ -177,27 +181,6 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl
|
|||
sam.set 'name', samsName
|
||||
done()
|
||||
|
||||
it 'should silently rename an anonymous user if their name conflicts upon signup', (done) ->
|
||||
request.post getURL('/auth/logout'), ->
|
||||
request.get getURL('/auth/whoami'), ->
|
||||
json = { name: 'admin' }
|
||||
request.post { url: getURL('/db/user'), json }, (err, response) ->
|
||||
expect(response.statusCode).toBe(200)
|
||||
request.get getURL('/auth/whoami'), (err, response) ->
|
||||
expect(err).toBeNull()
|
||||
guy = JSON.parse(response.body)
|
||||
expect(guy.anonymous).toBeTruthy()
|
||||
expect(guy.name).toEqual 'admin'
|
||||
|
||||
guy.email = 'blub@blub' # Email means registration
|
||||
req = request.post {url: getURL('/db/user'), json: guy}, (err, response) ->
|
||||
expect(err).toBeNull()
|
||||
finalGuy = response.body
|
||||
expect(finalGuy.anonymous).toBeFalsy()
|
||||
expect(finalGuy.name).not.toEqual guy.name
|
||||
expect(finalGuy.name.length).toBe guy.name.length + 1
|
||||
done()
|
||||
|
||||
it 'should be able to unset a slug by setting an empty name', (done) ->
|
||||
loginSam (sam) ->
|
||||
samsName = sam.get 'name'
|
||||
|
@ -690,3 +673,206 @@ describe 'Statistics', ->
|
|||
expect(err).toBeNull()
|
||||
|
||||
done()
|
||||
|
||||
|
||||
describe 'POST /db/user/:handle/signup-with-password', ->
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([User])
|
||||
yield new Promise((resolve) -> setTimeout(resolve, 10))
|
||||
done()
|
||||
|
||||
it 'signs up the user with the password and sends welcome emails', utils.wrap (done) ->
|
||||
spyOn(sendwithus.api, 'send')
|
||||
user = yield utils.becomeAnonymous()
|
||||
url = getURL("/db/user/#{user.id}/signup-with-password")
|
||||
email = 'some@email.com'
|
||||
json = { email, password: '12345' }
|
||||
[res, body] = yield request.postAsync({url, json})
|
||||
expect(res.statusCode).toBe(200)
|
||||
updatedUser = yield User.findById(user.id)
|
||||
expect(updatedUser.get('email')).toBe(email)
|
||||
expect(updatedUser.get('passwordHash')).toBeDefined()
|
||||
expect(sendwithus.api.send).toHaveBeenCalled()
|
||||
done()
|
||||
|
||||
it 'returns 409 if there is already a user with the given email', utils.wrap (done) ->
|
||||
email = 'some@email.com'
|
||||
initialUser = yield utils.initUser({email})
|
||||
expect(initialUser.get('emailLower')).toBeDefined()
|
||||
user = yield utils.becomeAnonymous()
|
||||
url = getURL("/db/user/#{user.id}/signup-with-password")
|
||||
json = { email, password: '12345' }
|
||||
[res, body] = yield request.postAsync({url, json})
|
||||
expect(res.statusCode).toBe(409)
|
||||
done()
|
||||
|
||||
|
||||
describe 'POST /db/user/:handle/signup-with-facebook', ->
|
||||
facebookID = '12345'
|
||||
facebookEmail = 'some@email.com'
|
||||
|
||||
validFacebookResponse = new Promise((resolve) -> resolve({
|
||||
id: facebookID,
|
||||
email: facebookEmail,
|
||||
first_name: 'Some',
|
||||
gender: 'male',
|
||||
last_name: 'Person',
|
||||
link: 'https://www.facebook.com/app_scoped_user_id/12345/',
|
||||
locale: 'en_US',
|
||||
name: 'Some Person',
|
||||
timezone: -7,
|
||||
updated_time: '2015-12-08T17:10:39+0000',
|
||||
verified: true
|
||||
}))
|
||||
|
||||
invalidFacebookResponse = new Promise((resolve) -> resolve({
|
||||
error: {
|
||||
message: 'Invalid OAuth access token.',
|
||||
type: 'OAuthException',
|
||||
code: 190,
|
||||
fbtrace_id: 'EC4dEdeKHBH'
|
||||
}
|
||||
}))
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([User])
|
||||
yield new Promise((resolve) -> setTimeout(resolve, 10))
|
||||
done()
|
||||
|
||||
it 'signs up the user with the facebookID and sends welcome emails', utils.wrap (done) ->
|
||||
spyOn(facebook, 'fetchMe').and.returnValue(validFacebookResponse)
|
||||
spyOn(sendwithus.api, 'send')
|
||||
user = yield utils.becomeAnonymous()
|
||||
url = getURL("/db/user/#{user.id}/signup-with-facebook")
|
||||
json = { email: facebookEmail, facebookID, facebookAccessToken: '...' }
|
||||
[res, body] = yield request.postAsync({url, json})
|
||||
expect(res.statusCode).toBe(200)
|
||||
updatedUser = yield User.findById(user.id)
|
||||
expect(updatedUser.get('email')).toBe(facebookEmail)
|
||||
expect(updatedUser.get('facebookID')).toBe(facebookID)
|
||||
expect(sendwithus.api.send).toHaveBeenCalled()
|
||||
done()
|
||||
|
||||
it 'returns 422 if facebook does not recognize the access token', utils.wrap (done) ->
|
||||
spyOn(facebook, 'fetchMe').and.returnValue(invalidFacebookResponse)
|
||||
user = yield utils.becomeAnonymous()
|
||||
url = getURL("/db/user/#{user.id}/signup-with-facebook")
|
||||
json = { email: facebookEmail, facebookID, facebookAccessToken: '...' }
|
||||
[res, body] = yield request.postAsync({url, json})
|
||||
expect(res.statusCode).toBe(422)
|
||||
done()
|
||||
|
||||
it 'returns 422 if the email or id do not match', utils.wrap (done) ->
|
||||
spyOn(facebook, 'fetchMe').and.returnValue(validFacebookResponse)
|
||||
user = yield utils.becomeAnonymous()
|
||||
url = getURL("/db/user/#{user.id}/signup-with-facebook")
|
||||
|
||||
json = { email: 'some-other@email.com', facebookID, facebookAccessToken: '...' }
|
||||
[res, body] = yield request.postAsync({url, json})
|
||||
expect(res.statusCode).toBe(422)
|
||||
|
||||
json = { email: facebookEmail, facebookID: '54321', facebookAccessToken: '...' }
|
||||
[res, body] = yield request.postAsync({url, json})
|
||||
expect(res.statusCode).toBe(422)
|
||||
|
||||
done()
|
||||
|
||||
it 'returns 409 if there is already a user with the given email', utils.wrap (done) ->
|
||||
initialUser = yield utils.initUser({email: facebookEmail})
|
||||
expect(initialUser.get('emailLower')).toBeDefined()
|
||||
spyOn(facebook, 'fetchMe').and.returnValue(validFacebookResponse)
|
||||
user = yield utils.becomeAnonymous()
|
||||
url = getURL("/db/user/#{user.id}/signup-with-facebook")
|
||||
json = { email: facebookEmail, facebookID, facebookAccessToken: '...' }
|
||||
[res, body] = yield request.postAsync({url, json})
|
||||
expect(res.statusCode).toBe(409)
|
||||
done()
|
||||
|
||||
|
||||
describe 'POST /db/user/:handle/signup-with-gplus', ->
|
||||
gplusID = '12345'
|
||||
gplusEmail = 'some@email.com'
|
||||
|
||||
validGPlusResponse = new Promise((resolve) -> resolve({
|
||||
id: gplusID
|
||||
email: gplusEmail,
|
||||
verified_email: true,
|
||||
name: 'Some Person',
|
||||
given_name: 'Some',
|
||||
family_name: 'Person',
|
||||
link: 'https://plus.google.com/12345',
|
||||
picture: 'https://lh6.googleusercontent.com/...',
|
||||
gender: 'male',
|
||||
locale: 'en'
|
||||
}))
|
||||
|
||||
invalidGPlusResponse = new Promise((resolve) -> resolve({
|
||||
"error": {
|
||||
"errors": [
|
||||
{
|
||||
"domain": "global",
|
||||
"reason": "authError",
|
||||
"message": "Invalid Credentials",
|
||||
"locationType": "header",
|
||||
"location": "Authorization"
|
||||
}
|
||||
],
|
||||
"code": 401,
|
||||
"message": "Invalid Credentials"
|
||||
}
|
||||
}))
|
||||
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels([User])
|
||||
yield new Promise((resolve) -> setTimeout(resolve, 10))
|
||||
done()
|
||||
|
||||
it 'signs up the user with the gplusID and sends welcome emails', utils.wrap (done) ->
|
||||
spyOn(gplus, 'fetchMe').and.returnValue(validGPlusResponse)
|
||||
spyOn(sendwithus.api, 'send')
|
||||
user = yield utils.becomeAnonymous()
|
||||
url = getURL("/db/user/#{user.id}/signup-with-gplus")
|
||||
json = { email: gplusEmail, gplusID, gplusAccessToken: '...' }
|
||||
[res, body] = yield request.postAsync({url, json})
|
||||
expect(res.statusCode).toBe(200)
|
||||
updatedUser = yield User.findById(user.id)
|
||||
expect(updatedUser.get('email')).toBe(gplusEmail)
|
||||
expect(updatedUser.get('gplusID')).toBe(gplusID)
|
||||
expect(sendwithus.api.send).toHaveBeenCalled()
|
||||
done()
|
||||
|
||||
it 'returns 422 if gplus does not recognize the access token', utils.wrap (done) ->
|
||||
spyOn(gplus, 'fetchMe').and.returnValue(invalidGPlusResponse)
|
||||
user = yield utils.becomeAnonymous()
|
||||
url = getURL("/db/user/#{user.id}/signup-with-gplus")
|
||||
json = { email: gplusEmail, gplusID, gplusAccessToken: '...' }
|
||||
[res, body] = yield request.postAsync({url, json})
|
||||
expect(res.statusCode).toBe(422)
|
||||
done()
|
||||
|
||||
it 'returns 422 if the email or id do not match', utils.wrap (done) ->
|
||||
spyOn(gplus, 'fetchMe').and.returnValue(validGPlusResponse)
|
||||
user = yield utils.becomeAnonymous()
|
||||
url = getURL("/db/user/#{user.id}/signup-with-gplus")
|
||||
|
||||
json = { email: 'some-other@email.com', gplusID, gplusAccessToken: '...' }
|
||||
[res, body] = yield request.postAsync({url, json})
|
||||
expect(res.statusCode).toBe(422)
|
||||
|
||||
json = { email: gplusEmail, gplusID: '54321', gplusAccessToken: '...' }
|
||||
[res, body] = yield request.postAsync({url, json})
|
||||
expect(res.statusCode).toBe(422)
|
||||
|
||||
done()
|
||||
|
||||
it 'returns 409 if there is already a user with the given email', utils.wrap (done) ->
|
||||
yield utils.initUser({email: gplusEmail})
|
||||
spyOn(gplus, 'fetchMe').and.returnValue(validGPlusResponse)
|
||||
user = yield utils.becomeAnonymous()
|
||||
url = getURL("/db/user/#{user.id}/signup-with-gplus")
|
||||
json = { email: gplusEmail, gplusID, gplusAccessToken: '...' }
|
||||
[res, body] = yield request.postAsync({url, json})
|
||||
expect(res.statusCode).toBe(409)
|
||||
done()
|
||||
|
||||
|
|
|
@ -74,7 +74,8 @@ module.exports = mw =
|
|||
|
||||
becomeAnonymous: Promise.promisify (done) ->
|
||||
request.post mw.getURL('/auth/logout'), ->
|
||||
request.get mw.getURL('/auth/whoami'), done
|
||||
request.get mw.getURL('/auth/whoami'), {json: true}, (err, res) ->
|
||||
User.findById(res.body._id).exec(done)
|
||||
|
||||
logout: Promise.promisify (done) ->
|
||||
request.post mw.getURL('/auth/logout'), done
|
||||
|
|
|
@ -1,240 +1,414 @@
|
|||
CreateAccountModal = require 'views/core/CreateAccountModal'
|
||||
COPPADenyModal = require 'views/core/COPPADenyModal'
|
||||
Classroom = require 'models/Classroom'
|
||||
#COPPADenyModal = require 'views/core/COPPADenyModal'
|
||||
forms = require 'core/forms'
|
||||
factories = require 'test/app/factories'
|
||||
|
||||
describe 'CreateAccountModal', ->
|
||||
# TODO: Figure out why these tests break Travis. Suspect it has to do with the
|
||||
# asynchronous, Promise system. On the browser, these work, but in Travis, they
|
||||
# sometimes fail, so it's some sort of race condition.
|
||||
|
||||
responses = {
|
||||
signupSuccess: { status: 200, responseText: JSON.stringify({ email: 'some@email.com' })}
|
||||
}
|
||||
|
||||
xdescribe 'CreateAccountModal', ->
|
||||
|
||||
modal = null
|
||||
|
||||
initModal = (options) -> (done) ->
|
||||
application.facebookHandler.fakeAPI()
|
||||
application.gplusHandler.fakeAPI()
|
||||
modal = new CreateAccountModal(options)
|
||||
modal.render()
|
||||
modal.render = _.noop
|
||||
jasmine.demoModal(modal)
|
||||
_.defer done
|
||||
# initModal = (options) -> ->
|
||||
# application.facebookHandler.fakeAPI()
|
||||
# application.gplusHandler.fakeAPI()
|
||||
# modal = new CreateAccountModal(options)
|
||||
# jasmine.demoModal(modal)
|
||||
|
||||
afterEach ->
|
||||
modal.stopListening()
|
||||
describe 'click SIGN IN button', ->
|
||||
it 'switches to AuthModal', ->
|
||||
modal = new CreateAccountModal()
|
||||
modal.render()
|
||||
jasmine.demoModal(modal)
|
||||
spyOn(modal, 'openModalView')
|
||||
modal.$('.login-link').click()
|
||||
expect(modal.openModalView).toHaveBeenCalled()
|
||||
|
||||
describe 'constructed with showRequiredError is true', ->
|
||||
beforeEach initModal({showRequiredError: true})
|
||||
it 'shows a modal explaining to login first', ->
|
||||
expect(modal.$('#required-error-alert').length).toBe(1)
|
||||
|
||||
describe 'constructed with showSignupRationale is true', ->
|
||||
beforeEach initModal({showSignupRationale: true})
|
||||
it 'shows a modal explaining signup rationale', ->
|
||||
expect(modal.$('#signup-rationale-alert').length).toBe(1)
|
||||
|
||||
describe 'clicking the save button', ->
|
||||
|
||||
beforeEach initModal()
|
||||
|
||||
it 'fails if nothing is in the form, showing errors for email, birthday, and password', ->
|
||||
modal.$('form').each (i, el) -> el.reset()
|
||||
modal.$('form').submit()
|
||||
expect(jasmine.Ajax.requests.all().length).toBe(0)
|
||||
expect(modal.$('.has-error').length).toBe(3)
|
||||
|
||||
it 'fails if email is missing', ->
|
||||
modal.$('form').each (i, el) -> el.reset()
|
||||
forms.objectToForm(modal.$el, { name: 'Name', password: 'xyzzy', birthdayDay: 24, birthdayMonth: 7, birthdayYear: 1988 })
|
||||
modal.$('form').submit()
|
||||
expect(jasmine.Ajax.requests.all().length).toBe(0)
|
||||
expect(modal.$('.has-error').length).toBeTruthy()
|
||||
|
||||
it 'fails if birthday is missing', ->
|
||||
modal.$('form').each (i, el) -> el.reset()
|
||||
forms.objectToForm(modal.$el, { email: 'some@email.com', password: 'xyzzy' })
|
||||
modal.$('form').submit()
|
||||
expect(jasmine.Ajax.requests.all().length).toBe(0)
|
||||
expect(modal.$('.has-error').length).toBe(1)
|
||||
|
||||
it 'fails if user is too young', ->
|
||||
modal.$('form').each (i, el) -> el.reset()
|
||||
forms.objectToForm(modal.$el, { email: 'some@email.com', password: 'xyzzy', birthdayDay: 24, birthdayMonth: 7, birthdayYear: (new Date().getFullYear() - 10) })
|
||||
modalOpened = false
|
||||
spyOn(modal, 'openModalView').and.callFake (modal) ->
|
||||
modalOpened = true
|
||||
expect(modal instanceof COPPADenyModal).toBe(true)
|
||||
|
||||
modal.$('form').submit()
|
||||
expect(jasmine.Ajax.requests.all().length).toBe(0)
|
||||
expect(modalOpened).toBeTruthy()
|
||||
|
||||
it 'signs up if only email, birthday, and password is provided', ->
|
||||
modal.$('form').each (i, el) -> el.reset()
|
||||
forms.objectToForm(modal.$el, { email: 'some@email.com', password: 'xyzzy', birthdayDay: 24, birthdayMonth: 7, birthdayYear: 1988 })
|
||||
modal.$('form').submit()
|
||||
requests = jasmine.Ajax.requests.all()
|
||||
expect(requests.length).toBe(1)
|
||||
expect(modal.$el.has('.has-warning').length).toBeFalsy()
|
||||
expect(modal.$('#signup-button').is(':disabled')).toBe(true)
|
||||
|
||||
describe 'and a class code is entered', ->
|
||||
describe 'ChooseAccountTypeView', ->
|
||||
beforeEach ->
|
||||
modal = new CreateAccountModal()
|
||||
modal.render()
|
||||
jasmine.demoModal(modal)
|
||||
|
||||
describe 'click sign up as TEACHER button', ->
|
||||
beforeEach ->
|
||||
modal.$('form').each (i, el) -> el.reset()
|
||||
forms.objectToForm(modal.$el, { email: 'some@email.com', password: 'xyzzy', classCode: 'qwerty' })
|
||||
modal.$('form').submit()
|
||||
expect(jasmine.Ajax.requests.all().length).toBe(1)
|
||||
spyOn application.router, 'navigate'
|
||||
modal.$('.teacher-path-button').click()
|
||||
|
||||
it 'checks for Classroom existence if a class code was entered', ->
|
||||
it 'navigates the user to /teachers/signup', ->
|
||||
expect(application.router.navigate).toHaveBeenCalled()
|
||||
args = application.router.navigate.calls.argsFor(0)
|
||||
expect(args[0]).toBe('/teachers/signup')
|
||||
|
||||
describe 'click sign up as STUDENT button', ->
|
||||
beforeEach ->
|
||||
modal.$('.student-path-button').click()
|
||||
|
||||
it 'switches to SegmentCheckView and sets "path" to "student"', ->
|
||||
expect(modal.signupState.get('path')).toBe('student')
|
||||
expect(modal.signupState.get('screen')).toBe('segment-check')
|
||||
|
||||
describe 'click sign up as INDIVIDUAL button', ->
|
||||
beforeEach ->
|
||||
modal.$('.individual-path-button').click()
|
||||
|
||||
it 'switches to SegmentCheckView and sets "path" to "individual"', ->
|
||||
expect(modal.signupState.get('path')).toBe('individual')
|
||||
expect(modal.signupState.get('screen')).toBe('segment-check')
|
||||
|
||||
describe 'SegmentCheckView', ->
|
||||
|
||||
segmentCheckView = null
|
||||
|
||||
describe 'INDIVIDUAL path', ->
|
||||
beforeEach ->
|
||||
modal = new CreateAccountModal()
|
||||
modal.render()
|
||||
jasmine.demoModal(modal)
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
expect(request.url).toBe('/db/classroom?code=qwerty')
|
||||
modal.$('.individual-path-button').click()
|
||||
segmentCheckView = modal.subviews.segment_check_view
|
||||
|
||||
it 'has not hidden the close-modal button', ->
|
||||
expect(modal.$('#close-modal').css('display')).not.toBe('none')
|
||||
it 'has a birthdate form', ->
|
||||
expect(modal.$('.birthday-form-group').length).toBe(1)
|
||||
|
||||
describe 'the Classroom exists', ->
|
||||
it 'continues with signup', ->
|
||||
describe 'STUDENT path', ->
|
||||
beforeEach ->
|
||||
modal = new CreateAccountModal()
|
||||
modal.render()
|
||||
jasmine.demoModal(modal)
|
||||
modal.$('.student-path-button').click()
|
||||
segmentCheckView = modal.subviews.segment_check_view
|
||||
spyOn(segmentCheckView, 'checkClassCodeDebounced')
|
||||
|
||||
it 'has a classCode input', ->
|
||||
expect(modal.$('.class-code-input').length).toBe(1)
|
||||
|
||||
it 'checks the class code when the input changes', ->
|
||||
modal.$('.class-code-input').val('test').trigger('input')
|
||||
expect(segmentCheckView.checkClassCodeDebounced).toHaveBeenCalled()
|
||||
|
||||
describe 'fetchClassByCode()', ->
|
||||
it 'is memoized', ->
|
||||
promise1 = segmentCheckView.fetchClassByCode('testA')
|
||||
promise2 = segmentCheckView.fetchClassByCode('testA')
|
||||
promise3 = segmentCheckView.fetchClassByCode('testB')
|
||||
expect(promise1).toBe(promise2)
|
||||
expect(promise1).not.toBe(promise3)
|
||||
|
||||
describe 'checkClassCode()', ->
|
||||
it 'shows a success message if the classCode is found', ->
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith({status: 200, responseText: JSON.stringify({})})
|
||||
expect(request).toBeUndefined()
|
||||
modal.$('.class-code-input').val('test').trigger('input')
|
||||
segmentCheckView.checkClassCode()
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
expect(request.url).toBe('/db/user')
|
||||
expect(request.method).toBe('POST')
|
||||
expect(request).toBeDefined()
|
||||
request.respondWith({
|
||||
status: 200
|
||||
responseText: JSON.stringify({
|
||||
data: factories.makeClassroom({name: 'Some Classroom'}).toJSON()
|
||||
owner: factories.makeUser({name: 'Some Teacher'}).toJSON()
|
||||
})
|
||||
})
|
||||
|
||||
describe 'the Classroom does not exist', ->
|
||||
it 'shows an error and clears the field', ->
|
||||
describe 'on submit with class code', ->
|
||||
|
||||
classCodeRequest = null
|
||||
|
||||
beforeEach ->
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith({status: 404, responseText: JSON.stringify({})})
|
||||
expect(jasmine.Ajax.requests.all().length).toBe(1)
|
||||
expect(modal.$el.has('.has-error').length).toBeTruthy()
|
||||
expect(modal.$('#class-code-input').val()).toBe('')
|
||||
expect(request).toBeUndefined()
|
||||
modal.$('.class-code-input').val('test').trigger('input')
|
||||
modal.$('form.segment-check').submit()
|
||||
classCodeRequest = jasmine.Ajax.requests.mostRecent()
|
||||
expect(classCodeRequest).toBeDefined()
|
||||
|
||||
describe 'when the classroom IS found', ->
|
||||
beforeEach (done) ->
|
||||
classCodeRequest.respondWith({
|
||||
status: 200
|
||||
responseText: JSON.stringify({
|
||||
data: factories.makeClassroom({name: 'Some Classroom'}).toJSON()
|
||||
owner: factories.makeUser({name: 'Some Teacher'}).toJSON()
|
||||
})
|
||||
})
|
||||
_.defer done
|
||||
|
||||
describe 'clicking the gplus button', ->
|
||||
it 'navigates to the BasicInfoView', ->
|
||||
expect(modal.signupState.get('screen')).toBe('basic-info')
|
||||
|
||||
signupButton = null
|
||||
describe 'when the classroom IS NOT found', ->
|
||||
beforeEach (done) ->
|
||||
classCodeRequest.respondWith({
|
||||
status: 404
|
||||
responseText: '{}'
|
||||
})
|
||||
segmentCheckView.once 'special-render', done
|
||||
|
||||
beforeEach initModal()
|
||||
it 'shows an error', ->
|
||||
expect(modal.$('[data-i18n="signup.classroom_not_found"]').length).toBe(1)
|
||||
|
||||
describe 'CoppaDenyView', ->
|
||||
|
||||
coppaDenyView = null
|
||||
|
||||
beforeEach ->
|
||||
forms.objectToForm(modal.$el, { birthdayDay: 24, birthdayMonth: 7, birthdayYear: 1988 })
|
||||
signupButton = modal.$('#gplus-signup-btn')
|
||||
expect(signupButton.attr('disabled')).toBeFalsy()
|
||||
signupButton.click()
|
||||
modal = new CreateAccountModal()
|
||||
modal.signupState.set({
|
||||
path: 'individual'
|
||||
screen: 'coppa-deny'
|
||||
})
|
||||
modal.render()
|
||||
jasmine.demoModal(modal)
|
||||
coppaDenyView = modal.subviews.coppa_deny_view
|
||||
|
||||
it 'checks to see if the user already exists in our system', ->
|
||||
requests = jasmine.Ajax.requests.all()
|
||||
expect(requests.length).toBe(1)
|
||||
expect(signupButton.attr('disabled')).toBeTruthy()
|
||||
it 'shows an input for a parent\'s email address to sign up their child', ->
|
||||
expect(modal.$('#parent-email-input').length).toBe(1)
|
||||
|
||||
|
||||
describe 'and finding the given person is already a user', ->
|
||||
beforeEach ->
|
||||
expect(modal.$('#gplus-account-exists-row').hasClass('hide')).toBe(true)
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith({status: 200, responseText: JSON.stringify({_id: 'existinguser'})})
|
||||
describe 'BasicInfoView', ->
|
||||
|
||||
it 'shows a message saying you are connected with Google+, with a button for logging in', ->
|
||||
expect(modal.$('#gplus-account-exists-row').hasClass('hide')).toBe(false)
|
||||
loginBtn = modal.$('#gplus-login-btn')
|
||||
expect(loginBtn.attr('disabled')).toBeFalsy()
|
||||
loginBtn.click()
|
||||
expect(loginBtn.attr('disabled')).toBeTruthy()
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
expect(request.method).toBe('POST')
|
||||
expect(request.params).toBe('gplusID=abcd&gplusAccessToken=1234')
|
||||
expect(request.url).toBe('/auth/login-gplus')
|
||||
|
||||
describe 'and the user finishes signup anyway with new info', ->
|
||||
beforeEach ->
|
||||
forms.objectToForm(modal.$el, { email: 'some@email.com', birthdayDay: 24, birthdayMonth: 7, birthdayYear: 1988 })
|
||||
modal.$('form').submit()
|
||||
|
||||
it 'upserts the values to the new user', ->
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
expect(request.method).toBe('PUT')
|
||||
expect(request.url).toBe('/db/user?gplusID=abcd&gplusAccessToken=1234')
|
||||
|
||||
|
||||
describe 'and finding the given person is not yet a user', ->
|
||||
beforeEach ->
|
||||
expect(modal.$('#gplus-logged-in-row').hasClass('hide')).toBe(true)
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith({status: 404})
|
||||
|
||||
it 'shows a message saying you are connected with Google+', ->
|
||||
expect(modal.$('#gplus-logged-in-row').hasClass('hide')).toBe(false)
|
||||
|
||||
describe 'and the user finishes signup', ->
|
||||
beforeEach ->
|
||||
modal.$('form').submit()
|
||||
|
||||
it 'creates the user with the gplus attributes', ->
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
expect(request.method).toBe('PUT')
|
||||
expect(request.url).toBe('/db/user?gplusID=abcd&gplusAccessToken=1234')
|
||||
expect(_.string.startsWith(request.url, '/db/user')).toBe(true)
|
||||
expect(modal.$('#signup-button').is(':disabled')).toBe(true)
|
||||
|
||||
|
||||
describe 'clicking the facebook button', ->
|
||||
|
||||
signupButton = null
|
||||
|
||||
beforeEach initModal()
|
||||
basicInfoView = null
|
||||
|
||||
beforeEach ->
|
||||
forms.objectToForm(modal.$el, { birthdayDay: 24, birthdayMonth: 7, birthdayYear: 1988 })
|
||||
signupButton = modal.$('#facebook-signup-btn')
|
||||
expect(signupButton.attr('disabled')).toBeFalsy()
|
||||
signupButton.click()
|
||||
modal = new CreateAccountModal()
|
||||
modal.signupState.set({
|
||||
path: 'individual'
|
||||
screen: 'basic-info'
|
||||
})
|
||||
modal.render()
|
||||
jasmine.demoModal(modal)
|
||||
basicInfoView = modal.subviews.basic_info_view
|
||||
|
||||
it 'checks to see if the user already exists in our system', ->
|
||||
requests = jasmine.Ajax.requests.all()
|
||||
expect(requests.length).toBe(1)
|
||||
expect(signupButton.attr('disabled')).toBeTruthy()
|
||||
it 'checks for name conflicts when the name input changes', ->
|
||||
spyOn(basicInfoView, 'checkName')
|
||||
basicInfoView.$('#username-input').val('test').trigger('change')
|
||||
expect(basicInfoView.checkName).toHaveBeenCalled()
|
||||
|
||||
|
||||
describe 'and finding the given person is already a user', ->
|
||||
describe 'checkEmail()', ->
|
||||
beforeEach ->
|
||||
expect(modal.$('#facebook-account-exists-row').hasClass('hide')).toBe(true)
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith({status: 200, responseText: JSON.stringify({_id: 'existinguser'})})
|
||||
basicInfoView.$('input[name="email"]').val('some@email.com')
|
||||
basicInfoView.checkEmail()
|
||||
|
||||
it 'shows a message saying you are connected with Facebook, with a button for logging in', ->
|
||||
expect(modal.$('#facebook-account-exists-row').hasClass('hide')).toBe(false)
|
||||
loginBtn = modal.$('#facebook-login-btn')
|
||||
expect(loginBtn.attr('disabled')).toBeFalsy()
|
||||
loginBtn.click()
|
||||
expect(loginBtn.attr('disabled')).toBeTruthy()
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
expect(request.method).toBe('POST')
|
||||
expect(request.params).toBe('facebookID=abcd&facebookAccessToken=1234')
|
||||
expect(request.url).toBe('/auth/login-facebook')
|
||||
it 'shows checking', ->
|
||||
expect(basicInfoView.$('[data-i18n="signup.checking"]').length).toBe(1)
|
||||
|
||||
describe 'and the user finishes signup anyway with new info', ->
|
||||
beforeEach ->
|
||||
forms.objectToForm(modal.$el, { email: 'some@email.com', birthdayDay: 24, birthdayMonth: 7, birthdayYear: 1988 })
|
||||
modal.$('form').submit()
|
||||
describe 'if email DOES exist', ->
|
||||
beforeEach (done) ->
|
||||
jasmine.Ajax.requests.mostRecent().respondWith({
|
||||
status: 200
|
||||
responseText: JSON.stringify({exists: true})
|
||||
})
|
||||
_.defer done
|
||||
|
||||
it 'upserts the values to the new user', ->
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
expect(request.method).toBe('PUT')
|
||||
expect(request.url).toBe('/db/user?facebookID=abcd&facebookAccessToken=1234')
|
||||
it 'says an account already exists and encourages to sign in', ->
|
||||
expect(basicInfoView.$('[data-i18n="signup.account_exists"]').length).toBe(1)
|
||||
expect(basicInfoView.$('.login-link[data-i18n="signup.sign_in"]').length).toBe(1)
|
||||
|
||||
describe 'if email DOES NOT exist', ->
|
||||
beforeEach (done) ->
|
||||
jasmine.Ajax.requests.mostRecent().respondWith({
|
||||
status: 200
|
||||
responseText: JSON.stringify({exists: false})
|
||||
})
|
||||
_.defer done
|
||||
|
||||
describe 'and finding the given person is not yet a user', ->
|
||||
it 'says email looks good', ->
|
||||
expect(basicInfoView.$('[data-i18n="signup.email_good"]').length).toBe(1)
|
||||
|
||||
describe 'checkName()', ->
|
||||
beforeEach ->
|
||||
expect(modal.$('#facebook-logged-in-row').hasClass('hide')).toBe(true)
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith({status: 404})
|
||||
basicInfoView.$('input[name="name"]').val('Some Name').trigger('change')
|
||||
basicInfoView.checkName()
|
||||
|
||||
it 'shows a message saying you are connected with Facebook', ->
|
||||
expect(modal.$('#facebook-logged-in-row').hasClass('hide')).toBe(false)
|
||||
it 'shows checking', ->
|
||||
expect(basicInfoView.$('[data-i18n="signup.checking"]').length).toBe(1)
|
||||
|
||||
describe 'and the user finishes signup', ->
|
||||
# does not work in travis since en.coffee is not included. TODO: Figure out workaround
|
||||
# describe 'if name DOES exist', ->
|
||||
# beforeEach (done) ->
|
||||
# jasmine.Ajax.requests.mostRecent().respondWith({
|
||||
# status: 200
|
||||
# responseText: JSON.stringify({conflicts: true, suggestedName: 'test123'})
|
||||
# })
|
||||
# _.defer done
|
||||
#
|
||||
# it 'says name is taken and suggests a different one', ->
|
||||
# expect(basicInfoView.$el.text().indexOf('test123') > -1).toBe(true)
|
||||
|
||||
describe 'if email DOES NOT exist', ->
|
||||
beforeEach (done) ->
|
||||
jasmine.Ajax.requests.mostRecent().respondWith({
|
||||
status: 200
|
||||
responseText: JSON.stringify({conflicts: false})
|
||||
})
|
||||
_.defer done
|
||||
|
||||
it 'says name looks good', ->
|
||||
expect(basicInfoView.$('[data-i18n="signup.name_available"]').length).toBe(1)
|
||||
|
||||
describe 'onSubmitForm()', ->
|
||||
it 'shows required errors for empty fields when on INDIVIDUAL path', ->
|
||||
basicInfoView.$('input').val('')
|
||||
basicInfoView.$('#basic-info-form').submit()
|
||||
expect(basicInfoView.$('.form-group.has-error').length).toBe(3)
|
||||
|
||||
it 'shows required errors for empty fields when on STUDENT path', ->
|
||||
modal.signupState.set('path', 'student')
|
||||
modal.render()
|
||||
basicInfoView.$('#basic-info-form').submit()
|
||||
expect(basicInfoView.$('.form-group.has-error').length).toBe(5) # includes first and last name
|
||||
|
||||
describe 'submit with password', ->
|
||||
beforeEach ->
|
||||
modal.$('form').submit()
|
||||
forms.objectToForm(basicInfoView.$el, {
|
||||
email: 'some@email.com'
|
||||
password: 'password'
|
||||
name: 'A Username'
|
||||
})
|
||||
basicInfoView.$('form').submit()
|
||||
|
||||
it 'creates the user with the facebook attributes', ->
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
expect(request.method).toBe('PUT')
|
||||
expect(_.string.startsWith(request.url, '/db/user')).toBe(true)
|
||||
expect(modal.$('#signup-button').is(':disabled')).toBe(true)
|
||||
expect(request.url).toBe('/db/user?facebookID=abcd&facebookAccessToken=1234')
|
||||
it 'checks for email and name conflicts', ->
|
||||
emailCheck = _.find(jasmine.Ajax.requests.all(), (r) -> _.string.startsWith(r.url, '/auth/email'))
|
||||
nameCheck = _.find(jasmine.Ajax.requests.all(), (r) -> _.string.startsWith(r.url, '/auth/name'))
|
||||
expect(_.all([emailCheck, nameCheck])).toBe(true)
|
||||
|
||||
describe 'a check does not pass', ->
|
||||
beforeEach (done) ->
|
||||
nameCheck = _.find(jasmine.Ajax.requests.all(), (r) -> _.string.startsWith(r.url, '/auth/name'))
|
||||
nameCheck.respondWith({
|
||||
status: 200
|
||||
responseText: JSON.stringify({conflicts: false})
|
||||
})
|
||||
emailCheck = _.find(jasmine.Ajax.requests.all(), (r) -> _.string.startsWith(r.url, '/auth/email'))
|
||||
emailCheck.respondWith({
|
||||
status: 200
|
||||
responseText: JSON.stringify({ exists: true })
|
||||
})
|
||||
_.defer done
|
||||
|
||||
it 're-enables the form and shows which field failed', ->
|
||||
|
||||
describe 'both checks do pass', ->
|
||||
beforeEach (done) ->
|
||||
nameCheck = _.find(jasmine.Ajax.requests.all(), (r) -> _.string.startsWith(r.url, '/auth/name'))
|
||||
nameCheck.respondWith({
|
||||
status: 200
|
||||
responseText: JSON.stringify({conflicts: false})
|
||||
})
|
||||
emailCheck = _.find(jasmine.Ajax.requests.all(), (r) -> _.string.startsWith(r.url, '/auth/email'))
|
||||
emailCheck.respondWith({
|
||||
status: 200
|
||||
responseText: JSON.stringify({ exists: false })
|
||||
})
|
||||
_.defer done
|
||||
|
||||
it 'saves the user', ->
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
expect(_.string.startsWith(request.url, '/db/user')).toBe(true)
|
||||
|
||||
describe 'saving the user FAILS', ->
|
||||
beforeEach (done) ->
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith({
|
||||
status: 422
|
||||
responseText: JSON.stringify({
|
||||
message: 'Some error happened'
|
||||
})
|
||||
})
|
||||
_.defer(done)
|
||||
|
||||
it 'displays the server error', ->
|
||||
expect(basicInfoView.$('.alert-danger').length).toBe(1)
|
||||
|
||||
describe 'saving the user SUCCEEDS', ->
|
||||
beforeEach (done) ->
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith({
|
||||
status: 200
|
||||
responseText: '{}'
|
||||
})
|
||||
_.defer(done)
|
||||
|
||||
it 'signs the user up with the password', ->
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
expect(_.string.endsWith(request.url, 'signup-with-password')).toBe(true)
|
||||
|
||||
describe 'after signup STUDENT', ->
|
||||
beforeEach (done) ->
|
||||
basicInfoView.signupState.set({
|
||||
path: 'student'
|
||||
classCode: 'ABC'
|
||||
classroom: new Classroom()
|
||||
})
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith(responses.signupSuccess)
|
||||
_.defer(done)
|
||||
|
||||
it 'joins the classroom', ->
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
expect(request.url).toBe('/db/classroom/~/members')
|
||||
|
||||
describe 'signing the user up SUCCEEDS', ->
|
||||
beforeEach (done) ->
|
||||
spyOn(basicInfoView, 'finishSignup')
|
||||
request = jasmine.Ajax.requests.mostRecent()
|
||||
request.respondWith(responses.signupSuccess)
|
||||
_.defer(done)
|
||||
|
||||
it 'calls finishSignup()', ->
|
||||
expect(basicInfoView.finishSignup).toHaveBeenCalled()
|
||||
|
||||
describe 'ConfirmationView', ->
|
||||
confirmationView = null
|
||||
|
||||
beforeEach ->
|
||||
modal = new CreateAccountModal()
|
||||
modal.signupState.set('screen', 'confirmation')
|
||||
modal.render()
|
||||
jasmine.demoModal(modal)
|
||||
confirmationView = modal.subviews.confirmation_view
|
||||
|
||||
it '(for demo testing)', ->
|
||||
me.set('name', 'A Sweet New Username')
|
||||
me.set('email', 'some@email.com')
|
||||
confirmationView.signupState.set('ssoUsed', 'gplus')
|
||||
|
||||
describe 'SingleSignOnConfirmView', ->
|
||||
singleSignOnConfirmView = null
|
||||
|
||||
beforeEach ->
|
||||
modal = new CreateAccountModal()
|
||||
modal.signupState.set({
|
||||
screen: 'sso-confirm'
|
||||
email: 'some@email.com'
|
||||
})
|
||||
modal.render()
|
||||
jasmine.demoModal(modal)
|
||||
singleSignOnConfirmView = modal.subviews.single_sign_on_confirm_view
|
||||
|
||||
it '(for demo testing)', ->
|
||||
me.set('name', 'A Sweet New Username')
|
||||
me.set('email', 'some@email.com')
|
||||
singleSignOnConfirmView.signupState.set('ssoUsed', 'facebook')
|
||||
|
||||
describe 'CoppaDenyView', ->
|
||||
coppaDenyView = null
|
||||
|
||||
beforeEach ->
|
||||
modal = new CreateAccountModal()
|
||||
modal.signupState.set({
|
||||
screen: 'coppa-deny'
|
||||
})
|
||||
modal.render()
|
||||
jasmine.demoModal(modal)
|
||||
coppaDenyView = modal.subviews.coppa_deny_view
|
||||
|
||||
it '(for demo testing)', ->
|
||||
|
|