diff --git a/common/app/entities/index.js b/common/app/entities/index.js index bb8aeda1c1..e479dfaa4a 100644 --- a/common/app/entities/index.js +++ b/common/app/entities/index.js @@ -1,4 +1,4 @@ -import { findIndex, invert, pick, property, merge, union } from 'lodash'; +import { findIndex, property, merge, union } from 'lodash'; import uuid from 'uuid/v4'; import { combineActions, @@ -12,6 +12,7 @@ import { themes } from '../../utils/themes'; import { usernameSelector, types as app } from '../redux'; import { types as challenges } from '../routes/Challenges/redux'; import { types as map } from '../Map/redux'; +import legacyProjects from '../../utils/legacyProjectData'; export const ns = 'entities'; export const getNS = state => state[ns]; @@ -91,10 +92,6 @@ const defaultState = { fullBlocks: [] }; -export function selectiveChallengeTitleSelector(state, dashedName) { - return getNS(state).challenge[dashedName].title; -} - export function portfolioSelector(state, props) { const username = usernameSelector(state); const { portfolio } = getNS(state).user[username]; @@ -103,27 +100,42 @@ export function portfolioSelector(state, props) { } export function projectsSelector(state) { - const blocks = getNS(state).block; - const challengeNameToIdMap = invert(challengeIdToNameMapSelector(state)); + const { + block: blocks, + challenge: challengeMap + } = getNS(state); + const idToNameMap = challengeIdToNameMapSelector(state); + const legacyWithDashedNames = legacyProjects + .reduce((list, current) => ([ + ...list, + { + ...current, + challenges: current.challenges.map(id => idToNameMap[id]) + } + ]), + [] + ); return Object.keys(blocks) .filter(key => key.includes('projects') && !key.includes('coding-interview') ) .map(key => blocks[key]) + .concat(legacyWithDashedNames) .map(({ title, challenges, superBlock }) => { const projectChallengeDashNames = challenges + // challengeIdToName is not available on appMount + .filter(Boolean) // remove any project intros .filter(chal => !chal.includes('get-set-for')); const projectChallenges = projectChallengeDashNames - .map(dashedName => selectiveChallengeTitleSelector(state, dashedName)); + .map(dashedName => { + const { id, title } = challengeMap[dashedName]; + return { id, title, dashedName }; + }); return { projectBlockName: title, superBlock, - challenges: projectChallenges, - challengeNameIdMap: pick( - challengeNameToIdMap, - projectChallengeDashNames - ) + challenges: projectChallenges }; }); } diff --git a/common/app/routes/Profile/components/Certificates.jsx b/common/app/routes/Profile/components/Certificates.jsx index 06d3ed6891..cecca5d961 100644 --- a/common/app/routes/Profile/components/Certificates.jsx +++ b/common/app/routes/Profile/components/Certificates.jsx @@ -61,7 +61,7 @@ const mapStateToProps = createSelector( { show: is2018DataVisCert, title: 'Data Visualization Certificate:', - showURL: 'data-visualization-2018' + showURL: 'data-visualization' }, { show: isApisMicroservicesCert, @@ -78,22 +78,22 @@ const mapStateToProps = createSelector( { show: isFullStackCert, title: 'Full Stack Certificate:', - showURL: 'full-stack' + showURL: 'legacy-full-stack' }, { show: isFrontEndCert, title: 'Front End Certificate:', - showURL: 'front-end' + showURL: 'legacy-front-end' }, { show: isBackEndCert, title: 'Back End Certificate:', - showURL: 'back-end' + showURL: 'legacy-back-end' }, { show: isDataVisCert, title: 'Data Visualization Certificate:', - showURL: 'data-visualization' + showURL: 'legacy-data-visualization' } ] }) @@ -134,7 +134,7 @@ function renderCertShow(username, cert) { block={ true } bsSize='lg' bsStyle='primary' - href={ `/c/${username}/${cert.showURL}`} + href={ `/certificates/${username}/${cert.showURL}`} > Show diff --git a/common/app/routes/Settings/components/Cert-Settings.jsx b/common/app/routes/Settings/components/Cert-Settings.jsx index f2214250ca..c926c4cb51 100644 --- a/common/app/routes/Settings/components/Cert-Settings.jsx +++ b/common/app/routes/Settings/components/Cert-Settings.jsx @@ -12,7 +12,12 @@ 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 @@ -30,11 +35,16 @@ const mapStateToProps = createSelector( isJsAlgoDataStructCert, isApisMicroservicesCert, isInfosecQaCert, + isFrontEndCert, + isBackEndCert, + isDataVisCert, username }, projects ) => ({ - projects, + allProjects: projects, + legacyProjects: projects.filter(p => p.superBlock.includes('legacy')), + modernProjects: projects.filter(p => !p.superBlock.includes('legacy')), userProjects: projects .map(block => buildUserProjectsMap(block, challengeMap)) .reduce((projects, current) => ({ @@ -49,7 +59,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,23 +71,35 @@ const mapStateToProps = createSelector( function mapDispatchToProps(dispatch) { return bindActionCreators({ claimCert, + createError: createErrorObservable, fetchChallenges, hardGoTo, updateUserBackend }, dispatch); } +const projectsTypes = PropTypes.arrayOf( + PropTypes.shape({ + projectBlockName: PropTypes.string, + challenges: PropTypes.arrayOf( + PropTypes.shape({ + dashedName: PropTypes.string, + id: PropTypes.string, + title: PropTypes.string + }) + ) + }), +); + const propTypes = { + allProjects: projectsTypes, blockNameIsCertMap: PropTypes.objectOf(PropTypes.bool), claimCert: PropTypes.func.isRequired, + createError: PropTypes.func.isRequired, fetchChallenges: PropTypes.func.isRequired, hardGoTo: PropTypes.func.isRequired, - projects: PropTypes.arrayOf( - PropTypes.shape({ - projectBlockName: PropTypes.string, - challenges: PropTypes.arrayOf(PropTypes.string) - }) - ), + legacyProjects: projectsTypes, + modernProjects: projectsTypes, superBlock: PropTypes.string, updateUserBackend: PropTypes.func.isRequired, userProjects: PropTypes.objectOf( @@ -92,50 +117,157 @@ class CertificationSettings extends PureComponent { constructor(props) { super(props); + this.buildProjectForms = this.buildProjectForms.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } componentDidMount() { - const { projects } = this.props; - if (!projects.length) { + const { modernProjects } = this.props; + if (!modernProjects.length) { this.props.fetchChallenges(); } } + buildProjectForms({ + projectBlockName, + challenges, + superBlock + }) { + const { + blockNameIsCertMap, + claimCert, + hardGoTo, + userProjects, + username + } = this.props; + const isCertClaimed = blockNameIsCertMap[projectBlockName]; + const challengeTitles = challenges + .map(challenge => challenge.title || 'Unknown Challenge'); + if (superBlock === jsProjectSuperBlock) { + return ( + + ); + } + const options = challengeTitles + .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 = challengeTitles + .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 === challengeTitles.length; + return ( + +

{ projectBlockName }

+
+ { + isCertClaimed ? + : + null + } +
+ + ); + } + handleSubmit(values) { const { id } = values; - const fullForm = _.values(values) - .filter(Boolean) - .filter(_.isString) - // 5 projects + 1 id prop - .length === 6; + const { allProjects } = this.props; + let project = _.find(allProjects, { superBlock: id }); + if (!project) { + // 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' + ) + ); + } const valuesSaved = _.values(this.props.userProjects[id]) .filter(Boolean) - .filter(_.isString) - .length === 6; - if (fullForm && valuesSaved) { + .filter(_.isString); + + // minus 1 due to the form id being in values + const isProjectSectionComplete = + (valuesSaved.length - 1) === project.challenges.length; + + if (isProjectSectionComplete) { return this.props.claimCert(id); } - const { projects } = this.props; - const pIndex = _.findIndex(projects, p => p.superBlock === id); - values.nameToIdMap = projects[pIndex].challengeNameIdMap; + const valuesToIds = project.challenges + .reduce((valuesMap, current) => { + const solution = values[current.title]; + if (solution) { + return { + ...valuesMap, + [current.id]: solution + }; + } + return valuesMap; + }, {}); return this.props.updateUserBackend({ projects: { - [id]: values + [id]: valuesToIds } }); } render() { const { - blockNameIsCertMap, - claimCert, - hardGoTo, - projects, - userProjects, - username + modernProjects, + legacyProjects } = this.props; - if (!projects.length) { + if (!modernProjects.length) { return null; } return ( @@ -150,88 +282,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 - } -
-
- ); - }) + { + modernProjects.map(this.buildProjectForms) + } + + Legacy Certificate Settings + + { + legacyProjects.map(this.buildProjectForms) } ); diff --git a/common/app/routes/Settings/components/JSAlgoAndDSForm.jsx b/common/app/routes/Settings/components/JSAlgoAndDSForm.jsx index 8cc19d2bd4..4b8558bf21 100644 --- a/common/app/routes/Settings/components/JSAlgoAndDSForm.jsx +++ b/common/app/routes/Settings/components/JSAlgoAndDSForm.jsx @@ -43,7 +43,7 @@ class JSAlgoAndDSForm extends PureComponent { e.preventDefault(); const { username, superBlock, isCertClaimed } = this.props; if (isCertClaimed) { - return this.props.hardGoTo(`/c/${username}/${superBlock}`); + return this.props.hardGoTo(`/certificates/${username}/${superBlock}`); } return this.props.claimCert(superBlock); } diff --git a/common/app/routes/Settings/utils/buildUserProjectsMap.js b/common/app/routes/Settings/utils/buildUserProjectsMap.js index 770099af38..5629a94939 100644 --- a/common/app/routes/Settings/utils/buildUserProjectsMap.js +++ b/common/app/routes/Settings/utils/buildUserProjectsMap.js @@ -1,19 +1,14 @@ -import { dasherize } from '../../../../../server/utils/index'; - export const jsProjectSuperBlock = 'javascript-algorithms-and-data-structures'; export function buildUserProjectsMap(projectBlock, challengeMap) { const { - challengeNameIdMap, challenges, superBlock } = projectBlock; return { [superBlock]: challenges.reduce((solutions, current) => { - const dashedName = dasherize(current) - .replace('java-script', 'javascript') - .replace('metric-imperial', 'metricimperial'); - const completed = challengeMap[challengeNameIdMap[dashedName]]; + const { id } = current; + const completed = challengeMap[id]; let solution = ''; if (superBlock === jsProjectSuperBlock) { solution = {}; @@ -25,7 +20,7 @@ export function buildUserProjectsMap(projectBlock, challengeMap) { } return { ...solutions, - [current]: solution + [current.title]: solution }; }, {}) }; diff --git a/common/models/user.js b/common/models/user.js index 8b1401a4b1..0e3386aaf7 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -9,7 +9,6 @@ import loopback from 'loopback'; import _ from 'lodash'; import { themes } from '../utils/themes'; -import { dasherize } from '../../server/utils'; import { saveUser, observeMethod } from '../../server/utils/rx.js'; import { blacklistedUsernames } from '../../server/utils/constants.js'; import { wrapHandledError } from '../../server/utils/create-handled-error.js'; @@ -43,28 +42,24 @@ function destroyAll(id, Model) { } function buildChallengeMapUpdate(challengeMap, project) { + const key = Object.keys(project)[0]; + const solutions = project[key]; const currentChallengeMap = { ...challengeMap }; - const { nameToIdMap } = _.values(project)[0]; - const incomingUpdate = _.pickBy( - _.omit(_.values(project)[0], [ 'id', 'nameToIdMap' ]), - Boolean + const currentCompletedProjects = _.pick( + currentChallengeMap, + Object.keys(solutions) ); - const currentCompletedProjects = _.pick(challengeMap, _.values(nameToIdMap)); const now = Date.now(); - const update = Object.keys(incomingUpdate).reduce((update, current) => { - const dashedName = dasherize(current) - .replace('java-script', 'javascript') - .replace('metric-imperial', 'metricimperial'); - const currentId = nameToIdMap[dashedName]; + const update = Object.keys(solutions).reduce((update, currentId) => { if ( currentId in currentCompletedProjects && - currentCompletedProjects[currentId].solution !== incomingUpdate[current] + currentCompletedProjects[currentId].solution !== solutions[currentId] ) { return { ...update, [currentId]: { ...currentCompletedProjects[currentId], - solution: incomingUpdate[current], + solution: solutions[currentId], numOfAttempts: currentCompletedProjects[currentId].numOfAttempts + 1 } }; @@ -74,7 +69,7 @@ function buildChallengeMapUpdate(challengeMap, project) { ...update, [currentId]: { id: currentId, - solution: incomingUpdate[current], + solution: solutions[currentId], challengeType: 3, completedDate: now, numOfAttempts: 1 diff --git a/common/utils/legacyProjectData.js b/common/utils/legacyProjectData.js new file mode 100644 index 0000000000..0d7d77dd93 --- /dev/null +++ b/common/utils/legacyProjectData.js @@ -0,0 +1,88 @@ +const legacyFrontEndProjects = { + challenges: [ + // 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' + ], + title: 'Legacy Front End Projects', + superBlock: 'legacy-front-end' +}; + +const legacyBackEndProjects = { + challenges: [ + // 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' + ], + title: 'Legacy Back End Projects', + superBlock: 'legacy-back-end' +}; + +const legacyDataVisProjects = { + challenges: [ + // 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' + ], + title: 'Legacy Data Visualization Projects', + superBlock: 'legacy-data-visualization' +}; + +const legacyProjects = [ + legacyFrontEndProjects, + legacyBackEndProjects, + legacyDataVisProjects +]; + +export default legacyProjects; diff --git a/seed/challenges/01-responsive-web-design/responsive-web-design-projects.json b/seed/challenges/01-responsive-web-design/responsive-web-design-projects.json index e7f40bc06d..e8a983fac6 100644 --- a/seed/challenges/01-responsive-web-design/responsive-web-design-projects.json +++ b/seed/challenges/01-responsive-web-design/responsive-web-design-projects.json @@ -273,7 +273,7 @@ "translations": {} }, { - "id": "587d78b0367417b2b2512b06", + "id": "bd7158d8c242eddfaeb5bd13", "title": "Build a Personal Portfolio Webpage", "description": [ "Fulfill the user stories by getting all of the tests to pass. Use whichever libraries you need. Give it your own personal style.", diff --git a/seed/challenges/08-coding-interview-questions-and-take-home-assignments/take-home-interview-projects.json b/seed/challenges/08-coding-interview-questions-and-take-home-assignments/take-home-interview-projects.json index 3ba3cbe2cf..5dc7275e6c 100644 --- a/seed/challenges/08-coding-interview-questions-and-take-home-assignments/take-home-interview-projects.json +++ b/seed/challenges/08-coding-interview-questions-and-take-home-assignments/take-home-interview-projects.json @@ -101,7 +101,7 @@ }, { "id": "bd7158d8c442eddfaeb5bd1f", - "title": "Use the Twitch.tv JSON API", + "title": "Use the Twitch JSON API", "description": [ "Objective: Build a CodePen.io app that is functionally similar to this: https://codepen.io/freeCodeCamp/full/Myvqmo/.", "Fulfill the below user stories. Use whichever libraries or APIs you need. Give it your own personal style.", @@ -123,7 +123,7 @@ "isRequired": false, "translations": { "es": { - "title": "Usa el API JSON de Twitch.tv", + "title": "Usa el API JSON de Twitch", "description": [ "Objetivo: Crea una aplicación con CodePen.io cuya funcionalidad sea similar a la de esta: https://codepen.io/freeCodeCamp/full/Myvqmo/.", "Satisface las siguientes historias de usuario. Usa cualquier librería o APIs que necesites. Dale tu estilo personal.", @@ -139,7 +139,7 @@ ] }, "ru": { - "title": "Используйте Twitch.tv JSON API", + "title": "Используйте Twitch JSON API", "description": [ "Задание: Создайте CodePen.io который успешно копирует вот этот: https://codepen.io/freeCodeCamp/full/Myvqmo/.", "Правило #1: Не подсматривайте код приведенного на CodePen примера. Напишите его самостоятельно.", @@ -682,6 +682,127 @@ "isRequired": false, "titleEs": "Crea un clon de Pinterest" }, + { + "id": "bd7158d8c443eddfaeb5bdff", + "title": "Build a Nightlife Coordination App", + "description": [ + "Objective: Build a full stack JavaScript app that is functionally similar to this: http://whatsgoinontonight.herokuapp.com/ and deploy it to Heroku.", + "Note that for each project, you should create a new GitHub repository and a new Heroku project. If you can't remember how to do this, revisit https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects.", + "Here are the specific user stories you should implement for this project:", + "User Story: As an unauthenticated user, I can view all bars in my area.", + "User Story: As an authenticated user, I can add myself to a bar to indicate I am going there tonight.", + "User Story: As an authenticated user, I can remove myself from a bar if I no longer want to go there.", + "User Story: As an unauthenticated user, when I login I should not have to search again.", + "Hint: Try using the Yelp API to find venues in the cities your users search for. If you use Yelp's API, be sure to mention so in your app.", + "Once you've finished implementing these user stories, click the \"I've completed this challenge\" button and enter the URLs for both your GitHub repository and your live app running on Heroku.", + "You can get feedback on your project by sharing it with your friends on Facebook." + ], + "challengeSeed": [ + "Gei7QfPmcMw" + ], + "tests": [], + "type": "basejump", + "challengeType": 4, + "isRequired": true, + "translations": { + "es": { + "description": [ + "Objetivo: Construye una aplicación de pila completa (full stack) en JavaScript que funcione de forma similar al siguiente proyecto: http://whatsgoinontonight.herokuapp.com/ y despliégala en Heroku.", + "Ten en cuenta que para cada proyecto, debes crear un nuevo repositorio en GitHub y un nuevo proyecto en Heroku. Si no recuerdas cómo hacerlo, visita de nuevo https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects.", + "Estas son las Historias de usuario que debes satisfacer para este Basejump:", + "Historia de usuario: Como un usuario no autenticado, puedo ver todos los bares en mi área.", + "Historia de usuario: Como un usuario autenticado, puedo agregarme a mí mismo a un bar para indicar que voy a estar allí esta noche.", + "Historia de usuario: Como un usuario autenticado, puedo removerme de un bar si ya no pienso ir allí.", + "Historia de usuario: Como un usuario no autenticado, cuando accedo a mi cuenta no debo tener la necesidad de buscar de nuevo.", + "Pista: Prueba utilizar el API de Yelp para encontrar lugares en las ciudades donde tus usuarios buscan. Si utilizas el API de Yelp, asegúrate de mencionarlo en tu aplicación.", + "Una vez hayas terminado de implementar estas historias de usuario, pulsa el botón de \"I've completed this challenge\" e incluye las URLs de tu repositorio GitHub y de tu aplicación corriendo en Heroku.", + "Puedes obtener retroalimentación acerca de tu proyecto de parte de tus compañeros campistas compartiéndolo en nuestro Cuarto de revisión de código. También puedes compartirlo en Twitter y en el campamento de tu ciudad (en Facebook)." + ] + } + } + }, + { + "id": "bd7158d8c443eddfaeb5bd0e", + "title": "Chart the Stock Market", + "description": [ + "Objective: Build a full stack JavaScript app that is functionally similar to this: http://watchstocks.herokuapp.com/ and deploy it to Heroku.", + "Note that for each project, you should create a new GitHub repository and a new Heroku project. If you can't remember how to do this, revisit https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects.", + "Here are the specific user stories you should implement for this project:", + "User Story: I can view a graph displaying the recent trend lines for each added stock.", + "User Story: I can add new stocks by their symbol name.", + "User Story: I can remove stocks.", + "User Story: I can see changes in real-time when any other user adds or removes a stock. For this you will need to use Web Sockets.", + "Once you've finished implementing these user stories, click the \"I've completed this challenge\" button and enter the URLs for both your GitHub repository and your live app running on Heroku.", + "You can get feedback on your project by sharing it with your friends on Facebook." + ], + "challengeSeed": [ + "CENs50cnRgM" + ], + "tests": [], + "type": "basejump", + "challengeType": 4, + "isRequired": true, + "translations": { + "es": { + "description": [ + "Objetivo: Construye una aplicación de pila completa (full stack) en JavaScript que funcione de forma similar al siguiente proyecto: http://watchstocks.herokuapp.com/ y despliégalo en Heroku.", + "Ten en cuenta que para cada proyecto, debes crear un nuevo repositorio en GitHub y un nuevo proyecto en Heroku. Si no recuerdas cómo hacerlo, visita de nuevo https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects.", + "Estas son las Historias de usuario que debes satisfacer para este Basejump:", + "Historia de usuario: Como usuario, puedo ver un gráfico que me muestre las líneas de tendencia recientes para cada acción agregada.", + "Historia de usuario: Como usuario, puedo agregar nuevas acciones por su símbolo.", + "Historia de usuario: Como usuario, puedo remover acciones.", + "Historia de usuario: Como usuario, puedo ver cambios en tiempo real cuando algún otro usuario agrega o remueve una acción. Puedes usar Web Sockets para hacer esto.", + "Una vez hayas terminado de implementar estas historias de usuario, pulsa el botón de \"I've completed this challenge\" e incluye las URLs de tu repositorio GitHub y de tu aplicación corriendo en Heroku.", + "Puedes obtener retroalimentación acerca de tu proyecto de parte de tus compañeros campistas compartiéndolo en nuestro Cuarto de revisión de código. También puedes compartirlo en Twitter y en el campamento de tu ciudad (en Facebook)." + ] + } + } + }, + { + "id": "bd7158d8c443eddfaeb5bdef", + "title": "Build a Voting App", + "description": [ + "Objective: Build a full stack JavaScript app that is functionally similar to this: https://fcc-voting-arthow4n.herokuapp.com/ and deploy it to Heroku.", + "Note that for each project, you should create a new GitHub repository and a new Heroku project. If you can't remember how to do this, revisit https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects.", + "Here are the specific user stories you should implement for this project:", + "User Story: As an authenticated user, I can keep my polls and come back later to access them.", + "User Story: As an authenticated user, I can share my polls with my friends.", + "User Story: As an authenticated user, I can see the aggregate results of my polls.", + "User Story: As an authenticated user, I can delete polls that I decide I don't want anymore.", + "User Story: As an authenticated user, I can create a poll with any number of possible items.", + "User Story: As an unauthenticated or authenticated user, I can see and vote on everyone's polls.", + "User Story: As an unauthenticated or authenticated user, I can see the results of polls in chart form. (This could be implemented using Chart.js or Google Charts.)", + "User Story: As an authenticated user, if I don't like the options on a poll, I can create a new option.", + "Once you've finished implementing these user stories, click the \"I've completed this challenge\" button and enter the URLs for both your GitHub repository and your live app running on Heroku.", + "You can get feedback on your project by sharing it with your friends on Facebook." + ], + "challengeSeed": [ + "JBKnbY_fdg4" + ], + "tests": [], + "type": "basejump", + "challengeType": 4, + "isRequired": true, + "translations": { + "es": { + "description": [ + "Objetivo: Construye una aplicación de pila completa (full stack) en JavaScript que funcione de forma similar al siguiente proyecto: https://fcc-voting-arthow4n.herokuapp.com/ y despliégala en Heroku.", + "Ten en cuenta que para cada proyecto, debes crear un nuevo repositorio en GitHub y un nuevo proyecto en Heroku. Si no recuerdas cómo hacerlo, visita de nuevo https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects.", + "Estas son las Historias de usuario que debes satisfacer para este proyecto:", + "Historia de usuario: Como un usuario autenticado, puedo guardar mis votaciones y acceder a ellas posteriormente.", + "Historia de usuario: Como un usuario autenticado, puedo compartir mis votaciones con mis amigos.", + "Historia de usuario: Como un usuario autenticado, puedo ver los resultados agregados de mis votaciones.", + "Historia de usuario: Como un usuario autenticado, puedo eliminar votaciones que ya no quiero tener guardadas.", + "Historia de usuario: Como un usuario autenticado, puedo crear una votación con cualquier número de opciones.", + "Historia de usuario: Como un usuario autenticado o no autenticado, puedo ver y votar en las votaciones de otros.", + "Historia de usuario: Como un usuario autenticado o no autenticado, puedo ver los resultados de las votaciones por medio de un gráfico. (Esto podría implementarse utilizando Chart.js o Google Charts.)", + "Historia de usuario: Como un usuario autenticado, si no me gustan las opciones en una votación, puedo crear una nueva opción.", + "Una vez hayas terminado de implementar estas historias de usuario, pulsa el botón de \"I've completed this challenge\" e incluye las URLs de tu repositorio GitHub y de tu aplicación corriendo en Heroku.", + "Puedes obtener retroalimentación acerca de tu proyecto de parte de tus compañeros campistas compartiéndolo en nuestro Cuarto de revisión de código. También puedes compartirlo en Twitter y en el campamento de tu ciudad (en Facebook)." + ] + } + } + }, { "id": "5a4b7fcdb66f799f199e11db", "title": "Build a Pong Game", 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..d1fd11ebad --- /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 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..cd22c68838 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( + '/certificates/: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..e25d05cbc7 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/certificates/#{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..5f5793565f 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/certificates/#{username}/apis-and-microservices diff --git a/server/views/certificate/data-visualization.jade b/server/views/certificate/data-visualization.jade index 0782d408aa..e4c34c3543 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/certificates/#{username}/data-visualization diff --git a/server/views/certificate/front-end-libraries.jade b/server/views/certificate/front-end-libraries.jade index f5d4206fd2..1108949a40 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/certificates/#{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..a6a7ecbd62 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/certificates/#{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..0033d50e2d 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/certificates/#{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 94% rename from server/views/certificate/back-end.jade rename to server/views/certificate/legacy/back-end.jade index e94d39a4ad..c2cce165da 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/certificates/#{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..bf9537fca4 --- /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/certificates/#{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..56cd262af4 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/certificates/#{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..5fd4ab7920 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/certificates/#{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..5030b879d4 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/certificates/#{username}/responsive-web-design