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>
);

View File

@ -0,0 +1,95 @@
const legacyFrontEndProjects = {
challengeNameIdMap: {
'build-a-personal-portfolio-webpage': 'bd7158d8c242eddfaeb5bd13',
'build-a-random-quote-machine': 'bd7158d8c442eddfaeb5bd13',
'build-a-pomodoro-clock': 'bd7158d8c442eddfaeb5bd0f',
'build-a-javascript-calculator': 'bd7158d8c442eddfaeb5bd17',
'show-the-local-weather': 'bd7158d8c442eddfaeb5bd10',
'use-the-twitchtv-json-api': 'bd7158d8c442eddfaeb5bd1f',
'stylize-stories-on-camper-news': 'bd7158d8c442eddfaeb5bd18',
'build-a-wikipedia-viewer': 'bd7158d8c442eddfaeb5bd19',
'build-a-tic-tac-toe-game': 'bd7158d8c442eedfaeb5bd1c',
'build-a-simon-game': 'bd7158d8c442eddfaeb5bd1c'
},
challenges: [
'Build a Personal Portfolio Webpage',
'Build a Random Quote Machine',
'Build a Pomodoro Clock',
'Build a JavaScript Calculator',
'Show the Local Weather',
'Use the Twitchtv JSON API',
'Stylize Stories on Camper News',
'Build a Wikipedia Viewer',
'Build a Tic Tac Toe Game',
'Build a Simon Game'
],
projectBlockName: 'Legacy Front End Projects',
superBlock: 'legacy-front-end'
};
const legacyBackEndProjects = {
challengeNameIdMap: {
'timestamp-microservice': 'bd7158d8c443edefaeb5bdef',
'request-header-parser-microservice': 'bd7158d8c443edefaeb5bdff',
'url-shortener-microservice': 'bd7158d8c443edefaeb5bd0e',
'image-search-abstraction-layer': 'bd7158d8c443edefaeb5bdee',
'file-metadata-microservice': 'bd7158d8c443edefaeb5bd0f',
'build-a-voting-app': 'bd7158d8c443eddfaeb5bdef',
'build-a-nightlife-coordination-app': 'bd7158d8c443eddfaeb5bdff',
'chart-the-stock-market': 'bd7158d8c443eddfaeb5bd0e',
'manage-a-book-trading-club': 'bd7158d8c443eddfaeb5bd0f',
'build-a-pinterest-clone': 'bd7158d8c443eddfaeb5bdee'
},
challenges: [
'Timestamp Microservice',
'Request Header Parser Microservice',
'URL Shortener Microservice',
'Image Search Abstraction Layer',
'File Metadata Microservice',
'Build a Voting App',
'Build a Nightlife Coordination App',
'Chart the Stock Market',
'Manage a Book Trading Club',
'Build a Pinterest Clone'
],
projectBlockName: 'Legacy Back End Projects',
superBlock: 'legacy-back-end'
};
const legacyDataVisProjects = {
challengeNameIdMap: {
'build-a-markdown-previewer': 'bd7157d8c242eddfaeb5bd13',
'build-a-camper-leaderboard': 'bd7156d8c242eddfaeb5bd13',
'build-a-recipe-box': 'bd7155d8c242eddfaeb5bd13',
'build-the-game-of-life': 'bd7154d8c242eddfaeb5bd13',
'build-a-roguelike-dungeon-crawler-game': 'bd7153d8c242eddfaeb5bd13',
'visualize-data-with-a-bar-chart': 'bd7168d8c242eddfaeb5bd13',
'visualize-data-with-a-scatterplot-graph': 'bd7178d8c242eddfaeb5bd13',
'visualize-data-with-a-heat-map': 'bd7188d8c242eddfaeb5bd13',
'show-national-contiguity-with-a-force-directed-graph':
'bd7198d8c242eddfaeb5bd13',
'map-data-across-the-globe': 'bd7108d8c242eddfaeb5bd13'
},
challenges: [
'Build a Markdown Previewer',
'Build a Camper Leaderboard',
'Build a Recipe Box',
'Build the Game of Life',
'Build a Roguelike Dungeon Crawler Game',
'Visualize Data with a Bar Chart',
'Visualize Data with a Scatterplot Graph',
'Visualize Data with a Heat Map',
'Show National Contiguity with a Force Directed Graph',
'Map Data Across the Globe'
],
projectBlockName: 'Legacy Data Visualization Projects',
superBlock: 'legacy-data-visualization'
};
const legacyProjects = [
legacyFrontEndProjects,
legacyBackEndProjects,
legacyDataVisProjects
];
export default legacyProjects;

