Merge pull request #16788 from Bouncey/feat/legacyCerts

Feat(legacy-certs): Claim legacy certificates from the settings page
This commit is contained in:
Berkeley Martinez
2018-03-08 07:33:20 -08:00
committed by GitHub
28 changed files with 822 additions and 366 deletions

View File

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

View File

@ -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
</Button>

View File

@ -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 (
<JSAlgoAndDSForm
challenges={ challengeTitles }
claimCert={ claimCert }
hardGoTo={ hardGoTo }
isCertClaimed={ isCertClaimed }
jsProjects={ userProjects[superBlock] }
key={ superBlock }
projectBlockName={ projectBlockName }
superBlock={ superBlock }
username={ username }
/>
);
}
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 (
<FullWidthRow key={superBlock}>
<h3 className='project-heading'>{ projectBlockName }</h3>
<Form
buttonText={ fullForm ? 'Claim Certificate' : 'Save Progress' }
enableSubmit={ fullForm }
formFields={ challengeTitles.concat([ 'id' ]) }
hideButton={isCertClaimed}
id={ superBlock }
initialValues={{
...initialValues,
...userValues
}}
options={ options }
submit={ this.handleSubmit }
/>
{
isCertClaimed ?
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
href={ `/certificates/${username}/${superBlock}`}
target='_blank'
>
Show Certificate
</Button> :
null
}
<hr />
</FullWidthRow>
);
}
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.
</p>
</FullWidthRow>
{
projects.map(({
projectBlockName,
challenges,
superBlock
}) => {
const isCertClaimed = blockNameIsCertMap[projectBlockName];
if (superBlock === jsProjectSuperBlock) {
return (
<JSAlgoAndDSForm
challenges={ challenges }
claimCert={ claimCert }
hardGoTo={ hardGoTo }
isCertClaimed={ isCertClaimed }
jsProjects={ userProjects[superBlock] }
key={ superBlock }
projectBlockName={ projectBlockName }
superBlock={ superBlock }
username={ username }
/>
);
}
const options = challenges
.reduce((options, current) => {
options.types[current] = 'url';
return options;
}, { types: {} });
options.types.id = 'hidden';
options.placeholder = false;
const userValues = userProjects[superBlock] || {};
if (!userValues.id) {
userValues.id = superBlock;
}
const initialValues = challenges
.reduce((accu, current) => ({
...accu,
[current]: ''
}), {});
const completedProjects = _.values(userValues)
.filter(Boolean)
.filter(_.isString)
// minus 1 to account for the id
.length - 1;
const fullForm = completedProjects === challenges.length;
return (
<FullWidthRow key={superBlock}>
<h3 className='project-heading'>{ projectBlockName }</h3>
<Form
buttonText={ fullForm ? 'Claim Certificate' : 'Save Progress' }
enableSubmit={ fullForm }
formFields={ challenges.concat([ 'id' ]) }
hideButton={isCertClaimed}
id={ superBlock }
initialValues={{
...initialValues,
...userValues
}}
options={ options }
submit={ this.handleSubmit }
/>
{
isCertClaimed ?
<Button
block={ true }
bsSize='lg'
bsStyle='primary'
href={ `/c/${username}/${superBlock}`}
>
Show Certificate
</Button> :
null
}
<hr />
</FullWidthRow>
);
})
{
modernProjects.map(this.buildProjectForms)
}
<SectionHeader>
Legacy Certificate Settings
</SectionHeader>
{
legacyProjects.map(this.buildProjectForms)
}
</div>
);

View File

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

View File

@ -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
};
}, {})
};

View File

@ -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

View File

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

View File

@ -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.",

View File

@ -101,7 +101,7 @@
},
{
"id": "bd7158d8c442eddfaeb5bd1f",
"title": "Use the Twitch.tv JSON API",
"title": "Use the Twitch JSON API",
"description": [
"<strong>Objective:</strong> Build a <a href='https://codepen.io' target='_blank'>CodePen.io</a> app that is functionally similar to this: <a href='https://codepen.io/freeCodeCamp/full/Myvqmo/' target='_blank'>https://codepen.io/freeCodeCamp/full/Myvqmo/</a>.",
"Fulfill the below <a href='https://en.wikipedia.org/wiki/User_story' target='_blank'>user stories</a>. 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": [
"<span class='text-info'>Objetivo:</span> Crea una aplicación con <a href='https://codepen.io' target='_blank'>CodePen.io</a> cuya funcionalidad sea similar a la de esta: <a href='https://codepen.io/freeCodeCamp/full/Myvqmo/' target='_blank'>https://codepen.io/freeCodeCamp/full/Myvqmo/</a>.",
"Satisface las siguientes <a href='https://en.wikipedia.org/wiki/User_story' target='_blank'>historias de usuario</a>. 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": [
"<span class='text-info'>Задание:</span> Создайте <a href='https://codepen.io' target='_blank'>CodePen.io</a> который успешно копирует вот этот: <a href='https://codepen.io/freeCodeCamp/full/Myvqmo/' target='_blank'>https://codepen.io/freeCodeCamp/full/Myvqmo/</a>.",
"<span class='text-info'>Правило #1:</span> Не подсматривайте код приведенного на CodePen примера. Напишите его самостоятельно.",
@ -682,6 +682,127 @@
"isRequired": false,
"titleEs": "Crea un clon de Pinterest"
},
{
"id": "bd7158d8c443eddfaeb5bdff",
"title": "Build a Nightlife Coordination App",
"description": [
"<strong>Objective:</strong> Build a full stack JavaScript app that is functionally similar to this: <a href='http://whatsgoinontonight.herokuapp.com/' target='_blank'>http://whatsgoinontonight.herokuapp.com/</a> 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 <a href='/challenges/get-set-for-our-dynamic-web-application-projects'>https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects</a>.",
"Here are the specific user stories you should implement for this project:",
"<strong>User Story:</strong> As an unauthenticated user, I can view all bars in my area.",
"<strong>User Story:</strong> As an authenticated user, I can add myself to a bar to indicate I am going there tonight.",
"<strong>User Story:</strong> As an authenticated user, I can remove myself from a bar if I no longer want to go there.",
"<strong>User Story:</strong> As an unauthenticated user, when I login I should not have to search again.",
"<strong>Hint:</strong> Try using the <a href='https://www.yelp.com/developers/documentation/v2/overview' target='_blank'>Yelp API</a> 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": [
"<strong>Objetivo:</strong> Construye una aplicación de pila completa (full stack) en JavaScript que funcione de forma similar al siguiente proyecto: <a href='http://whatsgoinontonight.herokuapp.com/' target='_blank'>http://whatsgoinontonight.herokuapp.com/</a> 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 <a href='/challenges/get-set-for-our-dynamic-web-application-projects'>https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects</a>.",
"Estas son las Historias de usuario que debes satisfacer para este Basejump:",
"<strong>Historia de usuario:</strong> Como un usuario no autenticado, puedo ver todos los bares en mi área.",
"<strong>Historia de usuario:</strong> Como un usuario autenticado, puedo agregarme a mí mismo a un bar para indicar que voy a estar allí esta noche.",
"<strong>Historia de usuario:</strong> Como un usuario autenticado, puedo removerme de un bar si ya no pienso ir allí.",
"<strong>Historia de usuario:</strong> Como un usuario no autenticado, cuando accedo a mi cuenta no debo tener la necesidad de buscar de nuevo.",
"<span class='text-info'>Pista:</span> Prueba utilizar el <a href='https://www.yelp.com/developers/documentation/v2/overview' target='_blank'>API de Yelp</a> 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 <a href='//gitter.im/freecodecamp/codereview' target='_blank'>Cuarto de revisión de código</a>. También puedes compartirlo en Twitter y en el campamento de tu ciudad (en Facebook)."
]
}
}
},
{
"id": "bd7158d8c443eddfaeb5bd0e",
"title": "Chart the Stock Market",
"description": [
"<strong>Objective:</strong> Build a full stack JavaScript app that is functionally similar to this: <a href='http://watchstocks.herokuapp.com/' target='_blank'>http://watchstocks.herokuapp.com/</a> 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 <a href='/challenges/get-set-for-our-dynamic-web-application-projects'>https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects</a>.",
"Here are the specific user stories you should implement for this project:",
"<strong>User Story:</strong> I can view a graph displaying the recent trend lines for each added stock.",
"<strong>User Story:</strong> I can add new stocks by their symbol name.",
"<strong>User Story:</strong> I can remove stocks.",
"<strong>User Story:</strong> 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": [
"<strong>Objetivo:</strong> Construye una aplicación de pila completa (full stack) en JavaScript que funcione de forma similar al siguiente proyecto: <a href='http://watchstocks.herokuapp.com/' target='_blank'>http://watchstocks.herokuapp.com/</a> 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 <a href='/challenges/get-set-for-our-dynamic-web-application-projects'>https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects</a>.",
"Estas son las Historias de usuario que debes satisfacer para este Basejump:",
"<strong>Historia de usuario:</strong> Como usuario, puedo ver un gráfico que me muestre las líneas de tendencia recientes para cada acción agregada.",
"<strong>Historia de usuario:</strong> Como usuario, puedo agregar nuevas acciones por su símbolo.",
"<strong>Historia de usuario:</strong> Como usuario, puedo remover acciones.",
"<strong>Historia de usuario:</strong> 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 <a href='//gitter.im/freecodecamp/codereview' target='_blank'>Cuarto de revisión de código</a>. También puedes compartirlo en Twitter y en el campamento de tu ciudad (en Facebook)."
]
}
}
},
{
"id": "bd7158d8c443eddfaeb5bdef",
"title": "Build a Voting App",
"description": [
"<strong>Objective:</strong> Build a full stack JavaScript app that is functionally similar to this: <a href='https://fcc-voting-arthow4n.herokuapp.com/' target='_blank'>https://fcc-voting-arthow4n.herokuapp.com/</a> 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 <a href='/challenges/get-set-for-our-dynamic-web-application-projects'>https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects</a>.",
"Here are the specific user stories you should implement for this project:",
"<strong>User Story:</strong> As an authenticated user, I can keep my polls and come back later to access them.",
"<strong>User Story:</strong> As an authenticated user, I can share my polls with my friends.",
"<strong>User Story:</strong> As an authenticated user, I can see the aggregate results of my polls.",
"<strong>User Story:</strong> As an authenticated user, I can delete polls that I decide I don't want anymore.",
"<strong>User Story:</strong> As an authenticated user, I can create a poll with any number of possible items.",
"<strong>User Story:</strong> As an unauthenticated or authenticated user, I can see and vote on everyone's polls.",
"<strong>User Story:</strong> 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.)",
"<strong>User Story:</strong> 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": [
"<strong>Objetivo:</strong> Construye una aplicación de pila completa (full stack) en JavaScript que funcione de forma similar al siguiente proyecto: <a href='https://fcc-voting-arthow4n.herokuapp.com/' target='_blank'>https://fcc-voting-arthow4n.herokuapp.com/</a> 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 <a href='/challenges/get-set-for-our-dynamic-web-application-projects'>https://freecodecamp.com/challenges/get-set-for-our-dynamic-web-application-projects</a>.",
"Estas son las Historias de usuario que debes satisfacer para este proyecto:",
"<strong>Historia de usuario:</strong> Como un usuario autenticado, puedo guardar mis votaciones y acceder a ellas posteriormente.",
"<strong>Historia de usuario:</strong> Como un usuario autenticado, puedo compartir mis votaciones con mis amigos.",
"<strong>Historia de usuario:</strong> Como un usuario autenticado, puedo ver los resultados agregados de mis votaciones.",
"<strong>Historia de usuario:</strong> Como un usuario autenticado, puedo eliminar votaciones que ya no quiero tener guardadas.",
"<strong>Historia de usuario:</strong> Como un usuario autenticado, puedo crear una votación con cualquier número de opciones.",
"<strong>Historia de usuario:</strong> Como un usuario autenticado o no autenticado, puedo ver y votar en las votaciones de otros.",
"<strong>Historia de usuario:</strong> 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.)",
"<strong>Historia de usuario:</strong> 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 <a href='//gitter.im/freecodecamp/codereview' target='_blank'>Cuarto de revisión de código</a>. También puedes compartirlo en Twitter y en el campamento de tu ciudad (en Facebook)."
]
}
}
},
{
"id": "5a4b7fcdb66f799f199e11db",
"title": "Build a Pong Game",

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import _ from 'lodash';
import loopback from 'loopback';
import moment from 'moment-timezone';
import path from 'path';
import dedent from 'dedent';
import { Observable } from 'rx';
@ -13,26 +14,23 @@ import {
import { observeQuery } from '../utils/rx';
import {
// legacy
frontEndChallengeId,
backEndChallengeId,
dataVisId,
legacyFrontEndChallengeId,
legacyBackEndChallengeId,
legacyDataVisId,
// modern
respWebDesignId,
frontEndLibsId,
dataVis2018Id,
jsAlgoDataStructId,
dataVis2018Id,
apisMicroservicesId,
infosecQaId
} from '../utils/constantStrings.json';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
import {
completeCommitment$
} from '../utils/commit';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
const log = debug('fcc:certification');
const renderCertifedEmail = loopback.template(path.join(
__dirname,
@ -46,6 +44,48 @@ function isCertified(ids, challengeMap = {}) {
return _.every(ids, ({ id }) => _.has(challengeMap, id));
}
const certIds = {
[certTypes.frontEnd]: legacyFrontEndChallengeId,
[certTypes.backEnd]: legacyBackEndChallengeId,
[certTypes.dataVis]: legacyDataVisId,
[certTypes.respWebDesign]: respWebDesignId,
[certTypes.frontEndLibs]: frontEndLibsId,
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
[certTypes.dataVis2018]: dataVis2018Id,
[certTypes.apisMicroservices]: apisMicroservicesId,
[certTypes.infosecQa]: infosecQaId
};
const certViews = {
[certTypes.frontEnd]: 'certificate/legacy/front-end.jade',
[certTypes.backEnd]: 'certificate/legacy/back-end.jade',
[certTypes.dataVis]: 'certificate/legacy/data-visualization.jade',
[certTypes.fullStack]: 'certificate/legacy/full-stack.jade',
[certTypes.respWebDesign]: 'certificate/responsive-web-design.jade',
[certTypes.frontEndLibs]: 'certificate/front-end-libraries.jade',
[certTypes.jsAlgoDataStruct]:
'certificate/javascript-algorithms-and-data-structures.jade',
[certTypes.dataVis2018]: 'certificate/data-visualization.jade',
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
[certTypes.infosecQa]:
'certificate/information-security-and-quality-assurance.jade'
};
const certText = {
[certTypes.frontEnd]: 'Legacy Front End certified',
[certTypes.backEnd]: 'Legacy Back End Certified',
[certTypes.dataVis]: 'Legacy Data Visualization Certified',
[certTypes.fullStack]: 'Legacy Full Stack Certified',
[certTypes.respWebDesign]: 'Responsive Web Design Certified',
[certTypes.frontEndLibs]: 'Front End Libraries Certified',
[certTypes.jsAlgoDataStruct]:
'JavaScript Algorithms and Data Structures Certified',
[certTypes.dataVis2018]: 'Data Visualization Certified',
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
};
function getIdsForCert$(id, Challenge) {
return observeQuery(
Challenge,
@ -117,13 +157,24 @@ function sendCertifiedEmail(
export default function certificate(app) {
const router = app.loopback.Router();
const { Email, Challenge } = app.models;
const { Email, Challenge, User } = app.models;
function findUserByUsername$(username, fields) {
return observeQuery(
User,
'findOne',
{
where: { username },
fields
}
);
}
const certTypeIds = {
// legacy
[certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge),
[certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge),
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge),
[certTypes.frontEnd]: getIdsForCert$(legacyFrontEndChallengeId, Challenge),
[certTypes.backEnd]: getIdsForCert$(legacyBackEndChallengeId, Challenge),
[certTypes.dataVis]: getIdsForCert$(legacyDataVisId, Challenge),
// modern
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
@ -145,6 +196,10 @@ export default function certificate(app) {
ifNoSuperBlock404,
verifyCert
);
router.get(
'/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
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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