quizlib 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
quizlib-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.3
2
+ Name: quizlib
3
+ Version: 0.1.0
4
+ Summary: Generate quizzes from Python code.
5
+ Keywords: quiz,moodle,education,testing,markdown
6
+ Author: Hannes Knoll
7
+ Author-email: Hannes Knoll <git@hannesknoll.de>
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Education
10
+ Classifier: Topic :: Education :: Testing
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: typst>=0.14.7
14
+ Requires-Python: >=3.13
15
+ Project-URL: Repository, https://gitlab.com/hhoegl-tha/snp-rs/quizlib
16
+ Description-Content-Type: text/markdown
17
+
18
+ # quizlib
19
+
20
+ Generate quizzes from Python code.
21
+
22
+ ## Features
23
+
24
+ - **Supported Question Types**: Essay (ES), Multiple Choice (MC), True/False (TF), Cloze (CZ).
25
+ - **Renderers**: Moodle XML, Markdown
26
+ - **Python-centric**: Define quiz structure and content using native Python classes.
27
+
28
+ ## Installation
29
+
30
+ Install using uv:
31
+
32
+ ```sh
33
+ uv add quizlib
34
+ ```
35
+
36
+
37
+ **For development run**
38
+ ```sh
39
+ uv pip install -e .
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```python
45
+ from quizlib import Quiz, Essay, MultipleChoice, TrueFalse, Cloze
46
+ from quizlib.renderers import MoodleXMLRenderer
47
+
48
+ # Initialize quiz
49
+ quiz = Quiz(category="Example", info="Sample quiz")
50
+
51
+ # Add questions
52
+ quiz.add(Essay(name="q1", defgrade=5, text="Explain the concept of recursion."))
53
+
54
+ quiz.add(MultipleChoice(
55
+ name="q2",
56
+ text="Which of these is a Python keyword?",
57
+ answers=[(1, "yield"), (0, "function"), (0, "define")],
58
+ ))
59
+
60
+ # Render to Moodle XML
61
+ renderer = MoodleXMLRenderer()
62
+ xml_output = renderer.render(quiz)
63
+ print(xml_output)
64
+ ```
@@ -0,0 +1,47 @@
1
+ # quizlib
2
+
3
+ Generate quizzes from Python code.
4
+
5
+ ## Features
6
+
7
+ - **Supported Question Types**: Essay (ES), Multiple Choice (MC), True/False (TF), Cloze (CZ).
8
+ - **Renderers**: Moodle XML, Markdown
9
+ - **Python-centric**: Define quiz structure and content using native Python classes.
10
+
11
+ ## Installation
12
+
13
+ Install using uv:
14
+
15
+ ```sh
16
+ uv add quizlib
17
+ ```
18
+
19
+
20
+ **For development run**
21
+ ```sh
22
+ uv pip install -e .
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```python
28
+ from quizlib import Quiz, Essay, MultipleChoice, TrueFalse, Cloze
29
+ from quizlib.renderers import MoodleXMLRenderer
30
+
31
+ # Initialize quiz
32
+ quiz = Quiz(category="Example", info="Sample quiz")
33
+
34
+ # Add questions
35
+ quiz.add(Essay(name="q1", defgrade=5, text="Explain the concept of recursion."))
36
+
37
+ quiz.add(MultipleChoice(
38
+ name="q2",
39
+ text="Which of these is a Python keyword?",
40
+ answers=[(1, "yield"), (0, "function"), (0, "define")],
41
+ ))
42
+
43
+ # Render to Moodle XML
44
+ renderer = MoodleXMLRenderer()
45
+ xml_output = renderer.render(quiz)
46
+ print(xml_output)
47
+ ```
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "quizlib"
3
+ version = "0.1.0"
4
+ description = "Generate quizzes from Python code."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Hannes Knoll", email = "git@hannesknoll.de" }
8
+ ]
9
+ keywords = ["quiz", "moodle", "education", "testing", "markdown"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Education",
13
+ "Topic :: Education :: Testing",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.13",
16
+ ]
17
+ requires-python = ">=3.13"
18
+ dependencies = [
19
+ "typst>=0.14.7",
20
+ ]
21
+
22
+ [project.urls]
23
+ Repository = "https://gitlab.com/hhoegl-tha/snp-rs/quizlib"
24
+
25
+ [project.scripts]
26
+ quizlib = "quizlib.__main__:main"
27
+
28
+ [build-system]
29
+ requires = ["uv_build>=0.9.25,<0.10.0"]
30
+ build-backend = "uv_build"
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "pytest>=9.0.2",
35
+ "rich>=14.2.0",
36
+ "ruff>=0.14.13",
37
+ "ty>=0.0.12",
38
+ ]
@@ -0,0 +1,51 @@
1
+ """
2
+ quizlib - Generate quizzes from Python.
3
+
4
+ Example usage:
5
+
6
+ >>> from quizlib import Quiz, Essay, MultipleChoice, TrueFalse, Cloze
7
+ >>> from quizlib.renderers import MoodleXMLRenderer
8
+ >>>
9
+ >>> quiz = Quiz(category="MyQuiz", info="A sample quiz")
10
+ >>> quiz.add(Essay(name="q1", defgrade=5, text="Explain X..."))
11
+ >>> quiz.add(MultipleChoice(
12
+ >>> name="q2",
13
+ >>> text="Which is correct?",
14
+ >>> answers=[(1, "Correct"), (0, "Wrong A"), (0, "Wrong B")],
15
+ >>> ))
16
+ >>>
17
+ >>> renderer = MoodleXMLRenderer()
18
+ >>> print(renderer.render(quiz))
19
+ """
20
+
21
+ from quizlib.questions import (
22
+ CZ,
23
+ ES,
24
+ MC,
25
+ TF,
26
+ Cloze,
27
+ Essay,
28
+ MultipleChoice,
29
+ Question,
30
+ TrueFalse,
31
+ )
32
+ from quizlib.quiz import Quiz
33
+ from quizlib.types import QuestionType, Qtype
34
+
35
+ __all__ = [
36
+ # Core
37
+ "Quiz",
38
+ "Question",
39
+ "QuestionType",
40
+ "Qtype",
41
+ # Question types (full names)
42
+ "Essay",
43
+ "MultipleChoice",
44
+ "TrueFalse",
45
+ "Cloze",
46
+ # Question types (aliases)
47
+ "ES",
48
+ "MC",
49
+ "TF",
50
+ "CZ",
51
+ ]
@@ -0,0 +1,24 @@
1
+ """
2
+ quizlib question type implementations
3
+ """
4
+
5
+ from quizlib.questions.base import Question
6
+ from quizlib.questions.cloze import CZ, Cloze
7
+ from quizlib.questions.essay import ES, Essay
8
+ from quizlib.questions.multiple_choice import MC, MultipleChoice
9
+ from quizlib.questions.true_false import TF, TrueFalse
10
+
11
+ __all__ = [
12
+ # Protocol
13
+ "Question",
14
+ # Full names
15
+ "Essay",
16
+ "MultipleChoice",
17
+ "TrueFalse",
18
+ "Cloze",
19
+ # Short aliases
20
+ "ES",
21
+ "MC",
22
+ "TF",
23
+ "CZ",
24
+ ]
@@ -0,0 +1,16 @@
1
+ """
2
+ quizlib base protocol for questions
3
+ """
4
+
5
+ from typing import Protocol, runtime_checkable
6
+
7
+ from quizlib.types import QuestionType
8
+
9
+
10
+ @runtime_checkable
11
+ class Question(Protocol):
12
+ """Protocol that all question types must implement."""
13
+
14
+ name: str
15
+ text: str
16
+ typ: QuestionType
@@ -0,0 +1,29 @@
1
+ """
2
+ quizlib cloze question type
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from quizlib.types import QuestionType
8
+
9
+
10
+ @dataclass
11
+ class Cloze:
12
+ """Cloze (fill-in-the-blank) question.
13
+
14
+ The text contains special Moodle cloze syntax for blanks, e.g.:
15
+ - {:MRH:=Ja#correct~Nein#incorrect} for multiple choice
16
+ - {:NUMERICAL:=42:0} for numeric answers
17
+
18
+ Attributes:
19
+ name: Question identifier/title.
20
+ text: Question text with embedded cloze syntax.
21
+ """
22
+
23
+ name: str
24
+ text: str
25
+ typ: QuestionType = field(default=QuestionType.CLOZE, init=False)
26
+
27
+
28
+ # Backwards-compatible alias
29
+ CZ = Cloze
@@ -0,0 +1,32 @@
1
+ """
2
+ quizlib essay question type
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from quizlib.types import QuestionType
8
+ from quizlib.utils import DEFAULT_ESSAY_LINES
9
+
10
+
11
+ @dataclass
12
+ class Essay:
13
+ """Essay (free-text) question.
14
+
15
+ Attributes:
16
+ name: Question identifier/title.
17
+ defgrade: Points for this question.
18
+ text: Question text in Markdown format.
19
+ lines: Number of lines for the response box.
20
+ graderinfo: Information for the grader (not shown to students).
21
+ """
22
+
23
+ name: str
24
+ defgrade: int | float
25
+ text: str
26
+ lines: int = DEFAULT_ESSAY_LINES
27
+ graderinfo: str = ""
28
+ typ: QuestionType = field(default=QuestionType.ESSAY, init=False)
29
+
30
+
31
+ # Backwards-compatible alias
32
+ ES = Essay
@@ -0,0 +1,50 @@
1
+ """
2
+ quizlib multiple choice question type
3
+ """
4
+
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass, field
7
+
8
+ from quizlib.types import MCAnswer, QuestionType
9
+
10
+
11
+ @dataclass
12
+ class MultipleChoice:
13
+ """Multiple choice question.
14
+
15
+ Supports single answer (radio buttons) or multiple answers (checkboxes).
16
+ The mode is automatically determined by the number of correct answers.
17
+
18
+ Attributes:
19
+ name: Question identifier/title.
20
+ text: Question text in Markdown format.
21
+ answers: Sequence of (is_correct, answer_text) tuples.
22
+ Accepts list or tuple. is_correct is 1 for correct, 0 for incorrect.
23
+ defgrade: Points for this question. Defaults to number of correct answers.
24
+ """
25
+
26
+ name: str
27
+ text: str
28
+ answers: Sequence[MCAnswer]
29
+ defgrade: int | float | None = None
30
+ typ: QuestionType = field(default=QuestionType.MULTIPLE_CHOICE, init=False)
31
+
32
+ def __post_init__(self) -> None:
33
+ if not isinstance(self.answers, list):
34
+ object.__setattr__(self, "answers", list(self.answers))
35
+ if self.defgrade is None:
36
+ self.defgrade = self.n_correct
37
+
38
+ @property
39
+ def n_correct(self) -> int:
40
+ """Number of correct answers."""
41
+ return sum(1 for is_correct, _ in self.answers if is_correct == 1)
42
+
43
+ @property
44
+ def is_single(self) -> bool:
45
+ """True if only one answer is correct (radio button mode)."""
46
+ return self.n_correct == 1
47
+
48
+
49
+ # Backwards-compatible alias
50
+ MC = MultipleChoice
@@ -0,0 +1,27 @@
1
+ """
2
+ quizlib True/False question type
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from quizlib.types import QuestionType
8
+
9
+
10
+ @dataclass
11
+ class TrueFalse:
12
+ """True/False question.
13
+
14
+ Attributes:
15
+ name: Question identifier/title.
16
+ text: Question text in Markdown format.
17
+ correct_answer: True if the correct answer is "true", False otherwise.
18
+ """
19
+
20
+ name: str
21
+ text: str
22
+ correct_answer: bool
23
+ typ: QuestionType = field(default=QuestionType.TRUE_FALSE, init=False)
24
+
25
+
26
+ # Backwards-compatible alias
27
+ TF = TrueFalse
@@ -0,0 +1,71 @@
1
+ """
2
+ quizli quiz container class
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ from collections.abc import Callable, Iterable, Iterator
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+ from quizlib.questions.base import Question
13
+
14
+
15
+ @dataclass
16
+ class Quiz:
17
+ """A collection of quiz questions with builder pattern support."""
18
+
19
+ category: str
20
+ info: str = ""
21
+ questions: list[Question] = field(default_factory=list)
22
+
23
+ def add(self, question: Question) -> Quiz:
24
+ """Add a question. Returns self for chaining."""
25
+ self.questions.append(question)
26
+ return self
27
+
28
+ def add_all(self, questions: Iterable[Question]) -> Quiz:
29
+ """Add multiple questions. Returns self for chaining."""
30
+ self.questions.extend(questions)
31
+ return self
32
+
33
+ def sort(
34
+ self,
35
+ *,
36
+ key: Callable[[Question], Any] | None = None,
37
+ reverse: bool = False,
38
+ ) -> Quiz:
39
+ """Sort questions in place. Default key is name. Returns self for chaining."""
40
+ self.questions.sort(key=key or (lambda q: q.name), reverse=reverse)
41
+ return self
42
+
43
+ def filter(self, pattern: str) -> Quiz:
44
+ """Return a NEW Quiz with only questions matching the regex pattern."""
45
+ regex = re.compile(pattern)
46
+ filtered = [q for q in self.questions if regex.search(q.name)]
47
+ return Quiz(
48
+ category=self.category,
49
+ info=self.info,
50
+ questions=filtered,
51
+ )
52
+
53
+ def to_xml(self) -> str:
54
+ """Render the quiz to Moodle XML format."""
55
+ from quizlib.renderers import MoodleXMLRenderer
56
+
57
+ return MoodleXMLRenderer().render(self)
58
+
59
+ def to_markdown(self) -> str:
60
+ """Render the quiz to Markdown format."""
61
+ from quizlib.renderers import MarkdownRenderer
62
+
63
+ return MarkdownRenderer().render(self)
64
+
65
+ def __len__(self) -> int:
66
+ """Number of questions in the quiz."""
67
+ return len(self.questions)
68
+
69
+ def __iter__(self) -> Iterator[Question]:
70
+ """Iterate over questions."""
71
+ return iter(self.questions)
@@ -0,0 +1,13 @@
1
+ """
2
+ quizlib renderer implementations
3
+ """
4
+
5
+ from quizlib.renderers.base import Renderer
6
+ from quizlib.renderers.moodle_xml import MoodleXMLRenderer
7
+ from quizlib.renderers.markdown import MarkdownRenderer
8
+
9
+ __all__ = [
10
+ "Renderer",
11
+ "MoodleXMLRenderer",
12
+ "MarkdownRenderer",
13
+ ]
@@ -0,0 +1,16 @@
1
+ """
2
+ quizlib base protocol for renderers
3
+ """
4
+
5
+ from typing import TYPE_CHECKING, Protocol
6
+
7
+ if TYPE_CHECKING:
8
+ from quizlib.quiz import Quiz
9
+
10
+
11
+ class Renderer(Protocol):
12
+ """Protocol that all renderers must implement."""
13
+
14
+ def render(self, quiz: "Quiz") -> str:
15
+ """Render a quiz to the target format."""
16
+ ...
@@ -0,0 +1,97 @@
1
+ """
2
+ quizlib markdown renderer
3
+ """
4
+
5
+ from typing import TYPE_CHECKING
6
+ import re
7
+
8
+ from quizlib.questions.cloze import Cloze
9
+ from quizlib.questions.essay import Essay
10
+ from quizlib.questions.multiple_choice import MultipleChoice
11
+ from quizlib.questions.true_false import TrueFalse
12
+
13
+ if TYPE_CHECKING:
14
+ from quizlib.questions.base import Question
15
+ from quizlib.quiz import Quiz
16
+
17
+
18
+ class MarkdownRenderer:
19
+ """Renders quizzes to Markdown format."""
20
+
21
+ def render(self, quiz: "Quiz") -> str:
22
+ """Render a quiz to Markdown."""
23
+ parts: list[str] = []
24
+
25
+ parts.append(f"# Quiz: {quiz.category}")
26
+ if quiz.info:
27
+ parts.append(f"\n{quiz.info}")
28
+
29
+ parts.append("\n---\n")
30
+
31
+ for i, question in enumerate(quiz.questions, 1):
32
+ parts.append(self._render_question(question, i))
33
+ parts.append("\n---\n")
34
+
35
+ return "\n".join(parts)
36
+
37
+ def _render_question(self, question: "Question", index: int) -> str:
38
+ """Render a single question."""
39
+ match question:
40
+ case Essay():
41
+ return self._render_essay(question, index)
42
+ case MultipleChoice():
43
+ return self._render_mc(question, index)
44
+ case TrueFalse():
45
+ return self._render_tf(question, index)
46
+ case Cloze():
47
+ return self._render_cloze(question, index)
48
+ case _:
49
+ raise TypeError(f"Unknown question type: {type(question)}")
50
+
51
+ def _render_header(
52
+ self, index: int, name: str, points: float | int | None = None
53
+ ) -> str:
54
+ """Render question header."""
55
+ header = f"## {index}. {name}"
56
+ if points is not None:
57
+ header += f" ({points} Punkte)"
58
+ return header + "\n"
59
+
60
+ def _render_essay(self, q: Essay, index: int) -> str:
61
+ parts = [self._render_header(index, q.name, q.defgrade)]
62
+ parts.append(q.name)
63
+ parts.append("\n\n> Antwort:\n")
64
+
65
+ for _ in range(q.lines):
66
+ parts.append("> " + "_" * 79 + "\\")
67
+ return "\n".join(parts)
68
+
69
+ def _render_mc(self, q: MultipleChoice, index: int) -> str:
70
+ symbol = "( )" if q.is_single else "[ ]"
71
+
72
+ lines = [self._render_header(index, q.name, q.defgrade), q.text]
73
+ for _, text in q.answers:
74
+ if text.startswith("```"):
75
+ text = f"&nbsp;\n\n{text}"
76
+
77
+ lines.append(f"- {symbol} {text}")
78
+
79
+ return "\n".join(lines)
80
+
81
+ def _render_tf(self, q: TrueFalse, index: int) -> str:
82
+ parts = [self._render_header(index, q.name)]
83
+ parts.append(q.name + "\n")
84
+
85
+ parts.append("- [ ] True")
86
+ parts.append("- [ ] False")
87
+
88
+ return "\n".join(parts)
89
+
90
+ def _render_cloze(self, q: Cloze, index: int) -> str:
91
+ parts = [self._render_header(index, q.name)]
92
+
93
+ # Simplify Cloze syntax: replace {...} with [__________]
94
+ simplified_text = re.sub(r"\{[^}]+\}", "[__________]", q.name)
95
+
96
+ parts.append(simplified_text)
97
+ return "\n".join(parts)
@@ -0,0 +1,194 @@
1
+ """
2
+ quizlib Moodle XML format renderer
3
+ """
4
+
5
+ import re
6
+ import html
7
+ from typing import TYPE_CHECKING
8
+ import xml.etree.ElementTree as ET
9
+
10
+ from quizlib.questions.cloze import Cloze
11
+ from quizlib.questions.essay import Essay
12
+ from quizlib.questions.multiple_choice import MultipleChoice
13
+ from quizlib.questions.true_false import TrueFalse
14
+
15
+ if TYPE_CHECKING:
16
+ from quizlib.questions.base import Question
17
+ from quizlib.quiz import Quiz
18
+
19
+
20
+ class MoodleXMLRenderer:
21
+ """Renders quizzes to Moodle XML format."""
22
+
23
+ def render(self, quiz: "Quiz") -> str:
24
+ """Render a complete quiz to Moodle XML."""
25
+ root = ET.Element("quiz")
26
+
27
+ q_cat = ET.SubElement(root, "question", type="category")
28
+ cat = ET.SubElement(q_cat, "category")
29
+ ET.SubElement(cat, "text").text = f"$course$/top/{quiz.category}"
30
+
31
+ info = ET.SubElement(q_cat, "info", format="moodle_auto_format")
32
+ ET.SubElement(info, "text").text = quiz.info
33
+ ET.SubElement(q_cat, "idnumber")
34
+
35
+ for question in quiz.questions:
36
+ self._render_question(root, question)
37
+
38
+ ET.indent(root, space=" ", level=0)
39
+ xml_str = ET.tostring(root, encoding="unicode", xml_declaration=True)
40
+ return self._post_process_cdata(xml_str)
41
+
42
+ def _post_process_cdata(self, xml_str: str) -> str:
43
+ """Wrap code blocks in answers with CDATA."""
44
+ pattern = re.compile(r"(<answer[^>]*>\s*<text>)(.*?)(</text>)", re.DOTALL)
45
+
46
+ def replacement(match):
47
+ prefix = match.group(1)
48
+ body = match.group(2)
49
+ suffix = match.group(3)
50
+
51
+ if "```" in body:
52
+ unescaped_body = html.unescape(body)
53
+ return f"{prefix}<![CDATA[{unescaped_body}]]>{suffix}"
54
+ return match.group(0)
55
+
56
+ return pattern.sub(replacement, xml_str)
57
+
58
+ def _render_question(self, parent: ET.Element, question: "Question") -> None:
59
+ """Render a single question."""
60
+ match question:
61
+ case Essay():
62
+ self._render_essay(parent, question)
63
+ case MultipleChoice():
64
+ self._render_mc(parent, question)
65
+ case TrueFalse():
66
+ self._render_tf(parent, question)
67
+ case Cloze():
68
+ self._render_cloze(parent, question)
69
+ case _:
70
+ raise TypeError(f"Unknown question type: {type(question)}")
71
+
72
+ def _render_essay(self, parent: ET.Element, q: Essay) -> None:
73
+ q_elem = ET.SubElement(parent, "question", type="essay")
74
+
75
+ name = ET.SubElement(q_elem, "name")
76
+ ET.SubElement(name, "text").text = q.name
77
+
78
+ qtext = ET.SubElement(q_elem, "questiontext", format="markdown")
79
+ ET.SubElement(qtext, "text").text = q.text
80
+
81
+ gen_feedback = ET.SubElement(q_elem, "generalfeedback", format="markdown")
82
+ ET.SubElement(gen_feedback, "text")
83
+
84
+ ET.SubElement(q_elem, "defaultgrade").text = str(q.defgrade)
85
+ ET.SubElement(q_elem, "penalty").text = "0.0000000"
86
+ ET.SubElement(q_elem, "hidden").text = "0"
87
+ ET.SubElement(q_elem, "idnumber")
88
+ ET.SubElement(q_elem, "responseformat").text = "monospaced"
89
+ ET.SubElement(q_elem, "responserequired").text = "1"
90
+ ET.SubElement(q_elem, "responsefieldlines").text = str(q.lines)
91
+ ET.SubElement(q_elem, "attachments").text = "0"
92
+ ET.SubElement(q_elem, "attachmentsrequired").text = "0"
93
+ ET.SubElement(q_elem, "maxbytes").text = "0"
94
+ ET.SubElement(q_elem, "filetypeslist")
95
+
96
+ graderinfo = ET.SubElement(q_elem, "graderinfo", format="markdown")
97
+ ET.SubElement(graderinfo, "text").text = q.graderinfo
98
+
99
+ resp_template = ET.SubElement(q_elem, "responsetemplate", format="markdown")
100
+ ET.SubElement(resp_template, "text")
101
+
102
+ def _render_mc(self, parent: ET.Element, q: MultipleChoice) -> None:
103
+ q_elem = ET.SubElement(parent, "question", type="multichoice")
104
+
105
+ name = ET.SubElement(q_elem, "name")
106
+ ET.SubElement(name, "text").text = q.name
107
+
108
+ qtext = ET.SubElement(q_elem, "questiontext", format="markdown")
109
+ ET.SubElement(qtext, "text").text = q.text
110
+
111
+ gen_feedback = ET.SubElement(
112
+ q_elem, "generalfeedback", format="moodle_auto_format"
113
+ )
114
+ ET.SubElement(gen_feedback, "text")
115
+
116
+ ET.SubElement(q_elem, "defaultgrade").text = str(q.defgrade)
117
+ ET.SubElement(q_elem, "penalty").text = "0.0000000"
118
+ ET.SubElement(q_elem, "hidden").text = "0"
119
+ ET.SubElement(q_elem, "idnumber")
120
+ ET.SubElement(q_elem, "single").text = "true" if q.is_single else "false"
121
+ ET.SubElement(q_elem, "shuffleanswers").text = "true"
122
+ ET.SubElement(q_elem, "answernumbering").text = "abc"
123
+ ET.SubElement(q_elem, "showstandardinstruction").text = "0"
124
+
125
+ for tag, text in [
126
+ ("correctfeedback", "Die Antwort ist richtig."),
127
+ ("partiallycorrectfeedback", "Die Antwort ist teilweise richtig."),
128
+ ("incorrectfeedback", "Die Antwort ist falsch."),
129
+ ]:
130
+ fb = ET.SubElement(q_elem, tag, format="markdown")
131
+ ET.SubElement(fb, "text").text = text
132
+
133
+ ET.SubElement(q_elem, "shownumcorrect")
134
+
135
+ for is_correct, answer_text in q.answers:
136
+ fraction = f"{100 / q.n_correct:8.6f}" if is_correct == 1 else "0"
137
+ answer = ET.SubElement(
138
+ q_elem, "answer", fraction=fraction, format="markdown"
139
+ )
140
+ ET.SubElement(answer, "text").text = answer_text
141
+ feedback = ET.SubElement(answer, "feedback", format="markdown")
142
+ ET.SubElement(feedback, "text")
143
+
144
+ def _render_tf(self, parent: ET.Element, q: TrueFalse) -> None:
145
+ q_elem = ET.SubElement(parent, "question", type="truefalse")
146
+
147
+ name = ET.SubElement(q_elem, "name")
148
+ ET.SubElement(name, "text").text = q.name
149
+
150
+ qtext = ET.SubElement(q_elem, "questiontext", format="markdown")
151
+ ET.SubElement(qtext, "text").text = q.text
152
+
153
+ gen_feedback = ET.SubElement(q_elem, "generalfeedback", format="markdown")
154
+ ET.SubElement(gen_feedback, "text")
155
+
156
+ ET.SubElement(q_elem, "defaultgrade").text = "1.0000000"
157
+ ET.SubElement(q_elem, "penalty").text = "1.0000000"
158
+ ET.SubElement(q_elem, "hidden").text = "0"
159
+ ET.SubElement(q_elem, "idnumber")
160
+
161
+ if q.correct_answer:
162
+ correct, wrong = "true", "false"
163
+ else:
164
+ correct, wrong = "false", "true"
165
+
166
+ # Wrong answer (fraction 0)
167
+ ans_wrong = ET.SubElement(
168
+ q_elem, "answer", fraction="0", format="moodle_auto_format"
169
+ )
170
+ ET.SubElement(ans_wrong, "text").text = wrong
171
+ feed_wrong = ET.SubElement(ans_wrong, "feedback", format="markdown")
172
+ ET.SubElement(feed_wrong, "text")
173
+
174
+ # Correct answer (fraction 100)
175
+ ans_correct = ET.SubElement(q_elem, "answer", fraction="100", format="markdown")
176
+ ET.SubElement(ans_correct, "text").text = correct
177
+ feed_correct = ET.SubElement(ans_correct, "feedback", format="markdown")
178
+ ET.SubElement(feed_correct, "text")
179
+
180
+ def _render_cloze(self, parent: ET.Element, q: Cloze) -> None:
181
+ q_elem = ET.SubElement(parent, "question", type="cloze")
182
+
183
+ name = ET.SubElement(q_elem, "name")
184
+ ET.SubElement(name, "text").text = q.name
185
+
186
+ qtext = ET.SubElement(q_elem, "questiontext", format="markdown")
187
+ ET.SubElement(qtext, "text").text = q.text
188
+
189
+ gen_feedback = ET.SubElement(q_elem, "generalfeedback", format="markdown")
190
+ ET.SubElement(gen_feedback, "text")
191
+
192
+ ET.SubElement(q_elem, "penalty").text = "0.3333333"
193
+ ET.SubElement(q_elem, "hidden").text = "0"
194
+ ET.SubElement(q_elem, "idnumber")
@@ -0,0 +1,21 @@
1
+ """
2
+ quizlib type definitions
3
+ """
4
+
5
+ from enum import Enum, auto
6
+
7
+
8
+ class QuestionType(Enum):
9
+ """Question type enumeration."""
10
+
11
+ MULTIPLE_CHOICE = auto()
12
+ ESSAY = auto()
13
+ TRUE_FALSE = auto()
14
+ CLOZE = auto()
15
+
16
+
17
+ # Backwards-compatible alias
18
+ Qtype = QuestionType
19
+
20
+ # Type alias for MC answers: (is_correct: bool, answer_text: str)
21
+ type MCAnswer = tuple[bool, str]
@@ -0,0 +1,6 @@
1
+ """
2
+ quizlib utility functions and constants
3
+ """
4
+
5
+ DEFAULT_MC_GRADE: int | None = None
6
+ DEFAULT_ESSAY_LINES: int = 5