AI bugbash
This commit is contained in:
parent
a97ca3852e
commit
ea84a911f4
10 changed files with 384 additions and 61 deletions
|
|
@ -6,6 +6,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"dev": "nodemon server.js",
|
"dev": "nodemon server.js",
|
||||||
|
"validate:data": "node validate-question-bank.js",
|
||||||
"test": "node --test"
|
"test": "node --test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,11 @@ app.get("/api/admin/questions", async (req, res) => {
|
||||||
app.post("/api/admin/questions", async (req, res) => {
|
app.post("/api/admin/questions", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const data = await readData();
|
const data = await readData();
|
||||||
|
if (data.some((q) => q.source === req.body.source)) {
|
||||||
|
return res
|
||||||
|
.status(409)
|
||||||
|
.json({ error: "Source identifier already exists" });
|
||||||
|
}
|
||||||
const maxId = Math.max(...data.map((q) => q.id), 0);
|
const maxId = Math.max(...data.map((q) => q.id), 0);
|
||||||
|
|
||||||
const newQuestion = { id: maxId + 1, ...req.body };
|
const newQuestion = { id: maxId + 1, ...req.body };
|
||||||
|
|
@ -156,7 +161,16 @@ app.put("/api/admin/questions/:id", async (req, res) => {
|
||||||
return res.status(404).json({ error: "Question not found" });
|
return res.status(404).json({ error: "Question not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
data[index] = { ...data[index], ...req.body };
|
const updatedQuestion = { ...data[index], ...req.body };
|
||||||
|
if (
|
||||||
|
data.some((q, i) => i !== index && q.source === updatedQuestion.source)
|
||||||
|
) {
|
||||||
|
return res
|
||||||
|
.status(409)
|
||||||
|
.json({ error: "Source identifier already exists" });
|
||||||
|
}
|
||||||
|
|
||||||
|
data[index] = updatedQuestion;
|
||||||
await writeData(data);
|
await writeData(data);
|
||||||
|
|
||||||
res.json(data[index]);
|
res.json(data[index]);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ process.env.DATA_PATH = dataPath;
|
||||||
process.env.PICS_PATH = picsPath;
|
process.env.PICS_PATH = picsPath;
|
||||||
|
|
||||||
const { app, isUnsafeFilename } = require("./server");
|
const { app, isUnsafeFilename } = require("./server");
|
||||||
|
const { validateQuestionBank } = require("./validate-question-bank");
|
||||||
|
|
||||||
const seedQuestions = [
|
const seedQuestions = [
|
||||||
{
|
{
|
||||||
|
|
@ -107,6 +108,61 @@ test("PUT updates an existing question", async () => {
|
||||||
assert.equal(updated.source, "2016/m1/1");
|
assert.equal(updated.source, "2016/m1/1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST rejects a duplicate source without changing the bank", async () => {
|
||||||
|
const before = fs.readFileSync(dataPath, "utf8");
|
||||||
|
const res = await sendJson("/api/admin/questions", "POST", seedQuestions[0]);
|
||||||
|
assert.equal(res.status, 409);
|
||||||
|
assert.equal((await res.json()).error, "Source identifier already exists");
|
||||||
|
assert.equal(fs.readFileSync(dataPath, "utf8"), before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("PUT rejects another question's source without changing the bank", async () => {
|
||||||
|
const before = fs.readFileSync(dataPath, "utf8");
|
||||||
|
const res = await sendJson("/api/admin/questions/2", "PUT", {
|
||||||
|
source: seedQuestions[0].source,
|
||||||
|
description: "must not be saved",
|
||||||
|
});
|
||||||
|
assert.equal(res.status, 409);
|
||||||
|
assert.equal((await res.json()).error, "Source identifier already exists");
|
||||||
|
assert.equal(fs.readFileSync(dataPath, "utf8"), before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("PUT allows a question to keep its own source", async () => {
|
||||||
|
const res = await sendJson("/api/admin/questions/1", "PUT", {
|
||||||
|
source: seedQuestions[0].source,
|
||||||
|
description: "updated",
|
||||||
|
});
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
assert.equal((await res.json()).description, "updated");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("correcting October 2025 question 6 leaves exactly one result for 5 and 6", async () => {
|
||||||
|
const questions = Array.from({ length: 15 }, (_, i) => ({
|
||||||
|
...seedQuestions[0],
|
||||||
|
id: 683 + i,
|
||||||
|
source: `2025/2/${i + 1}`,
|
||||||
|
}));
|
||||||
|
questions[5].source = "2025/2/5";
|
||||||
|
questions[5].image = "202526.JPG";
|
||||||
|
fs.writeFileSync(dataPath, JSON.stringify(questions, null, 2));
|
||||||
|
|
||||||
|
const res = await sendJson("/api/admin/questions/688", "PUT", {
|
||||||
|
source: "2025/2/6",
|
||||||
|
});
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
const bank = await (await api("/api/fizika")).json();
|
||||||
|
assert.deepEqual(
|
||||||
|
bank.filter((q) => q.source === "2025/2/5").map((q) => q.id),
|
||||||
|
[687],
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
bank.filter((q) => q.source === "2025/2/6").map((q) => q.id),
|
||||||
|
[688],
|
||||||
|
);
|
||||||
|
assert.equal(bank.find((q) => q.id === 688).image, "202526.JPG");
|
||||||
|
assert.deepEqual(validateQuestionBank(bank), []);
|
||||||
|
});
|
||||||
|
|
||||||
test("PUT returns 404 for a missing question", async () => {
|
test("PUT returns 404 for a missing question", async () => {
|
||||||
const res = await sendJson("/api/admin/questions/999", "PUT", { a: "x" });
|
const res = await sendJson("/api/admin/questions/999", "PUT", { a: "x" });
|
||||||
assert.equal(res.status, 404);
|
assert.equal(res.status, 404);
|
||||||
|
|
|
||||||
98
backend/validate-question-bank.js
Normal file
98
backend/validate-question-bank.js
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
// Validate a complete import separately from individual admin edits, which
|
||||||
|
// legitimately build up a paper one question at a time.
|
||||||
|
function validateQuestionBank(questions, { paper, questionCount = 15 } = {}) {
|
||||||
|
if (!Number.isSafeInteger(questionCount) || questionCount < 1) {
|
||||||
|
throw new Error("Expected question count must be a positive integer");
|
||||||
|
}
|
||||||
|
if (!Array.isArray(questions)) return ["Question bank must be an array"];
|
||||||
|
if (!questions.length && !paper) {
|
||||||
|
return ["Question bank must contain at least one question"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
const papers = new Map();
|
||||||
|
// An explicitly requested paper must be present, even if no rows match it.
|
||||||
|
if (paper) papers.set(paper, new Map());
|
||||||
|
|
||||||
|
for (const question of questions) {
|
||||||
|
const source = question?.source;
|
||||||
|
const match =
|
||||||
|
typeof source === "string" &&
|
||||||
|
source.match(/^(\d{4}\/[^/\s]+)\/([1-9]\d*)$/);
|
||||||
|
if (!match || !Number.isSafeInteger(Number(match[2]))) {
|
||||||
|
errors.push(`Invalid source identifier: ${JSON.stringify(source)}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const paperId = match[1];
|
||||||
|
if (paper && paperId !== paper) continue;
|
||||||
|
|
||||||
|
if (!papers.has(paperId)) papers.set(paperId, new Map());
|
||||||
|
const numbers = papers.get(paperId);
|
||||||
|
const number = Number(match[2]);
|
||||||
|
if (numbers.has(number)) {
|
||||||
|
errors.push(
|
||||||
|
`Duplicate source ${source} (question IDs ${numbers.get(number)} and ${question.id})`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
numbers.set(number, question.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [paperId, numbers] of papers) {
|
||||||
|
const missing = [];
|
||||||
|
for (let number = 1; number <= questionCount; number++) {
|
||||||
|
if (!numbers.has(number)) missing.push(number);
|
||||||
|
}
|
||||||
|
if (missing.length) {
|
||||||
|
errors.push(`${paperId}: missing question numbers ${missing.join(", ")}`);
|
||||||
|
}
|
||||||
|
const unexpected = [...numbers.keys()]
|
||||||
|
.filter((number) => number > questionCount)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
if (unexpected.length) {
|
||||||
|
errors.push(
|
||||||
|
`${paperId}: unexpected question numbers ${unexpected.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
try {
|
||||||
|
const { values, positionals } = require("node:util").parseArgs({
|
||||||
|
allowPositionals: true,
|
||||||
|
options: {
|
||||||
|
paper: { type: "string" },
|
||||||
|
count: { type: "string", default: "15" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const dataPath = positionals[0] || process.env.DATA_PATH;
|
||||||
|
if (!dataPath || positionals.length > 1) {
|
||||||
|
throw new Error(
|
||||||
|
"Usage: node validate-question-bank.js <path> [--paper YYYY/session] [--count 15]",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const questions = JSON.parse(
|
||||||
|
require("node:fs").readFileSync(dataPath, "utf8"),
|
||||||
|
);
|
||||||
|
const errors = validateQuestionBank(questions, {
|
||||||
|
paper: values.paper,
|
||||||
|
questionCount: Number(values.count),
|
||||||
|
});
|
||||||
|
if (errors.length) {
|
||||||
|
console.error(errors.join("\n"));
|
||||||
|
process.exitCode = 1;
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`Question bank validation passed (${values.paper || "all papers"}; ${values.count} questions per paper).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error.message);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { validateQuestionBank };
|
||||||
91
backend/validate-question-bank.test.js
Normal file
91
backend/validate-question-bank.test.js
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
const { test } = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const { validateQuestionBank } = require("./validate-question-bank");
|
||||||
|
|
||||||
|
const completePaper = (paper = "2025/2", count = 15) =>
|
||||||
|
Array.from({ length: count }, (_, i) => ({
|
||||||
|
id: 683 + i,
|
||||||
|
source: `${paper}/${i + 1}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("complete papers have unique sources and every expected question", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
validateQuestionBank([...completePaper(), ...completePaper("2016/m1")]),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the October 2025 mislabel reports both duplicate 5 and missing 6", () => {
|
||||||
|
const questions = completePaper();
|
||||||
|
questions[5].source = "2025/2/5";
|
||||||
|
const errors = validateQuestionBank(questions);
|
||||||
|
assert.equal(errors.length, 2);
|
||||||
|
assert.match(errors[0], /Duplicate source 2025\/2\/5.*687 and 688/);
|
||||||
|
assert.match(errors[1], /2025\/2: missing question numbers 6$/);
|
||||||
|
|
||||||
|
questions[5].source = "2025/2/6";
|
||||||
|
assert.deepEqual(validateQuestionBank(questions), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("completeness includes missing first and last questions", () => {
|
||||||
|
const errors = validateQuestionBank(completePaper().slice(1, -1));
|
||||||
|
assert.deepEqual(errors, ["2025/2: missing question numbers 1, 15"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unexpected question numbers fail validation", () => {
|
||||||
|
assert.deepEqual(validateQuestionBank(completePaper("2025/2", 16)), [
|
||||||
|
"2025/2: unexpected question numbers 16",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("paper checks ignore other papers but detect an entirely missing import", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
validateQuestionBank([...completePaper(), { id: 1, source: "2024/2/1" }], {
|
||||||
|
paper: "2025/2",
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
validateQuestionBank([], { paper: "2025/2" })[0],
|
||||||
|
/2025\/2: missing question numbers 1, 2, 3.*15$/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the expected count is explicit, so shorter imports cannot appear complete", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
validateQuestionBank(completePaper("2025/2", 3), { questionCount: 3 }),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
validateQuestionBank(completePaper("2025/2", 3))[0],
|
||||||
|
/missing question numbers 4, 5, 6.*15$/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("malformed sources and invalid banks fail validation", () => {
|
||||||
|
for (const source of [
|
||||||
|
undefined,
|
||||||
|
null,
|
||||||
|
"2025/2/0",
|
||||||
|
"2025/2/1.5",
|
||||||
|
"2025/2/01",
|
||||||
|
"2025/2/1 ",
|
||||||
|
]) {
|
||||||
|
assert.match(
|
||||||
|
validateQuestionBank([{ source }])[0],
|
||||||
|
/Invalid source identifier/,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert.deepEqual(validateQuestionBank({}), [
|
||||||
|
"Question bank must be an array",
|
||||||
|
]);
|
||||||
|
assert.deepEqual(validateQuestionBank([]), [
|
||||||
|
"Question bank must contain at least one question",
|
||||||
|
]);
|
||||||
|
for (const questionCount of [0, -1, 1.5, NaN, Infinity]) {
|
||||||
|
assert.throws(
|
||||||
|
() => validateQuestionBank([], { questionCount }),
|
||||||
|
/positive integer/,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -18,7 +18,18 @@ h2 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #0a0f14;
|
color: #0a0f14;
|
||||||
}
|
}
|
||||||
pre {
|
.card fieldset {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 1.5em 0 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.card legend {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
pre,
|
||||||
|
.card legend {
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
font-family: "Open sans", sans-serif;
|
font-family: "Open sans", sans-serif;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
|
|
@ -45,7 +56,8 @@ img {
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
pre {
|
pre,
|
||||||
|
.card legend {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ input[type="radio"] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
input[type="radio"] + label {
|
input[type="radio"] + label {
|
||||||
|
cursor: pointer;
|
||||||
display: inline;
|
display: inline;
|
||||||
font-family: "Open Sans", sans-serif;
|
font-family: "Open Sans", sans-serif;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
|
|
@ -29,12 +30,12 @@ input[type="radio"]:checked + label {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 1000px) {
|
@media only screen and (max-width: 1000px) {
|
||||||
.card form {
|
.card .answers {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 28px minmax(0, 1fr);
|
grid-template-columns: 28px minmax(0, 1fr);
|
||||||
gap: 0.75em 0.5em;
|
gap: 0.75em 0.5em;
|
||||||
}
|
}
|
||||||
.card form br {
|
.card .answers br {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
input[type="radio"] {
|
input[type="radio"] {
|
||||||
|
|
|
||||||
|
|
@ -135,94 +135,98 @@
|
||||||
<div class="fele" id="bal">
|
<div class="fele" id="bal">
|
||||||
<div class="negyzet main">
|
<div class="negyzet main">
|
||||||
<input type="checkbox" id="mec" />
|
<input type="checkbox" id="mec" />
|
||||||
<label class="mbcheck">Mechanika</label>
|
<label for="mec" class="mbcheck">Mechanika</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet">
|
<div class="negyzet">
|
||||||
<input type="checkbox" id="mk" />
|
<input type="checkbox" id="mk" />
|
||||||
<label class="mbcheck">Kinematika</label>
|
<label for="mk" class="mbcheck">Kinematika</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet">
|
<div class="negyzet">
|
||||||
<input type="checkbox" id="md" />
|
<input type="checkbox" id="md" />
|
||||||
<label class="mbcheck">Dinamika</label>
|
<label for="md" class="mbcheck">Dinamika</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet">
|
<div class="negyzet">
|
||||||
<input type="checkbox" id="me" />
|
<input type="checkbox" id="me" />
|
||||||
<label class="mbcheck">Munka és energia</label>
|
<label for="me" class="mbcheck">Munka és energia</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet">
|
<div class="negyzet">
|
||||||
<input type="checkbox" id="mf" />
|
<input type="checkbox" id="mf" />
|
||||||
<label class="mbcheck">Folyadékok és gázok mechanikája</label>
|
<label for="mf" class="mbcheck"
|
||||||
|
>Folyadékok és gázok mechanikája</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet">
|
<div class="negyzet">
|
||||||
<input type="checkbox" id="mr" />
|
<input type="checkbox" id="mr" />
|
||||||
<label class="mbcheck">Rezgések és hullámok</label>
|
<label for="mr" class="mbcheck">Rezgések és hullámok</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet main">
|
<div class="negyzet main">
|
||||||
<input type="checkbox" id="h" />
|
<input type="checkbox" id="h" />
|
||||||
<label class="mbcheck">Hőtan</label>
|
<label for="h" class="mbcheck">Hőtan</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet main">
|
<div class="negyzet main">
|
||||||
<input type="checkbox" id="ele" />
|
<input type="checkbox" id="ele" />
|
||||||
<label class="mbcheck">Elektromosság</label>
|
<label for="ele" class="mbcheck">Elektromosság</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet">
|
<div class="negyzet">
|
||||||
<input type="checkbox" id="es" />
|
<input type="checkbox" id="es" />
|
||||||
<label class="mbcheck">Elektrosztatika</label>
|
<label for="es" class="mbcheck">Elektrosztatika</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet">
|
<div class="negyzet">
|
||||||
<input type="checkbox" id="ee" />
|
<input type="checkbox" id="ee" />
|
||||||
<label class="mbcheck">Egyenáram</label>
|
<label for="ee" class="mbcheck">Egyenáram</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet">
|
<div class="negyzet">
|
||||||
<input type="checkbox" id="ev" />
|
<input type="checkbox" id="ev" />
|
||||||
<label class="mbcheck">Váltakozó áram</label>
|
<label for="ev" class="mbcheck">Váltakozó áram</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="fele" id="jobb">
|
<div class="fele" id="jobb">
|
||||||
<div class="negyzet main">
|
<div class="negyzet main">
|
||||||
<input type="checkbox" id="m" />
|
<input type="checkbox" id="m" />
|
||||||
<label class="mbcheck">Mágnesesség</label>
|
<label for="m" class="mbcheck">Mágnesesség</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet main">
|
<div class="negyzet main">
|
||||||
<input type="checkbox" id="o" />
|
<input type="checkbox" id="o" />
|
||||||
<label class="mbcheck">Fénytan</label>
|
<label for="o" class="mbcheck">Fénytan</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet main">
|
<div class="negyzet main">
|
||||||
<input type="checkbox" id="atm" />
|
<input type="checkbox" id="atm" />
|
||||||
<label class="mbcheck">Atomfizika</label>
|
<label for="atm" class="mbcheck">Atomfizika</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet">
|
<div class="negyzet">
|
||||||
<input type="checkbox" id="ah" />
|
<input type="checkbox" id="ah" />
|
||||||
<label class="mbcheck">Atomhéj</label>
|
<label for="ah" class="mbcheck">Atomhéj</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet">
|
<div class="negyzet">
|
||||||
<input type="checkbox" id="am" />
|
<input type="checkbox" id="am" />
|
||||||
<label class="mbcheck">Atommag</label>
|
<label for="am" class="mbcheck">Atommag</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet main">
|
<div class="negyzet main">
|
||||||
<input type="checkbox" id="cs" />
|
<input type="checkbox" id="cs" />
|
||||||
<label class="mbcheck">Égi mechanika, csillagászat</label>
|
<label for="cs" class="mbcheck"
|
||||||
|
>Égi mechanika, csillagászat</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet main">
|
<div class="negyzet main">
|
||||||
<input type="checkbox" id="v" />
|
<input type="checkbox" id="v" />
|
||||||
<label class="mbcheck">Vegyes</label>
|
<label for="v" class="mbcheck">Vegyes</label>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="negyzet">
|
<div class="negyzet">
|
||||||
|
|
@ -260,21 +264,7 @@
|
||||||
</select>
|
</select>
|
||||||
<select id="feladat">
|
<select id="feladat">
|
||||||
<option value="/all">Összes feladat</option>
|
<option value="/all">Összes feladat</option>
|
||||||
<option value="/1">1. feladat</option>
|
<!-- Question numbers will be populated for the selected paper -->
|
||||||
<option value="/2">2. feladat</option>
|
|
||||||
<option value="/3">3. feladat</option>
|
|
||||||
<option value="/4">4. feladat</option>
|
|
||||||
<option value="/5">5. feladat</option>
|
|
||||||
<option value="/6">6. feladat</option>
|
|
||||||
<option value="/7">7. feladat</option>
|
|
||||||
<option value="/8">8. feladat</option>
|
|
||||||
<option value="/9">9. feladat</option>
|
|
||||||
<option value="/10">10. feladat</option>
|
|
||||||
<option value="/11">11. feladat</option>
|
|
||||||
<option value="/12">12. feladat</option>
|
|
||||||
<option value="/13">13. feladat</option>
|
|
||||||
<option value="/14">14. feladat</option>
|
|
||||||
<option value="/15">15. feladat</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,22 @@ function aspect() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ajaxLoad(type) {
|
async function ajaxLoad(type) {
|
||||||
|
let topicQuestionCount = 15;
|
||||||
|
if (type != 1 && type != 2) {
|
||||||
|
const countInput = document.getElementById("numberof");
|
||||||
|
// Keep the empty-field default, but reject incomplete numeric input.
|
||||||
|
topicQuestionCount =
|
||||||
|
countInput.value === "" && !countInput.validity.badInput
|
||||||
|
? 15
|
||||||
|
: countInput.valueAsNumber;
|
||||||
|
if (!isValidQuestionCount(topicQuestionCount)) {
|
||||||
|
countInput.setCustomValidity(QUESTION_COUNT_ERROR);
|
||||||
|
countInput.reportValidity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
countInput.setCustomValidity("");
|
||||||
|
}
|
||||||
|
|
||||||
reviewMode = 0;
|
reviewMode = 0;
|
||||||
totalPoints = 0;
|
totalPoints = 0;
|
||||||
loadedQuestions = [];
|
loadedQuestions = [];
|
||||||
|
|
@ -61,7 +77,6 @@ async function ajaxLoad(type) {
|
||||||
15,
|
15,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
var NOQ = $("#numberof").val() ? $("#numberof").val() : 15;
|
|
||||||
categories = [
|
categories = [
|
||||||
$("#mk").prop("checked") ? "mk" : "",
|
$("#mk").prop("checked") ? "mk" : "",
|
||||||
$("#md").prop("checked") ? "md" : "",
|
$("#md").prop("checked") ? "md" : "",
|
||||||
|
|
@ -79,7 +94,12 @@ async function ajaxLoad(type) {
|
||||||
$("#cs").prop("checked") ? "cs" : "",
|
$("#cs").prop("checked") ? "cs" : "",
|
||||||
$("#v").prop("checked") ? "v" : "",
|
$("#v").prop("checked") ? "v" : "",
|
||||||
];
|
];
|
||||||
result = await loadQuestions(false, categories, undefined, NOQ);
|
result = await loadQuestions(
|
||||||
|
false,
|
||||||
|
categories,
|
||||||
|
undefined,
|
||||||
|
topicQuestionCount,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#loadingGif").hide();
|
$("#loadingGif").hide();
|
||||||
|
|
@ -121,7 +141,7 @@ async function ajaxLoad(type) {
|
||||||
// its correct answer, and report whether they got it right.
|
// its correct answer, and report whether they got it right.
|
||||||
function gradeQuestion(question) {
|
function gradeQuestion(question) {
|
||||||
var div = "#feladat" + question.id;
|
var div = "#feladat" + question.id;
|
||||||
var selected = "#form" + question.id + " #rad" + question.correct;
|
var selected = `#question-${question.id}-answer-${question.correct}`;
|
||||||
var isCorrect = $(selected).is(":checked");
|
var isCorrect = $(selected).is(":checked");
|
||||||
$(div).animate({ backgroundColor: isCorrect ? "#C6FF8C" : "#FF808C" }, 1100);
|
$(div).animate({ backgroundColor: isCorrect ? "#C6FF8C" : "#FF808C" }, 1100);
|
||||||
return isCorrect;
|
return isCorrect;
|
||||||
|
|
@ -163,7 +183,7 @@ function showCorrect() {
|
||||||
reviewMode = 1;
|
reviewMode = 1;
|
||||||
var percentage = scoreLoadedQuestions();
|
var percentage = scoreLoadedQuestions();
|
||||||
loadedQuestions.forEach(function (question) {
|
loadedQuestions.forEach(function (question) {
|
||||||
$("#label" + question.id + ".rad" + question.correct).css(
|
$(`label[for="question-${question.id}-answer-${question.correct}"]`).css(
|
||||||
"background-color",
|
"background-color",
|
||||||
"#C6FF8C",
|
"#C6FF8C",
|
||||||
);
|
);
|
||||||
|
|
@ -315,6 +335,7 @@ $(document).ready(function () {
|
||||||
initializeMonthDropdown($("#evszam").val());
|
initializeMonthDropdown($("#evszam").val());
|
||||||
$("#honap").val("all");
|
$("#honap").val("all");
|
||||||
});
|
});
|
||||||
|
$("#honap").change(initializeQuestionDropdown);
|
||||||
|
|
||||||
$("#mec").change(function () {
|
$("#mec").change(function () {
|
||||||
var isChecked = this.checked;
|
var isChecked = this.checked;
|
||||||
|
|
@ -338,6 +359,9 @@ $(document).ready(function () {
|
||||||
$("#berettsegi").click(function (event) {
|
$("#berettsegi").click(function (event) {
|
||||||
ajaxLoad(2);
|
ajaxLoad(2);
|
||||||
});
|
});
|
||||||
|
$("#numberof").on("input", function () {
|
||||||
|
this.setCustomValidity("");
|
||||||
|
});
|
||||||
$("#load").click(function (event) {
|
$("#load").click(function (event) {
|
||||||
ajaxLoad(isSearch ? 1 : 3);
|
ajaxLoad(isSearch ? 1 : 3);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -100,29 +100,25 @@ const loadQuestions = async (
|
||||||
({ id, source, description, a, b, c, d, image }, i) => {
|
({ id, source, description, a, b, c, d, image }, i) => {
|
||||||
const qid = Number(id);
|
const qid = Number(id);
|
||||||
const safeImage = image ? encodeURIComponent(image) : "";
|
const safeImage = image ? encodeURIComponent(image) : "";
|
||||||
|
const answersHtml = [a, b, c, ...(d ? [d] : [])]
|
||||||
|
.map((answer, answerIndex) => {
|
||||||
|
const answerNumber = answerIndex + 1;
|
||||||
|
const answerId = `question-${qid}-answer-${answerNumber}`;
|
||||||
|
return `
|
||||||
|
<input type="radio" id="${answerId}" name="question-${qid}" value="${answerNumber}">
|
||||||
|
<label for="${answerId}">${sanitizeQuestionHtml(answer)}</label>
|
||||||
|
<br>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
resultHtml += `
|
resultHtml += `
|
||||||
<div class="feladat card" id="feladat${qid}">
|
<div class="feladat card" id="feladat${qid}">
|
||||||
<h2 style="float: left;">${i + 1}.</h2><h2>${sanitizeQuestionHtml(source)}</h2>
|
<h2 style="float: left;">${i + 1}.</h2><h2>${sanitizeQuestionHtml(source)}</h2>
|
||||||
<pre>${sanitizeQuestionHtml(description)}</pre>
|
|
||||||
${image ? `<img src="${API_BASE}/api/pics/${safeImage}" crossorigin="anonymous" onerror="this.onerror=null; this.src='pics/${safeImage}'"><br>` : ""}
|
|
||||||
<form id="form${qid}">
|
<form id="form${qid}">
|
||||||
<input type="radio" id="rad1" name="group">
|
<fieldset>
|
||||||
<label id="label${qid}" class="rad1">${sanitizeQuestionHtml(a)}</label>
|
<legend>${sanitizeQuestionHtml(description)}</legend>
|
||||||
<br>
|
${image ? `<img src="${API_BASE}/api/pics/${safeImage}" crossorigin="anonymous" onerror="this.onerror=null; this.src='pics/${safeImage}'"><br>` : ""}
|
||||||
<input type="radio" id="rad2" name="group">
|
<div class="answers">${answersHtml}</div>
|
||||||
<label id="label${qid}" class="rad2">${sanitizeQuestionHtml(b)}</label>
|
</fieldset>
|
||||||
<br>
|
|
||||||
<input type="radio" id="rad3" name="group">
|
|
||||||
<label id="label${qid}" class="rad3">${sanitizeQuestionHtml(c)}</label>
|
|
||||||
<br>
|
|
||||||
${
|
|
||||||
d
|
|
||||||
? `
|
|
||||||
<input type="radio" id="rad4" name="group">
|
|
||||||
<label id="label${qid}" class="rad4">${sanitizeQuestionHtml(d)}</label>
|
|
||||||
<br>`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -281,6 +277,45 @@ const initializeMonthDropdown = (selectedYear) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
monthDropdown.innerHTML = monthOptions;
|
monthDropdown.innerHTML = monthOptions;
|
||||||
|
initializeQuestionDropdown();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Offer only imported question numbers matching the selected year and paper.
|
||||||
|
// With an "all" filter, use the union across the matching papers.
|
||||||
|
const initializeQuestionDropdown = () => {
|
||||||
|
if (!questions) return;
|
||||||
|
|
||||||
|
const questionDropdown = document.getElementById("feladat");
|
||||||
|
if (!questionDropdown) return;
|
||||||
|
|
||||||
|
const selectedYear = document.getElementById("evszam").value;
|
||||||
|
const selectedPaper = document.getElementById("honap").value;
|
||||||
|
const previousQuestion = questionDropdown.value;
|
||||||
|
const questionNumbers = new Set();
|
||||||
|
|
||||||
|
questions.forEach(({ source }) => {
|
||||||
|
const match = source.match(/^(\d{4})\/([^/\s]+)\/([1-9]\d*)$/);
|
||||||
|
if (
|
||||||
|
match &&
|
||||||
|
(selectedYear === "all/" || selectedYear === `${match[1]}/`) &&
|
||||||
|
(selectedPaper === "all" || selectedPaper === match[2]) &&
|
||||||
|
Number.isSafeInteger(Number(match[3]))
|
||||||
|
) {
|
||||||
|
questionNumbers.add(Number(match[3]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const numberOptions = [...questionNumbers]
|
||||||
|
.sort((a, b) => a - b)
|
||||||
|
.map((number) => `<option value="/${number}">${number}. feladat</option>`)
|
||||||
|
.join("");
|
||||||
|
questionDropdown.innerHTML =
|
||||||
|
'<option value="/all">Összes feladat</option>' + numberOptions;
|
||||||
|
|
||||||
|
// Retain a still-valid selection; clear it when the new paper lacks it.
|
||||||
|
if (questionNumbers.has(Number(previousQuestion.slice(1)))) {
|
||||||
|
questionDropdown.value = previousQuestion;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to get readable month labels
|
// Helper function to get readable month labels
|
||||||
|
|
@ -288,6 +323,7 @@ const getMonthLabel = (monthValue) => {
|
||||||
// Handle known patterns
|
// Handle known patterns
|
||||||
const knownLabels = {
|
const knownLabels = {
|
||||||
1: "Május-Június",
|
1: "Május-Június",
|
||||||
|
"1ma": "Május-Június (magyar, mint idegen nyelv)",
|
||||||
2: "Október-November",
|
2: "Október-November",
|
||||||
3: "Harmadik időszak",
|
3: "Harmadik időszak",
|
||||||
m1: "1. Mintafeladatsor",
|
m1: "1. Mintafeladatsor",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue