Implement teacher accounts

This commit is contained in:
Scott Erickson 2016-03-09 14:40:52 -08:00
parent 5e2c726a2b
commit bd3a77da9f
32 changed files with 2149 additions and 411 deletions

View file

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

View file

@ -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 <em>Student Account</em>. 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. <strong>Set up a class</strong>, add your students, and <strong>monitor their progress</strong>!"
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"

View file

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

View file

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

View file

@ -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
// After submit (anonymous)
h5
margin-top: 50px
#social-network-signups
margin: 20px 0
button
margin-left: 10px
.text-h1
margin: 40px 0 30px

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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") &times;
.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")

View file

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

View file

@ -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") &times;
.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")

View file

@ -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($("<div class='help-block error-help-block'>#{userExists} <a id='email-exists-login-link'>#{logIn}</a>"))
@$('#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()

View file

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

View file

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

View file

@ -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: "<p>#{$.i18n.t('teachers_quote.conversion_warning')}</p><p>#{$.i18n.t('teachers_quote.learn_more_modal')}</p>"
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' }
}

View file

@ -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($("<div class='help-block error-help-block'>#{userExists} <a class='login-link'>#{logIn}</a>"))
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' }
}

View file

@ -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: "<p>#{$.i18n.t('teachers_quote.conversion_warning')}</p><p>#{$.i18n.t('teachers_quote.learn_more_modal')}</p>"
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($("<div class='help-block error-help-block'>#{userExists} <a id='email-exists-login-link'>#{logIn}</a>"))
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' }
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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