From 630141167319b306c9882e95bbd26ac6edb2a018 Mon Sep 17 00:00:00 2001 From: Stuart Taylor Date: Mon, 5 Mar 2018 14:15:30 +0000 Subject: [PATCH] fix(cert-settings): Uncouple Cert-Settings from dashedName's --- common/app/entities/index.js | 38 +++-- .../Settings/components/Cert-Settings.jsx | 70 +++++---- .../Settings/utils/buildUserProjectsMap.js | 11 +- .../Settings/utils/legacyProjectData.js | 133 +++++++++--------- common/models/user.js | 24 ++-- .../take-home-interview-projects.json | 64 +-------- 6 files changed, 148 insertions(+), 192 deletions(-) diff --git a/common/app/entities/index.js b/common/app/entities/index.js index bb8aeda1c14..534d831e2ed 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 '../routes/Settings/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/Settings/components/Cert-Settings.jsx b/common/app/routes/Settings/components/Cert-Settings.jsx index 80afd6a2f37..0002d616dd0 100644 --- a/common/app/routes/Settings/components/Cert-Settings.jsx +++ b/common/app/routes/Settings/components/Cert-Settings.jsx @@ -22,7 +22,6 @@ import { buildUserProjectsMap, jsProjectSuperBlock } from '../utils/buildUserProjectsMap'; -import legacyProjects from '../utils/legacyProjectData'; const mapStateToProps = createSelector( userSelector, @@ -43,8 +42,10 @@ const mapStateToProps = createSelector( }, projects ) => ({ - projects, - userProjects: projects.concat(legacyProjects) + 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) => ({ ...projects, @@ -77,18 +78,28 @@ function mapDispatchToProps(dispatch) { }, 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( @@ -111,8 +122,8 @@ class CertificationSettings extends PureComponent { } componentDidMount() { - const { projects } = this.props; - if (!projects.length) { + const { modernProjects } = this.props; + if (!modernProjects.length) { this.props.fetchChallenges(); } } @@ -130,10 +141,12 @@ class CertificationSettings extends PureComponent { username } = this.props; const isCertClaimed = blockNameIsCertMap[projectBlockName]; + const challengeTitles = challenges + .map(challenge => challenge.title || 'Unknown Challenge'); if (superBlock === jsProjectSuperBlock) { return ( ); } - const options = challenges + const options = challengeTitles .reduce((options, current) => { options.types[current] = 'url'; return options; @@ -160,7 +173,7 @@ class CertificationSettings extends PureComponent { userValues.id = superBlock; } - const initialValues = challenges + const initialValues = challengeTitles .reduce((accu, current) => ({ ...accu, [current]: '' @@ -172,14 +185,14 @@ class CertificationSettings extends PureComponent { // minus 1 to account for the id .length - 1; - const fullForm = completedProjects === challenges.length; + const fullForm = completedProjects === challengeTitles.length; return (

{ projectBlockName }

{ + 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 { - projects + modernProjects, + legacyProjects } = this.props; - if (!projects.length) { + if (!modernProjects.length) { return null; } return ( @@ -260,7 +282,7 @@ class CertificationSettings extends PureComponent {

{ - projects.map(this.buildProjectForms) + modernProjects.map(this.buildProjectForms) } Legacy Certificate Settings diff --git a/common/app/routes/Settings/utils/buildUserProjectsMap.js b/common/app/routes/Settings/utils/buildUserProjectsMap.js index 770099af389..5629a949399 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/app/routes/Settings/utils/legacyProjectData.js b/common/app/routes/Settings/utils/legacyProjectData.js index ed70bee2d70..0d7d77dd938 100644 --- a/common/app/routes/Settings/utils/legacyProjectData.js +++ b/common/app/routes/Settings/utils/legacyProjectData.js @@ -1,88 +1,81 @@ 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' + // 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' ], - projectBlockName: 'Legacy Front End Projects', + title: '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' + // 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' ], - projectBlockName: 'Legacy Back End Projects', + title: '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' + // 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' ], - projectBlockName: 'Legacy Data Visualization Projects', + title: 'Legacy Data Visualization Projects', superBlock: 'legacy-data-visualization' }; diff --git a/common/models/user.js b/common/models/user.js index 8b1401a4b14..15e88b29c1a 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,25 @@ function destroyAll(id, Model) { } function buildChallengeMapUpdate(challengeMap, project) { + const key = Object.keys(project)[0]; + const solutions = project[key]; + console.log(solutions); 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 +70,7 @@ function buildChallengeMapUpdate(challengeMap, project) { ...update, [currentId]: { id: currentId, - solution: incomingUpdate[current], + solution: solutions[currentId], challengeType: 3, completedDate: now, numOfAttempts: 1 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 1a119f5c296..e87a6a4a269 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 @@ -4,68 +4,6 @@ "time": "", "helpRoom": "HelpFrontEnd", "challenges": [ - { - "id": "bd7158d8c242eddfaeb5bd13", - "title": "Build a Personal Portfolio Webpage", - "description": [ - "Objective: Build a CodePen.io app that is functionally similar to this: https://codepen.io/FreeCodeCamp/full/YqLyXB/.", - "Rule #1: Don't look at the example project's code. Figure it out for yourself.", - "Rule #2: Fulfill the below user stories. Use whichever libraries you need. Give it your own personal style.", - "Rule #3: You can use Bootstrap, or any other framework of your choice.", - "User Story: I can access all of the portfolio webpage's content just by scrolling.", - "User Story: I can click different buttons that will take me to the portfolio creator's different social media pages.", - "User Story: I can see thumbnail images of different projects the portfolio creator has built (if you haven't built any websites before, use placeholders.)", - "User Story: I navigate to different sections of the webpage by clicking buttons in the navigation.", - "Don't worry if you don't have anything to showcase on your portfolio yet - you will build several apps on the next few CodePen challenges, and can come back and update your portfolio later.", - "There are many great portfolio templates out there already. However, you should consider building your portfolio page as much as you can from the ground up. Using Bootstrap can help make this process much easier for you.", - "Remember to use Read-Search-Ask if you get stuck.", - "When you are finished, click the \"I've completed this challenge\" button and include a link to your CodePen. ", - "You can get feedback on your project by sharing it with your friends on Facebook." - ], - "challengeSeed": [ - "V72o34gY4Lw" - ], - "tests": [], - "type": "zipline", - "isRequired": true, - "challengeType": 3, - "translations": { - "es": { - "title": "Construye una página web para tu portafolio", - "description": [ - "Objetivo: Crea una aplicación con CodePen.io cuya funcionalidad sea similar a la de esta: https://codepen.io/FreeCodeCamp/full/QNmvEL/.", - "Regla #1: No veas el código del proyecto de ejemplo. Encuentra la forma de hacerlo por tu cuenta.", - "Regla #2: Satisface las siguientes historias de usuario. Usa cualquier librería que necesites. Dale tu estilo personal.", - "Historia de usuario: Puedo acceder a todo el contenido de la página del portafolio con sólo desplazarme en la ventana.", - "Historia de usuario: Puedo pulsar diferentes botones que me llevarán a las páginas de las diferentes cuentas de redes sociales del creador del portafolio.", - "Historia de usuario: Puedo ver una imagenes en miniatura de los diferentes proyectos que el creador del portafolio ha construido (si no has construido ningún sitio web antes, usa marcadores de posición.)", - "Historia de usuario: Puedo navegar a las diferentes secciones de la página web pulsando botones de navegación.", - "No te preocupes si no tienes nada que mostrar en tu portafolio todavía - en los siguientes desafíos crearás varias aplicaciones en CodePen, así que puedes regresar luego para actualizar tu portafolio.", - "Hay varias plantillas buenas, pero para este desafío, tendrás que construir la página web de tu portafolio completamente por tu cuenta. Usar Bootstrap hará el trabajo mucho más fácil para ti.", - "Recuerda utilizar Leer-Buscar-Preguntar si te sientes atascado.", - "Cuando hayas terminado, pulsa el botón \"I've completed this challenge\" e incluye un link a tu CodePen. ", - "Puedes obtener retroalimentación sobre tu proyecto por parte de otros campistas, compartiéndolo en nuestra Sala de chat para revisión de código. También puedes compartirlo en Twitter y en el campamento de tu ciudad (en Facebook)." - ] - }, - "ru": { - "title": "Создайте сайт-портфолио", - "description": [ - "Задание: Создайте приложение CodePen.io которое функционально соответствует вот этому: https://codepen.io/FreeCodeCamp/full/QNmvEL/.", - "Правило #1: Не подсматривайте код приведенного на CodePen примера. Напишите его самостоятельно.", - "Правило #2: Реализуйте следующие пользовательские истории. Используйте любые библиотеки, которые потребуются. Оформите приложение в вашем собственном стиле.", - "Пользовательская история: Я могу получить доступ ко всей информации на странице просто прокрутив ее сверху вниз.", - "Пользовательская история: Я могу нажать на различные кнопки и перейти к социальным страницам владельца портфолио.", - "Пользовательская история: Я могу увидеть эскизы проектов созданных владельцем портфолио (используйте временную картинку если у вас пока нет собственных веб-страниц).", - "Пользовательская история: Я могу перемещаться к различным частям страницы нажимая на соответствующие навигационные кнопки.", - "Не переживайте если вам пока нечего показать в портфолио - вы создадите несколько веб приложений в следующих заданиях, а затем вернетесь и обновите портфолио.", - "В сети существует много шаблонов для портфолио, но в этом задании вам необходимо создать собственную уникальную страницу. Используя Bootstrap, сделать это будет намного проще.", - "Если что-то не получается, воспользуйтесь Read-Search-Ask.", - "Когда выполните задание кликните кнопку \"I've completed this challenge\" и добавьте ссылку на ваш CodePen.", - "Вы можете получить отзыв о вашем проекте от коллег, поделившись ссылкой на него в нашем чате для рассмотрения кода. Также вы можете поделиться ею через Twitter и на странице Free Code Camp вашего города на Facebook." - ] - } - } - }, { "id": "bd7158d8c442eddfaeb5bd10", "title": "Show the Local Weather", @@ -163,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.",