mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(external curricula): build external curricula data v2 (#59533)
Co-authored-by: sembauke <semboot699@gmail.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import type { Module } from './modules';
|
||||||
|
|
||||||
// TODO: Dynamically create these from intro.json or full-stack.json
|
// TODO: Dynamically create these from intro.json or full-stack.json
|
||||||
export enum FsdChapters {
|
export enum FsdChapters {
|
||||||
Welcome = 'freecodecamp',
|
Welcome = 'freecodecamp',
|
||||||
@@ -9,3 +11,10 @@ export enum FsdChapters {
|
|||||||
BackendJavascript = 'backend-javascript',
|
BackendJavascript = 'backend-javascript',
|
||||||
Python = 'python'
|
Python = 'python'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Chapter {
|
||||||
|
dashedName: string;
|
||||||
|
comingSoon?: boolean;
|
||||||
|
modules: Module[];
|
||||||
|
chapterType?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export interface Module {
|
||||||
|
dashedName: string;
|
||||||
|
comingSoon?: boolean;
|
||||||
|
blocks: {
|
||||||
|
dashedName: string;
|
||||||
|
}[];
|
||||||
|
moduleType?: string;
|
||||||
|
}
|
||||||
@@ -4,9 +4,14 @@ import path from 'path';
|
|||||||
import { getChallengesForLang } from '../../../curriculum/get-challenges';
|
import { getChallengesForLang } from '../../../curriculum/get-challenges';
|
||||||
import {
|
import {
|
||||||
buildExtCurriculumDataV1,
|
buildExtCurriculumDataV1,
|
||||||
Curriculum,
|
type Curriculum as CurriculumV1,
|
||||||
CurriculumProps
|
type CurriculumProps as CurriculumPropsV1
|
||||||
} from './build-external-curricula-data-v1';
|
} from './build-external-curricula-data-v1';
|
||||||
|
import {
|
||||||
|
buildExtCurriculumDataV2,
|
||||||
|
type Curriculum as CurriculumV2,
|
||||||
|
type CurriculumProps as CurriculumPropsV2
|
||||||
|
} from './build-external-curricula-data-v2';
|
||||||
|
|
||||||
const globalConfigPath = path.resolve(__dirname, '../../../shared/config');
|
const globalConfigPath = path.resolve(__dirname, '../../../shared/config');
|
||||||
|
|
||||||
@@ -14,7 +19,8 @@ const globalConfigPath = path.resolve(__dirname, '../../../shared/config');
|
|||||||
// across all languages.
|
// across all languages.
|
||||||
void getChallengesForLang('english')
|
void getChallengesForLang('english')
|
||||||
.then((result: Record<string, unknown>) => {
|
.then((result: Record<string, unknown>) => {
|
||||||
buildExtCurriculumDataV1(result as Curriculum<CurriculumProps>);
|
buildExtCurriculumDataV1(result as CurriculumV1<CurriculumPropsV1>);
|
||||||
|
buildExtCurriculumDataV2(result as CurriculumV2<CurriculumPropsV2>);
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
.then(JSON.stringify)
|
.then(JSON.stringify)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { SuperBlocks } from '../../../shared/config/curriculum';
|
|||||||
import {
|
import {
|
||||||
superblockSchemaValidator,
|
superblockSchemaValidator,
|
||||||
availableSuperBlocksValidator
|
availableSuperBlocksValidator
|
||||||
} from './external-data-schema';
|
} from './external-data-schema-v1';
|
||||||
import {
|
import {
|
||||||
type Curriculum,
|
type Curriculum,
|
||||||
type CurriculumIntros,
|
type CurriculumIntros,
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import fs, { readFileSync } from 'fs';
|
||||||
|
|
||||||
|
import readdirp from 'readdirp';
|
||||||
|
|
||||||
|
import {
|
||||||
|
chapterBasedSuperBlocks,
|
||||||
|
SuperBlocks
|
||||||
|
} from '../../../shared/config/curriculum';
|
||||||
|
import {
|
||||||
|
superblockSchemaValidator,
|
||||||
|
availableSuperBlocksValidator
|
||||||
|
} from './external-data-schema-v2';
|
||||||
|
import {
|
||||||
|
type CurriculumIntros,
|
||||||
|
type Curriculum,
|
||||||
|
type GeneratedCurriculumProps,
|
||||||
|
type GeneratedBlockBasedCurriculumProps,
|
||||||
|
type GeneratedChapterBasedCurriculumProps,
|
||||||
|
orderedSuperBlockInfo
|
||||||
|
} from './build-external-curricula-data-v2';
|
||||||
|
|
||||||
|
const VERSION = 'v2';
|
||||||
|
const intros = JSON.parse(
|
||||||
|
readFileSync(
|
||||||
|
path.resolve(__dirname, '../../../client/i18n/locales/english/intro.json'),
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
|
) as CurriculumIntros;
|
||||||
|
|
||||||
|
describe('external curriculum data build', () => {
|
||||||
|
const clientStaticPath = path.resolve(__dirname, '../../../client/static');
|
||||||
|
|
||||||
|
const validateSuperBlock = superblockSchemaValidator();
|
||||||
|
|
||||||
|
test("the external curriculum data should be in the client's static directory", () => {
|
||||||
|
expect(
|
||||||
|
fs.existsSync(`${clientStaticPath}/curriculum-data/${VERSION}`)
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
fs.readdirSync(`${clientStaticPath}/curriculum-data/${VERSION}`).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('there should be an endpoint to request submit types from', () => {
|
||||||
|
expect(
|
||||||
|
fs.existsSync(
|
||||||
|
`${clientStaticPath}/curriculum-data/${VERSION}/submit-types.json`
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the available-superblocks file should have the correct structure', async () => {
|
||||||
|
const validateAvailableSuperBlocks = availableSuperBlocksValidator();
|
||||||
|
const availableSuperblocks: unknown = JSON.parse(
|
||||||
|
await fs.promises.readFile(
|
||||||
|
`${clientStaticPath}/curriculum-data/${VERSION}/available-superblocks.json`,
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = validateAvailableSuperBlocks(availableSuperblocks);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw Error(
|
||||||
|
`file: available-superblocks.json
|
||||||
|
${result.error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the super block files generated should have the correct schema', async () => {
|
||||||
|
const fileArray = (
|
||||||
|
await readdirp.promise(`${clientStaticPath}/curriculum-data/${VERSION}`, {
|
||||||
|
directoryFilter: ['!challenges'],
|
||||||
|
fileFilter: entry => {
|
||||||
|
// The directory contains super block files and other curriculum-related files.
|
||||||
|
// We're only interested in super block ones.
|
||||||
|
const superBlocks = Object.values(SuperBlocks);
|
||||||
|
return superBlocks.includes(entry.basename);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).map(file => file.path);
|
||||||
|
|
||||||
|
fileArray.forEach(fileInArray => {
|
||||||
|
const fileContent = fs.readFileSync(
|
||||||
|
`${clientStaticPath}/curriculum-data/${VERSION}/${fileInArray}`,
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = validateSuperBlock(JSON.parse(fileContent));
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw Error(`file: ${fileInArray}
|
||||||
|
${result.error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('block-based super blocks and blocks should have the correct data', async () => {
|
||||||
|
const superBlockFiles = (
|
||||||
|
await readdirp.promise(`${clientStaticPath}/curriculum-data/${VERSION}`, {
|
||||||
|
directoryFilter: ['!challenges'],
|
||||||
|
fileFilter: entry => {
|
||||||
|
// The directory contains super block files and other curriculum-related files.
|
||||||
|
// We're only interested in super block ones.
|
||||||
|
const superBlocks = Object.values(SuperBlocks);
|
||||||
|
return (
|
||||||
|
superBlocks.includes(entry.basename) &&
|
||||||
|
!chapterBasedSuperBlocks.includes(entry.basename)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).map(file => file.path);
|
||||||
|
|
||||||
|
superBlockFiles.forEach(file => {
|
||||||
|
const fileContentJson = fs.readFileSync(
|
||||||
|
`${clientStaticPath}/curriculum-data/${VERSION}/${file}`,
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileContent = JSON.parse(
|
||||||
|
fileContentJson
|
||||||
|
) as Curriculum<GeneratedCurriculumProps>;
|
||||||
|
|
||||||
|
const superBlock = Object.keys(fileContent)[0] as SuperBlocks;
|
||||||
|
const superBlockData = fileContent[
|
||||||
|
superBlock
|
||||||
|
] as GeneratedBlockBasedCurriculumProps;
|
||||||
|
|
||||||
|
// Randomly pick a block to check its data.
|
||||||
|
const blocks = superBlockData.blocks;
|
||||||
|
const randomBlockIndex = Math.floor(Math.random() * blocks.length);
|
||||||
|
|
||||||
|
expect(superBlockData.intro).toEqual(intros[superBlock].intro);
|
||||||
|
expect(superBlockData.blocks[randomBlockIndex].intro).toEqual(
|
||||||
|
intros[superBlock].blocks[randomBlockIndex].intro
|
||||||
|
);
|
||||||
|
expect(superBlockData.blocks[randomBlockIndex].meta.name).toEqual(
|
||||||
|
intros[superBlock].blocks[randomBlockIndex].title
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('chapter-based super blocks and blocks should have the correct data', async () => {
|
||||||
|
const superBlockFiles = (
|
||||||
|
await readdirp.promise(`${clientStaticPath}/curriculum-data/${VERSION}`, {
|
||||||
|
directoryFilter: ['!challenges'],
|
||||||
|
fileFilter: entry => {
|
||||||
|
// The directory contains super block files and other curriculum-related files.
|
||||||
|
// We're only interested in super block ones.
|
||||||
|
const superBlocks = Object.values(SuperBlocks);
|
||||||
|
return (
|
||||||
|
superBlocks.includes(entry.basename) &&
|
||||||
|
chapterBasedSuperBlocks.includes(entry.basename)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).map(file => file.path);
|
||||||
|
|
||||||
|
superBlockFiles.forEach(file => {
|
||||||
|
const fileContentJson = fs.readFileSync(
|
||||||
|
`${clientStaticPath}/curriculum-data/${VERSION}/${file}`,
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileContent = JSON.parse(
|
||||||
|
fileContentJson
|
||||||
|
) as Curriculum<GeneratedCurriculumProps>;
|
||||||
|
|
||||||
|
const superBlock = Object.keys(fileContent)[0] as SuperBlocks;
|
||||||
|
const superBlockData = fileContent[
|
||||||
|
superBlock
|
||||||
|
] as GeneratedChapterBasedCurriculumProps;
|
||||||
|
|
||||||
|
// Randomly pick a chapter.
|
||||||
|
const chapters = superBlockData.chapters;
|
||||||
|
const randomChapterIndex = Math.floor(Math.random() * chapters.length);
|
||||||
|
const randomChapter = chapters[randomChapterIndex];
|
||||||
|
|
||||||
|
// Randomly pick a module.
|
||||||
|
const modules = randomChapter.modules;
|
||||||
|
const randomModuleIndex = Math.floor(Math.random() * modules.length);
|
||||||
|
const randomModule = modules[randomModuleIndex];
|
||||||
|
|
||||||
|
// Randomly pick a block.
|
||||||
|
const blocks = randomModule.blocks;
|
||||||
|
const randomBlockIndex = Math.floor(Math.random() * blocks.length);
|
||||||
|
|
||||||
|
expect(superBlockData.intro).toEqual(intros[superBlock].intro);
|
||||||
|
expect(
|
||||||
|
superBlockData.chapters[randomChapterIndex].modules[randomModuleIndex]
|
||||||
|
.blocks[randomBlockIndex].intro
|
||||||
|
).toEqual(intros[superBlock].blocks[randomBlockIndex].intro);
|
||||||
|
expect(
|
||||||
|
superBlockData.chapters[randomChapterIndex].modules[randomModuleIndex]
|
||||||
|
.blocks[randomBlockIndex].meta.name
|
||||||
|
).toEqual(intros[superBlock].blocks[randomBlockIndex].title);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('All public SuperBlocks should be present in the SuperBlock object', () => {
|
||||||
|
const publicSuperBlockNames = Object.values(SuperBlocks);
|
||||||
|
|
||||||
|
const superBlockDashedNames = Object.keys(orderedSuperBlockInfo).reduce(
|
||||||
|
(acc, superBlockStage) => {
|
||||||
|
const dashedNames = orderedSuperBlockInfo[superBlockStage].map(
|
||||||
|
superBlock => superBlock.dashedName
|
||||||
|
);
|
||||||
|
acc.push(...dashedNames);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as SuperBlocks[]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(superBlockDashedNames).toEqual(
|
||||||
|
expect.arrayContaining(publicSuperBlockNames)
|
||||||
|
);
|
||||||
|
expect(superBlockDashedNames).toHaveLength(publicSuperBlockNames.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('challenge files should be created and in the correct directory', () => {
|
||||||
|
expect(
|
||||||
|
fs.existsSync(`${clientStaticPath}/curriculum-data/${VERSION}/challenges`)
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
fs.readdirSync(
|
||||||
|
`${clientStaticPath}/curriculum-data/${VERSION}/challenges`
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
import { mkdirSync, writeFileSync, readFileSync } from 'fs';
|
||||||
|
import { resolve, dirname } from 'path';
|
||||||
|
import { submitTypes } from '../../../shared/config/challenge-types';
|
||||||
|
import { type ChallengeNode } from '../../../client/src/redux/prop-types';
|
||||||
|
import {
|
||||||
|
SuperBlocks,
|
||||||
|
SuperBlockStage
|
||||||
|
} from '../../../shared/config/curriculum';
|
||||||
|
import fullStackSuperBlockStructure from '../../../curriculum/superblock-structure/full-stack.json';
|
||||||
|
import type { Chapter } from '../../../shared/config/chapters';
|
||||||
|
|
||||||
|
export type CurriculumIntros = {
|
||||||
|
[keyValue in SuperBlocks]: {
|
||||||
|
title: string;
|
||||||
|
intro: string[];
|
||||||
|
blocks: Record<string, { title: string; intro: string[] }>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Curriculum<T> = {
|
||||||
|
[keyValue in SuperBlocks]: T extends CurriculumProps
|
||||||
|
? CurriculumProps
|
||||||
|
: GeneratedCurriculumProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CurriculumProps {
|
||||||
|
intro: string[];
|
||||||
|
blocks: Record<string, Block<ChallengeNode['challenge'][]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Block<T> {
|
||||||
|
desc: string[];
|
||||||
|
intro: string[];
|
||||||
|
challenges: T;
|
||||||
|
meta: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GeneratedCurriculumProps =
|
||||||
|
| GeneratedBlockBasedCurriculumProps
|
||||||
|
| GeneratedChapterBasedCurriculumProps;
|
||||||
|
|
||||||
|
export interface GeneratedBlockBasedCurriculumProps {
|
||||||
|
intro: string[];
|
||||||
|
blocks: GeneratedBlock[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeneratedChapterBasedCurriculumProps {
|
||||||
|
intro: string[];
|
||||||
|
chapters: GeneratedChapter[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeneratedChapter {
|
||||||
|
dashedName: string;
|
||||||
|
comingSoon?: boolean;
|
||||||
|
modules: GeneratedModule[];
|
||||||
|
chapterType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeneratedModule {
|
||||||
|
dashedName: string;
|
||||||
|
comingSoon?: boolean;
|
||||||
|
blocks: GeneratedBlock[];
|
||||||
|
moduleType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeneratedBlock {
|
||||||
|
dashedName: string;
|
||||||
|
intro: string;
|
||||||
|
meta: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ver = 'v2';
|
||||||
|
|
||||||
|
const staticFolderPath = resolve(__dirname, '../../../client/static');
|
||||||
|
const dataPath = `${staticFolderPath}/curriculum-data/`;
|
||||||
|
const blockIntroPath = resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../../client/i18n/locales/english/intro.json'
|
||||||
|
);
|
||||||
|
const intros = JSON.parse(
|
||||||
|
readFileSync(blockIntroPath, 'utf-8')
|
||||||
|
) as CurriculumIntros;
|
||||||
|
|
||||||
|
export const orderedSuperBlockInfo: Record<
|
||||||
|
string,
|
||||||
|
Array<{ dashedName: SuperBlocks; public: boolean; title: string }>
|
||||||
|
> = {
|
||||||
|
[SuperBlockStage.Core]: [
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.FullStackDeveloper,
|
||||||
|
public: false,
|
||||||
|
title: intros[SuperBlocks.FullStackDeveloper].title
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
[SuperBlockStage.English]: [
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.A2English,
|
||||||
|
public: true,
|
||||||
|
title: intros[SuperBlocks.A2English].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.B1English,
|
||||||
|
public: true,
|
||||||
|
title: intros[SuperBlocks.B1English].title
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
[SuperBlockStage.Extra]: [
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.TheOdinProject,
|
||||||
|
public: true,
|
||||||
|
title: intros[SuperBlocks.TheOdinProject].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.CodingInterviewPrep,
|
||||||
|
public: false,
|
||||||
|
title: intros[SuperBlocks.CodingInterviewPrep].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.ProjectEuler,
|
||||||
|
public: false,
|
||||||
|
title: intros[SuperBlocks.ProjectEuler].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.RosettaCode,
|
||||||
|
public: false,
|
||||||
|
title: intros[SuperBlocks.RosettaCode].title
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
[SuperBlockStage.Legacy]: [
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.RespWebDesignNew,
|
||||||
|
public: true,
|
||||||
|
title: intros[SuperBlocks.RespWebDesignNew].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.JsAlgoDataStructNew,
|
||||||
|
public: false,
|
||||||
|
title: intros[SuperBlocks.JsAlgoDataStructNew].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.FrontEndDevLibs,
|
||||||
|
public: false,
|
||||||
|
title: intros[SuperBlocks.FrontEndDevLibs].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.DataVis,
|
||||||
|
public: false,
|
||||||
|
title: intros[SuperBlocks.DataVis].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.RelationalDb,
|
||||||
|
public: false,
|
||||||
|
title: intros[SuperBlocks.RelationalDb].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.BackEndDevApis,
|
||||||
|
public: false,
|
||||||
|
title: intros[SuperBlocks.BackEndDevApis].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.QualityAssurance,
|
||||||
|
public: false,
|
||||||
|
title: intros[SuperBlocks.QualityAssurance].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.SciCompPy,
|
||||||
|
public: false,
|
||||||
|
title: intros[SuperBlocks.SciCompPy].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.DataAnalysisPy,
|
||||||
|
public: true,
|
||||||
|
title: intros[SuperBlocks.DataAnalysisPy].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.InfoSec,
|
||||||
|
public: false,
|
||||||
|
title: intros[SuperBlocks.InfoSec].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.MachineLearningPy,
|
||||||
|
public: true,
|
||||||
|
title: intros[SuperBlocks.MachineLearningPy].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.CollegeAlgebraPy,
|
||||||
|
public: true,
|
||||||
|
title: intros[SuperBlocks.CollegeAlgebraPy].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.RespWebDesign,
|
||||||
|
public: true,
|
||||||
|
title: intros[SuperBlocks.RespWebDesign].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.JsAlgoDataStruct,
|
||||||
|
public: false,
|
||||||
|
title: intros[SuperBlocks.JsAlgoDataStruct].title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.PythonForEverybody,
|
||||||
|
public: true,
|
||||||
|
title: intros[SuperBlocks.PythonForEverybody].title
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
[SuperBlockStage.Professional]: [
|
||||||
|
{
|
||||||
|
dashedName: SuperBlocks.FoundationalCSharp,
|
||||||
|
public: false,
|
||||||
|
title: intros[SuperBlocks.FoundationalCSharp].title
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const superBlockDashedNames = Object.keys(orderedSuperBlockInfo).reduce(
|
||||||
|
(acc, superBlockStage) => {
|
||||||
|
const dashedNames = orderedSuperBlockInfo[superBlockStage].map(
|
||||||
|
superBlock => superBlock.dashedName
|
||||||
|
);
|
||||||
|
acc.push(...dashedNames);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as SuperBlocks[]
|
||||||
|
);
|
||||||
|
|
||||||
|
export function buildExtCurriculumDataV2(
|
||||||
|
curriculum: Curriculum<CurriculumProps>
|
||||||
|
): void {
|
||||||
|
mkdirSync(dataPath, { recursive: true });
|
||||||
|
|
||||||
|
parseCurriculumData();
|
||||||
|
getSubmitTypes();
|
||||||
|
|
||||||
|
function parseCurriculumData() {
|
||||||
|
const superBlockKeys = Object.values(SuperBlocks).filter(x =>
|
||||||
|
superBlockDashedNames.includes(x)
|
||||||
|
);
|
||||||
|
|
||||||
|
writeToFile('available-superblocks', {
|
||||||
|
superblocks: orderedSuperBlockInfo
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const superBlockKey of superBlockKeys) {
|
||||||
|
if (superBlockKey === SuperBlocks.FullStackDeveloper) {
|
||||||
|
buildChapterBasedCurriculum(superBlockKey);
|
||||||
|
} else {
|
||||||
|
buildBlockBasedCurriculum(superBlockKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildChallengeFiles(superBlockKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChapterBasedCurriculum(superBlockKey: SuperBlocks) {
|
||||||
|
const chapters: Chapter[] = fullStackSuperBlockStructure.chapters;
|
||||||
|
const blocksWithData = curriculum[superBlockKey].blocks;
|
||||||
|
|
||||||
|
// Skip upcoming chapter/module as the metadata of their blocks
|
||||||
|
// is not included in the `curriculum` object.
|
||||||
|
const allChapters = chapters.map(chapter => ({
|
||||||
|
dashedName: chapter.dashedName,
|
||||||
|
comingSoon: chapter.comingSoon,
|
||||||
|
chapterType: chapter.chapterType,
|
||||||
|
modules: chapter.comingSoon
|
||||||
|
? []
|
||||||
|
: chapter.modules.map(module => ({
|
||||||
|
dashedName: module.dashedName,
|
||||||
|
comingSoon: module.comingSoon,
|
||||||
|
moduleType: module.moduleType,
|
||||||
|
blocks: module.comingSoon
|
||||||
|
? []
|
||||||
|
: module.blocks.map(block => {
|
||||||
|
const blockData = blocksWithData[block.dashedName];
|
||||||
|
|
||||||
|
return {
|
||||||
|
intro: intros[superBlockKey].blocks[block.dashedName].intro,
|
||||||
|
meta: blockData.meta
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
const superBlock = {
|
||||||
|
[superBlockKey]: {
|
||||||
|
intro: intros[superBlockKey].intro,
|
||||||
|
chapters: allChapters
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
writeToFile(superBlockKey, superBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBlockBasedCurriculum(superBlockKey: SuperBlocks) {
|
||||||
|
const blockNames = Object.keys(curriculum[superBlockKey].blocks);
|
||||||
|
const blocks = blockNames.map(blockName => {
|
||||||
|
const blockData = curriculum[superBlockKey].blocks[blockName];
|
||||||
|
|
||||||
|
return {
|
||||||
|
intro: intros[superBlockKey].blocks[blockName].intro,
|
||||||
|
meta: blockData.meta
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const superBlock = {
|
||||||
|
[superBlockKey]: {
|
||||||
|
intro: intros[superBlockKey].intro,
|
||||||
|
blocks
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
writeToFile(superBlockKey, superBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildChallengeFiles(superBlockKey: SuperBlocks) {
|
||||||
|
const blocks = Object.keys(curriculum[superBlockKey].blocks);
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
const challenges = curriculum[superBlockKey]['blocks'][block].challenges;
|
||||||
|
|
||||||
|
for (const challenge of challenges) {
|
||||||
|
const challengeId = challenge.id;
|
||||||
|
const challengePath = `challenges/${superBlockKey}/${block}/${challengeId}`;
|
||||||
|
|
||||||
|
writeToFile(challengePath, challenge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeToFile(fileName: string, data: Record<string, unknown>): void {
|
||||||
|
const filePath = `${dataPath}/${ver}/${fileName}.json`;
|
||||||
|
mkdirSync(dirname(filePath), { recursive: true });
|
||||||
|
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubmitTypes() {
|
||||||
|
writeFileSync(
|
||||||
|
`${dataPath}/${ver}/submit-types.json`,
|
||||||
|
JSON.stringify(submitTypes, null, 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
const Joi = require('joi');
|
||||||
|
const {
|
||||||
|
chapterBasedSuperBlocks
|
||||||
|
} = require('../../../shared/config/curriculum');
|
||||||
|
|
||||||
|
const slugRE = new RegExp('^[a-z0-9-]+$');
|
||||||
|
|
||||||
|
const blockSchema = Joi.object().keys({
|
||||||
|
intro: Joi.array().min(1),
|
||||||
|
meta: Joi.object({}).keys({
|
||||||
|
name: Joi.string().required(),
|
||||||
|
isUpcomingChange: Joi.bool().required(),
|
||||||
|
usesMultifileEditor: Joi.bool().optional(),
|
||||||
|
hasEditableBoundaries: Joi.bool().optional(),
|
||||||
|
dashedName: Joi.string().required(),
|
||||||
|
helpCategory: Joi.valid(
|
||||||
|
'JavaScript',
|
||||||
|
'HTML-CSS',
|
||||||
|
'Python',
|
||||||
|
'Backend Development',
|
||||||
|
'C-Sharp',
|
||||||
|
'English',
|
||||||
|
'Odin',
|
||||||
|
'Euler',
|
||||||
|
'Rosetta'
|
||||||
|
).required(),
|
||||||
|
order: Joi.number().when('superBlock', {
|
||||||
|
is: chapterBasedSuperBlocks,
|
||||||
|
then: Joi.forbidden(),
|
||||||
|
otherwise: Joi.required()
|
||||||
|
}),
|
||||||
|
template: Joi.string().allow(''),
|
||||||
|
required: Joi.array(),
|
||||||
|
superBlock: Joi.string().required(),
|
||||||
|
blockLayout: Joi.valid(
|
||||||
|
'challenge-list',
|
||||||
|
'challenge-grid',
|
||||||
|
'dialogue-grid',
|
||||||
|
'link',
|
||||||
|
'project-list',
|
||||||
|
'legacy-challenge-list',
|
||||||
|
'legacy-link',
|
||||||
|
'legacy-challenge-grid'
|
||||||
|
).required(),
|
||||||
|
blockType: Joi.valid(
|
||||||
|
'lecture',
|
||||||
|
'workshop',
|
||||||
|
'lab',
|
||||||
|
'review',
|
||||||
|
'quiz',
|
||||||
|
'exam'
|
||||||
|
).when('superBlock', {
|
||||||
|
is: chapterBasedSuperBlocks,
|
||||||
|
then: Joi.required(),
|
||||||
|
otherwise: Joi.optional()
|
||||||
|
}),
|
||||||
|
challengeOrder: Joi.array().items(
|
||||||
|
Joi.object({}).keys({
|
||||||
|
id: Joi.string(),
|
||||||
|
title: Joi.string()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
disableLoopProtectTests: Joi.boolean(),
|
||||||
|
disableLoopProtectPreview: Joi.boolean(),
|
||||||
|
superOrder: Joi.number()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const blockBasedCurriculumSchema = Joi.object().pattern(
|
||||||
|
Joi.string(),
|
||||||
|
Joi.object().keys({
|
||||||
|
intro: Joi.array(),
|
||||||
|
blocks: Joi.array().items(blockSchema)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const chapterBasedCurriculumSchema = Joi.object().pattern(
|
||||||
|
Joi.string(),
|
||||||
|
Joi.object().keys({
|
||||||
|
intro: Joi.array(),
|
||||||
|
chapters: Joi.array().items(
|
||||||
|
Joi.object().keys({
|
||||||
|
dashedName: Joi.string().regex(slugRE).required(),
|
||||||
|
comingSoon: Joi.boolean().optional(),
|
||||||
|
chapterType: Joi.valid('exam').optional(),
|
||||||
|
modules: Joi.array()
|
||||||
|
.items(
|
||||||
|
Joi.object().keys({
|
||||||
|
moduleType: Joi.valid('review', 'exam').optional(),
|
||||||
|
comingSoon: Joi.boolean().optional(),
|
||||||
|
dashedName: Joi.string().regex(slugRE).required(),
|
||||||
|
blocks: Joi.array().items(blockSchema)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.required()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const availableSuperBlocksSchema = Joi.object({
|
||||||
|
superblocks: Joi.object().pattern(
|
||||||
|
Joi.string(),
|
||||||
|
Joi.array().items(
|
||||||
|
Joi.object({
|
||||||
|
dashedName: Joi.string().required(),
|
||||||
|
title: Joi.string().required(),
|
||||||
|
public: Joi.bool().required()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.superblockSchemaValidator = () => superBlock => {
|
||||||
|
const superBlockName = Object.keys(superBlock)[0];
|
||||||
|
|
||||||
|
if (chapterBasedSuperBlocks.includes(superBlockName)) {
|
||||||
|
return chapterBasedCurriculumSchema.validate(superBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockBasedCurriculumSchema.validate(superBlock);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.availableSuperBlocksValidator = () => data =>
|
||||||
|
availableSuperBlocksSchema.validate(data);
|
||||||
Reference in New Issue
Block a user