Files
freeCodeCamp/client/src/templates/Challenges/redux/completion-epic.js

218 lines
6.0 KiB
JavaScript
Raw Normal View History

import { navigate } from 'gatsby';
import { omit } from 'lodash-es';
import { ofType } from 'redux-observable';
import { of, empty } from 'rxjs';
2018-05-24 19:45:38 +01:00
import {
switchMap,
retry,
catchError,
concat,
2018-09-12 15:58:08 +03:00
filter,
finalize
2018-05-24 19:45:38 +01:00
} from 'rxjs/operators';
2018-04-06 14:51:52 +01:00
import { challengeTypes, submitTypes } from '../../../../utils/challenge-types';
import {
userSelector,
isSignedInSelector,
2019-01-03 01:03:43 +03:00
submitComplete,
updateComplete,
updateFailed,
usernameSelector
} from '../../../redux';
2018-04-06 14:51:52 +01:00
import { getVerifyCanClaimCert } from '../../../utils/ajax';
import postUpdate$ from '../utils/postUpdate$';
import { actionTypes } from './action-types';
import {
projectFormValuesSelector,
challengeMetaSelector,
challengeTestsSelector,
closeModal,
challengeFilesSelector,
updateSolutionFormValues
} from './';
2018-04-06 14:51:52 +01:00
function postChallenge(update, username) {
const saveChallenge = postUpdate$(update).pipe(
2018-05-24 19:45:38 +01:00
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)))
2018-05-24 19:45:38 +01:00
);
return saveChallenge;
}
2018-04-06 14:51:52 +01:00
2018-05-24 19:45:38 +01:00
function submitModern(type, state) {
const challengeType = state.challenge.challengeMeta.challengeType;
2018-05-24 19:45:38 +01:00
const tests = challengeTestsSelector(state);
if (
challengeType === 11 ||
(tests.length > 0 && tests.every(test => test.pass && !test.err))
) {
if (type === actionTypes.checkChallenge) {
2018-05-24 19:45:38 +01:00
return of({ type: 'this was a check challenge' });
}
2018-04-06 14:51:52 +01:00
if (type === actionTypes.submitChallenge) {
const { id, block } = challengeMetaSelector(state);
const challengeFiles = challengeFilesSelector(state);
2018-05-24 19:45:38 +01:00
const { username } = userSelector(state);
const challengeInfo = {
id
};
// Only send files to server, if it is a JS project
if (block === 'javascript-algorithms-and-data-structures-projects') {
challengeInfo.files = challengeFiles.reduce(
(acc, { fileKey, ...curr }) => [...acc, { ...curr, key: fileKey }],
[]
);
}
const update = {
endpoint: '/modern-challenge-completed',
payload: challengeInfo
};
return postChallenge(update, username);
2018-05-24 19:45:38 +01:00
}
}
return empty();
}
2018-04-06 14:51:52 +01:00
2018-05-24 19:45:38 +01:00
function submitProject(type, state) {
if (type === actionTypes.checkChallenge) {
2018-05-24 19:45:38 +01:00
return empty();
}
2018-04-06 14:51:52 +01:00
2018-09-27 14:19:03 +03:00
const { solution, githubLink } = projectFormValuesSelector(state);
2018-05-24 19:45:38 +01:00
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({})))
2018-05-24 19:45:38 +01:00
);
}
2018-04-06 14:51:52 +01:00
2018-05-24 19:45:38 +01:00
function submitBackendChallenge(type, state) {
const tests = challengeTestsSelector(state);
if (tests.length > 0 && tests.every(test => test.pass && !test.err)) {
if (type === actionTypes.submitChallenge) {
2018-05-24 19:45:38 +01:00
const { id } = challengeMetaSelector(state);
const { username } = userSelector(state);
2019-01-03 01:03:43 +03:00
const {
solution: { value: solution }
} = projectFormValuesSelector(state);
2018-05-24 19:45:38 +01:00
const challengeInfo = { id, solution };
const update = {
endpoint: '/backend-challenge-completed',
payload: challengeInfo
};
return postChallenge(update, username);
2018-05-24 19:45:38 +01:00
}
}
return empty();
}
2018-04-06 14:51:52 +01:00
2018-05-24 19:45:38 +01:00
const submitters = {
tests: submitModern,
backend: submitBackendChallenge,
'project.frontEnd': submitProject,
'project.backEnd': submitProject
};
2018-04-06 14:51:52 +01:00
export default function completionEpic(action$, state$) {
2018-04-06 14:51:52 +01:00
return action$.pipe(
ofType(actionTypes.submitChallenge),
2018-04-06 14:51:52 +01:00
switchMap(({ type }) => {
const state = state$.value;
2018-05-24 19:45:38 +01:00
const meta = challengeMetaSelector(state);
const { nextChallengePath, challengeType, superBlock } = meta;
2018-05-24 19:45:38 +01:00
const closeChallengeModal = of(closeModal('completion'));
let submitter = () => of({ type: 'no-user-signed-in' });
2018-05-24 19:45:38 +01:00
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(
nextChallengePath,
superBlock,
state,
challengeType
);
};
2018-05-24 19:45:38 +01:00
return submitter(type, state).pipe(
concat(closeChallengeModal),
filter(Boolean),
finalize(async () => navigate(await pathToNavigateTo()))
2018-05-24 19:45:38 +01:00
);
2018-04-06 14:51:52 +01:00
})
);
}
async function findPathToNavigateTo(
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, superBlock);
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;
}