diff --git a/api-server/src/common/models/user.json b/api-server/src/common/models/user.json index bc7187ab73..9630a2138e 100644 --- a/api-server/src/common/models/user.json +++ b/api-server/src/common/models/user.json @@ -208,6 +208,7 @@ "solution": "string", "githubLink": "string", "challengeType": "number", + "isManuallyApproved": "boolean", "files": { "type": [ { diff --git a/api-server/src/common/utils/index.js b/api-server/src/common/utils/index.js index c467d7c48a..3f86552081 100644 --- a/api-server/src/common/utils/index.js +++ b/api-server/src/common/utils/index.js @@ -17,5 +17,6 @@ export const fixCompletedChallengeItem = obj => 'solution', 'githubLink', 'challengeType', - 'files' + 'files', + 'isManuallyApproved' ]); diff --git a/api-server/src/server/boot/challenge.js b/api-server/src/server/boot/challenge.js index ccbfad9378..bab717e633 100644 --- a/api-server/src/server/boot/challenge.js +++ b/api-server/src/server/boot/challenge.js @@ -69,7 +69,7 @@ export default async function bootChallenge(app, done) { done(); } -const jsProjects = [ +const jsCertProjectIds = [ 'aaa48de84e1ecc7c742e1124', 'a7f4d8f2483413a6ce226cac', '56533eb9ac21ba0edf2244e2', @@ -77,6 +77,10 @@ const jsProjects = [ 'aa2e6f85cab2ab736c9a9b24' ]; +const multiFileCertProjectIds = getChallenges() + .filter(challenge => challenge.challengeType === 14) + .map(challenge => challenge.id); + export function buildUserUpdate( user, challengeId, @@ -85,7 +89,10 @@ export function buildUserUpdate( ) { const { files, completedDate = Date.now() } = _completedChallenge; let completedChallenge = {}; - if (jsProjects.includes(challengeId)) { + if ( + jsCertProjectIds.includes(challengeId) || + multiFileCertProjectIds.includes(challengeId) + ) { completedChallenge = { ..._completedChallenge, files: files.map(file => @@ -223,14 +230,19 @@ export function modernChallengeCompleted(req, res, next) { .getCompletedChallenges$() .flatMap(() => { const completedDate = Date.now(); - const { id, files } = req.body; + const { id, files, challengeType } = req.body; - const { alreadyCompleted, updateData } = buildUserUpdate(user, id, { + const data = { id, files, completedDate - }); + }; + if (challengeType === 14) { + data.isManuallyApproved = false; + } + + const { alreadyCompleted, updateData } = buildUserUpdate(user, id, data); const points = alreadyCompleted ? user.points : user.points + 1; const updatePromise = new Promise((resolve, reject) => user.updateAttributes(updateData, err => { diff --git a/client/src/templates/Challenges/classic/desktop-layout.tsx b/client/src/templates/Challenges/classic/desktop-layout.tsx index 8a8130169f..d123b023e7 100644 --- a/client/src/templates/Challenges/classic/desktop-layout.tsx +++ b/client/src/templates/Challenges/classic/desktop-layout.tsx @@ -2,6 +2,7 @@ import { first } from 'lodash-es'; import React, { useState, ReactElement } from 'react'; import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex'; import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles'; +import { challengeTypes } from '../../../../utils/challenge-types'; import { ChallengeFile, ChallengeFiles, @@ -14,6 +15,7 @@ type Pane = { flex: number }; interface DesktopLayoutProps { block: string; challengeFiles: ChallengeFiles; + challengeType: number; editor: ReactElement | null; hasEditableBoundaries: boolean; hasNotes: boolean; @@ -68,6 +70,7 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => { const { block, + challengeType, resizeProps, instructions, editor, @@ -83,11 +86,15 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => { const challengeFile = getChallengeFile(); const projectBasedChallenge = hasEditableBoundaries; - const displayPreview = projectBasedChallenge - ? showPreview && hasPreview - : hasPreview; + const isMultiFileCertProject = + challengeType === challengeTypes.multiFileCertProject; + const displayPreview = + projectBasedChallenge || isMultiFileCertProject + ? showPreview && hasPreview + : hasPreview; const displayNotes = projectBasedChallenge ? showNotes && hasNotes : false; - const displayConsole = projectBasedChallenge ? showConsole : true; + const displayConsole = + projectBasedChallenge || isMultiFileCertProject ? showConsole : true; const { codePane, editorPane, @@ -99,7 +106,7 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => { return (
- {projectBasedChallenge && ( + {(projectBasedChallenge || isMultiFileCertProject) && ( { const { challengeType } = this.getChallenge(); return ( challengeType === challengeTypes.html || - challengeType === challengeTypes.modern + challengeType === challengeTypes.modern || + challengeType === challengeTypes.multiFileCertProject ); } renderInstructionsPanel({ showToolPanel }: { showToolPanel: boolean }) { - const { block, description, instructions, superBlock, translationPending } = - this.getChallenge(); + const { + block, + challengeType, + description, + forumTopicId, + instructions, + superBlock, + title, + translationPending + } = this.getChallenge(); - const { forumTopicId, title } = this.getChallenge(); + const showBreadCrumbs = + challengeType !== challengeTypes.multiFileCertProject; return ( { @@ -405,6 +416,7 @@ class ShowClassic extends Component { render() { const { block, + challengeType, fields: { blockName }, forumTopicId, hasEditableBoundaries, @@ -457,6 +469,7 @@ class ShowClassic extends Component { )} - + {showBreadCrumbs && }
{children} diff --git a/client/src/templates/Challenges/redux/completion-epic.js b/client/src/templates/Challenges/redux/completion-epic.js index d044a2ee7f..71207811c7 100644 --- a/client/src/templates/Challenges/redux/completion-epic.js +++ b/client/src/templates/Challenges/redux/completion-epic.js @@ -71,10 +71,15 @@ function submitModern(type, state) { const challengeFiles = challengeFilesSelector(state); const { username } = userSelector(state); const challengeInfo = { - id + id, + challengeType }; - // Only send files to server, if it is a JS project - if (block === 'javascript-algorithms-and-data-structures-projects') { + + // 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 }], [] diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js index 7577a902f0..f21207b22a 100644 --- a/client/src/templates/Challenges/redux/index.js +++ b/client/src/templates/Challenges/redux/index.js @@ -190,7 +190,8 @@ export const challengeDataSelector = state => { }; } else if ( challengeType === challengeTypes.html || - challengeType === challengeTypes.modern + challengeType === challengeTypes.modern || + challengeType === challengeTypes.multiFileCertProject ) { const { required = [], template = '' } = challengeMetaSelector(state); challengeData = { diff --git a/client/src/templates/Challenges/utils/build.js b/client/src/templates/Challenges/utils/build.js index 9f393ee230..6d29684f52 100644 --- a/client/src/templates/Challenges/utils/build.js +++ b/client/src/templates/Challenges/utils/build.js @@ -72,7 +72,8 @@ const buildFunctions = { [challengeTypes.modern]: buildDOMChallenge, [challengeTypes.backend]: buildBackendChallenge, [challengeTypes.backEndProject]: buildBackendChallenge, - [challengeTypes.pythonProject]: buildBackendChallenge + [challengeTypes.pythonProject]: buildBackendChallenge, + [challengeTypes.multiFileCertProject]: buildDOMChallenge }; export function canBuildChallenge(challengeData) { @@ -93,7 +94,8 @@ const testRunners = { [challengeTypes.js]: getJSTestRunner, [challengeTypes.html]: getDOMTestRunner, [challengeTypes.backend]: getDOMTestRunner, - [challengeTypes.pythonProject]: getDOMTestRunner + [challengeTypes.pythonProject]: getDOMTestRunner, + [challengeTypes.multiFileCertProject]: getDOMTestRunner }; export function getTestRunner(buildData, runnerConfig, document) { const { challengeType } = buildData; @@ -146,7 +148,11 @@ export function buildDOMChallenge({ .then(checkFilesErrors) .then(challengeFiles => ({ challengeType: challengeTypes.html, - build: concatHtml({ required: finalRequires, template, challengeFiles }), + build: concatHtml({ + required: finalRequires, + template, + challengeFiles + }), sources: buildSourceMap(challengeFiles), loadEnzyme })); @@ -184,7 +190,10 @@ export function buildBackendChallenge({ url }) { } export function updatePreview(buildData, document, proxyLogger) { - if (buildData.challengeType === challengeTypes.html) { + if ( + buildData.challengeType === challengeTypes.html || + buildData.challengeType === challengeTypes.multiFileCertProject + ) { createMainPreviewFramer(document, proxyLogger)(buildData); } else { throw new Error( @@ -194,7 +203,10 @@ export function updatePreview(buildData, document, proxyLogger) { } export function updateProjectPreview(buildData, document) { - if (buildData.challengeType === challengeTypes.html) { + if ( + buildData.challengeType === challengeTypes.html || + buildData.challengeType === challengeTypes.multiFileCertProject + ) { createProjectPreviewFramer(document)(buildData); } else { throw new Error( @@ -206,7 +218,8 @@ export function updateProjectPreview(buildData, document) { export function challengeHasPreview({ challengeType }) { return ( challengeType === challengeTypes.html || - challengeType === challengeTypes.modern + challengeType === challengeTypes.modern || + challengeType === challengeTypes.multiFileCertProject ); } diff --git a/client/utils/challenge-types.js b/client/utils/challenge-types.js index 0713ea6cff..3f71aae933 100644 --- a/client/utils/challenge-types.js +++ b/client/utils/challenge-types.js @@ -12,6 +12,7 @@ const invalid = 9; const pythonProject = 10; const video = 11; const codeally = 12; +const multiFileCertProject = 14; // individual exports exports.backend = backend; @@ -33,7 +34,8 @@ exports.challengeTypes = { quiz, invalid, video, - codeally + codeally, + multiFileCertProject }; // (Oliver) I don't think we need this for codeally projects, so they're ignored @@ -67,7 +69,8 @@ exports.viewTypes = { [quiz]: 'quiz', [backend]: 'backend', [video]: 'video', - [codeally]: 'codeally' + [codeally]: 'codeally', + [multiFileCertProject]: 'classic' }; // determine the type of submit function to use for the challenge on completion @@ -87,7 +90,8 @@ exports.submitTypes = { [quiz]: 'quiz', [backend]: 'backend', [modern]: 'tests', - [video]: 'tests' + [video]: 'tests', + [multiFileCertProject]: 'tests' }; // determine which help forum questions should be posted to diff --git a/client/utils/gatsby/challenge-page-creator.js b/client/utils/gatsby/challenge-page-creator.js index d611665c77..463449980a 100644 --- a/client/utils/gatsby/challenge-page-creator.js +++ b/client/utils/gatsby/challenge-page-creator.js @@ -2,7 +2,7 @@ const path = require('path'); const { createPoly } = require('../../../utils/polyvinyl'); const { dasherize } = require('../../../utils/slugs'); const { sortChallengeFiles } = require('../../../utils/sort-challengefiles'); -const { viewTypes } = require('../challenge-types'); +const { challengeTypes, viewTypes } = require('../challenge-types'); const backend = path.resolve( __dirname, @@ -102,7 +102,8 @@ exports.createChallengePages = function (createPage) { }; function getProjectPreviewConfig(challenge, allChallengeEdges) { - const { block, challengeOrder, usesMultifileEditor } = challenge; + const { block, challengeOrder, challengeType, usesMultifileEditor } = + challenge; const challengesInBlock = allChallengeEdges .filter(({ node: { challenge } }) => challenge.block === block) @@ -122,7 +123,10 @@ function getProjectPreviewConfig(challenge, allChallengeEdges) { ); return { - showProjectPreview: challengeOrder === 0 && usesMultifileEditor, + showProjectPreview: + challengeOrder === 0 && + usesMultifileEditor && + challengeType !== challengeTypes.multiFileCertProject, challengeData: { challengeType: lastChallenge.challengeType, challengeFiles: projectPreviewChallengeFiles, diff --git a/curriculum/schema/challengeSchema.js b/curriculum/schema/challengeSchema.js index 43096efbf2..c01191d69c 100644 --- a/curriculum/schema/challengeSchema.js +++ b/curriculum/schema/challengeSchema.js @@ -28,7 +28,7 @@ const schema = Joi.object() challengeOrder: Joi.number(), removeComments: Joi.bool(), certification: Joi.string().regex(slugRE), - challengeType: Joi.number().min(0).max(12).required(), + challengeType: Joi.number().min(0).max(14).required(), checksum: Joi.number(), // __commentCounts is only used to test the comment replacement __commentCounts: Joi.object(),