From bd3a77da9fb9ae7a7c0ff46bdbb68e42820eef57 Mon Sep 17 00:00:00 2001 From: Scott Erickson Date: Wed, 9 Mar 2016 14:40:52 -0800 Subject: [PATCH] Implement teacher accounts --- app/core/Router.coffee | 15 +- app/locale/en.coffee | 24 +- app/models/User.coffee | 4 +- app/styles/style-flat.sass | 12 + .../{ => teachers}/request-quote-view.sass | 29 +- app/templates/base-flat.jade | 15 +- app/templates/base.jade | 17 +- app/templates/courses/courses-view.jade | 2 +- .../courses/purchase-courses-view.jade | 9 +- app/templates/editor/modal/confirm-modal.jade | 4 +- app/templates/new-home-view.jade | 2 +- app/templates/request-quote-view.jade | 154 ---------- .../convert-to-teacher-account-view.jade | 152 ++++++++++ .../teachers/create-teacher-account-view.jade | 172 ++++++++++++ .../teachers/request-quote-view.jade | 219 +++++++++++++++ app/views/RequestQuoteView.coffee | 135 --------- app/views/courses/PurchaseCoursesView.coffee | 4 +- app/views/courses/TeacherCoursesView.coffee | 3 +- .../ConvertToTeacherAccountView.coffee | 138 +++++++++ .../teachers/CreateTeacherAccountView.coffee | 264 ++++++++++++++++++ app/views/teachers/RequestQuoteView.coffee | 260 +++++++++++++++++ .../2016-03-18-init-school-roles.js | 25 ++ server/classrooms/classroom_handler.coffee | 12 +- server/middleware/trial-requests.coffee | 12 +- server/users/User.coffee | 10 + server/users/user_handler.coffee | 3 + spec/server/functional/classrooms.spec.coffee | 196 ++++++++----- .../functional/trial_request.spec.coffee | 25 +- spec/server/functional/user.spec.coffee | 26 ++ .../ConvertToTeacherAccountView.spec.coffee | 150 ++++++++++ .../CreateTeacherAccountView.spec.coffee | 257 +++++++++++++++++ .../teachers/RequestQuoteView.spec.coffee | 210 ++++++++++++++ 32 files changed, 2149 insertions(+), 411 deletions(-) rename app/styles/{ => teachers}/request-quote-view.sass (56%) delete mode 100644 app/templates/request-quote-view.jade create mode 100644 app/templates/teachers/convert-to-teacher-account-view.jade create mode 100644 app/templates/teachers/create-teacher-account-view.jade create mode 100644 app/templates/teachers/request-quote-view.jade delete mode 100644 app/views/RequestQuoteView.coffee create mode 100644 app/views/teachers/ConvertToTeacherAccountView.coffee create mode 100644 app/views/teachers/CreateTeacherAccountView.coffee create mode 100644 app/views/teachers/RequestQuoteView.coffee create mode 100644 scripts/mongodb/migrations/2016-03-18-init-school-roles.js create mode 100644 test/app/views/teachers/ConvertToTeacherAccountView.spec.coffee create mode 100644 test/app/views/teachers/CreateTeacherAccountView.spec.coffee create mode 100644 test/app/views/teachers/RequestQuoteView.spec.coffee diff --git a/app/core/Router.coffee b/app/core/Router.coffee index 11a6ce09d..624cdc67f 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -123,9 +123,15 @@ module.exports = class CocoRouter extends Backbone.Router 'schools': go('NewHomeView') 'teachers': go('NewHomeView') - 'teachers/freetrial': go('RequestQuoteView') - 'teachers/quote': go('RequestQuoteView') - 'teachers/demo': go('RequestQuoteView') + 'teachers/demo': go('teachers/RequestQuoteView') + 'teachers/freetrial': go('teachers/RequestQuoteView') + 'teachers/quote': go('teachers/RequestQuoteView') + 'teachers/signup': -> + return @routeDirectly('teachers/CreateTeacherAccountView', []) if me.isAnonymous() + @navigate('/teachers/convert', {trigger: true, replace: true}) + 'teachers/convert': -> + return @navigate('/teachers/signup', {trigger: true, replace: true}) if me.isAnonymous() + @routeDirectly('teachers/ConvertToTeacherAccountView', []) 'test(/*subpath)': go('TestView') @@ -228,3 +234,6 @@ module.exports = class CocoRouter extends Backbone.Router navigate: (fragment, options) -> super fragment, options Backbone.Mediator.publish 'router:navigated', route: fragment + + reload: -> + document.location.reload() \ No newline at end of file diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 1d9e86963..bce888537 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -295,6 +295,7 @@ subject: "Subject" email: "Email" password: "Password" + confirm_password: "Confirm Password" message: "Message" code: "Code" ladder: "Ladder" @@ -313,6 +314,9 @@ warrior: "Warrior" ranger: "Ranger" wizard: "Wizard" + first_name: "First Name" + last_name: "Last Name" + username: "Username" units: second: "second" @@ -766,6 +770,7 @@ phone_number_help: "Where can we reach you during the workday?" role_label: "Your role" role_help: "Select your primary role." + role_default: "Select Role" tech_coordinator: "Technology coordinator" advisor: "Advisor" principal: "Principal" @@ -776,6 +781,7 @@ state: "State" country: "Country" num_students_help: "How many do you anticipate enrolling in CodeCombat?" + num_students_default: "Select Range" education_level_label: "Education Level of Students" education_level_help: "Choose as many as apply." elementary_school: "Elementary School" @@ -784,10 +790,18 @@ middle_school: "Middle School" college_plus: "College or higher" anything_else: "Anything else we should know?" - thanks_header: "Thanks for requesting a demo!" - thanks_p: "We'll be in touch soon. Questions? Email us:" - thanks_anon: "Log in or create an account to set up a class, add your students, and monitor their progress as they learn computer science." - thanks_logged_in: "Set up a class, add your students, and monitor their progress as they learn computer science." + thanks_header: "Request Received!" # {change} + thanks_sub_header: "Thanks for expressing interest in CodeCombat for your school." + thanks_p: "We'll be in touch soon! If you need to get in contact, you can reach us at:" # {change} + finish_signup: "Finish creating your teacher account:" + finish_signup_p: "Create an account to set up a class, add your students, and monitor their progress as they learn computer science." + signup_with: "Sign up with:" + conversion_warning: "WARNING: Your current account is a Student Account. Once you submit this form, your account will be converted into a Teacher Account." + learn_more_modal: "Teacher accounts on CodeCombat have the ability to monitor student progress, assign enrollments and manage classrooms. Teacher accounts cannot be a part of a classroom - if you are currently enrolled in a class using this account, you will no longer be able to access it once you convert to a Teacher Account." + create_account: "Create a Teacher Account" + create_account_subtitle: "Get access to teacher-only tools for using CodeCombat in the classroom. Set up a class, add your students, and monitor their progress!" + convert_account_title: "Convert to Teacher Account" + not: "Not" setup_a_class: "Set Up a Class" versions: @@ -1674,4 +1688,4 @@ one_month_coupon: "coupon: choose either Rails or HTML" one_month_discount: "discount, 30% off: choose either Rails or HTML" license: "license" - oreilly: "ebook of your choice" + oreilly: "ebook of your choice" \ No newline at end of file diff --git a/app/models/User.coffee b/app/models/User.coffee index 9514092c2..dfcbd89e3 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -59,8 +59,10 @@ module.exports = class User extends CocoModel isEmailSubscriptionEnabled: (name) -> (@get('emails') or {})[name]?.enabled + isStudent: -> @get('role') is 'student' + isTeacher: -> - return @get('role') in ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent'] + return @get('role') in ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent', 'parent'] setRole: (role, force=false) -> return if me.isAdmin() diff --git a/app/styles/style-flat.sass b/app/styles/style-flat.sass index 3bf3c5955..b1fbe47ea 100644 --- a/app/styles/style-flat.sass +++ b/app/styles/style-flat.sass @@ -215,6 +215,18 @@ $forest: #20572B .btn-lg font-size: 18px + .btn-gplus + color: white + background-color: #DD4B39 + img + height: 22px + + .btn-facebook + color: white + background-color: #3B5998 + img + height: 22px + // Classes .text-navy diff --git a/app/styles/request-quote-view.sass b/app/styles/teachers/request-quote-view.sass similarity index 56% rename from app/styles/request-quote-view.sass rename to app/styles/teachers/request-quote-view.sass index 9749a4752..f00405168 100644 --- a/app/styles/request-quote-view.sass +++ b/app/styles/teachers/request-quote-view.sass @@ -1,12 +1,15 @@ +@import "app/styles/mixins" +@import "app/styles/bootstrap/variables" + #request-quote-view #site-content-area - //TODO: Maybe this should go in style-flat - margin: 50px 10px 100px + margin: 50px 0 100px + .row + margin: 20px 0 + + #conversion-warning + margin-top: 20px - .section - margin-top: 80px - margin-bottom: 50px - .form-group label margin-bottom: 0 @@ -34,5 +37,15 @@ #submit-request-btn margin-left: 10px - #login-btn - margin-right: 10px \ No newline at end of file + // After submit (anonymous) + + h5 + margin-top: 50px + + #social-network-signups + margin: 20px 0 + button + margin-left: 10px + + .text-h1 + margin: 40px 0 30px diff --git a/app/templates/base-flat.jade b/app/templates/base-flat.jade index 1ca69b21b..cc5519bae 100644 --- a/app/templates/base-flat.jade +++ b/app/templates/base-flat.jade @@ -58,12 +58,15 @@ a(href="/user/#{me.getSlugOrID()}", data-i18n="nav.profile") li a(href="/account/settings", data-i18n="play.settings") - li - a(href="/account/payments", data-i18n="account.payments") - li - a(href="/account/subscription", data-i18n="account.subscription") - li - a(href="/account/prepaid", data-i18n="account.prepaid_codes") + unless me.isStudent() + li + a(href="/account/payments", data-i18n="account.payments") + unless me.isTeacher() || me.isStudent() + li + a(href="/account/subscription", data-i18n="account.subscription") + unless me.isStudent() + li + a(href="/account/prepaid", data-i18n="account.prepaid_codes") li a#logout-button(data-i18n="login.log_out") diff --git a/app/templates/base.jade b/app/templates/base.jade index fc1c31127..cd4780d7f 100644 --- a/app/templates/base.jade +++ b/app/templates/base.jade @@ -29,15 +29,18 @@ block header div.img-circle(style="background-image: url(#{me.getPhotoURL()})") h3=me.displayName() li - a(href="/user/#{me.getSlugOrID()}" data-i18n="nav.profile") + a(href="/user/#{me.getSlugOrID()}", data-i18n="nav.profile") li a(href="/account/settings", data-i18n="play.settings") - li - a(href="/account/payments", data-i18n="account.payments") - li - a(href="/account/subscription", data-i18n="account.subscription") - li - a(href="/account/prepaid", data-i18n="account.prepaid_codes") + unless me.isStudent() + li + a(href="/account/payments", data-i18n="account.payments") + unless me.isTeacher() || me.isStudent() + li + a(href="/account/subscription", data-i18n="account.subscription") + unless me.isStudent() + li + a(href="/account/prepaid", data-i18n="account.prepaid_codes") li a#logout-button(data-i18n="login.log_out") diff --git a/app/templates/courses/courses-view.jade b/app/templates/courses/courses-view.jade index aeff6c8bb..f564a5ac5 100644 --- a/app/templates/courses/courses-view.jade +++ b/app/templates/courses/courses-view.jade @@ -3,7 +3,7 @@ extends /templates/base block content h3.text-right if me.isAnonymous() - a(href="/teachers") + a(href="/teachers/signup") span(data-i18n="courses.teachers_click") span ! else diff --git a/app/templates/courses/purchase-courses-view.jade b/app/templates/courses/purchase-courses-view.jade index 1ee184029..214f9846e 100644 --- a/app/templates/courses/purchase-courses-view.jade +++ b/app/templates/courses/purchase-courses-view.jade @@ -88,4 +88,11 @@ block content strong Invalid number of students p.text-center - button#purchase-btn.btn.btn-lg.btn-success.uppercase(data-i18n="courses.purchase_now") + button#purchase-btn.btn.btn-lg.btn-success.uppercase(data-i18n="courses.purchase_now" disabled=me.isAnonymous()) + + if me.isAnonymous() + // DNT. Temporary redirect until teacher-dashboard is finished + .alert.alert-danger.text-center + h2 You must be signed up to purchase enrollments. + p + a.btn.btn-primary.btn-lg(href="/teachers/signup") Create a teacher account diff --git a/app/templates/editor/modal/confirm-modal.jade b/app/templates/editor/modal/confirm-modal.jade index 58f016311..c397a87c2 100644 --- a/app/templates/editor/modal/confirm-modal.jade +++ b/app/templates/editor/modal/confirm-modal.jade @@ -1,10 +1,10 @@ -extends /templates/core/modal-base +extends /templates/core/modal-base-flat block modal-header-content h3= view.title block modal-body-content - p= view.body + p!= view.body block modal-footer-content button.btn.btn-secondary#decline-button(type="button", data-dismiss="modal")= view.decline diff --git a/app/templates/new-home-view.jade b/app/templates/new-home-view.jade index 0f5706230..ca333e175 100644 --- a/app/templates/new-home-view.jade +++ b/app/templates/new-home-view.jade @@ -83,7 +83,7 @@ mixin box a.btn.btn-primary.btn-lg.btn-block(href="https://sites.google.com/a/codecombat.com/teacher-guides/course-guides", data-i18n="new_home.educator_wiki") else h6(data-i18n="new_home.want_coco") - button.teacher-btn.btn.btn-primary.btn-lg.btn-block(data-i18n="new_home.get_started") + a.btn.btn-primary.btn-lg.btn-block(href="/teachers/convert", data-i18n="new_home.get_started") else if view.justPlaysCourses() div diff --git a/app/templates/request-quote-view.jade b/app/templates/request-quote-view.jade deleted file mode 100644 index 10761b4c0..000000000 --- a/app/templates/request-quote-view.jade +++ /dev/null @@ -1,154 +0,0 @@ -extends /templates/base-flat - -block content - .container - form.form(class=view.trialRequest.isNew() ? '' : 'hide') - h3.text-center(data-i18n="teachers_quote.title") - h4.text-center(data-i18n="[html]teachers_quote.subtitle") - - #form-teacher-info.section - .row - .col-sm-offset-2.col-sm-4 - .form-group - label.control-label(data-i18n="general.name") - - var name = me.get('name') || ''; - input.form-control(name="name" value=name, disabled=!!name) - - .col-sm-4 - #email-form-group.form-group - label.control-label(data-i18n="general.email") - - var email = me.get('email') || ''; - input.form-control(name="email" type="email", value=email, disabled=!!email) - - - .row - .col-sm-offset-2.col-sm-4 - .form-group - label.control-label - span(data-i18n="teachers_quote.phone_number") - span.spl.text-muted(data-i18n="signup.optional") - .help-block.small - em.text-info(data-i18n="teachers_quote.phone_number_help") - input.form-control(name="phoneNumber") - - .col-sm-4 - .form-group - label.control-label(data-i18n="teachers_quote.role_label") - .help-block.small - em.text-info(data-i18n="teachers_quote.role_help") - select.form-control(name="role") - option - option(data-i18n="courses.teacher", value="Teacher") - option(data-i18n="teachers_quote.tech_coordinator", value="Technology coordinator") - option(data-i18n="teachers_quote.advisor", value="Advisor") - option(data-i18n="teachers_quote.principal", value="Principal") - option(data-i18n="teachers_quote.superintendent", value="Superintendent") - option(data-i18n="teachers_quote.parent", value="Parent") - - #form-school-info.section - .row - .col-sm-offset-2.col-sm-4 - .form-group - label.control-label(data-i18n="teachers_quote.organization_label") - input.form-control(name="organization") - - .col-sm-4 - .form-group - label.control-label(data-i18n="teachers_quote.city") - input.form-control(name="city") - - .row - .col-sm-offset-2.col-sm-4 - .form-group - label.control-label(data-i18n="teachers_quote.state") - input.form-control(name="state") - - .col-sm-4 - .form-group - label.control-labellabel.control-label(data-i18n="teachers_quote.country") - input.form-control(name="country") - - #form-students-info.section - .row - .col-sm-offset-2.col-sm-5 - .form-group - label.control-label(data-i18n="courses.number_students") - .help-block.small - em.text-info(data-i18n="teachers_quote.num_students_help") - select.form-control(name="numStudents") - option - option 1-10 - option 11-50 - option 51-100 - option 101-200 - option 201-500 - option 501-1000 - option 1000+ - - .form-group - - .row - .col-sm-offset-2.col-sm-4 - label.control-label(data-i18n="teachers_quote.education_level_label") - .help-block.small - em.text-info(data-i18n="teachers_quote.education_level_help") - - .row - .col-sm-offset-2.col-sm-2 - label.control-label.checkbox - input(type="checkbox" name="educationLevel" value="Elementary") - span(data-i18n="teachers_quote.elementary_school") - .col-sm-2 - label.control-label.checkbox - input(type="checkbox" name="educationLevel" value="High") - span(data-i18n="teachers_quote.high_school") - .col-sm-2 - label.control-label.checkbox - input(type="checkbox" name="educationLevel" value="Middle") - span(data-i18n="teachers_quote.middle_school") - .col-sm-2 - label.control-label.checkbox - input(type="checkbox" name="educationLevel" value="College+") - span(data-i18n="teachers_quote.college_plus") - - .row - .col-sm-offset-2.col-sm-6 - // Other field uses custom logic, so no name field is included in either input. - // That way the forms library ignores it. - .form-group - label.control-label.checkbox - input#other-education-level-checkbox(type="checkbox") - span(data-i18n="nav.other") - | - span(data-i18n="teachers_quote.please_explain") - input#other-education-level-input.form-control - - #anything-else-row.section - .row - .col-sm-offset-2.col-sm-8 - label.control-label - span(data-i18n="teachers_quote.anything_else") - span.spl.text-muted(data-i18n="signup.optional") - - textarea.form-control(rows=8, name="notes") - - #buttons-row.row.text-center - input#submit-request-btn.btn.btn-lg.btn-primary(type="submit" data-i18n="[value]teachers_quote.title") - - - #form-submit-success.text-center(class=view.trialRequest.isNew() ? 'hide' : '') - h3.text-center(data-i18n="teachers_quote.thanks_header") - p.text-center - span.spr(data-i18n="teachers_quote.thanks_p") - a.spl(href="mailto:team@codecombat.com") team@codecombat.com - - if me.isAnonymous() - p.text-center(data-i18n="teachers_quote.thanks_anon") - - p.text-center - button#login-btn.btn.btn-info(data-i18n="login.log_in") - button#signup-btn.btn.btn-info(data-i18n="login.sign_up") - else - p.text-center(data-i18n="teachers_quote.thanks_logged_in") - div - a.btn.btn-primary.btn-lg(href="/courses/teachers", data-i18n="teachers_quote.setup_a_class") diff --git a/app/templates/teachers/convert-to-teacher-account-view.jade b/app/templates/teachers/convert-to-teacher-account-view.jade new file mode 100644 index 000000000..1a24be1ea --- /dev/null +++ b/app/templates/teachers/convert-to-teacher-account-view.jade @@ -0,0 +1,152 @@ +extends /templates/base-flat +block content + + #learn-more-modal.modal.fade + .modal-dialog.modal-sm + .modal-content.style-flat + .modal-header + .button.close(type="button", data-dismiss="modal", aria-hidden="true") × + .modal-body(data-i18n="teachers_quote.learn_more_modal") + + .container + form + .row.text-center + .col-md-offset-2.col-md-8 + h3.m-t-3(data-i18n="teachers_quote.convert_account_title") + h4(data-i18n="[html]teachers_quote.create_account_subtitle") + .alert.alert-info.m-y-2 + div + span.spr(data-i18n="teachers_quote.not") + strong= me.broadName() + | ? + a.spl#logout-link(data-i18n="login.log_out") + if me.get('role') === 'student' + div#conversion-warning.m-t-2 + span.spr(data-i18n="[html]teachers_quote.conversion_warning") + a(data-i18n="new_home.learn_more" data-toggle="modal" data-target="#learn-more-modal") + + .row.m-y-2 + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="general.username") + input.form-control(disabled=true value=me.get('name')) + + .col-md-4.col-sm-6 + #email-form-group.form-group + label.control-label(data-i18n="general.email") + input.form-control(disabled=true value=me.get('email')) + + .row.m-y-2 + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="general.first_name") + input.form-control(name="firstName" value=me.get('firstName') || '') + + .col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="general.last_name") + input.form-control(name="lastName" value=me.get('lastName') || '') + + .row.m-y-2 + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label + span(data-i18n="teachers_quote.phone_number") + span.spl.text-muted(data-i18n="signup.optional") + .help-block.small + em.text-info(data-i18n="teachers_quote.phone_number_help") + input.form-control(name="phoneNumber") + + .col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="teachers_quote.role_label") + .help-block.small + em.text-info(data-i18n="teachers_quote.role_help") + select.form-control(name="role") + option(data-i18n="teachers_quote.role_default", , value='') + option(data-i18n="courses.teacher", value="Teacher") + option(data-i18n="teachers_quote.tech_coordinator", value="Technology coordinator") + option(data-i18n="teachers_quote.advisor", value="Advisor") + option(data-i18n="teachers_quote.principal", value="Principal") + option(data-i18n="teachers_quote.superintendent", value="Superintendent") + option(data-i18n="teachers_quote.parent", value="Parent") + + #form-school-info + .row.m-y-2 + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="teachers_quote.organization_label") + input.form-control(name="organization") + + .col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="teachers_quote.city") + input.form-control(name="city") + + .row.m-y-2 + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="teachers_quote.state") + input.form-control(name="state") + + .col-md-4.col-sm-6 + .form-group + label.control-labellabel.control-label(data-i18n="teachers_quote.country") + input.form-control(name="country") + + #form-students-info + .row.m-y-2 + .col-md-offset-2.col-md-4 + .form-group + label.control-label(data-i18n="courses.number_students") + .help-block.small + em.text-info(data-i18n="teachers_quote.num_students_help") + select.form-control(name="numStudents") + option(data-i18n="teachers_quote.num_students_default", value='') + option 1-10 + option 11-50 + option 51-100 + option 101-200 + option 201-500 + option 501-1000 + option 1000+ + + .form-group + .row.m-y-2 + .col-md-offset-2.col-md-10 + label.control-label(data-i18n="teachers_quote.education_level_label") + .help-block.small + em.text-info(data-i18n="teachers_quote.education_level_help") + .col-md-offset-2.col-md-5 + .checkbox + label + input(type="checkbox" name="educationLevel" value="Elementary") + span(data-i18n="teachers_quote.elementary_school") + .checkbox + label + input(type="checkbox" name="educationLevel" value="High") + span(data-i18n="teachers_quote.high_school") + .checkbox + label + input(type="checkbox" name="educationLevel" value="Middle") + span(data-i18n="teachers_quote.middle_school") + .checkbox + label + input(type="checkbox" name="educationLevel" value="College+") + span(data-i18n="teachers_quote.college_plus") + .checkbox + label + input#other-education-level-checkbox(type="checkbox") + span(data-i18n="nav.other").spr + span(data-i18n="teachers_quote.please_explain") + input#other-education-level-input.form-control + + #anything-else-row.row.m-y-2 + .col-md-offset-2.col-md-8 + label.control-label + span(data-i18n="teachers_quote.anything_else") + span.spl.text-muted(data-i18n="signup.optional") + textarea.form-control(rows=8, name="notes") + #buttons-row.row.m-y-2.text-center + input#create-account-btn.btn.btn-lg.btn-primary(type="submit" data-i18n="[value]teachers_quote.convert_account_title") + diff --git a/app/templates/teachers/create-teacher-account-view.jade b/app/templates/teachers/create-teacher-account-view.jade new file mode 100644 index 000000000..6bc0e7b28 --- /dev/null +++ b/app/templates/teachers/create-teacher-account-view.jade @@ -0,0 +1,172 @@ +extends /templates/base-flat + +block content + .container + form + .row.text-center + .col-md-offset-2.col-md-8 + h3.m-t-3(data-i18n="teachers_quote.create_account") + h4(data-i18n="[html]teachers_quote.create_account_subtitle") + .alert.alert-info.m-y-2 + span.spr(data-i18n="signup.login_switch") + a.login-link(data-i18n="login.log_in") + #social-network-signups.m-y-2 + button#facebook-signup-btn.btn.btn-facebook.btn-lg.m-x-1 + span.spr(data-i18n="teachers_quote.signup_with") + | Facebook + img.m-l-1(src='/images/pages/community/logo_facebook.png') + + button#gplus-signup-btn.btn.btn-gplus.btn-lg.spr + span.spr(data-i18n="teachers_quote.signup_with") + | G+ + img.m-l-1(src='/images/pages/community/logo_g+.png') + + #gplus-logged-in-row.row.text-center.hide + .col-md-offset-2.col-md-8 + h2(data-i18n="signup.connected_gplus_header") + p(data-i18n="signup.connected_gplus_p") + #facebook-logged-in-row.row.text-center.hide + .col-md-offset-2.col-md-8 + h2(data-i18n="signup.connected_facebook_header") + p(data-i18n="signup.connected_facebook_p") + + + .row.m-y-2 + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="general.username") + input.form-control(name="name") + + .col-md-4.col-sm-6 + #email-form-group.form-group + label.control-label(data-i18n="general.email") + input.form-control(name="email") + + .row.m-y-2 + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="general.first_name") + input.form-control(name="firstName") + + .col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="general.last_name") + input.form-control(name="lastName") + + .row.m-y-2 + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="general.password") + input.form-control(name="password1", type="password") + + .col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="general.confirm_password") + input.form-control(name="password2", type="password") + + .row.m-y-2 + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label + span(data-i18n="teachers_quote.phone_number") + span.spl.text-muted(data-i18n="signup.optional") + .help-block.small + em.text-info(data-i18n="teachers_quote.phone_number_help") + input.form-control(name="phoneNumber") + + .col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="teachers_quote.role_label") + .help-block.small + em.text-info(data-i18n="teachers_quote.role_help") + select.form-control(name="role") + option(data-i18n="teachers_quote.role_default", , value='') + option(data-i18n="courses.teacher", value="Teacher") + option(data-i18n="teachers_quote.tech_coordinator", value="Technology coordinator") + option(data-i18n="teachers_quote.advisor", value="Advisor") + option(data-i18n="teachers_quote.principal", value="Principal") + option(data-i18n="teachers_quote.superintendent", value="Superintendent") + option(data-i18n="teachers_quote.parent", value="Parent") + + #form-school-info + .row.m-y-2 + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="teachers_quote.organization_label") + input.form-control(name="organization") + + .col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="teachers_quote.city") + input.form-control(name="city") + + .row.m-y-2 + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="teachers_quote.state") + input.form-control(name="state") + + .col-md-4.col-sm-6 + .form-group + label.control-labellabel.control-label(data-i18n="teachers_quote.country") + input.form-control(name="country") + + #form-students-info + .row.m-y-2 + .col-md-offset-2.col-md-4 + .form-group + label.control-label(data-i18n="courses.number_students") + .help-block.small + em.text-info(data-i18n="teachers_quote.num_students_help") + select.form-control(name="numStudents") + option(data-i18n="teachers_quote.num_students_default", value='') + option 1-10 + option 11-50 + option 51-100 + option 101-200 + option 201-500 + option 501-1000 + option 1000+ + + .form-group + + .row.m-y-2 + .col-md-offset-2.col-md-10 + label.control-label(data-i18n="teachers_quote.education_level_label") + .help-block.small + em.text-info(data-i18n="teachers_quote.education_level_help") + + .col-md-offset-2.col-md-5 + .checkbox + label + input(type="checkbox" name="educationLevel" value="Elementary") + span(data-i18n="teachers_quote.elementary_school") + .checkbox + label + input(type="checkbox" name="educationLevel" value="High") + span(data-i18n="teachers_quote.high_school") + .checkbox + label + input(type="checkbox" name="educationLevel" value="Middle") + span(data-i18n="teachers_quote.middle_school") + .checkbox + label + input(type="checkbox" name="educationLevel" value="College+") + span(data-i18n="teachers_quote.college_plus") + .checkbox + label + input#other-education-level-checkbox(type="checkbox") + span(data-i18n="nav.other").spr + span(data-i18n="teachers_quote.please_explain") + input#other-education-level-input.form-control + + #anything-else-row.row.m-y-2 + .col-md-offset-2.col-md-8 + label.control-label + span(data-i18n="teachers_quote.anything_else") + span.spl.text-muted(data-i18n="signup.optional") + + textarea.form-control(rows=8, name="notes") + + #buttons-row.row.m-y-2.text-center + input#create-account-btn.btn.btn-lg.btn-primary(type="submit" data-i18n="[value]teachers_quote.create_account") diff --git a/app/templates/teachers/request-quote-view.jade b/app/templates/teachers/request-quote-view.jade new file mode 100644 index 000000000..5385fa24a --- /dev/null +++ b/app/templates/teachers/request-quote-view.jade @@ -0,0 +1,219 @@ +extends /templates/base-flat + +block content + - var showDone = !view.trialRequest.isNew() && me.isAnonymous(); + + #learn-more-modal.modal.fade + .modal-dialog.modal-sm + .modal-content.style-flat + .modal-header + .button.close(type="button", data-dismiss="modal", aria-hidden="true") × + .modal-body(data-i18n="teachers_quote.learn_more_modal") + + .container + form#request-form(class=showDone ? 'hide' : '') + .row + .col-md-offset-2.col-md-8 + h3.text-center(data-i18n="teachers_quote.title") + h4.text-center(data-i18n="[html]teachers_quote.subtitle") + + if !me.isAnonymous() + .row + .col-md-offset-2.col-md-8 + .alert.alert-info.text-center + div + span.spr(data-i18n="teachers_quote.not") + strong= me.broadName() + | ? + a.spl#logout-link(data-i18n="login.log_out") + if me.get('role') === 'student' + div#conversion-warning + span.spr(data-i18n="[html]teachers_quote.conversion_warning") + a(data-i18n="new_home.learn_more" data-toggle="modal" data-target="#learn-more-modal") + + #form-teacher-info + if !me.isAnonymous() + .row + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="general.username") + - var name = me.get('name') || ''; + input.form-control(name="name" value=name, disabled=!!name) + + .col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="general.email") + - var email = me.get('email') || ''; + input.form-control(name="email" value=email, disabled=!!email) + + .row + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="general.first_name") + - var firstName = me.get('firstName') || ''; + input.form-control(name="firstName" value=firstName, disabled=!!firstName) + + .col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="general.last_name") + - var lastName = me.get('lastName') || ''; + input.form-control(name="lastName" value=lastName, disabled=!!lastName) + + if me.isAnonymous() + .row + .col-md-offset-2.col-md-4.col-sm-6 + #email-form-group.form-group + label.control-label(data-i18n="general.email") + - var email = me.get('email') || ''; + input.form-control(name="email" type="email", value=email, disabled=!!email) + + .row + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label + span(data-i18n="teachers_quote.phone_number") + span.spl.text-muted(data-i18n="signup.optional") + .help-block.small + em.text-info(data-i18n="teachers_quote.phone_number_help") + input.form-control(name="phoneNumber") + + .col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="teachers_quote.role_label") + .help-block.small + em.text-info(data-i18n="teachers_quote.role_help") + select.form-control(name="role") + option(data-i18n="teachers_quote.role_default", , value='') + option(data-i18n="courses.teacher", value="Teacher") + option(data-i18n="teachers_quote.tech_coordinator", value="Technology coordinator") + option(data-i18n="teachers_quote.advisor", value="Advisor") + option(data-i18n="teachers_quote.principal", value="Principal") + option(data-i18n="teachers_quote.superintendent", value="Superintendent") + option(data-i18n="teachers_quote.parent", value="Parent") + + #form-school-info + .row + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="teachers_quote.organization_label") + input.form-control(name="organization") + + .col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="teachers_quote.city") + input.form-control(name="city") + + .row + .col-md-offset-2.col-md-4.col-sm-6 + .form-group + label.control-label(data-i18n="teachers_quote.state") + input.form-control(name="state") + + .col-md-4.col-sm-6 + .form-group + label.control-labellabel.control-label(data-i18n="teachers_quote.country") + input.form-control(name="country") + + #form-students-info + .row + .col-md-offset-2.col-md-4 + .form-group + label.control-label(data-i18n="courses.number_students") + .help-block.small + em.text-info(data-i18n="teachers_quote.num_students_help") + select.form-control(name="numStudents") + option(data-i18n="teachers_quote.num_students_default", value='') + option 1-10 + option 11-50 + option 51-100 + option 101-200 + option 201-500 + option 501-1000 + option 1000+ + + .form-group + + .row + .col-md-offset-2.col-md-10 + label.control-label(data-i18n="teachers_quote.education_level_label") + .help-block.small + em.text-info(data-i18n="teachers_quote.education_level_help") + + .col-md-offset-2.col-md-5 + .checkbox + label + input(type="checkbox" name="educationLevel" value="Elementary") + span(data-i18n="teachers_quote.elementary_school") + .checkbox + label + input(type="checkbox" name="educationLevel" value="High") + span(data-i18n="teachers_quote.high_school") + .checkbox + label + input(type="checkbox" name="educationLevel" value="Middle") + span(data-i18n="teachers_quote.middle_school") + .checkbox + label + input(type="checkbox" name="educationLevel" value="College+") + span(data-i18n="teachers_quote.college_plus") + .checkbox + label + input#other-education-level-checkbox(type="checkbox") + span(data-i18n="nav.other").spr + span(data-i18n="teachers_quote.please_explain") + input#other-education-level-input.form-control + + #anything-else-row.row + .col-md-offset-2.col-md-8 + label.control-label + span(data-i18n="teachers_quote.anything_else") + span.spl.text-muted(data-i18n="signup.optional") + + textarea.form-control(rows=8, name="notes") + + #buttons-row.row.text-center + input#submit-request-btn.btn.btn-lg.btn-primary(type="submit" data-i18n="[value]teachers_quote.title") + + #form-submit-success.text-center(class=showDone ? '' : 'hide') + h3(data-i18n="teachers_quote.thanks_header") + h4(data-i18n="teachers_quote.thanks_sub_header") + p + span.spr(data-i18n="teachers_quote.thanks_p") + a.spl(href="mailto:team@codecombat.com") team@codecombat.com + + if me.isAnonymous() + h5(data-i18n="teachers_quote.finish_signup") + p(data-i18n="teachers_quote.finish_signup_p") + + #social-network-signups + span(data-i18n="teachers_quote.signup_with") + button#facebook-signup-btn.btn.btn-facebook.btn-lg.m-x-1 + span.spr(data-i18n="teachers_quote.signup_with") + | Facebook + img.m-l-1(src='/images/pages/community/logo_facebook.png') + button#gplus-signup-btn.btn.btn-gplus.btn-lg.spr + span.spr(data-i18n="teachers_quote.signup_with") + | G+ + img.m-l-1(src='/images/pages/community/logo_g+.png') + + .text-h1.text-uppercase(data-i18n="general.or") + + form#signup-form.text-left + .row + .col-md-offset-2.col-md-4 + .form-group + label.control-label(data-i18n="general.name") + input.form-control(name="name") + + .row + .col-md-offset-2.col-md-4 + .form-group + label.control-label(data-i18n="general.password") + input.form-control(name="password1", type="password") + .col-md-4 + .form-group + label.control-label(data-i18n="general.confirm_password") + input.form-control(name="password2", type="password") + + .text-center + button.btn.btn-lg.btn-navy(data-i18n="login.sign_up") diff --git a/app/views/RequestQuoteView.coffee b/app/views/RequestQuoteView.coffee deleted file mode 100644 index 9542277fd..000000000 --- a/app/views/RequestQuoteView.coffee +++ /dev/null @@ -1,135 +0,0 @@ -RootView = require 'views/core/RootView' -forms = require 'core/forms' -TrialRequest = require 'models/TrialRequest' -TrialRequests = require 'collections/TrialRequests' -AuthModal = require 'views/core/AuthModal' -CreateAccountModal = require 'views/core/CreateAccountModal' -storage = require 'core/storage' - -formSchema = { - type: 'object' - required: ['name', 'email', 'organization', 'role', 'numStudents'] - properties: - name: { type: 'string', minLength: 1 } - email: { type: 'string', format: 'email' } - phoneNumber: { type: 'string' } - role: { type: 'string' } - organization: { type: 'string' } - city: { type: 'string' } - state: { type: 'string' } - country: { type: 'string' } - numStudents: { type: 'string' } - educationLevel: { - type: 'array' - items: { type: 'string' } - } - notes: { type: 'string' } -} - -module.exports = class RequestQuoteView extends RootView - id: 'request-quote-view' - template: require 'templates/request-quote-view' - - events: - 'change form': 'onChangeForm' - 'submit form': 'onSubmitForm' - 'click #login-btn': 'onClickLoginButton' - 'click #signup-btn': 'onClickSignupButton' - 'click #email-exists-login-link': 'onClickEmailExistsLoginLink' - - initialize: -> - @trialRequest = new TrialRequest() - @trialRequests = new TrialRequests() - @trialRequests.fetchOwn() - @supermodel.loadCollection(@trialRequests) - - onLoaded: -> - if @trialRequests.size() - @trialRequest = @trialRequests.first() - if @trialRequest and @trialRequest.get('status') isnt 'submitted' and @trialRequest.get('status') isnt 'approved' - window.tracker?.trackEvent 'View Trial Request', category: 'Teachers', label: 'View Trial Request', ['Mixpanel'] - super() - - afterRender: -> - super() - obj = storage.load('request-quote-form') - if obj - @$('#other-education-level-checkbox').attr('checked', obj.otherChecked) - @$('#other-education-level-input').val(obj.otherInput) - forms.objectToForm(@$('form'), obj) - - onChangeForm: -> - obj = forms.formToObject(@$('form')) - obj.otherChecked = @$('#other-education-level-checkbox').is(':checked') - obj.otherInput = @$('#other-education-level-input').val() - storage.save('request-quote-form', obj, 10) - - onSubmitForm: (e) -> - e.preventDefault() - form = @$('form') - attrs = forms.formToObject(form) - - # custom other input logic (also used in form local storage save/restore) - if @$('#other-education-level-checkbox').is(':checked') - attrs.educationLevel.push(@$('#other-education-level-input').val()) - - forms.clearFormAlerts(form) - result = tv4.validateMultiple(attrs, formSchema) - error = true - if not result.valid - forms.applyErrorsToForm(form, result.errors) - else if not /^.+@.+\..+$/.test(attrs.email) - forms.setErrorToProperty(form, 'email', 'Invalid email.') - else if not _.size(attrs.educationLevel) - return forms.setErrorToProperty(form, 'educationLevel', 'Check at least one.') - else - error = false - if error - forms.scrollToFirstError() - return - @trialRequest = new TrialRequest({ - type: 'course' - properties: attrs - }) - @trialRequest.notyErrors = false - @$('#submit-request-btn').text('Sending').attr('disabled', true) - @trialRequest.save() - @trialRequest.on 'sync', @onTrialRequestSubmit, @ - @trialRequest.on 'error', @onTrialRequestError, @ - me.setRole attrs.role.toLowerCase(), true - - onTrialRequestError: (model, jqxhr) -> - if jqxhr.status is 409 - userExists = $.i18n.t('teachers_quote.email_exists') - logIn = $.i18n.t('login.log_in') - @$('#email-form-group') - .addClass('has-error') - .append($("
#{userExists} #{logIn}")) - @$('#submit-request-btn').text('Submit').attr('disabled', false) - forms.scrollToFirstError() - - onClickEmailExistsLoginLink: -> - modal = new AuthModal({ initialValues: { email: @trialRequest.get('properties')?.email } }) - @openModalView(modal) - - onTrialRequestSubmit: -> - @$('form, #form-submit-success').toggleClass('hide') - $('#flying-focus').css({top: 0, left: 0}) # Hack copied from Router.coffee#187. Ideally we'd swap out the view and have view-swapping logic handle this - window.tracker?.trackEvent 'Submit Trial Request', category: 'Teachers', label: 'Trial Request', ['Mixpanel'] - - onClickLoginButton: -> - modal = new AuthModal({ initialValues: { email: @trialRequest.get('properties')?.email } }) - @openModalView(modal) - window.nextURL = '/courses/teachers' unless @trialRequest.isNew() - - onClickSignupButton: -> - props = @trialRequest.get('properties') or {} - me.set('name', props.name) - modal = new CreateAccountModal({ - initialValues: { - email: props.email - schoolName: props.organization - } - }) - @openModalView(modal) - window.nextURL = '/courses/teachers' unless @trialRequest.isNew() diff --git a/app/views/courses/PurchaseCoursesView.coffee b/app/views/courses/PurchaseCoursesView.coffee index fbaea0a01..9afd4b2fe 100644 --- a/app/views/courses/PurchaseCoursesView.coffee +++ b/app/views/courses/PurchaseCoursesView.coffee @@ -1,5 +1,4 @@ app = require 'core/application' -CreateAccountModal = require 'views/core/CreateAccountModal' Classroom = require 'models/Classroom' CocoCollection = require 'collections/CocoCollection' Course = require 'models/Course' @@ -40,7 +39,6 @@ module.exports = class PurchaseCoursesView extends RootView onLoaded: -> @pricePerStudent = @products.findWhere({name: 'course'}).get('amount') - me.setRole 'teacher' super() getPriceString: -> '$' + (@getPrice()/100).toFixed(2) @@ -77,7 +75,7 @@ module.exports = class PurchaseCoursesView extends RootView numberOfStudentsIsValid: -> @numberOfStudents > 0 and @numberOfStudents < 100000 onClickPurchaseButton: -> - return @openModalView new CreateAccountModal() if me.isAnonymous() + return application.router.navigate('/teachers/signup', {trigger: true}) if me.isAnonymous() unless @numberOfStudentsIsValid() alert("Please enter the maximum number of students needed for your class.") return diff --git a/app/views/courses/TeacherCoursesView.coffee b/app/views/courses/TeacherCoursesView.coffee index 1b80eda0f..0f117b600 100644 --- a/app/views/courses/TeacherCoursesView.coffee +++ b/app/views/courses/TeacherCoursesView.coffee @@ -1,6 +1,5 @@ ActivateLicensesModal = require 'views/courses/ActivateLicensesModal' app = require 'core/application' -CreateAccountModal = require 'views/core/CreateAccountModal' CocoCollection = require 'collections/CocoCollection' CocoModel = require 'models/CocoModel' Course = require 'models/Course' @@ -70,7 +69,7 @@ module.exports = class TeacherCoursesView extends RootView application.tracker?.trackEvent 'Classroom started add students', category: 'Courses', classroomID: classroom.id onClickCreateNewClassButton: -> - return @openModalView new CreateAccountModal() if me.get('anonymous') + return application.router.navigate('/teachers/signup', {trigger: true}) if me.get('anonymous') modal = new ClassroomSettingsModal({}) @openModalView(modal) @listenToOnce modal, 'hide', => diff --git a/app/views/teachers/ConvertToTeacherAccountView.coffee b/app/views/teachers/ConvertToTeacherAccountView.coffee new file mode 100644 index 000000000..f587192df --- /dev/null +++ b/app/views/teachers/ConvertToTeacherAccountView.coffee @@ -0,0 +1,138 @@ +RootView = require 'views/core/RootView' +forms = require 'core/forms' +TrialRequest = require 'models/TrialRequest' +TrialRequests = require 'collections/TrialRequests' +AuthModal = require 'views/core/AuthModal' +storage = require 'core/storage' +errors = require 'core/errors' +User = require 'models/User' +ConfirmModal = require 'views/editor/modal/ConfirmModal' + +FORM_KEY = 'request-quote-form' + +module.exports = class ConvertToTeacherAccountView extends RootView + id: 'convert-to-teacher-account-view' + template: require 'templates/teachers/convert-to-teacher-account-view' + logoutRedirectURL: null + + events: + 'change form': 'onChangeForm' + 'submit form': 'onSubmitForm' + 'click #logout-link': -> me.logout() + + initialize: -> + if me.isAnonymous() + application.router.navigate('/teachers/signup', {trigger: true, replace: true}) + return + @trialRequest = new TrialRequest() + @trialRequests = new TrialRequests() + @trialRequests.fetchOwn() + @supermodel.trackCollection(@trialRequests) + + onLoaded: -> + if @trialRequests.size() and me.isTeacher() + return application.router.navigate('/courses/teachers', { trigger: true, replace: true }) + + super() + + afterRender: -> + super() + + # apply existing trial request on form + properties = @trialRequest.get('properties') + if properties + forms.objectToForm(@$('form'), properties) + commonLevels = _.map @$('[name="educationLevel"]'), (el) -> $(el).val() + submittedLevels = properties.educationLevel or [] + otherLevel = _.first(_.difference(submittedLevels, commonLevels)) or '' + @$('#other-education-level-checkbox').attr('checked', !!otherLevel) + @$('#other-education-level-input').val(otherLevel) + + # apply changes from local storage + obj = storage.load(FORM_KEY) + if obj + @$('#other-education-level-checkbox').attr('checked', obj.otherChecked) + @$('#other-education-level-input').val(obj.otherInput) + forms.objectToForm(@$('form'), obj, { overwriteExisting: true }) + + onChangeRequestForm: -> + # save changes to local storage + obj = forms.formToObject(@$('form')) + obj.otherChecked = @$('#other-education-level-checkbox').is(':checked') + obj.otherInput = @$('#other-education-level-input').val() + storage.save(FORM_KEY, obj, 10) + + onSubmitForm: (e) -> + e.preventDefault() + + form = @$('form') + attrs = forms.formToObject(form) + + if @$('#other-education-level-checkbox').is(':checked') + val = @$('#other-education-level-input').val() + attrs.educationLevel.push(val) if val + + forms.clearFormAlerts(form) + + result = tv4.validateMultiple(attrs, formSchema) + error = false + if not result.valid + forms.applyErrorsToForm(form, result.errors) + error = true + if not _.size(attrs.educationLevel) + forms.setErrorToProperty(form, 'educationLevel', 'Include at least one.') + error = true + if error + forms.scrollToFirstError() + return + @trialRequest = new TrialRequest({ + type: 'course' + properties: attrs + }) + if me.get('role') is 'student' and not me.isAnonymous() + modal = new ConfirmModal({ + title: '' + body: "

