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 React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import _ from 'lodash'; import { values as _values, isString, findIndex } from 'lodash';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -12,11 +12,17 @@ import JSAlgoAndDSForm from './JSAlgoAndDSForm.jsx';
import SectionHeader from './SectionHeader.jsx'; import SectionHeader from './SectionHeader.jsx';
import { projectsSelector } from '../../../entities'; import { projectsSelector } from '../../../entities';
import { claimCert, updateUserBackend } from '../redux'; import { claimCert, updateUserBackend } from '../redux';
import { fetchChallenges, userSelector, hardGoTo } from '../../../redux'; import {
fetchChallenges,
userSelector,
hardGoTo,
createErrorObservable
} from '../../../redux';
import { import {
buildUserProjectsMap, buildUserProjectsMap,
jsProjectSuperBlock jsProjectSuperBlock
} from '../utils/buildUserProjectsMap'; } from '../utils/buildUserProjectsMap';
import legacyProjects from '../utils/legacyProjectData';
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
userSelector, userSelector,
@ -30,12 +36,15 @@ const mapStateToProps = createSelector(
isJsAlgoDataStructCert, isJsAlgoDataStructCert,
isApisMicroservicesCert, isApisMicroservicesCert,
isInfosecQaCert, isInfosecQaCert,
isFrontEndCert,
isBackEndCert,
isDataVisCert,
username username
}, },
projects projects
) => ({ ) => ({
projects, projects,
userProjects: projects userProjects: projects.concat(legacyProjects)
.map(block => buildUserProjectsMap(block, challengeMap)) .map(block => buildUserProjectsMap(block, challengeMap))
.reduce((projects, current) => ({ .reduce((projects, current) => ({
...projects, ...projects,
@ -49,7 +58,10 @@ const mapStateToProps = createSelector(
'Front End Libraries Projects': isFrontEndLibsCert, 'Front End Libraries Projects': isFrontEndLibsCert,
'Data Visualization Projects': is2018DataVisCert, 'Data Visualization Projects': is2018DataVisCert,
'API and Microservice Projects': isApisMicroservicesCert, '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 username
}) })
@ -58,6 +70,7 @@ const mapStateToProps = createSelector(
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return bindActionCreators({ return bindActionCreators({
claimCert, claimCert,
createError: createErrorObservable,
fetchChallenges, fetchChallenges,
hardGoTo, hardGoTo,
updateUserBackend updateUserBackend
@ -67,6 +80,7 @@ function mapDispatchToProps(dispatch) {
const propTypes = { const propTypes = {
blockNameIsCertMap: PropTypes.objectOf(PropTypes.bool), blockNameIsCertMap: PropTypes.objectOf(PropTypes.bool),
claimCert: PropTypes.func.isRequired, claimCert: PropTypes.func.isRequired,
createError: PropTypes.func.isRequired,
fetchChallenges: PropTypes.func.isRequired, fetchChallenges: PropTypes.func.isRequired,
hardGoTo: PropTypes.func.isRequired, hardGoTo: PropTypes.func.isRequired,
projects: PropTypes.arrayOf( projects: PropTypes.arrayOf(
@ -88,11 +102,15 @@ const propTypes = {
username: PropTypes.string username: PropTypes.string
}; };
const compareSuperBlockWith = id => p => p.superBlock === id;
class CertificationSettings extends PureComponent { class CertificationSettings extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.buildProjectForms = this.buildProjectForms.bind(this);
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
this.isProjectSectionCompleted = this.isProjectSectionCompleted.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -102,60 +120,18 @@ class CertificationSettings extends PureComponent {
} }
} }
handleSubmit(values) { buildProjectForms({
const { id } = values; projectBlockName,
const fullForm = _.values(values) challenges,
.filter(Boolean) superBlock
.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) {
return this.props.claimCert(id);
}
const { projects } = this.props;
const pIndex = _.findIndex(projects, p => p.superBlock === id);
values.nameToIdMap = projects[pIndex].challengeNameIdMap;
return this.props.updateUserBackend({
projects: {
[id]: values
}
});
}
render() {
const { const {
blockNameIsCertMap, blockNameIsCertMap,
claimCert, claimCert,
hardGoTo, hardGoTo,
projects,
userProjects, userProjects,
username username
} = this.props; } = this.props;
if (!projects.length) {
return null;
}
return (
<div>
<SectionHeader>
Certification Settings
</SectionHeader>
<FullWidthRow>
<p>
Add links to the live demos of your projects as you finish them.
Then, once you have added all 5 projects required for a certificate,
you can claim it.
</p>
</FullWidthRow>
{
projects.map(({
projectBlockName,
challenges,
superBlock
}) => {
const isCertClaimed = blockNameIsCertMap[projectBlockName]; const isCertClaimed = blockNameIsCertMap[projectBlockName];
if (superBlock === jsProjectSuperBlock) { if (superBlock === jsProjectSuperBlock) {
return ( return (
@ -193,9 +169,9 @@ class CertificationSettings extends PureComponent {
[current]: '' [current]: ''
}), {}); }), {});
const completedProjects = _.values(userValues) const completedProjects = _values(userValues)
.filter(Boolean) .filter(Boolean)
.filter(_.isString) .filter(isString)
// minus 1 to account for the id // minus 1 to account for the id
.length - 1; .length - 1;
@ -231,7 +207,101 @@ class CertificationSettings extends PureComponent {
<hr /> <hr />
</FullWidthRow> </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;
if (this.isProjectSectionCompleted(values)) {
return this.props.claimCert(id);
}
const { projects } = this.props;
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
}
});
}
render() {
const {
projects
} = this.props;
if (!projects.length) {
return null;
}
return (
<div>
<SectionHeader>
Certification Settings
</SectionHeader>
<FullWidthRow>
<p>
Add links to the live demos of your projects as you finish them.
Then, once you have added all 5 projects required for a certificate,
you can claim it.
</p>
</FullWidthRow>
{
projects.map(this.buildProjectForms)
}
<SectionHeader>
Legacy Certificate Settings
</SectionHeader>
{
legacyProjects.map(this.buildProjectForms)
} }
</div> </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 _ from 'lodash';
import loopback from 'loopback'; import loopback from 'loopback';
import moment from 'moment-timezone';
import path from 'path'; import path from 'path';
import dedent from 'dedent'; import dedent from 'dedent';
import { Observable } from 'rx'; import { Observable } from 'rx';
@ -13,26 +14,23 @@ import {
import { observeQuery } from '../utils/rx'; import { observeQuery } from '../utils/rx';
import { import {
// legacy legacyFrontEndChallengeId,
frontEndChallengeId, legacyBackEndChallengeId,
backEndChallengeId, legacyDataVisId,
dataVisId,
// modern
respWebDesignId, respWebDesignId,
frontEndLibsId, frontEndLibsId,
dataVis2018Id,
jsAlgoDataStructId, jsAlgoDataStructId,
dataVis2018Id,
apisMicroservicesId, apisMicroservicesId,
infosecQaId infosecQaId
} from '../utils/constantStrings.json'; } from '../utils/constantStrings.json';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
import { import {
completeCommitment$ completeCommitment$
} from '../utils/commit'; } from '../utils/commit';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
const log = debug('fcc:certification'); const log = debug('fcc:certification');
const renderCertifedEmail = loopback.template(path.join( const renderCertifedEmail = loopback.template(path.join(
__dirname, __dirname,
@ -46,6 +44,48 @@ function isCertified(ids, challengeMap = {}) {
return _.every(ids, ({ id }) => _.has(challengeMap, id)); 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) { function getIdsForCert$(id, Challenge) {
return observeQuery( return observeQuery(
Challenge, Challenge,
@ -117,13 +157,24 @@ function sendCertifiedEmail(
export default function certificate(app) { export default function certificate(app) {
const router = app.loopback.Router(); 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 = { const certTypeIds = {
// legacy // legacy
[certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge), [certTypes.frontEnd]: getIdsForCert$(legacyFrontEndChallengeId, Challenge),
[certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge), [certTypes.backEnd]: getIdsForCert$(legacyBackEndChallengeId, Challenge),
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge), [certTypes.dataVis]: getIdsForCert$(legacyDataVisId, Challenge),
// modern // modern
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge), [certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
@ -145,6 +196,10 @@ export default function certificate(app) {
ifNoSuperBlock404, ifNoSuperBlock404,
verifyCert verifyCert
); );
router.get(
'/c/:username/:cert',
showCert
);
app.use(router); app.use(router);
@ -172,13 +227,9 @@ export default function certificate(app) {
function verifyCert(req, res, next) { function verifyCert(req, res, next) {
const { body: { superBlock }, user } = req; const { body: { superBlock }, user } = req;
log(superBlock);
let certType = superBlockCertTypeMap[superBlock]; let certType = superBlockCertTypeMap[superBlock];
log(certType); log(certType);
if (certType === 'isDataVisCert') {
certType = 'is2018DataVisCert';
log(certType);
}
return user.getChallengeMap$() return user.getChallengeMap$()
.flatMap(() => certTypeIds[certType]) .flatMap(() => certTypeIds[certType])
.flatMap(challenge => { .flatMap(challenge => {
@ -251,4 +302,101 @@ export default function certificate(app) {
} }
return res.status(404).end(); 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 dedent from 'dedent';
import moment from 'moment-timezone';
import debugFactory from 'debug'; import debugFactory from 'debug';
import { curry } from 'lodash'; 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 { import {
ifNoUser401, ifNoUser401,
ifNoUserRedirectTo, ifNoUserRedirectTo,
ifNotVerifiedRedirectToSettings ifNotVerifiedRedirectToSettings
} from '../utils/middleware'; } from '../utils/middleware';
import { observeQuery } from '../utils/rx';
const debug = debugFactory('fcc:boot:user'); const debug = debugFactory('fcc:boot:user');
const sendNonUserToMap = ifNoUserRedirectTo('/map'); const sendNonUserToMap = ifNoUserRedirectTo('/map');
const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/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) { module.exports = function(app) {
const router = app.loopback.Router(); const router = app.loopback.Router();
const api = app.loopback.Router(); const api = app.loopback.Router();
const { Email, User } = app.models; const { Email, User } = app.models;
function findUserByUsername$(username, fields) {
return observeQuery(
User,
'findOne',
{
where: { username },
fields
}
);
}
api.post( api.post(
'/account/delete', '/account/delete',
ifNoUser401, ifNoUser401,
@ -105,11 +39,6 @@ module.exports = function(app) {
); );
// Ensure these are the last routes! // Ensure these are the last routes!
api.get(
'/c/:username/:cert',
showCert
);
router.get( router.get(
'/user/:username/report-user/', '/user/:username/report-user/',
sendNonUserToMapWithMessage('You must be signed in to report a 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) { function postDeleteAccount(req, res, next) {
User.destroyById(req.user.id, function(err) { User.destroyById(req.user.id, function(err) {
if (err) { return next(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", "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", "legacyFrontEndChallengeId": "561add10cb82ac38a17513be",
"backEndChallengeId": "660add10cb82ac38a17513be", "legacyBackEndChallengeId": "660add10cb82ac38a17513be",
"dataVisId": "561add10cb82ac39a17513bc", "legacyDataVisId": "561add10cb82ac39a17513bc",
"respWebDesignId": "561add10cb82ac38a17513bc", "respWebDesignId": "561add10cb82ac38a17513bc",
"frontEndLibsId": "561acd10cb82ac38a17513bc", "frontEndLibsId": "561acd10cb82ac38a17513bc",

View File

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

View File

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

View File

@ -29,4 +29,4 @@ include styles
strong Quincy Larson strong Quincy Larson
p Executive Director, freeCodeCamp.org p Executive Director, freeCodeCamp.org
.row .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 strong Quincy Larson
p Executive Director, freeCodeCamp.org p Executive Director, freeCodeCamp.org
.row .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 strong Quincy Larson
p Executive Director, freeCodeCamp.org p Executive Director, freeCodeCamp.org
.row .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 strong Quincy Larson
p Executive Director, freeCodeCamp.org p Executive Director, freeCodeCamp.org
.row .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 strong Quincy Larson
p Executive Director, freeCodeCamp.org p Executive Director, freeCodeCamp.org
.row .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 strong Quincy Larson
p Executive Director, freeCodeCamp.org p Executive Director, freeCodeCamp.org
.row .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') 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') 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 .certificate-wrapper.container
.row .row
@ -29,4 +29,4 @@ include styles
strong Quincy Larson strong Quincy Larson
p Executive Director, freeCodeCamp.org p Executive Director, freeCodeCamp.org
.row .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') 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') 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 .certificate-wrapper.container
.row .row
@ -29,4 +29,4 @@ include styles
strong Quincy Larson strong Quincy Larson
p Executive Director, freeCodeCamp.org p Executive Director, freeCodeCamp.org
.row .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') 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') 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 .certificate-wrapper.container
.row .row
@ -19,8 +19,8 @@ include styles
strong= name strong= name
h3 has successfully completed freeCodeCamp's h3 has successfully completed freeCodeCamp's
h1 h1
strong Full Stack Development Projects strong Legacy Full Stack Development Program
h4 1 of 3 legacy freeCodeCamp certificates, representing approximately 400 hours of coursework h4 All three of the legacy freeCodeCamp certificates, representing approximately 12000 hours of coursework
footer footer
.row.signatures .row.signatures
@ -29,4 +29,4 @@ include styles
strong Quincy Larson strong Quincy Larson
p Executive Director, freeCodeCamp.org p Executive Director, freeCodeCamp.org
.row .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 strong Quincy Larson
p Executive Director, freeCodeCamp.org p Executive Director, freeCodeCamp.org
.row .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