Start new CreateAccountModal

This commit is contained in:
phoenixeliot 2016-06-07 14:51:41 -07:00 committed by Scott Erickson
parent 5e6c9709f9
commit e9b7543242
41 changed files with 1198 additions and 663 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -57,6 +57,14 @@ module.exports.applyErrorsToForm = (el, errors, warning=false) ->
message = error.message if error.formatted
prop = error.property
if error.code is tv4.errorCodes.FORMAT_CUSTOM
originalMessage = /Format validation failed \(([^\(\)]+)\)/.exec(message)[1]
unless _.isEmpty(originalMessage)
message = originalMessage
if error.code is 409 and error.property is 'email'
message += ' <a class="login-link">Log in?</a>'
missingErrors.push error unless setErrorToProperty el, prop, message, warning
missingErrors

View file

@ -26,7 +26,7 @@ module.exports = FacebookHandler = class FacebookHandler extends CocoClass
login: (cb, options) ->
cb({status: 'connected', authResponse: { accessToken: '1234' }})
api: (url, options, cb) ->
cb({
cb({
first_name: 'Mr'
last_name: 'Bean'
id: 'abcd'
@ -103,4 +103,4 @@ module.exports = FacebookHandler = class FacebookHandler extends CocoClass
options.success.bind(options.context)(attrs)
renderButtons: ->
setTimeout(FB.XFBML.parse, 10) if FB?.XFBML?.parse # Handles FB login and Like
setTimeout(FB.XFBML.parse, 10) if FB?.XFBML?.parse # Handles FB login and Like

View file

@ -265,7 +265,7 @@
school_name: "School Name and City"
optional: "optional"
school_name_placeholder: "Example High School, Springfield, IL"
or_sign_up_with: "or sign up with"
connect_with: "Connect with:"
connected_gplus_header: "You've successfully connected with Google+!"
connected_gplus_p: "Finish signing up so you can log in with your Google+ account."
gplus_exists: "You already have an account associated with Google+!"
@ -274,6 +274,8 @@
facebook_exists: "You already have an account associated with Facebook!"
hey_students: "Students, enter the class code from your teacher."
birthday: "Birthday"
parent_email_blurb: "We know you can't wait to learn programming &mdash; we're excited too! Your parents will receive an email with further instructions on how to create an account for you. Email {{email_link}} if you have any questions."
recover:
recover_account_title: "Recover Account"

View file

@ -1,249 +0,0 @@
@import "app/styles/mixins"
@import "app/styles/bootstrap/variables"
#create-account-modal
//- Clear modal defaults
.modal-dialog
padding: 0
width: 666px
height: 694px
//- Background
.auth-modal-background
position: absolute
top: -90px
left: -40px
//- Header
h1
position: absolute
left: 183px
top: 0px
margin: 0
width: 255px
text-align: center
color: rgb(254,188,68)
font-size: 32px
text-shadow: black 2px 2px 0, black -2px -2px 0, black 2px -2px 0, black -2px 2px 0, black 2px 0px 0, black 0px -2px 0, black -2px 0px 0, black 0px 2px 0
&.long-title
top: -14px
//- Close modal button
#close-modal
position: absolute
left: 442px
top: -15px
width: 60px
height: 60px
color: white
text-align: center
font-size: 30px
padding-top: 15px
cursor: pointer
@include rotate(-3deg)
&:hover
color: yellow
//- Modal body content
#gplus-account-exists-row, #facebook-account-exists-row
margin-bottom: 20px
#facebook-signup-btn
margin-bottom: 5px
.auth-form-content
position: absolute
top: 100px
left: 40px
width: 588px
.help-block
margin: 0
.alert
margin-top: -25px
margin-bottom: 0
padding: 10px 15px
.form-group
color: rgb(51,51,51)
padding: 0
margin: 0
.input-border
border: 2px solid rgb(233, 221, 194)
border-radius: 4px
margin-bottom: 7px
input, select
background-color: rgb(239, 232, 216)
border: 2px solid rgb(26, 21, 18)
border-radius: 4px
select
margin-right: 2px
label
font-size: 20px
text-transform: uppercase
font-family: $headings-font-family
margin-bottom: 0
.optional-note
font-size: 14px
.well
font-size: 18px
position: absolute
right: 0
bottom: 0
width: 278px
margin-bottom: 0
//- Check boxes
.form-group.checkbox
margin: 10px 0
label
position: relative
line-height: 34px
span:not(.custom-checkbox)
margin-left: 40px
input
display: none
& + .custom-checkbox
.glyphicon
display: none
&:checked + .custom-checkbox .glyphicon
display: inline
color: rgb(248,169,67)
text-align: center
text-shadow: 0 0 3px black, 0 0 3px black, 0 0 3px black
font-size: 20px
position: relative
top: -2px
.input-border
border-radius: 4px
height: 34px
width: 34px
position: absolute
.custom-checkbox
border-radius: 4px
position: absolute
height: 30px
width: 30px
border: 2px solid rgb(26,21,18)
background: rgb(228,217,196)
text-align: center
//- Primary auth button
#signup-button
position: absolute
top: 405px
height: 70px
font-size: 32px
line-height: 42px
//- Footer area
.auth-network-logins
.btn.btn-lg.network-login
width: 251px
height: 60px
text-align: center
position: relative
.network-logo
height: 30px
position: absolute
left: -10px
top: 2px
.sign-in-blurb
line-height: 34px
margin-left: 12px
.fb-login-button
$scaleX: 251 / 64
$scaleY: 60 / 23
transform: scale($scaleX, $scaleY)
position: absolute
top: 4px
left: 74px
@include opacity(0.01)
.gplus-login-wrapper
position: absolute
left: 65px
top: -6px
$scaleX: 251 / 84
$scaleY: 60 / 24
transform: scale($scaleX, $scaleY)
@include opacity(0.01)
#github-login-button
position: relative
top: -1px
border-radius: 5px
img
width: 16px
margin: 0 5px 0 -5px
#gplus-login-button
position: relative
top: 8px
//- Extra bottom pane area
.extra-pane
background-image: url(/images/pages/modal/auth/extra-pane.png)
width: 633px
height: 139px
padding: 23px 23px 23px 168px
position: absolute
top: 580px
.switch-explanation
margin: 25px 10px 0 0
width: 200px
color: rgb(254,188,68)
font-size: 20px
font-family: $headings-font-family
font-weight: bold
text-transform: uppercase
text-shadow: black 1px 1px 0, black -1px -1px 0, black 1px -1px 0, black -1px 1px 0, black 1px 0px 0, black 0px -1px 0, black -1px 0px 0, black 0px 1px 0
float: left
.btn
float: right
margin-top: 20px
width: 230px
height: 70px
line-height: 40px
.ie10 #create-account-modal .auth-network-logins .btn.btn-lg.network-login .network-logo, .lt-ie10 #create-account-modal .auth-network-logins .btn.btn-lg.network-login .network-logo
left: 15px
top: 15px

View file

@ -0,0 +1,22 @@
#basic-info-view
.network-login
width: 175px
height: 40px
border: solid 0.5px darkgray
span
visibility: hidden
// Forms
.basic-info
display: flex
flex-direction: column
.form-group
width: 255px
text-align: left
.btn-illustrated img
// Undo previous opacity-toggling hover behavior
opacity: 1

View file

@ -0,0 +1,68 @@
@import "app/styles/style-flat-variables"
#choose-account-type-view
.path-cards
margin-top: 15px
display: flex
.path-card ~ .path-card
margin-left: 6.5px
.path-card
display: flex
flex-direction: column
justify-content: space-between
width: 235px
min-height: 340px
border-style: solid
border-width: thin
border-radius: 5px
&.navy
border-color: $navy
.card-title
background-color: $navy
&.forest
border-color: $forest
.card-title
background-color: $forest
.card-title
display: flex
flex-direction: column
align-items: center
justify-content: center
height: 50px
color: white
font-weight: bold
text-align: center
.card-content
flex-grow: 1
display: flex
flex-direction: column
margin: 50px 20px 0
ul
align-self: center
text-align: left
padding-left: 20px
li
span
position: relative
left: -5px
.card-footer
margin: 20px
.individual-section
margin-top: 50px
max-width: 425px
.individual-title
font-weight: bold
.text-h6
color: white

View file

@ -0,0 +1,17 @@
#coppa-deny-view
.parent-email-blurb
width: 500px
.parent-email-input-group
display: flex
flex-direction: row
align-items: center
text-align: center
.glyphicon
width: 0
line-height: 40px
font-size: 30px
.error
color: red

View file

@ -0,0 +1,152 @@
//
Layout for CreateAccountModal is done largely with flexboxes.
Rules in this file should be ones that apply to all screens/subviews, but this
separation may not be perfect.
Currently it uses .modal-dialog, .modal-content, etc, for some parts of the modal.
Unfortunately those preexesting classes don't line up perfectly with the needs of this modal,
so many of the other styles for those classes don't apply or are overridden.
@import "app/styles/style-flat-variables"
#create-account-modal
.modal-dialog
width: 850px
.modal-content
display: flex
flex-direction: column
height: 850px
width: 850px
text-align: center
padding: 0
border: none
// General modal stuff
.modal-header, .modal-footer
&.teacher
background-color: $burgandy
&.student
background-color: $forest
.modal-header
display: flex
flex-direction: column
align-items: center
justify-content: flex-end
height: 100px
padding: 0
background-color: $navy
h3
color: white
.modal-footer
padding: 0
margin: 0
height: 50px
display: flex
align-items: center
justify-content: center
background-color: $navy
span
color: white
#choose-account-type-view, #segment-check-view, #basic-info-view, #coppa-deny-view, #single-sign-on-already-exists-view, #single-sign-on-confirm-view,
display: flex
flex-direction: column
flex-grow: 1
.modal-body
display: flex
flex-direction: column
flex-grow: 1
.modal-body-content
flex-grow: 2
display: flex
flex-direction: column
align-items: center
justify-content: center
// Back/forward buttons
.history-nav-buttons
width: 100%
display: flex
flex-direction: row-reverse
flex-grow: 1
align-items: flex-end
justify-content: space-between
.btn
// Undo .style-flat's .btn ~ .btn margin
margin: 0
&.just-one
flex-direction: row
// Forms
.full-name
display: flex
flex-direction: row
.form-group
display: flex
flex-direction: column
align-content: flex-start
&.text-center
text-align: center
input
height: 40px
&.row
display: block
&.last-initial
margin-left: 30px
width: auto
input
width: 70px
&.subscribe
width: 100%
// Fancy text inside horizontal rules
.hr-text
position: relative
hr
width: 430px
padding: 0
border: none
border-top: thin solid #444
color: #444
span
position: absolute
left: 50%
top: 0.45em
transform: translateX(-50%)
padding: 0 0.75em
font-weight: bold
font-size: 10pt
background: white
// Glyphicon colors
.glyphicon-ok-circle
color: green
.glyphicon-remove-circle
color: red

View file

@ -0,0 +1,18 @@
#segment-check-view
.class-code-input-group
display: flex
flex-direction: row
align-items: center
.class-code-input
text-align: center
.glyphicon
width: 0
line-height: 40px
font-size: 30px
.classroom-name
font-size: 26pt
.teacher-name
font-size: 14pt

View file

@ -0,0 +1,5 @@
#single-sign-on-already-exists-view
.modal-body
display: flex
flex-direction: column
align-items: center

View file

@ -0,0 +1,5 @@
#single-sign-on-confirm-view
.modal-body
display: flex
flex-direction: column
align-items: center

View file

@ -1,11 +0,0 @@
extends /templates/core/modal-base-flat
block modal-header-content
block modal-body-content
p
h2(data-i18n="coppa_deny.text1")
div(data-i18n="coppa_deny.text2")
block modal-footer-content
a.btn.btn-primary(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="coppa_deny.close").btn

View file

@ -1,118 +0,0 @@
.modal-dialog
.modal-content
img(src="/images/pages/modal/auth/signup-background.png", draggable="false").auth-modal-background
h1(data-i18n="login.sign_up")
div#close-modal
span.glyphicon.glyphicon-remove
form.auth-form-content
if view.options.showRequiredError
#required-error-alert.alert.alert-success
span(data-i18n="signup.required")
if view.options.showSignupRationale
#signup-rationale-alert.alert.alert-info
span(data-i18n="play_level.victory_sign_up_poke")
#email-password-row.row
.col-md-6
.form-group
label.control-label(for="email")
span(data-i18n="general.email")
| :
.input-border
input#email.input-large.form-control(name="email", type="email", value=view.previousFormInputs.email, tabindex=1)
.form-group
label.control-label(for="password")
span(data-i18n="general.password")
| :
.input-border
input#password.input-large.form-control(name="password", type="password", value=view.previousFormInputs.password, tabindex=2)
.col-md-6
.auth-network-logins.text-center
strong(data-i18n="signup.or_sign_up_with")
button#facebook-signup-btn.btn.btn-primary.btn-lg.btn-illustrated.network-login(type="button", disabled=true)
img.network-logo(src="/images/pages/community/logo_facebook.png", draggable="false")
span.sign-in-blurb(data-i18n="login.sign_in_with_facebook")
button#gplus-signup-btn.btn.btn-danger.btn-lg.btn-illustrated.network-login(type="button", disabled=true)
img.network-logo(src="/images/pages/community/logo_g+.png", draggable="false")
span.sign-in-blurb(data-i18n="login.sign_in_with_gplus")
.gplus-login-wrapper
.gplus-login-button
#gplus-logged-in-row.row.text-center.hide
h2(data-i18n="signup.connected_gplus_header")
p(data-i18n="signup.connected_gplus_p")
#gplus-account-exists-row.row.text-center.hide
h2(data-i18n="signup.gplus_exists")
a.btn.btn-primary#gplus-login-btn(data-i18n="login.log_in")
#facebook-logged-in-row.row.text-center.hide
h2(data-i18n="signup.connected_facebook_header")
p(data-i18n="signup.connected_facebook_p")
#facebook-account-exists-row.row.text-center.hide
h2(data-i18n="signup.facebook_exists")
a.btn.btn-primary#facebook-login-btn(data-i18n="login.log_in")
.row
.col-md-6
.form-group
label.control-label(for="name")
span(data-i18n="general.username")
| :
.input-border
if me.get('name')
input#name.input-large.form-control(name="name", type="text", value=me.get('name'), tabindex=3)
else
input#name.input-large.form-control(name="name", type="text", value="", placeholder="e.g. Alex W the Skater", tabindex=3)
.form-group
label.control-label(for="birthday-input")
span(data-i18n="signup.birthday")
| :
.input-border
select#birthday-month-input.input-large.form-control(name="birthdayMonth", value=view.previousFormInputs.birthdayMonth || '', tabindex=4, style="width: 106px; float: left")
option(value='',data-i18n="calendar.month")
for name, val in ['january','february','march','april','may','june','july','august','september','october','november','december']
option(data-i18n="calendar.#{name}" value=val+1)
select#birthday-day-input.input-large.form-control(name="birthdayDay", value=view.previousFormInputs.birthdayDay || '', tabindex=5, style="width: 75px; float: left")
option(value='',data-i18n="calendar.day")
for dummy, val in new Array(31)
option #{val+1}
select#birthday-year-input.input-large.form-control(name="birthdayYear", value=view.previousFormInputs.birthdayMonth || '', tabindex=6, style="width: 90px;")
option(value='',data-i18n="calendar.year")
- var startYear = new Date().getFullYear() - 1
for dummy, val in new Array(100)
option #{startYear-val}
.form-group
label.control-label(for="school-input")
span.spr(data-i18n="courses.class_code")
em.optional-note
| (
span(data-i18n="signup.optional")
| ):
.input-border
input#class-code-input.input-large.form-control(name="classCode", value=view.previousFormInputs.classCode || '', tabindex=7)
.col-md-6
.form-group.checkbox
label.control-label(for="subscribe")
.input-border
input#subscribe(type="checkbox", checked='checked')
span.custom-checkbox
.glyphicon.glyphicon-ok
span(data-i18n="signup.email_announcements")
.well(data-i18n="signup.hey_students")
input#signup-button.btn.btn-lg.btn-illustrated.btn-block.btn-success(value=translate("signup.sign_up"), type="submit")
.extra-pane
.switch-explanation(data-i18n="signup.login_switch")
.btn.btn-lg.btn-illustrated.btn-warning#switch-to-login-btn(data-i18n="login.log_in")

View file

@ -0,0 +1,67 @@
form#basic-info-form.modal-body.basic-info
.modal-body-content
.auth-network-logins.text-center
h4
span(data-i18n="signup.connect_with")
a#facebook-signup-btn.btn.btn-primary.btn-lg.btn-illustrated.network-login(disabled=!view.sharedState.get('facebookEnabled'), data-sso-used="facebook")
img.network-logo(src="/images/pages/modal/auth/facebook_sso_button.png", draggable="false", width="175", height="40")
span.sign-in-blurb(data-i18n="login.sign_in_with_facebook")
a#gplus-signup-btn.btn.btn-danger.btn-lg.btn-illustrated.network-login(disabled=!view.sharedState.get('gplusEnabled'), data-sso-used="gplus")
img.network-logo(src="/images/pages/modal/auth/google_plus_sso_button.png", draggable="false", width="175", height="40")
span.sign-in-blurb(data-i18n="login.sign_in_with_gplus")
.gplus-login-wrapper
.gplus-login-button
.hr-text
hr
span(data-i18n="TODO")
| or
div
if ['student', 'teacher'].indexOf(view.sharedState.get('path')) !== -1
.row.full-name
.form-group
label.control-label(for="first-name-input")
span(data-i18n="TODO")
| First name:
input#first-name-input(name="firstName")
.last-initial.form-group
label.control-label(for="last-name-input")
span(data-i18n="TODO")
| Last initial:
input#last-name-input(name="lastName" maxlength="1")
.row
.form-group
label.control-label(for="email-input")
span(data-i18n="TODO")
| Email address:
input#email-input(name="email" type="email")
.row
//- This form group needs a parent so its errors can be cleared individually
.form-group
label.control-label(for="username-input")
span(data-i18n="TODO")
| Username:
input#username-input(name="name")
.row
.form-group
label.control-label(for="password-input")
span(data-i18n="TODO")
| Password:
input#password-input(name="password" type="password")
.row
.form-group.checkbox.subscribe
label.control-label(for="subscribe-input")
input#subscribe-input(type="checkbox" checked="checked" name="subscribe")
span.small(data-i18n="TODO")
| Receive announcements about CodeCombat
// In reverse order for tabbing purposes
.history-nav-buttons
button.next-button.btn.btn-lg.btn-navy(type='submit')
span(data-i18n="TODO")
| Create Account
button.back-button.btn.btn-lg.btn-navy-alt(type='button')
span(data-i18n="TODO")
| Back

View file

@ -0,0 +1,67 @@
.modal-body-content
h4
span(data-i18n="TODO")
| Choose your account type:
.path-cards
.path-card.navy
.card-title
span(data-i18n="TODO")
| Teacher
.card-content
h6.card-description
span(data-i18n="TODO")
| Teach programming using CodeCombat!
ul.small.m-t-1
li
span(data-i18n="TODO")
| Create/manage classes
li
span(data-i18n="TODO")
| Access course guides
li
span(data-i18n="TODO")
| View student progress
.card-footer
button.btn.btn-lg.btn-navy.teacher-path-button
.text-h6
span(data-i18n="TODO")
| Sign up as a Teacher
.path-card.forest
.card-title
span(data-i18n="TODO")
| Student
.card-content
h6.card-description
span(data-i18n="TODO")
| Learn to program while playing an engaging game!
ul.small.m-t-1
li
span(data-i18n="TODO")
| Join a classroom
li
span(data-i18n="TODO")
| Play assigned Courses
li
span(data-i18n="TODO")
| Compete in arenas
.card-footer
i.small
span(data-i18n="TODO")
| Have your class code ready!
button.btn.btn-lg.btn-forest.student-path-button
.text-h6
span(data-i18n="TODO")
| Sign up as a Student
.individual-section
.individual-title
span(data-i18n="TODO")
| Individual
p.individual-description.small
span(data-i18n="TODO")
| Learn to program at your own pace! For players who don't have a class code and want to save their progress. Also parents!
button.btn.btn-lg.btn-navy.individual-path-button
.text-h6
span(data-i18n="TODO")
| I'm an Individual

View file

@ -0,0 +1,31 @@
form.modal-body.coppa-deny
.modal-body-content
.parent-email-input-group.form-group
label.control-label.text-h4(for="parent-email-input")
span(data-i18n="TODO")
| Enter your parent's email address:
input#parent-email-input(type="email" name="parentEmail" value=state.get('parentEmail'))
.render
if state.get('error')
p.small.error
span(data-i18n="TODO")
| Something went wrong when trying to send the email. Check the email address and try again.
p.small.parent-email-blurb.render
span
!= translate('signup.parent_email_blurb').replace('{{email_link}}', '<a href="mailto:team@codecombat.com">team@codecombat.com</a>')
// In reverse order for tabbing purposes
.history-nav-buttons.render
button.send-parent-email-button.btn.btn-lg.btn-navy(type='submit', disabled=state.get('parentEmailSent') || state.get('parentEmailSending'))
if state.get('parentEmailSent')
span(data-i18n="TODO")
| Sent
else
span(data-i18n="TODO")
| Send
button.back-to-segment-check.btn.btn-lg.btn-navy-alt(type='button')
span(data-i18n="TODO")
| Back

View file

@ -0,0 +1,63 @@
extends /templates/core/modal-base-flat
block modal-header
//-
This allows for the header color to switch without the subview templates
needing to contain the header
.modal-header(class=state.get('path'))
+modal-header-content
mixin modal-header-content
h3
case state.get('path')
when 'student'
span(data-i18n="TODO")
| Create Student Account
when 'teacher'
span(data-i18n="TODO")
| Create Teacher Account
when 'individual'
span(data-i18n="TODO")
| Create Individual Account
default
span(data-i18n="TODO")
| Create Account
//-
This is where the subviews (screens) are hooked up.
Most subview templates have a .modal-body at their root, but this is inconsistent and needs organization.
block modal-body
case state.get('screen')
when 'choose-account-type'
#choose-account-type-view
when 'segment-check'
#segment-check-view
when 'basic-info'
#basic-info-view
when 'coppa-deny'
#coppa-deny-view
when 'sso-already-exists'
#single-sign-on-already-exists-view
when 'sso-confirm'
#single-sign-on-confirm-view
//- These are not yet implemented
//- when 'extras'
//- #extras-view
//- when 'confirmation'
//- #confirmation-view
block modal-footer
//-
This allows for the footer color to switch without the subview templates
needing to contain the footer
.modal-footer(class=state.get('path'))
+modal-footer-content
mixin modal-footer-content
.modal-footer-content
.small-details
span.spr(data-i18n="TODO")
| Already have an account?
a.login-link
span(data-i18n="TODO")
| Sign in

View file

@ -0,0 +1,76 @@
form.modal-body.segment-check
.modal-body-content
case view.sharedState.get('path')
when 'student'
span(data-i18n="TODO")
| Enter your class code:
.class-code-input-group.form-group
input.class-code-input(name="classCode" value=view.sharedState.get('classCode'))
.render
unless _.isEmpty(view.sharedState.get('classCode'))
if state.get('classCodeValid')
span.glyphicon.glyphicon-ok-circle.class-code-valid-icon
else
span.glyphicon.glyphicon-remove-circle.class-code-valid-icon
p.render
if _.isEmpty(view.sharedState.get('classCode'))
span(data-i18n="TODO")
| Ask your teacher for your class code.
else if state.get('classCodeValid')
span.small(data-i18n="TODO")
| You're about to join:
br
span.classroom-name= view.classroom.get('name')
br
span.teacher-name= view.classroom.owner.get('name')
else
span(data-i18n="TODO")
| This class code doesn't exist! Check your spelling or ask your teacher for help.
if _.isEmpty(view.sharedState.get('classCode')) || !state.get('classCodeValid')
br
span.spr(data-i18n="TODO")
| Don't have a class code? Create an
a.individual-path-button
span(data-i18n="TODO")
| Individual Account
span.spl(data-i18n="TODO")
| instead.
when 'teacher'
// TODO
when 'individual'
.birthday-form-group.form-group
span(data-i18n="TODO")
| Enter your birthdate:
.input-border
select#birthday-month-input.input-large.form-control(name="birthdayMonth", style="width: 106px; float: left")
option(value='',data-i18n="calendar.month")
for name, index in ['january','february','march','april','may','june','july','august','september','october','november','december']
- var month = index + 1
option(data-i18n="calendar.#{name}" value=month, selected=(month == view.sharedState.get('birthdayMonth')))
select#birthday-day-input.input-large.form-control(name="birthdayDay", style="width: 75px; float: left")
option(value='',data-i18n="calendar.day")
for day in _.range(1,32)
option(selected=(day == view.sharedState.get('birthdayDay'))) #{day}
select#birthday-year-input.input-large.form-control(name="birthdayYear", style="width: 90px;")
option(value='',data-i18n="calendar.year")
- var thisYear = new Date().getFullYear()
for year in _.range(thisYear, thisYear - 100, -1)
option(selected=(year == view.sharedState.get('birthdayYear'))) #{year}
default
span
| Something went wrong :(
// In reverse order for tabbing purposes
.history-nav-buttons
//- disabled=!view.sharedState.get('segmentCheckValid')
button.next-button.btn.btn-lg.btn-navy(type='submit')
span(data-i18n="TODO")
| Next
button.back-to-account-type.btn.btn-lg.btn-navy-alt(type='button')
span(data-i18n="TODO")
| Back

View file

@ -0,0 +1,22 @@
.modal-body
.modal-body-content
if view.sharedState.get('ssoUsed')
h4
span(data-i18n="TODO")
| This email is already in use:
div.small
b
span= view.sharedState.get('email')
.hr-text
hr
span(data-i18n="TODO")
| continue
button.sso-login-btn.btn.btn-lg.btn-navy
span(data-i18n="login.log_in")
.history-nav-buttons.just-one
button.back-button.btn.btn-lg.btn-navy-alt(type='button')
span(data-i18n="TODO")
| Back

View file

@ -0,0 +1,40 @@
form#basic-info-form.modal-body
.modal-body-content
h4
span(data-i18n="TODO")
| Connect with:
div.small
b
span= view.sharedState.get('email')
span.glyphicon.glyphicon-ok-circle.class-code-valid-icon
.hr-text
hr
span(data-i18n="TODO")
| continue
div
input.hidden(name="email" value=view.sharedState.get('email'))
div
//- This form group needs a parent so its errors can be cleared individually
.form-group
h4
span(data-i18n="TODO")
| Pick a username:
input(name="name" value=view.sharedState.get('name'))
.form-group.checkbox.subscribe
label.control-label(for="subscribe-input")
input#subscribe-input(type="checkbox" checked="checked" name="subscribe")
span.small(data-i18n="TODO")
| Receive announcements about CodeCombat
// In reverse order for tabbing purposes
.history-nav-buttons
button.next-button.btn.btn-lg.btn-navy(type='submit')
span(data-i18n="TODO")
| Create Account
button.back-button.btn.btn-lg.btn-navy-alt(type='button')
span(data-i18n="TODO")
| Back

View file

@ -110,6 +110,7 @@ module.exports = class NewHomeView extends RootView
$(window).on 'resize', @fitToPage
@fitToPage()
setTimeout(@fitToPage, 0)
@$('#create-account-link').click()
super()
destroy: ->

View file

@ -22,6 +22,7 @@ module.exports = class AuthModal extends ModalView
initialize: (options={}) ->
@previousFormInputs = options.initialValues or {}
@previousFormInputs.emailOrUsername ?= @previousFormInputs.email or @previousFormInputs.username
# TODO: Switch to promises and state, rather than using defer to hackily enable buttons after render
application.gplusHandler.loadAPI({ success: => _.defer => @$('#gplus-login-btn').attr('disabled', false) })
@ -96,7 +97,7 @@ module.exports = class AuthModal extends ModalView
btn = @$('#gplus-login-btn')
btn.find('.sign-in-blurb').text($.i18n.t('login.sign_in_with_gplus'))
btn.attr('disabled', false)
errors.showNotyNetworkError(arguments...)
errors.showNotyNetworkError(arguments...)
# Facebook

View file

@ -1,12 +0,0 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/core/coppa-deny'
module.exports = class COPPADenyModal extends ModalView
id: 'coppa-deny-modal'
template: template
closeButton: true
constructor: ->
super()
window.tracker?.trackEvent 'COPPA Message Shown', category: 'Homepage'

View file

@ -100,8 +100,10 @@ module.exports = class CocoView extends Backbone.View
renderSelectors: (selectors...) ->
newTemplate = $(@template(@getRenderData()))
console.log newTemplate.find('p.render')
for selector, i in selectors
for elPair in _.zip(@$el.find(selector), newTemplate.find(selector))
console.log elPair
$(elPair[0]).replaceWith($(elPair[1]))
@delegateEvents()
@$el.i18n()

View file

@ -1,259 +0,0 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/core/create-account-modal'
forms = require 'core/forms'
User = require 'models/User'
application = require 'core/application'
Classroom = require 'models/Classroom'
errors = require 'core/errors'
COPPADenyModal = require 'views/core/COPPADenyModal'
utils = require 'core/utils'
module.exports = class CreateAccountModal extends ModalView
id: 'create-account-modal'
template: template
events:
'submit form': 'onSubmitForm'
'keyup #name': 'onNameChange'
'click #gplus-signup-btn': 'onClickGPlusSignupButton'
'click #gplus-login-btn': 'onClickGPlusLoginButton'
'click #facebook-signup-btn': 'onClickFacebookSignupButton'
'click #facebook-login-btn': 'onClickFacebookLoginButton'
'click #close-modal': 'hide'
'click #switch-to-login-btn': 'onClickSwitchToLoginButton'
# Initialization
initialize: (options={}) ->
@onNameChange = _.debounce(_.bind(@checkNameExists, @), 500)
options.initialValues ?= {}
options.initialValues?.classCode ?= utils.getQueryVariable('_cc', "")
@previousFormInputs = options.initialValues or {}
# TODO: Switch to promises and state, rather than using defer to hackily enable buttons after render
application.gplusHandler.loadAPI({ success: => _.defer => @$('#gplus-signup-btn').attr('disabled', false) })
application.facebookHandler.loadAPI({ success: => _.defer => @$('#facebook-signup-btn').attr('disabled', false) })
afterRender: ->
super()
@playSound 'game-menu-open'
afterInsert: ->
super()
_.delay (=> $('input:visible:first', @$el).focus()), 500
# User creation
onSubmitForm: (e) ->
e.preventDefault()
@playSound 'menu-button-click'
forms.clearFormAlerts(@$el)
attrs = forms.formToObject @$el
attrs.name = @suggestedName if @suggestedName
_.defaults attrs, me.pick([
'preferredLanguage', 'testGroupNumber', 'dateCreated', 'wizardColor1',
'name', 'music', 'volume', 'emails', 'schoolName'
])
attrs.emails ?= {}
attrs.emails.generalNews ?= {}
attrs.emails.generalNews.enabled = @$el.find('#subscribe').prop('checked')
@classCode = attrs.classCode
delete attrs.classCode
error = false
birthday = new Date Date.UTC attrs.birthdayYear, attrs.birthdayMonth - 1, attrs.birthdayDay
if @classCode
attrs.role = 'student'
else if isNaN(birthday.getTime())
forms.setErrorToProperty @$el, 'birthdayDay', 'Required'
error = true
else
age = (new Date().getTime() - birthday.getTime()) / 365.4 / 24 / 60 / 60 / 1000
attrs.birthday = birthday.toISOString()
delete attrs.birthdayYear
delete attrs.birthdayMonth
delete attrs.birthdayDay
_.assign attrs, @gplusAttrs if @gplusAttrs
_.assign attrs, @facebookAttrs if @facebookAttrs
res = tv4.validateMultiple attrs, User.schema
if not res.valid
forms.applyErrorsToForm(@$el, res.errors)
error = true
if not _.any([attrs.password, @gplusAttrs, @facebookAttrs])
forms.setErrorToProperty @$el, 'password', 'Required'
error = true
if not forms.validateEmail(attrs.email)
forms.setErrorToProperty @$el, 'email', 'Please enter a valid email address'
error = true
return if error
@$('#signup-button').text($.i18n.t('signup.creating')).attr('disabled', true)
@newUser = new User(attrs)
if @classCode
@signupClassroomPrecheck()
else
if age < 13
@openModalView new COPPADenyModal
else
@createUser()
signupClassroomPrecheck: ->
classroom = new Classroom()
classroom.fetch({ data: { code: @classCode } })
classroom.once 'sync', @createUser, @
classroom.once 'error', @onClassroomFetchError, @
onClassroomFetchError: ->
@$('#signup-button').text($.i18n.t('signup.sign_up')).attr('disabled', false)
forms.setErrorToProperty(@$el, 'classCode', "#{@classCode} is not a valid code. Please verify the code is typed correctly.")
@$('#class-code-input').val('')
createUser: ->
options = {}
window.tracker?.identify()
if @gplusAttrs
@newUser.set('_id', me.id)
options.url = "/db/user?gplusID=#{@gplusAttrs.gplusID}&gplusAccessToken=#{application.gplusHandler.accessToken.access_token}"
options.type = 'PUT'
if @facebookAttrs
@newUser.set('_id', me.id)
options.url = "/db/user?facebookID=#{@facebookAttrs.facebookID}&facebookAccessToken=#{application.facebookHandler.authResponse.accessToken}"
options.type = 'PUT'
@newUser.save(null, options)
@newUser.once 'sync', @onUserCreated, @
@newUser.once 'error', @onUserSaveError, @
onUserSaveError: (user, jqxhr) ->
@$('#signup-button').text($.i18n.t('signup.sign_up')).attr('disabled', false)
if _.isObject(jqxhr.responseJSON) and jqxhr.responseJSON.property
error = jqxhr.responseJSON
if jqxhr.status is 409 and error.property is 'name'
@newUser.unset 'name'
return @createUser()
return forms.applyErrorsToForm(@$el, [jqxhr.responseJSON])
errors.showNotyNetworkError(jqxhr)
onUserCreated: ->
Backbone.Mediator.publish "auth:signed-up", {}
if @gplusAttrs
window.tracker?.trackEvent 'Google Login', category: "Signup", label: 'GPlus'
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'GPlus'
else if @facebookAttrs
window.tracker?.trackEvent 'Facebook Login', category: "Signup", label: 'Facebook'
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'Facebook'
else
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'CodeCombat'
if @classCode
url = "/courses?_cc="+@classCode
location.href = url
else
window.location.reload()
# Google Plus
onClickGPlusSignupButton: ->
btn = @$('#gplus-signup-btn')
application.gplusHandler.connect({
context: @
success: ->
btn.find('.sign-in-blurb').text($.i18n.t('signup.creating'))
btn.attr('disabled', true)
application.gplusHandler.loadPerson({
context: @
success: (@gplusAttrs) ->
existingUser = new User()
existingUser.fetchGPlusUser(@gplusAttrs.gplusID, {
context: @
complete: ->
@$('#email-password-row').remove()
success: =>
@$('#gplus-account-exists-row').removeClass('hide')
error: (user, jqxhr) =>
if jqxhr.status is 404
@$('#gplus-logged-in-row').toggleClass('hide')
else
errors.showNotyNetworkError(jqxhr)
})
})
})
onClickGPlusLoginButton: ->
me.loginGPlusUser(@gplusAttrs.gplusID, {
context: @
success: -> window.location.reload()
error: ->
@$('#gplus-login-btn').text($.i18n.t('login.log_in')).attr('disabled', false)
errors.showNotyNetworkError(arguments...)
})
@$('#gplus-login-btn').text($.i18n.t('login.logging_in')).attr('disabled', true)
# Facebook
onClickFacebookSignupButton: ->
btn = @$('#facebook-signup-btn')
application.facebookHandler.connect({
context: @
success: ->
btn.find('.sign-in-blurb').text($.i18n.t('signup.creating'))
btn.attr('disabled', true)
application.facebookHandler.loadPerson({
context: @
success: (@facebookAttrs) ->
existingUser = new User()
existingUser.fetchFacebookUser(@facebookAttrs.facebookID, {
context: @
complete: ->
@$('#email-password-row').remove()
success: =>
@$('#facebook-account-exists-row').removeClass('hide')
error: (user, jqxhr) =>
if jqxhr.status is 404
@$('#facebook-logged-in-row').toggleClass('hide')
else
errors.showNotyNetworkError(jqxhr)
})
})
})
onClickFacebookLoginButton: ->
me.loginFacebookUser(@facebookAttrs.facebookID, {
context: @
success: -> window.location.reload()
error: =>
@$('#facebook-login-btn').text($.i18n.t('login.log_in')).attr('disabled', false)
errors.showNotyNetworkError(jqxhr)
})
@$('#facebook-login-btn').text($.i18n.t('login.logging_in')).attr('disabled', true)
# Misc
onHidden: ->
super()
@playSound 'game-menu-close'
checkNameExists: ->
name = $('#name', @$el).val()
return forms.clearFormAlerts(@$el) if name is ''
User.getUnconflictedName name, (newName) =>
forms.clearFormAlerts(@$el)
if name is newName
@suggestedName = undefined
else
@suggestedName = newName
forms.setErrorToProperty @$el, 'name', "That name is taken! How about #{newName}?"
onClickSwitchToLoginButton: ->
AuthModal = require('./AuthModal')
modal = new AuthModal({initialValues: forms.formToObject @$el})
currentView.openModalView(modal)

View file

@ -0,0 +1,206 @@
ModalView = require 'views/core/ModalView'
AuthModal = require 'views/core/AuthModal'
template = require 'templates/core/create-account-modal/basic-info-view'
forms = require 'core/forms'
errors = require 'core/errors'
User = require 'models/User'
State = require 'models/State'
###
This view handles the primary form for user details name, email, password, etc,
and the AJAX that actually creates the user.
It also handles facebook/g+ login, which if used, open one of two other screens:
sso-already-exists: If the facebook/g+ connection is already associated with a user, they're given a log in button
sso-confirm: If this is a new facebook/g+ connection, ask for a username, then allow creation of a user
The sso-confirm view *inherits from this view* in order to share its account-creation logic and events.
This means the selectors used in these events must work in both templates.
This view currently uses the old form API instead of stateful render.
It needs some work to make error UX and rendering better, but is functional.
###
module.exports = class BasicInfoView extends ModalView
id: 'basic-info-view'
template: template
events:
'input input[name="name"]': 'onInputName'
'click .back-button': 'onClickBackButton'
'submit form': 'onSubmitForm'
'click .use-suggested-name-link': 'onClickUseSuggestedNameLink'
'click #facebook-signup-btn': 'onClickSsoSignupButton'
'click #gplus-signup-btn': 'onClickSsoSignupButton'
initialize: ({ @sharedState } = {}) ->
@state = new State {
suggestedName: null
}
@onNameChange = _.debounce(_.bind(@checkNameUnique, @), 500)
@listenTo @sharedState, 'change:facebookEnabled', -> @renderSelectors('.auth-network-logins')
@listenTo @sharedState, 'change:gplusEnabled', -> @renderSelectors('.auth-network-logins')
checkNameUnique: ->
name = $('input[name="name"]', @$el).val()
return forms.clearFormAlerts(@$('input[name="name"]').closest('.form-group').parent()) if name is ''
@nameUniquePromise = new Promise((resolve, reject) => User.getUnconflictedName name, (newName) =>
if name is newName
@state.set { suggestedName: null }
@clearNameError()
resolve true
else
@state.set { suggestedName: newName }
@setNameError(newName)
resolve false
)
clearNameError: ->
forms.clearFormAlerts(@$('input[name="name"]').closest('.form-group').parent())
setNameError: (newName) ->
@clearNameError()
forms.setErrorToProperty @$el, 'name', "Username already taken!<br>Try <a class='use-suggested-name-link'>#{newName}</a>?" # TODO: Translate!
checkBasicInfo: (data) ->
# TODO: Move this to somewhere appropriate
tv4.addFormat({
'email': (email) ->
if forms.validateEmail(email)
return null
else
return {code: tv4.errorCodes.FORMAT_CUSTOM, message: "Please enter a valid email address."}
})
forms.clearFormAlerts(@$el)
res = tv4.validateMultiple data, @formSchema()
forms.applyErrorsToForm(@$('form'), res.errors) unless res.valid
return res.valid
formSchema: ->
type: 'object'
properties:
email: User.schema.properties.email
name: User.schema.properties.name
password: User.schema.properties.password
required: ['email', 'name', 'password'].concat (if @sharedState.get('path') is 'student' then ['firstName', 'lastName'] else [])
onClickBackButton: -> @trigger 'nav-back'
onInputName: ->
@nameUniquePromise = null
@onNameChange()
onClickUseSuggestedNameLink: (e) ->
@$('input[name="name"]').val(@state.get('suggestedName'))
forms.clearFormAlerts(@$el.find('input[name="name"]').closest('.form-group').parent())
onSubmitForm: (e) ->
e.preventDefault()
data = forms.formToObject(e.currentTarget)
valid = @checkBasicInfo(data)
# TODO: This promise logic is super weird and confusing. Rewrite.
@checkNameUnique() unless @nameUniquePromise
@nameUniquePromise.then ->
@nameUniquePromise = null
return unless valid
attrs = forms.formToObject @$el
_.defaults attrs, me.pick([
'preferredLanguage', 'testGroupNumber', 'dateCreated', 'wizardColor1',
'name', 'music', 'volume', 'emails', 'schoolName'
])
attrs.emails ?= {}
attrs.emails.generalNews ?= {}
attrs.emails.generalNews.enabled = (attrs.subscribe[0] is 'on')
delete attrs.subscribe
error = false
if @sharedState.get('birthday')
attrs.birthday = @sharedState.get('birthday').toISOString()
_.assign attrs, @sharedState.get('ssoAttrs') if @sharedState.get('ssoAttrs')
res = tv4.validateMultiple attrs, User.schema
@$('#signup-button').text($.i18n.t('signup.creating')).attr('disabled', true)
@newUser = new User(attrs)
@createUser()
createUser: ->
options = {}
window.tracker?.identify()
if @sharedState.get('ssoUsed') is 'gplus'
@newUser.set('_id', me.id)
options.url = "/db/user?gplusID=#{@sharedState.get('ssoAttrs').gplusID}&gplusAccessToken=#{application.gplusHandler.accessToken.access_token}"
options.type = 'PUT'
if @sharedState.get('ssoUsed') is 'facebook'
@newUser.set('_id', me.id)
options.url = "/db/user?facebookID=#{@sharedState.get('ssoAttrs').facebookID}&facebookAccessToken=#{application.facebookHandler.authResponse.accessToken}"
options.type = 'PUT'
@newUser.save(null, options)
@newUser.once 'sync', @onUserCreated, @
@newUser.once 'error', @onUserSaveError, @
onUserSaveError: (user, jqxhr) ->
# TODO: Do we need to enable/disable the submit button to prevent multiple users being created?
# Seems to work okay without that, but mongo had 2 copies of the user... temporarily. Very strange.
if _.isObject(jqxhr.responseJSON) and jqxhr.responseJSON.property
forms.applyErrorsToForm(@$el, [jqxhr.responseJSON])
@setNameError(@state.get('suggestedName'))
else
console.log "Error:", jqxhr.responseText
errors.showNotyNetworkError(jqxhr)
onUserCreated: ->
Backbone.Mediator.publish "auth:signed-up", {}
if @sharedState.get('gplusAttrs')
window.tracker?.trackEvent 'Google Login', category: "Signup", label: 'GPlus'
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'GPlus'
else if @sharedState.get('facebookAttrs')
window.tracker?.trackEvent 'Facebook Login', category: "Signup", label: 'Facebook'
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'Facebook'
else
window.tracker?.trackEvent 'Finished Signup', category: "Signup", label: 'CodeCombat'
if @sharedState.get('classCode')
url = "/courses?_cc="+@sharedState.get('classCode')
location.href = url
else
window.location.reload()
onClickSsoSignupButton: (e) ->
e.preventDefault()
ssoUsed = $(e.currentTarget).data('sso-used')
if ssoUsed is 'facebook'
handler = application.facebookHandler
fetchSsoUser = 'fetchFacebookUser'
idName = 'facebookID'
else
handler = application.gplusHandler
fetchSsoUser = 'fetchGPlusUser'
idName = 'gplusID'
handler.connect({
context: @
success: ->
handler.loadPerson({
context: @
success: (ssoAttrs) ->
@sharedState.set { ssoAttrs }
existingUser = new User()
existingUser[fetchSsoUser](@sharedState.get('ssoAttrs')[idName], {
context: @
success: =>
@sharedState.set {
ssoUsed
email: ssoAttrs.email
}
@trigger 'sso-connect:already-in-use'
error: (user, jqxhr) =>
@sharedState.set {
ssoUsed
email: ssoAttrs.email
}
@trigger 'sso-connect:new-user'
})
})
})

View file

@ -0,0 +1,11 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/core/create-account-modal/choose-account-type-view'
module.exports = class ChooseAccountTypeView extends ModalView
id: 'choose-account-type-view'
template: template
events:
'click .teacher-path-button': -> @trigger 'choose-path', 'teacher'
'click .student-path-button': -> @trigger 'choose-path', 'student'
'click .individual-path-button': -> @trigger 'choose-path', 'individual'

View file

@ -0,0 +1,32 @@
ModalView = require 'views/core/ModalView'
State = require 'models/State'
template = require 'templates/core/create-account-modal/coppa-deny-view'
forms = require 'core/forms'
module.exports = class SegmentCheckView extends ModalView
id: 'coppa-deny-view'
template: template
events:
'click .send-parent-email-button': 'onClickSendParentEmailButton'
'input input[name="parentEmail"]': 'onInputParentEmail'
initialize: ({ @sharedState } = {}) ->
@state = new State({ parentEmail: '' })
@listenTo @state, 'all', -> @renderSelectors('.render')
onInputParentEmail: (e) ->
@state.set { parentEmail: $(e.currentTarget).val() }, { silent: true }
onClickSendParentEmailButton: (e) ->
e.preventDefault()
@state.set({ parentEmailSending: true })
$.ajax('/send-parent-signup-instructions', {
method: 'POST'
data:
parentEmail: @state.get('parentEmail')
success: =>
@state.set({ error: false, parentEmailSent: true, parentEmailSending: false })
error: =>
@state.set({ error: true, parentEmailSent: false, parentEmailSending: false })
})

View file

@ -0,0 +1,115 @@
ModalView = require 'views/core/ModalView'
AuthModal = require 'views/core/AuthModal'
ChooseAccountTypeView = require 'views/core/CreateAccountModal/ChooseAccountTypeView'
SegmentCheckView = require 'views/core/CreateAccountModal/SegmentCheckView'
CoppaDenyView = require 'views/core/CreateAccountModal/CoppaDenyView'
BasicInfoView = require 'views/core/CreateAccountModal/BasicInfoView'
SingleSignOnAlreadyExistsView = require 'views/core/CreateAccountModal/SingleSignOnAlreadyExistsView'
SingleSignOnConfirmView = require 'views/core/CreateAccountModal/SingleSignOnConfirmView'
State = require 'models/State'
template = require 'templates/core/create-account-modal/create-account-modal'
forms = require 'core/forms'
User = require 'models/User'
application = require 'core/application'
errors = require 'core/errors'
utils = require 'core/utils'
###
CreateAccountModal is a wizard-style modal with several subviews, one for each
`screen` that the user navigates forward and back through.
There are three `path`s, one for each account type (individual, student).
Teacher account path will be added later; for now it defers to /teachers/signup)
Each subview handles only one `screen`, but all three `path` variants because
their logic is largely the same.
They `screen`s are:
choose-account-type: Sets the `path`.
segment-check: Checks required info for the path (age, )
coppa-deny: Seen if the indidual segment-check age is < 13 years old
basic-info: This is the form for username/password/email/etc.
It asks for whatever is needed for this type of user.
It also handles the actual user creation.
A user may create their account here, or connect with facebook/g+
sso-confirm: Alternate version of basic-info for new facebook/g+ users
sso-already-exists: When facebook/g+ user already exists, this prompts them to sign in.
extras: Not yet implemented
confirmation: Not yet implemented
NOTE: BasicInfoView's two children (SingleSignOn...View) inherit from it.
This allows them to have the same form-handling logic, but different templates.
###
module.exports = class CreateAccountModal extends ModalView
id: 'create-account-modal'
template: template
events:
'click .login-link': 'onClickLoginLink'
'click .back-to-segment-check': -> @state.set { screen: 'segment-check' }
initialize: (options={}) ->
classCode = utils.getQueryVariable('_cc', undefined)
@state = new State {
path: if classCode then 'student' else null
screen: if classCode then 'segment-check' else 'choose-account-type'
facebookEnabled: application.facebookHandler.apiLoaded
gplusEnabled: application.gplusHandler.apiLoaded
classCode
birthday: new Date('') # so that birthday.getTime() is NaN
}
@listenTo @state, 'all', @render #TODO: debounce
@customSubviews = {
choose_account_type: new ChooseAccountTypeView()
segment_check: new SegmentCheckView({ sharedState: @state })
coppa_deny_view: new CoppaDenyView({ sharedState: @state })
basic_info_view: new BasicInfoView({ sharedState: @state })
sso_already_exists: new SingleSignOnAlreadyExistsView({ sharedState: @state })
sso_confirm: new SingleSignOnConfirmView({ sharedState: @state })
}
@listenTo @customSubviews.choose_account_type, 'choose-path', (path) ->
if path is 'teacher'
application.router.navigate('/teachers/signup', trigger: true)
else
@state.set { path, screen: 'segment-check' }
@listenTo @customSubviews.segment_check, 'choose-path', (path) ->
@state.set { path, screen: 'segment-check' }
@listenTo @customSubviews.segment_check, 'nav-back', ->
@state.set { path: null, screen: 'choose-account-type' }
@listenTo @customSubviews.segment_check, 'nav-forward', (screen) ->
@state.set { screen: screen or 'basic-info' }
@listenTo @customSubviews.basic_info_view, 'sso-connect:already-in-use', ->
@state.set { screen: 'sso-already-exists' }
@listenTo @customSubviews.basic_info_view, 'sso-connect:new-user', ->
@state.set { screen: 'sso-confirm' }
@listenTo @customSubviews.basic_info_view, 'nav-back', ->
@state.set { screen: 'segment-check' }
@listenTo @customSubviews.sso_confirm, 'nav-back', ->
@state.set { screen: 'basic-info' }
@listenTo @customSubviews.sso_already_exists, 'nav-back', ->
@state.set { screen: 'basic-info' }
# options.initialValues ?= {}
# options.initialValues?.classCode ?= utils.getQueryVariable('_cc', "")
# @previousFormInputs = options.initialValues or {}
# TODO: Switch to promises and state, rather than using defer to hackily enable buttons after render
application.facebookHandler.loadAPI({ success: => @state.set { facebookEnabled: true } unless @destroyed })
application.gplusHandler.loadAPI({ success: => @state.set { gplusEnabled: true } unless @destroyed })
afterRender: ->
# @$el.html(@template(@getRenderData()))
for key, subview of @customSubviews
subview.setElement(@$('#' + subview.id))
subview.render()
onClickLoginLink: ->
# TODO: Make sure the right information makes its way into the state.
@openModalView(new AuthModal({ initialValues: @state.pick(['email', 'name', 'password']) }))

View file

@ -0,0 +1,63 @@
CocoView = require 'views/core/CocoView'
template = require 'templates/core/create-account-modal/segment-check-view'
forms = require 'core/forms'
Classroom = require 'models/Classroom'
State = require 'models/State'
module.exports = class SegmentCheckView extends CocoView
id: 'segment-check-view'
template: template
events:
'click .back-to-account-type': -> @trigger 'nav-back'
'input .class-code-input': 'onInputClassCode'
'input .birthday-form-group': 'onInputBirthday'
'submit form.segment-check': 'onSubmitSegmentCheck'
'click .individual-path-button': ->
@trigger 'choose-path', 'individual'
onInputClassCode: (e) ->
classCode = $(e.currentTarget).val()
@checkClassCodeDebounced(classCode)
@sharedState.set { classCode }, { silent: true }
onInputBirthday: ->
{ birthdayYear, birthdayMonth, birthdayDay } = forms.formToObject(@$('form'))
birthday = new Date Date.UTC(birthdayYear, birthdayMonth - 1, birthdayDay)
@sharedState.set { birthdayYear, birthdayMonth, birthdayDay, birthday }, { silent: true }
unless isNaN(birthday.getTime())
forms.clearFormAlerts(@$el)
onSubmitSegmentCheck: (e) ->
e.preventDefault()
if @sharedState.get('path') is 'student'
@trigger 'nav-forward' if @state.get('segmentCheckValid')
else if @sharedState.get('path') is 'individual'
if isNaN(@sharedState.get('birthday').getTime())
forms.clearFormAlerts(@$el)
forms.setErrorToProperty @$el, 'birthdayDay', 'Required'
else
age = (new Date().getTime() - @sharedState.get('birthday').getTime()) / 365.4 / 24 / 60 / 60 / 1000
if age > 13
@trigger 'nav-forward'
else
@trigger 'nav-forward', 'coppa-deny'
initialize: ({ @sharedState } = {}) ->
@checkClassCodeDebounced = _.debounce @checkClassCode, 1000
@state = new State()
@classroom = new Classroom()
if @sharedState.get('classCode')
@checkClassCode(@sharedState.get('classCode'))
@listenTo @state, 'all', -> @renderSelectors('.render')
checkClassCode: (classCode) ->
@classroom.clear()
return forms.clearFormAlerts(@$el) if classCode is ''
new Promise(@classroom.fetchByCode(classCode).then)
.then =>
@state.set { classCodeValid: true, segmentCheckValid: true }
.catch =>
@state.set { classCodeValid: false, segmentCheckValid: false }

View file

@ -0,0 +1,39 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/core/create-account-modal/single-sign-on-already-exists-view'
forms = require 'core/forms'
User = require 'models/User'
module.exports = class SingleSignOnAlreadyExistsView extends ModalView
id: 'single-sign-on-already-exists-view'
template: template
events:
'click .back-button': 'onClickBackButton'
'click .sso-login-btn': 'onClickSsoLoginButton'
initialize: ({ @sharedState } = {}) ->
onClickBackButton: ->
@state.set {
ssoUsed: undefined
ssoAttrs: undefined
}
-> @trigger 'nav-back'
onClickSsoLoginButton: ->
options = {
context: @
success: -> window.location.reload()
error: ->
@$('#gplus-login-btn').text($.i18n.t('login.log_in')).attr('disabled', false)
errors.showNotyNetworkError(arguments...)
}
if @sharedState.get('ssoUsed') is 'gplus'
me.loginGPlusUser(@sharedState.get('ssoAttrs').gplusID, options)
@$('#gplus-login-btn').text($.i18n.t('login.logging_in')).attr('disabled', true)
else if @sharedState.get('ssoUsed') is 'facebook'
me.loginFacebookUser(@sharedState.get('ssoAttrs').facebookID, options)
@$('#facebook-login-btn').text($.i18n.t('login.log_in')).attr('disabled', false)
else
console.log "Uh oh, we didn't record which SSO they used"

View file

@ -0,0 +1,31 @@
ModalView = require 'views/core/ModalView'
BasicInfoView = require 'views/core/CreateAccountModal/BasicInfoView'
template = require 'templates/core/create-account-modal/single-sign-on-confirm-view'
forms = require 'core/forms'
User = require 'models/User'
module.exports = class SingleSignOnConfirmView extends BasicInfoView
id: 'single-sign-on-confirm-view'
template: template
events: _.extend {}, BasicInfoView.prototype.events, {
'click .back-button': 'onClickBackButton'
}
initialize: ({ @sharedState } = {}) ->
super(arguments...)
onClickBackButton: ->
@sharedState.set {
ssoUsed: undefined
ssoAttrs: undefined
}
console.log @sharedState.attributes
@trigger 'nav-back'
formSchema: ->
type: 'object'
properties:
name: User.schema.properties.name
required: ['name']

View file

@ -0,0 +1 @@
module.exports = require 'views/core/CreateAccountModal/CreateAccountModal'

View file

@ -103,7 +103,7 @@ UserHandler = class UserHandler extends Handler
return callback(res: 'Facebook user login error.', code: 500) if err
return callback(null, req, otherUser)
)
r = {message: 'is already used by another account', property: 'email'}
r = {message: 'is already used by another account', property: 'email', code: 409}
return callback({res: r, code: 409}) if otherUser
user.set('email', req.body.email)
callback(null, req, user)
@ -118,7 +118,7 @@ UserHandler = class UserHandler extends Handler
User.findOne({nameLower: nameLower, anonymous: false}).exec (err, otherUser) ->
log.error "Database error setting user name: #{err}" if err
return callback(res: 'Database error.', code: 500) if err
r = {message: 'is already used by another account', property: 'name'}
r = {message: 'is already used by another account', property: 'name', code: 409}
log.info 'Another user exists' if otherUser
return callback({res: r, code: 409}) if otherUser
user.set('name', req.body.name)

View file

@ -0,0 +1,20 @@
sendwithus = require '../sendwithus'
utils = require '../lib/utils'
errors = require '../commons/errors'
wrap = require 'co-express'
database = require '../commons/database'
parse = require '../commons/parse'
module.exports =
sendParentSignupInstructions: wrap (req, res, next) ->
context =
email_id: sendwithus.templates.coppa_deny_parent_signup
recipient:
address: req.body.parentEmail
sendwithus.api.send context, (err, result) ->
console.log err
console.log result
if err
return next(new errors.InternalServerError("Error sending email. Check that it's valid and try again."))
else
res.status(200).send()

View file

@ -4,6 +4,7 @@ module.exports =
classrooms: require './classrooms'
campaigns: require './campaigns'
codelogs: require './codelogs'
coppaDeny: require './coppa-deny'
courseInstances: require './course-instances'
courses: require './courses'
files: require './files'

View file

@ -264,12 +264,7 @@ UserSchema.statics.unconflictName = unconflictName = (name, done) ->
UserSchema.methods.register = (done) ->
@set('anonymous', false)
if (name = @get 'name')? and name isnt ''
unconflictName name, (err, uniqueName) =>
return done err if err
@set 'name', uniqueName
done()
else done()
done()
{ welcome_email_student, welcome_email_user } = sendwithus.templates
timestamp = (new Date).getTime()
data =

View file

@ -111,3 +111,5 @@ module.exports.setup = (app) ->
app.get('/db/trial.request/-/users', mw.auth.checkHasPermission(['admin']), mw.trialRequests.getUsers)
app.get('/healthcheck', mw.healthcheck)
app.post('/send-parent-signup-instructions', mw.coppaDeny.sendParentSignupInstructions)

View file

@ -11,12 +11,13 @@ module.exports.api =
send: (context, cb) ->
log.debug('Tried to send email with context: ', JSON.stringify(context, null, ' '))
setTimeout(cb, 10)
if swuAPIKey
module.exports.api = new sendwithusAPI swuAPIKey, debug
module.exports.templates =
parent_subscribe_email: 'tem_2APERafogvwKhmcnouigud'
coppa_deny_parent_signup: 'tem_d5fCpXS8V7jgff2sYKCinX'
share_progress_email: 'tem_VHE3ihhGmVa3727qds9zY8'
welcome_email_user: 'tem_z7Xvj3mtWYk6ec6aW7RwFk'
welcome_email_student: 'tem_4WYPZNLzs5wawMF9qUJXUH'