diff --git a/tools/challenge-helper-scripts/create-language-block.ts b/tools/challenge-helper-scripts/create-language-block.ts index cbb4a1c19e8..58e780a6013 100644 --- a/tools/challenge-helper-scripts/create-language-block.ts +++ b/tools/challenge-helper-scripts/create-language-block.ts @@ -19,7 +19,12 @@ import { import { superBlockToFilename } from '../../curriculum/build-curriculum'; import { getBaseMeta } from './helpers/get-base-meta'; import { createIntroMD } from './helpers/create-intro'; -import { createDialogueFile, createQuizFile, validateBlockName } from './utils'; +import { + createDialogueFile, + createQuizFile, + getAllBlocks, + validateBlockName +} from './utils'; import { updateSimpleSuperblockStructure, updateChapterModuleSuperblockStructure @@ -222,118 +227,122 @@ function withTrace( }); } -void prompt([ - { - name: 'superBlock', - message: 'Which certification does this belong to?', - default: SuperBlocks.A2English, - type: 'list', - choices: Object.values(languageSuperBlocks) - }, - { - name: 'block', - message: 'What is the dashed name (in kebab-case) for this block?', - validate: validateBlockName, - filter: (block: string) => { - return block.toLowerCase().trim(); - } - }, - { - name: 'title', - default: ({ block }: { block: string }) => block - }, - { - name: 'helpCategory', - message: 'Choose a help category', - default: 'English', - type: 'list', - choices: helpCategories - }, - { - name: 'blockType', - message: 'Choose a block type', - default: BlockTypes.learn, - type: 'list', - choices: Object.values(BlockTypes), - when: (answers: CreateBlockArgs) => - chapterBasedSuperBlocks.includes(answers.superBlock) - }, - { - name: 'blockLayout', - message: 'Choose a block layout', - default: BlockLayouts.DialogueGrid, - type: 'list', - choices: Object.values(BlockLayouts), - when: (answers: CreateBlockArgs) => - chapterBasedSuperBlocks.includes(answers.superBlock) && - answers.blockType !== BlockTypes.quiz - }, - { - name: 'questionCount', - message: 'Choose a question count', - default: 20, - type: 'list', - choices: [10, 20], - when: (answers: CreateBlockArgs) => answers.blockType === BlockTypes.quiz - }, - { - name: 'chapter', - message: 'What chapter should this language block go in?', - type: 'list', - choices: (answers: CreateBlockArgs) => { - const superblockFilename = ( - superBlockToFilename as Record - )[answers.superBlock]; - const structure = getSuperblockStructure(superblockFilename) as { - chapters: { - dashedName: string; - modules: { dashedName: string; blocks: string[] }[]; - }[]; - }; - return structure.chapters.map(chapter => chapter.dashedName); - }, - when: (answers: CreateBlockArgs) => - chapterBasedSuperBlocks.includes(answers.superBlock) - }, - { - name: 'module', - message: 'What module should this language block go in?', - type: 'list', - choices: (answers: CreateBlockArgs) => { - const superblockFilename = ( - superBlockToFilename as Record - )[answers.superBlock]; - const structure = getSuperblockStructure(superblockFilename) as { - chapters: { - dashedName: string; - modules: { dashedName: string; blocks: string[] }[]; - }[]; - }; - return ( - structure.chapters - .find(chapter => chapter.dashedName === answers.chapter) - ?.modules.map(module => module.dashedName) ?? [] - ); - }, - when: (answers: CreateBlockArgs) => - chapterBasedSuperBlocks.includes(answers.superBlock) - }, - { - name: 'position', - message: 'At which position does this appear in the module?', - default: 1, - validate: (position: string) => { - return parseInt(position, 10) > 0 - ? true - : 'Position must be an number greater than zero.'; - }, - when: (answers: CreateBlockArgs) => - chapterBasedSuperBlocks.includes(answers.superBlock), - filter: (position: string) => { - return parseInt(position, 10); - } - } -]) +void getAllBlocks() + .then(existingBlocks => + prompt([ + { + name: 'superBlock', + message: 'Which certification does this belong to?', + default: SuperBlocks.A2English, + type: 'list', + choices: Object.values(languageSuperBlocks) + }, + { + name: 'block', + message: 'What is the dashed name (in kebab-case) for this block?', + validate: (block: string) => validateBlockName(block, existingBlocks), + filter: (block: string) => { + return block.toLowerCase().trim(); + } + }, + { + name: 'title', + default: ({ block }: { block: string }) => block + }, + { + name: 'helpCategory', + message: 'Choose a help category', + default: 'English', + type: 'list', + choices: helpCategories + }, + { + name: 'blockType', + message: 'Choose a block type', + default: BlockTypes.learn, + type: 'list', + choices: Object.values(BlockTypes), + when: (answers: CreateBlockArgs) => + chapterBasedSuperBlocks.includes(answers.superBlock) + }, + { + name: 'blockLayout', + message: 'Choose a block layout', + default: BlockLayouts.DialogueGrid, + type: 'list', + choices: Object.values(BlockLayouts), + when: (answers: CreateBlockArgs) => + chapterBasedSuperBlocks.includes(answers.superBlock) && + answers.blockType !== BlockTypes.quiz + }, + { + name: 'questionCount', + message: 'Choose a question count', + default: 20, + type: 'list', + choices: [10, 20], + when: (answers: CreateBlockArgs) => + answers.blockType === BlockTypes.quiz + }, + { + name: 'chapter', + message: 'What chapter should this language block go in?', + type: 'list', + choices: (answers: CreateBlockArgs) => { + const superblockFilename = ( + superBlockToFilename as Record + )[answers.superBlock]; + const structure = getSuperblockStructure(superblockFilename) as { + chapters: { + dashedName: string; + modules: { dashedName: string; blocks: string[] }[]; + }[]; + }; + return structure.chapters.map(chapter => chapter.dashedName); + }, + when: (answers: CreateBlockArgs) => + chapterBasedSuperBlocks.includes(answers.superBlock) + }, + { + name: 'module', + message: 'What module should this language block go in?', + type: 'list', + choices: (answers: CreateBlockArgs) => { + const superblockFilename = ( + superBlockToFilename as Record + )[answers.superBlock]; + const structure = getSuperblockStructure(superblockFilename) as { + chapters: { + dashedName: string; + modules: { dashedName: string; blocks: string[] }[]; + }[]; + }; + return ( + structure.chapters + .find(chapter => chapter.dashedName === answers.chapter) + ?.modules.map(module => module.dashedName) ?? [] + ); + }, + when: (answers: CreateBlockArgs) => + chapterBasedSuperBlocks.includes(answers.superBlock) + }, + { + name: 'position', + message: 'At which position does this appear in the module?', + default: 1, + validate: (position: string) => { + return parseInt(position, 10) > 0 + ? true + : 'Position must be an number greater than zero.'; + }, + when: (answers: CreateBlockArgs) => + chapterBasedSuperBlocks.includes(answers.superBlock), + filter: (position: string) => { + return parseInt(position, 10); + } + } + ]) + ) .then( async ({ superBlock, diff --git a/tools/challenge-helper-scripts/create-project.ts b/tools/challenge-helper-scripts/create-project.ts index daacce39061..d431d8aca51 100644 --- a/tools/challenge-helper-scripts/create-project.ts +++ b/tools/challenge-helper-scripts/create-project.ts @@ -15,7 +15,12 @@ import { writeBlockStructure } from '../../curriculum/file-handler'; import { superBlockToFilename } from '../../curriculum/build-curriculum'; -import { createQuizFile, createStepFile, validateBlockName } from './utils'; +import { + createQuizFile, + createStepFile, + validateBlockName, + getAllBlocks +} from './utils'; import { getBaseMeta } from './helpers/get-base-meta'; import { createIntroMD } from './helpers/create-intro'; import { @@ -293,145 +298,151 @@ async function getModules(superBlock: string, chapterName: string) { return modifiedChapter?.modules; } -void prompt([ - { - name: 'superBlock', - message: 'Which certification does this belong to?', - default: SuperBlocks.FullStackDeveloper, - type: 'list', - choices: Object.values(SuperBlocks) - }, - { - name: 'block', - message: 'What is the dashed name (in kebab-case) for this project?', - validate: validateBlockName, - filter: (block: string) => { - return block.toLowerCase().trim(); - } - }, - { - name: 'title', - default: ({ block }: { block: string }) => block - }, - { - name: 'helpCategory', - message: 'Choose a help category', - default: 'HTML-CSS', - type: 'list', - choices: helpCategories - }, - { - name: 'blockType', - message: 'Choose a block type', - default: BlockTypes.lab, - type: 'list', - choices: Object.values(BlockTypes), - when: (answers: CreateProjectArgs) => - chapterBasedSuperBlocks.includes(answers.superBlock) - }, - { - name: 'blockLayout', - message: 'Choose a block layout', +void getAllBlocks() + .then(existingBlocks => + prompt([ + { + name: 'superBlock', + message: 'Which certification does this belong to?', + default: SuperBlocks.FullStackDeveloper, + type: 'list', + choices: Object.values(SuperBlocks) + }, + { + name: 'block', + message: 'What is the dashed name (in kebab-case) for this project?', + validate: (block: string) => validateBlockName(block, existingBlocks), + filter: (block: string) => { + return block.toLowerCase().trim(); + } + }, + { + name: 'title', + default: ({ block }: { block: string }) => block + }, + { + name: 'helpCategory', + message: 'Choose a help category', + default: 'HTML-CSS', + type: 'list', + choices: helpCategories + }, + { + name: 'blockType', + message: 'Choose a block type', + default: BlockTypes.lab, + type: 'list', + choices: Object.values(BlockTypes), + when: (answers: CreateProjectArgs) => + chapterBasedSuperBlocks.includes(answers.superBlock) + }, + { + name: 'blockLayout', + message: 'Choose a block layout', - default: (answers: { blockType: BlockTypes }) => - answers.blockType == BlockTypes.quiz - ? BlockLayouts.Link - : BlockLayouts.ChallengeList, - type: 'list', - choices: Object.values(BlockLayouts), - when: (answers: CreateProjectArgs) => - chapterBasedSuperBlocks.includes(answers.superBlock) - }, - { - name: 'questionCount', - message: 'Choose a question count', - default: 20, - type: 'list', - choices: [10, 20], - when: (answers: CreateProjectArgs) => answers.blockType === BlockTypes.quiz - }, - { - name: 'chapter', - message: 'What chapter should this project go in?', - default: 'html', - type: 'list', - choices: async (answers: CreateProjectArgs) => { - const chapters = await getChapters(answers.superBlock); - return chapters.map(x => x.dashedName); - }, - when: (answers: CreateProjectArgs) => - chapterBasedSuperBlocks.includes(answers.superBlock) - }, - { - name: 'module', - message: 'What module should this project go in?', - default: 'html', - type: 'list', - choices: async (answers: CreateProjectArgs) => { - const modules = await getModules(answers.superBlock, answers.chapter!); - return modules!.map(x => x.dashedName); - }, - when: (answers: CreateProjectArgs) => - chapterBasedSuperBlocks.includes(answers.superBlock) - }, - { - name: 'position', - message: 'At which position does this appear in the module?', - default: 1, - validate: (position: string) => { - return parseInt(position, 10) > 0 - ? true - : 'Position must be an number greater than zero.'; - }, - when: (answers: CreateProjectArgs) => - chapterBasedSuperBlocks.includes(answers.superBlock), - filter: (position: string) => { - return parseInt(position, 10); - } - }, - { - name: 'order', - message: 'Which position does this appear in the certificate?', - default: 42, - validate: (order: string) => { - return parseInt(order, 10) > 0 - ? true - : 'Order must be an number greater than zero.'; - }, - when: (answers: CreateProjectArgs) => - !chapterBasedSuperBlocks.includes(answers.superBlock), - filter: (order: string) => { - return parseInt(order, 10); - } - } -]) - .then( - async ({ - superBlock, - block, - title, - helpCategory, - blockType, - blockLayout, - questionCount, - chapter, - module, - position, - order - }: CreateProjectArgs) => - await createProject({ + default: (answers: { blockType: BlockTypes }) => + answers.blockType == BlockTypes.quiz + ? BlockLayouts.Link + : BlockLayouts.ChallengeList, + type: 'list', + choices: Object.values(BlockLayouts), + when: (answers: CreateProjectArgs) => + chapterBasedSuperBlocks.includes(answers.superBlock) + }, + { + name: 'questionCount', + message: 'Choose a question count', + default: 20, + type: 'list', + choices: [10, 20], + when: (answers: CreateProjectArgs) => + answers.blockType === BlockTypes.quiz + }, + { + name: 'chapter', + message: 'What chapter should this project go in?', + default: 'html', + type: 'list', + choices: async (answers: CreateProjectArgs) => { + const chapters = await getChapters(answers.superBlock); + return chapters.map(x => x.dashedName); + }, + when: (answers: CreateProjectArgs) => + chapterBasedSuperBlocks.includes(answers.superBlock) + }, + { + name: 'module', + message: 'What module should this project go in?', + default: 'html', + type: 'list', + choices: async (answers: CreateProjectArgs) => { + const modules = await getModules( + answers.superBlock, + answers.chapter! + ); + return modules!.map(x => x.dashedName); + }, + when: (answers: CreateProjectArgs) => + chapterBasedSuperBlocks.includes(answers.superBlock) + }, + { + name: 'position', + message: 'At which position does this appear in the module?', + default: 1, + validate: (position: string) => { + return parseInt(position, 10) > 0 + ? true + : 'Position must be an number greater than zero.'; + }, + when: (answers: CreateProjectArgs) => + chapterBasedSuperBlocks.includes(answers.superBlock), + filter: (position: string) => { + return parseInt(position, 10); + } + }, + { + name: 'order', + message: 'Which position does this appear in the certificate?', + default: 42, + validate: (order: string) => { + return parseInt(order, 10) > 0 + ? true + : 'Order must be an number greater than zero.'; + }, + when: (answers: CreateProjectArgs) => + !chapterBasedSuperBlocks.includes(answers.superBlock), + filter: (order: string) => { + return parseInt(order, 10); + } + } + ]).then( + async ({ superBlock, block, + title, helpCategory, blockType, blockLayout, questionCount, - title, chapter, module, position, order - }) + }: CreateProjectArgs) => + await createProject({ + superBlock, + block, + helpCategory, + blockType, + blockLayout, + questionCount, + title, + chapter, + module, + position, + order + }) + ) ) .then(() => console.log( diff --git a/tools/challenge-helper-scripts/create-quiz.ts b/tools/challenge-helper-scripts/create-quiz.ts index b834bc6f20a..d140b6bb34a 100644 --- a/tools/challenge-helper-scripts/create-quiz.ts +++ b/tools/challenge-helper-scripts/create-quiz.ts @@ -10,7 +10,7 @@ import { writeBlockStructure } from '../../curriculum/file-handler'; import { superBlockToFilename } from '../../curriculum/build-curriculum'; -import { createQuizFile, validateBlockName } from './utils'; +import { createQuizFile, getAllBlocks, validateBlockName } from './utils'; import { getBaseMeta } from './helpers/get-base-meta'; import { createIntroMD } from './helpers/create-intro'; import { updateSimpleSuperblockStructure } from './helpers/create-project'; @@ -145,41 +145,44 @@ function withTrace( }); } -void prompt([ - { - name: 'superBlock', - message: 'Which certification does this belong to?', - default: SuperBlocks.FullStackDeveloper, - type: 'list', - choices: Object.values(SuperBlocks) - }, - { - name: 'block', - message: 'What is the dashed name (in kebab-case) for this quiz?', - validate: validateBlockName, - filter: (block: string) => { - return block.toLowerCase().trim(); - } - }, - { - name: 'title', - default: ({ block }: { block: string }) => block - }, - { - name: 'helpCategory', - message: 'Choose a help category', - default: 'HTML-CSS', - type: 'list', - choices: helpCategories - }, - { - name: 'questionCount', - message: 'Should this quiz have either ten or twenty questions?', - default: 20, - type: 'list', - choices: [20, 10] - } -]) +void getAllBlocks() + .then(existingBlocks => + prompt([ + { + name: 'superBlock', + message: 'Which certification does this belong to?', + default: SuperBlocks.FullStackDeveloper, + type: 'list', + choices: Object.values(SuperBlocks) + }, + { + name: 'block', + message: 'What is the dashed name (in kebab-case) for this quiz?', + validate: (block: string) => validateBlockName(block, existingBlocks), + filter: (block: string) => { + return block.toLowerCase().trim(); + } + }, + { + name: 'title', + default: ({ block }: { block: string }) => block + }, + { + name: 'helpCategory', + message: 'Choose a help category', + default: 'HTML-CSS', + type: 'list', + choices: helpCategories + }, + { + name: 'questionCount', + message: 'Should this quiz have either ten or twenty questions?', + default: 20, + type: 'list', + choices: [20, 10] + } + ]) + ) .then( async ({ superBlock, diff --git a/tools/challenge-helper-scripts/utils.test.ts b/tools/challenge-helper-scripts/utils.test.ts index f4bc5576172..0cbceb73620 100644 --- a/tools/challenge-helper-scripts/utils.test.ts +++ b/tools/challenge-helper-scripts/utils.test.ts @@ -98,24 +98,29 @@ describe('Challenge utils helper scripts', () => { describe('createProject util', () => { it('should allow alphanumerical names with trailing whitespace', () => { expect( - validateBlockName('learn-callbacks-by-creating-a-bookshelf ') + validateBlockName('learn-callbacks-by-creating-a-bookshelf ', []) ).toBe(true); }); it('should allow alphanumerical names with no trailing whitespace', () => { - expect(validateBlockName('learn-callbacks-by-creating-a-bookshelf')).toBe( - true - ); + expect( + validateBlockName('learn-callbacks-by-creating-a-bookshelf', []) + ).toBe(true); }); it('should not allow non-kebab case names', () => { - expect(validateBlockName('learnCallbacksBetter')).toBe( + expect(validateBlockName('learnCallbacksBetter', [])).toBe( 'please use alphanumerical characters and kebab case' ); }); it('should not allow white space names', () => { - expect(validateBlockName(' ')).toBe('please enter a dashed name'); + expect(validateBlockName(' ', [])).toBe('please enter a dashed name'); }); it('should not allow empty names', () => { - expect(validateBlockName('')).toBe('please enter a dashed name'); + expect(validateBlockName('', [])).toBe('please enter a dashed name'); + }); + it('should not allow names that already exist', () => { + expect(validateBlockName('name', ['name'])).toBe( + 'a block with this name already exists' + ); }); }); diff --git a/tools/challenge-helper-scripts/utils.ts b/tools/challenge-helper-scripts/utils.ts index f761ae9e072..e5f418d2c83 100644 --- a/tools/challenge-helper-scripts/utils.ts +++ b/tools/challenge-helper-scripts/utils.ts @@ -2,7 +2,10 @@ import fs from 'fs'; import path from 'path'; import ObjectID from 'bson-objectid'; import matter from 'gray-matter'; +import { uniq } from 'lodash'; + import { challengeTypes } from '../../shared/config/challenge-types'; +import { parseCurriculumStructure } from '../../curriculum/build-curriculum'; import { parseMDSync } from '../challenge-parser/parser'; import { getMetaData, updateMetaData } from './helpers/project-metadata'; import { getProjectPath } from './helpers/get-project-info'; @@ -28,6 +31,19 @@ interface QuizOptions { questionCount: number; } +export async function getAllBlocks() { + const { fullSuperblockList } = (await parseCurriculumStructure()) as { + fullSuperblockList: { + blocks: { dashedName: string }[]; + }[]; + }; + const existingBlocks = fullSuperblockList.flatMap(({ blocks }) => + blocks.map(({ dashedName }) => dashedName) + ); + + return uniq(existingBlocks); +} + const createStepFile = ({ stepNum, challengeType, @@ -259,7 +275,13 @@ const getChallenge = (challengeId: string): Challenge => { return challenge; }; -const validateBlockName = (block: string): boolean | string => { +const validateBlockName = ( + block: string, + existingBlocks: string[] +): true | string => { + if (existingBlocks.includes(block.trim())) { + return 'a block with this name already exists'; + } if (!block.trim().length) { return 'please enter a dashed name'; }