Files
freeCodeCamp/client/src/templates/Challenges/redux/completion-epic.js
Tom b061a760c1 feat: add framework for rwd cert projects (#44505)
* feat: add rwd cert projects

feat: save projects with flag

revert: not needed things

revert: empty line

revert: empty line

fix: it

fix: remove log

* fix: snapshot tests

* fix: show bread crumbs by default

* revert: snapshot fix

* Update curriculum/challenges/_meta/responsive-web-design-projects/meta.json

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>

* fix: manuallyApproved -> isManuallyApproved

* fix: add review suggestions

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
2022-01-06 13:26:54 +00:00

226 lines
6.2 KiB
JavaScript

import { navigate } from 'gatsby';
import { omit } from 'lodash-es';
import { ofType } from 'redux-observable';
import { of, empty } from 'rxjs';
import {
switchMap,
retry,
catchError,
concat,
filter,
finalize
} from 'rxjs/operators';
import { challengeTypes, submitTypes } from '../../../../utils/challenge-types';
import {
userSelector,
isSignedInSelector,
submitComplete,
updateComplete,
updateFailed,
usernameSelector
} from '../../../redux';
import { getVerifyCanClaimCert } from '../../../utils/ajax';
import postUpdate$ from '../utils/postUpdate$';
import { actionTypes } from './action-types';
import {
projectFormValuesSelector,
challengeMetaSelector,
challengeTestsSelector,
closeModal,
challengeFilesSelector,
updateSolutionFormValues
} from './';
function postChallenge(update, username) {
const saveChallenge = postUpdate$(update).pipe(
retry(3),
switchMap(({ points }) => {
const payloadWithClientProperties = {
...omit(update.payload, ['files']),
challengeFiles: update.payload.files ?? null
};
return of(
submitComplete({
username,
points,
...payloadWithClientProperties
}),
updateComplete()
);
}),
catchError(() => of(updateFailed(update)))
);
return saveChallenge;
}
function submitModern(type, state) {
const challengeType = state.challenge.challengeMeta.challengeType;
const tests = challengeTestsSelector(state);
if (
challengeType === 11 ||
(tests.length > 0 && tests.every(test => test.pass && !test.err))
) {
if (type === actionTypes.checkChallenge) {
return of({ type: 'this was a check challenge' });
}
if (type === actionTypes.submitChallenge) {
const { id, block } = challengeMetaSelector(state);
const challengeFiles = challengeFilesSelector(state);
const { username } = userSelector(state);
const challengeInfo = {
id,
challengeType
};
// Only send files to server, if it is a JS project or multiFile cert project
if (
block === 'javascript-algorithms-and-data-structures-projects' ||
challengeType === challengeTypes.multiFileCertProject
) {
challengeInfo.files = challengeFiles.reduce(
(acc, { fileKey, ...curr }) => [...acc, { ...curr, key: fileKey }],
[]
);
}
const update = {
endpoint: '/modern-challenge-completed',
payload: challengeInfo
};
return postChallenge(update, username);
}
}
return empty();
}
function submitProject(type, state) {
if (type === actionTypes.checkChallenge) {
return empty();
}
const { solution, githubLink } = projectFormValuesSelector(state);
const { id, challengeType } = challengeMetaSelector(state);
const { username } = userSelector(state);
const challengeInfo = { id, challengeType, solution };
if (challengeType === challengeTypes.backEndProject) {
challengeInfo.githubLink = githubLink;
}
const update = {
endpoint: '/project-completed',
payload: challengeInfo
};
return postChallenge(update, username).pipe(
concat(of(updateSolutionFormValues({})))
);
}
function submitBackendChallenge(type, state) {
const tests = challengeTestsSelector(state);
if (tests.length > 0 && tests.every(test => test.pass && !test.err)) {
if (type === actionTypes.submitChallenge) {
const { id } = challengeMetaSelector(state);
const { username } = userSelector(state);
const {
solution: { value: solution }
} = projectFormValuesSelector(state);
const challengeInfo = { id, solution };
const update = {
endpoint: '/backend-challenge-completed',
payload: challengeInfo
};
return postChallenge(update, username);
}
}
return empty();
}
const submitters = {
tests: submitModern,
backend: submitBackendChallenge,
'project.frontEnd': submitProject,
'project.backEnd': submitProject
};
export default function completionEpic(action$, state$) {
return action$.pipe(
ofType(actionTypes.submitChallenge),
switchMap(({ type }) => {
const state = state$.value;
const meta = challengeMetaSelector(state);
const { nextChallengePath, challengeType, superBlock, certification } =
meta;
const closeChallengeModal = of(closeModal('completion'));
let submitter = () => of({ type: 'no-user-signed-in' });
if (
!(challengeType in submitTypes) ||
!(submitTypes[challengeType] in submitters)
) {
throw new Error(
'Unable to find the correct submit function for challengeType ' +
challengeType
);
}
if (isSignedInSelector(state)) {
submitter = submitters[submitTypes[challengeType]];
}
const pathToNavigateTo = async () => {
return await findPathToNavigateTo(
certification,
nextChallengePath,
superBlock,
state,
challengeType
);
};
return submitter(type, state).pipe(
concat(closeChallengeModal),
filter(Boolean),
finalize(async () => navigate(await pathToNavigateTo()))
);
})
);
}
async function findPathToNavigateTo(
certification,
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 {
pathToNavigateTo = `/learn/${superBlock}/#${superBlock}-projects`;
}
return pathToNavigateTo;
}