View File

@ -0,0 +1,57 @@
{
"name": "Legacy Back End Certificate",
"order": 1,
"isPrivate": true,
"challenges": [
{
"id": "660add10cb82ac38a17513be",
"title": "Legacy Back End Certificate",
"challengeType": 7,
"description": [],
"challengeSeed": [],
"isPrivate": true,
"tests": [
{
"id": "bd7158d8c443edefaeb5bdef",
"title": "Timestamp Microservice"
},
{
"id": "bd7158d8c443edefaeb5bdff",
"title": "Request Header Parser Microservice"
},
{
"id": "bd7158d8c443edefaeb5bd0e",
"title": "URL Shortener Microservice"
},
{
"id": "bd7158d8c443edefaeb5bdee",
"title": "Image Search Abstraction Layer"
},
{
"id": "bd7158d8c443edefaeb5bd0f",
"title": "File Metadata Microservice"
},
{
"id": "bd7158d8c443eddfaeb5bdef",
"title": "Build a Voting App"
},
{
"id": "bd7158d8c443eddfaeb5bdff",
"title": "Build a Nightlife Coordination App"
},
{
"id": "bd7158d8c443eddfaeb5bd0e",
"title": "Chart the Stock Market"
},
{
"id": "bd7158d8c443eddfaeb5bd0f",
"title": "Manage a Book Trading Club"
},
{
"id": "bd7158d8c443eddfaeb5bdee",
"title": "Build a Pinterest Clone"
}
]
}
]
}

View File

@ -0,0 +1,57 @@
{
"name": "Legacy Data Visualization Certificate",
"order": 1,
"isPrivate": true,
"challenges": [
{
"id": "561add10cb82ac39a17513bc",
"title": "Legacy Data Visualization Certificate",
"challengeType": 7,
"description": [],
"challengeSeed": [],
"isPrivate": true,
"tests": [
{
"id": "bd7157d8c242eddfaeb5bd13",
"title": "Build a Markdown Previewer"
},
{
"id": "bd7156d8c242eddfaeb5bd13",
"title": "Build a Camper Leaderboard"
},
{
"id": "bd7155d8c242eddfaeb5bd13",
"title": "Build a Recipe Box"
},
{
"id": "bd7154d8c242eddfaeb5bd13",
"title": "Build the Game of Life"
},
{
"id": "bd7153d8c242eddfaeb5bd13",
"title": "Build a Roguelike Dungeon Crawler Game"
},
{
"id": "bd7168d8c242eddfaeb5bd13",
"title": "Visualize Data with a Bar Chart"
},
{
"id": "bd7178d8c242eddfaeb5bd13",
"title": "Visualize Data with a Scatterplot Graph"
},
{
"id": "bd7188d8c242eddfaeb5bd13",
"title": "Visualize Data with a Heat Map"
},
{
"id": "bd7198d8c242eddfaeb5bd13",
"title": "Show National Contiguity with a Force Directed Graph"
},
{
"id": "bd7108d8c242eddfaeb5bd13",
"title": "Map Data Across the Globe"
}
]
}
]
}

View File

@ -0,0 +1,57 @@
{
"name": "Legacy Front End Certificate",
"order": 1,
"isPrivate": true,
"challenges": [
{
"id": "561add10cb82ac38a17513be",
"title": "Legacy Front End Certificate",
"challengeType": 7,
"description": [],
"challengeSeed": [],
"isPrivate": true,
"tests": [
{
"id": "bd7158d8c242eddfaeb5bd13",
"title": "Build a Personal Portfolio Webpage"
},
{
"id": "bd7158d8c442eddfaeb5bd13",
"title": "Build a Random Quote Machine"
},
{
"id": "bd7158d8c442eddfaeb5bd0f",
"title": "Build a Pomodoro Clock"
},
{
"id": "bd7158d8c442eddfaeb5bd17",
"title": "Build a JavaScript Calculator"
},
{
"id": "bd7158d8c442eddfaeb5bd10",
"title": "Show the Local Weather"
},
{
"id": "bd7158d8c442eddfaeb5bd1f",
"title": "Use the Twitch.tv JSON API"
},
{
"id": "bd7158d8c442eddfaeb5bd18",
"title": "Stylize Stories on Camper News"
},
{
"id": "bd7158d8c442eddfaeb5bd19",
"title": "Build a Wikipedia Viewer"
},
{
"id": "bd7158d8c442eedfaeb5bd1c",
"title": "Build a Tic Tac Toe Game"
},
{
"id": "bd7158d8c442eddfaeb5bd1c",
"title": "Build a Simon Game"
}
]
}
]
}

