Commit 4074e218 authored by Ray Schamp's avatar Ray Schamp Committed by GitHub

Merge pull request #744 from rschamp/feature/3679-complete-registration

Add Student registration update view
parents 6144c6f8 74558075
...@@ -51,12 +51,13 @@ module.exports = { ...@@ -51,12 +51,13 @@ module.exports = {
UsernameStep: intl.injectIntl(React.createClass({ UsernameStep: intl.injectIntl(React.createClass({
getDefaultProps: function () { getDefaultProps: function () {
return { return {
showPassword: false,
waiting: false waiting: false
}; };
}, },
getInitialState: function () { getInitialState: function () {
return { return {
showPassword: false, showPassword: this.props.showPassword,
waiting: false, waiting: false,
validUsername: '' validUsername: ''
}; };
...@@ -185,6 +186,68 @@ module.exports = { ...@@ -185,6 +186,68 @@ module.exports = {
); );
} }
})), })),
ChoosePasswordStep: intl.injectIntl(React.createClass({
getDefaultProps: function () {
return {
showPassword: false,
waiting: false
};
},
getInitialState: function () {
return {
showPassword: this.props.showPassword
};
},
onChangeShowPassword: function (field, value) {
this.setState({showPassword: value});
},
render: function () {
var formatMessage = this.props.intl.formatMessage;
return (
<Slide className="registration-step choose-password-step">
<h2>{formatMessage({id: 'registration.choosePasswordStepTitle'})}</h2>
<p className="description">
<intl.FormattedMessage id="registration.choosePasswordStepDescription" />
<Tooltip title={'?'}
tipContent={formatMessage({id: 'registration.choosePasswordStepTooltip'})} />
</p>
<Card>
<Form onValidSubmit={this.props.onNextStep}>
<Input label={formatMessage({id: 'registration.newPassword'})}
type={this.state.showPassword ? 'text' : 'password'}
name="user.password"
validations={{
minLength: 6,
notEquals: 'password',
notEqualsField: 'user.username'
}}
validationErrors={{
minLength: formatMessage({
id: 'registration.validationPasswordLength'
}),
notEquals: formatMessage({
id: 'registration.validationPasswordNotEquals'
}),
notEqualsField: formatMessage({
id: 'registration.validationPasswordNotUsername'
})
}}
required />
<Checkbox label={formatMessage({id: 'registration.showPassword'})}
value={this.state.showPassword}
onChange={this.onChangeShowPassword}
help={null}
name="showPassword" />
<NextStepButton waiting={this.props.waiting || this.state.waiting}
text={<intl.FormattedMessage id="registration.nextStep" />} />
</Form>
</Card>
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
</Slide>
);
}
})),
DemographicsStep: intl.injectIntl(React.createClass({ DemographicsStep: intl.injectIntl(React.createClass({
getDefaultProps: function () { getDefaultProps: function () {
return { return {
...@@ -742,13 +805,9 @@ module.exports = { ...@@ -742,13 +805,9 @@ module.exports = {
); );
} }
})), })),
ClassInviteStep: React.createClass({ ClassInviteStep: intl.injectIntl(React.createClass({
getDefaultProps: function () { getDefaultProps: function () {
return { return {
messages: {
'general.getStarted': 'Get Started',
'registration.classroomInviteStepDescription': 'has invited you to join the class:'
},
waiting: false waiting: false
}; };
}, },
...@@ -756,6 +815,7 @@ module.exports = { ...@@ -756,6 +815,7 @@ module.exports = {
this.props.onNextStep(); this.props.onNextStep();
}, },
render: function () { render: function () {
var formatMessage = this.props.intl.formatMessage;
return ( return (
<Slide className="registration-step class-invite-step"> <Slide className="registration-step class-invite-step">
{this.props.waiting ? [ {this.props.waiting ? [
...@@ -765,7 +825,7 @@ module.exports = { ...@@ -765,7 +825,7 @@ module.exports = {
src={this.props.classroom.educator.profile.images['50x50']} />, src={this.props.classroom.educator.profile.images['50x50']} />,
<h2>{this.props.classroom.educator.username}</h2>, <h2>{this.props.classroom.educator.username}</h2>,
<p className="description"> <p className="description">
{this.props.messages['registration.classroomInviteStepDescription']} {formatMessage({id: 'registration.classroomInviteStepDescription'})}
</p>, </p>,
<Card> <Card>
<div className="contents"> <div className="contents">
...@@ -774,24 +834,17 @@ module.exports = { ...@@ -774,24 +834,17 @@ module.exports = {
</div> </div>
<NextStepButton onClick={this.onNextStep} <NextStepButton onClick={this.onNextStep}
waiting={this.props.waiting} waiting={this.props.waiting}
text={this.props.messages['general.getStarted']} /> text={formatMessage({id: 'general.getStarted'})} />
</Card>, </Card>,
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} /> <StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
]} ]}
</Slide> </Slide>
); );
} }
}), })),
ClassWelcomeStep: React.createClass({ ClassWelcomeStep: intl.injectIntl(React.createClass({
getDefaultProps: function () { getDefaultProps: function () {
return { return {
messages: {
'registration.goToClass': 'Go to Class',
'registration.welcomeStepDescription': 'You have successfully set up a Scratch account! ' +
'You are now a member of the class:',
'registration.welcomeStepPrompt': 'To get started, click on the button below.',
'registration.welcomeStepTitle': 'Hurray! Welcome to Scratch!'
},
waiting: false waiting: false
}; };
}, },
...@@ -799,32 +852,33 @@ module.exports = { ...@@ -799,32 +852,33 @@ module.exports = {
this.props.onNextStep(); this.props.onNextStep();
}, },
render: function () { render: function () {
var formatMessage = this.props.intl.formatMessage;
return ( return (
<Slide className="registration-step class-welcome-step"> <Slide className="registration-step class-welcome-step">
{this.props.waiting ? [ {this.props.waiting ? [
<Spinner /> <Spinner />
] : [ ] : [
<h2>{this.props.messages['registration.welcomeStepTitle']}</h2>, <h2>{formatMessage({id: 'registration.welcomeStepTitle'})}</h2>,
<p className="description">{this.props.messages['registration.welcomeStepDescription']}</p>, <p className="description">{formatMessage({id: 'registration.welcomeStepDescription'})}</p>,
<Card> <Card>
{this.props.classroom ? ( {this.props.classroom ? (
<div className="contents"> <div className="contents">
<h3>{this.props.classroom.title}</h3> <h3>{this.props.classroom.title}</h3>
<img className="class-image" src={this.props.classroom.images['250x150']} /> <img className="class-image" src={this.props.classroom.images['250x150']} />
<p>{this.props.messages['registration.welcomeStepPrompt']}</p> <p>{formatMessage({id: 'registration.welcomeStepPrompt'})}</p>
</div> </div>
) : ( ) : (
null null
)} )}
<NextStepButton onClick={this.onNextStep} <NextStepButton onClick={this.onNextStep}
waiting={this.props.waiting} waiting={this.props.waiting}
text={this.props.messages['registration.goToClass']} /> text={formatMessage({id: 'registration.goToClass'})} />
</Card> </Card>
]} ]}
</Slide> </Slide>
); );
} }
}), })),
RegistrationError: intl.injectIntl(React.createClass({ RegistrationError: intl.injectIntl(React.createClass({
render: function () { render: function () {
return ( return (
...@@ -833,7 +887,7 @@ module.exports = { ...@@ -833,7 +887,7 @@ module.exports = {
<Card> <Card>
<h4>There was an error while processing your registration</h4> <h4>There was an error while processing your registration</h4>
<p> <p>
{this.props.registrationError} {this.props.children}
</p> </p>
</Card> </Card>
</Slide> </Slide>
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
"general.forParents": "For Parents", "general.forParents": "For Parents",
"general.forEducators": "For Educators", "general.forEducators": "For Educators",
"general.forDevelopers": "For Developers", "general.forDevelopers": "For Developers",
"general.getStarted": "Get Started",
"general.gender": "Gender", "general.gender": "Gender",
"general.guidelines": "Community Guidelines", "general.guidelines": "Community Guidelines",
"general.help": "Help", "general.help": "Help",
...@@ -107,13 +108,19 @@ ...@@ -107,13 +108,19 @@
"registration.checkOutResources": "Get Started with Resources", "registration.checkOutResources": "Get Started with Resources",
"registration.checkOutResourcesDescription": "Explore materials for educators and facilitators written by the Scratch Team, including <a href='/educators#resources'>tips, tutorials, and guides</a>.", "registration.checkOutResourcesDescription": "Explore materials for educators and facilitators written by the Scratch Team, including <a href='/educators#resources'>tips, tutorials, and guides</a>.",
"registration.choosePasswordStepDescription": "Type in a new password for your account. You will use this password the next time you log into Scratch.",
"registration.choosePasswordStepTitle": "Create a password",
"registration.choosePasswordStepTooltip": "Don't use your name or anything that's easy for someone else to guess.",
"registration.classroomInviteStepDescription": "has invited you to join the class:",
"registration.confirmYourEmail": "Confirm Your Email", "registration.confirmYourEmail": "Confirm Your Email",
"registration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:", "registration.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:",
"registration.createUsername": "Create a Username", "registration.createUsername": "Create a Username",
"registration.goToClass": "Go to Class", "registration.goToClass": "Go to Class",
"registration.lastStepTitle": "Thank you for requesting a Scratch Teacher Account", "registration.lastStepTitle": "Thank you for requesting a Scratch Teacher Account",
"registration.lastStepDescription": "We are currently processing your application. ", "registration.lastStepDescription": "We are currently processing your application. ",
"registration.mustBeNewStudent": "You must be a new student to complete your registration",
"registration.nameStepTooltip": "This information is used for verification and to aggregate usage statistics.", "registration.nameStepTooltip": "This information is used for verification and to aggregate usage statistics.",
"registration.newPassword": "New Password",
"registration.nextStep": "Next Step", "registration.nextStep": "Next Step",
"registration.personalStepTitle": "Personal Information", "registration.personalStepTitle": "Personal Information",
"registration.personalStepDescription": "Your individual responses will not be displayed publicly, and will be kept confidential and secure", "registration.personalStepDescription": "Your individual responses will not be displayed publicly, and will be kept confidential and secure",
......
...@@ -68,10 +68,17 @@ module.exports.refreshSession = function () { ...@@ -68,10 +68,17 @@ module.exports.refreshSession = function () {
uri: '/session/' uri: '/session/'
}, function (err, body) { }, function (err, body) {
if (err) return dispatch(module.exports.setSessionError(err)); if (err) return dispatch(module.exports.setSessionError(err));
if (typeof body === 'undefined') return dispatch(module.exports.setSessionError('No session content'));
if (typeof body !== 'undefined') { if (
if (body.banned) { body.user &&
return window.location = body.url; body.user.banned &&
window.location.pathname !== '/accounts/banned-response/') {
return window.location = '/accounts/banned-response/';
} else if (
body.flags &&
body.flags.must_complete_registration &&
window.location.pathname !== '/classes/complete_registration') {
return window.location = '/classes/complete_registration';
} else { } else {
dispatch(tokenActions.getToken()); dispatch(tokenActions.getToken());
dispatch(module.exports.setSession(body)); dispatch(module.exports.setSession(body));
...@@ -81,7 +88,6 @@ module.exports.refreshSession = function () { ...@@ -81,7 +88,6 @@ module.exports.refreshSession = function () {
dispatch(permissionsActions.getPermissions()); dispatch(permissionsActions.getPermissions());
return; return;
} }
}
}); });
}; };
}; };
...@@ -17,6 +17,12 @@ ...@@ -17,6 +17,12 @@
"view": "guidelines/guidelines", "view": "guidelines/guidelines",
"title": "Scratch Community Guidelines" "title": "Scratch Community Guidelines"
}, },
{
"name": "student-complete-registration",
"pattern": "^/classes/complete_registration",
"view": "studentcompleteregistration/studentcompleteregistration",
"title": "Complete your Registration"
},
{ {
"name": "student-registration", "name": "student-registration",
"pattern": "^/classes/:id/register/:token", "pattern": "^/classes/:id/register/:token",
......
var connect = require('react-redux').connect;
var defaults = require('lodash.defaultsdeep');
var React = require('react');
var render = require('../../lib/render.jsx');
var sessionStatus = require('../../redux/session').Status;
var api = require('../../lib/api');
var intl = require('../../lib/intl.jsx');
var Deck = require('../../components/deck/deck.jsx');
var Progression = require('../../components/progression/progression.jsx');
var Spinner = require('../../components/spinner/spinner.jsx');
var Steps = require('../../components/registration/steps.jsx');
require('./studentcompleteregistration.scss');
var StudentCompleteRegistration = intl.injectIntl(React.createClass({
type: 'StudentCompleteRegistration',
getInitialState: function () {
return {
classroom: null,
formData: {},
registrationErrors: null,
step: 0,
waiting: false
};
},
advanceStep: function (formData) {
formData = formData || {};
this.setState({
step: this.state.step + 1,
formData: defaults({}, formData, this.state.formData)
});
},
componentDidUpdate: function (prevProps) {
if (prevProps.session.session !== this.props.session.session &&
this.props.session.session.permissions &&
this.props.session.session.permissions.student) {
var classroomId = this.props.session.session.user.classroomId;
api({
uri: '/classrooms/' + classroomId
}, function (err, body, res) {
if (err || res.statusCode === 404) {
return this.setState({
registrationErrors: {
__all__: this.props.intl.formatMessage({id: 'studentRegistration.classroomApiGeneralError'})
}
});
}
this.setState({classroom: body});
}.bind(this));
}
},
register: function (formData) {
this.setState({waiting: true});
formData = defaults({}, formData || {}, this.state.formData);
var submittedData = {
birth_month: formData.user.birth.month,
birth_year: formData.user.birth.year,
gender: (
formData.user.gender === 'other' ?
formData.user.genderOther :
formData.user.gender
),
country: formData.user.country,
is_robot: formData.user.isRobot
};
if (this.props.session.session.flags.must_reset_password) {
submittedData.password = formData.user.password;
}
api({
host: '',
uri: '/classes/student_update_registration/',
method: 'post',
useCsrf: true,
formData: submittedData
}, function (err, body) {
this.setState({waiting: false});
if (err) return this.setState({registrationError: err});
if (body.success) return this.advanceStep(formData);
this.setState({registrationErrors: body.errors});
}.bind(this));
},
goToClass: function () {
window.location = '/classes/' + this.state.classroom.id + '/';
},
render: function () {
var demographicsDescription = this.props.intl.formatMessage({
id: 'registration.studentPersonalStepDescription'});
var registrationErrors = this.state.registrationErrors;
var sessionFetched = this.props.session.status === sessionStatus.FETCHED;
if (sessionFetched &&
!(this.props.session.session.permissions.student &&
this.props.session.session.flags.must_complete_registration)) {
registrationErrors = {
__all__: this.props.intl.formatMessage({id: 'registration.mustBeNewStudent'})
};
}
return (
<Deck className="student-registration">
{sessionFetched && this.state.classroom ?
(registrationErrors ?
<Steps.RegistrationError>
<ul>
{Object.keys(registrationErrors).map(function (field) {
var label = field + ': ';
if (field === '__all__') {
label = '';
}
return (<li>{label}{registrationErrors[field]}</li>);
})}
</ul>
</Steps.RegistrationError>
:
<Progression {... this.state}>
<Steps.ClassInviteStep classroom={this.state.classroom}
messages={this.props.messages}
onNextStep={this.advanceStep}
waiting={this.state.waiting} />
{this.props.session.session.flags.must_reset_password ?
<Steps.ChoosePasswordStep onNextStep={this.advanceStep}
showPassword={true}
waiting={this.state.waiting} />
:
[]
}
<Steps.DemographicsStep description={demographicsDescription}
onNextStep={this.register}
waiting={this.state.waiting} />
<Steps.ClassWelcomeStep classroom={this.state.classroom}
onNextStep={this.goToClass}
waiting={this.state.waiting} />
</Progression>
)
:
<Spinner />
}
</Deck>
);
}
}));
var mapStateToProps = function (state) {
return {
session: state.session
};
};
var ConnectedStudentCompleteRegistration = connect(mapStateToProps)(StudentCompleteRegistration);
render(<ConnectedStudentCompleteRegistration />, document.getElementById('app'));
@import "../../colors";
@import "../../frameless";
@include responsive-layout (".student-registration", ".slide");
html,
body {
background-color: darken($ui-purple, 8%);
}
.student-complete-registration {
background-color: $ui-purple;
}
...@@ -99,11 +99,12 @@ var StudentRegistration = intl.injectIntl(React.createClass({ ...@@ -99,11 +99,12 @@ var StudentRegistration = intl.injectIntl(React.createClass({
return ( return (
<Deck className="student-registration"> <Deck className="student-registration">
{this.state.registrationError ? {this.state.registrationError ?
<Steps.RegistrationError {... this.state} /> <Steps.RegistrationError>
{this.state.registrationError}
</Steps.RegistrationError>
: :
<Progression {... this.state}> <Progression {... this.state}>
<Steps.ClassInviteStep classroom={this.state.classroom} <Steps.ClassInviteStep classroom={this.state.classroom}
messages={this.props.messages}
onNextStep={this.advanceStep} onNextStep={this.advanceStep}
waiting={this.state.waiting || !this.state.classroom} /> waiting={this.state.waiting || !this.state.classroom} />
<Steps.UsernameStep onNextStep={this.advanceStep} <Steps.UsernameStep onNextStep={this.advanceStep}
...@@ -116,7 +117,6 @@ var StudentRegistration = intl.injectIntl(React.createClass({ ...@@ -116,7 +117,6 @@ var StudentRegistration = intl.injectIntl(React.createClass({
onNextStep={this.register} onNextStep={this.register}
waiting={this.state.waiting} /> waiting={this.state.waiting} />
<Steps.ClassWelcomeStep classroom={this.state.classroom} <Steps.ClassWelcomeStep classroom={this.state.classroom}
messages={this.props.messages}
onNextStep={this.goToClass} onNextStep={this.goToClass}
waiting={this.state.waiting || !this.state.classroom} /> waiting={this.state.waiting || !this.state.classroom} />
</Progression> </Progression>
......
...@@ -8,6 +8,6 @@ body { ...@@ -8,6 +8,6 @@ body {
background-color: darken($ui-purple, 8%); background-color: darken($ui-purple, 8%);
} }
.teacher-registration { .student-registration {
background-color: $ui-purple; background-color: $ui-purple;
} }
...@@ -82,7 +82,9 @@ var TeacherRegistration = React.createClass({ ...@@ -82,7 +82,9 @@ var TeacherRegistration = React.createClass({
return ( return (
<Deck className="teacher-registration"> <Deck className="teacher-registration">
{this.state.registrationError ? {this.state.registrationError ?
<Steps.RegistrationError {... this.state} /> <Steps.RegistrationError>
{this.state.registrationError}
</Steps.RegistrationError>
: :
<Progression {... this.state}> <Progression {... this.state}>
<Steps.UsernameStep onNextStep={this.advanceStep} <Steps.UsernameStep onNextStep={this.advanceStep}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment