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.
@@ -1,11 +1,12 @@
1
- """Implementation of the cloze question type.
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 math
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 ParametricQuestion
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.answerVariants: dict[int, list[float]] = {}
28
- self.answerStrings: dict[int, list[str]] = {}
29
- self.answerTypes: dict[int, str] = {}
30
- self.questionTexts: dict[int, list[ET.Element]] = {}
31
- self.partsNum: int = 0
32
-
33
- def _assembleAnswer(self, variant: int = 1) -> None:
34
- for part, ans in self.answerVariants.items():
35
- result = ans[variant - 1]
36
- if self.answerTypes.get(part, None) == "MC":
37
- ansStr = ClozeQuestionParser.getMCAnsStr(answers)
38
- else:
39
- ansStr = ClozeQuestionParser.getNumericAnsStr(
40
- result,
41
- self.rawData.get(Tags.TOLERANCE),
42
- wrongSignCount=self.rawData.get(Tags.WRONGSIGNPERCENT),
43
- )
44
- ul = TextElements.ULIST.create()
45
- item = TextElements.LISTITEM.create()
46
- item.text = ansStr
47
- ul.append(item)
48
- self.questionTexts[part].append(ul)
49
- self.logger.debug("Appended Question Parts %s to main text", part)
50
- self.questionTexts[part].append(ET.Element("hr"))
51
-
52
- def _assembleText(self, variant=0) -> list[ET.Element]:
53
- textParts = super()._assembleText(variant=variant)
54
- self.logger.debug("Appending QuestionParts to main text")
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 _parseAnswerParts(self) -> None:
77
- """Parse the numeric or MC result items."""
78
- self.question.answerTypes: dict[int, str] = {
79
- self.getPartNumber(key): self.rawInput[key]
80
- for key in self.rawInput
81
- if key.startswith(Tags.PARTTYPE)
82
- }
83
- equations: dict[int, str] = {
84
- self.getPartNumber(key): self.rawInput[key]
85
- for key in self.rawInput
86
- if key.startswith(Tags.RESULT)
87
- }
88
- self.logger.debug("Got the following answers: %s", equations)
89
- bps = str(self.rawInput[Tags.BPOINTS])
90
- varNames: list[str] = self._getVarsList(bps)
91
- numericAnswers: dict[int, list[float]] = {key: [] for key in equations}
92
- self.question.variables, number = self._getVariablesDict(varNames)
93
- for n in range(number):
94
- self.setupAstIntprt(self.question.variables, n)
95
- for ansNum, eq in equations.items():
96
- result = self.astEval(eq)
97
- if isinstance(result, float):
98
- firstResult = self.rawInput.get(Tags.FIRSTRESULT)
99
- if n == 0 and not math.isclose(result, firstResult, rel_tol=0.01):
100
- self.logger.warning(
101
- "The calculated result %s differs from given firstResult: %s",
102
- result,
103
- firstResult,
104
- )
105
- numericAnswers[ansNum].append(result)
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
- msg = f"The expression {eq} could not be evaluated."
108
- raise QNotParsedException(msg, self.question.id)
109
-
110
- self.question.answerVariants = numericAnswers
111
- self._setVariants(number)
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
- def _parseQuestionParts(self) -> None:
114
- """Generate the question parts aka the clozes."""
115
- parts: dict[int, list[str]] = {
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(Tags.QUESTIONPART)
260
+ if key.startswith(Tag)
119
261
  }
120
- questionParts: dict[int, list[ET.Element]] = {}
121
- for number, text in parts.items():
122
- questionParts[number] = []
123
- for t in text:
124
- questionParts[number].append(TextElements.PLEFT.create())
125
- questionParts[number][-1].text = t
126
- self.logger.debug("The Question Parts are created:", questionParts)
127
- self.question.questionTexts = questionParts
128
- self.question.partsNum = len(questionParts)
129
- self.logger.info("The Question has %s parts", self.question.partsNum)
130
-
131
- # def setMainText(self) -> None:
132
- # super().setMainText()
133
- # self.question.qtextParagraphs
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 `@` sign.
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
- weight: int = 1,
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
- weight,
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
- weight: int = 1,
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
- weight,
203
- ":MC:",
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(answerPString)
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 TYPE_CHECKING, ClassVar
4
+ from typing import ClassVar
7
5
 
8
- from asteval import Interpreter
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.answerVariants: list[ET.Element]
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
- def _assembleAnswer(self, variant: int = 1) -> None:
34
- prevAnsElement = self.element.find(XMLTags.ANSWER)
35
- if prevAnsElement is not None:
36
- self.element.remove(prevAnsElement)
37
- self.logger.debug("removed previous answer element")
38
- self.element.insert(5, self.answerVariants[variant - 1])
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
- def _parseAnswers(self) -> None:
58
- equation = self.rawInput.get(Tags.EQUATION)
59
- bps = self.rawInput.get(Tags.BPOINTS)
60
- ansElementsList: list[ET.Element] = []
61
- varNames: list[str] = self._getVarsList(bps)
62
- self.question.variables, number = self._getVariablesDict(varNames)
63
- for n in range(number):
64
- type(self).setupAstIntprt(self.question.variables, n)
65
- result = type(self).astEval(equation)
66
- if isinstance(result, float):
67
- firstResult = self.rawInput.get(Tags.FIRSTRESULT)
68
- if n == 0 and not math.isclose(result, firstResult, rel_tol=0.01):
69
- self.logger.warning(
70
- "The calculated result %s differs from given firstResult: %s",
71
- result,
72
- firstResult,
73
- )
74
- ansElementsList.append(
75
- self.getNumericAnsElement(result=round(result, 3)),
76
- )
77
- else:
78
- msg = f"The expression {equation} could not be evaluated."
79
- raise QNotParsedException(msg, self.question.id)
80
- self.question.answerVariants = ansElementsList
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)