refactor(curriculum): remove block name metadata and source titles from intro (#66415)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Sem Bauke
2026-03-17 16:43:14 +01:00
committed by GitHub
parent 776ce24c8e
commit 7c3c64bf8d
984 changed files with 340 additions and 1133 deletions

View File

@@ -14,6 +14,7 @@ import {
superBlockStages,
SuperBlockStage
} from '@freecodecamp/shared/config/curriculum';
import { getCurriculum } from '../tools/get-curriculum';
import intro from './locales/english/intro.json';
interface Intro {
@@ -110,3 +111,38 @@ describe('Intro file structure tests:', () => {
});
}
});
type SuperBlockInfo = {
blocks: Record<string, unknown>;
};
describe('Curriculum validation', () => {
const curriculum = getCurriculum() as Record<string, SuperBlockInfo>;
// certifications are not superblocks, they're just mixed in with them.
const superblocks = Object.entries(curriculum).filter(
([key]) => key !== 'certifications'
);
// It's important that we check that each block in the curriculum has a title
// in the intro, rather than the other way around, because the intro must
// include upcoming changes. The curriculum only does if SHOW_UPCOMING_CHANGES
// is true.
superblocks.forEach(superblock => {
const [name, superBlockInfo] = superblock;
const blockObject = superBlockInfo.blocks;
describe(`${name}`, () => {
test('should have titles for each block in intro.json', () => {
const blocks = Object.keys(blockObject);
blocks.forEach(block => {
const blockFromIntro = (intro as unknown as Intro)[superblock[0]]
.blocks[block];
expect(
blockFromIntro.title,
`block ${block} needs a non-empty title`
).toBeTruthy();
});
});
});
});
});

View File

@@ -9904,7 +9904,7 @@
"title": "Full-Stack Open",
"intro": ["A good intro is to be added here."],
"blocks": {
"cat-blog-page": {
"workshop-blog-page": {
"title": "Build a Cat Blog Page",
"intro": [
"In this workshop, you will build an HTML only blog page using semantic elements including the <code>main</code>, <code>nav</code>, <code>article</code> and <code>footer</code> elements."

View File

@@ -161,21 +161,17 @@ describe('external curriculum data build', () => {
superBlock
] as GeneratedBlockBasedCurriculumProps;
// Temporary skip these checks to keep CI stable.
// TODO: uncomment these once https://github.com/freeCodeCamp/freeCodeCamp/issues/60660 is completed.
// Randomly pick a block to check its data.
// const blocks = superBlockData.blocks;
// const randomBlockIndex = Math.floor(Math.random() * blocks.length);
// const randomBlock = blocks[randomBlockIndex];
expect(superBlockData.intro).toEqual(intros[superBlock].intro);
// expect(superBlockData.blocks[randomBlockIndex].intro).toEqual(
// intros[superBlock].blocks[randomBlock.meta.dashedName as string].intro
// );
// expect(superBlockData.blocks[randomBlockIndex].meta.name).toEqual(
// intros[superBlock].blocks[randomBlock.meta.dashedName as string].title
// );
const blocks = superBlockData.blocks;
for (const block of blocks) {
expect(block.intro).toEqual(
intros[superBlock].blocks[block.meta.dashedName as string].intro
);
expect(block.meta.name).toEqual(
intros[superBlock].blocks[block.meta.dashedName as string].title
);
}
});
});
@@ -244,22 +240,22 @@ describe('external curriculum data build', () => {
});
});
// Temporary skip these checks to keep CI stable.
// TODO: uncomment these once https://github.com/freeCodeCamp/freeCodeCamp/issues/60660 is completed.
for (const chapter of superBlockData.chapters) {
if (chapter.comingSoon) continue;
// Check block data
// expect(
// superBlockData.chapters[randomChapterIndex].modules[randomModuleIndex]
// .blocks[randomBlockIndex].intro
// ).toEqual(
// superBlockIntros.blocks[randomBlock.meta.dashedName as string].intro
// );
// expect(
// superBlockData.chapters[randomChapterIndex].modules[randomModuleIndex]
// .blocks[randomBlockIndex].meta.name
// ).toEqual(
// superBlockIntros.blocks[randomBlock.meta.dashedName as string].title
// );
for (const module of chapter.modules) {
if (module.comingSoon) continue;
for (const block of module.blocks) {
expect(block.intro).toEqual(
superBlockIntros.blocks[block.meta.dashedName as string].intro
);
expect(block.meta.name).toEqual(
superBlockIntros.blocks[block.meta.dashedName as string].title
);
}
}
}
});
});

