mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
feat(scripts): add scripts to seed exams (#50836)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -75,6 +75,7 @@
|
||||
"preseed": "npm-run-all create:*",
|
||||
"seed": "cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-demo-user",
|
||||
"seed:certified-user": "cross-env DEBUG=fcc:* node ./tools/scripts/seed/seed-demo-user certified-user",
|
||||
"seed:exams": "cd tools/scripts/seed-exams && cross-env node ./create-exams.js",
|
||||
"serve:client": "cd ./client && pnpm run serve",
|
||||
"serve:client-ci": "cd ./client && pnpm run serve-ci",
|
||||
"start": "npm-run-all create:* -p develop:server serve:client",
|
||||
|
||||
121
pnpm-lock.yaml
generated
121
pnpm-lock.yaml
generated
@@ -1254,6 +1254,30 @@ importers:
|
||||
specifier: 5.7.0
|
||||
version: 5.7.0
|
||||
|
||||
tools/scripts/seed-exams:
|
||||
devDependencies:
|
||||
dotenv:
|
||||
specifier: 16.3.1
|
||||
version: 16.3.1
|
||||
joi:
|
||||
specifier: 17.9.2
|
||||
version: 17.9.2
|
||||
joi-objectid:
|
||||
specifier: 3.0.1
|
||||
version: 3.0.1
|
||||
js-yaml:
|
||||
specifier: 4.1.0
|
||||
version: 4.1.0
|
||||
lodash:
|
||||
specifier: 4.17.21
|
||||
version: 4.17.21
|
||||
mongodb:
|
||||
specifier: 5.6.0
|
||||
version: 5.6.0
|
||||
nanoid:
|
||||
specifier: 4.0.2
|
||||
version: 4.0.2
|
||||
|
||||
tools/ui-components:
|
||||
dependencies:
|
||||
'@fortawesome/free-solid-svg-icons':
|
||||
@@ -16109,6 +16133,7 @@ packages:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
supports-color: 8.1.1
|
||||
dev: true
|
||||
|
||||
/debug@4.3.1:
|
||||
resolution: {integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==}
|
||||
@@ -16423,7 +16448,7 @@ packages:
|
||||
'@types/tmp': 0.0.33
|
||||
application-config-path: 0.1.1
|
||||
command-exists: 1.2.9
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
eol: 0.9.1
|
||||
get-port: 3.2.0
|
||||
glob: 7.2.3
|
||||
@@ -16729,7 +16754,7 @@ packages:
|
||||
hasBin: true
|
||||
dependencies:
|
||||
cross-spawn: 7.0.3
|
||||
dotenv: 16.0.3
|
||||
dotenv: 16.3.1
|
||||
dotenv-expand: 10.0.0
|
||||
minimist: 1.2.8
|
||||
dev: true
|
||||
@@ -16747,11 +16772,6 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/dotenv@16.0.3:
|
||||
resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/dotenv@16.3.1:
|
||||
resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -17326,7 +17346,7 @@ packages:
|
||||
/eslint-import-resolver-node@0.3.7:
|
||||
resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==}
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
is-core-module: 2.11.0
|
||||
resolve: 1.22.2
|
||||
transitivePeerDependencies:
|
||||
@@ -17354,7 +17374,7 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@4.9.5)
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
eslint: 7.32.0
|
||||
eslint-import-resolver-node: 0.3.7
|
||||
transitivePeerDependencies:
|
||||
@@ -17382,7 +17402,7 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.61.0(eslint@8.44.0)(typescript@4.9.5)
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
eslint: 8.44.0
|
||||
eslint-import-resolver-node: 0.3.7
|
||||
transitivePeerDependencies:
|
||||
@@ -17455,7 +17475,7 @@ packages:
|
||||
array-includes: 3.1.6
|
||||
array.prototype.flat: 1.3.1
|
||||
array.prototype.flatmap: 1.3.1
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
doctrine: 2.1.0
|
||||
eslint: 7.32.0
|
||||
eslint-import-resolver-node: 0.3.7
|
||||
@@ -17487,7 +17507,7 @@ packages:
|
||||
array-includes: 3.1.6
|
||||
array.prototype.flat: 1.3.1
|
||||
array.prototype.flatmap: 1.3.1
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.44.0
|
||||
eslint-import-resolver-node: 0.3.7
|
||||
@@ -18758,7 +18778,7 @@ packages:
|
||||
debug:
|
||||
optional: true
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
|
||||
/follow-redirects@1.15.2(debug@4.3.4):
|
||||
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
||||
@@ -19124,7 +19144,7 @@ packages:
|
||||
gatsby-telemetry: 2.15.0
|
||||
hosted-git-info: 3.0.8
|
||||
is-valid-path: 0.1.1
|
||||
joi: 17.8.3
|
||||
joi: 17.9.2
|
||||
lodash: 4.17.21
|
||||
meant: 1.0.3
|
||||
node-fetch: 2.6.9
|
||||
@@ -19587,7 +19607,7 @@ packages:
|
||||
css-minimizer-webpack-plugin: 2.0.0(webpack@5.88.1)
|
||||
css.escape: 1.5.1
|
||||
date-fns: 2.30.0
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
deepmerge: 4.3.0
|
||||
del: 5.1.0
|
||||
detect-port: 1.5.1
|
||||
@@ -19633,7 +19653,7 @@ packages:
|
||||
invariant: 2.2.4
|
||||
is-relative: 1.0.0
|
||||
is-relative-url: 3.0.0
|
||||
joi: 17.8.3
|
||||
joi: 17.9.2
|
||||
json-loader: 0.5.7
|
||||
latest-version: 5.1.0
|
||||
lodash: 4.17.21
|
||||
@@ -20805,23 +20825,13 @@ packages:
|
||||
dependencies:
|
||||
'@types/express': 4.17.17
|
||||
'@types/http-proxy': 1.17.10
|
||||
http-proxy: 1.18.1
|
||||
http-proxy: 1.18.1(debug@3.2.7)
|
||||
is-glob: 4.0.3
|
||||
is-plain-obj: 3.0.0
|
||||
micromatch: 4.0.5
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
/http-proxy@1.18.1:
|
||||
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
dependencies:
|
||||
eventemitter3: 4.0.7
|
||||
follow-redirects: 1.15.2(debug@4.3.4)
|
||||
requires-port: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
/http-proxy@1.18.1(debug@3.2.7):
|
||||
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@@ -22990,15 +23000,6 @@ packages:
|
||||
resolution: {integrity: sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ==}
|
||||
dev: true
|
||||
|
||||
/joi@17.8.3:
|
||||
resolution: {integrity: sha512-q5Fn6Tj/jR8PfrLrx4fpGH4v9qM6o+vDUfD4/3vxxyg34OmKcNqYZ1qn2mpLza96S8tL0p0rIw2gOZX+/cTg9w==}
|
||||
dependencies:
|
||||
'@hapi/hoek': 9.3.0
|
||||
'@hapi/topo': 5.1.0
|
||||
'@sideway/address': 4.1.4
|
||||
'@sideway/formula': 3.0.1
|
||||
'@sideway/pinpoint': 2.0.0
|
||||
|
||||
/joi@17.9.2:
|
||||
resolution: {integrity: sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==}
|
||||
dependencies:
|
||||
@@ -23789,7 +23790,7 @@ packages:
|
||||
dependencies:
|
||||
async: 0.9.2
|
||||
commondir: 1.0.1
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
lodash: 4.17.21
|
||||
semver: 5.7.1
|
||||
strong-globalize: 4.1.3
|
||||
@@ -23802,7 +23803,7 @@ packages:
|
||||
resolution: {integrity: sha512-vDRR4gqkvGOEXh5yL383xGuGxUW9xtF+NCY6/lJu1VAgupKltZxEx3Vw+L3nsGvQrlkJTSmiK3jk72qxkoBtbw==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
lodash: 4.17.21
|
||||
loopback-swagger: 5.9.0
|
||||
strong-globalize: 4.1.3
|
||||
@@ -23817,7 +23818,7 @@ packages:
|
||||
dependencies:
|
||||
async: 2.6.4
|
||||
bson: 1.1.6
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
loopback-connector: 4.11.1
|
||||
mongodb: 3.6.9
|
||||
strong-globalize: 4.1.3
|
||||
@@ -23861,7 +23862,7 @@ packages:
|
||||
dependencies:
|
||||
async: 2.6.4
|
||||
bluebird: 3.7.2
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
depd: 1.1.2
|
||||
inflection: 1.13.4
|
||||
lodash: 4.17.21
|
||||
@@ -23885,7 +23886,7 @@ packages:
|
||||
resolution: {integrity: sha512-p0qSzuuX7eATe5Bxy+RqCj3vSfSFfdCtqyf3yuC+DpchMvgal33XlhEi2UmywyK/Ym28oVnZxxWmfrwFMzSwLQ==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
@@ -23895,7 +23896,7 @@ packages:
|
||||
engines: {node: '>=8.9'}
|
||||
dependencies:
|
||||
async: 2.6.4
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
strong-globalize: 4.1.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -23906,7 +23907,7 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
async: 2.6.4
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
ejs: 2.7.4
|
||||
lodash: 4.17.21
|
||||
strong-globalize: 4.1.3
|
||||
@@ -25281,6 +25282,28 @@ packages:
|
||||
- aws-crt
|
||||
dev: false
|
||||
|
||||
/mongodb@5.6.0:
|
||||
resolution: {integrity: sha512-z8qVs9NfobHJm6uzK56XBZF8XwM9H294iRnB7wNjF0SnY93si5HPziIJn+qqvUR5QOff/4L0gCD6SShdR/GtVQ==}
|
||||
engines: {node: '>=14.20.1'}
|
||||
peerDependencies:
|
||||
'@aws-sdk/credential-providers': ^3.201.0
|
||||
mongodb-client-encryption: '>=2.3.0 <3'
|
||||
snappy: ^7.2.2
|
||||
peerDependenciesMeta:
|
||||
'@aws-sdk/credential-providers':
|
||||
optional: true
|
||||
mongodb-client-encryption:
|
||||
optional: true
|
||||
snappy:
|
||||
optional: true
|
||||
dependencies:
|
||||
bson: 5.4.0
|
||||
mongodb-connection-string-url: 2.6.0
|
||||
socks: 2.7.1
|
||||
optionalDependencies:
|
||||
saslprep: 1.0.3
|
||||
dev: true
|
||||
|
||||
/mongodb@5.7.0:
|
||||
resolution: {integrity: sha512-zm82Bq33QbqtxDf58fLWBwTjARK3NSvKYjyz997KSy6hpat0prjeX/kxjbPVyZY60XYPDNETaHkHJI2UCzSLuw==}
|
||||
engines: {node: '>=14.20.1'}
|
||||
@@ -25453,6 +25476,12 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
/nanoid@4.0.2:
|
||||
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
|
||||
engines: {node: ^14 || ^16 || >=18}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/nanomatch@1.2.13:
|
||||
resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -25501,7 +25530,7 @@ packages:
|
||||
engines: {node: '>= 4.4.x'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
iconv-lite: 0.4.24
|
||||
sax: 1.2.4
|
||||
transitivePeerDependencies:
|
||||
@@ -33625,7 +33654,7 @@ packages:
|
||||
/webpack-virtual-modules@0.2.2:
|
||||
resolution: {integrity: sha512-kDUmfm3BZrei0y+1NTHJInejzxfhtU8eDj2M7OKb2IWrPFAeO1SOH2KuQ68MSZu9IGEHcxbkKKR1v18FrUSOmA==}
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -33633,7 +33662,7 @@ packages:
|
||||
/webpack-virtual-modules@0.3.2:
|
||||
resolution: {integrity: sha512-RXQXioY6MhzM4CNQwmBwKXYgBs6ulaiQ8bkNQEl2J6Z+V+s7lgl/wGvaI/I0dLnYKB8cKsxQc17QOAVIphPLDw==}
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
debug: 3.2.7(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
||||
@@ -12,4 +12,5 @@ packages:
|
||||
- 'tools/crowdin'
|
||||
- 'tools/scripts/build'
|
||||
- 'tools/scripts/seed'
|
||||
- 'tools/scripts/seed-exams'
|
||||
- 'tools/ui-components'
|
||||
|
||||
33
tools/scripts/seed-exams/README.md
Normal file
33
tools/scripts/seed-exams/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
## WARNING: Never change any of the ID's or delete anything. Mark things as deprecated instead.
|
||||
|
||||
### How to create a new exam:
|
||||
|
||||
1. Add the exam file in the `exams` folder
|
||||
|
||||
2. Add the exam meta data:
|
||||
|
||||
- `_id`: This should match the `id` in the exam challenge markdown file,
|
||||
- `title`: This should match the `title` in the exam challenge markdown file,
|
||||
- `numberOfQuestionsInExam`: This is how many questions will be given to campers who take the exam. It must be less than or equal to the length of the `questions` array.
|
||||
- `passingPercent`: Percent of questions needed to get correct to pass the exam
|
||||
- `prerequisites`: Array of challenges that are required to complete before being able to take the exam. Each should have an `id` and `title` that match what's in their challenge markdown file
|
||||
- `questions`: Array of exam questions
|
||||
|
||||
3. Add the exam questions to the `questions` array:
|
||||
|
||||
- `question`: The exam question
|
||||
- `wrongAnswers`: Array of answers. There should be at least 4, but 6-7 would be ideal.
|
||||
- `correctAnswers`: Array of answers. There should be at least 1, but 2 or more would be ideal when possible.
|
||||
|
||||
4. `wrongAnswers` and `correctAnwers` arrays:
|
||||
|
||||
- `answer`: This is one of the multiple choice options
|
||||
|
||||
5. Add the ID's:
|
||||
|
||||
- Change the `examPath` variable in the `add-nano-ids.js` file to the name of the new exam file
|
||||
- Run it with `node add-nano-ids.js`. It will add an `id` to each `question`, and each `answer`.
|
||||
|
||||
Add a `deprecated: true` property to any of the `questions`, `wrongAnswers`, or `correctAnwers`. Any that include this will be omitted when generating an exam.
|
||||
|
||||
The exam files in this folder are not used in production. Never push real exam questions to GitHub or anywhere public. These exams are for local development and testing. To seed the real exams to staging/production databases, replace the example exams here with the real exams, connect to the desired database, and run the `create-exams.js` script.
|
||||
54
tools/scripts/seed-exams/add-nano-ids.js
Normal file
54
tools/scripts/seed-exams/add-nano-ids.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Script that will add `id` fields to the `questions`, `wrong_answers`, and
|
||||
* `correct_answers` of an exam file where the `id` doesn't exist.
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
const newId = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 10);
|
||||
|
||||
// Change this to the path of the file you want to add id's to
|
||||
const examPath = './exams/example-certification-exam.yml';
|
||||
const examFile = readFileSync(examPath, { encoding: 'utf-8' });
|
||||
const examJson = yaml.load(examFile);
|
||||
|
||||
// The strangeness of how the id's are added below is to keep the id at the top
|
||||
// of each object when rewriting the YAML at the end
|
||||
const newQuestions = [];
|
||||
examJson.questions?.forEach(question => {
|
||||
let newQuestion;
|
||||
|
||||
if (!question.id) {
|
||||
newQuestion = { id: newId(), ...question };
|
||||
} else {
|
||||
newQuestion = { ...question };
|
||||
}
|
||||
|
||||
let newWrongAnswers = [];
|
||||
question.wrongAnswers?.forEach(answer => {
|
||||
if (!answer.id) {
|
||||
newWrongAnswers.push({ id: newId(), ...answer });
|
||||
} else {
|
||||
newWrongAnswers.push(answer);
|
||||
}
|
||||
});
|
||||
|
||||
let newCorrectAnswers = [];
|
||||
question.correctAnswers?.forEach(answer => {
|
||||
if (!answer.id) {
|
||||
newCorrectAnswers.push({ id: newId(), ...answer });
|
||||
} else {
|
||||
newCorrectAnswers.push(answer);
|
||||
}
|
||||
});
|
||||
|
||||
newQuestion.wrongAnswers = newWrongAnswers;
|
||||
newQuestion.correctAnswers = newCorrectAnswers;
|
||||
newQuestions.push(newQuestion);
|
||||
});
|
||||
|
||||
examJson.questions = newQuestions;
|
||||
|
||||
const yamlStr = yaml.dump(examJson);
|
||||
writeFileSync(examPath, yamlStr, 'utf8');
|
||||
76
tools/scripts/seed-exams/create-exams.js
Executable file
76
tools/scripts/seed-exams/create-exams.js
Executable file
@@ -0,0 +1,76 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import path, { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import dotenv from 'dotenv';
|
||||
import yaml from 'js-yaml';
|
||||
import { MongoClient, ObjectId } from 'mongodb';
|
||||
|
||||
import { validateExamSchema } from './exam-schema.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
|
||||
const { MONGOHQ_URL } = process.env;
|
||||
|
||||
// Only these will be added to or updated in the database
|
||||
const examFilenames = [
|
||||
'foundational-c-sharp-with-microsoft-certification-exam.yml',
|
||||
'example-certification-exam.yml'
|
||||
];
|
||||
|
||||
const client = new MongoClient(MONGOHQ_URL, { useUnifiedTopology: true });
|
||||
|
||||
const db = client.db('freecodecamp');
|
||||
const exams = db.collection('Exam');
|
||||
|
||||
function handleError(err, client) {
|
||||
if (err) {
|
||||
console.error('Oh noes!! Error seeding exams.');
|
||||
console.error(err);
|
||||
try {
|
||||
client.close();
|
||||
} catch (e) {
|
||||
// no-op
|
||||
} finally {
|
||||
/* eslint-disable-next-line no-process-exit */
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const seed = async () => {
|
||||
for (const filename of examFilenames) {
|
||||
try {
|
||||
const examPath = join('./exams', filename);
|
||||
const examFile = readFileSync(examPath, { encoding: 'utf-8' });
|
||||
const examJson = yaml.load(examFile);
|
||||
const validExam = validateExamSchema(examJson);
|
||||
|
||||
if (validExam.error) {
|
||||
throw new Error(
|
||||
`Invalid exam schema for '${filename}': ${validExam.error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
examJson._id = new ObjectId(examJson._id);
|
||||
|
||||
// Update existing database object, or create new if it doesn't exist
|
||||
await exams.updateOne(
|
||||
{ _id: examJson._id },
|
||||
{ $set: examJson },
|
||||
{ upsert: true }
|
||||
);
|
||||
|
||||
console.log(`'${examJson.title}' added to exams database.`);
|
||||
} catch (err) {
|
||||
handleError(err, client);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Finished seeding exams.');
|
||||
};
|
||||
|
||||
seed()
|
||||
.then(() => client.close())
|
||||
.catch(err => handleError(err, client));
|
||||
86
tools/scripts/seed-exams/exam-schema.js
Normal file
86
tools/scripts/seed-exams/exam-schema.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import Joi from 'joi';
|
||||
import JoiObjectId from 'joi-objectid';
|
||||
|
||||
Joi.objectId = JoiObjectId(Joi);
|
||||
|
||||
const nanoIdRE = new RegExp('[a-z0-9]{10}');
|
||||
|
||||
const PrerequisitesJoi = Joi.object().keys({
|
||||
id: Joi.objectId().required(),
|
||||
title: Joi.string()
|
||||
});
|
||||
|
||||
const AnswerJoi = Joi.object().keys({
|
||||
id: Joi.string().regex(nanoIdRE).required(),
|
||||
deprecated: Joi.bool(),
|
||||
answer: Joi.string().required()
|
||||
});
|
||||
|
||||
const QuestionJoi = Joi.object().keys({
|
||||
id: Joi.string().regex(nanoIdRE).required(),
|
||||
question: Joi.string().required(),
|
||||
deprecated: Joi.bool(),
|
||||
wrongAnswers: Joi.array()
|
||||
.items(AnswerJoi)
|
||||
.required()
|
||||
.custom((value, helpers) => {
|
||||
const nonDeprecatedCount = value.reduce(
|
||||
(count, answer) => (answer.deprecated ? count : count + 1),
|
||||
0
|
||||
);
|
||||
const minimumAnswers = 4;
|
||||
|
||||
if (nonDeprecatedCount < minimumAnswers) {
|
||||
return helpers.message(
|
||||
`'wrongAnswers' must have at least ${minimumAnswers} non-deprecated answers.`
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}),
|
||||
correctAnswers: Joi.array()
|
||||
.items(AnswerJoi)
|
||||
.required()
|
||||
.custom((value, helpers) => {
|
||||
const nonDeprecatedCount = value.reduce(
|
||||
(count, answer) => (answer.deprecated ? count : count + 1),
|
||||
0
|
||||
);
|
||||
const minimumAnswers = 1;
|
||||
|
||||
if (nonDeprecatedCount < minimumAnswers) {
|
||||
return helpers.message(
|
||||
`'correctAnswers' must have at least ${minimumAnswers} non-deprecated answer.`
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
})
|
||||
});
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
// TODO: make sure _id and title match what's in the challenge markdown file
|
||||
_id: Joi.objectId().required(),
|
||||
title: Joi.string().required(),
|
||||
numberOfQuestionsInExam: Joi.number()
|
||||
.min(1)
|
||||
.max(
|
||||
Joi.ref('questions', {
|
||||
adjust: questions => {
|
||||
const nonDeprecatedCount = questions.reduce(
|
||||
(count, question) => (question.deprecated ? count : count + 1),
|
||||
0
|
||||
);
|
||||
return nonDeprecatedCount;
|
||||
}
|
||||
})
|
||||
)
|
||||
.required(),
|
||||
passingPercent: Joi.number().min(0).max(100).required(),
|
||||
prerequisites: Joi.array().items(PrerequisitesJoi),
|
||||
questions: Joi.array().items(QuestionJoi).min(1).required()
|
||||
});
|
||||
|
||||
export const validateExamSchema = exam => {
|
||||
return schema.validate(exam);
|
||||
};
|
||||
166
tools/scripts/seed-exams/exams/example-certification-exam.yml
Normal file
166
tools/scripts/seed-exams/exams/example-certification-exam.yml
Normal file
@@ -0,0 +1,166 @@
|
||||
_id: 645147516c245de4d11eb7ba
|
||||
title: Example Certification Exam
|
||||
numberOfQuestionsInExam: 5
|
||||
passingPercent: 70
|
||||
questions:
|
||||
- id: vjxyn2ngzs
|
||||
question: Which of the following is NOT a JavaScript data type?
|
||||
wrongAnswers:
|
||||
- id: h1x269fts9
|
||||
answer: Integer
|
||||
- id: 3x3j6ts2sb
|
||||
answer: Boolean
|
||||
- id: 9g4ix2tr1z
|
||||
answer: Symbol
|
||||
- id: n1rfllrvvy
|
||||
answer: String
|
||||
- id: 33oq8q3aio
|
||||
answer: Bigint
|
||||
- id: 7w1gaddxup
|
||||
answer: Undefined
|
||||
- id: ywgqr2wp6i
|
||||
answer: Object
|
||||
correctAnswers:
|
||||
- id: ydmnlkjviv
|
||||
answer: Array
|
||||
- id: sy2trjs7p8
|
||||
answer: Float
|
||||
- id: lpmv63p5sv
|
||||
question: What does the `NaN` keyword represent in JavaScript?
|
||||
wrongAnswers:
|
||||
- id: 3cxs9as4bt
|
||||
answer: Not a Node
|
||||
- id: axn8omt3zf
|
||||
answer: Null and None
|
||||
- id: 7ssyazt4zu
|
||||
answer: No Assignment Named
|
||||
- id: do4cm37x7y
|
||||
answer: Negative Assertion Needed
|
||||
- id: r2hbfnrodz
|
||||
answer: Never Allow Null
|
||||
correctAnswers:
|
||||
- id: ah5kkj38s1
|
||||
answer: Not a Number
|
||||
- id: g2hudvmgoj
|
||||
question: >-
|
||||
Which method is used to remove the last element from an array in
|
||||
JavaScript?
|
||||
wrongAnswers:
|
||||
- id: jrxd1hjv63
|
||||
answer: '`shift()`'
|
||||
- id: udc3kecrqn
|
||||
answer: '`unshift()`'
|
||||
- id: a12aysp2et
|
||||
answer: '`slice()`'
|
||||
- id: xljr0lkb0n
|
||||
answer: '`splice()`'
|
||||
- id: qe4z3kxgyz
|
||||
answer: '`concat()`'
|
||||
correctAnswers:
|
||||
- id: gwddf01p89
|
||||
answer: '`pop()`'
|
||||
- id: 7tfdjebrnq
|
||||
question: >-
|
||||
What is the correct way to create a new instance of an object in
|
||||
JavaScript?
|
||||
wrongAnswers:
|
||||
- id: 5bygwpaz4d
|
||||
answer: '`Object.create(MyObject);`'
|
||||
- id: h5s3304gbl
|
||||
answer: '`MyObject.create();`'
|
||||
- id: mdsjsjpi9t
|
||||
answer: '`Object.new(MyObject);`'
|
||||
- id: 3o8otd8j9h
|
||||
answer: '`MyObject.new();`'
|
||||
- id: h4ozml27zn
|
||||
answer: '`MyObject.instance();`'
|
||||
deprecated: true
|
||||
correctAnswers:
|
||||
- id: qbfhqmnedn
|
||||
answer: '`new MyObject();`'
|
||||
- id: hx0gxr5yjc
|
||||
question: What is the purpose of the `querySelectorAll` method in JavaScript?
|
||||
deprecated: true
|
||||
wrongAnswers:
|
||||
- id: 71iry42cy1
|
||||
answer: To select all elements with a specific ID
|
||||
- id: jjyk5e9e1l
|
||||
answer: To select all elements of a specific tag name
|
||||
- id: k5tpc2o2q8
|
||||
answer: To select all child elements of a specific parent
|
||||
- id: s3cm1tq4wt
|
||||
answer: To select all elements with a specific attribute
|
||||
- id: l1gtuheh87
|
||||
answer: To select elements based on their position in the DOM
|
||||
correctAnswers:
|
||||
- id: e6xz6zdpxr
|
||||
answer: To select all elements with a specific class name
|
||||
- id: eigcj3e0mg
|
||||
question: Which operator is used to compare two values for equality in JavaScript?
|
||||
wrongAnswers:
|
||||
- id: ltgdcoyonb
|
||||
answer: '`!=`'
|
||||
- id: f3szfel7x6
|
||||
answer: '`<`'
|
||||
- id: b0meyxhvtu
|
||||
answer: '`>`'
|
||||
- id: 31dd0pte4i
|
||||
answer: '`<=`'
|
||||
- id: 9u8bi7wqa2
|
||||
answer: '`>=`'
|
||||
correctAnswers:
|
||||
- id: fmo9sof6v2
|
||||
answer: '`==`'
|
||||
deprecated: true
|
||||
- id: tl6eo06r4x
|
||||
answer: '`===`'
|
||||
- id: l5f2beg2ih
|
||||
question: >-
|
||||
Which of the following statements is used to terminate a loop in
|
||||
JavaScript?
|
||||
wrongAnswers:
|
||||
- id: kqi1fg8vtm
|
||||
answer: stop
|
||||
- id: a2clf7fmyx
|
||||
answer: exit
|
||||
- id: v58wx865e7
|
||||
answer: return
|
||||
- id: 5mgeaetnf0
|
||||
answer: halt
|
||||
- id: 2u73kyry6o
|
||||
answer: skip
|
||||
correctAnswers:
|
||||
- id: 7j046bkjmf
|
||||
answer: break
|
||||
- id: atuq2zwg4h
|
||||
question: What is the correct syntax to define a function in JavaScript?
|
||||
wrongAnswers:
|
||||
- id: dusl68wx2o
|
||||
answer: '`function myFunction {}`'
|
||||
- id: dlwjt59a5s
|
||||
answer: '`def myFunction():`'
|
||||
- id: 98p8ai27ko
|
||||
answer: '`function = myFunction {}`'
|
||||
- id: 7ciq8sood9
|
||||
answer: '`function myFunction():`'
|
||||
correctAnswers:
|
||||
- id: wc3h7uc4zn
|
||||
answer: '`function myFunction() {}`'
|
||||
- id: 3ne0ju7yzq
|
||||
question: >-
|
||||
Which of the following methods is used to add a new element to the end of
|
||||
an array in JavaScript?
|
||||
wrongAnswers:
|
||||
- id: fytb9yvbug
|
||||
answer: shift()
|
||||
- id: wnc31qc1ie
|
||||
answer: unshift()
|
||||
- id: 22mi7j64h5
|
||||
answer: pop()
|
||||
- id: da96ukvtol
|
||||
answer: splice()
|
||||
- id: gc6elnmtzw
|
||||
answer: concat()
|
||||
correctAnswers:
|
||||
- id: pymbxhyn12
|
||||
answer: push()
|
||||
@@ -0,0 +1,179 @@
|
||||
_id: 647e22d18acb466c97ccbef8
|
||||
title: Foundational C# with Microsoft Certification Exam
|
||||
numberOfQuestionsInExam: 5
|
||||
passingPercent: 75
|
||||
prerequisites:
|
||||
- id: 647f85d407d29547b3bee1bb
|
||||
title: Trophy - Write Your First Code Using C#
|
||||
- id: 647f87dc07d29547b3bee1bf
|
||||
title: Trophy - Create and Run Simple C# Console Applications
|
||||
- id: 647f882207d29547b3bee1c0
|
||||
title: Trophy - Add Logic to C# Console Applications
|
||||
- id: 647f867a07d29547b3bee1bc
|
||||
title: Trophy - Work with Variable Data in C# Console Applications
|
||||
- id: 647f877f07d29547b3bee1be
|
||||
title: Trophy - Create Methods in C# Console Applications
|
||||
- id: 647f86ff07d29547b3bee1bd
|
||||
title: Trophy - Debug C# Console Applications
|
||||
questions:
|
||||
- id: vjxyn2ngzs
|
||||
question: Which of the following is NOT a JavaScript data type?
|
||||
wrongAnswers:
|
||||
- id: 5y7xv9ppc1
|
||||
answer: Integer
|
||||
- id: 3x3j6ts2sb
|
||||
answer: Boolean
|
||||
- id: 9g4ix2tr1z
|
||||
answer: Symbol
|
||||
- id: n1rfllrvvy
|
||||
answer: String
|
||||
- id: 33oq8q3aio
|
||||
answer: Bigint
|
||||
- id: 7w1gaddxup
|
||||
answer: Undefined
|
||||
- id: ywgqr2wp6i
|
||||
answer: Object
|
||||
correctAnswers:
|
||||
- id: ydmnlkjviv
|
||||
answer: Array
|
||||
- id: sy2trjs7p8
|
||||
answer: Float
|
||||
- id: lpmv63p5sv
|
||||
question: What does the `NaN` keyword represent in JavaScript?
|
||||
wrongAnswers:
|
||||
- id: 3cxs9as4bt
|
||||
answer: Not a Node
|
||||
- id: axn8omt3zf
|
||||
answer: Null and None
|
||||
- id: 7ssyazt4zu
|
||||
answer: No Assignment Named
|
||||
- id: do4cm37x7y
|
||||
answer: Negative Assertion Needed
|
||||
- id: r2hbfnrodz
|
||||
answer: Never Allow Null
|
||||
correctAnswers:
|
||||
- id: ah5kkj38s1
|
||||
answer: Not a Number
|
||||
- id: g2hudvmgoj
|
||||
question: >-
|
||||
Which method is used to remove the last element from an array in
|
||||
JavaScript?
|
||||
wrongAnswers:
|
||||
- id: jrxd1hjv63
|
||||
answer: '`shift()`'
|
||||
- id: udc3kecrqn
|
||||
answer: '`unshift()`'
|
||||
- id: a12aysp2et
|
||||
answer: '`slice()`'
|
||||
- id: xljr0lkb0n
|
||||
answer: '`splice()`'
|
||||
- id: qe4z3kxgyz
|
||||
answer: '`concat()`'
|
||||
correctAnswers:
|
||||
- id: gwddf01p89
|
||||
answer: '`pop()`'
|
||||
- id: 7tfdjebrnq
|
||||
question: >-
|
||||
What is the correct way to create a new instance of an object in
|
||||
JavaScript?
|
||||
wrongAnswers:
|
||||
- id: 5bygwpaz4d
|
||||
answer: '`Object.create(MyObject);`'
|
||||
- id: h5s3304gbl
|
||||
answer: '`MyObject.create();`'
|
||||
- id: mdsjsjpi9t
|
||||
answer: '`Object.new(MyObject);`'
|
||||
- id: 3o8otd8j9h
|
||||
answer: '`MyObject.new();`'
|
||||
- id: h4ozml27zn
|
||||
answer: '`MyObject.instance();`'
|
||||
deprecated: true
|
||||
correctAnswers:
|
||||
- id: qbfhqmnedn
|
||||
answer: '`new MyObject();`'
|
||||
- id: hx0gxr5yjc
|
||||
question: What is the purpose of the `querySelectorAll` method in JavaScript?
|
||||
deprecated: true
|
||||
wrongAnswers:
|
||||
- id: 71iry42cy1
|
||||
answer: To select all elements with a specific ID
|
||||
- id: jjyk5e9e1l
|
||||
answer: To select all elements of a specific tag name
|
||||
- id: k5tpc2o2q8
|
||||
answer: To select all child elements of a specific parent
|
||||
- id: s3cm1tq4wt
|
||||
answer: To select all elements with a specific attribute
|
||||
- id: l1gtuheh87
|
||||
answer: To select elements based on their position in the DOM
|
||||
correctAnswers:
|
||||
- id: e6xz6zdpxr
|
||||
answer: To select all elements with a specific class name
|
||||
- id: eigcj3e0mg
|
||||
question: Which operator is used to compare two values for equality in JavaScript?
|
||||
wrongAnswers:
|
||||
- id: ltgdcoyonb
|
||||
answer: '`!=`'
|
||||
- id: f3szfel7x6
|
||||
answer: '`<`'
|
||||
- id: b0meyxhvtu
|
||||
answer: '`>`'
|
||||
- id: 31dd0pte4i
|
||||
answer: '`<=`'
|
||||
- id: 9u8bi7wqa2
|
||||
answer: '`>=`'
|
||||
correctAnswers:
|
||||
- id: fmo9sof6v2
|
||||
answer: '`==`'
|
||||
deprecated: true
|
||||
- id: tl6eo06r4x
|
||||
answer: '`===`'
|
||||
- id: l5f2beg2ih
|
||||
question: >-
|
||||
Which of the following statements is used to terminate a loop in
|
||||
JavaScript?
|
||||
wrongAnswers:
|
||||
- id: kqi1fg8vtm
|
||||
answer: stop
|
||||
- id: a2clf7fmyx
|
||||
answer: exit
|
||||
- id: v58wx865e7
|
||||
answer: return
|
||||
- id: 5mgeaetnf0
|
||||
answer: halt
|
||||
- id: 2u73kyry6o
|
||||
answer: skip
|
||||
correctAnswers:
|
||||
- id: 7j046bkjmf
|
||||
answer: break
|
||||
- id: atuq2zwg4h
|
||||
question: What is the correct syntax to define a function in JavaScript?
|
||||
wrongAnswers:
|
||||
- id: dusl68wx2o
|
||||
answer: '`function myFunction {}`'
|
||||
- id: dlwjt59a5s
|
||||
answer: '`def myFunction():`'
|
||||
- id: 98p8ai27ko
|
||||
answer: '`function = myFunction {}`'
|
||||
- id: 7ciq8sood9
|
||||
answer: '`function myFunction():`'
|
||||
correctAnswers:
|
||||
- id: wc3h7uc4zn
|
||||
answer: '`function myFunction() {}`'
|
||||
- id: 3ne0ju7yzq
|
||||
question: >-
|
||||
Which of the following methods is used to add a new element to the end of
|
||||
an array in JavaScript?
|
||||
wrongAnswers:
|
||||
- id: fytb9yvbug
|
||||
answer: shift()
|
||||
- id: wnc31qc1ie
|
||||
answer: unshift()
|
||||
- id: 22mi7j64h5
|
||||
answer: pop()
|
||||
- id: da96ukvtol
|
||||
answer: splice()
|
||||
- id: gc6elnmtzw
|
||||
answer: concat()
|
||||
correctAnswers:
|
||||
- id: pymbxhyn12
|
||||
answer: push()
|
||||
31
tools/scripts/seed-exams/package.json
Normal file
31
tools/scripts/seed-exams/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@freecodecamp/seed-exams",
|
||||
"version": "0.0.1",
|
||||
"description": "Seed certification exams questions to database",
|
||||
"license": "BSD-3-Clause",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"pnpm": "8"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
|
||||
},
|
||||
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
|
||||
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||
"main": "none",
|
||||
"devDependencies": {
|
||||
"dotenv": "16.3.1",
|
||||
"joi": "17.9.2",
|
||||
"joi-objectid": "3.0.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"mongodb": "5.6.0",
|
||||
"nanoid": "4.0.2"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
Reference in New Issue
Block a user