refactor: optimized python code and offload things to vue code
This commit is contained in:
+110
-28
@@ -7,22 +7,67 @@
|
||||
var ANSWER_LABELS = ["A", "B", "C", "D"];
|
||||
|
||||
var PART_DEFINITIONS = {
|
||||
B: { label: "Betriebliche Kenntnisse", formLabel: "Betriebliche Kenntnisse", minutes: 45 },
|
||||
V: { label: "Kenntnisse von Vorschriften", formLabel: "Kenntnisse von Vorschriften", minutes: 45 },
|
||||
N: { label: "Technische Kenntnisse Klasse N", formLabel: "Technische Kenntnisse Klasse N", minutes: 45 },
|
||||
E: { label: "Technische Kenntnisse Klasse E", formLabel: "Technische Kenntnisse Klasse E", minutes: 45 },
|
||||
A: { label: "Technische Kenntnisse Klasse A", formLabel: "Technische Kenntnisse Klasse A", minutes: 60 }
|
||||
B: { label: "Betriebliche Kenntnisse", minutes: 45 },
|
||||
V: { label: "Kenntnisse von Vorschriften", minutes: 45 },
|
||||
N: { label: "Technische Kenntnisse Klasse N", minutes: 45 },
|
||||
E: { label: "Technische Kenntnisse Klasse E", minutes: 45 },
|
||||
A: { label: "Technische Kenntnisse Klasse A", minutes: 60 }
|
||||
};
|
||||
|
||||
var EXAM_CHOICES = [
|
||||
{ id: "N", group: "primary", shortLabel: "N", label: "Prüfung Klasse N", parts: ["B", "V", "N"], color: "#47ABE8" },
|
||||
{ id: "E", group: "primary", shortLabel: "N + E", label: "Prüfung Klasse E", parts: ["B", "V", "N", "E"], color: "#FE756C" },
|
||||
{ id: "A", group: "primary", shortLabel: "N + E + A", label: "Prüfung Klasse A", parts: ["B", "V", "N", "E", "A"], color: "#3BB583" },
|
||||
{ id: "N-E", group: "upgrade", shortLabel: "N → E", label: "Aufstockung von N auf E", parts: ["E"], color: "#FE756C" },
|
||||
{ id: "E-A", group: "upgrade", shortLabel: "E → A", label: "Aufstockung von E auf A", parts: ["A"], color: "#3BB583" },
|
||||
{ id: "N-A", group: "upgrade", shortLabel: "N → A", label: "Aufstockung von N auf A", parts: ["E", "A"], color: "#3BB583" }
|
||||
{ id: "N", group: "primary", label: "Klasse N", detail: "B + V + N", parts: ["B", "V", "N"], color: "#47ABE8" },
|
||||
{ id: "E", group: "primary", label: "Klasse E", detail: "B + V + N + E", parts: ["B", "V", "N", "E"], color: "#FE756C" },
|
||||
{ id: "A", group: "primary", label: "Klasse A", detail: "B + V + N + E + A", parts: ["B", "V", "N", "E", "A"], color: "#3BB583" },
|
||||
{ id: "N-E", group: "upgrade", label: "N → E", detail: "Technik E", parts: ["E"], color: "#FE756C" },
|
||||
{ id: "N-A", group: "upgrade", label: "N → A", detail: "Technik E + A", parts: ["E", "A"], color: "#3BB583" },
|
||||
{ id: "E-A", group: "upgrade", label: "E → A", detail: "Technik A", parts: ["A"], color: "#3BB583" }
|
||||
];
|
||||
|
||||
function normalizeCatalog(rawCatalog, metadata, questionIndex) {
|
||||
var questions = {};
|
||||
var partForSection = { 1: "B", 2: "V" };
|
||||
var partForClass = { "1": "N", "2": "E", "3": "A" };
|
||||
|
||||
function pictureId(value) {
|
||||
return value ? String(value) : "";
|
||||
}
|
||||
|
||||
function walk(section, sectionIndex, category) {
|
||||
(section.questions || []).forEach(function (question) {
|
||||
var part = sectionIndex === 0
|
||||
? partForClass[String(question.class)]
|
||||
: partForSection[sectionIndex];
|
||||
if (!part) return;
|
||||
|
||||
var questionMetadata = metadata[question.number] || {};
|
||||
questions[question.number] = {
|
||||
id: question.number,
|
||||
part: part,
|
||||
category: category.slice(),
|
||||
question: question.question,
|
||||
layout: questionMetadata.layout || "default",
|
||||
picture: pictureId(questionMetadata.picture_question),
|
||||
answers: ["a", "b", "c", "d"].map(function (letter) {
|
||||
return {
|
||||
text: question["answer_" + letter] || "",
|
||||
picture: pictureId(questionMetadata["picture_" + letter])
|
||||
};
|
||||
}),
|
||||
hasSolution: Boolean(questionIndex[question.number] && questionIndex[question.number].has_solution)
|
||||
};
|
||||
});
|
||||
|
||||
(section.sections || []).forEach(function (child, childIndex) {
|
||||
walk(child, sectionIndex, category.concat(childIndex));
|
||||
});
|
||||
}
|
||||
|
||||
(rawCatalog.sections || []).forEach(function (section, sectionIndex) {
|
||||
walk(section, sectionIndex, []);
|
||||
});
|
||||
return questions;
|
||||
}
|
||||
|
||||
function shuffled(values) {
|
||||
var result = values.slice();
|
||||
for (var i = result.length - 1; i > 0; i -= 1) {
|
||||
@@ -120,6 +165,8 @@
|
||||
var loading = ref(true);
|
||||
var loadError = ref("");
|
||||
var catalog = ref({});
|
||||
var altTexts = Vue.reactive({});
|
||||
var requestedAltTexts = new Set();
|
||||
var session = ref(null);
|
||||
var view = ref("exam");
|
||||
var reviewPartIndex = ref(null);
|
||||
@@ -199,14 +246,38 @@
|
||||
return catalog.value[questionState.id];
|
||||
}
|
||||
|
||||
function partSummary(parts) {
|
||||
return parts.join(" + ");
|
||||
}
|
||||
|
||||
function padNumber(number) {
|
||||
return String(number).padStart(2, "0");
|
||||
}
|
||||
|
||||
function pictureAlt(picture, fallback) {
|
||||
return altTexts[picture] || fallback;
|
||||
}
|
||||
|
||||
function loadAltText(picture) {
|
||||
if (!picture || requestedAltTexts.has(picture)) return;
|
||||
requestedAltTexts.add(picture);
|
||||
fetch("pictures/" + encodeURIComponent(picture) + ".txt")
|
||||
.then(function (response) {
|
||||
if (!response.ok) return "";
|
||||
return response.text();
|
||||
})
|
||||
.then(function (description) {
|
||||
if (description && description.trim()) altTexts[picture] = description.trim();
|
||||
})
|
||||
.catch(function () { /* the generic alternative text remains available */ });
|
||||
}
|
||||
|
||||
function loadPartAltTexts(part) {
|
||||
if (!part) return;
|
||||
part.questions.forEach(function (questionState) {
|
||||
var question = questionData(questionState);
|
||||
if (!question) return;
|
||||
loadAltText(question.picture);
|
||||
question.answers.forEach(function (answer) { loadAltText(answer.picture); });
|
||||
});
|
||||
}
|
||||
|
||||
function createPart(code) {
|
||||
var picked = balancedSample(Object.values(catalog.value).filter(function (question) {
|
||||
return question.part === code;
|
||||
@@ -351,8 +422,7 @@
|
||||
try { window.localStorage.removeItem(STORAGE_KEY); } catch (error) { /* storage can be unavailable */ }
|
||||
}
|
||||
|
||||
function abortExam() {
|
||||
if (!window.confirm("Möchtest du diese Simulation abbrechen? Der gespeicherte Prüfungsstand wird gelöscht.")) return;
|
||||
function resetExam() {
|
||||
clearStoredSession();
|
||||
session.value = null;
|
||||
view.value = "exam";
|
||||
@@ -360,12 +430,13 @@
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
|
||||
function abortExam() {
|
||||
if (!window.confirm("Möchtest du diese Simulation abbrechen? Der gespeicherte Prüfungsstand wird gelöscht.")) return;
|
||||
resetExam();
|
||||
}
|
||||
|
||||
function newExam() {
|
||||
clearStoredSession();
|
||||
session.value = null;
|
||||
view.value = "exam";
|
||||
reviewPartIndex.value = null;
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
resetExam();
|
||||
}
|
||||
|
||||
function scrollToQuestion(index) {
|
||||
@@ -445,18 +516,29 @@
|
||||
} catch (error) { /* simulator remains usable without persistence */ }
|
||||
}, { deep: true });
|
||||
|
||||
watch(currentPart, function (part) {
|
||||
loadPartAltTexts(part);
|
||||
});
|
||||
|
||||
watch(remainingSeconds, function (seconds) {
|
||||
if (session.value && currentPart.value && currentPart.value.status === "running" && seconds <= 0) submitPart(true);
|
||||
});
|
||||
|
||||
function loadCatalog() {
|
||||
fetch("assets/exam-questions.json")
|
||||
.then(function (response) {
|
||||
if (!response.ok) throw new Error("Fragenkatalog nicht gefunden (HTTP " + response.status + ").");
|
||||
return response.json();
|
||||
Promise.all([
|
||||
fetch("assets/fragenkatalog.json"),
|
||||
fetch("assets/metadata.json"),
|
||||
fetch("assets/question_index.json")
|
||||
])
|
||||
.then(function (responses) {
|
||||
var names = ["Fragenkatalog", "Metadaten", "Fragenindex"];
|
||||
responses.forEach(function (response, index) {
|
||||
if (!response.ok) throw new Error(names[index] + " nicht gefunden (HTTP " + response.status + ").");
|
||||
});
|
||||
return Promise.all(responses.map(function (response) { return response.json(); }));
|
||||
})
|
||||
.then(function (data) {
|
||||
catalog.value = data.questions || {};
|
||||
catalog.value = normalizeCatalog(data[0], data[1] || {}, data[2] || {});
|
||||
if (Object.keys(catalog.value).length === 0) throw new Error("Der Fragenkatalog ist leer.");
|
||||
restoreSession();
|
||||
loading.value = false;
|
||||
@@ -520,8 +602,8 @@
|
||||
overallIcon: overallIcon,
|
||||
overallTitle: overallTitle,
|
||||
overallText: overallText,
|
||||
partSummary: partSummary,
|
||||
padNumber: padNumber,
|
||||
pictureAlt: pictureAlt,
|
||||
questionData: questionData,
|
||||
startExam: startExam,
|
||||
selectAnswer: selectAnswer,
|
||||
|
||||
+96
-157
@@ -24,7 +24,11 @@ class Build:
|
||||
|
||||
self.env = Environment(loader=FileSystemLoader(self.config.p_templates))
|
||||
self.env.filters["shuffle_answers"] = self.__filter_shuffle_answers
|
||||
self.questions = self.__parse_katalog()
|
||||
with self.config.p_data_fragenkatalog.open(encoding="utf-8") as file:
|
||||
self.fragenkatalog = json.load(file)
|
||||
with self.config.p_data_metadata.open(encoding="utf-8") as file:
|
||||
self.question_metadata = json.load(file)
|
||||
self.questions = self.__parse_katalog(self.fragenkatalog)
|
||||
|
||||
self.question_index = {}
|
||||
self.question_token_pattern = re.compile(r"^\s*\[question:([\w\d]+)\]", re.MULTILINE)
|
||||
@@ -32,101 +36,88 @@ class Build:
|
||||
self.index_entries = {}
|
||||
self.index_token_pattern = Index.pattern
|
||||
|
||||
def __parse_katalog(self):
|
||||
with self.config.p_data_fragenkatalog.open(encoding="utf-8") as file:
|
||||
fragenkatalog = json.load(file)
|
||||
@staticmethod
|
||||
def __parse_katalog(fragenkatalog):
|
||||
questions = {}
|
||||
|
||||
questions = {}
|
||||
def collect(section):
|
||||
for question in section.get("questions", []):
|
||||
questions[question["number"]] = question
|
||||
for child in section.get("sections", []):
|
||||
collect(child)
|
||||
|
||||
for exampart in fragenkatalog["sections"]:
|
||||
for chapter in exampart["sections"]:
|
||||
if "questions" in chapter:
|
||||
for question in chapter["questions"]:
|
||||
questions[question["number"]] = question
|
||||
if "sections" in chapter:
|
||||
for section in chapter["sections"]:
|
||||
for question in section["questions"]:
|
||||
questions[question["number"]] = question
|
||||
|
||||
return questions
|
||||
for exam_part in fragenkatalog["sections"]:
|
||||
collect(exam_part)
|
||||
return questions
|
||||
|
||||
def __build_question(self, number, template_file="html/question.html"):
|
||||
"""Combines the original question dataset from BNetzA with our internal metadata"""
|
||||
|
||||
question_template = self.env.get_template(template_file)
|
||||
|
||||
with (self.config.p_data_metadata).open(encoding="utf-8") as file:
|
||||
metadata_json = json.load(file)
|
||||
question = self.questions.get(number)
|
||||
metadata = self.question_metadata.get(number)
|
||||
|
||||
question = None
|
||||
metadata = None
|
||||
|
||||
if number in self.questions:
|
||||
question = self.questions[number]
|
||||
|
||||
if number in metadata_json:
|
||||
metadata = metadata_json[number]
|
||||
|
||||
if question is None or metadata is None:
|
||||
tqdm.write(
|
||||
f"\033[31mQuestion #{number} is missing"
|
||||
+ (" (Question not in question pool)" if question is None else "")
|
||||
+ (" (Question not in metadata)" if metadata is None else "")
|
||||
+ "\033[0m"
|
||||
)
|
||||
metadata = {"layout": "not-found", "picture_a": ""}
|
||||
number = 404
|
||||
question = {"question": f"Frage {input} nicht gefunden"}
|
||||
|
||||
if "answer_a" in question:
|
||||
answers = [question["answer_a"], question["answer_b"], question["answer_c"], question["answer_d"]]
|
||||
else:
|
||||
answers = []
|
||||
|
||||
if metadata["picture_a"] != "":
|
||||
alt_text_a = self.__picture_handler(metadata["picture_a"])
|
||||
alt_text_b = self.__picture_handler(metadata["picture_b"])
|
||||
alt_text_c = self.__picture_handler(metadata["picture_c"])
|
||||
alt_text_d = self.__picture_handler(metadata["picture_d"])
|
||||
|
||||
answer_pictures = [
|
||||
metadata["picture_a"],
|
||||
metadata["picture_b"],
|
||||
metadata["picture_c"],
|
||||
metadata["picture_d"],
|
||||
]
|
||||
|
||||
alt_text_answers = [
|
||||
alt_text_a,
|
||||
alt_text_b,
|
||||
alt_text_c,
|
||||
alt_text_d,
|
||||
]
|
||||
|
||||
else:
|
||||
answer_pictures = []
|
||||
alt_text_answers = []
|
||||
|
||||
if "picture_question" in question and metadata["picture_question"] != "":
|
||||
picture_question = metadata["picture_question"]
|
||||
alt_text_question = self.__picture_handler(picture_question)
|
||||
else:
|
||||
picture_question = ""
|
||||
alt_text_question = ""
|
||||
|
||||
solution_file = self.config.p_data_solutions / f"{number}.md"
|
||||
|
||||
return question_template.render(
|
||||
question=question["question"],
|
||||
number=number,
|
||||
layout=metadata["layout"],
|
||||
picture_question=picture_question,
|
||||
answers=answers,
|
||||
answer_pictures=answer_pictures,
|
||||
alt_text_answers=alt_text_answers,
|
||||
alt_text_question=alt_text_question,
|
||||
has_solution=solution_file.exists(),
|
||||
if question is None or metadata is None:
|
||||
tqdm.write(
|
||||
f"\033[31mQuestion #{number} is missing"
|
||||
+ (" (Question not in question pool)" if question is None else "")
|
||||
+ (" (Question not in metadata)" if metadata is None else "")
|
||||
+ "\033[0m"
|
||||
)
|
||||
metadata = {"layout": "not-found", "picture_a": ""}
|
||||
number = 404
|
||||
question = {"question": f"Frage {input} nicht gefunden"}
|
||||
|
||||
if "answer_a" in question:
|
||||
answers = [question["answer_a"], question["answer_b"], question["answer_c"], question["answer_d"]]
|
||||
else:
|
||||
answers = []
|
||||
|
||||
if metadata["picture_a"] != "":
|
||||
alt_text_a = self.__picture_handler(metadata["picture_a"])
|
||||
alt_text_b = self.__picture_handler(metadata["picture_b"])
|
||||
alt_text_c = self.__picture_handler(metadata["picture_c"])
|
||||
alt_text_d = self.__picture_handler(metadata["picture_d"])
|
||||
|
||||
answer_pictures = [
|
||||
metadata["picture_a"],
|
||||
metadata["picture_b"],
|
||||
metadata["picture_c"],
|
||||
metadata["picture_d"],
|
||||
]
|
||||
|
||||
alt_text_answers = [
|
||||
alt_text_a,
|
||||
alt_text_b,
|
||||
alt_text_c,
|
||||
alt_text_d,
|
||||
]
|
||||
|
||||
else:
|
||||
answer_pictures = []
|
||||
alt_text_answers = []
|
||||
|
||||
if "picture_question" in question and metadata["picture_question"] != "":
|
||||
picture_question = metadata["picture_question"]
|
||||
alt_text_question = self.__picture_handler(picture_question)
|
||||
else:
|
||||
picture_question = ""
|
||||
alt_text_question = ""
|
||||
|
||||
solution_file = self.config.p_data_solutions / f"{number}.md"
|
||||
|
||||
return question_template.render(
|
||||
question=question["question"],
|
||||
number=number,
|
||||
layout=metadata["layout"],
|
||||
picture_question=picture_question,
|
||||
answers=answers,
|
||||
answer_pictures=answer_pictures,
|
||||
alt_text_answers=alt_text_answers,
|
||||
alt_text_question=alt_text_question,
|
||||
has_solution=solution_file.exists(),
|
||||
)
|
||||
|
||||
def __build_question_slide(self, input):
|
||||
return self.__build_question(input, template_file="slide/question.html")
|
||||
@@ -148,73 +139,22 @@ class Build:
|
||||
page_scripts=page_scripts or [],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def __walk_exam_questions(section, category=()):
|
||||
for question in section.get("questions", []):
|
||||
yield category, question
|
||||
for index, child in enumerate(section.get("sections", [])):
|
||||
yield from Build.__walk_exam_questions(child, (*category, index))
|
||||
|
||||
def __build_exam_catalog(self):
|
||||
"""Create the compact client-side catalog used by the exam simulator."""
|
||||
with (
|
||||
self.config.p_data_fragenkatalog.open(encoding="utf-8") as catalog_file,
|
||||
self.config.p_data_metadata.open(encoding="utf-8") as metadata_file,
|
||||
):
|
||||
catalog = json.load(catalog_file)
|
||||
metadata = json.load(metadata_file)
|
||||
|
||||
part_for_section = {1: "B", 2: "V"}
|
||||
part_for_class = {"1": "N", "2": "E", "3": "A"}
|
||||
questions = {}
|
||||
|
||||
def picture_data(picture_id):
|
||||
if not picture_id:
|
||||
return {"picture": "", "pictureAlt": ""}
|
||||
return {
|
||||
"picture": picture_id,
|
||||
"pictureAlt": self.__picture_handler(picture_id) or "Bildbeschreibung noch nicht verfügbar",
|
||||
}
|
||||
|
||||
for section_index, section in enumerate(catalog["sections"]):
|
||||
for category, question in self.__walk_exam_questions(section):
|
||||
if section_index == 0:
|
||||
part = part_for_class.get(str(question.get("class")))
|
||||
else:
|
||||
part = part_for_section.get(section_index)
|
||||
if part is None:
|
||||
continue
|
||||
|
||||
question_metadata = metadata.get(question["number"], {})
|
||||
question_picture = picture_data(question_metadata.get("picture_question", ""))
|
||||
answers = []
|
||||
for answer_letter in "abcd":
|
||||
answer_picture = picture_data(question_metadata.get(f"picture_{answer_letter}", ""))
|
||||
answers.append(
|
||||
{
|
||||
"text": question.get(f"answer_{answer_letter}", ""),
|
||||
**answer_picture,
|
||||
}
|
||||
)
|
||||
|
||||
questions[question["number"]] = {
|
||||
"id": question["number"],
|
||||
"part": part,
|
||||
"category": list(category),
|
||||
"question": question["question"],
|
||||
"layout": question_metadata.get("layout", "") or "default",
|
||||
"answers": answers,
|
||||
"hasSolution": self.__has_solution(question["number"]),
|
||||
**question_picture,
|
||||
}
|
||||
|
||||
self.config.p_build_assets.mkdir(parents=True, exist_ok=True)
|
||||
with (self.config.p_build_assets / "exam-questions.json").open("w", encoding="utf-8") as file:
|
||||
json.dump({"version": 1, "questions": questions}, file, ensure_ascii=False, separators=(",", ":"))
|
||||
file.write("\n")
|
||||
|
||||
def __build_exam_simulator(self):
|
||||
self.__build_exam_catalog()
|
||||
self.config.p_build_assets.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(self.config.p_data_fragenkatalog, self.config.p_build_assets / "fragenkatalog.json")
|
||||
shutil.copyfile(self.config.p_data_metadata, self.config.p_build_assets / "metadata.json")
|
||||
(self.config.p_build_assets / "exam-questions.json").unlink(missing_ok=True)
|
||||
|
||||
picture_fields = ("picture_question", "picture_a", "picture_b", "picture_c", "picture_d")
|
||||
picture_ids = {
|
||||
metadata[field]
|
||||
for metadata in self.question_metadata.values()
|
||||
for field in picture_fields
|
||||
if metadata.get(field)
|
||||
}
|
||||
for picture_id in picture_ids:
|
||||
self.__picture_handler(picture_id)
|
||||
|
||||
exam_template = self.env.get_template("html/exam-simulator.html")
|
||||
content = exam_template.render()
|
||||
page = self.__build_page(
|
||||
@@ -222,9 +162,6 @@ class Build:
|
||||
page_stylesheets=["assets/exam-simulator.css"],
|
||||
page_scripts=["assets/vue.global.prod.js", "assets/exam-simulator.js"],
|
||||
)
|
||||
legacy_path = self.config.p_build / "probeprüfung.html"
|
||||
if legacy_path.exists():
|
||||
legacy_path.unlink()
|
||||
with (self.config.p_build / "simulation.html").open("w", encoding="utf-8") as file:
|
||||
file.write(page)
|
||||
|
||||
@@ -232,8 +169,10 @@ class Build:
|
||||
file = f"{id}.svg"
|
||||
try:
|
||||
shutil.copyfile(self.config.p_data_pictures / file, self.config.p_build_pictures / file)
|
||||
if (self.config.p_data_pictures / f"{id}.txt").exists():
|
||||
return (self.config.p_data_pictures / f"{id}.txt").read_text(encoding="utf-8")
|
||||
description_source = self.config.p_data_pictures / f"{id}.txt"
|
||||
if description_source.exists():
|
||||
shutil.copyfile(description_source, self.config.p_build_pictures / description_source.name)
|
||||
return description_source.read_text(encoding="utf-8")
|
||||
else:
|
||||
return "Bildbeschreibung noch nicht verfügbar"
|
||||
except FileNotFoundError:
|
||||
|
||||
@@ -11,14 +11,15 @@
|
||||
|
||||
<section v-else-if="!session" class="mx-auto mb-5 exam-start" aria-labelledby="exam-heading">
|
||||
<h1 id="exam-heading">Prüfungssimulation</h1>
|
||||
<p class="lead">Wähle die Prüfung, die du unter möglichst realistischen Bedingungen üben möchtest.</p>
|
||||
<p class="lead">Welche Prüfung möchtest du simulieren?</p>
|
||||
|
||||
<h2 class="h4 mt-5">Erstprüfung</h2>
|
||||
<div class="row row-cols-1 row-cols-md-3 g-2">
|
||||
<div v-for="choice in primaryChoices" :key="choice.id" class="col">
|
||||
<button type="button" class="btn w-100 h-100 py-3 text-white"
|
||||
:style="{ backgroundColor: choice.color, borderColor: choice.color }" @click="startExam(choice)">
|
||||
<span v-text="choice.label + ' (' + partSummary(choice.parts) + ')' "></span>
|
||||
<span class="d-block" v-text="choice.label"></span>
|
||||
<small class="d-block" v-text="choice.detail"></small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,7 +29,8 @@
|
||||
<div v-for="choice in upgradeChoices" :key="choice.id" class="col">
|
||||
<button type="button" class="btn w-100 h-100 py-3 text-white"
|
||||
:style="{ backgroundColor: choice.color, borderColor: choice.color }" @click="startExam(choice)">
|
||||
<span v-text="choice.label + ' (' + partSummary(choice.parts) + ')' "></span>
|
||||
<span class="d-block" v-text="choice.label"></span>
|
||||
<small class="d-block" v-text="choice.detail"></small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,7 +117,8 @@
|
||||
<span v-text="padNumber(questionIndex + 1) + ': '"></span><span v-html="questionData(questionState).question"></span>
|
||||
</div>
|
||||
<div v-if="questionData(questionState).picture" class="question-picture">
|
||||
<img :src="'pictures/' + questionData(questionState).picture + '.svg'" :alt="questionData(questionState).pictureAlt">
|
||||
<img :src="'pictures/' + questionData(questionState).picture + '.svg'"
|
||||
:alt="pictureAlt(questionData(questionState).picture, 'Abbildung zu Frage ' + questionState.id)">
|
||||
</div>
|
||||
<div class="answers">
|
||||
<button v-for="(sourceIndex, answerIndex) in questionState.order" :key="answerIndex" type="button"
|
||||
@@ -125,7 +128,8 @@
|
||||
v-html="questionData(questionState).answers[sourceIndex].text"></span>
|
||||
<span v-if="questionData(questionState).answers[sourceIndex].picture" class="question-picture">
|
||||
<img :src="'pictures/' + questionData(questionState).answers[sourceIndex].picture + '.svg'"
|
||||
:alt="questionData(questionState).answers[sourceIndex].pictureAlt">
|
||||
:alt="pictureAlt(questionData(questionState).answers[sourceIndex].picture,
|
||||
'Abbildung zu Antwort ' + answerLabels[answerIndex] + ' bei Frage ' + questionState.id)">
|
||||
</span>
|
||||
<span v-if="currentPart.status === 'evaluated' && sourceIndex === 0" class="result material-icons">check</span>
|
||||
<span v-else-if="currentPart.status === 'evaluated' && questionState.selected === answerIndex" class="result material-icons">clear</span>
|
||||
@@ -162,7 +166,7 @@
|
||||
<div class="border border-dark bg-white p-2">
|
||||
<h2 class="h5 text-center">Antwortbogen</h2>
|
||||
<div class="border-top border-bottom border-dark py-1 mb-2">
|
||||
<strong class="d-block" v-text="currentPartDefinition.formLabel"></strong>
|
||||
<strong class="d-block" v-text="currentPartDefinition.label"></strong>
|
||||
<small><span v-text="currentPartDefinition.minutes"></span> Minuten · 25 Fragen</small>
|
||||
</div>
|
||||
<table class="answer-table">
|
||||
|
||||
Reference in New Issue
Block a user