diff --git a/client/src/redux/propTypes.js b/client/src/redux/propTypes.js index 09b45b2493..6e4e9a74ac 100644 --- a/client/src/redux/propTypes.js +++ b/client/src/redux/propTypes.js @@ -35,6 +35,7 @@ export const ChallengeNode = PropTypes.shape({ forumTopicId: PropTypes.number, guideUrl: PropTypes.string, head: PropTypes.arrayOf(PropTypes.string), + helpCategory: PropTypes.string, instructions: PropTypes.string, isComingSoon: PropTypes.bool, isLocked: PropTypes.bool, diff --git a/client/src/templates/Challenges/classic/Show.js b/client/src/templates/Challenges/classic/Show.js index e9a0206533..eb7981491c 100644 --- a/client/src/templates/Challenges/classic/Show.js +++ b/client/src/templates/Challenges/classic/Show.js @@ -153,7 +153,8 @@ class ShowClassic extends Component { challengeNode: { files, fields: { tests }, - challengeType + challengeType, + helpCategory } }, pageContext: { challengeMeta } @@ -161,7 +162,12 @@ class ShowClassic extends Component { initConsole(''); createFiles(files); initTests(tests); - updateChallengeMeta({ ...challengeMeta, title, challengeType }); + updateChallengeMeta({ + ...challengeMeta, + title, + challengeType, + helpCategory + }); challengeMounted(challengeMeta.id); } @@ -341,6 +347,7 @@ export const query = graphql` description instructions challengeType + helpCategory videoUrl forumTopicId fields { diff --git a/client/src/templates/Challenges/projects/backend/Show.js b/client/src/templates/Challenges/projects/backend/Show.js index 45b6b34456..63f826af5a 100644 --- a/client/src/templates/Challenges/projects/backend/Show.js +++ b/client/src/templates/Challenges/projects/backend/Show.js @@ -123,14 +123,20 @@ export class BackEnd extends Component { challengeNode: { fields: { tests }, title, - challengeType + challengeType, + helpCategory } }, pageContext: { challengeMeta } } = this.props; initConsole(); initTests(tests); - updateChallengeMeta({ ...challengeMeta, title, challengeType }); + updateChallengeMeta({ + ...challengeMeta, + title, + challengeType, + helpCategory + }); challengeMounted(challengeMeta.id); } @@ -224,6 +230,7 @@ export const query = graphql` description instructions challengeType + helpCategory fields { blockName slug diff --git a/client/src/templates/Challenges/projects/frontend/Show.js b/client/src/templates/Challenges/projects/frontend/Show.js index 28ac6c08e1..ca1700fb28 100644 --- a/client/src/templates/Challenges/projects/frontend/Show.js +++ b/client/src/templates/Challenges/projects/frontend/Show.js @@ -55,12 +55,17 @@ export class Project extends Component { const { challengeMounted, data: { - challengeNode: { title, challengeType } + challengeNode: { title, challengeType, helpCategory } }, pageContext: { challengeMeta }, updateChallengeMeta } = this.props; - updateChallengeMeta({ ...challengeMeta, title, challengeType }); + updateChallengeMeta({ + ...challengeMeta, + title, + challengeType, + helpCategory + }); challengeMounted(challengeMeta.id); this._container.focus(); } @@ -74,7 +79,7 @@ export class Project extends Component { const { challengeMounted, data: { - challengeNode: { title: currentTitle, challengeType } + challengeNode: { title: currentTitle, challengeType, helpCategory } }, pageContext: { challengeMeta }, updateChallengeMeta @@ -83,7 +88,8 @@ export class Project extends Component { updateChallengeMeta({ ...challengeMeta, title: currentTitle, - challengeType + challengeType, + helpCategory }); challengeMounted(challengeMeta.id); } @@ -161,6 +167,7 @@ export const query = graphql` title description challengeType + helpCategory fields { blockName slug diff --git a/client/src/templates/Challenges/redux/create-question-epic.js b/client/src/templates/Challenges/redux/create-question-epic.js index 112ef31b2d..9eedf5cab9 100644 --- a/client/src/templates/Challenges/redux/create-question-epic.js +++ b/client/src/templates/Challenges/redux/create-question-epic.js @@ -7,7 +7,6 @@ import { challengeMetaSelector } from '../redux'; import { tap, mapTo } from 'rxjs/operators'; -import { helpCategory } from '../../../../utils/challengeTypes'; import { forumLocation } from '../../../../../config/env.json'; function filesToMarkdown(files = {}) { @@ -29,7 +28,9 @@ function createQuestionEpic(action$, state$, { window }) { tap(() => { const state = state$.value; const files = challengeFilesSelector(state); - const { block, title: challengeTitle } = challengeMetaSelector(state); + const { title: challengeTitle, helpCategory } = challengeMetaSelector( + state + ); const { navigator: { userAgent }, location: { href } @@ -69,13 +70,13 @@ function createQuestionEpic(action$, state$, { window }) { \`\`\` Replace these two sentences with your copied code. - Please leave the \`\`\` line above and the \`\`\` line below, + Please leave the \`\`\` line above and the \`\`\` line below, because they allow your code to properly format in the post. \`\`\`\n${endingText}` ); - const category = window.encodeURIComponent(helpCategory[block] || 'Help'); + const category = window.encodeURIComponent(helpCategory || 'Help'); const studentCode = window.encodeURIComponent(textMessage); const altStudentCode = window.encodeURIComponent(altTextMessage); diff --git a/client/src/templates/Challenges/video/Show.js b/client/src/templates/Challenges/video/Show.js index a6a11ba000..66b551306e 100644 --- a/client/src/templates/Challenges/video/Show.js +++ b/client/src/templates/Challenges/video/Show.js @@ -85,12 +85,17 @@ export class Project extends Component { const { challengeMounted, data: { - challengeNode: { title, challengeType } + challengeNode: { title, challengeType, helpCategory } }, pageContext: { challengeMeta }, updateChallengeMeta } = this.props; - updateChallengeMeta({ ...challengeMeta, title, challengeType }); + updateChallengeMeta({ + ...challengeMeta, + title, + challengeType, + helpCategory + }); challengeMounted(challengeMeta.id); this._container.focus(); } @@ -104,7 +109,7 @@ export class Project extends Component { const { challengeMounted, data: { - challengeNode: { title: currentTitle, challengeType } + challengeNode: { title: currentTitle, challengeType, helpCategory } }, pageContext: { challengeMeta }, updateChallengeMeta @@ -113,7 +118,8 @@ export class Project extends Component { updateChallengeMeta({ ...challengeMeta, title: currentTitle, - challengeType + challengeType, + helpCategory }); challengeMounted(challengeMeta.id); } @@ -305,6 +311,7 @@ export const query = graphql` title description challengeType + helpCategory fields { blockName slug diff --git a/client/utils/challengeTypes.js b/client/utils/challengeTypes.js index e67a55d657..45c92dd524 100644 --- a/client/utils/challengeTypes.js +++ b/client/utils/challengeTypes.js @@ -76,7 +76,7 @@ exports.submitTypes = { }; // determine which help forum questions should be posted to -exports.helpCategory = { +exports.helpCategoryMap = { 'basic-html-and-html5': 'HTML-CSS', 'basic-css': 'HTML-CSS', 'applied-visual-design': 'HTML-CSS', @@ -84,7 +84,7 @@ exports.helpCategory = { 'responsive-web-design-principles': 'HTML-CSS', 'css-flexbox': 'HTML-CSS', 'css-grid': 'HTML-CSS', - 'responsive-web-design-projects': 'Certification Projects', + 'responsive-web-design-projects': 'HTML-CSS', 'basic-javascript': 'JavaScript', es6: 'JavaScript', 'regular-expressions': 'JavaScript', @@ -94,38 +94,37 @@ exports.helpCategory = { 'object-oriented-programming': 'JavaScript', 'functional-programming': 'JavaScript', 'intermediate-algorithm-scripting': 'JavaScript', - 'javascript-algorithms-and-data-structures-projects': - 'Certification Projects', + 'javascript-algorithms-and-data-structures-projects': 'JavaScript', bootstrap: 'HTML-CSS', jquery: 'JavaScript', sass: 'HTML-CSS', react: 'JavaScript', redux: 'JavaScript', 'react-and-redux': 'JavaScript', - 'front-end-libraries-projects': 'Certification Projects', + 'front-end-libraries-projects': 'JavaScript', 'data-visualization-with-d3': 'JavaScript', 'json-apis-and-ajax': 'JavaScript', - 'data-visualization-projects': 'Certification Projects', + 'data-visualization-projects': 'JavaScript', 'managing-packages-with-npm': 'JavaScript', 'basic-node-and-express': 'JavaScript', 'mongodb-and-mongoose': 'JavaScript', - 'apis-and-microservices-projects': 'Certification Projects', + 'apis-and-microservices-projects': 'JavaScript', 'information-security-with-helmetjs': 'JavaScript', 'quality-assurance-and-testing-with-chai': 'JavaScript', 'advanced-node-and-express': 'JavaScript', - 'quality-assurance-projects': 'Certification Projects', - 'information-security-projects': 'Certification Projects', + 'quality-assurance-projects': 'JavaScript', + 'information-security-projects': 'JavaScript', algorithms: 'JavaScript', 'data-structures': 'JavaScript', - 'take-home-projects': 'Certification Projects', + 'take-home-projects': 'JavaScript', 'rosetta-code': 'JavaScript', 'project-euler': 'JavaScript', 'scientific-computing-with-python': 'Python', - 'scientific-computing-with-python-projects': 'Certification Projects', + 'scientific-computing-with-python-projects': 'Python', 'data-analysis-with-python': 'Python', - 'data-analysis-with-python-projects': 'Certification Projects', + 'data-analysis-with-python-projects': 'Python', 'machine-learning-with-python': 'Python', - 'machine-learning-with-python-projects': 'Certification Projects', + 'machine-learning-with-python-projects': 'Python', 'python-for-everybody': 'Python', tensorflow: 'Python', 'how-neural-networks-work': 'Python', diff --git a/curriculum/challenges/english/09-information-security/information-security-projects/port-scanner.md b/curriculum/challenges/english/09-information-security/information-security-projects/port-scanner.md index 04b114f995..946847dd1f 100644 --- a/curriculum/challenges/english/09-information-security/information-security-projects/port-scanner.md +++ b/curriculum/challenges/english/09-information-security/information-security-projects/port-scanner.md @@ -2,6 +2,7 @@ id: 5e46f979ac417301a38fb932 title: Port Scanner challengeType: 10 +helpCategory: Python --- ## Description diff --git a/curriculum/challenges/english/09-information-security/information-security-projects/sha-1-password-cracker.md b/curriculum/challenges/english/09-information-security/information-security-projects/sha-1-password-cracker.md index 39d4b4f3af..334254d58d 100644 --- a/curriculum/challenges/english/09-information-security/information-security-projects/sha-1-password-cracker.md +++ b/curriculum/challenges/english/09-information-security/information-security-projects/sha-1-password-cracker.md @@ -2,6 +2,7 @@ id: 5e46f983ac417301a38fb933 title: SHA-1 Password Cracker challengeType: 10 +helpCategory: Python --- ## Description diff --git a/curriculum/getChallenges.js b/curriculum/getChallenges.js index 9e506c4e1a..6a6cadba0e 100644 --- a/curriculum/getChallenges.js +++ b/curriculum/getChallenges.js @@ -16,6 +16,7 @@ const { dasherize, nameify } = require('../utils/slugs'); const { createPoly } = require('../utils/polyvinyl'); const { blockNameify } = require('../utils/block-nameify'); const { supportedLangs } = require('./utils'); +const { helpCategoryMap } = require('../client/utils/challengeTypes'); const access = util.promisify(fs.access); @@ -275,6 +276,8 @@ ${getFullPath('english')} challenge.required = required.concat(challenge.required || []); challenge.template = template; challenge.time = time; + challenge.helpCategory = + challenge.helpCategory || helpCategoryMap[dasherize(blockName)]; return prepareChallenge(challenge); }; diff --git a/curriculum/schema/challengeSchema.js b/curriculum/schema/challengeSchema.js index 787f119923..d0798ecf04 100644 --- a/curriculum/schema/challengeSchema.js +++ b/curriculum/schema/challengeSchema.js @@ -17,89 +17,92 @@ const fileJoi = Joi.object().keys({ history: [Joi.array().items(Joi.string().allow('')), Joi.string().allow('')] }); -const schema = Joi.object().keys({ - block: Joi.string(), - blockId: Joi.objectId(), - challengeOrder: Joi.number(), - challengeType: Joi.number() - .min(0) - .max(11) - .required(), - checksum: Joi.number(), - dashedName: Joi.string(), - description: Joi.when('challengeType', { - is: Joi.only([challengeTypes.step, challengeTypes.video]), - then: Joi.string().allow(''), - otherwise: Joi.string().required() - }), - fileName: Joi.string(), - files: Joi.object().keys({ - indexcss: fileJoi, - indexhtml: fileJoi, - indexjs: fileJoi, - indexjsx: fileJoi - }), - guideUrl: Joi.string().uri({ scheme: 'https' }), - videoUrl: Joi.string().allow(''), - forumTopicId: Joi.number(), - helpRoom: Joi.string(), - id: Joi.objectId().required(), - instructions: Joi.string().allow(''), - isComingSoon: Joi.bool(), - isLocked: Joi.bool(), - isPrivate: Joi.bool(), - name: Joi.string(), - order: Joi.number(), - // video challenges only: - videoId: Joi.when('challengeType', { - is: challengeTypes.video, - then: Joi.string().required() - }), - question: Joi.object().keys({ - text: Joi.string().required(), - answers: Joi.array() - .items(Joi.string()) +const schema = Joi.object() + .keys({ + block: Joi.string(), + blockId: Joi.objectId(), + challengeOrder: Joi.number(), + challengeType: Joi.number() + .min(0) + .max(11) .required(), - solution: Joi.number().required() - }), - required: Joi.array().items( - Joi.object().keys({ - link: Joi.string(), - raw: Joi.bool(), - src: Joi.string(), - crossDomain: Joi.bool() - }) - ), - solutions: Joi.array().items( - Joi.object().keys({ + checksum: Joi.number(), + dashedName: Joi.string(), + description: Joi.when('challengeType', { + is: Joi.only([challengeTypes.step, challengeTypes.video]), + then: Joi.string().allow(''), + otherwise: Joi.string().required() + }), + fileName: Joi.string(), + files: Joi.object().keys({ indexcss: fileJoi, indexhtml: fileJoi, indexjs: fileJoi, - indexjsx: fileJoi, - indexpy: fileJoi - }) - ), - superBlock: Joi.string(), - superOrder: Joi.number(), - suborder: Joi.number(), - tests: Joi.array().items( - // public challenges - Joi.object().keys({ - text: Joi.string().required(), - testString: Joi.string() - .allow('') - .required() + indexjsx: fileJoi }), - // our tests used in certification verification - Joi.object().keys({ - id: Joi.string().required(), - title: Joi.string().required() - }) - ), - template: Joi.string().allow(''), - time: Joi.string().allow(''), - title: Joi.string().required() -}); + guideUrl: Joi.string().uri({ scheme: 'https' }), + helpCategory: Joi.only(['JavaScript', 'HTML-CSS', 'Python']), + videoUrl: Joi.string().allow(''), + forumTopicId: Joi.number(), + helpRoom: Joi.string(), + id: Joi.objectId().required(), + instructions: Joi.string().allow(''), + isComingSoon: Joi.bool(), + isLocked: Joi.bool(), + isPrivate: Joi.bool(), + name: Joi.string(), + order: Joi.number(), + // video challenges only: + videoId: Joi.when('challengeType', { + is: challengeTypes.video, + then: Joi.string().required() + }), + question: Joi.object().keys({ + text: Joi.string().required(), + answers: Joi.array() + .items(Joi.string()) + .required(), + solution: Joi.number().required() + }), + required: Joi.array().items( + Joi.object().keys({ + link: Joi.string(), + raw: Joi.bool(), + src: Joi.string(), + crossDomain: Joi.bool() + }) + ), + solutions: Joi.array().items( + Joi.object().keys({ + indexcss: fileJoi, + indexhtml: fileJoi, + indexjs: fileJoi, + indexjsx: fileJoi, + indexpy: fileJoi + }) + ), + superBlock: Joi.string(), + superOrder: Joi.number(), + suborder: Joi.number(), + tests: Joi.array().items( + // public challenges + Joi.object().keys({ + text: Joi.string().required(), + testString: Joi.string() + .allow('') + .required() + }), + // our tests used in certification verification + Joi.object().keys({ + id: Joi.string().required(), + title: Joi.string().required() + }) + ), + template: Joi.string().allow(''), + time: Joi.string().allow(''), + title: Joi.string().required() + }) + .xor('helpCategory', 'isPrivate'); exports.challengeSchemaValidator = () => { return challenge => Joi.validate(challenge, schema); diff --git a/curriculum/test/test-challenges.js b/curriculum/test/test-challenges.js index 52d9d4fc98..bc67c8fd31 100644 --- a/curriculum/test/test-challenges.js +++ b/curriculum/test/test-challenges.js @@ -42,10 +42,7 @@ const { const MongoIds = require('./utils/mongoIds'); const ChallengeTitles = require('./utils/challengeTitles'); const { challengeSchemaValidator } = require('../schema/challengeSchema'); -const { - challengeTypes, - helpCategory -} = require('../../client/utils/challengeTypes'); +const { challengeTypes } = require('../../client/utils/challengeTypes'); const { dasherize } = require('../../utils/slugs'); const { toSortedArray } = require('../../utils/sort-files'); @@ -249,15 +246,6 @@ async function getChallenges(lang) { return sortChallenges(challenges); } -function validateBlock(challenge) { - const dashedBlock = dasherize(challenge.block); - if (!helpCategory.hasOwnProperty(dashedBlock)) { - return `'${dashedBlock}' block not found as a helpCategory in client/utils/challengeTypes.js file for the '${challenge.title}' challenge`; - } else { - return null; - } -} - function populateTestsForLang({ lang, challenges, meta }) { const mongoIds = new MongoIds(); const challengeTitles = new ChallengeTitles(); @@ -285,14 +273,10 @@ function populateTestsForLang({ lang, challenges, meta }) { it('Common checks', function() { const result = validateChallenge(challenge); - const invalidBlock = validateBlock(challenge); if (result.error) { throw new AssertionError(result.error); } - if (challenge.challengeType !== 7 && invalidBlock) { - throw new Error(invalidBlock); - } const { id, title, block, dashedName } = challenge; const dashedBlock = dasherize(block); const pathAndTitle = `${dashedBlock}/${dashedName}`;