diff --git a/common/app/routes/Settings/components/Cert-Settings.jsx b/common/app/routes/Settings/components/Cert-Settings.jsx index f2214250ca..40af0960e3 100644 --- a/common/app/routes/Settings/components/Cert-Settings.jsx +++ b/common/app/routes/Settings/components/Cert-Settings.jsx @@ -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 ( + + ); + } + 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 ( + +

{ projectBlockName }

+
+ { + isCertClaimed ? + : + null + } +
+ + ); + } + + 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.

- { - projects.map(({ - projectBlockName, - challenges, - superBlock - }) => { - const isCertClaimed = blockNameIsCertMap[projectBlockName]; - if (superBlock === jsProjectSuperBlock) { - return ( - - ); - } - 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 ( - -

{ projectBlockName }

- - { - isCertClaimed ? - : - null - } -
-
- ); - }) + { + projects.map(this.buildProjectForms) + } + + Legacy Certificate Settings + + { + legacyProjects.map(this.buildProjectForms) } ); diff --git a/common/app/routes/Settings/utils/legacyProjectData.js b/common/app/routes/Settings/utils/legacyProjectData.js new file mode 100644 index 0000000000..ed70bee2d7 --- /dev/null +++ b/common/app/routes/Settings/utils/legacyProjectData.js @@ -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; diff --git a/seed/challenges/09-certificates/legacy-back-end-certificate.json b/seed/challenges/09-certificates/legacy-back-end-certificate.json new file mode 100644 index 0000000000..8c09367c6c --- /dev/null +++ b/seed/challenges/09-certificates/legacy-back-end-certificate.json @@ -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" + } + ] + } + ] +} \ No newline at end of file diff --git a/seed/challenges/09-certificates/legacy-data-visualization-certificate.json b/seed/challenges/09-certificates/legacy-data-visualization-certificate.json new file mode 100644 index 0000000000..673c901d27 --- /dev/null +++ b/seed/challenges/09-certificates/legacy-data-visualization-certificate.json @@ -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" + } + ] + } + ] +} \ No newline at end of file diff --git a/seed/challenges/09-certificates/legacy-front-end-certificate.json b/seed/challenges/09-certificates/legacy-front-end-certificate.json new file mode 100644 index 0000000000..da0a9d1f2e --- /dev/null +++ b/seed/challenges/09-certificates/legacy-front-end-certificate.json @@ -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" + } + ] + } + ] +} \ No newline at end of file diff --git a/server/boot/certificate.js b/server/boot/certificate.js index ce4162f614..11243e2aa6 100644 --- a/server/boot/certificate.js +++ b/server/boot/certificate.js @@ -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 + ); + } } diff --git a/server/boot/user.js b/server/boot/user.js index add4f91cb5..1e901c31a7 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -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); } diff --git a/server/utils/constantStrings.json b/server/utils/constantStrings.json index 78d72a5a01..74303fe654 100644 --- a/server/utils/constantStrings.json +++ b/server/utils/constantStrings.json @@ -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", diff --git a/server/utils/publicUserProps.js b/server/utils/publicUserProps.js index 964cad0ab9..36a153234d 100644 --- a/server/utils/publicUserProps.js +++ b/server/utils/publicUserProps.js @@ -15,6 +15,7 @@ export const publicUserProps = [ 'isApisMicroservicesCert', 'isBackEndCert', 'isCheater', + 'is2018DataVisCert', 'isDataVisCert', 'isFrontEndCert', 'isFullStackCert', diff --git a/server/utils/superBlockCertTypeMap.js b/server/utils/superBlockCertTypeMap.js index 49c91e1aed..9d62df67be 100644 --- a/server/utils/superBlockCertTypeMap.js +++ b/server/utils/superBlockCertTypeMap.js @@ -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 }; diff --git a/server/views/certificate/advanced-front-end.jade b/server/views/certificate/advanced-front-end.jade index 78f781f4b4..01a76199f1 100644 --- a/server/views/certificate/advanced-front-end.jade +++ b/server/views/certificate/advanced-front-end.jade @@ -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 \ No newline at end of file diff --git a/server/views/certificate/apis-and-microservices.jade b/server/views/certificate/apis-and-microservices.jade index 6a37a5722a..8fcdd5e371 100644 --- a/server/views/certificate/apis-and-microservices.jade +++ b/server/views/certificate/apis-and-microservices.jade @@ -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 diff --git a/server/views/certificate/data-visualization.jade b/server/views/certificate/data-visualization.jade index 0782d408aa..c0570958e1 100644 --- a/server/views/certificate/data-visualization.jade +++ b/server/views/certificate/data-visualization.jade @@ -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 diff --git a/server/views/certificate/front-end-libraries.jade b/server/views/certificate/front-end-libraries.jade index f5d4206fd2..62a4e99e7d 100644 --- a/server/views/certificate/front-end-libraries.jade +++ b/server/views/certificate/front-end-libraries.jade @@ -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 diff --git a/server/views/certificate/information-security-and-quality-assurance.jade b/server/views/certificate/information-security-and-quality-assurance.jade index 0a71cf3b97..9215bc4902 100644 --- a/server/views/certificate/information-security-and-quality-assurance.jade +++ b/server/views/certificate/information-security-and-quality-assurance.jade @@ -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 diff --git a/server/views/certificate/javascript-algorithms-and-data-structures.jade b/server/views/certificate/javascript-algorithms-and-data-structures.jade index ebe72a1f44..2c821113e4 100644 --- a/server/views/certificate/javascript-algorithms-and-data-structures.jade +++ b/server/views/certificate/javascript-algorithms-and-data-structures.jade @@ -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 diff --git a/server/views/certificate/back-end.jade b/server/views/certificate/legacy/back-end.jade similarity index 95% rename from server/views/certificate/back-end.jade rename to server/views/certificate/legacy/back-end.jade index e94d39a4ad..56c3a29266 100644 --- a/server/views/certificate/back-end.jade +++ b/server/views/certificate/legacy/back-end.jade @@ -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 diff --git a/server/views/certificate/legacy/data-visualization.jade b/server/views/certificate/legacy/data-visualization.jade new file mode 100644 index 0000000000..640bf815d8 --- /dev/null +++ b/server/views/certificate/legacy/data-visualization.jade @@ -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  + 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 diff --git a/server/views/certificate/front-end.jade b/server/views/certificate/legacy/front-end.jade similarity index 94% rename from server/views/certificate/front-end.jade rename to server/views/certificate/legacy/front-end.jade index 1179721bb0..d68947129e 100644 --- a/server/views/certificate/front-end.jade +++ b/server/views/certificate/legacy/front-end.jade @@ -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 diff --git a/server/views/certificate/full-stack.jade b/server/views/certificate/legacy/full-stack.jade similarity index 80% rename from server/views/certificate/full-stack.jade rename to server/views/certificate/legacy/full-stack.jade index 30782f0137..c8f4f233a4 100644 --- a/server/views/certificate/full-stack.jade +++ b/server/views/certificate/legacy/full-stack.jade @@ -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 \ No newline at end of file diff --git a/server/views/certificate/responsive-web-design.jade b/server/views/certificate/responsive-web-design.jade index 2873106ba0..cc4eb37871 100644 --- a/server/views/certificate/responsive-web-design.jade +++ b/server/views/certificate/responsive-web-design.jade @@ -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