View File

@@ -376,9 +376,15 @@ export function buildExtCurriculumDataV2(
.filter(block => blocksWithData[block])
.map(block => {
const blockData = blocksWithData[block];
const blockIntro = superBlockIntros.blocks[block];
return {
intro: superBlockIntros.blocks[block].intro,
meta: omit(blockData.meta, ['chapter', 'module'])
intro: blockIntro.intro,
// Keep `meta.name` for backward compatibility with
// consumers that have not migrated to intro-based titles.
meta: {
...omit(blockData.meta, ['chapter', 'module']),
name: blockIntro.title
}
};
})
}))
@@ -398,10 +404,13 @@ export function buildExtCurriculumDataV2(
const blockNames = Object.keys(curriculum[superBlockKey].blocks);
const blocks = blockNames.map(blockName => {
const blockData = curriculum[superBlockKey].blocks[blockName];
const blockIntro = intros[superBlockKey].blocks[blockName];
return {
intro: intros[superBlockKey].blocks[blockName].intro,
meta: blockData.meta
intro: blockIntro.intro,
// Keep `meta.name` for backward compatibility with
// consumers that have not migrated to intro-based titles.
meta: { ...blockData.meta, name: blockIntro.title }
};
});

View File

@@ -1,13 +1,4 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
const CURRICULUM_PATH = '../../../curriculum/generated/curriculum.json';
// const __dirname = dirname(fileURLToPath(import.meta.url));
// Curriculum is read using fs, because it is too large for VSCode's LSP to handle type inference which causes annoying behavior.
const curriculum = JSON.parse(
readFileSync(join(__dirname, CURRICULUM_PATH), 'utf-8')
);
import { getCurriculum } from '../get-curriculum';
import {
buildExtCurriculumDataV2,
Curriculum as CurriculumV2,
@@ -24,5 +15,5 @@ if (isSelectiveBuild) {
'Skipping external curriculum build (selective build mode active)'
);
} else {
buildExtCurriculumDataV2(curriculum as CurriculumV2<CurriculumPropsV2>);
buildExtCurriculumDataV2(getCurriculum() as CurriculumV2<CurriculumPropsV2>);
}

View File

@@ -0,0 +1,8 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
const CURRICULUM_PATH = '../../curriculum/generated/curriculum.json';
// Curriculum is read using fs, because it is too large for VSCode's LSP to handle type inference which causes annoying behavior.
export const getCurriculum = () =>
JSON.parse(readFileSync(join(__dirname, CURRICULUM_PATH), 'utf-8'));

View File

@@ -0,0 +1,69 @@
const Joi = require('joi');
const blockIntroSchema = Joi.object()
.keys({
title: Joi.string().trim().min(1).required(),
intro: Joi.array().items(Joi.string().allow('')).required()
})
.unknown(true);
const superBlockIntroSchema = Joi.object()
.keys({
title: Joi.string().trim().min(1).required(),
intro: Joi.array().items(Joi.string().allow('')).required(),
blocks: Joi.object().pattern(Joi.string(), blockIntroSchema).required()
})
.unknown(true);
function createIntroSchema(expectedBlocksBySuperblock) {
return Joi.object()
.unknown(true)
.custom((intros, helpers) => {
for (const [superblock, blocks] of Object.entries(
expectedBlocksBySuperblock
)) {
const superBlockIntro = intros[superblock];
if (!superBlockIntro) {
return helpers.error('any.custom', {
message: `Missing intro.json entry for superblock "${superblock}"`
});
}
const { error: superBlockError } =
superBlockIntroSchema.validate(superBlockIntro);
if (superBlockError) {
return helpers.error('any.custom', {
message: `Invalid intro.json shape for superblock "${superblock}": ${superBlockError.message}`
});
}
for (const block of blocks) {
const blockIntro = superBlockIntro.blocks?.[block];
if (!blockIntro) {
return helpers.error('any.custom', {
message: `Missing intro.json block title entry for "${superblock}/${block}"`
});
}
const { error: blockError } = blockIntroSchema.validate(blockIntro);
if (blockError) {
return helpers.error('any.custom', {
message: `Invalid intro.json block entry for "${superblock}/${block}": ${blockError.message}`
});
}
}
}
return intros;
}, 'intro block coverage validation')
.messages({
'any.custom': '{{#message}}'
});
}
exports.validateIntroSchema = (intros, expectedBlocksBySuperblock) => {
return createIntroSchema(expectedBlocksBySuperblock).validate(intros, {
abortEarly: false
});
};

View File

@@ -0,0 +1,53 @@
import path from 'node:path';
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import { addSuperblockStructure } from '../src/build-curriculum.js';
import { getCurriculumStructure } from '../src/file-handler.js';
import introSchema from './intro-schema.js';
const { validateIntroSchema } = introSchema;
const introPath = path.resolve(
import.meta.dirname,
'../../client/i18n/locales/english/intro.json'
);
const intros = JSON.parse(readFileSync(introPath, 'utf8'));
function getExpectedBlocksBySuperblock() {
const { superblocks } = getCurriculumStructure();
return addSuperblockStructure(superblocks, true).reduce(
(expected, { name, blocks }) => {
expected[name] = blocks.map(({ dashedName }) => dashedName);
return expected;
},
{}
);
}
describe('intro schema', () => {
it('includes block titles for every curriculum block', () => {
const result = validateIntroSchema(intros, getExpectedBlocksBySuperblock());
expect(result.error).toBeUndefined();
});
it('fails if a block title entry is missing', () => {
const expectedBlocksBySuperblock = getExpectedBlocksBySuperblock();
const [superblock, blocks] = Object.entries(expectedBlocksBySuperblock)[0];
const block = blocks[0];
const introsWithoutBlock = structuredClone(intros);
delete introsWithoutBlock[superblock].blocks[block];
const result = validateIntroSchema(introsWithoutBlock, {
[superblock]: [block]
});
expect(result.error).toBeDefined();
expect(result.error?.message).toContain(`${superblock}/${block}`);
});
});

View File

@@ -4,7 +4,6 @@ const slugRE = new RegExp('^[a-z0-9-]+$');
const schema = Joi.object()
.keys({
name: Joi.string().required(),
blockLayout: Joi.valid(
'challenge-list',
'challenge-grid',

View File

@@ -68,7 +68,6 @@ const dummyUnfinishedSuperBlock = {
};
const dummyBlockMeta = {
name: 'Test Block',
blockLayout: 'challenge-list',
blockLabel: 'workshop',
isUpcomingChange: false,
@@ -327,7 +326,6 @@ describe('buildSuperblock pure functions', () => {
];
const meta = {
name: 'Test Block',
dashedName: 'test-block',
challengeOrder: [
{ id: '1', title: 'Challenge 1' },
@@ -346,7 +344,6 @@ describe('buildSuperblock pure functions', () => {
const foundChallenges = [{ id: '2', title: 'Challenge 2' }];
const meta = {
name: 'Test Block',
dashedName: 'test-block',
challengeOrder: [
{ id: '1', title: 'Challenge 1' }, // Missing
@@ -363,7 +360,6 @@ describe('buildSuperblock pure functions', () => {
const foundChallenges = [{ id: '1', title: 'Challenge 1' }];
const meta = {
name: 'Test Block',
dashedName: 'test-block',
challengeOrder: [{ id: '1', title: 'Challenge 1' }]
};

View File

@@ -117,7 +117,7 @@ export function validateChallenges(
/**
* Builds a block object from challenges and meta data
* @param {Array<object>} foundChallenges - Array of challenge objects
* @param {object} meta - Meta object with name, dashedName, and challengeOrder
* @param {object} meta - Meta object with dashedName and challengeOrder
* @returns {object} Block object with ordered challenges
*/
export function buildBlock(foundChallenges: Challenge[], meta: Meta) {
@@ -415,7 +415,7 @@ export class BlockCreator {
const blockResult = buildBlock(foundChallenges, meta);
log(
`Completed block "${meta.name}" with ${blockResult.challenges.length} challenges (${blockResult.challenges.filter(c => !c.missing).length} built successfully)`
`Completed block "${meta.dashedName}" with ${blockResult.challenges.length} challenges (${blockResult.challenges.filter(c => !c.missing).length} built successfully)`
);
return blockResult;

View File

@@ -161,7 +161,6 @@ export type Challenge = {
};
export interface BlockStructure {
name: string;
hasEditableBoundaries?: boolean;
required?: string[];
template?: string;

View File

@@ -1,5 +1,4 @@
{
"name": "Add Logic to C# Console Applications",
"isUpcomingChange": false,
"dashedName": "add-logic-to-c-sharp-console-applications",
"helpCategory": "C-Sharp",

View File

@@ -1,5 +1,4 @@
{
"name": "Advanced Node and Express",
"isUpcomingChange": false,
"dashedName": "advanced-node-and-express",
"helpCategory": "JavaScript",

View File

@@ -1,5 +1,4 @@
{
"name": "Advanced OOP Concepts",
"isUpcomingChange": false,
"blockLabel": "lecture",
"blockLayout": "challenge-list",

View File

@@ -1,5 +1,4 @@
{
"name": "Advanced Trig & Conics",
"blockLabel": "lecture",
"blockLayout": "challenge-list",
"isUpcomingChange": false,

View File

@@ -1,5 +1,4 @@
{
"name": "Algorithms in Code",
"blockLabel": "lecture",
"blockLayout": "challenge-list",
"isUpcomingChange": false,

View File

@@ -1,5 +1,4 @@
{
"name": "Algorithms",
"isUpcomingChange": false,
"dashedName": "algorithms",
"helpCategory": "JavaScript",

View File

@@ -1,5 +1,4 @@
{
"name": "Angles and Circular Motion",
"blockLabel": "lecture",
"blockLayout": "challenge-list",
"isUpcomingChange": false,

View File

@@ -1,5 +1,4 @@
{
"name": "Applied Accessibility",
"isUpcomingChange": false,
"dashedName": "applied-accessibility",
"helpCategory": "HTML-CSS",

View File

@@ -1,5 +1,4 @@
{
"name": "Applied Visual Design",
"isUpcomingChange": false,
"dashedName": "applied-visual-design",
"helpCategory": "HTML-CSS",

View File

@@ -1,5 +1,4 @@
{
"name": "Back-End Development and APIs Projects",
"isUpcomingChange": false,
"dashedName": "back-end-development-and-apis-projects",
"helpCategory": "JavaScript",

View File

@@ -1,5 +1,4 @@
{
"name": "Basic Algorithm Scripting",
"isUpcomingChange": false,
"dashedName": "basic-algorithm-scripting",
"helpCategory": "JavaScript",

View File

@@ -1,5 +1,4 @@
{
"name": "Basic CSS",
"isUpcomingChange": false,
"dashedName": "basic-css",
"helpCategory": "HTML-CSS",

View File

@@ -1,5 +1,4 @@
{
"name": "Basic Data Structures",
"isUpcomingChange": false,
"dashedName": "basic-data-structures",
"helpCategory": "JavaScript",

View File

@@ -1,5 +1,4 @@
{
"name": "Basic HTML and HTML5",
"isUpcomingChange": false,
"dashedName": "basic-html-and-html5",
"helpCategory": "HTML-CSS",

View File

@@ -1,5 +1,4 @@
{
"name": "Basic JavaScript",
"isUpcomingChange": false,
"dashedName": "basic-javascript",
"helpCategory": "JavaScript",

View File

@@ -1,5 +1,4 @@
{
"name": "Basic Node and Express",
"isUpcomingChange": false,
"dashedName": "basic-node-and-express",
"helpCategory": "JavaScript",

View File

@@ -1,5 +1,4 @@
{
"name": "Bootstrap",
"isUpcomingChange": false,
"dashedName": "bootstrap",
"helpCategory": "HTML-CSS",

View File

@@ -1,5 +1,4 @@
{
"name": "Build a Budget App Project",
"isUpcomingChange": false,
"usesMultifileEditor": true,
"dashedName": "build-a-budget-app-project",

View File

@@ -1,5 +1,4 @@
{
"name": "Build a Cash Register Project",
"isUpcomingChange": false,
"dashedName": "build-a-cash-register-project",
"usesMultifileEditor": true,

View File

@@ -1,5 +1,4 @@
{
"name": "Celestial Bodies Database",
"isUpcomingChange": false,
"dashedName": "build-a-celestial-bodies-database-project",
"helpCategory": "Backend Development",

View File

@@ -1,5 +1,4 @@
{
"name": "Data Graph Explorer",
"isUpcomingChange": false,
"dashedName": "build-a-data-graph-explorer-project",
"helpCategory": "Python",

View File

@@ -1,5 +1,4 @@
{
"name": "Financial Calculator",
"isUpcomingChange": false,
"dashedName": "build-a-financial-calculator-project",
"helpCategory": "Python",

View File

@@ -1,5 +1,4 @@
{
"name": "Graphing Calculator",
"isUpcomingChange": false,
"dashedName": "build-a-graphing-calculator-project",
"helpCategory": "Python",

View File

@@ -1,5 +1,4 @@
{
"name": "Multi-Function Calculator",
"isUpcomingChange": false,
"dashedName": "build-a-multi-function-calculator-project",
"helpCategory": "Python",

View File

@@ -1,5 +1,4 @@
{
"name": "Number Guessing Game",
"isUpcomingChange": false,
"dashedName": "build-a-number-guessing-game-project",
"helpCategory": "Backend Development",

View File

@@ -1,5 +1,4 @@
{
"name": "Build a Palindrome Checker Project",
"isUpcomingChange": false,
"dashedName": "build-a-palindrome-checker-project",
"usesMultifileEditor": true,

View File

@@ -1,5 +1,4 @@
{
"name": "Periodic Table Database",
"isUpcomingChange": false,
"dashedName": "build-a-periodic-table-database-project",
"helpCategory": "Backend Development",

View File

@@ -1,5 +1,4 @@
{
"name": "Personal Portfolio Webpage",
"isUpcomingChange": false,
"dashedName": "build-a-personal-portfolio-webpage-project",
"usesMultifileEditor": true,

View File

@@ -1,5 +1,4 @@
{
"name": "Build a Polygon Area Calculator Project",
"isUpcomingChange": false,
"usesMultifileEditor": true,
"dashedName": "build-a-polygon-area-calculator-project",

View File

@@ -1,5 +1,4 @@
{
"name": "Build a Probability Calculator Project",
"isUpcomingChange": false,
"usesMultifileEditor": true,
"dashedName": "build-a-probability-calculator-project",

View File

@@ -1,5 +1,4 @@
{
"name": "Product Landing Page",
"isUpcomingChange": false,
"dashedName": "build-a-product-landing-page-project",
"usesMultifileEditor": true,

View File

@@ -1,5 +1,4 @@
{
"name": "Build a Roman Numeral Converter Project",
"isUpcomingChange": false,
"dashedName": "build-a-roman-numeral-converter-project",
"usesMultifileEditor": true,

View File

@@ -1,5 +1,4 @@
{
"name": "Salon Appointment Scheduler",
"isUpcomingChange": false,
"dashedName": "build-a-salon-appointment-scheduler-project",
"helpCategory": "Backend Development",

View File

@@ -1,5 +1,4 @@
{
"name": "Survey Form",
"isUpcomingChange": false,
"dashedName": "build-a-survey-form-project",
"usesMultifileEditor": true,

View File

@@ -1,5 +1,4 @@
{
"name": "Technical Documentation Page",
"isUpcomingChange": false,
"dashedName": "build-a-technical-documentation-page-project",
"usesMultifileEditor": true,

View File

@@ -1,5 +1,4 @@
{
"name": "Build a Telephone Number Validator Project",
"isUpcomingChange": false,
"dashedName": "build-a-telephone-number-validator-project",
"usesMultifileEditor": true,

View File

@@ -1,5 +1,4 @@
{
"name": "Build a Time Calculator Project",
"isUpcomingChange": false,
"usesMultifileEditor": true,
"dashedName": "build-a-time-calculator-project",

View File

@@ -1,5 +1,4 @@
{
"name": "Tribute Page",
"isUpcomingChange": false,
"dashedName": "build-a-tribute-page-project",
"usesMultifileEditor": true,

View File

@@ -1,5 +1,4 @@
{
"name": "World Cup Database",
"isUpcomingChange": false,
"dashedName": "build-a-world-cup-database-project",
"helpCategory": "Backend Development",

View File

@@ -1,5 +1,4 @@
{
"name": "Build an Arithmetic Formatter Project",
"isUpcomingChange": false,
"usesMultifileEditor": true,
"dashedName": "build-an-arithmetic-formatter-project",

View File

@@ -1,5 +1,4 @@
{
"name": "Build an RPG Creature Search App Project",
"isUpcomingChange": false,
"dashedName": "build-an-rpg-creature-search-app-project",
"usesMultifileEditor": true,

View File

@@ -1,5 +1,4 @@
{
"name": "Three Math Games",
"isUpcomingChange": false,
"dashedName": "build-three-math-games-project",
"helpCategory": "Python",

View File

@@ -1,5 +1,4 @@
{
"name": "College Algebra with Python: Conclusion",
"isUpcomingChange": false,
"dashedName": "college-algebra-with-python-conclusion",
"helpCategory": "Python",

View File

@@ -1,5 +1,4 @@
{
"name": "Control Flow and Functions",
"blockLabel": "lecture",
"blockLayout": "challenge-list",
"isUpcomingChange": false,

View File

@@ -1,5 +1,4 @@
{
"name": "Core Primitives in Python",
"blockLabel": "lecture",
"blockLayout": "challenge-list",
"isUpcomingChange": false,

View File

@@ -1,5 +1,4 @@
{
"name": "Create and Run Simple C# Console Applications",
"isUpcomingChange": false,
"dashedName": "create-and-run-simple-c-sharp-console-applications",
"helpCategory": "C-Sharp",

View File

@@ -1,5 +1,4 @@
{
"name": "Create Methods in C# Console Applications",
"isUpcomingChange": false,
"dashedName": "create-methods-in-c-sharp-console-applications",
"helpCategory": "C-Sharp",

View File

@@ -1,5 +1,4 @@
{
"name": "CSS Flexbox",
"isUpcomingChange": false,
"dashedName": "css-flexbox",
"helpCategory": "HTML-CSS",

View File

@@ -1,5 +1,4 @@
{
"name": "CSS Grid",
"isUpcomingChange": false,
"dashedName": "css-grid",
"helpCategory": "HTML-CSS",

View File

@@ -1,5 +1,4 @@
{
"name": "Daily Coding Challenges JavaScript",
"isUpcomingChange": true,
"dashedName": "daily-coding-challenges-javascript",
"usesMultifileEditor": true,

View File

@@ -1,5 +1,4 @@
{
"name": "Daily Coding Challenges Python",
"isUpcomingChange": true,
"dashedName": "daily-coding-challenges-python",
"usesMultifileEditor": true,

View File

@@ -1,5 +1,4 @@
{
"name": "Data Analysis with Python",
"isUpcomingChange": false,
"dashedName": "data-analysis-with-python-course",
"helpCategory": "Python",

View File

@@ -1,5 +1,4 @@
{
"name": "Data Analysis with Python Projects",
"isUpcomingChange": false,
"dashedName": "data-analysis-with-python-projects",
"helpCategory": "Python",

View File

@@ -1,5 +1,4 @@
{
"name": "Data Structures",
"isUpcomingChange": false,
"dashedName": "data-structures",
"helpCategory": "JavaScript",

View File

@@ -1,5 +1,4 @@
{
"name": "Data Visualization Projects",
"isUpcomingChange": false,
"dashedName": "data-visualization-projects",
"helpCategory": "JavaScript",

View File

@@ -1,5 +1,4 @@
{
"name": "Data Visualization with D3",
"isUpcomingChange": false,
"dashedName": "data-visualization-with-d3",
"helpCategory": "JavaScript",

View File

@@ -1,5 +1,4 @@
{
"name": "Debug C# Console Applications",
"isUpcomingChange": false,
"dashedName": "debug-c-sharp-console-applications",
"helpCategory": "C-Sharp",

View File

@@ -1,5 +1,4 @@
{
"name": "Debugging",
"isUpcomingChange": false,
"dashedName": "debugging",
"helpCategory": "JavaScript",

View File

@@ -1,5 +1,4 @@
{
"name": "Designing Reliable RAG Systems",
"isUpcomingChange": false,
"blockLabel": "lecture",
"blockLayout": "challenge-list",

View File

@@ -1,5 +1,4 @@
{
"name": "Dictionaries and Loops",
"blockLabel": "lecture",
"blockLayout": "challenge-list",
"isUpcomingChange": false,

View File

@@ -1,5 +1,4 @@
{
"name": "A2 English for Developers Certification Exam",
"isUpcomingChange": false,
"dashedName": "en-a2-certification-exam",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Analyzing Code Documentation Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-analyze-code-documentation",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Asking for Code Clarification Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-ask-for-code-clarification",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Basic Programming Vocabulary Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-basic-programming-vocabulary",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Clarifying Information Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-clarify-information-interactions",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Code Concepts and Terms Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-code-related-concepts-terms",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Break Room Conversations Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-conversation-starters-break-room",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Describing Your Current Project Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-describe-current-project",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Daily Routines at Work Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-discuss-morning-evening-routine",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Roles and Responsibilities Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-discuss-roles-responsibilities",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Documenting Code Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-document-code-project",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Educational and Professional Background Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-educational-professional-background",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Expressing Agreement and Disagreement Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-express-agreement-disagreement",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "First Day at The Office Greetings Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-greetings-first-day-office",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Helping a Coworker on GitHub Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-help-coworker-github-troubleshooting",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Online Team Introductions Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-introductions-online-team-meeting",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Offering Technical Support Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-offer-technical-support-guidance",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Technology Trends Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-popular-technology-trends",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Preferences and Motivations Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-preferences-motivations",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Explaining Things to Others Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-provide-explanations-helping-others",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Understanding Code Documentation Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-read-understand-code-documentation",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Requesting and Receiving Guidance Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-request-receive-guidance",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Sharing Progress and Achievements Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-share-progress-accomplishments",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Weekly Meeting Progress Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-share-progress-weekly-meeting",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Talking About Hobbies and Interests Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-talk-about-hobbies-interests",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Talking About Your Workday Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-talk-about-typical-workday-tasks",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Task and Project Updates Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-task-project-updates-plans",
"helpCategory": "English",

View File

@@ -1,5 +1,4 @@
{
"name": "Tech Updates and Trends Quiz",
"isUpcomingChange": true,
"dashedName": "en-a2-quiz-tech-trends-updates",
"helpCategory": "English",

Some files were not shown because too many files have changed in this diff Show More