excel2moodle 0.5.1__tar.gz → 0.5.2__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.
Files changed (61) hide show
  1. {excel2moodle-0.5.1/excel2moodle.egg-info → excel2moodle-0.5.2}/PKG-INFO +41 -1
  2. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/README.md +40 -0
  3. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/dataStructure.py +3 -3
  4. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/question.py +1 -1
  5. excel2moodle-0.5.2/excel2moodle/question_types/cloze.py +342 -0
  6. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/question_types/nfm.py +5 -4
  7. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/appUi.py +18 -22
  8. {excel2moodle-0.5.1 → excel2moodle-0.5.2/excel2moodle.egg-info}/PKG-INFO +41 -1
  9. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle.egg-info/SOURCES.txt +0 -11
  10. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/pyproject.toml +10 -1
  11. excel2moodle-0.5.2/test/test_nfmParsing.py +85 -0
  12. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/test/test_picture.py +16 -14
  13. excel2moodle-0.5.1/docs/_build/html/exampleQuestions.html +0 -599
  14. excel2moodle-0.5.1/docs/_build/html/excel2moodle.core.html +0 -1687
  15. excel2moodle-0.5.1/docs/_build/html/excel2moodle.extra.html +0 -237
  16. excel2moodle-0.5.1/docs/_build/html/excel2moodle.html +0 -467
  17. excel2moodle-0.5.1/docs/_build/html/excel2moodle.ui.html +0 -667
  18. excel2moodle-0.5.1/docs/_build/html/genindex.html +0 -908
  19. excel2moodle-0.5.1/docs/_build/html/howto.html +0 -384
  20. excel2moodle-0.5.1/docs/_build/html/index.html +0 -228
  21. excel2moodle-0.5.1/docs/_build/html/py-modindex.html +0 -228
  22. excel2moodle-0.5.1/docs/_build/html/search.html +0 -133
  23. excel2moodle-0.5.1/docs/_build/html/userReference.html +0 -444
  24. excel2moodle-0.5.1/excel2moodle/question_types/cloze.py +0 -207
  25. excel2moodle-0.5.1/test/test_nfmParsing.py +0 -30
  26. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/LICENSE +0 -0
  27. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/MANIFEST.in +0 -0
  28. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/__init__.py +0 -0
  29. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/__main__.py +0 -0
  30. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/__init__.py +0 -0
  31. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/category.py +0 -0
  32. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/etHelpers.py +0 -0
  33. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/exceptions.py +0 -0
  34. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/globals.py +0 -0
  35. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/numericMultiQ.py +0 -0
  36. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/parser.py +0 -0
  37. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/settings.py +0 -0
  38. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/stringHelpers.py +0 -0
  39. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/validator.py +0 -0
  40. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/extra/__init__.py +0 -0
  41. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/extra/equationVerification.py +0 -0
  42. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/logger.py +0 -0
  43. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/question_types/__init__.py +0 -0
  44. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/question_types/mc.py +0 -0
  45. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/question_types/nf.py +0 -0
  46. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/UI_equationChecker.py +0 -0
  47. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/UI_exportSettingsDialog.py +0 -0
  48. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/UI_mainWindow.py +0 -0
  49. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/UI_variantDialog.py +0 -0
  50. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/__init__.py +0 -0
  51. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/dialogs.py +0 -0
  52. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/equationChecker.py +0 -0
  53. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/treewidget.py +0 -0
  54. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/windowDoc.py +0 -0
  55. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle.egg-info/dependency_links.txt +0 -0
  56. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle.egg-info/entry_points.txt +0 -0
  57. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle.egg-info/requires.txt +0 -0
  58. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle.egg-info/top_level.txt +0 -0
  59. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/setup.cfg +0 -0
  60. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/test/test_parseQuestion.py +0 -0
  61. {excel2moodle-0.5.1 → excel2moodle-0.5.2}/test/test_questionDataGet.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: excel2moodle
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: A package for converting questions from a spreadsheet, to valid moodle-xml
5
5
  Author: Jakob Bosse
6
6
  License-Expression: GPL-3.0-or-later
