mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2025-02-17 00:21:20 -05:00
Merge pull request #744 from rschamp/feature/3679-complete-registration
Add Student registration update view
This commit is contained in:
commit
4074e218b2
9 changed files with 279 additions and 40 deletions
|
@ -51,12 +51,13 @@ module.exports = {
|
|||
UsernameStep: intl.injectIntl(React.createClass({
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
showPassword: false,
|
||||
waiting: false
|
||||
};
|
||||
},
|
||||
getInitialState: function () {
|
||||
return {
|
||||
showPassword: false,
|
||||
showPassword: this.props.showPassword,
|
||||
waiting: false,
|
||||
validUsername: ''
|
||||
};
|
||||
|
@ -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({
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
|
@ -742,13 +805,9 @@ module.exports = {
|
|||
);
|
||||
}
|
||||
})),
|
||||
ClassInviteStep: React.createClass({
|
||||
ClassInviteStep: intl.injectIntl(React.createClass({
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
messages: {
|
||||
'general.getStarted': 'Get Started',
|
||||
'registration.classroomInviteStepDescription': 'has invited you to join the class:'
|
||||
},
|
||||
waiting: false
|
||||
};
|
||||
},
|
||||
|
@ -756,6 +815,7 @@ module.exports = {
|
|||
this.props.onNextStep();
|
||||
},
|
||||
render: function () {
|
||||
var formatMessage = this.props.intl.formatMessage;
|
||||
return (
|
||||
<Slide className="registration-step class-invite-step">
|
||||
{this.props.waiting ? [
|
||||
|
@ -765,7 +825,7 @@ module.exports = {
|
|||
src={this.props.classroom.educator.profile.images['50x50']} />,
|
||||
<h2>{this.props.classroom.educator.username}</h2>,
|
||||
<p className="description">
|
||||
{this.props.messages['registration.classroomInviteStepDescription']}
|
||||
{formatMessage({id: 'registration.classroomInviteStepDescription'})}
|
||||
</p>,
|
||||
<Card>
|
||||
<div className="contents">
|
||||
|
@ -774,24 +834,17 @@ module.exports = {
|
|||
</div>
|
||||
<NextStepButton onClick={this.onNextStep}
|
||||
waiting={this.props.waiting}
|
||||
text={this.props.messages['general.getStarted']} />
|
||||
text={formatMessage({id: 'general.getStarted'})} />
|
||||
</Card>,
|
||||
<StepNavigation steps={this.props.totalSteps - 1} active={this.props.activeStep} />
|
||||
]}
|
||||
</Slide>
|
||||
);
|
||||
}
|
||||
}),
|
||||
ClassWelcomeStep: React.createClass({
|
||||
})),
|
||||
ClassWelcomeStep: intl.injectIntl(React.createClass({
|
||||
getDefaultProps: function () {
|
||||
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
|
||||
};
|
||||
},
|
||||
|
@ -799,32 +852,33 @@ module.exports = {
|
|||
this.props.onNextStep();
|
||||
},
|
||||
render: function () {
|
||||
var formatMessage = this.props.intl.formatMessage;
|
||||
return (
|
||||
<Slide className="registration-step class-welcome-step">
|
||||
{this.props.waiting ? [
|
||||
<Spinner />
|
||||
] : [
|
||||
<h2>{this.props.messages['registration.welcomeStepTitle']}</h2>,
|
||||
<p className="description">{this.props.messages['registration.welcomeStepDescription']}</p>,
|
||||
<h2>{formatMessage({id: 'registration.welcomeStepTitle'})}</h2>,
|
||||
<p className="description">{formatMessage({id: 'registration.welcomeStepDescription'})}</p>,
|
||||
<Card>
|
||||
{this.props.classroom ? (
|
||||
<div className="contents">
|
||||
<h3>{this.props.classroom.title}</h3>
|
||||
<img className="class-image" src={this.props.classroom.images['250x150']} />
|
||||
<p>{this.props.messages['registration.welcomeStepPrompt']}</p>
|
||||
<p>{formatMessage({id: 'registration.welcomeStepPrompt'})}</p>
|
||||
</div>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
<NextStepButton onClick={this.onNextStep}
|
||||
waiting={this.props.waiting}
|
||||
text={this.props.messages['registration.goToClass']} />
|
||||
text={formatMessage({id: 'registration.goToClass'})} />
|
||||
</Card>
|
||||
]}
|
||||
</Slide>
|
||||
);
|
||||
}
|
||||
}),
|
||||
})),
|
||||
RegistrationError: intl.injectIntl(React.createClass({
|
||||
render: function () {
|
||||
return (
|
||||
|
@ -833,7 +887,7 @@ module.exports = {
|
|||
<Card>
|
||||
<h4>There was an error while processing your registration</h4>
|
||||
<p>
|
||||
{this.props.registrationError}
|
||||
{this.props.children}
|
||||
</p>
|
||||
</Card>
|
||||
</Slide>
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"general.forParents": "For Parents",
|
||||
"general.forEducators": "For Educators",
|
||||
"general.forDevelopers": "For Developers",
|
||||
"general.getStarted": "Get Started",
|
||||
"general.gender": "Gender",
|
||||
"general.guidelines": "Community Guidelines",
|
||||
"general.help": "Help",
|
||||
|
@ -107,13 +108,19 @@
|
|||
|
||||
"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.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.confirmYourEmailDescription": "If you haven't already, please click the link in the confirmation email sent to:",
|
||||
"registration.createUsername": "Create a Username",
|
||||
"registration.goToClass": "Go to Class",
|
||||
"registration.lastStepTitle": "Thank you for requesting a Scratch Teacher Account",
|
||||
"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.newPassword": "New Password",
|
||||
"registration.nextStep": "Next Step",
|
||||
"registration.personalStepTitle": "Personal Information",
|
||||
"registration.personalStepDescription": "Your individual responses will not be displayed publicly, and will be kept confidential and secure",
|
||||
|
|
|
@ -68,19 +68,25 @@ module.exports.refreshSession = function () {
|
|||
uri: '/session/'
|
||||
}, function (err, body) {
|
||||
if (err) return dispatch(module.exports.setSessionError(err));
|
||||
if (typeof body === 'undefined') return dispatch(module.exports.setSessionError('No session content'));
|
||||
if (
|
||||
body.user &&
|
||||
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 {
|
||||
dispatch(tokenActions.getToken());
|
||||
dispatch(module.exports.setSession(body));
|
||||
dispatch(module.exports.setStatus(module.exports.Status.FETCHED));
|
||||
|
||||
if (typeof body !== 'undefined') {
|
||||
if (body.banned) {
|
||||
return window.location = body.url;
|
||||
} else {
|
||||
dispatch(tokenActions.getToken());
|
||||
dispatch(module.exports.setSession(body));
|
||||
dispatch(module.exports.setStatus(module.exports.Status.FETCHED));
|
||||
|
||||
// get the permissions from the updated session
|
||||
dispatch(permissionsActions.getPermissions());
|
||||
return;
|
||||
}
|
||||
// get the permissions from the updated session
|
||||
dispatch(permissionsActions.getPermissions());
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -17,6 +17,12 @@
|
|||
"view": "guidelines/guidelines",
|
||||
"title": "Scratch Community Guidelines"
|
||||
},
|
||||
{
|
||||
"name": "student-complete-registration",
|
||||
"pattern": "^/classes/complete_registration",
|
||||
"view": "studentcompleteregistration/studentcompleteregistration",
|
||||
"title": "Complete your Registration"
|
||||
},
|
||||
{
|
||||
"name": "student-registration",
|
||||
"pattern": "^/classes/:id/register/:token",
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
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'));
|
|
@ -0,0 +1,13 @@
|
|||
@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({
|
|||
return (
|
||||
<Deck className="student-registration">
|
||||
{this.state.registrationError ?
|
||||
<Steps.RegistrationError {... this.state} />
|
||||
<Steps.RegistrationError>
|
||||
{this.state.registrationError}
|
||||
</Steps.RegistrationError>
|
||||
:
|
||||
<Progression {... this.state}>
|
||||
<Steps.ClassInviteStep classroom={this.state.classroom}
|
||||
messages={this.props.messages}
|
||||
onNextStep={this.advanceStep}
|
||||
waiting={this.state.waiting || !this.state.classroom} />
|
||||
<Steps.UsernameStep onNextStep={this.advanceStep}
|
||||
|
@ -116,7 +117,6 @@ var StudentRegistration = intl.injectIntl(React.createClass({
|
|||
onNextStep={this.register}
|
||||
waiting={this.state.waiting} />
|
||||
<Steps.ClassWelcomeStep classroom={this.state.classroom}
|
||||
messages={this.props.messages}
|
||||
onNextStep={this.goToClass}
|
||||
waiting={this.state.waiting || !this.state.classroom} />
|
||||
</Progression>
|
||||
|
|
|
@ -8,6 +8,6 @@ body {
|
|||
background-color: darken($ui-purple, 8%);
|
||||
}
|
||||
|
||||
.teacher-registration {
|
||||
.student-registration {
|
||||
background-color: $ui-purple;
|
||||
}
|
||||
|
|
|
@ -82,7 +82,9 @@ var TeacherRegistration = React.createClass({
|
|||
return (
|
||||
<Deck className="teacher-registration">
|
||||
{this.state.registrationError ?
|
||||
<Steps.RegistrationError {... this.state} />
|
||||
<Steps.RegistrationError>
|
||||
{this.state.registrationError}
|
||||
</Steps.RegistrationError>
|
||||
:
|
||||
<Progression {... this.state}>
|
||||
<Steps.UsernameStep onNextStep={this.advanceStep}
|
||||
|
|
Loading…
Reference in a new issue