#{$.i18n.t('teachers_quote.conversion_warning')}

#{$.i18n.t('teachers_quote.learn_more_modal')}

" + confirm: $.i18n.t('common.continue') + decline: $.i18n.t('common.cancel') + }) + @openModalView(modal) + modal.once 'confirm', @saveTrialRequest, @ + else + @saveTrialRequest() + + saveTrialRequest: -> + @trialRequest.notyErrors = false + @$('#create-account-btn').text('Sending').attr('disabled', true) + @trialRequest.save() + @trialRequest.on 'sync', @onTrialRequestSubmit, @ + @trialRequest.on 'error', @onTrialRequestError, @ + + onTrialRequestError: (model, jqxhr) -> + @$('#submit-request-btn').text('Submit').attr('disabled', false) + errors.showNotyNetworkError(arguments...) + + onTrialRequestSubmit: -> + me.setRole @trialRequest.get('properties').role.toLowerCase(), true + storage.remove(FORM_KEY) + application.router.navigate('/courses/teachers', {trigger: true}) + +formSchema = { + type: 'object' + required: ['firstName', 'lastName', 'organization', 'role', 'numStudents'] + properties: + firstName: { type: 'string' } + lastName: { type: 'string' } + phoneNumber: { type: 'string' } + role: { type: 'string' } + organization: { type: 'string' } + city: { type: 'string' } + state: { type: 'string' } + country: { type: 'string' } + numStudents: { type: 'string' } + educationLevel: { + type: 'array' + items: { type: 'string' } + } + notes: { type: 'string' } +} diff --git a/app/views/teachers/CreateTeacherAccountView.coffee b/app/views/teachers/CreateTeacherAccountView.coffee new file mode 100644 index 000000000..cee2b24aa --- /dev/null +++ b/app/views/teachers/CreateTeacherAccountView.coffee @@ -0,0 +1,264 @@ +RootView = require 'views/core/RootView' +forms = require 'core/forms' +TrialRequest = require 'models/TrialRequest' +TrialRequests = require 'collections/TrialRequests' +AuthModal = require 'views/core/AuthModal' +storage = require 'core/storage' +errors = require 'core/errors' +User = require 'models/User' + +FORM_KEY = 'request-quote-form' +SIGNUP_REDIRECT = '/courses/teachers' + +module.exports = class CreateTeacherAccountView extends RootView + id: 'create-teacher-account-view' + template: require 'templates/teachers/create-teacher-account-view' + + events: + 'click .login-link': 'onClickLoginLink' + 'change form': 'onChangeForm' + 'submit form': 'onSubmitForm' + 'click #gplus-signup-btn': 'onClickGPlusSignupButton' + 'click #facebook-signup-btn': 'onClickFacebookSignupButton' + + initialize: -> + @trialRequest = new TrialRequest() + @trialRequests = new TrialRequests() + @trialRequests.fetchOwn() + @supermodel.trackCollection(@trialRequests) + + onLoaded: -> + if @trialRequests.size() + @trialRequest = @trialRequests.first() + if @trialRequest and @trialRequest.get('status') isnt 'submitted' and @trialRequest.get('status') isnt 'approved' + window.tracker?.trackEvent 'View Trial Request', category: 'Teachers', label: 'View Trial Request', ['Mixpanel'] + super() + + afterRender: -> + super() + + # apply existing trial request on form + properties = @trialRequest.get('properties') + if properties + forms.objectToForm(@$('form'), properties) + commonLevels = _.map @$('[name="educationLevel"]'), (el) -> $(el).val() + submittedLevels = properties.educationLevel or [] + otherLevel = _.first(_.difference(submittedLevels, commonLevels)) or '' + @$('#other-education-level-checkbox').attr('checked', !!otherLevel) + @$('#other-education-level-input').val(otherLevel) + + # apply changes from local storage + obj = storage.load(FORM_KEY) + if obj + @$('#other-education-level-checkbox').attr('checked', obj.otherChecked) + @$('#other-education-level-input').val(obj.otherInput) + forms.objectToForm(@$('form'), obj, { overwriteExisting: true }) + + onClickLoginLink: -> + modal = new AuthModal({ initialValues: { email: @trialRequest.get('properties')?.email } }) + @openModalView(modal) + + onChangeRequestForm: -> + # Local storage is being used to store the contents of the form whenever it changes, + # and filling in the stored values if the page is reloaded or navigated away from and returned to. + # save changes to local storage + obj = forms.formToObject(@$('form')) + obj.otherChecked = @$('#other-education-level-checkbox').is(':checked') + obj.otherInput = @$('#other-education-level-input').val() + storage.save(FORM_KEY, obj, 10) + + onSubmitForm: (e) -> + e.preventDefault() + + # Creating Trial Request first, validate user attributes but do not use them + form = @$('form') + allAttrs = forms.formToObject(form) + trialRequestAttrs = _.omit(allAttrs, 'name', 'password1', 'password2') + + if @$('#other-education-level-checkbox').is(':checked') + val = @$('#other-education-level-input').val() + trialRequestAttrs.educationLevel.push(val) if val + + forms.clearFormAlerts(form) + + result = tv4.validateMultiple(trialRequestAttrs, formSchema) + error = false + if not result.valid + forms.applyErrorsToForm(form, result.errors) + error = true + if not forms.validateEmail(trialRequestAttrs.email) + forms.setErrorToProperty(form, 'email', 'Invalid email.') + error = true + if not _.size(trialRequestAttrs.educationLevel) + forms.setErrorToProperty(form, 'educationLevel', 'Include at least one.') + error = true + unless @gplusAttrs or @facebookAttrs + if not allAttrs.password1 + forms.setErrorToProperty(form, 'password1', 'Required field') + error = true + else if not allAttrs.password2 + forms.setErrorToProperty(form, 'password2', 'Required field') + error = true + else if allAttrs.password1 isnt allAttrs.password2 + forms.setErrorToProperty(form, 'password1', 'Password fields are not equivalent') + error = true + if error + forms.scrollToFirstError() + return + @trialRequest = new TrialRequest({ + type: 'course' + properties: trialRequestAttrs + }) + @trialRequest.notyErrors = false + @$('#create-account-btn').text('Sending').attr('disabled', true) + @trialRequest.save() + @trialRequest.on 'sync', @onTrialRequestSubmit, @ + @trialRequest.on 'error', @onTrialRequestError, @ + + onTrialRequestError: (model, jqxhr) -> + @$('#create-account-btn').text('Submit').attr('disabled', false) + if jqxhr.status is 409 + userExists = $.i18n.t('teachers_quote.email_exists') + logIn = $.i18n.t('login.log_in') + @$('#email-form-group') + .addClass('has-error') + .append($("
#{userExists} ")) + forms.scrollToFirstError() + else + errors.showNotyNetworkError(arguments...) + + onClickEmailExistsLoginLink: -> + modal = new AuthModal({ initialValues: { email: @trialRequest.get('properties')?.email } }) + @openModalView(modal) + + onTrialRequestSubmit: -> + storage.remove(FORM_KEY) + attrs = _.pick(forms.formToObject(@$('form')), 'name', 'email', 'role') + options = {} + newUser = new User(attrs) + if @gplusAttrs + newUser.set('_id', me.id) + options.url = "/db/user?gplusID=#{@gplusAttrs.gplusID}&gplusAccessToken=#{application.gplusHandler.accessToken.access_token}" + options.type = 'PUT' + newUser.set(@gplusAttrs) + else if @facebookAttrs + newUser.set('_id', me.id) + options.url = "/db/user?facebookID=#{@facebookAttrs.facebookID}&facebookAccessToken=#{application.facebookHandler.authResponse.accessToken}" + options.type = 'PUT' + newUser.set(@facebookAttrs) + else + newUser.set('password', @$('input[name="password1"]').val()) + newUser.save(null, options) + newUser.once 'sync', -> + application.router.navigate(SIGNUP_REDIRECT, { trigger: true }) + application.router.reload() + newUser.once 'error', errors.showNotyNetworkError + + # GPlus signup + + onClickGPlusSignupButton: -> + btn = @$('#gplus-signup-btn') + btn.attr('disabled', true) + application.gplusHandler.loadAPI({ + success: => + btn.attr('disabled', false) + application.gplusHandler.connect({ + success: => + btn.find('.sign-in-blurb').text($.i18n.t('signup.creating')) + btn.attr('disabled', true) + application.gplusHandler.loadPerson({ + success: (@gplusAttrs) => + existingUser = new User() + existingUser.fetchGPlusUser(@gplusAttrs.gplusID, { + error: (user, jqxhr) => + if jqxhr.status is 404 + @onGPlusConnected() + else + errors.showNotyNetworkError(jqxhr) + success: => + me.loginGPlusUser(@gplusAttrs.gplusID, { + success: -> + application.router.navigate('/teachers/convert') + error: errors.showNotyNetworkError + }) + }) + }) + }) + }) + + onGPlusConnected: -> + forms.objectToForm(@$('form'), @gplusAttrs) + for field in ['email', 'firstName', 'lastName'] + input = @$("input[name='#{field}']") + if input.val() + input.attr('disabled', true) + @$('input[type="password"]').attr('disabled', true) + @$('#gplus-logged-in-row, #social-network-signups').toggleClass('hide') + + + # Facebook signup + + onClickFacebookSignupButton: -> + btn = @$('#facebook-signup-btn') + btn.attr('disabled', true) + application.facebookHandler.loadAPI({ + success: => + btn.attr('disabled', false) + application.facebookHandler.connect({ + success: => + btn.find('.sign-in-blurb').text($.i18n.t('signup.creating')) + btn.attr('disabled', true) + application.facebookHandler.loadPerson({ + success: (@facebookAttrs) => + existingUser = new User() + existingUser.fetchFacebookUser(@facebookAttrs.facebookID, { + error: (user, jqxhr) => + if jqxhr.status is 404 + @onFacebookConnected() + else + errors.showNotyNetworkError(jqxhr) + success: => + me.loginFacebookUser(@facebookAttrs.facebookID, { + success: -> + application.router.navigate('/teachers/convert') + error: errors.showNotyNetworkError + }) + }) + }) + }) + }) + + onFacebookConnected: -> + forms.objectToForm(@$('form'), @facebookAttrs) + for field in ['email', 'firstName', 'lastName'] + input = @$("input[name='#{field}']") + if input.val() + input.attr('disabled', true) + @$('input[type="password"]').attr('disabled', true) + @$('#facebook-logged-in-row, #social-network-signups').toggleClass('hide') + + + +formSchema = { + type: 'object' + required: ['firstName', 'lastName', 'email', 'organization', 'role', 'numStudents'] + properties: + password1: { type: 'string' } + password2: { type: 'string' } + firstName: { type: 'string' } + lastName: { type: 'string' } + name: { type: 'string', minLength: 1 } + email: { type: 'string', format: 'email' } + phoneNumber: { type: 'string' } + role: { type: 'string' } + organization: { type: 'string' } + city: { type: 'string' } + state: { type: 'string' } + country: { type: 'string' } + numStudents: { type: 'string' } + educationLevel: { + type: 'array' + items: { type: 'string' } + } + notes: { type: 'string' } +} diff --git a/app/views/teachers/RequestQuoteView.coffee b/app/views/teachers/RequestQuoteView.coffee new file mode 100644 index 000000000..dee4c622f --- /dev/null +++ b/app/views/teachers/RequestQuoteView.coffee @@ -0,0 +1,260 @@ +RootView = require 'views/core/RootView' +forms = require 'core/forms' +TrialRequest = require 'models/TrialRequest' +TrialRequests = require 'collections/TrialRequests' +AuthModal = require 'views/core/AuthModal' +storage = require 'core/storage' +errors = require 'core/errors' +ConfirmModal = require 'views/editor/modal/ConfirmModal' + +FORM_KEY = 'request-quote-form' +SIGNUP_REDIRECT = '/courses/teachers' + +module.exports = class RequestQuoteView extends RootView + id: 'request-quote-view' + template: require 'templates/teachers/request-quote-view' + logoutRedirectURL: null + + events: + 'change #request-form': 'onChangeRequestForm' + 'submit #request-form': 'onSubmitRequestForm' + 'click #email-exists-login-link': 'onClickEmailExistsLoginLink' + 'submit #signup-form': 'onSubmitSignupForm' + 'click #logout-link': -> me.logout() + 'click #gplus-signup-btn': 'onClickGPlusSignupButton' + 'click #facebook-signup-btn': 'onClickFacebookSignupButton' + + initialize: -> + @trialRequest = new TrialRequest() + @trialRequests = new TrialRequests() + @trialRequests.fetchOwn() + @supermodel.trackCollection(@trialRequests) + + onLoaded: -> + if @trialRequests.size() + @trialRequest = @trialRequests.first() + if @trialRequest and @trialRequest.get('status') isnt 'submitted' and @trialRequest.get('status') isnt 'approved' + window.tracker?.trackEvent 'View Trial Request', category: 'Teachers', label: 'View Trial Request', ['Mixpanel'] + super() + + afterRender: -> + super() + + # apply existing trial request on form + properties = @trialRequest.get('properties') + if properties + forms.objectToForm(@$('#request-form'), properties) + commonLevels = _.map @$('[name="educationLevel"]'), (el) -> $(el).val() + submittedLevels = properties.educationLevel or [] + otherLevel = _.first(_.difference(submittedLevels, commonLevels)) or '' + @$('#other-education-level-checkbox').attr('checked', !!otherLevel) + @$('#other-education-level-input').val(otherLevel) + + # apply changes from local storage + obj = storage.load(FORM_KEY) + if obj + @$('#other-education-level-checkbox').attr('checked', obj.otherChecked) + @$('#other-education-level-input').val(obj.otherInput) + forms.objectToForm(@$('#request-form'), obj, { overwriteExisting: true }) + + onChangeRequestForm: -> + # save changes to local storage + obj = forms.formToObject(@$('form')) + obj.otherChecked = @$('#other-education-level-checkbox').is(':checked') + obj.otherInput = @$('#other-education-level-input').val() + storage.save(FORM_KEY, obj, 10) + + onSubmitRequestForm: (e) -> + e.preventDefault() + form = @$('#request-form') + attrs = forms.formToObject(form) + + # custom other input logic (also used in form local storage save/restore) + if @$('#other-education-level-checkbox').is(':checked') + val = @$('#other-education-level-input').val() + attrs.educationLevel.push(val) if val + + forms.clearFormAlerts(form) + requestFormSchema = if me.isAnonymous() then requestFormSchemaAnonymous else requestFormSchemaLoggedIn + result = tv4.validateMultiple(attrs, requestFormSchemaAnonymous) + error = false + if not result.valid + forms.applyErrorsToForm(form, result.errors) + error = true + if not forms.validateEmail(attrs.email) + forms.setErrorToProperty(form, 'email', 'Invalid email.') + error = true + if not _.size(attrs.educationLevel) + forms.setErrorToProperty(form, 'educationLevel', 'Include at least one.') + error = true + if error + forms.scrollToFirstError() + return + @trialRequest = new TrialRequest({ + type: 'course' + properties: attrs + }) + if me.get('role') is 'student' and not me.isAnonymous() + modal = new ConfirmModal({ + title: '' + body: "

