fix(client): display legacy certs like current ones (#42038)

* fix: display legacy certs like the current ones

* fix: link projects in legacy certs to project pages

* fix: update tests to changed legacy cert display

* fix: update tests for removed legacy certs forms

* fix: display legacy certs like the current ones

* fix: submit projects for cert on projects pages

* fix: remove legacy certs form submitting handling

* fix: move claiming cert setup before both tests

* fix: remove legacy cert update props and actions

* fix: remove legacy cert updates from api

* fix: correct merge conflict
This commit is contained in:
gikf
2021-06-11 18:06:46 +02:00
committed by GitHub
parent c6aa6ddbcd
commit bc9e8a69de
10 changed files with 155 additions and 385 deletions

View File

@@ -1,8 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { find, first, values, isString } from 'lodash-es';
import { find, first } from 'lodash-es';
import {
Table,
Button,
@@ -21,16 +19,10 @@ import {
import SectionHeader from './SectionHeader';
import ProjectModal from '../SolutionViewer/ProjectModal';
import { FullWidthRow, Spacer } from '../helpers';
import { Form } from '../formHelpers';
import { maybeUrlRE } from '../../utils';
import reallyWeirdErrorMessage from '../../utils/reallyWeirdErrorMessage';
import './certification.css';
import { updateLegacyCert } from '../../redux/settings';
const mapDispatchToProps = dispatch =>
bindActionCreators({ updateLegacyCert }, dispatch);
const propTypes = {
completedChallenges: PropTypes.arrayOf(
@@ -61,7 +53,6 @@ const propTypes = {
isRespWebDesignCert: PropTypes.bool,
isSciCompPyCertV7: PropTypes.bool,
t: PropTypes.func.isRequired,
updateLegacyCert: PropTypes.func.isRequired,
username: PropTypes.string,
verifyCert: PropTypes.func.isRequired
};
@@ -156,7 +147,6 @@ export class CertificationSettings extends Component {
super(props);
this.state = { ...initialState };
this.handleSubmitLegacy = this.handleSubmitLegacy.bind(this);
}
createHandleLinkButtonClick = to => e => {
@@ -258,9 +248,9 @@ export class CertificationSettings extends Component {
);
};
renderCertifications = certName => {
renderCertifications = (certName, projectsMap) => {
const { t } = this.props;
const { certSlug } = first(projectMap[certName]);
const { certSlug } = first(projectsMap[certName]);
return (
<FullWidthRow key={certName}>
<Spacer />
@@ -277,17 +267,18 @@ export class CertificationSettings extends Component {
<tbody>
{this.renderProjectsFor(
certName,
this.getUserIsCertMap()[certName]
this.getUserIsCertMap()[certName],
projectsMap
)}
</tbody>
</Table>
</FullWidthRow>
);
};
renderProjectsFor = (certName, isCert) => {
renderProjectsFor = (certName, isCert, projectsMap) => {
const { username, isHonest, createFlashMessage, t, verifyCert } =
this.props;
const { certSlug } = first(projectMap[certName]);
const { certSlug } = first(projectsMap[certName]);
const certLocation = `/certification/${username}/${certSlug}`;
const createClickHandler = certSlug => e => {
e.preventDefault();
@@ -298,7 +289,7 @@ export class CertificationSettings extends Component {
? verifyCert(certSlug)
: createFlashMessage(honestyInfoMessage);
};
return projectMap[certName]
return projectsMap[certName]
.map(({ link, title, id }) => (
<tr className='project-row' key={id}>
<td className='project-title col-sm-8'>
@@ -325,160 +316,6 @@ export class CertificationSettings extends Component {
]);
};
// legacy projects rendering
handleSubmitLegacy({ values: formChalObj }) {
const { isHonest, createFlashMessage, verifyCert, updateLegacyCert } =
this.props;
let legacyTitle;
let certSlug;
let certs = Object.keys(legacyProjectMap);
let loopBreak = false;
for (let certTitle of certs) {
for (let chalTitle of legacyProjectMap[certTitle]) {
if (chalTitle.title === Object.keys(formChalObj)[0]) {
certSlug = chalTitle.certSlug;
loopBreak = true;
legacyTitle = certTitle;
break;
}
}
if (loopBreak) {
break;
}
}
// make an object with keys as challenge ids and values as solutions
let idsToSolutions = {};
for (let i of Object.keys(formChalObj)) {
for (let j of legacyProjectMap[legacyTitle]) {
if (i === j.title) {
idsToSolutions[j.id] = formChalObj[i];
break;
}
}
}
// filter the new solutions that need to be updated
const completedChallenges = this.props.completedChallenges;
let challengesToUpdate = {};
let newChallengeFound = true;
let oldSubmissions = 0;
for (let submittedChal of Object.keys(idsToSolutions)) {
for (let i of completedChallenges) {
if (i.id === submittedChal) {
if (idsToSolutions[submittedChal] !== i.solution) {
challengesToUpdate[submittedChal] = idsToSolutions[submittedChal];
}
oldSubmissions++;
newChallengeFound = false;
break;
}
}
if (newChallengeFound && idsToSolutions[submittedChal] !== '') {
challengesToUpdate[submittedChal] = idsToSolutions[submittedChal];
}
newChallengeFound = true;
}
const valuesSaved = values(formChalObj).filter(Boolean).filter(isString);
const isProjectSectionComplete = valuesSaved.length === oldSubmissions;
if (isProjectSectionComplete) {
return isHonest
? verifyCert(certSlug)
: createFlashMessage(honestyInfoMessage);
}
return updateLegacyCert({ challengesToUpdate, certSlug });
}
renderLegacyCertifications = certName => {
const { username, createFlashMessage, completedChallenges, t } = this.props;
const { certSlug } = first(legacyProjectMap[certName]);
const certLocation = `/certification/${username}/${certSlug}`;
const challengeTitles = legacyProjectMap[certName].map(item => item.title);
const isCertClaimed = this.getUserIsCertMap()[certName];
const initialObject = {};
let filledforms = 0;
legacyProjectMap[certName].forEach(project => {
let completedProject = find(completedChallenges, function (challenge) {
return challenge['id'] === project['id'];
});
if (!completedProject) {
initialObject[project.title] = '';
} else {
initialObject[project.title] = completedProject.solution;
filledforms++;
}
});
const options = challengeTitles.reduce(
(options, current) => {
options.types[current] = 'url';
return options;
},
{ types: {} }
);
const formFields = challengeTitles.map(title => ({
name: title,
label: title
}));
const fullForm = filledforms === challengeTitles.length;
const createClickHandler = certLocation => e => {
e.preventDefault();
if (isCertClaimed) {
return navigate(certLocation);
}
return createFlashMessage(reallyWeirdErrorMessage);
};
const buttonStyle = {
marginBottom: '1.45rem'
};
return (
<FullWidthRow key={certSlug}>
<Spacer />
<h3 className='text-center' id={`cert-${certSlug}`}>
{certName}
</h3>
<Form
buttonText={
fullForm ? t('buttons.claim-cert') : t('buttons.save-progress')
}
enableSubmit={fullForm}
formFields={formFields}
hideButton={isCertClaimed}
id={certSlug}
initialValues={{
...initialObject
}}
options={options}
submit={this.handleSubmitLegacy}
/>
{isCertClaimed ? (
<div className={'col-xs-12'}>
<Button
bsSize='sm'
bsStyle='primary'
className={'col-xs-12'}
href={certLocation}
id={'button-' + certSlug}
onClick={createClickHandler(certLocation)}
style={buttonStyle}
target='_blank'
>
{t('buttons.show-cert')}
</Button>
</div>
) : null}
</FullWidthRow>
);
};
renderLegacyFullStack = () => {
const {
isFullStackCert,
@@ -587,10 +424,14 @@ export class CertificationSettings extends Component {
return (
<section id='certification-settings'>
<SectionHeader>{t('settings.headings.certs')}</SectionHeader>
{certifications.map(this.renderCertifications)}
{certifications.map(certName =>
this.renderCertifications(certName, projectMap)
)}
<SectionHeader>{t('settings.headings.legacy-certs')}</SectionHeader>
{this.renderLegacyFullStack()}
{legacyCertifications.map(this.renderLegacyCertifications)}
{legacyCertifications.map(certName =>
this.renderCertifications(certName, legacyProjectMap)
)}
{isOpen ? (
<ProjectModal
files={files}
@@ -609,7 +450,4 @@ export class CertificationSettings extends Component {
CertificationSettings.displayName = 'CertificationSettings';
CertificationSettings.propTypes = propTypes;
export default connect(
null,
mapDispatchToProps
)(withTranslation()(CertificationSettings));
export default withTranslation()(CertificationSettings);

View File

@@ -22,7 +22,9 @@ describe('<certification />', () => {
);
expect(
container.querySelector('#button-legacy-data-visualization')
container.querySelector(
'a[href="/certification/developmentuser/legacy-data-visualization"]'
)
).toHaveTextContent('buttons.show-cert');
});
@@ -32,33 +34,36 @@ describe('<certification />', () => {
);
expect(
container.querySelector('#button-legacy-data-visualization')
).toHaveAttribute(
'href',
'/certification/developmentuser/legacy-data-visualization'
);
container.querySelector(
'a[href="/certification/developmentuser/legacy-data-visualization"]'
)
).toBeInTheDocument();
});
// full forms with unclaimed certs should should not shallow render button
it('Should not render show cert button for unclaimed full form', () => {
// full forms with unclaimed certs should not shallow render show cert button
it('Should not render show cert button for unclaimed cert with completed projects', () => {
const { container } = renderWithRedux(
<CertificationSettings {...defaultTestProps} />
);
expect(
container.querySelector('#button-legacy-back-end')
).not.toBeInTheDocument();
container.querySelector(
'a[href="/certification/developmentuser/legacy-back-end"]'
)
).not.toHaveTextContent('buttons.show-cert');
});
// empty forms with unclaimed certs should should not shallow render button
it('Should not render show cert button for empty form', () => {
// empty forms with unclaimed certs should not shallow render show cert button
it('Should not render show cert button for cert with no completed projects', () => {
const { container } = renderWithRedux(
<CertificationSettings {...defaultTestProps} />
);
expect(
container.querySelector('#button-legacy-front-end')
).not.toBeInTheDocument();
container.querySelector(
'a[href="/certification/developmentuser/legacy-front-end"]'
)
).not.toHaveTextContent('buttons.show-cert');
});
it('Render button when only solution is present', () => {
@@ -243,7 +248,6 @@ const defaultTestProps = {
isSciCompPyCertV7: false,
isDataAnalysisPyCertV7: false,
isMachineLearningPyCertV7: false,
updateLegacyCert: () => {},
username: 'developmentuser',
verifyCert: () => {},
errors: {},

View File

@@ -5,10 +5,6 @@ import { createDangerZoneSaga } from './danger-zone-saga';
import { createSettingsSagas } from './settings-sagas';
import { createUpdateMyEmailSaga } from './update-email-saga';
// prettier-ignore
import { createUpdateLegacyCertSaga } from
'./update-legacy-certificate-saga';
export const ns = 'settings';
const defaultFetchState = {
@@ -31,7 +27,6 @@ export const types = createTypes(
...createAsyncTypes('submitNewAbout'),
...createAsyncTypes('submitNewUsername'),
...createAsyncTypes('updateMyEmail'),
...createAsyncTypes('updateLegacyCert'),
...createAsyncTypes('updateUserFlag'),
...createAsyncTypes('submitProfileUI'),
...createAsyncTypes('verifyCert'),
@@ -44,8 +39,7 @@ export const types = createTypes(
export const sagas = [
...createSettingsSagas(types),
...createUpdateMyEmailSaga(types),
...createDangerZoneSaga(types),
...createUpdateLegacyCertSaga(types)
...createDangerZoneSaga(types)
];
const checkForSuccessPayload = ({ type, payload }) =>
@@ -78,12 +72,6 @@ export const updateMyEmail = createAction(types.updateMyEmail);
export const updateMyEmailComplete = createAction(types.updateMyEmailComplete);
export const updateMyEmailError = createAction(types.updateMyEmailError);
export const updateLegacyCert = createAction(types.updateLegacyCert);
export const updateLegacyCertComplete = createAction(
types.updateLegacyCertComplete
);
export const updateLegacyCertError = createAction(types.updateLegacyCertError);
export const updateUserFlag = createAction(types.updateUserFlag);
export const updateUserFlagComplete = createAction(
types.updateUserFlagComplete,

View File

@@ -1,38 +0,0 @@
import { takeEvery, call, put } from 'redux-saga/effects';
import { putUpdateLegacyCert } from '../../utils/ajax';
import { submitComplete } from '../';
import { createFlashMessage } from '../../components/Flash/redux';
import reallyWeirdErrorMessage from '../../utils/reallyWeirdErrorMessage';
import { updateLegacyCertError } from './';
function* updateLegacyCertSaga({
payload: { superBlock, challengesToUpdate }
}) {
// shape the body of the http call so it is consumable by api
const body = {
projects: {
[superBlock]: challengesToUpdate
}
};
// shape to update completed challenges in redux store
let reduxShape = [];
for (let obj in challengesToUpdate) {
if (challengesToUpdate.hasOwnProperty(obj)) {
reduxShape.push({ id: obj, solution: challengesToUpdate[obj] });
}
}
try {
const { data: response } = yield call(putUpdateLegacyCert, body);
yield put(submitComplete({ challArray: reduxShape }));
yield put(createFlashMessage(response));
} catch (e) {
yield put(updateLegacyCertError(e));
yield put(createFlashMessage(reallyWeirdErrorMessage));
}
}
export function createUpdateLegacyCertSaga(types) {
return [takeEvery(types.updateLegacyCert, updateLegacyCertSaga)];
}

View File

@@ -16,10 +16,17 @@ const dataAnalysisPyBase =
'/learn/data-analysis-with-python/data-analysis-with-python-projects';
const machineLearningPyBase =
'/learn/machine-learning-with-python/machine-learning-with-python-projects';
const legacyFrontEndBase = '';
const legacyBackEndBase = '';
const legacyDataVisBase = '';
const legacyInfosecQaBase = '';
const takeHomeBase = '/learn/coding-interview-prep/take-home-projects';
const legacyFrontEndBase = feLibsBase;
const legacyFrontEndResponsiveBase = responsiveWebBase;
const legacyFrontEndTakeHomeBase = takeHomeBase;
const legacyBackEndBase = apiMicroBase;
const legacyBackEndTakeHomeBase = takeHomeBase;
const legacyDataVisBase = dataVisBase;
const legacyDataVisFrontEndBase = feLibsBase;
const legacyDataVisTakeHomeBase = takeHomeBase;
const legacyInfosecQaQaBase = qaBase;
const legacyInfosecQaInfosecBase = infoSecBase;
// TODO: generate this automatically in a separate file
// from the md/meta.json files for each cert and projects
@@ -33,7 +40,7 @@ const certMap = [
{
id: 'bd7158d8c242eddfaeb5bd13',
title: 'Build a Personal Portfolio Webpage',
link: `${legacyFrontEndBase}/build-a-personal-portfolio-webpage`,
link: `${legacyFrontEndResponsiveBase}/build-a-personal-portfolio-webpage`,
certSlug: 'legacy-front-end'
},
{
@@ -57,37 +64,37 @@ const certMap = [
{
id: 'bd7158d8c442eddfaeb5bd10',
title: 'Show the Local Weather',
link: `${legacyFrontEndBase}/show-the-local-weather`,
link: `${legacyFrontEndTakeHomeBase}/show-the-local-weather`,
certSlug: 'legacy-front-end'
},
{
id: 'bd7158d8c442eddfaeb5bd1f',
title: 'Use the TwitchTV JSON API',
link: `${legacyFrontEndBase}/use-the-twitchtv-json-api`,
link: `${legacyFrontEndTakeHomeBase}/use-the-twitch-json-api`,
certSlug: 'legacy-front-end'
},
{
id: 'bd7158d8c442eddfaeb5bd18',
title: 'Stylize Stories on Camper News',
link: `${legacyFrontEndBase}/stylize-stories-on-camper-news`,
title: 'Build a Tribute Page',
link: `${legacyFrontEndResponsiveBase}/build-a-tribute-page`,
certSlug: 'legacy-front-end'
},
{
id: 'bd7158d8c442eddfaeb5bd19',
title: 'Build a Wikipedia Viewer',
link: `${legacyFrontEndBase}/build-a-wikipedia-viewer`,
link: `${legacyFrontEndTakeHomeBase}/build-a-wikipedia-viewer`,
certSlug: 'legacy-front-end'
},
{
id: 'bd7158d8c442eedfaeb5bd1c',
title: 'Build a Tic Tac Toe Game',
link: `${legacyFrontEndBase}/build-a-tic-tac-toe-game`,
link: `${legacyFrontEndTakeHomeBase}/build-a-tic-tac-toe-game`,
certSlug: 'legacy-front-end'
},
{
id: 'bd7158d8c442eddfaeb5bd1c',
title: 'Build a Simon Game',
link: `${legacyFrontEndBase}/build-a-simon-game`,
link: `${legacyFrontEndTakeHomeBase}/build-a-simon-game`,
certSlug: 'legacy-front-end'
}
]
@@ -119,7 +126,7 @@ const certMap = [
{
id: 'bd7158d8c443edefaeb5bdee',
title: 'Image Search Abstraction Layer',
link: `${legacyBackEndBase}/image-search-abstraction-layer`,
link: `${legacyBackEndTakeHomeBase}/build-an-image-search-abstraction-layer`,
certSlug: 'legacy-back-end'
},
{
@@ -131,31 +138,31 @@ const certMap = [
{
id: 'bd7158d8c443eddfaeb5bdef',
title: 'Build a Voting App',
link: `${legacyBackEndBase}/build-a-voting-app`,
link: `${legacyBackEndTakeHomeBase}/build-a-voting-app`,
certSlug: 'legacy-back-end'
},
{
id: 'bd7158d8c443eddfaeb5bdff',
title: 'Build a Nightlife Coordination App',
link: `${legacyBackEndBase}/build-a-nightlife-coordination-app`,
link: `${legacyBackEndTakeHomeBase}/build-a-nightlife-coordination-app`,
certSlug: 'legacy-back-end'
},
{
id: 'bd7158d8c443eddfaeb5bd0e',
title: 'Chart the Stock Market',
link: `${legacyBackEndBase}/chart-the-stock-market`,
link: `${legacyBackEndTakeHomeBase}/chart-the-stock-market`,
certSlug: 'legacy-back-end'
},
{
id: 'bd7158d8c443eddfaeb5bd0f',
title: 'Manage a Book Trading Club',
link: `${legacyBackEndBase}/manage-a-book-trading-club`,
link: `${legacyBackEndTakeHomeBase}/manage-a-book-trading-club`,
certSlug: 'legacy-back-end'
},
{
id: 'bd7158d8c443eddfaeb5bdee',
title: 'Build a Pinterest Clone',
link: `${legacyBackEndBase}/build-a-pinterest-clone`,
link: `${legacyBackEndTakeHomeBase}/build-a-pinterest-clone`,
certSlug: 'legacy-back-end'
}
]
@@ -177,31 +184,31 @@ const certMap = [
{
id: 'bd7157d8c242eddfaeb5bd13',
title: 'Build a Markdown Previewer',
link: `${legacyDataVisBase}/build-a-markdown-previewer`,
link: `${legacyDataVisFrontEndBase}/build-a-markdown-previewer`,
certSlug: 'legacy-data-visualization'
},
{
id: 'bd7156d8c242eddfaeb5bd13',
title: 'Build a Camper Leaderboard',
link: `${legacyDataVisBase}/build-a-camper-leaderboard`,
title: 'Build a freeCodeCamp Forum Homepage',
link: `${legacyDataVisTakeHomeBase}/build-a-freecodecamp-forum-homepage`,
certSlug: 'legacy-data-visualization'
},
{
id: 'bd7155d8c242eddfaeb5bd13',
title: 'Build a Recipe Box',
link: `${legacyDataVisBase}/build-a-recipe-box`,
link: `${legacyDataVisTakeHomeBase}/build-a-recipe-box`,
certSlug: 'legacy-data-visualization'
},
{
id: 'bd7154d8c242eddfaeb5bd13',
title: 'Build the Game of Life',
link: `${legacyDataVisBase}/build-the-game-of-life`,
link: `${legacyDataVisTakeHomeBase}/build-the-game-of-life`,
certSlug: 'legacy-data-visualization'
},
{
id: 'bd7153d8c242eddfaeb5bd13',
title: 'Build a Roguelike Dungeon Crawler Game',
link: `${legacyDataVisBase}/build-a-roguelike-dungeon-crawler-game`,
link: `${legacyDataVisTakeHomeBase}/build-a-roguelike-dungeon-crawler-game`,
certSlug: 'legacy-data-visualization'
},
{
@@ -225,13 +232,13 @@ const certMap = [
{
id: 'bd7198d8c242eddfaeb5bd13',
title: 'Show National Contiguity with a Force Directed Graph',
link: `${legacyDataVisBase}/show-national-contiguity-with-a-force-directed-graph`,
link: `${legacyDataVisTakeHomeBase}/show-national-contiguity-with-a-force-directed-graph`,
certSlug: 'legacy-data-visualization'
},
{
id: 'bd7108d8c242eddfaeb5bd13',
title: 'Map Data Across the Globe',
link: `${legacyDataVisBase}/map-data-across-the-globe`,
link: `${legacyDataVisTakeHomeBase}/map-data-across-the-globe`,
certSlug: 'legacy-data-visualization'
}
]
@@ -247,31 +254,31 @@ const certMap = [
{
id: '587d8249367417b2b2512c41',
title: 'Metric-Imperial Converter',
link: `${legacyInfosecQaBase}/metric-imperial-converter`,
link: `${legacyInfosecQaQaBase}/metric-imperial-converter`,
certSlug: 'information-security-and-quality-assurance'
},
{
id: '587d8249367417b2b2512c42',
title: 'Issue Tracker',
link: `${legacyInfosecQaBase}/issue-tracker`,
link: `${legacyInfosecQaQaBase}/issue-tracker`,
certSlug: 'information-security-and-quality-assurance'
},
{
id: '587d824a367417b2b2512c43',
title: 'Personal Library',
link: `${legacyInfosecQaBase}/personal-library`,
link: `${legacyInfosecQaQaBase}/personal-library`,
certSlug: 'information-security-and-quality-assurance'
},
{
id: '587d824a367417b2b2512c44',
title: 'Stock Price Checker',
link: `${legacyInfosecQaBase}/stock-price-checker`,
link: `${legacyInfosecQaInfosecBase}/stock-price-checker`,
certSlug: 'information-security-and-quality-assurance'
},
{
id: '587d824a367417b2b2512c45',
title: 'Anonymous Message Board',
link: `${legacyInfosecQaBase}/anonymous-message-board`,
link: `${legacyInfosecQaInfosecBase}/anonymous-message-board`,
certSlug: 'information-security-and-quality-assurance'
}
]

View File

@@ -65,10 +65,6 @@ export function addDonation(body) {
return post('/donate/add-donation', body);
}
export function putUpdateLegacyCert(body) {
return post('/update-my-projects', body);
}
export function postReportUser(body) {
return post('/user/report-user', body);
}