chore: remove verify-can-claim-cert logic (#44574)
* chore: remove verify-can-claim-cert logic * remove extraneous * remove console log before Nich wakes up * add api route back with flash * remove unnecessary logic in completion-epic * change tests for new layout * dynamically use api location * rename file * fix Cypress api location * fix(test): anchor does not have disabled class * fix(tests): change js test to claim from /settings * chore: change status to 410 (gone) * update testing again * oliver is nitpicky Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * make oliver happy Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -12,15 +12,13 @@ import {
|
|||||||
certTypeTitleMap,
|
certTypeTitleMap,
|
||||||
certTypeIdMap,
|
certTypeIdMap,
|
||||||
certIds,
|
certIds,
|
||||||
oldDataVizId,
|
oldDataVizId
|
||||||
superBlockCertTypeMap
|
|
||||||
} from '../../../../config/certification-settings';
|
} from '../../../../config/certification-settings';
|
||||||
import { reportError } from '../middlewares/sentry-error-handler.js';
|
import { reportError } from '../middlewares/sentry-error-handler.js';
|
||||||
|
|
||||||
import { getChallenges } from '../utils/get-curriculum';
|
import { getChallenges } from '../utils/get-curriculum';
|
||||||
import { ifNoUser401 } from '../utils/middleware';
|
import { ifNoUser401 } from '../utils/middleware';
|
||||||
import { observeQuery } from '../utils/rx';
|
import { observeQuery } from '../utils/rx';
|
||||||
import { ensureLowerCaseString } from '../../common/models/user';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
legacyFrontEndChallengeId,
|
legacyFrontEndChallengeId,
|
||||||
@ -50,7 +48,6 @@ export default function bootCertificate(app) {
|
|||||||
const certTypeIds = createCertTypeIds(getChallenges());
|
const certTypeIds = createCertTypeIds(getChallenges());
|
||||||
const showCert = createShowCert(app);
|
const showCert = createShowCert(app);
|
||||||
const verifyCert = createVerifyCert(certTypeIds, app);
|
const verifyCert = createVerifyCert(certTypeIds, app);
|
||||||
const verifyCanClaimCert = createVerifyCanClaim(certTypeIds, app);
|
|
||||||
|
|
||||||
api.put('/certificate/verify', ifNoUser401, ifNoSuperBlock404, verifyCert);
|
api.put('/certificate/verify', ifNoUser401, ifNoSuperBlock404, verifyCert);
|
||||||
api.get('/certificate/showCert/:username/:certSlug', showCert);
|
api.get('/certificate/showCert/:username/:certSlug', showCert);
|
||||||
@ -59,6 +56,15 @@ export default function bootCertificate(app) {
|
|||||||
app.use(api);
|
app.use(api);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function verifyCanClaimCert(_req, res) {
|
||||||
|
return res.status(410).json({
|
||||||
|
message: {
|
||||||
|
type: 'info',
|
||||||
|
message: 'Please reload the app, this feature is no longer available.'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getFallbackFullStackDate(completedChallenges, completedDate) {
|
export function getFallbackFullStackDate(completedChallenges, completedDate) {
|
||||||
var chalIds = [
|
var chalIds = [
|
||||||
certTypeIdMap[certTypes.respWebDesign],
|
certTypeIdMap[certTypes.respWebDesign],
|
||||||
@ -508,97 +514,3 @@ function createShowCert(app) {
|
|||||||
}, next);
|
}, next);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createVerifyCanClaim(certTypeIds, app) {
|
|
||||||
const { User } = app.models;
|
|
||||||
|
|
||||||
function findUserByUsername$(username, fields) {
|
|
||||||
return observeQuery(User, 'findOne', {
|
|
||||||
where: { username },
|
|
||||||
fields
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return function verifyCert(req, res, next) {
|
|
||||||
const { superBlock, username } = req.query;
|
|
||||||
log(superBlock);
|
|
||||||
let certType = superBlockCertTypeMap[superBlock];
|
|
||||||
log(certType);
|
|
||||||
|
|
||||||
return findUserByUsername$(ensureLowerCaseString(username), {
|
|
||||||
isFrontEndCert: true,
|
|
||||||
isBackEndCert: true,
|
|
||||||
isFullStackCert: true,
|
|
||||||
isRespWebDesignCert: true,
|
|
||||||
isFrontEndLibsCert: true,
|
|
||||||
isJsAlgoDataStructCert: true,
|
|
||||||
isDataVisCert: true,
|
|
||||||
is2018DataVisCert: true,
|
|
||||||
isApisMicroservicesCert: true,
|
|
||||||
isInfosecQaCert: true,
|
|
||||||
isQaCertV7: true,
|
|
||||||
isInfosecCertV7: true,
|
|
||||||
isSciCompPyCertV7: true,
|
|
||||||
isDataAnalysisPyCertV7: true,
|
|
||||||
isMachineLearningPyCertV7: true,
|
|
||||||
username: true,
|
|
||||||
name: true,
|
|
||||||
isHonest: true,
|
|
||||||
completedChallenges: true
|
|
||||||
}).subscribe(user => {
|
|
||||||
if (!user) {
|
|
||||||
return res.status(404).json({
|
|
||||||
message: {
|
|
||||||
type: 'info',
|
|
||||||
message: 'flash.username-not-found',
|
|
||||||
variables: { username }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!certTypeIds[certType]) {
|
|
||||||
return res.status(404).json({
|
|
||||||
message: {
|
|
||||||
type: 'info',
|
|
||||||
// TODO: create a specific 'flash.cert-not-found' message
|
|
||||||
message: 'flash.could-not-find'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Observable.of(certTypeIds[certType])
|
|
||||||
.flatMap(challenge => {
|
|
||||||
const certName = certTypeTitleMap[certType];
|
|
||||||
const { tests = [] } = challenge;
|
|
||||||
let result = 'incomplete-requirements';
|
|
||||||
let status = false;
|
|
||||||
|
|
||||||
const { isHonest, completedChallenges } = user;
|
|
||||||
const isProjectsCompleted = canClaim(tests, completedChallenges);
|
|
||||||
|
|
||||||
if (isHonest && isProjectsCompleted) {
|
|
||||||
status = true;
|
|
||||||
result = 'requirements-met';
|
|
||||||
} else if (isProjectsCompleted) {
|
|
||||||
result = 'projects-completed';
|
|
||||||
} else if (isHonest) {
|
|
||||||
result = 'is-honest';
|
|
||||||
}
|
|
||||||
return Observable.just({
|
|
||||||
type: 'success',
|
|
||||||
message: { status, result },
|
|
||||||
variables: { name: certName }
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.subscribe(message => {
|
|
||||||
return res.status(200).json({
|
|
||||||
response: message,
|
|
||||||
isCertMap: getUserIsCertMap(user),
|
|
||||||
// send back the completed challenges
|
|
||||||
// NOTE: we could just send back the latest challenge, but this
|
|
||||||
// ensures the challenges are synced.
|
|
||||||
completedChallenges: user.completedChallenges
|
|
||||||
});
|
|
||||||
}, next);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -63,7 +63,8 @@
|
|||||||
"submit-and-go": "Submit and go to next challenge",
|
"submit-and-go": "Submit and go to next challenge",
|
||||||
"go-to-next": "Go to next challenge",
|
"go-to-next": "Go to next challenge",
|
||||||
"ask-later": "Ask me later",
|
"ask-later": "Ask me later",
|
||||||
"start-coding": "Start coding!"
|
"start-coding": "Start coding!",
|
||||||
|
"go-to-settings": "Go to settings to claim your certification"
|
||||||
},
|
},
|
||||||
"landing": {
|
"landing": {
|
||||||
"big-heading-1": "Learn to code — for free.",
|
"big-heading-1": "Learn to code — for free.",
|
||||||
|
@ -189,19 +189,7 @@ export const completedChallengesSelector = state =>
|
|||||||
export const completionCountSelector = state => state[MainApp].completionCount;
|
export const completionCountSelector = state => state[MainApp].completionCount;
|
||||||
export const currentChallengeIdSelector = state =>
|
export const currentChallengeIdSelector = state =>
|
||||||
state[MainApp].currentChallengeId;
|
state[MainApp].currentChallengeId;
|
||||||
export const stepsToClaimSelector = state => {
|
|
||||||
const user = userSelector(state);
|
|
||||||
const currentCerts = certificatesByNameSelector(user.username)(
|
|
||||||
state
|
|
||||||
).currentCerts;
|
|
||||||
return {
|
|
||||||
currentCerts: currentCerts,
|
|
||||||
isHonest: user?.isHonest,
|
|
||||||
isShowName: user?.profileUI?.showName,
|
|
||||||
isShowCerts: user?.profileUI?.showCerts,
|
|
||||||
isShowProfile: !user?.profileUI?.isLocked
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export const emailSelector = state => userSelector(state).email;
|
export const emailSelector = state => userSelector(state).email;
|
||||||
export const isAVariantSelector = state => {
|
export const isAVariantSelector = state => {
|
||||||
const email = emailSelector(state);
|
const email = emailSelector(state);
|
||||||
@ -271,6 +259,9 @@ export const userByNameSelector = username => state => {
|
|||||||
return user[username] ?? initialState.user;
|
return user[username] ?? initialState.user;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const currentCertsSelector = state =>
|
||||||
|
certificatesByNameSelector(state[MainApp]?.appUsername)(state)?.currentCerts;
|
||||||
|
|
||||||
export const certificatesByNameSelector = username => state => {
|
export const certificatesByNameSelector = username => state => {
|
||||||
const {
|
const {
|
||||||
isRespWebDesignCert,
|
isRespWebDesignCert,
|
||||||
|
@ -17,11 +17,9 @@ import {
|
|||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
submitComplete,
|
submitComplete,
|
||||||
updateComplete,
|
updateComplete,
|
||||||
updateFailed,
|
updateFailed
|
||||||
usernameSelector
|
|
||||||
} from '../../../redux';
|
} from '../../../redux';
|
||||||
|
|
||||||
import { getVerifyCanClaimCert } from '../../../utils/ajax';
|
|
||||||
import postUpdate$ from '../utils/postUpdate$';
|
import postUpdate$ from '../utils/postUpdate$';
|
||||||
import { actionTypes } from './action-types';
|
import { actionTypes } from './action-types';
|
||||||
import {
|
import {
|
||||||
@ -151,8 +149,7 @@ export default function completionEpic(action$, state$) {
|
|||||||
switchMap(({ type }) => {
|
switchMap(({ type }) => {
|
||||||
const state = state$.value;
|
const state = state$.value;
|
||||||
const meta = challengeMetaSelector(state);
|
const meta = challengeMetaSelector(state);
|
||||||
const { nextChallengePath, challengeType, superBlock, certification } =
|
const { nextChallengePath, challengeType, superBlock } = meta;
|
||||||
meta;
|
|
||||||
const closeChallengeModal = of(closeModal('completion'));
|
const closeChallengeModal = of(closeModal('completion'));
|
||||||
|
|
||||||
let submitter = () => of({ type: 'no-user-signed-in' });
|
let submitter = () => of({ type: 'no-user-signed-in' });
|
||||||
@ -170,13 +167,7 @@ export default function completionEpic(action$, state$) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pathToNavigateTo = async () => {
|
const pathToNavigateTo = async () => {
|
||||||
return await findPathToNavigateTo(
|
return await findPathToNavigateTo(nextChallengePath, superBlock);
|
||||||
certification,
|
|
||||||
nextChallengePath,
|
|
||||||
superBlock,
|
|
||||||
state,
|
|
||||||
challengeType
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return submitter(type, state).pipe(
|
return submitter(type, state).pipe(
|
||||||
@ -188,38 +179,10 @@ export default function completionEpic(action$, state$) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findPathToNavigateTo(
|
async function findPathToNavigateTo(nextChallengePath, superBlock) {
|
||||||
certification,
|
if (nextChallengePath.includes(superBlock)) {
|
||||||
nextChallengePath,
|
return nextChallengePath;
|
||||||
superBlock,
|
|
||||||
state,
|
|
||||||
challengeType
|
|
||||||
) {
|
|
||||||
let canClaimCert = false;
|
|
||||||
const isProjectSubmission = [
|
|
||||||
challengeTypes.frontEndProject,
|
|
||||||
challengeTypes.backEndProject,
|
|
||||||
challengeTypes.pythonProject
|
|
||||||
].includes(challengeType);
|
|
||||||
if (isProjectSubmission) {
|
|
||||||
const username = usernameSelector(state);
|
|
||||||
try {
|
|
||||||
const response = await getVerifyCanClaimCert(username, certification);
|
|
||||||
if (response.status === 200) {
|
|
||||||
canClaimCert = response.data?.response?.message === 'can-claim-cert';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('failed to verify if user can claim certificate', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let pathToNavigateTo;
|
|
||||||
|
|
||||||
if (nextChallengePath.includes(superBlock) && !canClaimCert) {
|
|
||||||
pathToNavigateTo = nextChallengePath;
|
|
||||||
} else if (canClaimCert) {
|
|
||||||
pathToNavigateTo = `/learn/${superBlock}/#claim-cert-block`;
|
|
||||||
} else {
|
} else {
|
||||||
pathToNavigateTo = `/learn/${superBlock}/#${superBlock}-projects`;
|
return `/learn/${superBlock}/#${superBlock}-projects`;
|
||||||
}
|
}
|
||||||
return pathToNavigateTo;
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Button } from '@freecodecamp/react-bootstrap';
|
import { Button } from '@freecodecamp/react-bootstrap';
|
||||||
import { navigate } from 'gatsby-link';
|
import { navigate } from 'gatsby-link';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, MouseEvent } from 'react';
|
||||||
import { TFunction, withTranslation } from 'react-i18next';
|
import { TFunction, withTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
@ -13,14 +13,12 @@ import { createFlashMessage } from '../../../components/Flash/redux';
|
|||||||
import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
|
import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
|
||||||
import {
|
import {
|
||||||
userFetchStateSelector,
|
userFetchStateSelector,
|
||||||
stepsToClaimSelector,
|
isSignedInSelector,
|
||||||
isSignedInSelector
|
currentCertsSelector
|
||||||
} from '../../../redux';
|
} from '../../../redux';
|
||||||
import { User, Steps } from '../../../redux/prop-types';
|
import { User, Steps } from '../../../redux/prop-types';
|
||||||
import { verifyCert } from '../../../redux/settings';
|
import { verifyCert } from '../../../redux/settings';
|
||||||
import { certMap } from '../../../resources/cert-and-project-map';
|
import { certMap } from '../../../resources/cert-and-project-map';
|
||||||
import { getVerifyCanClaimCert } from '../../../utils/ajax';
|
|
||||||
import CertificationCard from './certification-card';
|
|
||||||
|
|
||||||
interface CertChallengeProps {
|
interface CertChallengeProps {
|
||||||
// TODO: create enum/reuse SuperBlocks enum somehow
|
// TODO: create enum/reuse SuperBlocks enum somehow
|
||||||
@ -33,7 +31,7 @@ interface CertChallengeProps {
|
|||||||
error: null | string;
|
error: null | string;
|
||||||
};
|
};
|
||||||
isSignedIn: boolean;
|
isSignedIn: boolean;
|
||||||
steps: Steps;
|
currentCerts: Steps['currentCerts'];
|
||||||
superBlock: SuperBlocks;
|
superBlock: SuperBlocks;
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
title: typeof certMap[number]['title'];
|
title: typeof certMap[number]['title'];
|
||||||
@ -48,11 +46,16 @@ const honestyInfoMessage = {
|
|||||||
|
|
||||||
const mapStateToProps = (state: unknown) => {
|
const mapStateToProps = (state: unknown) => {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
stepsToClaimSelector,
|
currentCertsSelector,
|
||||||
userFetchStateSelector,
|
userFetchStateSelector,
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
(steps, fetchState: CertChallengeProps['fetchState'], isSignedIn) => ({
|
(
|
||||||
steps,
|
currentCerts,
|
||||||
|
fetchState: CertChallengeProps['fetchState'],
|
||||||
|
isSignedIn
|
||||||
|
) => ({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
currentCerts,
|
||||||
fetchState,
|
fetchState,
|
||||||
isSignedIn
|
isSignedIn
|
||||||
})
|
})
|
||||||
@ -65,9 +68,8 @@ const mapDispatchToProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CertChallenge = ({
|
const CertChallenge = ({
|
||||||
certification,
|
|
||||||
createFlashMessage,
|
createFlashMessage,
|
||||||
steps = {},
|
currentCerts,
|
||||||
superBlock,
|
superBlock,
|
||||||
t,
|
t,
|
||||||
verifyCert,
|
verifyCert,
|
||||||
@ -76,45 +78,8 @@ const CertChallenge = ({
|
|||||||
isSignedIn,
|
isSignedIn,
|
||||||
user: { isHonest, username }
|
user: { isHonest, username }
|
||||||
}: CertChallengeProps): JSX.Element => {
|
}: CertChallengeProps): JSX.Element => {
|
||||||
const [canClaimCert, setCanClaimCert] = useState(false);
|
|
||||||
const [certVerificationMessage, setCertVerificationMessage] = useState('');
|
|
||||||
const [isCertified, setIsCertified] = useState(false);
|
const [isCertified, setIsCertified] = useState(false);
|
||||||
const [userLoaded, setUserLoaded] = useState(false);
|
const [userLoaded, setUserLoaded] = useState(false);
|
||||||
const [verificationComplete, setVerificationComplete] = useState(false);
|
|
||||||
const [stepState, setStepState] = useState({
|
|
||||||
numberOfSteps: 0,
|
|
||||||
completedCount: 0
|
|
||||||
});
|
|
||||||
const [hasCompletedRequiredSteps, setHasCompletedRequiredSteps] =
|
|
||||||
useState(false);
|
|
||||||
const [isProjectsCompleted, setIsProjectsCompleted] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (username) {
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
const data = await getVerifyCanClaimCert(username, certification);
|
|
||||||
if (data?.message) {
|
|
||||||
setCanClaimCert(false);
|
|
||||||
createFlashMessage(data.message);
|
|
||||||
} else {
|
|
||||||
const { status, result } = data?.response?.message;
|
|
||||||
setCanClaimCert(status);
|
|
||||||
setCertVerificationMessage(result);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
createFlashMessage({
|
|
||||||
type: 'danger',
|
|
||||||
message: FlashMessages.ReallyWeird
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setVerificationComplete(true);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [username]);
|
|
||||||
|
|
||||||
// @ts-expect-error Typescript is confused
|
// @ts-expect-error Typescript is confused
|
||||||
const certSlug = certMap.find(x => x.title === title).certSlug;
|
const certSlug = certMap.find(x => x.title === title).certSlug;
|
||||||
@ -133,40 +98,19 @@ const CertChallenge = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsCertified(
|
setIsCertified(
|
||||||
steps?.currentCerts?.find(
|
currentCerts?.find(
|
||||||
(cert: { certSlug: string }) =>
|
(cert: { certSlug: string }) =>
|
||||||
certSlugTypeMapTyped[cert.certSlug] ===
|
certSlugTypeMapTyped[cert.certSlug] ===
|
||||||
superBlockCertTypeMapTyped[superBlock]
|
superBlockCertTypeMapTyped[superBlock]
|
||||||
)?.show ?? false
|
)?.show ?? false
|
||||||
);
|
);
|
||||||
|
|
||||||
const projectsCompleted =
|
|
||||||
canClaimCert || certVerificationMessage === 'projects-completed';
|
|
||||||
const projectsCompletedNumber = projectsCompleted ? 1 : 0;
|
|
||||||
const completedCount =
|
|
||||||
Object.values(steps).filter(
|
|
||||||
stepVal => typeof stepVal === 'boolean' && stepVal
|
|
||||||
).length + projectsCompletedNumber;
|
|
||||||
const numberOfSteps = Object.keys(steps).length;
|
|
||||||
setHasCompletedRequiredSteps(completedCount === numberOfSteps);
|
|
||||||
setStepState({ numberOfSteps, completedCount });
|
|
||||||
setIsProjectsCompleted(projectsCompleted);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [steps, canClaimCert, certVerificationMessage]);
|
}, [currentCerts]);
|
||||||
|
|
||||||
const certLocation = `/certification/${username}/${certSlug}`;
|
const certLocation = `/certification/${username}/${certSlug}`;
|
||||||
const i18nSuperBlock = t(`intro:${superBlock}.title`);
|
|
||||||
const i18nCertText = t(`intro:misc-text.certification`, {
|
|
||||||
cert: i18nSuperBlock
|
|
||||||
});
|
|
||||||
|
|
||||||
const showCertificationCard =
|
|
||||||
userLoaded &&
|
|
||||||
isSignedIn &&
|
|
||||||
(!isCertified || (!hasCompletedRequiredSteps && verificationComplete));
|
|
||||||
|
|
||||||
const createClickHandler =
|
const createClickHandler =
|
||||||
(certSlug: string | undefined) => (e: { preventDefault: () => void }) => {
|
(certSlug: string | undefined) => (e: MouseEvent<HTMLAnchorElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (isCertified) {
|
if (isCertified) {
|
||||||
return navigate(certLocation);
|
return navigate(certLocation);
|
||||||
@ -177,33 +121,19 @@ const CertChallenge = ({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className='block'>
|
<div className='block'>
|
||||||
{showCertificationCard && (
|
|
||||||
<CertificationCard
|
|
||||||
i18nCertText={i18nCertText}
|
|
||||||
isProjectsCompleted={isProjectsCompleted}
|
|
||||||
steps={steps}
|
|
||||||
stepState={stepState}
|
|
||||||
superBlock={superBlock}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<>
|
|
||||||
{isSignedIn && (
|
{isSignedIn && (
|
||||||
<Button
|
<Button
|
||||||
block={true}
|
block={true}
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
className='cert-btn'
|
className='cert-btn'
|
||||||
disabled={
|
href={isCertified ? certLocation : `/settings#certification-settings`}
|
||||||
!canClaimCert || (isCertified && !hasCompletedRequiredSteps)
|
onClick={() => (isCertified ? createClickHandler(certSlug) : false)}
|
||||||
}
|
|
||||||
href={certLocation}
|
|
||||||
onClick={createClickHandler(certSlug)}
|
|
||||||
>
|
>
|
||||||
{isCertified && userLoaded
|
{isCertified && userLoaded
|
||||||
? t('buttons.show-cert')
|
? t('buttons.show-cert')
|
||||||
: t('buttons.claim-cert')}
|
: t('buttons.go-to-settings')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import ScrollableAnchor from 'react-scrollable-anchor';
|
|
||||||
import { SuperBlocks } from '../../../../../config/certification-settings';
|
|
||||||
import Caret from '../../../assets/icons/caret';
|
|
||||||
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
|
|
||||||
import GreenPass from '../../../assets/icons/green-pass';
|
|
||||||
import { Steps } from '../../../redux/prop-types';
|
|
||||||
import ClaimCertSteps from './claim-cert-steps';
|
|
||||||
|
|
||||||
interface CertificationCardProps {
|
|
||||||
i18nCertText: string;
|
|
||||||
isProjectsCompleted: boolean;
|
|
||||||
stepState: {
|
|
||||||
numberOfSteps: number;
|
|
||||||
completedCount: number;
|
|
||||||
};
|
|
||||||
steps: Steps;
|
|
||||||
superBlock: SuperBlocks;
|
|
||||||
}
|
|
||||||
const mapIconStyle = { height: '15px', marginRight: '10px', width: '15px' };
|
|
||||||
|
|
||||||
const CertificationCard = ({
|
|
||||||
isProjectsCompleted,
|
|
||||||
superBlock,
|
|
||||||
i18nCertText,
|
|
||||||
stepState: { completedCount, numberOfSteps },
|
|
||||||
steps
|
|
||||||
}: CertificationCardProps): JSX.Element => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isExpanded, setIsExpanded] = useState(true);
|
|
||||||
|
|
||||||
const handleBlockClick = () => {
|
|
||||||
setIsExpanded(!isExpanded);
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
expand: expandText,
|
|
||||||
collapse: collapseText
|
|
||||||
}: {
|
|
||||||
expand: string;
|
|
||||||
collapse: string;
|
|
||||||
} = t('intro:misc-text');
|
|
||||||
return (
|
|
||||||
<ScrollableAnchor id='claim-cert-block'>
|
|
||||||
<div className={`block ${isExpanded ? 'open' : ''}`}>
|
|
||||||
<div className='block-title-wrapper'>
|
|
||||||
<a className='block-link' href='#claim-cert-block'>
|
|
||||||
<h3 className='big-block-title'>
|
|
||||||
{t('certification-card.title')}
|
|
||||||
<span className='block-link-icon'>#</span>
|
|
||||||
</h3>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className='block-description'>
|
|
||||||
{t('certification-card.intro', { i18nCertText })}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
aria-expanded={isExpanded}
|
|
||||||
className='map-title'
|
|
||||||
onClick={handleBlockClick}
|
|
||||||
>
|
|
||||||
<Caret />
|
|
||||||
<h4 className='course-title'>
|
|
||||||
{`${isExpanded ? collapseText : expandText}`}
|
|
||||||
</h4>
|
|
||||||
<div className='map-title-completed course-title'>
|
|
||||||
{completedCount === numberOfSteps ? (
|
|
||||||
<GreenPass style={mapIconStyle} />
|
|
||||||
) : (
|
|
||||||
<GreenNotCompleted style={mapIconStyle} />
|
|
||||||
)}
|
|
||||||
<span className='map-completed-count'>{`${completedCount}/${numberOfSteps}`}</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{isExpanded && (
|
|
||||||
<ClaimCertSteps
|
|
||||||
i18nCertText={i18nCertText}
|
|
||||||
isProjectsCompleted={isProjectsCompleted}
|
|
||||||
steps={steps}
|
|
||||||
superBlock={superBlock}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollableAnchor>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
CertificationCard.displayName = 'CertStatus';
|
|
||||||
|
|
||||||
export default CertificationCard;
|
|
@ -1,88 +0,0 @@
|
|||||||
import { Link } from 'gatsby';
|
|
||||||
import React from 'react';
|
|
||||||
import { withTranslation, useTranslation } from 'react-i18next';
|
|
||||||
import { SuperBlocks } from '../../../../../config/certification-settings';
|
|
||||||
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
|
|
||||||
import GreenPass from '../../../assets/icons/green-pass';
|
|
||||||
import { Steps } from '../../../redux/prop-types';
|
|
||||||
|
|
||||||
const mapIconStyle = { height: '15px', marginRight: '10px', width: '15px' };
|
|
||||||
|
|
||||||
interface ClaimCertStepsProps {
|
|
||||||
i18nCertText: string;
|
|
||||||
isProjectsCompleted: boolean;
|
|
||||||
steps: Steps;
|
|
||||||
superBlock: SuperBlocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ClaimCertSteps = ({
|
|
||||||
isProjectsCompleted,
|
|
||||||
i18nCertText,
|
|
||||||
steps,
|
|
||||||
superBlock
|
|
||||||
}: ClaimCertStepsProps): JSX.Element => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const renderCheckMark = (isCompleted: boolean) => {
|
|
||||||
return isCompleted ? (
|
|
||||||
<GreenPass style={mapIconStyle} />
|
|
||||||
) : (
|
|
||||||
<GreenNotCompleted style={mapIconStyle} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const settingsLink = '/settings#privacy-settings';
|
|
||||||
const honestyPolicyAnchor = '/settings#honesty-policy';
|
|
||||||
const {
|
|
||||||
isHonest = false,
|
|
||||||
isShowName = false,
|
|
||||||
isShowCerts = false,
|
|
||||||
isShowProfile = false
|
|
||||||
} = steps;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className='map-challenges-ul' data-cy='claim-cert-steps'>
|
|
||||||
<li className='map-challenge-title map-challenge-wrap'>
|
|
||||||
<Link to={honestyPolicyAnchor}>
|
|
||||||
<span className='badge map-badge'>{renderCheckMark(isHonest)}</span>
|
|
||||||
{t('certification-card.accept-honesty')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li className='map-challenge-title map-challenge-wrap'>
|
|
||||||
<a href={`#${superBlock}-projects`}>
|
|
||||||
<span className='badge map-badge'>
|
|
||||||
{renderCheckMark(isProjectsCompleted)}
|
|
||||||
</span>
|
|
||||||
{t('certification-card.complete-project', {
|
|
||||||
i18nCertText
|
|
||||||
})}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className='map-challenge-title map-challenge-wrap'>
|
|
||||||
<Link to={settingsLink}>
|
|
||||||
<span className='badge map-badge'>
|
|
||||||
{renderCheckMark(isShowProfile)}
|
|
||||||
</span>
|
|
||||||
{t('certification-card.set-profile-public')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li className='map-challenge-title map-challenge-wrap'>
|
|
||||||
<Link to={settingsLink}>
|
|
||||||
<span className='badge map-badge'>
|
|
||||||
{renderCheckMark(isShowCerts)}
|
|
||||||
</span>
|
|
||||||
{t('certification-card.set-certs-public')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li className='map-challenge-title map-challenge-wrap'>
|
|
||||||
<Link to={settingsLink}>
|
|
||||||
<span className='badge map-badge'>{renderCheckMark(isShowName)}</span>
|
|
||||||
{t('certification-card.set-name')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ClaimCertSteps.displayName = 'ClaimCertSteps';
|
|
||||||
|
|
||||||
export default withTranslation()(ClaimCertSteps);
|
|
@ -1,10 +1,8 @@
|
|||||||
import cookies from 'browser-cookies';
|
import cookies from 'browser-cookies';
|
||||||
import envData from '../../../config/env.json';
|
import envData from '../../../config/env.json';
|
||||||
import { FlashMessageArg } from '../components/Flash/redux';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ChallengeFile,
|
ChallengeFile,
|
||||||
ClaimedCertifications,
|
|
||||||
CompletedChallenge,
|
CompletedChallenge,
|
||||||
User
|
User
|
||||||
} from '../redux/prop-types';
|
} from '../redux/prop-types';
|
||||||
@ -25,6 +23,8 @@ function getCSRFToken() {
|
|||||||
return token ?? '';
|
return token ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Might want to handle flash messages as close to the request as possible
|
||||||
|
// to make use of the Response object (message, status, etc)
|
||||||
async function get<T>(path: string): Promise<T> {
|
async function get<T>(path: string): Promise<T> {
|
||||||
return fetch(`${base}${path}`, defaultOptions).then<T>(res => res.json());
|
return fetch(`${base}${path}`, defaultOptions).then<T>(res => res.json());
|
||||||
}
|
}
|
||||||
@ -160,31 +160,6 @@ export function getUsernameExists(username: string): Promise<boolean> {
|
|||||||
return get(`/api/users/exists?username=${username}`);
|
return get(`/api/users/exists?username=${username}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetVerifyCanClaimCert {
|
|
||||||
response: {
|
|
||||||
type: string;
|
|
||||||
message: {
|
|
||||||
status: boolean;
|
|
||||||
result: string;
|
|
||||||
};
|
|
||||||
variables: {
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
isCertMap: ClaimedCertifications;
|
|
||||||
completedChallenges: CompletedChallenge[];
|
|
||||||
message?: FlashMessageArg;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getVerifyCanClaimCert(
|
|
||||||
username: string,
|
|
||||||
certification: string
|
|
||||||
): Promise<GetVerifyCanClaimCert> {
|
|
||||||
return get(
|
|
||||||
`/certificate/verify-can-claim-cert?username=${username}&superBlock=${certification}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** POST **/
|
/** POST **/
|
||||||
|
|
||||||
interface Donation {
|
interface Donation {
|
||||||
|
@ -78,7 +78,8 @@ describe('project submission', () => {
|
|||||||
// We need to wait for everything to finish loading and hydrating, so we
|
// We need to wait for everything to finish loading and hydrating, so we
|
||||||
// use this text as a proxy for that.
|
// use this text as a proxy for that.
|
||||||
const textInNextPage = projectTitles.slice(1);
|
const textInNextPage = projectTitles.slice(1);
|
||||||
textInNextPage.push('Claim Your Certification');
|
// The following text exists on the donation modal
|
||||||
|
textInNextPage.push('Nicely done');
|
||||||
|
|
||||||
projectsInOrder.forEach(
|
projectsInOrder.forEach(
|
||||||
({ block, superBlock, dashedName, solutions }, i) => {
|
({ block, superBlock, dashedName, solutions }, i) => {
|
||||||
@ -118,8 +119,10 @@ describe('project submission', () => {
|
|||||||
// Claim and view solutions on certification page
|
// Claim and view solutions on certification page
|
||||||
|
|
||||||
cy.toggleAll();
|
cy.toggleAll();
|
||||||
cy.visit('/learn/javascript-algorithms-and-data-structures');
|
cy.visit('/settings');
|
||||||
cy.contains('Claim Certification').click();
|
cy.get(
|
||||||
|
`a[href="/certification/developmentuser/${projectsInOrder[0]?.superBlock}"]`
|
||||||
|
).click();
|
||||||
cy.contains('Show Certification').click();
|
cy.contains('Show Certification').click();
|
||||||
|
|
||||||
projectTitles.forEach(title => {
|
projectTitles.forEach(title => {
|
||||||
|
@ -34,19 +34,9 @@ describe('Responsive Web Design Superblock', () => {
|
|||||||
cy.visit('/learn/responsive-web-design');
|
cy.visit('/learn/responsive-web-design');
|
||||||
});
|
});
|
||||||
describe('Before submitting projects', () => {
|
describe('Before submitting projects', () => {
|
||||||
it('should have a card with href "claim-cert-block"', () => {
|
it('should navigate to "/settings#certification-settings" when clicking the "Go to settings to claim your certification" anchor', () => {
|
||||||
cy.get('a[href="#claim-cert-block"]').scrollIntoView();
|
cy.contains('Go to settings to claim your certification').click();
|
||||||
cy.get('a[href="#claim-cert-block"]').should('be.visible');
|
cy.url().should('include', '/settings#certification-settings');
|
||||||
});
|
|
||||||
|
|
||||||
it('should have an anchor element with the text "Claim Certification", and class "disabled"', () => {
|
|
||||||
cy.get('a.disabled').should('be.visible');
|
|
||||||
cy.get('a.disabled').should('have.text', 'Claim Certification');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have an unordered list with class "map-challenges-ul" containing 5 items', () => {
|
|
||||||
cy.get('[data-cy=claim-cert-steps]').should('be.visible');
|
|
||||||
cy.get('[data-cy=claim-cert-steps]').children().should('have.length', 5);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('After submitting all 5 projects', () => {
|
describe('After submitting all 5 projects', () => {
|
||||||
@ -64,7 +54,7 @@ describe('Responsive Web Design Superblock', () => {
|
|||||||
cy.contains("I've completed this challenge")
|
cy.contains("I've completed this challenge")
|
||||||
.should('not.be.disabled')
|
.should('not.be.disabled')
|
||||||
.click();
|
.click();
|
||||||
cy.intercept('http://localhost:3000/project-completed').as(
|
cy.intercept(`${Cypress.env('API_LOCATION')}/project-completed`).as(
|
||||||
'challengeCompleted'
|
'challengeCompleted'
|
||||||
);
|
);
|
||||||
cy.contains('Submit and go to next challenge').click();
|
cy.contains('Submit and go to next challenge').click();
|
||||||
@ -74,26 +64,21 @@ describe('Responsive Web Design Superblock', () => {
|
|||||||
cy.location().should(loc => {
|
cy.location().should(loc => {
|
||||||
expect(loc.pathname).to.not.eq(url);
|
expect(loc.pathname).to.not.eq(url);
|
||||||
});
|
});
|
||||||
|
cy.visit('/settings');
|
||||||
|
cy.get(
|
||||||
|
`[href="/certification/developmentuser/${projects.superBlock}"]`
|
||||||
|
).click();
|
||||||
|
cy.visit(`/learn/${projects.superBlock}/`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should be possible to claim and view certifications from the superBlock page', () => {
|
it('should be possible to view certifications from the superBlock page', () => {
|
||||||
cy.location().should(loc => {
|
cy.location().should(loc => {
|
||||||
expect(loc.pathname).to.eq(`/learn/${projects.superBlock}/`);
|
expect(loc.pathname).to.eq(`/learn/${projects.superBlock}/`);
|
||||||
});
|
});
|
||||||
cy.get('.donation-modal').should('be.visible');
|
|
||||||
cy.contains('Ask me later').click();
|
|
||||||
cy.get('.donation-modal').should('not.exist');
|
|
||||||
// directed to claim-cert-block section
|
|
||||||
cy.url().should('include', '#claim-cert-block');
|
|
||||||
// make sure that the window has not snapped to the top (a weird bug that
|
|
||||||
// we never figured out and so could randomly reappear)
|
|
||||||
cy.window().its('scrollY').should('not.equal', 0);
|
|
||||||
cy.contains('Claim Your Certification');
|
|
||||||
cy.contains('Claim Certification').should('not.be.disabled').click();
|
|
||||||
cy.contains('Show Certification').click();
|
cy.contains('Show Certification').click();
|
||||||
cy.location().should(loc => {
|
cy.location().should(loc => {
|
||||||
expect(loc.pathname).to.eq(
|
expect(loc.pathname).to.eq(
|
||||||
'/certification/developmentuser/responsive-web-design'
|
`/certification/developmentuser/${projects.superBlock}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -28,5 +28,6 @@ module.exports = (on, config) => {
|
|||||||
|
|
||||||
// Allows us to test the new curriculum before it's released:
|
// Allows us to test the new curriculum before it's released:
|
||||||
config.env.SHOW_UPCOMING_CHANGES = process.env.SHOW_UPCOMING_CHANGES;
|
config.env.SHOW_UPCOMING_CHANGES = process.env.SHOW_UPCOMING_CHANGES;
|
||||||
|
config.env.API_LOCATION = process.env.API_LOCATION;
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => {});
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => {});
|
||||||
|
|
||||||
Cypress.Commands.add('login', () => {
|
Cypress.Commands.add('login', () => {
|
||||||
cy.visit('http://localhost:3000/signin');
|
cy.visit(`${Cypress.env('API_LOCATION')}/signin`);
|
||||||
cy.contains('Welcome back');
|
cy.contains('Welcome back');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user