#{$.i18n.t('teachers_quote.conversion_warning')}

#{$.i18n.t('teachers_quote.learn_more_modal')}

" + confirm: $.i18n.t('common.continue') + decline: $.i18n.t('common.cancel') + }) + @openModalView(modal) + modal.once 'confirm', @saveTrialRequest, @ + else + @saveTrialRequest() + + saveTrialRequest: -> + @trialRequest.notyErrors = false + @$('#submit-request-btn').text('Sending').attr('disabled', true) + @trialRequest.save() + @trialRequest.on 'sync', @onTrialRequestSubmit, @ + @trialRequest.on 'error', @onTrialRequestError, @ + + onTrialRequestError: (model, jqxhr) -> + @$('#submit-request-btn').text('Submit').attr('disabled', false) + if jqxhr.status is 409 + userExists = $.i18n.t('teachers_quote.email_exists') + logIn = $.i18n.t('login.log_in') + @$('#email-form-group') + .addClass('has-error') + .append($("
#{userExists} #{logIn}")) + forms.scrollToFirstError() + else + errors.showNotyNetworkError(arguments...) + + onClickEmailExistsLoginLink: -> + modal = new AuthModal({ initialValues: { email: @trialRequest.get('properties')?.email } }) + @openModalView(modal) + + onTrialRequestSubmit: -> + me.setRole @trialRequest.get('properties').role.toLowerCase(), true + storage.remove(FORM_KEY) + @$('#request-form, #form-submit-success').toggleClass('hide') + @scrollToTop(0) + $('#flying-focus').css({top: 0, left: 0}) # Hack copied from Router.coffee#187. Ideally we'd swap out the view and have view-swapping logic handle this + window.tracker?.trackEvent 'Submit Trial Request', category: 'Teachers', label: 'Trial Request', ['Mixpanel'] + + onClickGPlusSignupButton: -> + btn = @$('#gplus-signup-btn') + btn.attr('disabled', true) + application.gplusHandler.loadAPI({ + context: @ + success: -> + btn.attr('disabled', false) + 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) -> + me.set(gplusAttrs) + me.save(null, { + url: "/db/user?gplusID=#{gplusAttrs.gplusID}&gplusAccessToken=#{application.gplusHandler.token()}" + type: 'PUT' + success: -> + application.router.navigate(SIGNUP_REDIRECT) + window.location.reload() + error: errors.showNotyNetworkError + }) + }) + }) + }) + + onClickFacebookSignupButton: -> + btn = @$('#facebook-signup-btn') + btn.attr('disabled', true) + application.facebookHandler.loadAPI({ + context: @ + success: -> + btn.attr('disabled', false) + 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) -> + me.set(facebookAttrs) + me.save(null, { + url: "/db/user?facebookID=#{facebookAttrs.facebookID}&facebookAccessToken=#{application.facebookHandler.token()}" + type: 'PUT' + success: -> + application.router.navigate(SIGNUP_REDIRECT) + window.location.reload() + error: errors.showNotyNetworkError + }) + }) + }) + }) + + + onSubmitSignupForm: (e) -> + e.preventDefault() + form = @$('#signup-form') + attrs = forms.formToObject(form) + + forms.clearFormAlerts(form) + result = tv4.validateMultiple(attrs, signupFormSchema) + error = false + if not result.valid + forms.applyErrorsToForm(form, result.errors) + error = true + if attrs.password1 isnt attrs.password2 + forms.setErrorToProperty(form, 'password1', 'Passwords do not match') + error = true + return if error + + me.set({ + password: attrs.password1 + name: attrs.name + email: @trialRequest.get('properties').email + }) + me.save(null, { + success: -> + application.router.navigate(SIGNUP_REDIRECT) + window.location.reload() + error: errors.showNotyNetworkError + }) + + + +requestFormSchemaAnonymous = { + type: 'object' + required: ['firstName', 'lastName', 'email', 'organization', 'role', 'numStudents'] + properties: + firstName: { type: 'string' } + lastName: { type: 'string' } + name: { type: 'string', minLength: 1 } + email: { type: 'string', format: 'email' } + phoneNumber: { type: 'string' } + role: { type: 'string' } + organization: { type: 'string' } + city: { type: 'string' } + state: { type: 'string' } + country: { type: 'string' } + numStudents: { type: 'string' } + educationLevel: { + type: 'array' + items: { type: 'string' } + } + notes: { type: 'string' } +} + +# same form, but add username input +requestFormSchemaLoggedIn = _.cloneDeep(requestFormSchemaAnonymous) +requestFormSchemaLoggedIn.required.push('name') + +signupFormSchema = { + type: 'object' + required: ['name', 'password1', 'password2'] + properties: + name: { type: 'string' } + password1: { type: 'string' } + password2: { type: 'string' } +} diff --git a/scripts/mongodb/migrations/2016-03-18-init-school-roles.js b/scripts/mongodb/migrations/2016-03-18-init-school-roles.js new file mode 100644 index 000000000..efac703b1 --- /dev/null +++ b/scripts/mongodb/migrations/2016-03-18-init-school-roles.js @@ -0,0 +1,25 @@ +// Removes all users with a teacher-like role from classroom membership +// Usage: copy and paste into mongo + +var teacherRoles = ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent']; + +db.users.find({'role': {$in: teacherRoles}}, {_id: 1, name: 1, email: 1, role: 1}).forEach(function(user) { + print('Updating user', JSON.stringify(user)); + print(db.classrooms.find({members: user._id}, {name: 1}).toArray().length); + print(db.classrooms.update({members: user._id}, {$pull: {members: user._id}}, {multi: true})); +}); + + +// Finds all members of classrooms, sets their role to 'student' if they do not already have a role +// Usage: copy and paste into mongo + +db.classrooms.find({}, {members: 1}).forEach(function(classroom) { + if(!classroom.members) { + return; + } + for (var i in classroom.members) { + var memberID = classroom.members[i]; + print('updating member', memberID); + print(db.users.update({_id: memberID, role: {$exists: false}}, {$set: {role: 'student'}})); + } +}); \ No newline at end of file diff --git a/server/classrooms/classroom_handler.coffee b/server/classrooms/classroom_handler.coffee index 63666ff27..31e1b00b0 100644 --- a/server/classrooms/classroom_handler.coffee +++ b/server/classrooms/classroom_handler.coffee @@ -15,6 +15,7 @@ ClassroomHandler = class ClassroomHandler extends Handler hasAccess: (req) -> return false unless req.user return true if req.method is 'GET' +# return false if req.method is 'POST' and not req.user?.isTeacher() req.method in @allowedMethods or req.user?.isAdmin() hasAccessToDocument: (req, document, method=null) -> @@ -52,6 +53,7 @@ ClassroomHandler = class ClassroomHandler extends Handler joinClassroomAPI: (req, res, classroomID) -> return @sendBadInputError(res, 'Need an object with a code') unless req.body?.code +# return @sendForbiddenError(res, 'Cannot join a classroom as a teacher') if req.user.isTeacher() code = req.body.code.toLowerCase() Classroom.findOne {code: code}, (err, classroom) => return @sendDatabaseError(res, err) if err @@ -64,7 +66,15 @@ ClassroomHandler = class ClassroomHandler extends Handler return @sendDatabaseError(res, err) if err members.push req.user.get('_id') classroom.set('members', members) - return @sendSuccess(res, @formatEntity(req, classroom)) + # TODO: remove teacher check here after forbidden above + if req.user.get('role') not in ['student', 'teacher'] + User.findById(req.user.get('_id')).exec (err, user) => + user.set('role', 'student') + user.save (err, user) => + return @sendDatabaseError(res, err) if err + return @sendSuccess(res, @formatEntity(req, classroom)) + else + return @sendSuccess(res, @formatEntity(req, classroom)) removeMember: (req, res, classroomID) -> userID = req.body.userID diff --git a/server/middleware/trial-requests.coffee b/server/middleware/trial-requests.coffee index de08691e6..0618ede2f 100644 --- a/server/middleware/trial-requests.coffee +++ b/server/middleware/trial-requests.coffee @@ -16,11 +16,15 @@ module.exports = user = yield User.findOne({emailLower: email}) throw new errors.Conflict('User with this email already exists.') if user - trialRequest = database.initDoc(req, TrialRequest) - trialRequest.set 'applicant', req.user._id - trialRequest.set 'created', new Date() + trialRequest = yield TrialRequest.findOne({applicant: req.user._id}) + if not trialRequest + trialRequest = database.initDoc(req, TrialRequest) + trialRequest.set 'applicant', req.user._id + trialRequest.set 'created', new Date() trialRequest.set 'status', 'submitted' - database.assignBody(req, trialRequest) + attrs = _.pick req.body, 'properties', 'type' + trialRequest.set 'properties', _.extend {}, trialRequest.get('properties'), attrs.properties + trialRequest.set 'type', attrs.type database.validateDoc(trialRequest) trialRequest = yield trialRequest.save() res.status(201).send(trialRequest.toObject({req: req})) diff --git a/server/users/User.coffee b/server/users/User.coffee index e260ef417..8cb63f1c7 100644 --- a/server/users/User.coffee +++ b/server/users/User.coffee @@ -61,6 +61,11 @@ UserSchema.methods.isArtisan = -> UserSchema.methods.isAnonymous = -> @get 'anonymous' +UserSchema.statics.teacherRoles = ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent', 'parent'] + +UserSchema.methods.isTeacher = -> + return @get('role') in User.teacherRoles + UserSchema.methods.getUserInfo = -> id: @get('_id') email: if @get('anonymous') then 'Unregistered User' else @get('email') @@ -303,6 +308,10 @@ UserSchema.methods.saveActiveUser = (event, done=null) -> done?() UserSchema.pre('save', (next) -> + Classroom = require '../classrooms/Classroom' + if @isTeacher() and not @wasTeacher + Classroom.update({members: @_id}, {$pull: {members: @_id}}, {multi: true}).exec (err, res) -> + console.log 'removed self from all classrooms as a member', err, res if email = @get('email') @set('emailLower', email.toLowerCase()) if name = @get('name') @@ -322,6 +331,7 @@ UserSchema.post 'save', (doc) -> UserSchema.statics.updateServiceSettings(doc) UserSchema.post 'init', (doc) -> + doc.wasTeacher = doc.isTeacher() doc.startingEmails = _.cloneDeep(doc.get('emails')) UserSchema.statics.hashPassword = (password) -> diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index c750a4f73..e07e663a0 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -42,6 +42,9 @@ UserHandler = class UserHandler extends Handler props.push 'permissions' unless config.isProduction or global.testing props.push 'jobProfileApproved', 'jobProfileNotes','jobProfileApprovedDate' if req.user.isAdmin() # Admins naturally edit these props.push @privateProperties... if req.user.isAdmin() # Admins are mad with power + if not req.user.isAdmin() + if document.isTeacher() and req.body.role not in User.teacherRoles + props = _.without props, 'role' props formatEntity: (req, document, publicOnly=false) => diff --git a/spec/server/functional/classrooms.spec.coffee b/spec/server/functional/classrooms.spec.coffee index 72fcaff29..258379184 100644 --- a/spec/server/functional/classrooms.spec.coffee +++ b/spec/server/functional/classrooms.spec.coffee @@ -42,14 +42,16 @@ describe 'GET /db/classroom/:id', -> it 'returns the classroom for the given id', (done) -> loginNewUser (user1) -> - data = { name: 'Classroom 1' } - request.post {uri: classroomsURL, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(200) - classroomID = body._id - request.get {uri: classroomsURL + '/' + body._id }, (err, res, body) -> + user1.set('role', 'teacher') + user1.save (err) -> + data = { name: 'Classroom 1' } + request.post {uri: classroomsURL, json: data }, (err, res, body) -> expect(res.statusCode).toBe(200) - expect(body._id).toBe(classroomID = body._id) - done() + classroomID = body._id + request.get {uri: classroomsURL + '/' + body._id }, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body._id).toBe(classroomID = body._id) + done() describe 'POST /db/classroom', -> @@ -60,13 +62,15 @@ describe 'POST /db/classroom', -> it 'creates a new classroom for the given user', (done) -> loginNewUser (user1) -> - data = { name: 'Classroom 1' } - request.post {uri: classroomsURL, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(200) - expect(body.name).toBe('Classroom 1') - expect(body.members.length).toBe(0) - expect(body.ownerID).toBe(user1.id) - done() + user1.set('role', 'teacher') + user1.save (err) -> + data = { name: 'Classroom 1' } + request.post {uri: classroomsURL, json: data }, (err, res, body) -> + expect(res.statusCode).toBe(200) + expect(body.name).toBe('Classroom 1') + expect(body.members.length).toBe(0) + expect(body.ownerID).toBe(user1.id) + done() it 'does not work for anonymous users', (done) -> logoutUser -> @@ -74,6 +78,14 @@ describe 'POST /db/classroom', -> request.post {uri: classroomsURL, json: data }, (err, res, body) -> expect(res.statusCode).toBe(401) done() + + # TODO: Re-enable when we enforce this again + xit 'does not work for non-teacher users', (done) -> + loginNewUser (user1) -> + data = { name: 'Classroom 1' } + request.post {uri: classroomsURL, json: data }, (err, res, body) -> + expect(res.statusCode).toBe(403) + done() describe 'PUT /db/classroom', -> @@ -85,31 +97,35 @@ describe 'PUT /db/classroom', -> it 'edits name and description', (done) -> loginNewUser (user1) -> - data = { name: 'Classroom 2' } - request.post {uri: classroomsURL, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(200) - data = { name: 'Classroom 3', description: 'New Description' } - url = classroomsURL + '/' + body._id - request.put { uri: url, json: data }, (err, res, body) -> - expect(body.name).toBe('Classroom 3') - expect(body.description).toBe('New Description') - done() + user1.set('role', 'teacher') + user1.save (err) -> + data = { name: 'Classroom 2' } + request.post {uri: classroomsURL, json: data }, (err, res, body) -> + expect(res.statusCode).toBe(200) + data = { name: 'Classroom 3', description: 'New Description' } + url = classroomsURL + '/' + body._id + request.put { uri: url, json: data }, (err, res, body) -> + expect(body.name).toBe('Classroom 3') + expect(body.description).toBe('New Description') + done() it 'is not allowed if you are just a member', (done) -> loginNewUser (user1) -> - data = { name: 'Classroom 4' } - request.post {uri: classroomsURL, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(200) - classroomCode = body.code - loginNewUser (user2) -> - url = getURL("/db/classroom/~/members") - data = { code: classroomCode } - request.post { uri: url, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(200) - url = classroomsURL + '/' + body._id - request.put { uri: url, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(403) - done() + user1.set('role', 'teacher') + user1.save (err) -> + data = { name: 'Classroom 4' } + request.post {uri: classroomsURL, json: data }, (err, res, body) -> + expect(res.statusCode).toBe(200) + classroomCode = body.code + loginNewUser (user2) -> + url = getURL("/db/classroom/~/members") + data = { code: classroomCode } + request.post { uri: url, json: data }, (err, res, body) -> + expect(res.statusCode).toBe(200) + url = classroomsURL + '/' + body._id + request.put { uri: url, json: data }, (err, res, body) -> + expect(res.statusCode).toBe(403) + done() describe 'POST /db/classroom/~/members', -> @@ -120,19 +136,45 @@ describe 'POST /db/classroom/~/members', -> it 'adds the signed in user to the list of members in the classroom', (done) -> loginNewUser (user1) -> - data = { name: 'Classroom 5' } - request.post {uri: classroomsURL, json: data }, (err, res, body) -> - classroomCode = body.code - classroomID = body._id - expect(res.statusCode).toBe(200) - loginNewUser (user2) -> - url = getURL("/db/classroom/~/members") - data = { code: classroomCode } - request.post { uri: url, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(200) - Classroom.findById classroomID, (err, classroom) -> - expect(classroom.get('members').length).toBe(1) - done() + user1.set('role', 'teacher') + user1.save (err) -> + data = { name: 'Classroom 5' } + request.post {uri: classroomsURL, json: data }, (err, res, body) -> + classroomCode = body.code + classroomID = body._id + expect(res.statusCode).toBe(200) + loginNewUser (user2) -> + url = getURL("/db/classroom/~/members") + data = { code: classroomCode } + request.post { uri: url, json: data }, (err, res, body) -> + expect(res.statusCode).toBe(200) + Classroom.findById classroomID, (err, classroom) -> + expect(classroom.get('members').length).toBe(1) + expect(classroom.get('members')?[0]?.equals(user2.get('_id'))).toBe(true) + User.findById user2.get('_id'), (err, user2) -> + expect(user2.get('role')).toBe('student') + done() + + # TODO: Re-enable when we enforce this again + xit 'does not work if the user is a teacher', (done) -> + loginNewUser (user1) -> + user1.set('role', 'teacher') + user1.save (err) -> + data = { name: 'Classroom 5' } + request.post {uri: classroomsURL, json: data }, (err, res, body) -> + classroomCode = body.code + classroomID = body._id + expect(res.statusCode).toBe(200) + loginNewUser (user2) -> + user2.set('role', 'teacher') + user2.save (err, user2) -> + url = getURL("/db/classroom/~/members") + data = { code: classroomCode } + request.post { uri: url, json: data }, (err, res, body) -> + expect(res.statusCode).toBe(403) + Classroom.findById classroomID, (err, classroom) -> + expect(classroom.get('members').length).toBe(0) + done() describe 'DELETE /db/classroom/:id/members', -> @@ -144,36 +186,40 @@ describe 'DELETE /db/classroom/:id/members', -> it 'removes the given user from the list of members in the classroom', (done) -> loginNewUser (user1) -> - data = { name: 'Classroom 6' } - request.post {uri: classroomsURL, json: data }, (err, res, body) -> - classroomCode = body.code - classroomID = body._id - expect(res.statusCode).toBe(200) - loginNewUser (user2) -> - url = getURL("/db/classroom/~/members") - data = { code: classroomCode } - request.post { uri: url, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(200) - Classroom.findById classroomID, (err, classroom) -> - expect(classroom.get('members').length).toBe(1) - url = getURL("/db/classroom/#{classroom.id}/members") - data = { userID: user2.id } - request.del { uri: url, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(200) - Classroom.findById classroomID, (err, classroom) -> - expect(classroom.get('members').length).toBe(0) - done() + user1.set('role', 'teacher') + user1.save (err) -> + data = { name: 'Classroom 6' } + request.post {uri: classroomsURL, json: data }, (err, res, body) -> + classroomCode = body.code + classroomID = body._id + expect(res.statusCode).toBe(200) + loginNewUser (user2) -> + url = getURL("/db/classroom/~/members") + data = { code: classroomCode } + request.post { uri: url, json: data }, (err, res, body) -> + expect(res.statusCode).toBe(200) + Classroom.findById classroomID, (err, classroom) -> + expect(classroom.get('members').length).toBe(1) + url = getURL("/db/classroom/#{classroom.id}/members") + data = { userID: user2.id } + request.del { uri: url, json: data }, (err, res, body) -> + expect(res.statusCode).toBe(200) + Classroom.findById classroomID, (err, classroom) -> + expect(classroom.get('members').length).toBe(0) + done() describe 'POST /db/classroom/:id/invite-members', -> it 'takes a list of emails and sends invites', (done) -> loginNewUser (user1) -> - data = { name: 'Classroom 6' } - request.post {uri: classroomsURL, json: data }, (err, res, body) -> - expect(res.statusCode).toBe(200) - url = classroomsURL + '/' + body._id + '/invite-members' - data = { emails: ['test@test.com'] } - request.post { uri: url, json: data }, (err, res, body) -> + user1.set('role', 'teacher') + user1.save (err) -> + data = { name: 'Classroom 6' } + request.post {uri: classroomsURL, json: data }, (err, res, body) -> expect(res.statusCode).toBe(200) - done() + url = classroomsURL + '/' + body._id + '/invite-members' + data = { emails: ['test@test.com'] } + request.post { uri: url, json: data }, (err, res, body) -> + expect(res.statusCode).toBe(200) + done() diff --git a/spec/server/functional/trial_request.spec.coffee b/spec/server/functional/trial_request.spec.coffee index f1c45de00..ac9638044 100644 --- a/spec/server/functional/trial_request.spec.coffee +++ b/spec/server/functional/trial_request.spec.coffee @@ -13,8 +13,6 @@ fixture = { } describe 'POST /db/trial.request', -> - URL = getURL('/db/trial.request') - ownURL = getURL('/db/trial.request/-/own') it 'sets type and properties given', utils.wrap (done) -> yield utils.clearModels([User, TrialRequest]) @@ -62,6 +60,29 @@ describe 'POST /db/trial.request', -> [res, body] = yield request.postAsync(getURL('/db/trial.request'), { json: true }) expect(res.statusCode).toBe(422) done() + + it 'updates an existing TrialRequest if there is one', utils.wrap (done) -> + yield utils.clearModels([User, TrialRequest]) + @user = yield utils.initUser() + yield utils.loginUser(@user) + fixture.properties.email = @user.get('email') + [res, body] = yield request.postAsync(getURL('/db/trial.request'), { json: fixture }) + expect(res.statusCode).toBe(201) + expect(body._id).toBeDefined() + trialRequest = yield TrialRequest.findById(body._id) + + update = { + type: 'course' + properties: + location: 'Bahamas' + } + [res, body] = yield request.postAsync(getURL('/db/trial.request'), { json: update }) + expect(body.type).toBe('course') + expect(body.properties.location).toBe('Bahamas') + expect(body._id).toBe(trialRequest.id) + count = yield TrialRequest.count() + expect(count).toBe(1) + done() describe 'GET /db/trial.request', -> diff --git a/spec/server/functional/user.spec.coffee b/spec/server/functional/user.spec.coffee index 3eb9648e3..c217cd148 100644 --- a/spec/server/functional/user.spec.coffee +++ b/spec/server/functional/user.spec.coffee @@ -255,6 +255,32 @@ ghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghlfarghlarghl request.put {uri:getURL(urlUser + '/' + sam.id), json: sam.toObject()}, (err, response) -> expect(err).toBeNull() done() + + describe 'when role is changed to teacher or other school administrator', -> + it 'removes the user from all classrooms they are in', utils.wrap (done) -> + user = yield utils.initUser() + classroom = new Classroom({members: [user._id]}) + yield classroom.save() + expect(classroom.get('members').length).toBe(1) + yield utils.loginUser(user) + [res, body] = yield request.putAsync { uri: getURL('/db/user/'+user.id), json: { role: 'teacher' }} + yield new Promise (resolve) -> setTimeout(resolve, 10) + classroom = yield Classroom.findById(classroom.id) + expect(classroom.get('members').length).toBe(0) + done() + + it 'ignores attempts to change away from a teacher role', utils.wrap (done) -> + user = yield utils.initUser() + yield utils.loginUser(user) + url = getURL('/db/user/'+user.id) + [res, body] = yield request.putAsync { uri: url, json: { role: 'teacher' }} + expect(body.role).toBe('teacher') + [res, body] = yield request.putAsync { uri: url, json: { role: 'advisor' }} + expect(body.role).toBe('advisor') + [res, body] = yield request.putAsync { uri: url, json: { role: 'student' }} + expect(body.role).toBe('advisor') + done() + describe 'GET /db/user', -> diff --git a/test/app/views/teachers/ConvertToTeacherAccountView.spec.coffee b/test/app/views/teachers/ConvertToTeacherAccountView.spec.coffee new file mode 100644 index 000000000..ad97ceea5 --- /dev/null +++ b/test/app/views/teachers/ConvertToTeacherAccountView.spec.coffee @@ -0,0 +1,150 @@ +ConvertToTeacherAccountView = require 'views/teachers/ConvertToTeacherAccountView' +storage = require 'core/storage' +forms = require 'core/forms' + +describe '/teachers/convert', -> + describe 'when logged out', -> + it 'redirects to /teachers/signup', -> + spyOn(me, 'isAnonymous').and.returnValue(true) + spyOn(application.router, 'navigate') + Backbone.history.loadUrl('/teachers/convert') + expect(application.router.navigate.calls.count()).toBe(1) + args = application.router.navigate.calls.argsFor(0) + expect(args[0]).toBe('/teachers/signup') + + describe 'when logged in', -> + it 'displays ConvertToTeacherAccountView', -> + spyOn(me, 'isAnonymous').and.returnValue(false) + spyOn(me, 'isTeacher').and.returnValue(false) + spyOn(application.router, 'routeDirectly') + Backbone.history.loadUrl('/teachers/convert') + expect(application.router.routeDirectly.calls.count()).toBe(1) + args = application.router.routeDirectly.calls.argsFor(0) + expect(args[0]).toBe('teachers/ConvertToTeacherAccountView') + + +describe 'ConvertToTeacherAccountView (/teachers/convert)', -> + + view = null + + successForm = { + phoneNumber: '555-555-5555' + role: 'Teacher' + organization: 'School' + city: 'Springfield' + state: 'AA' + country: 'asdf' + numStudents: '1-10' + educationLevel: ['Middle'] + firstName: 'Mr' + lastName: 'Bean' + } + + beforeEach -> + me.clear() + me.set({ + _id: '1234' + anonymous: false + email: 'some@email.com' + name: 'Existing User' + }) + me._revertAttributes = {} + view = new ConvertToTeacherAccountView() + view.render() + jasmine.demoEl(view.$el) + + spyOn(storage, 'load').and.returnValue({ lastName: 'Saved Changes' }) + + + describe 'when the user already has a TrialRequest and is a teacher', -> + beforeEach (done) -> + spyOn(application.router, 'navigate') + spyOn(me, 'isTeacher').and.returnValue(true) + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ + status: 200 + responseText: JSON.stringify([{ + _id: '1' + properties: { + firstName: 'First' + lastName: 'Last' + } + }]) + }) + _.defer done # Let SuperModel finish + + it 'redirects to /courses/teachers', -> + expect(application.router.navigate).toHaveBeenCalled() + args = application.router.navigate.calls.argsFor(0) + expect(args[0]).toBe('/courses/teachers') + + + describe 'when the user has role "student"', -> + beforeEach -> + me.set('role', 'student') + view.render() + + it 'shows a warning that they will convert to a teacher account', -> + expect(view.$('#conversion-warning').length).toBe(1) + +# TODO: Figure out how to test this +# describe 'the warning', -> +# it 'includes a learn more link which opens a modal with more info' + + describe 'submitting the form', -> + beforeEach -> + form = view.$('form') + forms.objectToForm(form, successForm, {overwriteExisting: true}) + spyOn(view, 'openModalView') + form.submit() + + it 'requires confirmation', -> + request = jasmine.Ajax.requests.mostRecent() + expect(request.url).not.toBe('/db/trial.request') + expect(request.method).not.toBe('POST') + confirmModal = view.openModalView.calls.argsFor(0)[0] + confirmModal.trigger 'confirm' + request = jasmine.Ajax.requests.mostRecent() + expect(request.url).toBe('/db/trial.request') + expect(request.method).toBe('POST') + + describe '"Log out" link', -> + + it 'logs out the user and redirects them to /teachers/signup', -> + spyOn(me, 'logout') + view.$('#logout-link').click() + expect(me.logout).toHaveBeenCalled() + + describe 'submitting the form', -> + beforeEach -> + form = view.$('form') + forms.objectToForm(form, successForm, {overwriteExisting: true}) + form.submit() + + it 'creates a new TrialRequest with the information', -> + request = _.last(jasmine.Ajax.requests.filter((r) -> _.string.startsWith(r.url, '/db/trial.request'))) + expect(request).toBeTruthy() + expect(request.method).toBe('POST') + attrs = JSON.parse(request.params) + expect(attrs.properties?.firstName).toBe('Mr') + + it 'redirects to /courses/teachers', -> + spyOn(application.router, 'navigate') + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ + status: 201 + responseText: JSON.stringify(_.extend({_id:'fraghlarghl'}, JSON.parse(request.params))) + }) + expect(application.router.navigate).toHaveBeenCalled() + args = application.router.navigate.calls.argsFor(0) + expect(args[0]).toBe('/courses/teachers') + + it 'sets a teacher role', -> + spyOn(application.router, 'navigate') + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ + status: 201 + responseText: JSON.stringify(_.extend({_id:'fraghlarghl'}, JSON.parse(request.params))) + }) + expect(me.get('role')).toBe(successForm.role.toLowerCase()) + diff --git a/test/app/views/teachers/CreateTeacherAccountView.spec.coffee b/test/app/views/teachers/CreateTeacherAccountView.spec.coffee new file mode 100644 index 000000000..f24b3164a --- /dev/null +++ b/test/app/views/teachers/CreateTeacherAccountView.spec.coffee @@ -0,0 +1,257 @@ +CreateTeacherAccountView = require 'views/teachers/CreateTeacherAccountView' +storage = require 'core/storage' +forms = require 'core/forms' + +describe '/teachers/signup', -> + + describe 'when logged out', -> + + it 'displays CreateTeacherAccountView', -> + spyOn(me, 'isAnonymous').and.returnValue(true) + spyOn(application.router, 'routeDirectly') + Backbone.history.loadUrl('/teachers/signup') + expect(application.router.routeDirectly.calls.count()).toBe(1) + args = application.router.routeDirectly.calls.argsFor(0) + expect(args[0]).toBe('teachers/CreateTeacherAccountView') + + describe 'when logged in', -> + + it 'redirects to /teachers/convert', -> + spyOn(me, 'isAnonymous').and.returnValue(false) + spyOn(application.router, 'navigate') + Backbone.history.loadUrl('/teachers/signup') + expect(application.router.navigate.calls.count()).toBe(1) + args = application.router.navigate.calls.argsFor(0) + expect(args[0]).toBe('/teachers/convert') + + +describe 'CreateTeacherAccountView', -> + + view = null + + successForm = { + name: 'New Name' + phoneNumber: '555-555-5555' + role: 'Teacher' + organization: 'School' + city: 'Springfield' + state: 'AA' + country: 'asdf' + numStudents: '1-10' + educationLevel: ['Middle'] + email: 'some@email.com' + firstName: 'Mr' + lastName: 'Bean' + password1: 'letmein' + password2: 'letmein' + } + + beforeEach (done) -> + me.clear() + me.set('_id', '1234') + me._revertAttributes = {} + spyOn(me, 'isAnonymous').and.returnValue(true) + view = new CreateTeacherAccountView() + view.render() + jasmine.demoEl(view.$el) + + spyOn(storage, 'load').and.returnValue({ lastName: 'Saved Changes' }) + + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ + status: 200 + responseText: JSON.stringify([{ + _id: '1' + properties: { + firstName: 'First' + lastName: 'Last' + } + }]) + }) + _.defer done # Let SuperModel finish + + describe '"Log in" link', -> + + it 'opens the log in modal', -> + spyOn(view, 'openModalView') + view.$('.alert .login-link').click() + expect(view.openModalView.calls.count()).toBe(1) + AuthModal = require 'views/core/AuthModal' + expect(view.openModalView.calls.argsFor(0)[0] instanceof AuthModal).toBe(true) + + describe 'clicking the Facebook button', -> + + beforeEach -> + application.facebookHandler.fakeAPI() + view.$('#facebook-signup-btn').click() + request = jasmine.Ajax.requests.mostRecent() + expect(request.url).toBe('/db/user?facebookID=abcd&facebookAccessToken=1234') + expect(request.method).toBe('GET') + + describe 'when an associated user already exists', -> + beforeEach -> + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ + status: 200 + responseText: JSON.stringify({_id: 'abcd'}) + }) + + it 'logs them in and redirects them to the ConvertToTeacherAccountView', -> + request = jasmine.Ajax.requests.mostRecent() + expect(request.url).toBe('/auth/login-facebook') + + describe 'when the user\s info is loaded', -> + beforeEach -> + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ status: 404, responseText: '{}' }) + + it 'disables and fills in the email, first name, last name and password fields', -> + for field in ['email', 'firstName', 'lastName', 'password1', 'password2'] + expect(view.$("input[name='#{field}']").attr('disabled')).toBeTruthy() + + it 'hides the social login buttons and shows a success message', -> + expect(view.$('#facebook-logged-in-row').hasClass('hide')).toBe(false) + expect(view.$('#social-network-signups').hasClass('hide')).toBe(true) + + describe 'and the user finishes filling in the form and submits', -> + + beforeEach -> + form = view.$('form') + forms.objectToForm(form, successForm) + form.submit() + + it 'creates a user associated with the Facebook account', -> + request = jasmine.Ajax.requests.mostRecent() + expect(request.url).toBe('/db/trial.request') + request.respondWith({ + status: 201 + responseText: JSON.stringify(_.extend({_id:'fraghlarghl'}, JSON.parse(request.params))) + }) + request = jasmine.Ajax.requests.mostRecent() + expect(request.url).toBe("/db/user?facebookID=abcd&facebookAccessToken=1234") + body = JSON.parse(request.params) + expect(body.name).toBe('New Name') + expect(body.email).toBe('some@email.com') + expect(body.firstName).toBe('Mr') + expect(body.lastName).toBe('Bean') + + describe 'clicking the G+ button', -> + + beforeEach -> + application.gplusHandler.fakeAPI() + view.$('#gplus-signup-btn').click() + request = jasmine.Ajax.requests.mostRecent() + expect(request.url).toBe('/db/user?gplusID=abcd&gplusAccessToken=1234') + expect(request.method).toBe('GET') + + describe 'when an associated user already exists', -> + beforeEach -> + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ + status: 200 + responseText: JSON.stringify({_id: 'abcd'}) + }) + + it 'logs them in and redirects them to the ConvertToTeacherAccountView', -> + request = jasmine.Ajax.requests.mostRecent() + expect(request.url).toBe('/auth/login-gplus') + + describe 'when the user\s info is loaded', -> + beforeEach -> + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ status: 404, responseText: '{}' }) + + it 'disables and fills in the email, first name, last name and password fields', -> + for field in ['email', 'firstName', 'lastName', 'password1', 'password2'] + expect(view.$("input[name='#{field}']").attr('disabled')).toBeTruthy() + + it 'hides the social login buttons and shows a success message', -> + expect(view.$('#gplus-logged-in-row').hasClass('hide')).toBe(false) + expect(view.$('#social-network-signups').hasClass('hide')).toBe(true) + + describe 'and the user finishes filling in the form and submits', -> + + beforeEach -> + form = view.$('form') + forms.objectToForm(form, successForm) + form.submit() + + it 'creates a user associated with the GPlus account', -> + request = jasmine.Ajax.requests.mostRecent() + expect(request.url).toBe('/db/trial.request') + request.respondWith({ + status: 201 + responseText: JSON.stringify(_.extend({_id:'fraghlarghl'}, JSON.parse(request.params))) + }) + request = jasmine.Ajax.requests.mostRecent() + expect(request.url).toBe("/db/user?gplusID=abcd&gplusAccessToken=1234") + body = JSON.parse(request.params) + expect(body.name).toBe('New Name') + expect(body.email).toBe('some@email.com') + expect(body.firstName).toBe('Mr') + expect(body.lastName).toBe('Bean') + + describe 'submitting the form successfully', -> + + beforeEach -> + form = view.$('form') + forms.objectToForm(form, successForm) + form.submit() + + it 'submits a trial request, which does not include "account" settings', -> + request = jasmine.Ajax.requests.mostRecent() + expect(request.url).toBe('/db/trial.request') + expect(request.method).toBe('POST') + attrs = JSON.parse(request.params) + expect(attrs.password1).toBeUndefined() + expect(attrs.password2).toBeUndefined() + expect(attrs.name).toBeUndefined() + + describe 'after saving the new trial request', -> + beforeEach -> + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ + status: 201 + responseText: JSON.stringify(_.extend({_id:'fraghlarghl'}, JSON.parse(request.params))) + }) + + it 'creates a new user', -> + request = jasmine.Ajax.requests.mostRecent() + expect(request.url).toBe('/db/user') + expect(request.method).toBe('POST') + attrs = JSON.parse(request.params) + for attr in ['password', 'name', 'email', 'role'] + expect(attrs[attr]).toBeDefined() + + describe 'after saving the new user', -> + + beforeEach -> + spyOn(application.router, 'navigate') + spyOn(application.router, 'reload') + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ + status: 201 + responseText: JSON.stringify(_.extend({_id:'fraghlarghl'}, JSON.parse(request.params))) + }) + + it 'redirects to "/courses/teachers"', -> + expect(application.router.navigate).toHaveBeenCalled() + expect(application.router.reload).toHaveBeenCalled() + + + describe 'submitting the form with an email for an existing account', -> + + beforeEach -> + form = view.$('form') + forms.objectToForm(form, successForm) + form.submit() + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ status: 409, responseText: '{}' }) + + it 'displays an error with a log in link', -> + expect(view.$('#email-form-group').hasClass('has-error')).toBe(true) + spyOn(view, 'openModalView') + view.$('#email-form-group .login-link').click() + expect(view.openModalView).toHaveBeenCalled() + + \ No newline at end of file diff --git a/test/app/views/teachers/RequestQuoteView.spec.coffee b/test/app/views/teachers/RequestQuoteView.spec.coffee new file mode 100644 index 000000000..d6db14b22 --- /dev/null +++ b/test/app/views/teachers/RequestQuoteView.spec.coffee @@ -0,0 +1,210 @@ +RequestQuoteView = require 'views/teachers/RequestQuoteView' +storage = require 'core/storage' +forms = require 'core/forms' + +describe 'RequestQuoteView', -> + + view = null + + successFormValues = { + firstName: 'A' + lastName: 'B' + email: 'C@D.com' + phoneNumber: '555-555-5555' + role: 'Teacher' + organization: 'School' + city: 'Springfield' + state: 'AA' + country: 'asdf' + numStudents: '1-10' + educationLevel: ['Middle'] + } + + isSubmitRequest = (r) -> _.string.startsWith(r.url, '/db/trial.request') and r.method is 'POST' + + describe 'when user is anonymous and has an existing trial request', -> + beforeEach (done) -> + me.clear() + me.set('_id', '1234') + me._revertAttributes = {} + spyOn(me, 'isAnonymous').and.returnValue(true) + view = new RequestQuoteView() + view.render() + jasmine.demoEl(view.$el) + + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ + status: 200 + responseText: JSON.stringify([{ + _id: '1' + properties: { + firstName: 'First' + lastName: 'Last' + } + }]) + }) + _.defer done # Let SuperModel finish + + it 'shows request received', -> + expect(view.$('#request-form').hasClass('hide')).toBe(true) + expect(view.$('#form-submit-success').hasClass('hide')).toBe(false) + + + describe 'when user is signed in and has an existing trial request', -> + beforeEach (done) -> + me.clear() + me.set('_id', '1234') + me._revertAttributes = {} + spyOn(me, 'isAnonymous').and.returnValue(false) + view = new RequestQuoteView() + view.render() + jasmine.demoEl(view.$el) + + spyOn(storage, 'load').and.returnValue({ lastName: 'Saved Changes' }) + + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ + status: 200 + responseText: JSON.stringify([{ + _id: '1' + properties: { + firstName: 'First' + lastName: 'Last' + } + }]) + }) + _.defer done # Let SuperModel finish + + it 'shows form with data from the most recent request', -> + expect(view.$('input[name="firstName"]').val()).toBe('First') + + it 'prioritizes showing local, unsaved changes', -> + expect(view.$('input[name="lastName"]').val()).toBe('Saved Changes') + + describe 'when the form changes', -> + + it 'stores local, unsaved changes', -> + spyOn(storage, 'save') + view.$('input[name="firstName"]').val('Just Changed').change() + expect(storage.save).toHaveBeenCalled() + args = storage.save.calls.argsFor(0) + expect(args[1].firstName).toBe('Just Changed') + + describe 'when a user is anonymous and does NOT have an existing trial request', -> + beforeEach (done) -> + me.clear() + me.set('_id', '1234') + me._revertAttributes = {} + spyOn(me, 'isAnonymous').and.returnValue(true) + view = new RequestQuoteView() + view.render() + jasmine.demoEl(view.$el) + + spyOn(storage, 'load').and.returnValue({ lastName: 'Saved Changes' }) + + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ + status: 200 + responseText: '[]' + }) + _.defer done # Let SuperModel finish + + describe 'on successful form submit', -> + beforeEach -> + forms.objectToForm(view.$el, successFormValues) + view.$('#request-form').submit() + @submitRequest = _.last(jasmine.Ajax.requests.filter(isSubmitRequest)) + @submitRequest.respondWith({ + status: 201 + responseText: JSON.stringify(_.extend({_id: 'a'}, successFormValues)) + }) + + it 'creates a new trial request', -> + expect(@submitRequest).toBeTruthy() + expect(@submitRequest.method).toBe('POST') + + it 'sets the user\'s role to the one they chose', -> + request = _.last(jasmine.Ajax.requests.filter((r) -> _.string.startsWith(r.url, '/db/user'))) + expect(request).toBeTruthy() + expect(request.method).toBe('PUT') + expect(JSON.parse(request.params).role).toBe('teacher') + + it 'shows a signup form', -> + expect(view.$('#form-submit-success').hasClass('hide')).toBe(false) + expect(view.$('#request-form').hasClass('hide')).toBe(true) + + describe 'signup form', -> + beforeEach -> + application.facebookHandler.fakeAPI() + application.gplusHandler.fakeAPI() + + it 'includes a facebook button which will sign them in immediately', -> + view.$('#facebook-signup-btn').click() + request = jasmine.Ajax.requests.mostRecent() + expect(request.method).toBe('PUT') + expect(request.url).toBe('/db/user?facebookID=abcd&facebookAccessToken=1234') + + it 'includes a gplus button which will sign them in immediately', -> + view.$('#gplus-signup-btn').click() + request = jasmine.Ajax.requests.mostRecent() + expect(request.method).toBe('PUT') + expect(request.url).toBe('/db/user?gplusID=abcd&gplusAccessToken=1234') + + it 'can sign them up with username and password', -> + form = view.$('#signup-form') + forms.objectToForm(form, { + password1: 'asdf' + password2: 'asdf' + name: 'some name' + }) + form.submit() + request = jasmine.Ajax.requests.mostRecent() + expect(request.method).toBe('PUT') + expect(request.url).toBe('/db/user/1234') + + describe 'when an anonymous user tries to submit a request with an existing user\'s email', -> + + beforeEach -> + forms.objectToForm(view.$el, successFormValues) + view.$('#request-form').submit() + @submitRequest = _.last(jasmine.Ajax.requests.filter(isSubmitRequest)) + @submitRequest.respondWith({ + status: 409 + responseText: '{}' + }) + + it 'shows an error that the email already exists', -> + expect(view.$('#email-form-group').hasClass('has-error')).toBe(true) + expect(view.$('#email-form-group .error-help-block').length).toBe(1) + + describe 'when user is signed in and has role "student"', -> + beforeEach (done) -> + me.set('role', 'student') + me.set('name', 'Some User') + spyOn(me, 'isAnonymous').and.returnValue(false) + view = new RequestQuoteView() + view.render() + jasmine.demoEl(view.$el) + + request = jasmine.Ajax.requests.mostRecent() + request.respondWith({ status: 200, responseText: '[]'}) + _.defer done # Let SuperModel finish + + it 'shows a conversion warning', -> + expect(view.$('#conversion-warning').length).toBe(1) + + it 'requires confirmation to submit the form', -> + form = view.$('#request-form') + forms.objectToForm(form, successFormValues) + spyOn(view, 'openModalView') + form.submit() + expect(view.openModalView).toHaveBeenCalled() + + submitRequest = _.last(jasmine.Ajax.requests.filter(isSubmitRequest)) + expect(submitRequest).toBeFalsy() + confirmModal = view.openModalView.calls.argsFor(0)[0] + confirmModal.trigger 'confirm' + submitRequest = _.last(jasmine.Ajax.requests.filter(isSubmitRequest)) + expect(submitRequest).toBeTruthy() + + \ No newline at end of file