Merge pull request #16788 from Bouncey/feat/legacyCerts
Feat(legacy-certs): Claim legacy certificates from the settings page
This commit is contained in:
@ -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 uuid from 'uuid/v4';
|
||||||
import {
|
import {
|
||||||
combineActions,
|
combineActions,
|
||||||
@ -12,6 +12,7 @@ import { themes } from '../../utils/themes';
|
|||||||
import { usernameSelector, types as app } from '../redux';
|
import { usernameSelector, types as app } from '../redux';
|
||||||
import { types as challenges } from '../routes/Challenges/redux';
|
import { types as challenges } from '../routes/Challenges/redux';
|
||||||
import { types as map } from '../Map/redux';
|
import { types as map } from '../Map/redux';
|
||||||
|
import legacyProjects from '../../utils/legacyProjectData';
|
||||||
|
|
||||||
export const ns = 'entities';
|
export const ns = 'entities';
|
||||||
export const getNS = state => state[ns];
|
export const getNS = state => state[ns];
|
||||||
@ -91,10 +92,6 @@ const defaultState = {
|
|||||||
fullBlocks: []
|
fullBlocks: []
|
||||||
};
|
};
|
||||||
|
|
||||||
export function selectiveChallengeTitleSelector(state, dashedName) {
|
|
||||||
return getNS(state).challenge[dashedName].title;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function portfolioSelector(state, props) {
|
export function portfolioSelector(state, props) {
|
||||||
const username = usernameSelector(state);
|
const username = usernameSelector(state);
|
||||||
const { portfolio } = getNS(state).user[username];
|
const { portfolio } = getNS(state).user[username];
|
||||||
@ -103,27 +100,42 @@ export function portfolioSelector(state, props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function projectsSelector(state) {
|
export function projectsSelector(state) {
|
||||||
const blocks = getNS(state).block;
|
const {
|
||||||
const challengeNameToIdMap = invert(challengeIdToNameMapSelector(state));
|
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)
|
return Object.keys(blocks)
|
||||||
.filter(key =>
|
.filter(key =>
|
||||||
key.includes('projects') && !key.includes('coding-interview')
|
key.includes('projects') && !key.includes('coding-interview')
|
||||||
)
|
)
|
||||||
.map(key => blocks[key])
|
.map(key => blocks[key])
|
||||||
|
.concat(legacyWithDashedNames)
|
||||||
.map(({ title, challenges, superBlock }) => {
|
.map(({ title, challenges, superBlock }) => {
|
||||||
const projectChallengeDashNames = challenges
|
const projectChallengeDashNames = challenges
|
||||||
|
// challengeIdToName is not available on appMount
|
||||||
|
.filter(Boolean)
|
||||||
// remove any project intros
|
// remove any project intros
|
||||||
.filter(chal => !chal.includes('get-set-for'));
|
.filter(chal => !chal.includes('get-set-for'));
|
||||||
const projectChallenges = projectChallengeDashNames
|
const projectChallenges = projectChallengeDashNames
|
||||||
.map(dashedName => selectiveChallengeTitleSelector(state, dashedName));
|
.map(dashedName => {
|
||||||
|
const { id, title } = challengeMap[dashedName];
|
||||||
|
return { id, title, dashedName };
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
projectBlockName: title,
|
projectBlockName: title,
|
||||||
superBlock,
|
superBlock,
|
||||||
challenges: projectChallenges,
|
challenges: projectChallenges
|
||||||
challengeNameIdMap: pick(
|
|
||||||
challengeNameToIdMap,
|
|
||||||
projectChallengeDashNames
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ const mapStateToProps = createSelector(
|
|||||||
{
|
{
|
||||||
show: is2018DataVisCert,
|
show: is2018DataVisCert,
|
||||||
title: 'Data Visualization Certificate:',
|
title: 'Data Visualization Certificate:',
|
||||||
showURL: 'data-visualization-2018'
|
showURL: 'data-visualization'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
show: isApisMicroservicesCert,
|
show: isApisMicroservicesCert,
|
||||||
@ -78,22 +78,22 @@ const mapStateToProps = createSelector(
|
|||||||
{
|
{
|
||||||
show: isFullStackCert,
|
show: isFullStackCert,
|
||||||
title: 'Full Stack Certificate:',
|
title: 'Full Stack Certificate:',
|
||||||
showURL: 'full-stack'
|
showURL: 'legacy-full-stack'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
show: isFrontEndCert,
|
show: isFrontEndCert,
|
||||||
title: 'Front End Certificate:',
|
title: 'Front End Certificate:',
|
||||||
showURL: 'front-end'
|
showURL: 'legacy-front-end'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
show: isBackEndCert,
|
show: isBackEndCert,
|
||||||
title: 'Back End Certificate:',
|
title: 'Back End Certificate:',
|
||||||
showURL: 'back-end'
|
showURL: 'legacy-back-end'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
show: isDataVisCert,
|
show: isDataVisCert,
|
||||||
title: 'Data Visualization Certificate:',
|
title: 'Data Visualization Certificate:',
|
||||||
showURL: 'data-visualization'
|
showURL: 'legacy-data-visualization'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@ -134,7 +134,7 @@ function renderCertShow(username, cert) {
|
|||||||
block={ true }
|
block={ true }
|
||||||
bsSize='lg'
|
bsSize='lg'
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
href={ `/c/${username}/${cert.showURL}`}
|
href={ `/certificates/${username}/${cert.showURL}`}
|
||||||
>
|
>
|
||||||
Show
|
Show
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -12,7 +12,12 @@ import JSAlgoAndDSForm from './JSAlgoAndDSForm.jsx';
|
|||||||
import SectionHeader from './SectionHeader.jsx';
|
import SectionHeader from './SectionHeader.jsx';
|
||||||
import { projectsSelector } from '../../../entities';
|
import { projectsSelector } from '../../../entities';
|
||||||
import { claimCert, updateUserBackend } from '../redux';
|
import { claimCert, updateUserBackend } from '../redux';
|
||||||
import { fetchChallenges, userSelector, hardGoTo } from '../../../redux';
|
import {
|
||||||
|
fetchChallenges,
|
||||||
|
userSelector,
|
||||||
|
hardGoTo,
|
||||||
|
createErrorObservable
|
||||||
|
} from '../../../redux';
|
||||||
import {
|
import {
|
||||||
buildUserProjectsMap,
|
buildUserProjectsMap,
|
||||||
jsProjectSuperBlock
|
jsProjectSuperBlock
|
||||||
@ -30,11 +35,16 @@ const mapStateToProps = createSelector(
|
|||||||
isJsAlgoDataStructCert,
|
isJsAlgoDataStructCert,
|
||||||
isApisMicroservicesCert,
|
isApisMicroservicesCert,
|
||||||
isInfosecQaCert,
|
isInfosecQaCert,
|
||||||
|
isFrontEndCert,
|
||||||
|
isBackEndCert,
|
||||||
|
isDataVisCert,
|
||||||
username
|
username
|
||||||
},
|
},
|
||||||
projects
|
projects
|
||||||
) => ({
|
) => ({
|
||||||
projects,
|
allProjects: projects,
|
||||||
|
legacyProjects: projects.filter(p => p.superBlock.includes('legacy')),
|
||||||
|
modernProjects: projects.filter(p => !p.superBlock.includes('legacy')),
|
||||||
userProjects: projects
|
userProjects: projects
|
||||||
.map(block => buildUserProjectsMap(block, challengeMap))
|
.map(block => buildUserProjectsMap(block, challengeMap))
|
||||||
.reduce((projects, current) => ({
|
.reduce((projects, current) => ({
|
||||||
@ -49,7 +59,10 @@ const mapStateToProps = createSelector(
|
|||||||
'Front End Libraries Projects': isFrontEndLibsCert,
|
'Front End Libraries Projects': isFrontEndLibsCert,
|
||||||
'Data Visualization Projects': is2018DataVisCert,
|
'Data Visualization Projects': is2018DataVisCert,
|
||||||
'API and Microservice Projects': isApisMicroservicesCert,
|
'API and Microservice Projects': isApisMicroservicesCert,
|
||||||
'Information Security and Quality Assurance Projects': isInfosecQaCert
|
'Information Security and Quality Assurance Projects': isInfosecQaCert,
|
||||||
|
'Legacy Front End Projects': isFrontEndCert,
|
||||||
|
'Legacy Back End Projects': isBackEndCert,
|
||||||
|
'Legacy Data Visualization Projects': isDataVisCert
|
||||||
},
|
},
|
||||||
username
|
username
|
||||||
})
|
})
|
||||||
@ -58,23 +71,35 @@ const mapStateToProps = createSelector(
|
|||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return bindActionCreators({
|
return bindActionCreators({
|
||||||
claimCert,
|
claimCert,
|
||||||
|
createError: createErrorObservable,
|
||||||
fetchChallenges,
|
fetchChallenges,
|
||||||
hardGoTo,
|
hardGoTo,
|
||||||
updateUserBackend
|
updateUserBackend
|
||||||
}, 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 = {
|
const propTypes = {
|
||||||
|
allProjects: projectsTypes,
|
||||||
blockNameIsCertMap: PropTypes.objectOf(PropTypes.bool),
|
blockNameIsCertMap: PropTypes.objectOf(PropTypes.bool),
|
||||||
claimCert: PropTypes.func.isRequired,
|
claimCert: PropTypes.func.isRequired,
|
||||||
|
createError: PropTypes.func.isRequired,
|
||||||
fetchChallenges: PropTypes.func.isRequired,
|
fetchChallenges: PropTypes.func.isRequired,
|
||||||
hardGoTo: PropTypes.func.isRequired,
|
hardGoTo: PropTypes.func.isRequired,
|
||||||
projects: PropTypes.arrayOf(
|
legacyProjects: projectsTypes,
|
||||||
PropTypes.shape({
|
modernProjects: projectsTypes,
|
||||||
projectBlockName: PropTypes.string,
|
|
||||||
challenges: PropTypes.arrayOf(PropTypes.string)
|
|
||||||
})
|
|
||||||
),
|
|
||||||
superBlock: PropTypes.string,
|
superBlock: PropTypes.string,
|
||||||
updateUserBackend: PropTypes.func.isRequired,
|
updateUserBackend: PropTypes.func.isRequired,
|
||||||
userProjects: PropTypes.objectOf(
|
userProjects: PropTypes.objectOf(
|
||||||
@ -92,50 +117,157 @@ class CertificationSettings extends PureComponent {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.buildProjectForms = this.buildProjectForms.bind(this);
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { projects } = this.props;
|
const { modernProjects } = this.props;
|
||||||
if (!projects.length) {
|
if (!modernProjects.length) {
|
||||||
this.props.fetchChallenges();
|
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) {
|
handleSubmit(values) {
|
||||||
const { id } = values;
|
const { id } = values;
|
||||||
const fullForm = _.values(values)
|
const { allProjects } = this.props;
|
||||||
.filter(Boolean)
|
let project = _.find(allProjects, { superBlock: id });
|
||||||
.filter(_.isString)
|
if (!project) {
|
||||||
// 5 projects + 1 id prop
|
// the submitted projects do not belong to current/legacy certificates
|
||||||
.length === 6;
|
return this.props.createError(
|
||||||
|
new Error(
|
||||||
|
'Submitted projects do not belong to either current or ' +
|
||||||
|
'legacy certificates'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
const valuesSaved = _.values(this.props.userProjects[id])
|
const valuesSaved = _.values(this.props.userProjects[id])
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter(_.isString)
|
.filter(_.isString);
|
||||||
.length === 6;
|
|
||||||
if (fullForm && valuesSaved) {
|
// 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);
|
return this.props.claimCert(id);
|
||||||
}
|
}
|
||||||
const { projects } = this.props;
|
const valuesToIds = project.challenges
|
||||||
const pIndex = _.findIndex(projects, p => p.superBlock === id);
|
.reduce((valuesMap, current) => {
|
||||||
values.nameToIdMap = projects[pIndex].challengeNameIdMap;
|
const solution = values[current.title];
|
||||||
|
if (solution) {
|
||||||
|
return {
|
||||||
|
...valuesMap,
|
||||||
|
[current.id]: solution
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return valuesMap;
|
||||||
|
}, {});
|
||||||
return this.props.updateUserBackend({
|
return this.props.updateUserBackend({
|
||||||
projects: {
|
projects: {
|
||||||
[id]: values
|
[id]: valuesToIds
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
blockNameIsCertMap,
|
modernProjects,
|
||||||
claimCert,
|
legacyProjects
|
||||||
hardGoTo,
|
|
||||||
projects,
|
|
||||||
userProjects,
|
|
||||||
username
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
if (!projects.length) {
|
if (!modernProjects.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@ -150,88 +282,14 @@ class CertificationSettings extends PureComponent {
|
|||||||
you can claim it.
|
you can claim it.
|
||||||
</p>
|
</p>
|
||||||
</FullWidthRow>
|
</FullWidthRow>
|
||||||
{
|
{
|
||||||
projects.map(({
|
modernProjects.map(this.buildProjectForms)
|
||||||
projectBlockName,
|
}
|
||||||
challenges,
|
<SectionHeader>
|
||||||
superBlock
|
Legacy Certificate Settings
|
||||||
}) => {
|
</SectionHeader>
|
||||||
const isCertClaimed = blockNameIsCertMap[projectBlockName];
|
{
|
||||||
if (superBlock === jsProjectSuperBlock) {
|
legacyProjects.map(this.buildProjectForms)
|
||||||
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>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -43,7 +43,7 @@ class JSAlgoAndDSForm extends PureComponent {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { username, superBlock, isCertClaimed } = this.props;
|
const { username, superBlock, isCertClaimed } = this.props;
|
||||||
if (isCertClaimed) {
|
if (isCertClaimed) {
|
||||||
return this.props.hardGoTo(`/c/${username}/${superBlock}`);
|
return this.props.hardGoTo(`/certificates/${username}/${superBlock}`);
|
||||||
}
|
}
|
||||||
return this.props.claimCert(superBlock);
|
return this.props.claimCert(superBlock);
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,14 @@
|
|||||||
import { dasherize } from '../../../../../server/utils/index';
|
|
||||||
|
|
||||||
export const jsProjectSuperBlock = 'javascript-algorithms-and-data-structures';
|
export const jsProjectSuperBlock = 'javascript-algorithms-and-data-structures';
|
||||||
|
|
||||||
export function buildUserProjectsMap(projectBlock, challengeMap) {
|
export function buildUserProjectsMap(projectBlock, challengeMap) {
|
||||||
const {
|
const {
|
||||||
challengeNameIdMap,
|
|
||||||
challenges,
|
challenges,
|
||||||
superBlock
|
superBlock
|
||||||
} = projectBlock;
|
} = projectBlock;
|
||||||
return {
|
return {
|
||||||
[superBlock]: challenges.reduce((solutions, current) => {
|
[superBlock]: challenges.reduce((solutions, current) => {
|
||||||
const dashedName = dasherize(current)
|
const { id } = current;
|
||||||
.replace('java-script', 'javascript')
|
const completed = challengeMap[id];
|
||||||
.replace('metric-imperial', 'metricimperial');
|
|
||||||
const completed = challengeMap[challengeNameIdMap[dashedName]];
|
|
||||||
let solution = '';
|
let solution = '';
|
||||||
if (superBlock === jsProjectSuperBlock) {
|
if (superBlock === jsProjectSuperBlock) {
|
||||||
solution = {};
|
solution = {};
|
||||||
@ -25,7 +20,7 @@ export function buildUserProjectsMap(projectBlock, challengeMap) {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...solutions,
|
...solutions,
|
||||||
[current]: solution
|
[current.title]: solution
|
||||||
};
|
};
|
||||||
}, {})
|
}, {})
|
||||||
};
|
};
|
||||||
|
@ -9,7 +9,6 @@ import loopback from 'loopback';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { themes } from '../utils/themes';
|
import { themes } from '../utils/themes';
|
||||||
import { dasherize } from '../../server/utils';
|
|
||||||
import { saveUser, observeMethod } from '../../server/utils/rx.js';
|
import { saveUser, observeMethod } from '../../server/utils/rx.js';
|
||||||
import { blacklistedUsernames } from '../../server/utils/constants.js';
|
import { blacklistedUsernames } from '../../server/utils/constants.js';
|
||||||
import { wrapHandledError } from '../../server/utils/create-handled-error.js';
|
import { wrapHandledError } from '../../server/utils/create-handled-error.js';
|
||||||
@ -43,28 +42,24 @@ function destroyAll(id, Model) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildChallengeMapUpdate(challengeMap, project) {
|
function buildChallengeMapUpdate(challengeMap, project) {
|
||||||
|
const key = Object.keys(project)[0];
|
||||||
|
const solutions = project[key];
|
||||||
const currentChallengeMap = { ...challengeMap };
|
const currentChallengeMap = { ...challengeMap };
|
||||||
const { nameToIdMap } = _.values(project)[0];
|
const currentCompletedProjects = _.pick(
|
||||||
const incomingUpdate = _.pickBy(
|
currentChallengeMap,
|
||||||
_.omit(_.values(project)[0], [ 'id', 'nameToIdMap' ]),
|
Object.keys(solutions)
|
||||||
Boolean
|
|
||||||
);
|
);
|
||||||
const currentCompletedProjects = _.pick(challengeMap, _.values(nameToIdMap));
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const update = Object.keys(incomingUpdate).reduce((update, current) => {
|
const update = Object.keys(solutions).reduce((update, currentId) => {
|
||||||
const dashedName = dasherize(current)
|
|
||||||
.replace('java-script', 'javascript')
|
|
||||||
.replace('metric-imperial', 'metricimperial');
|
|
||||||
const currentId = nameToIdMap[dashedName];
|
|
||||||
if (
|
if (
|
||||||
currentId in currentCompletedProjects &&
|
currentId in currentCompletedProjects &&
|
||||||
currentCompletedProjects[currentId].solution !== incomingUpdate[current]
|
currentCompletedProjects[currentId].solution !== solutions[currentId]
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
...update,
|
...update,
|
||||||
[currentId]: {
|
[currentId]: {
|
||||||
...currentCompletedProjects[currentId],
|
...currentCompletedProjects[currentId],
|
||||||
solution: incomingUpdate[current],
|
solution: solutions[currentId],
|
||||||
numOfAttempts: currentCompletedProjects[currentId].numOfAttempts + 1
|
numOfAttempts: currentCompletedProjects[currentId].numOfAttempts + 1
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -74,7 +69,7 @@ function buildChallengeMapUpdate(challengeMap, project) {
|
|||||||
...update,
|
...update,
|
||||||
[currentId]: {
|
[currentId]: {
|
||||||
id: currentId,
|
id: currentId,
|
||||||
solution: incomingUpdate[current],
|
solution: solutions[currentId],
|
||||||
challengeType: 3,
|
challengeType: 3,
|
||||||
completedDate: now,
|
completedDate: now,
|
||||||
numOfAttempts: 1
|
numOfAttempts: 1
|
||||||
|
88
common/utils/legacyProjectData.js
Normal file
88
common/utils/legacyProjectData.js
Normal 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;
|
@ -273,7 +273,7 @@
|
|||||||
"translations": {}
|
"translations": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "587d78b0367417b2b2512b06",
|
"id": "bd7158d8c242eddfaeb5bd13",
|
||||||
"title": "Build a Personal Portfolio Webpage",
|
"title": "Build a Personal Portfolio Webpage",
|
||||||
"description": [
|
"description": [
|
||||||
"Fulfill the user stories by getting all of the tests to pass. Use whichever libraries you need. Give it your own personal style.",
|
"Fulfill the user stories by getting all of the tests to pass. Use whichever libraries you need. Give it your own personal style.",
|
||||||
|
@ -101,7 +101,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "bd7158d8c442eddfaeb5bd1f",
|
"id": "bd7158d8c442eddfaeb5bd1f",
|
||||||
"title": "Use the Twitch.tv JSON API",
|
"title": "Use the Twitch JSON API",
|
||||||
"description": [
|
"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>.",
|
"<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.",
|
"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,
|
"isRequired": false,
|
||||||
"translations": {
|
"translations": {
|
||||||
"es": {
|
"es": {
|
||||||
"title": "Usa el API JSON de Twitch.tv",
|
"title": "Usa el API JSON de Twitch",
|
||||||
"description": [
|
"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>.",
|
"<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.",
|
"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": {
|
"ru": {
|
||||||
"title": "Используйте Twitch.tv JSON API",
|
"title": "Используйте Twitch JSON API",
|
||||||
"description": [
|
"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'>Задание:</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 примера. Напишите его самостоятельно.",
|
"<span class='text-info'>Правило #1:</span> Не подсматривайте код приведенного на CodePen примера. Напишите его самостоятельно.",
|
||||||
@ -682,6 +682,127 @@
|
|||||||
"isRequired": false,
|
"isRequired": false,
|
||||||
"titleEs": "Crea un clon de Pinterest"
|
"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",
|
"id": "5a4b7fcdb66f799f199e11db",
|
||||||
"title": "Build a Pong Game",
|
"title": "Build a Pong Game",
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import loopback from 'loopback';
|
import loopback from 'loopback';
|
||||||
|
import moment from 'moment-timezone';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import dedent from 'dedent';
|
import dedent from 'dedent';
|
||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
@ -13,26 +14,23 @@ import {
|
|||||||
import { observeQuery } from '../utils/rx';
|
import { observeQuery } from '../utils/rx';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
// legacy
|
legacyFrontEndChallengeId,
|
||||||
frontEndChallengeId,
|
legacyBackEndChallengeId,
|
||||||
backEndChallengeId,
|
legacyDataVisId,
|
||||||
dataVisId,
|
|
||||||
|
|
||||||
// modern
|
|
||||||
respWebDesignId,
|
respWebDesignId,
|
||||||
frontEndLibsId,
|
frontEndLibsId,
|
||||||
dataVis2018Id,
|
|
||||||
jsAlgoDataStructId,
|
jsAlgoDataStructId,
|
||||||
|
dataVis2018Id,
|
||||||
apisMicroservicesId,
|
apisMicroservicesId,
|
||||||
infosecQaId
|
infosecQaId
|
||||||
} from '../utils/constantStrings.json';
|
} from '../utils/constantStrings.json';
|
||||||
|
import certTypes from '../utils/certTypes.json';
|
||||||
|
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
|
||||||
import {
|
import {
|
||||||
completeCommitment$
|
completeCommitment$
|
||||||
} from '../utils/commit';
|
} from '../utils/commit';
|
||||||
|
|
||||||
import certTypes from '../utils/certTypes.json';
|
|
||||||
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
|
|
||||||
|
|
||||||
const log = debug('fcc:certification');
|
const log = debug('fcc:certification');
|
||||||
const renderCertifedEmail = loopback.template(path.join(
|
const renderCertifedEmail = loopback.template(path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
@ -46,6 +44,48 @@ function isCertified(ids, challengeMap = {}) {
|
|||||||
return _.every(ids, ({ id }) => _.has(challengeMap, id));
|
return _.every(ids, ({ id }) => _.has(challengeMap, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const certIds = {
|
||||||
|
[certTypes.frontEnd]: legacyFrontEndChallengeId,
|
||||||
|
[certTypes.backEnd]: legacyBackEndChallengeId,
|
||||||
|
[certTypes.dataVis]: legacyDataVisId,
|
||||||
|
[certTypes.respWebDesign]: respWebDesignId,
|
||||||
|
[certTypes.frontEndLibs]: frontEndLibsId,
|
||||||
|
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
|
||||||
|
[certTypes.dataVis2018]: dataVis2018Id,
|
||||||
|
[certTypes.apisMicroservices]: apisMicroservicesId,
|
||||||
|
[certTypes.infosecQa]: infosecQaId
|
||||||
|
};
|
||||||
|
|
||||||
|
const certViews = {
|
||||||
|
[certTypes.frontEnd]: 'certificate/legacy/front-end.jade',
|
||||||
|
[certTypes.backEnd]: 'certificate/legacy/back-end.jade',
|
||||||
|
[certTypes.dataVis]: 'certificate/legacy/data-visualization.jade',
|
||||||
|
[certTypes.fullStack]: 'certificate/legacy/full-stack.jade',
|
||||||
|
|
||||||
|
[certTypes.respWebDesign]: 'certificate/responsive-web-design.jade',
|
||||||
|
[certTypes.frontEndLibs]: 'certificate/front-end-libraries.jade',
|
||||||
|
[certTypes.jsAlgoDataStruct]:
|
||||||
|
'certificate/javascript-algorithms-and-data-structures.jade',
|
||||||
|
[certTypes.dataVis2018]: 'certificate/data-visualization.jade',
|
||||||
|
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
|
||||||
|
[certTypes.infosecQa]:
|
||||||
|
'certificate/information-security-and-quality-assurance.jade'
|
||||||
|
};
|
||||||
|
|
||||||
|
const certText = {
|
||||||
|
[certTypes.frontEnd]: 'Legacy Front End certified',
|
||||||
|
[certTypes.backEnd]: 'Legacy Back End Certified',
|
||||||
|
[certTypes.dataVis]: 'Legacy Data Visualization Certified',
|
||||||
|
[certTypes.fullStack]: 'Legacy Full Stack Certified',
|
||||||
|
[certTypes.respWebDesign]: 'Responsive Web Design Certified',
|
||||||
|
[certTypes.frontEndLibs]: 'Front End Libraries Certified',
|
||||||
|
[certTypes.jsAlgoDataStruct]:
|
||||||
|
'JavaScript Algorithms and Data Structures Certified',
|
||||||
|
[certTypes.dataVis2018]: 'Data Visualization Certified',
|
||||||
|
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
|
||||||
|
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
|
||||||
|
};
|
||||||
|
|
||||||
function getIdsForCert$(id, Challenge) {
|
function getIdsForCert$(id, Challenge) {
|
||||||
return observeQuery(
|
return observeQuery(
|
||||||
Challenge,
|
Challenge,
|
||||||
@ -117,13 +157,24 @@ function sendCertifiedEmail(
|
|||||||
|
|
||||||
export default function certificate(app) {
|
export default function certificate(app) {
|
||||||
const router = app.loopback.Router();
|
const router = app.loopback.Router();
|
||||||
const { Email, Challenge } = app.models;
|
const { Email, Challenge, User } = app.models;
|
||||||
|
|
||||||
|
function findUserByUsername$(username, fields) {
|
||||||
|
return observeQuery(
|
||||||
|
User,
|
||||||
|
'findOne',
|
||||||
|
{
|
||||||
|
where: { username },
|
||||||
|
fields
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const certTypeIds = {
|
const certTypeIds = {
|
||||||
// legacy
|
// legacy
|
||||||
[certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge),
|
[certTypes.frontEnd]: getIdsForCert$(legacyFrontEndChallengeId, Challenge),
|
||||||
[certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge),
|
[certTypes.backEnd]: getIdsForCert$(legacyBackEndChallengeId, Challenge),
|
||||||
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge),
|
[certTypes.dataVis]: getIdsForCert$(legacyDataVisId, Challenge),
|
||||||
|
|
||||||
// modern
|
// modern
|
||||||
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
|
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
|
||||||
@ -145,6 +196,10 @@ export default function certificate(app) {
|
|||||||
ifNoSuperBlock404,
|
ifNoSuperBlock404,
|
||||||
verifyCert
|
verifyCert
|
||||||
);
|
);
|
||||||
|
router.get(
|
||||||
|
'/certificates/:username/:cert',
|
||||||
|
showCert
|
||||||
|
);
|
||||||
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
@ -172,16 +227,12 @@ export default function certificate(app) {
|
|||||||
|
|
||||||
function verifyCert(req, res, next) {
|
function verifyCert(req, res, next) {
|
||||||
const { body: { superBlock }, user } = req;
|
const { body: { superBlock }, user } = req;
|
||||||
|
log(superBlock);
|
||||||
let certType = superBlockCertTypeMap[superBlock];
|
let certType = superBlockCertTypeMap[superBlock];
|
||||||
log(certType);
|
log(certType);
|
||||||
if (certType === 'isDataVisCert') {
|
|
||||||
certType = 'is2018DataVisCert';
|
|
||||||
log(certType);
|
|
||||||
}
|
|
||||||
return user.getChallengeMap$()
|
return user.getChallengeMap$()
|
||||||
.flatMap(() => certTypeIds[certType])
|
.flatMap(() => certTypeIds[certType])
|
||||||
.flatMap(challenge => {
|
.flatMap(challenge => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
tests,
|
tests,
|
||||||
@ -251,4 +302,101 @@ export default function certificate(app) {
|
|||||||
}
|
}
|
||||||
return res.status(404).end();
|
return res.status(404).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showCert(req, res, next) {
|
||||||
|
let { username, cert } = req.params;
|
||||||
|
username = username.toLowerCase();
|
||||||
|
const certType = superBlockCertTypeMap[cert];
|
||||||
|
const certId = certIds[certType];
|
||||||
|
return findUserByUsername$(
|
||||||
|
username,
|
||||||
|
{
|
||||||
|
isCheater: true,
|
||||||
|
isLocked: true,
|
||||||
|
isFrontEndCert: true,
|
||||||
|
isBackEndCert: true,
|
||||||
|
isFullStackCert: true,
|
||||||
|
isRespWebDesignCert: true,
|
||||||
|
isFrontEndLibsCert: true,
|
||||||
|
isJsAlgoDataStructCert: true,
|
||||||
|
isDataVisCert: true,
|
||||||
|
is2018DataVisCert: true,
|
||||||
|
isApisMicroservicesCert: true,
|
||||||
|
isInfosecQaCert: true,
|
||||||
|
isHonest: true,
|
||||||
|
username: true,
|
||||||
|
name: true,
|
||||||
|
challengeMap: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
user => {
|
||||||
|
const profile = `/${user.username}`;
|
||||||
|
if (!user) {
|
||||||
|
req.flash(
|
||||||
|
'danger',
|
||||||
|
`We couldn't find a user with the username ${username}`
|
||||||
|
);
|
||||||
|
return res.redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.name) {
|
||||||
|
req.flash(
|
||||||
|
'danger',
|
||||||
|
dedent`
|
||||||
|
This user needs to add their name to their account
|
||||||
|
in order for others to be able to view their certificate.
|
||||||
|
`
|
||||||
|
);
|
||||||
|
return res.redirect(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isCheater) {
|
||||||
|
return res.redirect(`/${user.username}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isLocked) {
|
||||||
|
req.flash(
|
||||||
|
'danger',
|
||||||
|
dedent`
|
||||||
|
${username} has chosen to make their profile
|
||||||
|
private. They will need to make their profile public
|
||||||
|
in order for others to be able to view their certificate.
|
||||||
|
`
|
||||||
|
);
|
||||||
|
return res.redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.isHonest) {
|
||||||
|
req.flash(
|
||||||
|
'danger',
|
||||||
|
dedent`
|
||||||
|
${username} has not yet agreed to our Academic Honesty Pledge.
|
||||||
|
`
|
||||||
|
);
|
||||||
|
return res.redirect(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user[certType]) {
|
||||||
|
const { challengeMap = {} } = user;
|
||||||
|
const { completedDate = new Date() } = challengeMap[certId] || {};
|
||||||
|
|
||||||
|
return res.render(
|
||||||
|
certViews[certType],
|
||||||
|
{
|
||||||
|
username: user.username,
|
||||||
|
date: moment(new Date(completedDate)).format('MMMM D, YYYY'),
|
||||||
|
name: user.name
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
req.flash(
|
||||||
|
'danger',
|
||||||
|
`Looks like user ${username} is not ${certText[certType]}`
|
||||||
|
);
|
||||||
|
return res.redirect(profile);
|
||||||
|
},
|
||||||
|
next
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,88 +1,22 @@
|
|||||||
import dedent from 'dedent';
|
import dedent from 'dedent';
|
||||||
import moment from 'moment-timezone';
|
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
import { curry } from 'lodash';
|
import { curry } from 'lodash';
|
||||||
|
|
||||||
import {
|
|
||||||
frontEndChallengeId,
|
|
||||||
backEndChallengeId,
|
|
||||||
respWebDesignId,
|
|
||||||
frontEndLibsId,
|
|
||||||
jsAlgoDataStructId,
|
|
||||||
dataVisId,
|
|
||||||
dataVis2018Id,
|
|
||||||
apisMicroservicesId,
|
|
||||||
infosecQaId
|
|
||||||
} from '../utils/constantStrings.json';
|
|
||||||
import certTypes from '../utils/certTypes.json';
|
|
||||||
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
|
|
||||||
import {
|
import {
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
ifNoUserRedirectTo,
|
ifNoUserRedirectTo,
|
||||||
ifNotVerifiedRedirectToSettings
|
ifNotVerifiedRedirectToSettings
|
||||||
} from '../utils/middleware';
|
} from '../utils/middleware';
|
||||||
import { observeQuery } from '../utils/rx';
|
|
||||||
|
|
||||||
const debug = debugFactory('fcc:boot:user');
|
const debug = debugFactory('fcc:boot:user');
|
||||||
const sendNonUserToMap = ifNoUserRedirectTo('/map');
|
const sendNonUserToMap = ifNoUserRedirectTo('/map');
|
||||||
const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/map');
|
const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/map');
|
||||||
const certIds = {
|
|
||||||
[certTypes.frontEnd]: frontEndChallengeId,
|
|
||||||
[certTypes.backEnd]: backEndChallengeId,
|
|
||||||
[certTypes.respWebDesign]: respWebDesignId,
|
|
||||||
[certTypes.frontEndLibs]: frontEndLibsId,
|
|
||||||
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
|
|
||||||
[certTypes.dataVis]: dataVisId,
|
|
||||||
[certTypes.dataVis2018]: dataVis2018Id,
|
|
||||||
[certTypes.apisMicroservices]: apisMicroservicesId,
|
|
||||||
[certTypes.infosecQa]: infosecQaId
|
|
||||||
};
|
|
||||||
|
|
||||||
const certViews = {
|
|
||||||
[certTypes.frontEnd]: 'certificate/front-end.jade',
|
|
||||||
[certTypes.backEnd]: 'certificate/back-end.jade',
|
|
||||||
[certTypes.fullStack]: 'certificate/full-stack.jade',
|
|
||||||
[certTypes.respWebDesign]: 'certificate/responsive-web-design.jade',
|
|
||||||
[certTypes.frontEndLibs]: 'certificate/front-end-libraries.jade',
|
|
||||||
[certTypes.jsAlgoDataStruct]:
|
|
||||||
'certificate/javascript-algorithms-and-data-structures.jade',
|
|
||||||
[certTypes.dataVis]: 'certificate/data-visualization.jade',
|
|
||||||
[certTypes.dataVis2018]: 'certificate/data-visualization-2018.jade',
|
|
||||||
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
|
|
||||||
[certTypes.infosecQa]:
|
|
||||||
'certificate/information-security-and-quality-assurance.jade'
|
|
||||||
};
|
|
||||||
|
|
||||||
const certText = {
|
|
||||||
[certTypes.frontEnd]: 'Front End certified',
|
|
||||||
[certTypes.backEnd]: 'Back End Certified',
|
|
||||||
[certTypes.fullStack]: 'Full Stack Certified',
|
|
||||||
[certTypes.respWebDesign]: 'Responsive Web Design Certified',
|
|
||||||
[certTypes.frontEndLibs]: 'Front End Libraries Certified',
|
|
||||||
[certTypes.jsAlgoDataStruct]:
|
|
||||||
'JavaScript Algorithms and Data Structures Certified',
|
|
||||||
[certTypes.dataVis]: 'Data Visualization Certified',
|
|
||||||
[certTypes.dataVis2018]: 'Data Visualization Certified',
|
|
||||||
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
|
|
||||||
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
const router = app.loopback.Router();
|
const router = app.loopback.Router();
|
||||||
const api = app.loopback.Router();
|
const api = app.loopback.Router();
|
||||||
const { Email, User } = app.models;
|
const { Email, User } = app.models;
|
||||||
|
|
||||||
function findUserByUsername$(username, fields) {
|
|
||||||
return observeQuery(
|
|
||||||
User,
|
|
||||||
'findOne',
|
|
||||||
{
|
|
||||||
where: { username },
|
|
||||||
fields
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
api.post(
|
api.post(
|
||||||
'/account/delete',
|
'/account/delete',
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
@ -105,11 +39,6 @@ module.exports = function(app) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Ensure these are the last routes!
|
// Ensure these are the last routes!
|
||||||
api.get(
|
|
||||||
'/c/:username/:cert',
|
|
||||||
showCert
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/user/:username/report-user/',
|
'/user/:username/report-user/',
|
||||||
sendNonUserToMapWithMessage('You must be signed in to report a user'),
|
sendNonUserToMapWithMessage('You must be signed in to report a user'),
|
||||||
@ -185,100 +114,6 @@ module.exports = function(app) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCert(req, res, next) {
|
|
||||||
let { username, cert } = req.params;
|
|
||||||
username = username.toLowerCase();
|
|
||||||
const certType = superBlockCertTypeMap[cert];
|
|
||||||
const certId = certIds[certType];
|
|
||||||
return findUserByUsername$(username, {
|
|
||||||
isCheater: true,
|
|
||||||
isLocked: true,
|
|
||||||
isFrontEndCert: true,
|
|
||||||
isBackEndCert: true,
|
|
||||||
isFullStackCert: true,
|
|
||||||
isRespWebDesignCert: true,
|
|
||||||
isFrontEndLibsCert: true,
|
|
||||||
isJsAlgoDataStructCert: true,
|
|
||||||
isDataVisCert: true,
|
|
||||||
is2018DataVisCert: true,
|
|
||||||
isApisMicroservicesCert: true,
|
|
||||||
isInfosecQaCert: true,
|
|
||||||
isHonest: true,
|
|
||||||
username: true,
|
|
||||||
name: true,
|
|
||||||
challengeMap: true
|
|
||||||
})
|
|
||||||
.subscribe(
|
|
||||||
user => {
|
|
||||||
const profile = `/${user.username}`;
|
|
||||||
if (!user) {
|
|
||||||
req.flash(
|
|
||||||
'danger',
|
|
||||||
`We couldn't find a user with the username ${username}`
|
|
||||||
);
|
|
||||||
return res.redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.name) {
|
|
||||||
req.flash(
|
|
||||||
'danger',
|
|
||||||
dedent`
|
|
||||||
This user needs to add their name to their account
|
|
||||||
in order for others to be able to view their certificate.
|
|
||||||
`
|
|
||||||
);
|
|
||||||
return res.redirect(profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isCheater) {
|
|
||||||
return res.redirect(`/${user.username}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isLocked) {
|
|
||||||
req.flash(
|
|
||||||
'danger',
|
|
||||||
dedent`
|
|
||||||
${username} has chosen to make their profile
|
|
||||||
private. They will need to make their profile public
|
|
||||||
in order for others to be able to view their certificate.
|
|
||||||
`
|
|
||||||
);
|
|
||||||
return res.redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.isHonest) {
|
|
||||||
req.flash(
|
|
||||||
'danger',
|
|
||||||
dedent`
|
|
||||||
${username} has not yet agreed to our Academic Honesty Pledge.
|
|
||||||
`
|
|
||||||
);
|
|
||||||
return res.redirect(profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user[certType]) {
|
|
||||||
const { challengeMap = {} } = user;
|
|
||||||
const { completedDate = new Date() } = challengeMap[certId] || {};
|
|
||||||
|
|
||||||
return res.render(
|
|
||||||
certViews[certType],
|
|
||||||
{
|
|
||||||
username: user.username,
|
|
||||||
date: moment(new Date(completedDate)).format('MMMM D, YYYY'),
|
|
||||||
name: user.name
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
req.flash(
|
|
||||||
'danger',
|
|
||||||
`Looks like user ${username} is not ${certText[certType]}`
|
|
||||||
);
|
|
||||||
return res.redirect(profile);
|
|
||||||
},
|
|
||||||
next
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function postDeleteAccount(req, res, next) {
|
function postDeleteAccount(req, res, next) {
|
||||||
User.destroyById(req.user.id, function(err) {
|
User.destroyById(req.user.id, function(err) {
|
||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"gitHubUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1521.3 Safari/537.36",
|
"gitHubUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1521.3 Safari/537.36",
|
||||||
|
|
||||||
"frontEndChallengeId": "561add10cb82ac38a17513be",
|
"legacyFrontEndChallengeId": "561add10cb82ac38a17513be",
|
||||||
"backEndChallengeId": "660add10cb82ac38a17513be",
|
"legacyBackEndChallengeId": "660add10cb82ac38a17513be",
|
||||||
"dataVisId": "561add10cb82ac39a17513bc",
|
"legacyDataVisId": "561add10cb82ac39a17513bc",
|
||||||
|
|
||||||
"respWebDesignId": "561add10cb82ac38a17513bc",
|
"respWebDesignId": "561add10cb82ac38a17513bc",
|
||||||
"frontEndLibsId": "561acd10cb82ac38a17513bc",
|
"frontEndLibsId": "561acd10cb82ac38a17513bc",
|
||||||
|
@ -15,6 +15,7 @@ export const publicUserProps = [
|
|||||||
'isApisMicroservicesCert',
|
'isApisMicroservicesCert',
|
||||||
'isBackEndCert',
|
'isBackEndCert',
|
||||||
'isCheater',
|
'isCheater',
|
||||||
|
'is2018DataVisCert',
|
||||||
'isDataVisCert',
|
'isDataVisCert',
|
||||||
'isFrontEndCert',
|
'isFrontEndCert',
|
||||||
'isFullStackCert',
|
'isFullStackCert',
|
||||||
|
@ -2,16 +2,16 @@ import certTypes from './certTypes.json';
|
|||||||
|
|
||||||
const superBlockCertTypeMap = {
|
const superBlockCertTypeMap = {
|
||||||
// legacy
|
// legacy
|
||||||
'front-end': certTypes.frontEnd,
|
'legacy-front-end': certTypes.frontEnd,
|
||||||
'back-end': certTypes.backEnd,
|
'legacy-back-end': certTypes.backEnd,
|
||||||
'data-visualization': certTypes.dataVis,
|
'legacy-data-visualization': certTypes.dataVis,
|
||||||
'full-stack': certTypes.fullStack,
|
'legacy-full-stack': certTypes.fullStack,
|
||||||
|
|
||||||
// modern
|
// modern
|
||||||
'responsive-web-design': certTypes.respWebDesign,
|
'responsive-web-design': certTypes.respWebDesign,
|
||||||
'javascript-algorithms-and-data-structures': certTypes.jsAlgoDataStruct,
|
'javascript-algorithms-and-data-structures': certTypes.jsAlgoDataStruct,
|
||||||
'front-end-libraries': certTypes.frontEndLibs,
|
'front-end-libraries': certTypes.frontEndLibs,
|
||||||
'data-visualization-2018': certTypes.dataVis2018,
|
'data-visualization': certTypes.dataVis2018,
|
||||||
'apis-and-microservices': certTypes.apisMicroservices,
|
'apis-and-microservices': certTypes.apisMicroservices,
|
||||||
'information-security-and-quality-assurance': certTypes.infosecQa
|
'information-security-and-quality-assurance': certTypes.infosecQa
|
||||||
};
|
};
|
||||||
|
@ -29,4 +29,4 @@ include styles
|
|||||||
strong Quincy Larson
|
strong Quincy Larson
|
||||||
p Executive Director, freeCodeCamp.org
|
p Executive Director, freeCodeCamp.org
|
||||||
.row
|
.row
|
||||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/advanced-front-end-certification
|
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/advanced-front-end
|
@ -29,4 +29,4 @@ include styles
|
|||||||
strong Quincy Larson
|
strong Quincy Larson
|
||||||
p Executive Director, freeCodeCamp.org
|
p Executive Director, freeCodeCamp.org
|
||||||
.row
|
.row
|
||||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/apis-and-microservices-certification
|
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/apis-and-microservices
|
||||||
|
@ -29,4 +29,4 @@ include styles
|
|||||||
strong Quincy Larson
|
strong Quincy Larson
|
||||||
p Executive Director, freeCodeCamp.org
|
p Executive Director, freeCodeCamp.org
|
||||||
.row
|
.row
|
||||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/data-visualization-certification
|
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/data-visualization
|
||||||
|
@ -29,4 +29,4 @@ include styles
|
|||||||
strong Quincy Larson
|
strong Quincy Larson
|
||||||
p Executive Director, freeCodeCamp.org
|
p Executive Director, freeCodeCamp.org
|
||||||
.row
|
.row
|
||||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/front-end-libraries-certification
|
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/front-end-libraries
|
||||||
|
@ -29,4 +29,4 @@ include styles
|
|||||||
strong Quincy Larson
|
strong Quincy Larson
|
||||||
p Executive Director, freeCodeCamp.org
|
p Executive Director, freeCodeCamp.org
|
||||||
.row
|
.row
|
||||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/information-security-and-quality-assurance-certification
|
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/information-security-and-quality-assurance
|
||||||
|
@ -29,4 +29,4 @@ include styles
|
|||||||
strong Quincy Larson
|
strong Quincy Larson
|
||||||
p Executive Director, freeCodeCamp.org
|
p Executive Director, freeCodeCamp.org
|
||||||
.row
|
.row
|
||||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/javascript-algorithms-and-data-structures-certification
|
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/javascript-algorithms-and-data-structures
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
meta(name='viewport', content='width=device-width, initial-scale=1')
|
meta(name='viewport', content='width=device-width, initial-scale=1')
|
||||||
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css')
|
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css')
|
||||||
include styles
|
include ../styles
|
||||||
|
|
||||||
.certificate-wrapper.container
|
.certificate-wrapper.container
|
||||||
.row
|
.row
|
||||||
@ -29,4 +29,4 @@ include styles
|
|||||||
strong Quincy Larson
|
strong Quincy Larson
|
||||||
p Executive Director, freeCodeCamp.org
|
p Executive Director, freeCodeCamp.org
|
||||||
.row
|
.row
|
||||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/back-end-certification
|
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/legacy-back-end
|
32
server/views/certificate/legacy/data-visualization.jade
Normal file
32
server/views/certificate/legacy/data-visualization.jade
Normal 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
|
||||||
|
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
|
@ -1,6 +1,6 @@
|
|||||||
meta(name='viewport', content='width=device-width, initial-scale=1')
|
meta(name='viewport', content='width=device-width, initial-scale=1')
|
||||||
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css')
|
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css')
|
||||||
include styles
|
include ../styles
|
||||||
|
|
||||||
.certificate-wrapper.container
|
.certificate-wrapper.container
|
||||||
.row
|
.row
|
||||||
@ -29,4 +29,4 @@ include styles
|
|||||||
strong Quincy Larson
|
strong Quincy Larson
|
||||||
p Executive Director, freeCodeCamp.org
|
p Executive Director, freeCodeCamp.org
|
||||||
.row
|
.row
|
||||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/front-end-certification
|
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/legacy-front-end
|
@ -1,6 +1,6 @@
|
|||||||
meta(name='viewport', content='width=device-width, initial-scale=1')
|
meta(name='viewport', content='width=device-width, initial-scale=1')
|
||||||
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css')
|
link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css')
|
||||||
include styles
|
include ../styles
|
||||||
|
|
||||||
.certificate-wrapper.container
|
.certificate-wrapper.container
|
||||||
.row
|
.row
|
||||||
@ -19,8 +19,8 @@ include styles
|
|||||||
strong= name
|
strong= name
|
||||||
h3 has successfully completed freeCodeCamp's
|
h3 has successfully completed freeCodeCamp's
|
||||||
h1
|
h1
|
||||||
strong Full Stack Development Projects
|
strong Legacy Full Stack Development Program
|
||||||
h4 1 of 3 legacy freeCodeCamp certificates, representing approximately 400 hours of coursework
|
h4 All three of the legacy freeCodeCamp certificates, representing approximately 12000 hours of coursework
|
||||||
|
|
||||||
footer
|
footer
|
||||||
.row.signatures
|
.row.signatures
|
||||||
@ -29,4 +29,4 @@ include styles
|
|||||||
strong Quincy Larson
|
strong Quincy Larson
|
||||||
p Executive Director, freeCodeCamp.org
|
p Executive Director, freeCodeCamp.org
|
||||||
.row
|
.row
|
||||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/full-stack-certification
|
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/legacy-full-stack
|
@ -29,4 +29,4 @@ include styles
|
|||||||
strong Quincy Larson
|
strong Quincy Larson
|
||||||
p Executive Director, freeCodeCamp.org
|
p Executive Director, freeCodeCamp.org
|
||||||
.row
|
.row
|
||||||
p.verify Verify this certificate at: https://freecodecamp.org/c/#{username}/responsive-web-design-certification
|
p.verify Verify this certificate at: https://freecodecamp.org/certificates/#{username}/responsive-web-design
|
||||||
|
Reference in New Issue
Block a user