feat(certs): Claim Certs

This commit is contained in:
Bouncey
2018-09-25 12:51:17 +01:00
committed by Stuart Taylor
parent d698d52794
commit 87837f480d
9 changed files with 309 additions and 92 deletions

View File

@ -34,7 +34,7 @@ export default function bootCertificate(app) {
const showCert = createShowCert(app);
const verifyCert = createVerifyCert(certTypeIds, app);
api.post('/certificate/verify', ifNoUser401, ifNoSuperBlock404, verifyCert);
api.put('/certificate/verify', ifNoUser401, ifNoSuperBlock404, verifyCert);
api.get('/certificate/showCert/:username/:cert', showCert);
app.use('/internal', api);
@ -47,18 +47,18 @@ const noNameMessage = dedent`
`;
const notCertifiedMessage = name => dedent`
it looks like you have not completed the necessary steps.
Please complete the required challenges to claim the
${name}
It looks like you have not completed the necessary steps.
Please complete the required projects to claim the
${name} Certification
`;
const alreadyClaimedMessage = name => dedent`
It looks like you already have claimed the ${name}
It looks like you already have claimed the ${name} Certification
`;
const successMessage = (username, name) => dedent`
@${username}, you have successfully claimed
the ${name}!
the ${name} Certification!
Congratulations on behalf of the freeCodeCamp.org team!
`;
@ -194,6 +194,34 @@ function sendCertifiedEmail(
return send$(notifyUser).map(() => true);
}
function getUserIsCertMap(user) {
const {
isRespWebDesignCert = false,
isJsAlgoDataStructCert = false,
isFrontEndLibsCert = false,
is2018DataVisCert = false,
isApisMicroservicesCert = false,
isInfosecQaCert = false,
isFrontEndCert = false,
isBackEndCert = false,
isDataVisCert = false,
isFullStackCert = false
} = user;
return {
isRespWebDesignCert,
isJsAlgoDataStructCert,
isFrontEndLibsCert,
is2018DataVisCert,
isApisMicroservicesCert,
isInfosecQaCert,
isFrontEndCert,
isBackEndCert,
isDataVisCert,
isFullStackCert
};
}
function createVerifyCert(certTypeIds, app) {
const { Email } = app.models;
return function verifyCert(req, res, next) {
@ -264,8 +292,11 @@ function createVerifyCert(certTypeIds, app) {
})
.subscribe(message => {
return res.status(200).json({
message,
success: message.includes('Congratulations')
response: {
type: message.includes('Congratulations') ? 'success' : 'info',
message
},
isCertMap: getUserIsCertMap(user)
});
}, next);
};

View File

@ -7,7 +7,8 @@ import { Grid, Button } from '@freecodecamp/react-bootstrap';
import Helmet from 'react-helmet';
import { signInLoadingSelector, userSelector } from '../redux';
import { submitNewAbout, updateUserFlag } from '../redux/settings';
import { submitNewAbout, updateUserFlag, verifyCert } from '../redux/settings';
import { createFlashMessage } from '../components/Flash/redux';
import Layout from '../components/Layout';
import Spacer from '../components/helpers/Spacer';
@ -22,6 +23,7 @@ import Honesty from '../components/settings/Honesty';
import Certification from '../components/settings/Certification';
const propTypes = {
createFlashMessage: PropTypes.func.isRequired,
showLoading: PropTypes.bool,
submitNewAbout: PropTypes.func.isRequired,
toggleNightMode: PropTypes.func.isRequired,
@ -74,7 +76,8 @@ const propTypes = {
twitter: PropTypes.string,
username: PropTypes.string,
website: PropTypes.string
})
}),
verifyCert: PropTypes.func.isRequired
};
const mapStateToProps = createSelector(
@ -89,18 +92,21 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
createFlashMessage,
submitNewAbout,
toggleNightMode: theme => updateUserFlag({ theme }),
updateInternetSettings: updateUserFlag,
updateIsHonest: updateUserFlag,
updatePortfolio: updateUserFlag,
updateQuincyEmail: sendQuincyEmail => updateUserFlag({ sendQuincyEmail })
updateQuincyEmail: sendQuincyEmail => updateUserFlag({ sendQuincyEmail }),
verifyCert
},
dispatch
);
function ShowSettings(props) {
const {
createFlashMessage,
submitNewAbout,
toggleNightMode,
user: {
@ -115,6 +121,7 @@ function ShowSettings(props) {
isInfosecQaCert,
isFrontEndLibsCert,
isFullStackCert,
isRespWebDesignCert,
isEmailVerified,
isHonest,
sendQuincyEmail,
@ -135,7 +142,8 @@ function ShowSettings(props) {
updateQuincyEmail,
updateInternetSettings,
updatePortfolio,
updateIsHonest
updateIsHonest,
verifyCert
} = props;
if (showLoading) {
@ -212,6 +220,7 @@ function ShowSettings(props) {
<Spacer />
<Certification
completedChallenges={completedChallenges}
createFlashMessage={createFlashMessage}
is2018DataVisCert={is2018DataVisCert}
isApisMicroservicesCert={isApisMicroservicesCert}
isBackEndCert={isBackEndCert}
@ -219,8 +228,12 @@ function ShowSettings(props) {
isFrontEndCert={isFrontEndCert}
isFrontEndLibsCert={isFrontEndLibsCert}
isFullStackCert={isFullStackCert}
isHonest={isHonest}
isInfosecQaCert={isInfosecQaCert}
isJsAlgoDataStructCert={isJsAlgoDataStructCert}
isRespWebDesignCert={isRespWebDesignCert}
username={username}
verifyCert={verifyCert}
/>
<Spacer />
{/* <DangerZone /> */}

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { find } from 'lodash';
import { find, first } from 'lodash';
import {
Table,
Button,
@ -9,14 +9,17 @@ import {
Modal
} from '@freecodecamp/react-bootstrap';
import { Link, navigate } from 'gatsby';
import { createSelector } from 'reselect';
import { projectMap } from '../../resources/certProjectMap';
import SectionHeader from './SectionHeader';
import SolutionViewer from './SolutionViewer';
import { FullWidthRow } from '../helpers';
import { FullWidthRow, Spacer } from '../helpers';
import { maybeUrlRE } from '../../utils';
import './certification.css';
const propTypes = {
completedChallenges: PropTypes.arrayOf(
PropTypes.shape({
@ -27,10 +30,71 @@ const propTypes = {
completedDate: PropTypes.number,
files: PropTypes.array
})
)
),
createFlashMessage: PropTypes.func.isRequired,
is2018DataVisCert: PropTypes.bool,
isApisMicroservicesCert: PropTypes.bool,
isBackEndCert: PropTypes.bool,
isDataVisCert: PropTypes.bool,
isFrontEndCert: PropTypes.bool,
isFrontEndLibsCert: PropTypes.bool,
isFullStackCert: PropTypes.bool,
isHonest: PropTypes.bool,
isInfosecQaCert: PropTypes.bool,
isJsAlgoDataStructCert: PropTypes.bool,
isRespWebDesignCert: PropTypes.bool,
username: PropTypes.string,
verifyCert: PropTypes.func.isRequired
};
const certifications = Object.keys(projectMap);
const isCertSelector = ({
is2018DataVisCert,
isApisMicroservicesCert,
isJsAlgoDataStructCert,
isBackEndCert,
isDataVisCert,
isFrontEndCert,
isInfosecQaCert,
isFrontEndLibsCert,
isFullStackCert,
isRespWebDesignCert
}) => ({
is2018DataVisCert,
isApisMicroservicesCert,
isJsAlgoDataStructCert,
isBackEndCert,
isDataVisCert,
isFrontEndCert,
isInfosecQaCert,
isFrontEndLibsCert,
isFullStackCert,
isRespWebDesignCert
});
const isCertMapSelector = createSelector(
isCertSelector,
({
is2018DataVisCert,
isApisMicroservicesCert,
isJsAlgoDataStructCert,
isBackEndCert,
isDataVisCert,
isFrontEndCert,
isInfosecQaCert,
isFrontEndLibsCert,
isFullStackCert,
isRespWebDesignCert
}) => ({
'Responsive Web Design': isRespWebDesignCert,
'JavaScript Algorithms and Data Structures': isJsAlgoDataStructCert,
'Front End Libraries': isFrontEndLibsCert,
'Data Visualization': is2018DataVisCert,
"API's and Microservices": isApisMicroservicesCert,
'Information Security And Quality Assurance': isInfosecQaCert
})
);
const initialState = {
solutionViewer: {
projectTitle: '',
@ -51,8 +115,11 @@ class CertificationSettings extends Component {
e.preventDefault();
return navigate(to);
};
handleSolutionModalHide = () => this.setState({ ...initialState });
getUserIsCertMap = () => isCertMapSelector(this.props);
getProjectSolution = (projectId, projectTitle) => {
const { completedChallenges } = this.props;
const completedProject = find(
@ -75,6 +142,7 @@ class CertificationSettings extends Component {
if (files && files.length) {
return (
<Button
block={true}
bsStyle='primary'
className='btn-invert'
onClick={onClickHandler}
@ -85,7 +153,9 @@ class CertificationSettings extends Component {
}
if (githubLink) {
return (
<div className='solutions-dropdown'>
<DropdownButton
block={true}
bsStyle='primary'
className='btn-invert'
id={`dropdown-for-${projectId}`}
@ -108,11 +178,13 @@ class CertificationSettings extends Component {
Back End
</MenuItem>
</DropdownButton>
</div>
);
}
if (maybeUrlRE.test(solution)) {
return (
<Button
block={true}
bsStyle='primary'
className='btn-invert'
href={solution}
@ -124,7 +196,12 @@ class CertificationSettings extends Component {
);
}
return (
<Button bsStyle='primary' className='btn-invert' onClick={onClickHandler}>
<Button
block={true}
bsStyle='primary'
className='btn-invert'
onClick={onClickHandler}
>
Show Code
</Button>
);
@ -132,6 +209,7 @@ class CertificationSettings extends Component {
renderCertifications = certName => (
<FullWidthRow key={certName}>
<Spacer />
<h3>{certName}</h3>
<Table>
<thead>
@ -140,13 +218,33 @@ class CertificationSettings extends Component {
<th>Solution</th>
</tr>
</thead>
<tbody>{this.renderProjectsFor(certName)}</tbody>
<tbody>
{this.renderProjectsFor(certName, this.getUserIsCertMap()[certName])}
</tbody>
</Table>
</FullWidthRow>
);
renderProjectsFor = certName =>
projectMap[certName].map(({ link, title, id }) => (
renderProjectsFor = (certName, isCert) => {
const { username, isHonest, createFlashMessage, verifyCert } = this.props;
const { superBlock } = first(projectMap[certName]);
const certLocation = `/certification/${username}/${superBlock}`;
const createClickHandler = superBlock => e => {
e.preventDefault();
if (isCert) {
return navigate(certLocation);
}
return isHonest
? verifyCert(superBlock)
: createFlashMessage({
type: 'info',
message:
'To claim a certification, you must first accept our acedemic ' +
'honesty policy'
});
};
return projectMap[certName]
.map(({ link, title, id }) => (
<tr className='project-row' key={id}>
<td className='project-title col-sm-8'>
<Link to={link}>{title}</Link>
@ -155,7 +253,22 @@ class CertificationSettings extends Component {
{this.getProjectSolution(id, title)}
</td>
</tr>
));
))
.concat([
<tr key={`cert-${superBlock}-button`}>
<td colSpan={2}>
<Button
block={true}
bsStyle='primary'
href={certLocation}
onClick={createClickHandler(superBlock)}
>
{isCert ? 'Show Certification' : 'Claim Certification'}
</Button>
</td>
</tr>
]);
};
render() {
const {

View File

@ -1,13 +1,13 @@
#certifcation-settings .project-title {
display: flex;
#certifcation-settings .solutions-dropdown,
#certifcation-settings .solutions-dropdown .dropdown-menu,
#certifcation-settings .solutions-dropdown .dropdown {
width: 100%;
}
#certifcation-settings .project-solution {
display: flex;
#certifcation-settings tr {
height: 57px;
}
#certifcation-settings .project-row {
display: flex;
#certifcation-settings .project-title > a {
line-height: 40px;
}

View File

@ -174,6 +174,8 @@ export const reducer = handleActions(
[settingsTypes.updateMyEmailComplete]: (state, { payload }) =>
payload ? spreadThePayloadOnUser(state, payload) : state,
[settingsTypes.updateUserFlagComplete]: (state, { payload }) =>
payload ? spreadThePayloadOnUser(state, payload) : state,
[settingsTypes.verifyCertComplete]: (state, { payload }) =>
payload ? spreadThePayloadOnUser(state, payload) : state
},
initialState

View File

@ -27,7 +27,8 @@ export const types = createTypes(
...createAsyncTypes('submitNewUsername'),
...createAsyncTypes('updateMyEmail'),
...createAsyncTypes('updateUserFlag'),
...createAsyncTypes('submitProfileUI')
...createAsyncTypes('submitProfileUI'),
...createAsyncTypes('verifyCert')
],
ns
);
@ -80,6 +81,13 @@ export const validateUsernameComplete = createAction(
);
export const validateUsernameError = createAction(types.validateUsernameError);
export const verifyCert = createAction(types.verifyCert);
export const verifyCertComplete = createAction(
types.verifyCertComplete,
checkForSuccessPayload
);
export const verifyCertError = createAction(types.verifyCertError);
export const usernameValidationSelector = state => state[ns].usernameValidation;
export const reducer = handleActions(

View File

@ -11,14 +11,17 @@ import {
submitNewUsernameComplete,
submitNewUsernameError,
submitProfileUIComplete,
submitProfileUIError
submitProfileUIError,
verifyCertComplete,
verifyCertError
} from './';
import {
getUsernameExists,
putUpdateMyAbout,
putUpdateMyProfileUI,
putUpdateMyUsername,
putUpdateUserFlag
putUpdateUserFlag,
putVerifyCert
} from '../../utils/ajax';
import { createFlashMessage } from '../../components/Flash/redux';
@ -74,12 +77,25 @@ function* validateUsernameSaga({ payload }) {
}
}
function* verifyCertificationSaga({ payload }) {
try {
const {
data: { response, isCertMap }
} = yield call(putVerifyCert, payload);
yield put(verifyCertComplete({ ...response, payload: isCertMap }));
yield put(createFlashMessage(response));
} catch (e) {
yield put(verifyCertError(e));
}
}
export function createSettingsSagas(types) {
return [
takeEvery(types.updateUserFlag, updateUserFlagSaga),
takeLatest(types.submitNewAbout, submitNewAboutSaga),
takeLatest(types.submitNewUsername, submitNewUsernameSaga),
takeLatest(types.validateUsername, validateUsernameSaga),
takeLatest(types.submitProfileUI, sumbitProfileUISaga)
takeLatest(types.submitProfileUI, sumbitProfileUISaga),
takeEvery(types.verifyCert, verifyCertificationSaga)
];
}

View File

@ -16,162 +16,192 @@ export const projectMap = {
{
id: 'bd7158d8c442eddfaeb5bd18',
title: 'Build a Tribute Page',
link: `${responsiveWebBase}/build-a-tribute-page`
link: `${responsiveWebBase}/build-a-tribute-page`,
superBlock: 'responsive-web-design'
},
{
id: '587d78af367417b2b2512b03',
title: 'Build a Survey Form',
link: `${responsiveWebBase}/build-a-survey-form`
link: `${responsiveWebBase}/build-a-survey-form`,
superBlock: 'responsive-web-design'
},
{
id: '587d78af367417b2b2512b04',
title: 'Build a Product Landing Page',
link: `${responsiveWebBase}/build-a-product-landing-page`
link: `${responsiveWebBase}/build-a-product-landing-page`,
superBlock: 'responsive-web-design'
},
{
id: '587d78b0367417b2b2512b05',
title: 'Build a Technical Documentation Page',
link: `${responsiveWebBase}/build-a-technical-documentation-page`
link: `${responsiveWebBase}/build-a-technical-documentation-page`,
superBlock: 'responsive-web-design'
},
{
id: 'bd7158d8c242eddfaeb5bd13',
title: 'Build a Personal Portfolio Webpage',
link: `${responsiveWebBase}/build-a-personal-portfolio-webpage`
link: `${responsiveWebBase}/build-a-personal-portfolio-webpage`,
superBlock: 'responsive-web-design'
}
],
'JavaScript Algorithms and Data Structures': [
{
id: 'aaa48de84e1ecc7c742e1124',
title: 'Palindrome Checker',
link: `${jsAlgoBase}/palindrome-checker`
link: `${jsAlgoBase}/palindrome-checker`,
superBlock: 'javascript-algorithms-and-data-structures'
},
{
id: 'a7f4d8f2483413a6ce226cac',
title: 'Roman Numeral Converter',
link: `${jsAlgoBase}/roman-numeral-converter`
link: `${jsAlgoBase}/roman-numeral-converter`,
superBlock: 'javascript-algorithms-and-data-structures'
},
{
id: '56533eb9ac21ba0edf2244e2',
title: 'Caesars Cipher',
link: `${jsAlgoBase}/caesars-cipher`
link: `${jsAlgoBase}/caesars-cipher`,
superBlock: 'javascript-algorithms-and-data-structures'
},
{
id: 'aff0395860f5d3034dc0bfc9',
title: 'Telephone Number Validator',
link: `${jsAlgoBase}/telephone-number-validator`
link: `${jsAlgoBase}/telephone-number-validator`,
superBlock: 'javascript-algorithms-and-data-structures'
},
{
id: 'aa2e6f85cab2ab736c9a9b24',
title: 'Cash Register',
link: `${jsAlgoBase}/cash-register`
link: `${jsAlgoBase}/cash-register`,
superBlock: 'javascript-algorithms-and-data-structures'
}
],
'Front End Libraries': [
{
id: 'bd7158d8c442eddfaeb5bd13',
title: 'Build a Random Quote Machine',
link: `${feLibsBase}/build-a-random-quote-machine`
link: `${feLibsBase}/build-a-random-quote-machine`,
superBlock: 'front-end-libraries'
},
{
id: 'bd7157d8c242eddfaeb5bd13',
title: 'Build a Markdown Previewer',
link: `${feLibsBase}/build-a-markdown-previewer`
link: `${feLibsBase}/build-a-markdown-previewer`,
superBlock: 'front-end-libraries'
},
{
id: '587d7dbc367417b2b2512bae',
title: 'Build a Drum Machine',
link: `${feLibsBase}/build-a-drum-machine`
link: `${feLibsBase}/build-a-drum-machine`,
superBlock: 'front-end-libraries'
},
{
id: 'bd7158d8c442eddfaeb5bd17',
title: 'Build a JavaScript Calculator',
link: `${feLibsBase}/build-a-javascript-calculator`
link: `${feLibsBase}/build-a-javascript-calculator`,
superBlock: 'front-end-libraries'
},
{
id: 'bd7158d8c442eddfaeb5bd0f',
title: 'Build a Pomodoro Clock',
link: `${feLibsBase}/build-a-pomodoro-clock`
link: `${feLibsBase}/build-a-pomodoro-clock`,
superBlock: 'front-end-libraries'
}
],
'Data Visualization': [
{
id: 'bd7168d8c242eddfaeb5bd13',
title: 'Visualize Data with a Bar Chart',
link: `${dataVisBase}/visualize-data-with-a-bar-chart`
link: `${dataVisBase}/visualize-data-with-a-bar-chart`,
superBlock: 'data-visualization'
},
{
id: 'bd7178d8c242eddfaeb5bd13',
title: 'Visualize Data with a Scatterplot Graph',
link: `${dataVisBase}/visualize-data-with-a-scatterplot-graph`
link: `${dataVisBase}/visualize-data-with-a-scatterplot-graph`,
superBlock: 'data-visualization'
},
{
id: 'bd7188d8c242eddfaeb5bd13',
title: 'Visualize Data with a Heat Map',
link: `${dataVisBase}/visualize-data-with-a-heat-map`
link: `${dataVisBase}/visualize-data-with-a-heat-map`,
superBlock: 'data-visualization'
},
{
id: '587d7fa6367417b2b2512bbf',
title: 'Visualize Data with a Choropleth Map',
link: `${dataVisBase}/visualize-data-with-a-choropleth-map`
link: `${dataVisBase}/visualize-data-with-a-choropleth-map`,
superBlock: 'data-visualization'
},
{
id: '587d7fa6367417b2b2512bc0',
title: 'Visualize Data with a Treemap Diagram',
link: `${dataVisBase}/visualize-data-with-a-treemap-diagram`
link: `${dataVisBase}/visualize-data-with-a-treemap-diagram`,
superBlock: 'data-visualization'
}
],
"API's and Microservices": [
{
id: 'bd7158d8c443edefaeb5bdef',
title: 'Timestamp Microservice',
link: `${apiMicroBase}/timestamp-microservice`
link: `${apiMicroBase}/timestamp-microservice`,
superBlock: 'apis-and-microservices'
},
{
id: 'bd7158d8c443edefaeb5bdff',
title: 'Request Header Parser Microservice',
link: `${apiMicroBase}/request-header-parser-microservice`
link: `${apiMicroBase}/request-header-parser-microservice`,
superBlock: 'apis-and-microservices'
},
{
id: 'bd7158d8c443edefaeb5bd0e',
title: 'URL Shortener Microservice',
link: `${apiMicroBase}/url-shortener-microservice`
link: `${apiMicroBase}/url-shortener-microservice`,
superBlock: 'apis-and-microservices'
},
{
id: '5a8b073d06fa14fcfde687aa',
title: 'Exercise Tracker',
link: `${apiMicroBase}/exercise-tracker`
link: `${apiMicroBase}/exercise-tracker`,
superBlock: 'apis-and-microservices'
},
{
id: 'bd7158d8c443edefaeb5bd0f',
title: 'File Metadata Microservice',
link: `${apiMicroBase}/file-metadata-microservice`
link: `${apiMicroBase}/file-metadata-microservice`,
superBlock: 'apis-and-microservices'
}
],
'Information Security And Quality Assurance': [
{
id: '587d8249367417b2b2512c41',
title: 'Metric-Imperial Converter',
link: `${infoSecBase}/metric-imperial-converter`
link: `${infoSecBase}/metric-imperial-converter`,
superBlock: 'information-security-and-quality-assurance'
},
{
id: '587d8249367417b2b2512c42',
title: 'Issue Tracker',
link: `${infoSecBase}/issue-tracker`
link: `${infoSecBase}/issue-tracker`,
superBlock: 'information-security-and-quality-assurance'
},
{
id: '587d824a367417b2b2512c43',
title: 'Personal Library',
link: `${infoSecBase}/personal-library`
link: `${infoSecBase}/personal-library`,
superBlock: 'information-security-and-quality-assurance'
},
{
id: '587d824a367417b2b2512c44',
title: 'Stock Price Checker',
link: `${infoSecBase}/stock-price-checker`
link: `${infoSecBase}/stock-price-checker`,
superBlock: 'information-security-and-quality-assurance'
},
{
id: '587d824a367417b2b2512c45',
title: 'Anonymous Message Board',
link: `${infoSecBase}/anonymous-message-board`
link: `${infoSecBase}/anonymous-message-board`,
superBlock: 'information-security-and-quality-assurance'
}
]
};

View File

@ -64,4 +64,8 @@ export function putUserUpdateEmail(email) {
return put('/update-my-email', { email });
}
export function putVerifyCert(superBlock) {
return put('/certificate/verify', { superBlock });
}
/** DELETE **/