View File

@ -1,5 +1,6 @@
import _ from 'lodash';
import loopback from 'loopback';
import moment from 'moment-timezone';
import path from 'path';
import dedent from 'dedent';
import { Observable } from 'rx';
@ -13,26 +14,23 @@ import {
import { observeQuery } from '../utils/rx';
import {
// legacy
frontEndChallengeId,
backEndChallengeId,
dataVisId,
legacyFrontEndChallengeId,
legacyBackEndChallengeId,
legacyDataVisId,
// modern
respWebDesignId,
frontEndLibsId,
dataVis2018Id,
jsAlgoDataStructId,
dataVis2018Id,
apisMicroservicesId,
infosecQaId
} from '../utils/constantStrings.json';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
import {
completeCommitment$
} from '../utils/commit';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
const log = debug('fcc:certification');
const renderCertifedEmail = loopback.template(path.join(
__dirname,
@ -46,6 +44,48 @@ function isCertified(ids, challengeMap = {}) {
return _.every(ids, ({ id }) => _.has(challengeMap, id));
}
const certIds = {
[certTypes.frontEnd]: legacyFrontEndChallengeId,
[certTypes.backEnd]: legacyBackEndChallengeId,
[certTypes.dataVis]: legacyDataVisId,
[certTypes.respWebDesign]: respWebDesignId,
[certTypes.frontEndLibs]: frontEndLibsId,
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
[certTypes.dataVis2018]: dataVis2018Id,
[certTypes.apisMicroservices]: apisMicroservicesId,
[certTypes.infosecQa]: infosecQaId
};
const certViews = {
[certTypes.frontEnd]: 'certificate/legacy/front-end.jade',
[certTypes.backEnd]: 'certificate/legacy/back-end.jade',
[certTypes.dataVis]: 'certificate/legacy/data-visualization.jade',
[certTypes.fullStack]: 'certificate/legacy/full-stack.jade',
[certTypes.respWebDesign]: 'certificate/responsive-web-design.jade',
[certTypes.frontEndLibs]: 'certificate/front-end-libraries.jade',
[certTypes.jsAlgoDataStruct]:
'certificate/javascript-algorithms-and-data-structures.jade',
[certTypes.dataVis2018]: 'certificate/data-visualization.jade',
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
[certTypes.infosecQa]:
'certificate/information-security-and-quality-assurance.jade'
};
const certText = {
[certTypes.frontEnd]: 'Legacy Front End certified',
[certTypes.backEnd]: 'Legacy Back End Certified',
[certTypes.dataVis]: 'Legacy Data Visualization Certified',
[certTypes.fullStack]: 'Legacy Full Stack Certified',
[certTypes.respWebDesign]: 'Responsive Web Design Certified',
[certTypes.frontEndLibs]: 'Front End Libraries Certified',
[certTypes.jsAlgoDataStruct]:
'JavaScript Algorithms and Data Structures Certified',
[certTypes.dataVis2018]: 'Data Visualization Certified',
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
};
function getIdsForCert$(id, Challenge) {
return observeQuery(
Challenge,
@ -117,13 +157,24 @@ function sendCertifiedEmail(
export default function certificate(app) {
const router = app.loopback.Router();
const { Email, Challenge } = app.models;
const { Email, Challenge, User } = app.models;
function findUserByUsername$(username, fields) {
return observeQuery(
User,
'findOne',
{
where: { username },
fields
}
);
}
const certTypeIds = {
// legacy
[certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge),
[certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge),
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge),
[certTypes.frontEnd]: getIdsForCert$(legacyFrontEndChallengeId, Challenge),
[certTypes.backEnd]: getIdsForCert$(legacyBackEndChallengeId, Challenge),
[certTypes.dataVis]: getIdsForCert$(legacyDataVisId, Challenge),
// modern
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
@ -145,6 +196,10 @@ export default function certificate(app) {
ifNoSuperBlock404,
verifyCert
);
router.get(
'/c/:username/:cert',
showCert
);
app.use(router);
@ -172,16 +227,12 @@ export default function certificate(app) {
function verifyCert(req, res, next) {
const { body: { superBlock }, user } = req;
log(superBlock);
let certType = superBlockCertTypeMap[superBlock];
log(certType);
if (certType === 'isDataVisCert') {
certType = 'is2018DataVisCert';
log(certType);
}
return user.getChallengeMap$()
.flatMap(() => certTypeIds[certType])
.flatMap(challenge => {
.flatMap(() => certTypeIds[certType])
.flatMap(challenge => {
const {
id,
tests,
@ -251,4 +302,101 @@ export default function certificate(app) {
}
return res.status(404).end();
}
function showCert(req, res, next) {
let { username, cert } = req.params;
username = username.toLowerCase();
const certType = superBlockCertTypeMap[cert];
const certId = certIds[certType];
return findUserByUsername$(
username,
{
isCheater: true,
isLocked: true,
isFrontEndCert: true,
isBackEndCert: true,
isFullStackCert: true,
isRespWebDesignCert: true,
isFrontEndLibsCert: true,
isJsAlgoDataStructCert: true,
isDataVisCert: true,
is2018DataVisCert: true,
isApisMicroservicesCert: true,
isInfosecQaCert: true,
isHonest: true,
username: true,
name: true,
challengeMap: true
}
)
.subscribe(
user => {
const profile = `/${user.username}`;
if (!user) {
req.flash(
'danger',
`We couldn't find a user with the username ${username}`
);
return res.redirect('/');
}
if (!user.name) {
req.flash(
'danger',
dedent`
This user needs to add their name to their account
in order for others to be able to view their certificate.
`
);
return res.redirect(profile);
}
if (user.isCheater) {
return res.redirect(`/${user.username}`);
}
if (user.isLocked) {
req.flash(
'danger',
dedent`
${username} has chosen to make their profile
private. They will need to make their profile public
in order for others to be able to view their certificate.
`
);
return res.redirect('/');
}
if (!user.isHonest) {
req.flash(
'danger',
dedent`
${username} has not yet agreed to our Academic Honesty Pledge.
`
);
return res.redirect(profile);
}
if (user[certType]) {
const { challengeMap = {} } = user;
const { completedDate = new Date() } = challengeMap[certId] || {};
return res.render(
certViews[certType],
{
username: user.username,
date: moment(new Date(completedDate)).format('MMMM D, YYYY'),
name: user.name
}
);
}
req.flash(
'danger',
`Looks like user ${username} is not ${certText[certType]}`
);
return res.redirect(profile);
},
next
);
}
}

View File

@ -1,88 +1,22 @@
import dedent from 'dedent';
import moment from 'moment-timezone';
import debugFactory from 'debug';
import { curry } from 'lodash';
import {
frontEndChallengeId,
backEndChallengeId,
respWebDesignId,
frontEndLibsId,
jsAlgoDataStructId,
dataVisId,
dataVis2018Id,
apisMicroservicesId,
infosecQaId
} from '../utils/constantStrings.json';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
import {
ifNoUser401,
ifNoUserRedirectTo,
ifNotVerifiedRedirectToSettings
} from '../utils/middleware';
import { observeQuery } from '../utils/rx';
const debug = debugFactory('fcc:boot:user');
const sendNonUserToMap = ifNoUserRedirectTo('/map');
const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/map');
const certIds = {
[certTypes.frontEnd]: frontEndChallengeId,
[certTypes.backEnd]: backEndChallengeId,
[certTypes.respWebDesign]: respWebDesignId,
[certTypes.frontEndLibs]: frontEndLibsId,
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
[certTypes.dataVis]: dataVisId,
[certTypes.dataVis2018]: dataVis2018Id,
[certTypes.apisMicroservices]: apisMicroservicesId,
[certTypes.infosecQa]: infosecQaId
};
const certViews = {
[certTypes.frontEnd]: 'certificate/front-end.jade',
[certTypes.backEnd]: 'certificate/back-end.jade',
[certTypes.fullStack]: 'certificate/full-stack.jade',
[certTypes.respWebDesign]: 'certificate/responsive-web-design.jade',
[certTypes.frontEndLibs]: 'certificate/front-end-libraries.jade',
[certTypes.jsAlgoDataStruct]:
'certificate/javascript-algorithms-and-data-structures.jade',
[certTypes.dataVis]: 'certificate/data-visualization.jade',
[certTypes.dataVis2018]: 'certificate/data-visualization-2018.jade',
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
[certTypes.infosecQa]:
'certificate/information-security-and-quality-assurance.jade'
};
const certText = {
[certTypes.frontEnd]: 'Front End certified',
[certTypes.backEnd]: 'Back End Certified',
[certTypes.fullStack]: 'Full Stack Certified',
[certTypes.respWebDesign]: 'Responsive Web Design Certified',
[certTypes.frontEndLibs]: 'Front End Libraries Certified',
[certTypes.jsAlgoDataStruct]:
'JavaScript Algorithms and Data Structures Certified',
[certTypes.dataVis]: 'Data Visualization Certified',
[certTypes.dataVis2018]: 'Data Visualization Certified',
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
};
module.exports = function(app) {
const router = app.loopback.Router();
const api = app.loopback.Router();
const { Email, User } = app.models;
function findUserByUsername$(username, fields) {
return observeQuery(
User,
'findOne',
{
where: { username },
fields
}
);
}
api.post(
'/account/delete',
ifNoUser401,
@ -105,11 +39,6 @@ module.exports = function(app) {
);
// Ensure these are the last routes!
api.get(
'/c/:username/:cert',
showCert
);
router.get(
'/user/:username/report-user/',
sendNonUserToMapWithMessage('You must be signed in to report a user'),
@ -185,100 +114,6 @@ module.exports = function(app) {
});
}
function showCert(req, res, next) {
let { username, cert } = req.params;
username = username.toLowerCase();
const certType = superBlockCertTypeMap[cert];
const certId = certIds[certType];
return findUserByUsername$(username, {
isCheater: true,
isLocked: true,
isFrontEndCert: true,
isBackEndCert: true,
isFullStackCert: true,
isRespWebDesignCert: true,
isFrontEndLibsCert: true,
isJsAlgoDataStructCert: true,
isDataVisCert: true,
is2018DataVisCert: true,
isApisMicroservicesCert: true,
isInfosecQaCert: true,
isHonest: true,
username: true,
name: true,
challengeMap: true
})
.subscribe(
user => {
const profile = `/${user.username}`;
if (!user) {
req.flash(
'danger',
`We couldn't find a user with the username ${username}`
);
return res.redirect('/');
}
if (!user.name) {
req.flash(
'danger',
dedent`
This user needs to add their name to their account
in order for others to be able to view their certificate.
`
);
return res.redirect(profile);
}
if (user.isCheater) {
return res.redirect(`/${user.username}`);
}
if (user.isLocked) {
req.flash(
'danger',
dedent`
${username} has chosen to make their profile
private. They will need to make their profile public
in order for others to be able to view their certificate.
`
);
return res.redirect('/');
}
if (!user.isHonest) {
req.flash(
'danger',
dedent`
${username} has not yet agreed to our Academic Honesty Pledge.
`
);
return res.redirect(profile);
}
if (user[certType]) {
const { challengeMap = {} } = user;
const { completedDate = new Date() } = challengeMap[certId] || {};
return res.render(
certViews[certType],
{
username: user.username,
date: moment(new Date(completedDate)).format('MMMM D, YYYY'),
name: user.name
}
);
}
req.flash(
'danger',
`Looks like user ${username} is not ${certText[certType]}`
);
return res.redirect(profile);
},
next
);
}
function postDeleteAccount(req, res, next) {
User.destroyById(req.user.id, function(err) {
if (err) { return next(err); }

View File

@ -1,9 +1,9 @@
{
"gitHubUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1521.3 Safari/537.36",
"frontEndChallengeId": "561add10cb82ac38a17513be",
"backEndChallengeId": "660add10cb82ac38a17513be",
"dataVisId": "561add10cb82ac39a17513bc",
"legacyFrontEndChallengeId": "561add10cb82ac38a17513be",
"legacyBackEndChallengeId": "660add10cb82ac38a17513be",
"legacyDataVisId": "561add10cb82ac39a17513bc",
"respWebDesignId": "561add10cb82ac38a17513bc",
"frontEndLibsId": "561acd10cb82ac38a17513bc",

View File

@ -15,6 +15,7 @@ export const publicUserProps = [
'isApisMicroservicesCert',
'isBackEndCert',
'isCheater',
'is2018DataVisCert',
'isDataVisCert',
'isFrontEndCert',
'isFullStackCert',

View File

@ -2,16 +2,16 @@ import certTypes from './certTypes.json';
const superBlockCertTypeMap = {
// legacy
'front-end': certTypes.frontEnd,
'back-end': certTypes.backEnd,
'data-visualization': certTypes.dataVis,
'full-stack': certTypes.fullStack,
'legacy-front-end': certTypes.frontEnd,
'legacy-back-end': certTypes.backEnd,
'legacy-data-visualization': certTypes.dataVis,
'legacy-full-stack': certTypes.fullStack,
// modern
'responsive-web-design': certTypes.respWebDesign,
'javascript-algorithms-and-data-structures': certTypes.jsAlgoDataStruct,
'front-end-libraries': certTypes.frontEndLibs,
'data-visualization-2018': certTypes.dataVis2018,
'data-visualization': certTypes.dataVis2018,
'apis-and-microservices': certTypes.apisMicroservices,
'information-security-and-quality-assurance': certTypes.infosecQa
};

View File

@ -29,4 +29,4 @@ include styles
strong Quincy Larson
p Executive Director, freeCodeCamp.org
.row
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/advanced-front-end-certification
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/advanced-front-end

View File

@ -29,4 +29,4 @@ include styles
strong Quincy Larson
p Executive Director, freeCodeCamp.org
.row
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/apis-and-microservices-certification
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/apis-and-microservices

View File

@ -29,4 +29,4 @@ include styles
strong Quincy Larson
p Executive Director, freeCodeCamp.org
.row
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/data-visualization-certification
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/data-visualization

View File

@ -29,4 +29,4 @@ include styles
strong Quincy Larson
p Executive Director, freeCodeCamp.org
.row
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/front-end-libraries-certification
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/front-end-libraries

View File

@ -29,4 +29,4 @@ include styles
strong Quincy Larson
p Executive Director, freeCodeCamp.org
.row
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/information-security-and-quality-assurance-certification
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/information-security-and-quality-assurance

View File

@ -29,4 +29,4 @@ include styles
strong Quincy Larson
p Executive Director, freeCodeCamp.org
.row
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/javascript-algorithms-and-data-structures-certification
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/javascript-algorithms-and-data-structures

View File

@ -1,6 +1,6 @@
meta(name='viewport', content='width=device-width, initial-scale=1')
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css')
include styles
include ../styles
.certificate-wrapper.container
.row
@ -29,4 +29,4 @@ include styles
strong Quincy Larson
p Executive Director, freeCodeCamp.org
.row
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/back-end-certification
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/legacy-back-end

View File

@ -0,0 +1,32 @@
meta(name='viewport', content='width=device-width, initial-scale=1')
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css')
include ../styles
.certificate-wrapper.container
.row
header
.col-md-5.col-sm-12
.logo
img(class='img-responsive', src='https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg', alt="freeCodeCamp's Logo")
.col-md-7.col-sm-12
.issue-date Issued&nbsp;
strong #{date}
section.information
.information-container
h3 This certifies that
h1
strong= name
h3 has successfully completed freeCodeCamp's
h1
strong Data Visualization Projects
h4 1 of 3 legacy freeCodeCamp certificates, representing approximately 400 hours of coursework
footer
.row.signatures
img(class='img-responsive', src='https://i.imgur.com/OJFVJKg.png', alt="Quincy Larson's Signature")
p
strong Quincy Larson
p Executive Director, freeCodeCamp.org
.row
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/legacy-data-visualization

View File

@ -1,6 +1,6 @@
meta(name='viewport', content='width=device-width, initial-scale=1')
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css')
include styles
include ../styles
.certificate-wrapper.container
.row
@ -29,4 +29,4 @@ include styles
strong Quincy Larson
p Executive Director, freeCodeCamp.org
.row
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/front-end-certification
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/legacy-front-end

View File

@ -1,6 +1,6 @@
meta(name='viewport', content='width=device-width, initial-scale=1')
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css')
include styles
include ../styles
.certificate-wrapper.container
.row
@ -19,8 +19,8 @@ include styles
strong= name
h3 has successfully completed freeCodeCamp's
h1
strong Full Stack Development Projects
h4 1 of 3 legacy freeCodeCamp certificates, representing approximately 400 hours of coursework
strong Legacy Full Stack Development Program
h4 All three of the legacy freeCodeCamp certificates, representing approximately 12000 hours of coursework
footer
.row.signatures
@ -29,4 +29,4 @@ include styles
strong Quincy Larson
p Executive Director, freeCodeCamp.org
.row
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/full-stack-certification
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/legacy-full-stack

View File

@ -29,4 +29,4 @@ include styles
strong Quincy Larson
p Executive Director, freeCodeCamp.org
.row
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/responsive-web-design-certification
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/responsive-web-design