@@ -81,6 +81,46 @@ If You want to support my work as well, you can by me a [coffee](https://ko-fi.c
81
81
 
82
82
  # Changelogs
83
83
 
84
+ ## 0.5.2 (2025-06-30)
85
+ Extended Documentation and bugfix for import Module
86
+
87
+ ### bugfix (2 changes)
88
+
89
+ - [Default question variant saved and reused.](https://gitlab.com/jbosse3/excel2moodle/-/commit/097705ba83727463a9b27cd76e99814a7ecf28df)
90
+ - [bugfix: Import module working again](https://gitlab.com/jbosse3/excel2moodle/-/commit/5f293970bcdac3858911cdcc102b72714af057bd)
91
+
92
+ ### documentation (1 change)
93
+
94
+ - [documentation: Added how to build question database](https://gitlab.com/jbosse3/excel2moodle/-/commit/71ceb122aa37e8bf2735b659359ae37d81017599)
95
+
96
+ ### feature (1 change)
97
+
98
+ - [Implemented MC question string method](https://gitlab.com/jbosse3/excel2moodle/-/commit/c4f2081d0000ee60322fe8eec8468fa3317ce7be)
99
+
100
+ ### improvement (1 change)
101
+
102
+ - [Implemented ClozePart object](https://gitlab.com/jbosse3/excel2moodle/-/commit/878f90f45e37421384c4f8f602115e7596b4ceb9)
103
+
104
+ ## 0.5.2 (2025-06-30)
105
+ Extended Documentation and bugfix for import Module
106
+
107
+ ### bugfix (2 changes)
108
+
109
+ - [Default question variant saved and reused.](https://gitlab.com/jbosse3/excel2moodle/-/commit/097705ba83727463a9b27cd76e99814a7ecf28df)
110
+ - [bugfix: Import module working again](https://gitlab.com/jbosse3/excel2moodle/-/commit/5f293970bcdac3858911cdcc102b72714af057bd)
111
+
112
+ ### documentation (1 change)
113
+
114
+ - [documentation: Added how to build question database](https://gitlab.com/jbosse3/excel2moodle/-/commit/71ceb122aa37e8bf2735b659359ae37d81017599)
115
+
116
+ ### feature (1 change)
117
+
118
+ - [Implemented MC question string method](https://gitlab.com/jbosse3/excel2moodle/-/commit/c4f2081d0000ee60322fe8eec8468fa3317ce7be)
119
+
120
+ ### improvement (1 change)
121
+
122
+ - [Implemented ClozePart object](https://gitlab.com/jbosse3/excel2moodle/-/commit/878f90f45e37421384c4f8f602115e7596b4ceb9)
123
+
84
124
  ## 0.5.1 (2025-06-24)
85
125
  Minor docs improvement and question variant bugfix
86
126
 
@@ -60,6 +60,46 @@ If You want to support my work as well, you can by me a [coffee](https://ko-fi.c
60
60
 
61
61
  # Changelogs
62
62
 
63
+ ## 0.5.2 (2025-06-30)
64
+ Extended Documentation and bugfix for import Module
65
+
66
+ ### bugfix (2 changes)
67
+
68
+ - [Default question variant saved and reused.](https://gitlab.com/jbosse3/excel2moodle/-/commit/097705ba83727463a9b27cd76e99814a7ecf28df)
69
+ - [bugfix: Import module working again](https://gitlab.com/jbosse3/excel2moodle/-/commit/5f293970bcdac3858911cdcc102b72714af057bd)
70
+
71
+ ### documentation (1 change)
72
+
73
+ - [documentation: Added how to build question database](https://gitlab.com/jbosse3/excel2moodle/-/commit/71ceb122aa37e8bf2735b659359ae37d81017599)
74
+
75
+ ### feature (1 change)
76
+
77
+ - [Implemented MC question string method](https://gitlab.com/jbosse3/excel2moodle/-/commit/c4f2081d0000ee60322fe8eec8468fa3317ce7be)
78
+
79
+ ### improvement (1 change)
80
+
81
+ - [Implemented ClozePart object](https://gitlab.com/jbosse3/excel2moodle/-/commit/878f90f45e37421384c4f8f602115e7596b4ceb9)
82
+
83
+ ## 0.5.2 (2025-06-30)
84
+ Extended Documentation and bugfix for import Module
85
+
86
+ ### bugfix (2 changes)
87
+
88
+ - [Default question variant saved and reused.](https://gitlab.com/jbosse3/excel2moodle/-/commit/097705ba83727463a9b27cd76e99814a7ecf28df)
89
+ - [bugfix: Import module working again](https://gitlab.com/jbosse3/excel2moodle/-/commit/5f293970bcdac3858911cdcc102b72714af057bd)
90
+
91
+ ### documentation (1 change)
92
+
93
+ - [documentation: Added how to build question database](https://gitlab.com/jbosse3/excel2moodle/-/commit/71ceb122aa37e8bf2735b659359ae37d81017599)
94
+
95
+ ### feature (1 change)
96
+
97
+ - [Implemented MC question string method](https://gitlab.com/jbosse3/excel2moodle/-/commit/c4f2081d0000ee60322fe8eec8468fa3317ce7be)
98
+
99
+ ### improvement (1 change)
100
+
101
+ - [Implemented ClozePart object](https://gitlab.com/jbosse3/excel2moodle/-/commit/878f90f45e37421384c4f8f602115e7596b4ceb9)
102
+
63
103
  ## 0.5.1 (2025-06-24)
64
104
  Minor docs improvement and question variant bugfix
65
105
 
@@ -131,7 +131,7 @@ class QuestionDB:
131
131
  if Tags.IMPORTMODULE in self.settings:
132
132
  logger.warning(
133
133
  "Appending: %s to sys.path. All names defined by it will be usable",
134
- sheetPath,
134
+ sheetPath.parent,
135
135
  )
136
136
  sys.path.append(str(sheetPath.parent))
137
137
  if Tags.PICTURESUBFOLDER not in self.settings:
@@ -286,7 +286,7 @@ class QuestionDB:
286
286
  self.signals.categoryQuestionsReady.emit(category)
287
287
 
288
288
  @classmethod
289
- def setupAndParseQuestion(cls, category: Category, qNumber: int) -> Question | None:
289
+ def setupAndParseQuestion(cls, category: Category, qNumber: int) -> Question:
290
290
  """Check if the Question Data is valid. Then parse it.
291
291
 
292
292
  The Question data is accessed from `category.dataframe` via its number
@@ -322,7 +322,7 @@ class QuestionDB:
322
322
  question = QuestionTypeMapping[qtype].create(category, validData)
323
323
  if question.isParsed:
324
324
  locallogger.info("Question already parsed")
325
- return None
325
+ return question
326
326
  if isinstance(question, NFQuestion):
327
327
  cls.nfParser.setup(question)
328
328
  locallogger.debug("setup a new NF parser ")
@@ -161,7 +161,7 @@ class Question:
161
161
  def assemble(self, variant=0) -> None:
162
162
  """Assemble the question to the valid xml Tree."""
163
163
  mainText = self._getTextElement()
164
- self.logger.debug("Starting assembly")
164
+ self.logger.info("Starting assembly variant: %s", variant)
165
165
  self._assembleAnswer(variant=variant)
166
166
  textParts = self._assembleText(variant=variant)
167
167
  if hasattr(self, "picture") and self.picture.ready:
@@ -0,0 +1,342 @@
1
+ """Implementation of tde cloze question type.
2
+
3
+ This question type is like the NFM but supports multiple fields of answers.
4
+ All Answers are calculated off an equation using the same variables.
5
+ """
6
+
7
+ import logging
8
+ import math
9
+ import re
10
+ from typing import Literal, overload
11
+
12
+ import lxml.etree as ET
13
+
14
+ from excel2moodle.core.exceptions import QNotParsedException
15
+ from excel2moodle.core.globals import (
16
+ Tags,
17
+ TextElements,
18
+ )
19
+ from excel2moodle.core.question import ParametricQuestion
20
+ from excel2moodle.core.settings import Tags
21
+ from excel2moodle.question_types.nfm import NFMQuestionParser
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class ClozePart:
27
+ def __init__(
28
+ self,
29
+ question: ParametricQuestion,
30
+ text: list[str],
31
+ ) -> None:
32
+ self.question = question
33
+ self.text: list[ET.Element] = self._setupText(text)
34
+ if not self.text:
35
+ msg = f"Answer part for cloze question {self.question.id} is invalid without partText"
36
+ raise ValueError(msg)
37
+
38
+ @property
39
+ def points(self) -> float:
40
+ if hasattr(self, "_points"):
41
+ return self._points
42
+ return 0.0
43
+ self.question.logger.error("Invalid call to points of unparsed cloze part")
44
+ return 0.0
45
+
46
+ @points.setter
47
+ def points(self, points: float) -> None:
48
+ self._points = points if points > 0 else 0.0
49
+
50
+ @property
51
+ def typ(self) -> Literal["MC", "NFM"] | None:
52
+ if hasattr(self, "_typ"):
53
+ return self._typ
54
+ return None
55
+
56
+ @property
57
+ def mcAnswerString(self) -> str:
58
+ if hasattr(self, "_mcAnswer"):
59
+ return self._mcAnswer
60
+ msg = "No MC Answer was set"
61
+ raise ValueError(msg)
62
+
63
+ @mcAnswerString.setter
64
+ def mcAnswerString(self, answerString: str) -> None:
65
+ self._mcAnswer: str = answerString
66
+
67
+ def _setupText(self, text: list[str]) -> ET.Element:
68
+ textList: list[ET.Element] = []
69
+ for t in text:
70
+ textList.append(TextElements.PLEFT.create())
71
+ textList[-1].text = t
72
+ return textList
73
+
74
+ def setAnswer(
75
+ self,
76
+ equation: str | None = None,
77
+ trueAns: list[str] | None = None,
78
+ falseAns: list[str] | None = None,
79
+ ) -> bool:
80
+ if falseAns is not None:
81
+ self.falseAnswers: list[str] = falseAns
82
+ if trueAns is not None:
83
+ self.trueAnswers: list[str] = trueAns
84
+ if equation is not None:
85
+ self.equation: str = equation
86
+ check = False
87
+ t = hasattr(self, "trueAnswers")
88
+ f = hasattr(self, "falseAnswers")
89
+ eq = hasattr(self, "equation")
90
+ if t and f and not eq:
91
+ self._typ: Literal["MC", "NFM"] = "MC"
92
+ return True
93
+ if eq and not t and not f:
94
+ self._typ: Literal["MC", "NFM"] = "NFM"
95
+ self.nfResults: list[float] = []
96
+ return True
97
+ return False
98
+
99
+ def __repr__(self) -> str:
100
+ answers: str = (
101
+ self.equation
102
+ if self.typ == "NFM"
103
+ else f"{self.trueAnswers}\n {self.falseAnswers}"
104
+ )
105
+ return f"Cloze Part {self.typ}\n Answers: '{answers}'"
106
+
107
+
108
+ class ClozeQuestion(ParametricQuestion):
109
+ """Cloze Question Type."""
110
+
111
+ def __init__(self, *args, **kwargs) -> None:
112
+ super().__init__(*args, **kwargs)
113
+ self.questionParts: dict[int, ClozePart] = {}
114
+ self.questionTexts: list[ET.Element] = []
115
+
116
+ @property
117
+ def partsNum(self) -> int:
118
+ return len(self.questionParts)
119
+
120
+ @property
121
+ def points(self) -> float:
122
+ pts: float = 0
123
+ if self.isParsed:
124
+ for p in self.questionParts.values():
125
+ pts = pts + p.points
126
+ else:
127
+ pts = self.rawData.get(Tags.POINTS)
128
+ return pts
129
+
130
+ def _assembleAnswer(self, variant: int = 1) -> None:
131
+ for partNum, part in self.questionParts.items():
132
+ if part.typ == "MC":
133
+ ansStr = part.mcAnswerString
134
+ self.logger.info("MC answer part: %s ", ansStr)
135
+ elif part.typ == "NFM":
136
+ result = part.nfResults[variant - 1]
137
+ ansStr = ClozeQuestionParser.getNumericAnsStr(
138
+ result,
139
+ self.rawData.get(Tags.TOLERANCE),
140
+ wrongSignCount=self.rawData.get(Tags.WRONGSIGNPERCENT),
141
+ points=part.points,
142
+ )
143
+ self.logger.info("NF answer part: %s ", ansStr)
144
+ else:
145
+ msg = "Type of the answer part is invalid"
146
+ raise QNotParsedException(msg, self.id)
147
+ ul = TextElements.ULIST.create()
148
+ item = TextElements.LISTITEM.create()
149
+ item.text = ansStr
150
+ ul.append(item)
151
+ part.text.append(ul)
152
+ self.logger.debug("Appended part %s %s to main text", partNum, part)
153
+ part.text.append(ET.Element("hr"))
154
+ self.questionTexts.extend(part.text)
155
+
156
+ def _assembleText(self, variant=0) -> list[ET.Element]:
157
+ textParts = super()._assembleText(variant=variant)
158
+ self.logger.debug("Appending QuestionParts to main text")
159
+ textParts.extend(self.questionTexts)
160
+ return textParts
161
+
162
+
163
+ class ClozeQuestionParser(NFMQuestionParser):
164
+ """Parser for the cloze question type."""
165
+
166
+ def __init__(self, *args, **kwargs) -> None:
167
+ super().__init__(*args, **kwargs)
168
+ self.question: ClozeQuestion
169
+
170
+ def setup(self, question: ClozeQuestion) -> None:
171
+ self.question: ClozeQuestion = question
172
+ super().setup(question)
173
+
174
+ def _parseAnswers(self) -> None:
175
+ self._setupParts()
176
+ self._parseAnswerParts()
177
+
178
+ def _setupParts(self) -> None:
179
+ parts: dict[int, ClozePart] = {
180
+ self.getPartNumber(key): ClozePart(self.question, self.rawInput[key])
181
+ for key in self.rawInput
182
+ if key.startswith(Tags.QUESTIONPART)
183
+ }
184
+ partsNum = len(parts)
185
+ equations: dict[int, str] = self._getPartValues(Tags.RESULT)
186
+ trueAnsws: dict[int, list[str]] = self._getPartValues(Tags.TRUE)
187
+ falseAnsws: dict[int, list[str]] = self._getPartValues(Tags.FALSE)
188
+ points: dict[int, float] = self._getPartValues(Tags.POINTS)
189
+ for num, part in parts.items():
190
+ eq = equations.get(num)
191
+ true = trueAnsws.get(num)
192
+ false = falseAnsws.get(num)
193
+ part.setAnswer(equation=eq, trueAns=true, falseAns=false)
194
+ if len(points) == 0:
195
+ pts = round(self.rawInput.get(Tags.POINTS) / partsNum, 3)
196
+ for part in parts.values():
197
+ part.points = pts
198
+ elif len(points) != partsNum:
199
+ logger.warning(
200
+ "Some Answer parts are missing the points, they will get the standard points"
201
+ )
202
+ for num, part in parts.items():
203
+ p = points.get(num)
204
+ part.points = p if p is not None else self.rawInput.get(Tags.POINTS)
205
+
206
+ self.question.questionParts = parts
207
+
208
+ @overload
209
+ def _getPartValues(self, Tag: Literal[Tags.RESULT]) -> dict[int, str]: ...
210
+ @overload
211
+ def _getPartValues(self, Tag: Literal[Tags.POINTS]) -> dict[int, float]: ...
212
+ @overload
213
+ def _getPartValues(
214
+ self, Tag: Literal[Tags.TRUE, Tags.FALSE]
215
+ ) -> dict[int, list[str]]: ...
216
+ def _getPartValues(self, Tag):
217
+ tagValues: dict = {
218
+ self.getPartNumber(key): self.rawInput[key]
219
+ for key in self.rawInput
220
+ if key.startswith(Tag)
221
+ }
222
+ self.logger.warning("Found part data %s: %s", Tag, tagValues)
223
+ return tagValues
224
+
225
+ def _parseAnswerParts(self) -> None:
226
+ """Parse the numeric or MC result items."""
227
+ try:
228
+ bps = str(self.rawInput[Tags.BPOINTS])
229
+ except KeyError:
230
+ bps = None
231
+ number = 1
232
+ else:
233
+ varNames: list[str] = self._getVarsList(bps)
234
+ self.question.variables, number = self._getVariablesDict(varNames)
235
+ for variant in range(number):
236
+ self.setupAstIntprt(self.question.variables, variant)
237
+ for partNum, part in self.question.questionParts.items():
238
+ if part.typ == "NFM":
239
+ result = self._calculateNFMPartResult(part, partNum, variant)
240
+ part.nfResults.append(result)
241
+ logger.debug("Appended NF part %s result: %s", partNum, result)
242
+ elif part.typ == "MC":
243
+ ansStr = self.getMCAnsStr(
244
+ part.trueAnswers, part.falseAnswers, points=part.points
245
+ )
246
+ part.mcAnswerString = ansStr
247
+ logger.debug("Appended MC part %s: %s", partNum, ansStr)
248
+ self._setVariants(number)
249
+
250
+ def _calculateNFMPartResult(
251
+ self, part: ClozePart, partNum: int, variant: int
252
+ ) -> float:
253
+ result = self.astEval(part.equation)
254
+ if isinstance(result, float):
255
+ try:
256
+ firstResult = self.rawInput[f"{Tags.FIRSTRESULT}:{partNum}"]
257
+ except KeyError:
258
+ firstResult = 0.0
259
+ if variant == 0 and not math.isclose(result, firstResult, rel_tol=0.002):
260
+ self.logger.warning(
261
+ "The calculated result %s differs from given firstResult: %s",
262
+ result,
263
+ firstResult,
264
+ )
265
+ return result
266
+ msg = f"The expression {part.equation} could not be evaluated."
267
+ raise QNotParsedException(msg, self.question.id)
268
+
269
+ def getPartNumber(self, indexKey: str) -> int:
270
+ """Return the number of the question Part.
271
+
272
+ The number should be given after the `@` sign.
273
+ This is number is used, to reference the question Text
274
+ and the expected answer fields together.
275
+ """
276
+ try:
277
+ num = re.findall(r":(\d+)$", indexKey)[0]
278
+ except IndexError:
279
+ msg = f"No :i question Part value given for {indexKey}"
280
+ raise QNotParsedException(msg, self.question.id)
281
+ else:
282
+ return int(num)
283
+
284
+ @staticmethod
285
+ def getNumericAnsStr(
286
+ result: float,
287
+ tolerance: float,
288
+ points: float = 1,
289
+ wrongSignCount: int = 50,
290
+ wrongSignFeedback: str = "your result has the wrong sign (+-)",
291
+ ) -> str:
292
+ """Generate the answer string from `result`.
293
+
294
+ Parameters.
295
+ ----------
296
+ wrongSignCount:
297
+ If the wrong sign `+` or `-` is given, how much of the points should be given.
298
+ Interpreted as percent.
299
+ tolerance:
300
+ The relative tolerance, as fraction
301
+
302
+ """
303
+ absTol = f":{round(result * tolerance, 3)}"
304
+ answerParts: list[str | float] = [
305
+ "{",
306
+ points,
307
+ ":NUMERICAL:=",
308
+ round(result, 3),
309
+ absTol,
310
+ "~%",
311
+ wrongSignCount,
312
+ "%",
313
+ round(result * (-1), 3),
314
+ absTol,
315
+ f"#{wrongSignFeedback}",
316
+ "}",
317
+ ]
318
+ answerPStrings = [str(part) for part in answerParts]
319
+ return "".join(answerPStrings)
320
+
321
+ @staticmethod
322
+ def getMCAnsStr(
323
+ true: list[str],
324
+ false: list[str],
325
+ points: float = 1,
326
+ ) -> str:
327
+ """Generate the answer string for the MC answers."""
328
+ truePercent: float = round(100 / len(true), 1)
329
+ falsePercent: float = round(100 / len(false), 1)
330
+ falseList: list[str] = [f"~%-{falsePercent}%{ans}" for ans in false]
331
+ trueList: list[str] = [f"~%{truePercent}%{ans}" for ans in true]
332
+ answerParts: list[str | float] = [
333
+ "{",
334
+ points,
335
+ ":MULTIRESPONSE:",
336
+ ]
337
+ answerParts.extend(trueList)
338
+ answerParts.extend(falseList)
339
+ answerParts.append("}")
340
+
341
+ answerPStrings = [str(part) for part in answerParts]
342
+ return "".join(answerPStrings)
@@ -45,14 +45,15 @@ class NFMQuestionParser(QuestionParser):
45
45
  super().__init__()
46
46
  self.genFeedbacks = [XMLTags.GENFEEDB]
47
47
  self.question: NFMQuestion
48
- module = self.settings.get(Tags.IMPORTMODULE)
49
- if module and not type(self).astEval.symtable.get(module):
50
- type(self).astEval(f"import {module}")
51
- self.logger.warning("Imported '%s' to Asteval symtable", module)
52
48
 
53
49
  def setup(self, question: NFMQuestion) -> None:
54
50
  self.question: NFMQuestion = question
55
51
  super().setup(question)
52
+ module = self.settings.get(Tags.IMPORTMODULE)
53
+ if module and type(self).astEval.symtable.get(module, None) is None:
54
+ type(self).astEval(f"import {module}")
55
+ imported = type(self).astEval.symtable.get(module)
56
+ self.logger.warning("Imported '%s' to Asteval symtable.", module)
56
57
 
57
58
  def _parseAnswers(self) -> None:
58
59
  equation = self.rawInput.get(Tags.EQUATION)
@@ -51,23 +51,27 @@ class MainWindow(QtWidgets.QMainWindow):
51
51
  self.ui.treeWidget.header().setSectionResizeMode(
52
52
  QtWidgets.QHeaderView.ResizeToContents,
53
53
  )
54
+ self.ui.pointCounter.setReadOnly(True)
55
+ self.ui.questionCounter.setReadOnly(True)
56
+ self.setStatus(
57
+ "Wählen Sie eine Excel Tabelle mit den Fragen aus",
58
+ )
59
+ self.threadPool = QThreadPool()
60
+ self._restoreSettings()
61
+
62
+ def _restoreSettings(self) -> None:
63
+ """Restore the settings from the last session, if they exist."""
54
64
  self.exportDialog.ui.checkBoxIncludeCategories.setChecked(
55
65
  self.qSettings.value(Tags.INCLUDEINCATS, defaultValue=True, type=bool)
56
66
  )
57
67
  self.exportDialog.ui.spinBoxDefaultQVariant.setValue(
58
68
  self.qSettings.value(Tags.QUESTIONVARIANT, defaultValue=1, type=int)
59
69
  )
60
- self.ui.pointCounter.setReadOnly(True)
61
- self.ui.questionCounter.setReadOnly(True)
62
- self.setStatus(
63
- "Wählen Sie eine Excel Tabelle mit den Fragen aus",
64
- )
65
70
  try:
66
71
  self.resize(self.qSettings.value("windowSize"))
67
72
  self.move(self.qSettings.value("windowPosition"))
68
73
  except Exception:
69
74
  pass
70
- self.threadPool = QThreadPool()
71
75
  if self.qSettings.contains(Tags.SPREADSHEETPATH.full):
72
76
  sheet = self.qSettings.value(Tags.SPREADSHEETPATH.full)
73
77
  self.setSheetPath(sheet)
@@ -79,9 +83,6 @@ class MainWindow(QtWidgets.QMainWindow):
79
83
  )
80
84
  loggerSignal.emitter.signal.connect(self.updateLog)
81
85
  self.ui.actionEquationChecker.triggered.connect(self.openEqCheckerDlg)
82
- self.exportDialog.ui.checkBoxIncludeCategories.checkStateChanged.connect(
83
- self.setIncludeCategoriesSetting,
84
- )
85
86
  self.ui.actionParseAll.triggered.connect(self.parseSpreadsheetAll)
86
87
  self.testDB.signals.categoryQuestionsReady.connect(self.treeRefreshCategory)
87
88
  self.ui.actionSpreadsheet.triggered.connect(self.actionSpreadsheet)
@@ -91,13 +92,6 @@ class MainWindow(QtWidgets.QMainWindow):
91
92
  self.ui.treeWidget.itemClicked.connect(self.updateQuestionPreview)
92
93
  self.ui.actionAbout.triggered.connect(self.openAboutDlg)
93
94
  self.ui.actionDocumentation.triggered.connect(self.openDocumentation)
94
- self.exportDialog.ui.spinBoxDefaultQVariant.valueChanged.connect(
95
- self.setQVariantDefault
96
- )
97
-
98
- @QtCore.Slot()
99
- def setQVariantDefault(self, value: int) -> None:
100
- self.settings.set(Tags.QUESTIONVARIANT, value)
101
95
 
102
96
  @QtCore.Slot()
103
97
  def parseSpreadsheetAll(self) -> None:
@@ -128,12 +122,6 @@ class MainWindow(QtWidgets.QMainWindow):
128
122
  def updateLog(self, log) -> None:
129
123
  self.ui.loggerWindow.append(log)
130
124
 
131
- def setIncludeCategoriesSetting(self) -> None:
132
- if self.exportDialog.ui.checkBoxIncludeCategories.isChecked():
133
- self.settings.set(Tags.INCLUDEINCATS, True)
134
- else:
135
- self.settings.set(Tags.INCLUDEINCATS, False)
136
-
137
125
  def closeEvent(self, event) -> None:
138
126
  logger.info("Closing. Saving window stats.")
139
127
  self.qSettings.setValue("windowSize", self.size())
@@ -179,6 +167,14 @@ class MainWindow(QtWidgets.QMainWindow):
179
167
  self.exportDialog.ui.pointCount.setValue(self.ui.pointCounter.value())
180
168
  if self.exportDialog.exec():
181
169
  self.exportFile = self.exportDialog.exportFile
170
+ self.settings.set(
171
+ Tags.INCLUDEINCATS,
172
+ self.exportDialog.ui.checkBoxIncludeCategories.isChecked(),
173
+ )
174
+ self.settings.set(
175
+ Tags.QUESTIONVARIANT,
176
+ self.exportDialog.ui.spinBoxDefaultQVariant.value(),
177
+ )
182
178
  logger.info("New Export File is set %s", self.exportFile)
183
179
  self.testDB.appendQuestions(selection, self.exportFile)
184
180
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: excel2moodle
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: A package for converting questions from a spreadsheet, to valid moodle-xml
5
5
  Author: Jakob Bosse
6
6
  License-Expression: GPL-3.0-or-later
@@ -81,6 +81,46 @@ If You want to support my work as well, you can by me a [coffee](https://ko-fi.c
81
81
 
82
82
  # Changelogs
83
83
 
84
+ ## 0.5.2 (2025-06-30)
85
+ Extended Documentation and bugfix for import Module
86
+
87
+ ### bugfix (2 changes)
88
+
89
+ - [Default question variant saved and reused.](https://gitlab.com/jbosse3/excel2moodle/-/commit/097705ba83727463a9b27cd76e99814a7ecf28df)
90
+ - [bugfix: Import module working again](https://gitlab.com/jbosse3/excel2moodle/-/commit/5f293970bcdac3858911cdcc102b72714af057bd)
91
+
92
+ ### documentation (1 change)
93
+
94
+ - [documentation: Added how to build question database](https://gitlab.com/jbosse3/excel2moodle/-/commit/71ceb122aa37e8bf2735b659359ae37d81017599)
95
+
96
+ ### feature (1 change)
97
+
98
+ - [Implemented MC question string method](https://gitlab.com/jbosse3/excel2moodle/-/commit/c4f2081d0000ee60322fe8eec8468fa3317ce7be)
99
+
100
+ ### improvement (1 change)
101
+
102
+ - [Implemented ClozePart object](https://gitlab.com/jbosse3/excel2moodle/-/commit/878f90f45e37421384c4f8f602115e7596b4ceb9)
103
+
104
+ ## 0.5.2 (2025-06-30)
105
+ Extended Documentation and bugfix for import Module
106
+
107
+ ### bugfix (2 changes)
108
+
109
+ - [Default question variant saved and reused.](https://gitlab.com/jbosse3/excel2moodle/-/commit/097705ba83727463a9b27cd76e99814a7ecf28df)
110
+ - [bugfix: Import module working again](https://gitlab.com/jbosse3/excel2moodle/-/commit/5f293970bcdac3858911cdcc102b72714af057bd)
111
+
112
+ ### documentation (1 change)
113
+
114
+ - [documentation: Added how to build question database](https://gitlab.com/jbosse3/excel2moodle/-/commit/71ceb122aa37e8bf2735b659359ae37d81017599)
115
+
116
+ ### feature (1 change)
117
+
118
+ - [Implemented MC question string method](https://gitlab.com/jbosse3/excel2moodle/-/commit/c4f2081d0000ee60322fe8eec8468fa3317ce7be)
119
+
120
+ ### improvement (1 change)
121
+
122
+ - [Implemented ClozePart object](https://gitlab.com/jbosse3/excel2moodle/-/commit/878f90f45e37421384c4f8f602115e7596b4ceb9)
123
+
84
124
  ## 0.5.1 (2025-06-24)
85
125
  Minor docs improvement and question variant bugfix
86
126
 
@@ -2,17 +2,6 @@ LICENSE
2
2
  MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
- docs/_build/html/exampleQuestions.html
6
- docs/_build/html/excel2moodle.core.html
7
- docs/_build/html/excel2moodle.extra.html
8
- docs/_build/html/excel2moodle.html
9
- docs/_build/html/excel2moodle.ui.html
10
- docs/_build/html/genindex.html
11
- docs/_build/html/howto.html
12
- docs/_build/html/index.html
13
- docs/_build/html/py-modindex.html
14
- docs/_build/html/search.html
15
- docs/_build/html/userReference.html
16
5
  excel2moodle/__init__.py
17
6
  excel2moodle/__main__.py
18
7
  excel2moodle/logger.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "excel2moodle"
7
- version = "0.5.1"
7
+ version = "0.5.2"
8
8
  authors = [
9
9
  { name="Jakob Bosse" },
10
10
  ]
@@ -45,6 +45,15 @@ pythonpath = ["."]
45
45
  dev = [
46
46
  "ty>=0.0.1a10",
47
47
  ]
48
+ doc = [
49
+ "asteval>=1.0.6",
50
+ "lxml>=5.4.0",
51
+ "sphinx>=7.4.7",
52
+ "sphinx-autodoc-typehints>=3.0.1",
53
+ "sphinx-copybutton>=0.5.2",
54
+ "sphinx-rtd-theme>=2.0.0",
55
+ "toml>=0.10.2",
56
+ ]
48
57
 
49
58
  [project.urls]
50
59
  Repository = "https://gitlab.com/jbosse3/excel2moodle.git"