feat(legacy-certs): Claim legacy certificates from the settings page

This commit is contained in:
Stuart Taylor
2018-02-27 14:03:06 +00:00
parent b3aed512d6
commit 2d0f8f7b9b
21 changed files with 664 additions and 312 deletions

View File

@@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { values as _values, isString, findIndex } from 'lodash';
import { createSelector } from 'reselect';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
@@ -12,11 +12,17 @@ import JSAlgoAndDSForm from './JSAlgoAndDSForm.jsx';
import SectionHeader from './SectionHeader.jsx';
import { projectsSelector } from '../../../entities';
import { claimCert, updateUserBackend } from '../redux';
import { fetchChallenges, userSelector, hardGoTo } from '../../../redux';
import {
fetchChallenges,
userSelector,
hardGoTo,
createErrorObservable
} from '../../../redux';
import {
buildUserProjectsMap,
jsProjectSuperBlock
} from '../utils/buildUserProjectsMap';
import legacyProjects from '../utils/legacyProjectData';
const mapStateToProps = createSelector(
userSelector,
@@ -30,12 +36,15 @@ const mapStateToProps = createSelector(
isJsAlgoDataStructCert,
isApisMicroservicesCert,
isInfosecQaCert,
isFrontEndCert,
isBackEndCert,
isDataVisCert,
username
},
projects
) => ({
projects,
userProjects: projects
userProjects: projects.concat(legacyProjects)
.map(block => buildUserProjectsMap(block, challengeMap))
.reduce((projects, current) => ({
...projects,
@@ -49,7 +58,10 @@ const mapStateToProps = createSelector(
'Front End Libraries Projects': isFrontEndLibsCert,
'Data Visualization Projects': is2018DataVisCert,
'API and Microservice Projects': isApisMicroservicesCert,
'Information Security and Quality Assurance Projects': isInfosecQaCert
'Information Security and Quality Assurance Projects': isInfosecQaCert,
'Legacy Front End Projects': isFrontEndCert,
'Legacy Back End Projects': isBackEndCert,
'Legacy Data Visualization Projects': isDataVisCert
},
username
})
@@ -58,6 +70,7 @@ const mapStateToProps = createSelector(
function mapDispatchToProps(dispatch) {
return bindActionCreators({
claimCert,
createError: createErrorObservable,
fetchChallenges,
hardGoTo,
updateUserBackend
@@ -67,6 +80,7 @@ function mapDispatchToProps(dispatch) {
const propTypes = {
blockNameIsCertMap: PropTypes.objectOf(PropTypes.bool),
claimCert: PropTypes.func.isRequired,
createError: PropTypes.func.isRequired,
fetchChallenges: PropTypes.func.isRequired,
hardGoTo: PropTypes.func.isRequired,
projects: PropTypes.arrayOf(
@@ -88,11 +102,15 @@ const propTypes = {
username: PropTypes.string
};
const compareSuperBlockWith = id => p => p.superBlock === id;
class CertificationSettings extends PureComponent {
constructor(props) {
super(props);
this.buildProjectForms = this.buildProjectForms.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.isProjectSectionCompleted = this.isProjectSectionCompleted.bind(this);
}
componentDidMount() {
@@ -102,23 +120,154 @@ class CertificationSettings extends PureComponent {
}
}
buildProjectForms({
projectBlockName,
challenges,
superBlock
}) {
const {
blockNameIsCertMap,
claimCert,
hardGoTo,
userProjects,
username
} = this.props;
const isCertClaimed = blockNameIsCertMap[projectBlockName];
if (superBlock === jsProjectSuperBlock) {
return (
<JSAlgoAndDSForm
challenges={ challenges }
claimCert={ claimCert }
hardGoTo={ hardGoTo }
isCertClaimed={ isCertClaimed }
jsProjects={ userProjects[superBlock] }
key={ superBlock }
projectBlockName={ projectBlockName }
superBlock={ superBlock }
username={ username }
/>
);
}
const options = challenges
.reduce((options, current) => {
options.types[current] = 'url';
return options;
}, { types: {} });
options.types.id = 'hidden';
options.placeholder = false;
const userValues = userProjects[superBlock] || {};
if (!userValues.id) {
userValues.id = superBlock;
}
const initialValues = challenges
.reduce((accu, current) => ({
...accu,
[current]: ''
}), {});
const completedProjects = _values(userValues)
.filter(Boolean)
.filter(isString)
// minus 1 to account for the id
.length - 1;
const fullForm = completedProjects === challenges.length;
return (
<FullWidthRow key={superBlock}>
<h3 className='project-heading'>{ projectBlockName }</h3>
<Form
buttonText={ fullForm ? 'Claim Certificate' : 'Save Progress' }
enableSubmit={ fullForm }
formFields={ challenges.concat([ 'id' ]) }
hideButton={isCertClaimed}
id={ superBlock }
initialValues={{
...initialValues,
...userValues
}}
options={ options }
submit={ this.handleSubmit }
/>
{
isCertClaimed ?
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
href={ `/c/${username}/${superBlock}`}
>
Show Certificate
</Button> :
null
}
<hr />
</FullWidthRow>
);
}
isProjectSectionCompleted(values) {
const { id } = values;
const { projects } = this.props;
const whereSuperBlockIsId = compareSuperBlockWith(id);
let pIndex = findIndex(projects, whereSuperBlockIsId);
let projectChallenges = [];
if (pIndex === -1) {
// submitted projects might be in a legacy certificate
pIndex = findIndex(legacyProjects, whereSuperBlockIsId);
projectChallenges = legacyProjects[pIndex].challenges;
if (pIndex === -1) {
// the submitted projects do not belong to current/legacy certificates
return this.props.createError(
new Error(
'Submitted projects do not belong to either current or ' +
'legacy certificates'
)
);
}
} else {
projectChallenges = projects[pIndex].challenges;
}
const valuesSaved = _values(this.props.userProjects[id])
.filter(Boolean)
.filter(isString);
// minus 1 due to the form id being in values
return (valuesSaved.length - 1) === projectChallenges.length;
}
handleSubmit(values) {
const { id } = values;
const fullForm = _.values(values)
.filter(Boolean)
.filter(_.isString)
// 5 projects + 1 id prop
.length === 6;
const valuesSaved = _.values(this.props.userProjects[id])
.filter(Boolean)
.filter(_.isString)
.length === 6;
if (fullForm && valuesSaved) {
if (this.isProjectSectionCompleted(values)) {
return this.props.claimCert(id);
}
const { projects } = this.props;
const pIndex = _.findIndex(projects, p => p.superBlock === id);
values.nameToIdMap = projects[pIndex].challengeNameIdMap;
const whereSuperBlockIsId = compareSuperBlockWith(id);
let pIndex = findIndex(projects, whereSuperBlockIsId);
let projectNameIdMap = {};
if (pIndex === -1) {
// submitted projects might be in a legacy certificate
pIndex = findIndex(legacyProjects, whereSuperBlockIsId);
projectNameIdMap = legacyProjects[pIndex].challengeNameIdMap;
if (pIndex === -1) {
// the submitted projects do not belong to current/legacy certificates
return this.props.createError(
new Error(
'Submitted projects do not belong to either current or ' +
'legacy certificates'
)
);
}
} else {
projectNameIdMap = projects[pIndex].challengeNameIdMap;
}
values.nameToIdMap = projectNameIdMap;
return this.props.updateUserBackend({
projects: {
[id]: values
@@ -128,12 +277,7 @@ class CertificationSettings extends PureComponent {
render() {
const {
blockNameIsCertMap,
claimCert,
hardGoTo,
projects,
userProjects,
username
projects
} = this.props;
if (!projects.length) {
return null;
@@ -150,88 +294,14 @@ class CertificationSettings extends PureComponent {
you can claim it.
</p>
</FullWidthRow>
{
projects.map(({
projectBlockName,
challenges,
superBlock
}) => {
const isCertClaimed = blockNameIsCertMap[projectBlockName];
if (superBlock === jsProjectSuperBlock) {
return (
<JSAlgoAndDSForm
challenges={ challenges }
claimCert={ claimCert }
hardGoTo={ hardGoTo }
isCertClaimed={ isCertClaimed }
jsProjects={ userProjects[superBlock] }
key={ superBlock }
projectBlockName={ projectBlockName }
superBlock={ superBlock }
username={ username }
/>
);
}
const options = challenges
.reduce((options, current) => {
options.types[current] = 'url';
return options;
}, { types: {} });
options.types.id = 'hidden';
options.placeholder = false;
const userValues = userProjects[superBlock] || {};
if (!userValues.id) {
userValues.id = superBlock;
}
const initialValues = challenges
.reduce((accu, current) => ({
...accu,
[current]: ''
}), {});
const completedProjects = _.values(userValues)
.filter(Boolean)
.filter(_.isString)
// minus 1 to account for the id
.length - 1;
const fullForm = completedProjects === challenges.length;
return (
<FullWidthRow key={superBlock}>
<h3 className='project-heading'>{ projectBlockName }</h3>
<Form
buttonText={ fullForm ? 'Claim Certificate' : 'Save Progress' }
enableSubmit={ fullForm }
formFields={ challenges.concat([ 'id' ]) }
hideButton={isCertClaimed}
id={ superBlock }
initialValues={{
...initialValues,
...userValues
}}
options={ options }
submit={ this.handleSubmit }
/>
{
isCertClaimed ?
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
href={ `/c/${username}/${superBlock}`}
>
Show Certificate
</Button> :
null
}
<hr />
</FullWidthRow>
);
})
{
projects.map(this.buildProjectForms)
}
<SectionHeader>
Legacy Certificate Settings
</SectionHeader>
{
legacyProjects.map(this.buildProjectForms)
}
</div>
);