excel2moodle 0.5.1__py3-none-any.whl → 0.6.0__py3-none-any.whl
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.
- excel2moodle/core/bullets.py +98 -0
- excel2moodle/core/dataStructure.py +6 -7
- excel2moodle/core/globals.py +3 -8
- excel2moodle/core/parser.py +37 -65
- excel2moodle/core/question.py +144 -76
- excel2moodle/extra/variableGenerator.py +250 -0
- excel2moodle/question_types/cloze.py +269 -103
- excel2moodle/question_types/nfm.py +41 -102
- excel2moodle/ui/UI_mainWindow.py +63 -36
- excel2moodle/ui/UI_variableGenerator.py +197 -0
- excel2moodle/ui/appUi.py +107 -44
- excel2moodle/ui/dialogs.py +44 -77
- excel2moodle/ui/equationChecker.py +2 -2
- excel2moodle/ui/treewidget.py +9 -24
- {excel2moodle-0.5.1.dist-info → excel2moodle-0.6.0.dist-info}/METADATA +67 -2
- {excel2moodle-0.5.1.dist-info → excel2moodle-0.6.0.dist-info}/RECORD +20 -19
- excel2moodle/core/numericMultiQ.py +0 -80
- excel2moodle/ui/windowDoc.py +0 -27
- {excel2moodle-0.5.1.dist-info → excel2moodle-0.6.0.dist-info}/WHEEL +0 -0
- {excel2moodle-0.5.1.dist-info → excel2moodle-0.6.0.dist-info}/entry_points.txt +0 -0
- {excel2moodle-0.5.1.dist-info → excel2moodle-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.5.1.dist-info → excel2moodle-0.6.0.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,12 @@
|
|
1
|
-
"""Implementation of
|
1
|
+
"""Implementation of tde cloze question type.
|
2
2
|
|
3
3
|
This question type is like the NFM but supports multiple fields of answers.
|
4
4
|
All Answers are calculated off an equation using the same variables.
|
5
5
|
"""
|
6
6
|
|
7
|
-
import
|
7
|
+
import logging
|
8
8
|
import re
|
9
|
+
from typing import Literal, overload
|
9
10
|
|
10
11
|
import lxml.etree as ET
|
11
12
|
|
@@ -14,48 +15,156 @@ from excel2moodle.core.globals import (
|
|
14
15
|
Tags,
|
15
16
|
TextElements,
|
16
17
|
)
|
17
|
-
from excel2moodle.core.question import
|
18
|
+
from excel2moodle.core.question import (
|
19
|
+
ParametricQuestion,
|
20
|
+
Parametrics,
|
21
|
+
)
|
18
22
|
from excel2moodle.core.settings import Tags
|
23
|
+
from excel2moodle.logger import LogAdapterQuestionID
|
19
24
|
from excel2moodle.question_types.nfm import NFMQuestionParser
|
20
25
|
|
26
|
+
logger = logging.getLogger(__name__)
|
27
|
+
|
28
|
+
|
29
|
+
class ClozePart:
|
30
|
+
def __init__(
|
31
|
+
self,
|
32
|
+
question: ParametricQuestion,
|
33
|
+
text: list[str],
|
34
|
+
number: int,
|
35
|
+
) -> None:
|
36
|
+
self.question = question
|
37
|
+
self.text: ET.Element = self._setupText(text)
|
38
|
+
self.num: int = number
|
39
|
+
if not self.text:
|
40
|
+
msg = f"Answer part for cloze question {self.question.id} is invalid without partText"
|
41
|
+
raise ValueError(msg)
|
42
|
+
self.logger = LogAdapterQuestionID(
|
43
|
+
logger, {"qID": f"{self.question.id}-{self.num}"}
|
44
|
+
)
|
45
|
+
self._typ: Literal["NFM", "MC", "UNSET"]
|
46
|
+
self._element: ET.Element
|
47
|
+
self.result: Parametrics
|
48
|
+
|
49
|
+
@property
|
50
|
+
def clozeElement(self) -> ET.Element:
|
51
|
+
if not hasattr(self, "_clozeElement"):
|
52
|
+
msg = "Cloze Part has no _clozeElement"
|
53
|
+
raise QNotParsedException(msg, f"{self.question.id}-{self.num}")
|
54
|
+
return self._element
|
55
|
+
|
56
|
+
@clozeElement.setter
|
57
|
+
def clozeElement(self, element: ET.Element) -> None:
|
58
|
+
self._element = element
|
59
|
+
|
60
|
+
def updateCloze(self, variant: int = 1) -> None:
|
61
|
+
self.logger.info("Updating cloze to variant %s", variant)
|
62
|
+
if not hasattr(self, "_element"):
|
63
|
+
msg = "Cloze Part has no _clozeElement"
|
64
|
+
raise QNotParsedException(msg, f"{self.question.id}-{self.num}")
|
65
|
+
if self.typ == "MC":
|
66
|
+
self.logger.debug("MC Answer Part already up to date.")
|
67
|
+
return
|
68
|
+
if self.typ == "NFM":
|
69
|
+
result = self.result.getResult(variant)
|
70
|
+
self._element.text = ClozeQuestionParser.getNumericAnsStr(
|
71
|
+
result,
|
72
|
+
self.question.rawData.get(Tags.TOLERANCE),
|
73
|
+
wrongSignCount=self.question.rawData.get(Tags.WRONGSIGNPERCENT),
|
74
|
+
points=self.points,
|
75
|
+
)
|
76
|
+
self.logger.debug("Updated NFM cloze: %s", self._element.text)
|
77
|
+
return
|
78
|
+
|
79
|
+
@property
|
80
|
+
def typ(self) -> Literal["NFM", "MC", "UNSET"]:
|
81
|
+
if not hasattr(self, "_typ"):
|
82
|
+
self.logger.warning("Type not set")
|
83
|
+
return "UNSET"
|
84
|
+
return self._typ
|
85
|
+
|
86
|
+
@typ.setter
|
87
|
+
def typ(self, partType: Literal["NFM", "MC", "UNSET"]) -> None:
|
88
|
+
if not hasattr(self, "_typ"):
|
89
|
+
self._typ = partType
|
90
|
+
self.logger.info("Set type to: %s", self._typ)
|
91
|
+
if self._typ == "NFM":
|
92
|
+
self.result: Parametrics
|
93
|
+
elif self._typ == "MC":
|
94
|
+
self.falseAnswers: list[str] = []
|
95
|
+
self.trueAnswers: list[str] = []
|
96
|
+
|
97
|
+
@property
|
98
|
+
def id(self) -> str:
|
99
|
+
return f"{self.question.id}-{self.num}"
|
100
|
+
|
101
|
+
@property
|
102
|
+
def points(self) -> float:
|
103
|
+
if hasattr(self, "_points"):
|
104
|
+
return self._points
|
105
|
+
return 0.0
|
106
|
+
self.question.logger.error("Invalid call to points of unparsed cloze part")
|
107
|
+
return 0.0
|
108
|
+
|
109
|
+
@points.setter
|
110
|
+
def points(self, points: float) -> None:
|
111
|
+
self._points = points if points > 0 else 0.0
|
112
|
+
|
113
|
+
@property
|
114
|
+
def mcAnswerString(self) -> str:
|
115
|
+
if hasattr(self, "_mcAnswer"):
|
116
|
+
return self._mcAnswer
|
117
|
+
msg = "No MC Answer was set"
|
118
|
+
raise ValueError(msg)
|
119
|
+
|
120
|
+
@mcAnswerString.setter
|
121
|
+
def mcAnswerString(self, answerString: str) -> None:
|
122
|
+
self._mcAnswer: str = answerString
|
123
|
+
|
124
|
+
def _setupText(self, text: list[str]) -> ET.Element:
|
125
|
+
textItem: ET.Element = TextElements.LISTITEM.create()
|
126
|
+
for t in text:
|
127
|
+
textItem.append(TextElements.PLEFT.create())
|
128
|
+
textItem[-1].text = t
|
129
|
+
return textItem
|
130
|
+
|
131
|
+
def __repr__(self) -> str:
|
132
|
+
return f"Cloze Part {self.id}-{self.typ}"
|
133
|
+
|
21
134
|
|
22
135
|
class ClozeQuestion(ParametricQuestion):
|
23
136
|
"""Cloze Question Type."""
|
24
137
|
|
25
138
|
def __init__(self, *args, **kwargs) -> None:
|
26
139
|
super().__init__(*args, **kwargs)
|
27
|
-
self.
|
28
|
-
self.
|
29
|
-
self.
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
for paragraphs in self.questionTexts.values():
|
56
|
-
for par in paragraphs:
|
57
|
-
textParts.append(par)
|
58
|
-
return textParts
|
140
|
+
self.questionParts: dict[int, ClozePart] = {}
|
141
|
+
self.questionTexts: list[ET.Element] = []
|
142
|
+
self.parametrics: Parametrics
|
143
|
+
|
144
|
+
@property
|
145
|
+
def partsNum(self) -> int:
|
146
|
+
return len(self.questionParts)
|
147
|
+
|
148
|
+
@property
|
149
|
+
def points(self) -> float:
|
150
|
+
pts: float = 0
|
151
|
+
if self.isParsed:
|
152
|
+
for p in self.questionParts.values():
|
153
|
+
pts = pts + p.points
|
154
|
+
else:
|
155
|
+
pts = self.rawData.get(Tags.POINTS)
|
156
|
+
return pts
|
157
|
+
|
158
|
+
def getUpdatedElement(self, variant: int = 0) -> ET.Element:
|
159
|
+
"""Update and get the Question Elements to reflect the version.
|
160
|
+
|
161
|
+
`ClozeQuestion` Updates the text.
|
162
|
+
`ParametricQuestion` updates the bullet points.
|
163
|
+
`Question` returns the element.
|
164
|
+
"""
|
165
|
+
for part in self.questionParts.values():
|
166
|
+
part.updateCloze(variant=variant)
|
167
|
+
return super().getUpdatedElement(variant=variant)
|
59
168
|
|
60
169
|
|
61
170
|
class ClozeQuestionParser(NFMQuestionParser):
|
@@ -70,72 +179,128 @@ class ClozeQuestionParser(NFMQuestionParser):
|
|
70
179
|
super().setup(question)
|
71
180
|
|
72
181
|
def _parseAnswers(self) -> None:
|
182
|
+
self._setupParts()
|
73
183
|
self._parseAnswerParts()
|
74
|
-
self._parseQuestionParts()
|
75
184
|
|
76
|
-
def
|
77
|
-
|
78
|
-
self.
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
185
|
+
def _setupParts(self) -> None:
|
186
|
+
parts: dict[int, ClozePart] = {}
|
187
|
+
for key in self.rawInput:
|
188
|
+
if key.startswith(Tags.QUESTIONPART):
|
189
|
+
partNumber = self.getPartNumber(key)
|
190
|
+
parts[partNumber] = ClozePart(
|
191
|
+
self.question, self.rawInput[key], partNumber
|
192
|
+
)
|
193
|
+
partsNum = len(parts)
|
194
|
+
equations: dict[int, str] = self._getPartValues(Tags.RESULT)
|
195
|
+
trueAnsws: dict[int, list[str]] = self._getPartValues(Tags.TRUE)
|
196
|
+
falseAnsws: dict[int, list[str]] = self._getPartValues(Tags.FALSE)
|
197
|
+
points: dict[int, float] = self._getPartValues(Tags.POINTS)
|
198
|
+
firstResult: dict[int, float] = self._getPartValues(Tags.FIRSTRESULT)
|
199
|
+
for num, part in parts.items():
|
200
|
+
loclogger = LogAdapterQuestionID(
|
201
|
+
logger, {"qID": f"{self.question.id}-{num}"}
|
202
|
+
)
|
203
|
+
eq = equations.get(num)
|
204
|
+
trueAns = trueAnsws.get(num)
|
205
|
+
falseAns = falseAnsws.get(num)
|
206
|
+
if falseAns is not None and trueAns is not None and eq is None:
|
207
|
+
loclogger.info("Setting up MC answer part...")
|
208
|
+
part.typ = "MC"
|
209
|
+
part.falseAnswers = falseAns
|
210
|
+
part.trueAnswers = trueAns
|
211
|
+
elif eq is not None and falseAns is None and trueAns is None:
|
212
|
+
loclogger.info("Seting up NFM part..")
|
213
|
+
if not hasattr(self.question, "parametrics"):
|
214
|
+
loclogger.info("Adding new Parametrics Object to cloze question")
|
215
|
+
self.question.parametrics = Parametrics(
|
216
|
+
equation=eq,
|
217
|
+
firstResult=firstResult.get(num, 0.0),
|
218
|
+
identifier=f"{self.question.id}-{num}",
|
219
|
+
)
|
106
220
|
else:
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
221
|
+
loclogger.info("Adding new equation to parametrics")
|
222
|
+
self.question.parametrics.equations[num] = eq
|
223
|
+
self.question.parametrics._resultChecker[num] = firstResult.get(
|
224
|
+
num, 0.0
|
225
|
+
)
|
226
|
+
if not hasattr(part, "result"):
|
227
|
+
part.result = self.question.parametrics
|
228
|
+
part.typ = "NFM"
|
229
|
+
loclogger.info("Set up NFM answer part.")
|
230
|
+
else:
|
231
|
+
msg = f"Unclear Parts are defined. Either define `true:{num}` and `false:{num}` or `result:{num}` "
|
232
|
+
raise QNotParsedException(msg, self.question.id)
|
233
|
+
if len(points) == 0:
|
234
|
+
pts = round(self.rawInput.get(Tags.POINTS) / partsNum, 3)
|
235
|
+
for part in parts.values():
|
236
|
+
part.points = pts
|
237
|
+
elif len(points) != partsNum:
|
238
|
+
logger.warning(
|
239
|
+
"Some Answer parts are missing the points, they will get the standard points"
|
240
|
+
)
|
241
|
+
for num, part in parts.items():
|
242
|
+
p = points.get(num)
|
243
|
+
part.points = p if p is not None else self.rawInput.get(Tags.POINTS)
|
244
|
+
self.question.questionParts = parts
|
112
245
|
|
113
|
-
|
114
|
-
|
115
|
-
|
246
|
+
@overload
|
247
|
+
def _getPartValues(self, Tag: Literal[Tags.RESULT]) -> dict[int, str]: ...
|
248
|
+
@overload
|
249
|
+
def _getPartValues(
|
250
|
+
self, Tag: Literal[Tags.POINTS, Tags.FIRSTRESULT]
|
251
|
+
) -> dict[int, float]: ...
|
252
|
+
@overload
|
253
|
+
def _getPartValues(
|
254
|
+
self, Tag: Literal[Tags.TRUE, Tags.FALSE]
|
255
|
+
) -> dict[int, list[str]]: ...
|
256
|
+
def _getPartValues(self, Tag):
|
257
|
+
tagValues: dict = {
|
116
258
|
self.getPartNumber(key): self.rawInput[key]
|
117
259
|
for key in self.rawInput
|
118
|
-
if key.startswith(
|
260
|
+
if key.startswith(Tag)
|
119
261
|
}
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
self.question.
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
262
|
+
return tagValues
|
263
|
+
|
264
|
+
def _parseAnswerParts(self) -> None:
|
265
|
+
"""Parse the numeric or MC result items."""
|
266
|
+
answersList = ET.Element("ol")
|
267
|
+
self.question.parametrics.variables = self.question.bulletList.getVariablesDict(
|
268
|
+
self.question
|
269
|
+
)
|
270
|
+
for partNum, part in self.question.questionParts.items():
|
271
|
+
if part.typ == "NFM":
|
272
|
+
result = self.question.parametrics.getResult(1, partNum)
|
273
|
+
ansStr = self.getNumericAnsStr(
|
274
|
+
result,
|
275
|
+
self.rawInput.get(Tags.TOLERANCE),
|
276
|
+
wrongSignCount=self.rawInput.get(Tags.WRONGSIGNPERCENT),
|
277
|
+
points=part.points,
|
278
|
+
)
|
279
|
+
self.logger.info("NF answer part: %s ", ansStr)
|
280
|
+
logger.debug("Appended NF part %s result", partNum)
|
281
|
+
elif part.typ == "MC":
|
282
|
+
ansStr = self.getMCAnsStr(
|
283
|
+
part.trueAnswers, part.falseAnswers, points=part.points
|
284
|
+
)
|
285
|
+
part.mcAnswerString = ansStr
|
286
|
+
logger.debug("Appended MC part %s: %s", partNum, ansStr)
|
287
|
+
else:
|
288
|
+
msg = "Type of the answer part is invalid"
|
289
|
+
raise QNotParsedException(msg, self.id)
|
290
|
+
unorderedList = TextElements.ULIST.create()
|
291
|
+
answerItem = TextElements.LISTITEM.create()
|
292
|
+
answerItem.text = ansStr
|
293
|
+
part.clozeElement = answerItem
|
294
|
+
unorderedList.append(answerItem)
|
295
|
+
part.text.append(unorderedList)
|
296
|
+
self.logger.debug("Appended part %s %s to main text", partNum, part)
|
297
|
+
answersList.append(part.text)
|
298
|
+
self.htmlRoot.append(answersList)
|
134
299
|
|
135
300
|
def getPartNumber(self, indexKey: str) -> int:
|
136
301
|
"""Return the number of the question Part.
|
137
302
|
|
138
|
-
The number should be given after the
|
303
|
+
The number should be given after the `:` colon.
|
139
304
|
This is number is used, to reference the question Text
|
140
305
|
and the expected answer fields together.
|
141
306
|
"""
|
@@ -151,7 +316,7 @@ class ClozeQuestionParser(NFMQuestionParser):
|
|
151
316
|
def getNumericAnsStr(
|
152
317
|
result: float,
|
153
318
|
tolerance: float,
|
154
|
-
|
319
|
+
points: float = 1,
|
155
320
|
wrongSignCount: int = 50,
|
156
321
|
wrongSignFeedback: str = "your result has the wrong sign (+-)",
|
157
322
|
) -> str:
|
@@ -159,12 +324,6 @@ class ClozeQuestionParser(NFMQuestionParser):
|
|
159
324
|
|
160
325
|
Parameters.
|
161
326
|
----------
|
162
|
-
weight:
|
163
|
-
The weight of the answer relative to the other answer elements.
|
164
|
-
Of one answer has `weight=2` and two other answers `weight=1`,
|
165
|
-
this answer will be counted as 50% of the questions points.
|
166
|
-
The other two will counted as 25% of the questions points.
|
167
|
-
|
168
327
|
wrongSignCount:
|
169
328
|
If the wrong sign `+` or `-` is given, how much of the points should be given.
|
170
329
|
Interpreted as percent.
|
@@ -175,7 +334,7 @@ class ClozeQuestionParser(NFMQuestionParser):
|
|
175
334
|
absTol = f":{round(result * tolerance, 3)}"
|
176
335
|
answerParts: list[str | float] = [
|
177
336
|
"{",
|
178
|
-
|
337
|
+
points,
|
179
338
|
":NUMERICAL:=",
|
180
339
|
round(result, 3),
|
181
340
|
absTol,
|
@@ -194,14 +353,21 @@ class ClozeQuestionParser(NFMQuestionParser):
|
|
194
353
|
def getMCAnsStr(
|
195
354
|
true: list[str],
|
196
355
|
false: list[str],
|
197
|
-
|
356
|
+
points: float = 1,
|
198
357
|
) -> str:
|
199
358
|
"""Generate the answer string for the MC answers."""
|
359
|
+
truePercent: float = round(100 / len(true), 1)
|
360
|
+
falsePercent: float = round(100 / len(false), 1)
|
361
|
+
falseList: list[str] = [f"~%-{falsePercent}%{ans}" for ans in false]
|
362
|
+
trueList: list[str] = [f"~%{truePercent}%{ans}" for ans in true]
|
200
363
|
answerParts: list[str | float] = [
|
201
364
|
"{",
|
202
|
-
|
203
|
-
":
|
204
|
-
"}",
|
365
|
+
points,
|
366
|
+
":MULTIRESPONSE:",
|
205
367
|
]
|
368
|
+
answerParts.extend(trueList)
|
369
|
+
answerParts.extend(falseList)
|
370
|
+
answerParts.append("}")
|
371
|
+
|
206
372
|
answerPStrings = [str(part) for part in answerParts]
|
207
|
-
return "".join(
|
373
|
+
return "".join(answerPStrings)
|
@@ -1,23 +1,16 @@
|
|
1
1
|
"""Numerical question multi implementation."""
|
2
2
|
|
3
|
-
import math
|
4
|
-
import re
|
5
3
|
from types import UnionType
|
6
|
-
from typing import
|
4
|
+
from typing import ClassVar
|
7
5
|
|
8
|
-
|
6
|
+
import lxml.etree as ET
|
9
7
|
|
10
|
-
from excel2moodle.core import stringHelpers
|
11
|
-
from excel2moodle.core.exceptions import QNotParsedException
|
12
8
|
from excel2moodle.core.globals import (
|
13
9
|
Tags,
|
14
10
|
XMLTags,
|
15
11
|
)
|
16
12
|
from excel2moodle.core.parser import QuestionParser
|
17
|
-
from excel2moodle.core.question import ParametricQuestion
|
18
|
-
|
19
|
-
if TYPE_CHECKING:
|
20
|
-
import lxml.etree as ET
|
13
|
+
from excel2moodle.core.question import ParametricQuestion, Parametrics
|
21
14
|
|
22
15
|
|
23
16
|
class NFMQuestion(ParametricQuestion):
|
@@ -28,107 +21,53 @@ class NFMQuestion(ParametricQuestion):
|
|
28
21
|
|
29
22
|
def __init__(self, *args, **kwargs) -> None:
|
30
23
|
super().__init__(*args, **kwargs)
|
31
|
-
self.
|
24
|
+
self.answerElement: ET.Element
|
25
|
+
|
26
|
+
def getUpdatedElement(self, variant: int = 1) -> ET.Element:
|
27
|
+
"""Update and get the Question Elements to reflect the version.
|
32
28
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
self.
|
29
|
+
`NFMQuestion` updates the answer Elements.
|
30
|
+
`ParametricQuestion` updates the bullet points.
|
31
|
+
`Question` returns the Element.
|
32
|
+
"""
|
33
|
+
result = self.parametrics.getResult(variant)
|
34
|
+
tolerance = round(self.rawData.get(Tags.TOLERANCE) * result, 3)
|
35
|
+
self.answerElement.find(XMLTags.TEXT).text = str(result)
|
36
|
+
self.answerElement.find(XMLTags.TOLERANCE).text = str(tolerance)
|
37
|
+
return super().getUpdatedElement(variant)
|
39
38
|
|
40
39
|
|
41
40
|
class NFMQuestionParser(QuestionParser):
|
42
|
-
astEval = Interpreter(with_import=True)
|
43
|
-
|
44
41
|
def __init__(self) -> None:
|
45
42
|
super().__init__()
|
46
43
|
self.genFeedbacks = [XMLTags.GENFEEDB]
|
47
44
|
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
45
|
|
53
46
|
def setup(self, question: NFMQuestion) -> None:
|
54
47
|
self.question: NFMQuestion = question
|
55
48
|
super().setup(question)
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
self._setVariants(len(ansElementsList))
|
82
|
-
|
83
|
-
def _setVariants(self, number: int) -> None:
|
84
|
-
self.question.variants = number
|
85
|
-
mvar = self.question.category.maxVariants
|
86
|
-
if mvar is None:
|
87
|
-
self.question.category.maxVariants = number
|
88
|
-
else:
|
89
|
-
self.question.category.maxVariants = min(number, mvar)
|
90
|
-
|
91
|
-
@classmethod
|
92
|
-
def setupAstIntprt(cls, var: dict[str, list[float | int]], index: int) -> None:
|
93
|
-
"""Setup the asteval Interpreter with the variables."""
|
94
|
-
for name, value in var.items():
|
95
|
-
cls.astEval.symtable[name] = value[index]
|
96
|
-
|
97
|
-
def _getVariablesDict(self, keyList: list) -> tuple[dict[str, list[float]], int]:
|
98
|
-
"""Read variabel values for vars in `keyList` from `question.rawData`.
|
99
|
-
|
100
|
-
Returns
|
101
|
-
-------
|
102
|
-
A dictionary containing a list of values for each variable
|
103
|
-
The number of values for each variable
|
104
|
-
|
105
|
-
"""
|
106
|
-
dic: dict = {}
|
107
|
-
num: int = 0
|
108
|
-
for k in keyList:
|
109
|
-
val = self.rawInput[k.lower()]
|
110
|
-
if isinstance(val, str):
|
111
|
-
li = stringHelpers.getListFromStr(val)
|
112
|
-
num = len(li)
|
113
|
-
variables: list[float] = [float(i.replace(",", ".")) for i in li]
|
114
|
-
dic[str(k)] = variables
|
115
|
-
else:
|
116
|
-
dic[str(k)] = [str(val)]
|
117
|
-
num = 1
|
118
|
-
self.logger.debug("The following variables were provided: %s", dic)
|
119
|
-
return dic, num
|
120
|
-
|
121
|
-
@staticmethod
|
122
|
-
def _getVarsList(bps: str | list[str]) -> list:
|
123
|
-
"""Durchsucht den bulletPoints String nach den Variablen ``{var}``.
|
124
|
-
|
125
|
-
It only finds variables after the ``=`` sign, to not catch LaTex.
|
126
|
-
"""
|
127
|
-
varNames = []
|
128
|
-
regexFinder = re.compile(r"=\s*\{(\w+)\}")
|
129
|
-
if isinstance(bps, list):
|
130
|
-
for _p in bps:
|
131
|
-
varNames.extend(regexFinder.findall(str(_p)))
|
132
|
-
else:
|
133
|
-
varNames = regexFinder.findall(str(bps))
|
134
|
-
return varNames
|
49
|
+
module = self.settings.get(Tags.IMPORTMODULE)
|
50
|
+
if module and Parametrics.astEval.symtable.get(module, None) is None:
|
51
|
+
Parametrics.astEval(f"import {module}")
|
52
|
+
imported = Parametrics.astEval.symtable.get(module)
|
53
|
+
self.logger.warning("Imported '%s' to Asteval symtable.", module)
|
54
|
+
|
55
|
+
def _parseAnswers(self) -> list[ET.Element]:
|
56
|
+
variables = self.question.bulletList.getVariablesDict(self.question)
|
57
|
+
self.question.parametrics = Parametrics(
|
58
|
+
self.rawInput.get(Tags.EQUATION),
|
59
|
+
self.rawInput.get(Tags.FIRSTRESULT),
|
60
|
+
self.question.id,
|
61
|
+
)
|
62
|
+
self.question.parametrics.variables = variables
|
63
|
+
self.question.answerElement = self.getNumericAnsElement()
|
64
|
+
return [self.question.answerElement]
|
65
|
+
|
66
|
+
# TODO: @jbosse3: Implement a new _setVariants() method, to show in treewidget
|
67
|
+
# def _setVariants(self, number: int) -> None:
|
68
|
+
# self.question.variants = number
|
69
|
+
# mvar = self.question.category.maxVariants
|
70
|
+
# if mvar is None:
|
71
|
+
# self.question.category.maxVariants = number
|
72
|
+
# else:
|
73
|
+
# self.question.category.maxVariants = min(number, mvar)
|