excel2moodle 0.5.2__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.
@@ -0,0 +1,250 @@
1
+ import logging
2
+ import random
3
+
4
+ from asteval import Interpreter
5
+ from PySide6.QtWidgets import (
6
+ QDialog,
7
+ QLineEdit,
8
+ QMainWindow,
9
+ QMessageBox,
10
+ QTableWidget,
11
+ QTableWidgetItem,
12
+ )
13
+
14
+ from excel2moodle.core.question import Parametrics
15
+ from excel2moodle.ui.UI_variableGenerator import Ui_VariableGeneratorDialog
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class VariableGeneratorDialog(QDialog):
21
+ def __init__(self, parent: QMainWindow, parametrics: Parametrics) -> None:
22
+ super().__init__(parent)
23
+ self.ui = Ui_VariableGeneratorDialog()
24
+ self.ui.setupUi(self)
25
+ self.origParametrics = parametrics
26
+ self._generatedParametrics: Parametrics = Parametrics(
27
+ parametrics.equations, parametrics._resultChecker, identifier="genr"
28
+ )
29
+ # Load existing rules
30
+ for rule in self.origParametrics.variableRules:
31
+ self.ui.listWidget_rules.addItem(rule)
32
+
33
+ self._populate_variables_table()
34
+ populateDataSetTable(
35
+ self.ui.tableWidget_existing_variables,
36
+ parametrics=self.origParametrics,
37
+ )
38
+ self._connect_signals()
39
+
40
+ # Initially hide the existing variables table and generated variables table
41
+ self.ui.tableWidget_existing_variables.hide()
42
+ self.ui.groupBox_generated_variables.hide()
43
+
44
+ self.aeval = Interpreter(minimal=True) # Initialize asteval interpreter
45
+
46
+ def _populate_variables_table(self) -> None:
47
+ self.ui.tableWidget_variables.setRowCount(len(self.origParametrics.variables))
48
+ for row, (var_name, values) in enumerate(
49
+ self.origParametrics.variables.items()
50
+ ):
51
+ self.ui.tableWidget_variables.setItem(row, 0, QTableWidgetItem(var_name))
52
+ # Add QLineEdit for Min, Max, and Decimal Places
53
+ min_le = QLineEdit(str(min(values)) if values else "0")
54
+ max_le = QLineEdit(str(max(values)) if values else "100")
55
+ dec_le = QLineEdit("0") # Default to 0 decimal places
56
+
57
+ self.ui.tableWidget_variables.setCellWidget(row, 1, min_le)
58
+ self.ui.tableWidget_variables.setCellWidget(row, 2, max_le)
59
+ self.ui.tableWidget_variables.setCellWidget(row, 3, dec_le)
60
+
61
+ def _connect_signals(self) -> None:
62
+ self.ui.pushButton_addRule.clicked.connect(self._add_rule)
63
+ self.ui.pushButton_removeRule.clicked.connect(self._remove_rule)
64
+ self.ui.pushButton_generate.clicked.connect(self.generateSets)
65
+ self.ui.pushButton_cancel.clicked.connect(self.reject)
66
+ self.ui.pushButton_save.clicked.connect(self._save_variables_and_close)
67
+ self.ui.groupBox_existing_variables.toggled.connect(
68
+ self.ui.tableWidget_existing_variables.setVisible
69
+ )
70
+
71
+ def _add_rule(self) -> None:
72
+ rule_text = self.ui.lineEdit_newRule.text().strip()
73
+ if rule_text:
74
+ self.ui.listWidget_rules.addItem(rule_text)
75
+ self.ui.lineEdit_newRule.clear()
76
+
77
+ def _remove_rule(self) -> None:
78
+ for item in self.ui.listWidget_rules.selectedItems():
79
+ self.ui.listWidget_rules.takeItem(self.ui.listWidget_rules.row(item))
80
+
81
+ def generateSets(self) -> None:
82
+ self._generatedParametrics.resetVariables() # Clear previous generated sets
83
+ self._rule_error_occurred = False # Reset error flag
84
+
85
+ varConstraints = {}
86
+ for row in range(self.ui.tableWidget_variables.rowCount()):
87
+ var_name = self.ui.tableWidget_variables.item(row, 0).text()
88
+ varConstraints[var_name] = {
89
+ "min": float(self.ui.tableWidget_variables.cellWidget(row, 1).text()),
90
+ "max": float(self.ui.tableWidget_variables.cellWidget(row, 2).text()),
91
+ "decimal_places": int(
92
+ self.ui.tableWidget_variables.cellWidget(row, 3).text()
93
+ ),
94
+ }
95
+
96
+ rules = [
97
+ self.ui.listWidget_rules.item(i).text()
98
+ for i in range(self.ui.listWidget_rules.count())
99
+ ]
100
+
101
+ num_sets = self.ui.spinBox_numSets.value()
102
+
103
+ try:
104
+ generated_sets = [
105
+ self._findSet(varConstraints, rules) for _ in range(num_sets)
106
+ ]
107
+ except IndexError as e:
108
+ logger.exception("Invalid variables in Rule:")
109
+ QMessageBox.critical(self, "Rule Error", f"{e}")
110
+ except ValueError as e:
111
+ logger.warning("No variable set found:")
112
+ QMessageBox.warning(
113
+ self,
114
+ "Generation Failed",
115
+ f"{e} Consider relaxing your rules or increasing the number of attempts.",
116
+ )
117
+ else:
118
+ # convert the generated_sets list[dict[str, float]] into dict[str, list[float]]
119
+ # [{A:7, B:8}, {A:11, B:9}] -> {A: [7, 11], B: [8, 9]}
120
+ newVariables = {}
121
+ for var in self.origParametrics.variables:
122
+ newVariables[var] = [dataSet[var] for dataSet in generated_sets]
123
+ self._generatedParametrics.variables = newVariables
124
+ self.ui.groupBox_generated_variables.show()
125
+ populateDataSetTable(
126
+ self.ui.tableWidget_generated_variables,
127
+ parametrics=self._generatedParametrics,
128
+ )
129
+
130
+ def _findSet(
131
+ self,
132
+ constraints: dict[str, dict[str, float | int]],
133
+ rules: list[str],
134
+ maxAttempts: int = 1000,
135
+ ) -> dict[str, float]:
136
+ """Generate Random numbers for each variable and check if the rules apply.
137
+
138
+ Raises
139
+ ------
140
+ `IndexError`: if the evaluation of the rule returns `None`
141
+ `ValueError`: if fater `maxAttemps` no set is found
142
+
143
+ """
144
+ attempts = 0
145
+ while attempts < maxAttempts:
146
+ current_set: dict[str, float] = {}
147
+ # Generate initial values based on min/max constraints
148
+ for var_name, constr in constraints.items():
149
+ min_val: float = constr["min"]
150
+ max_val: float = constr["max"]
151
+ dec_places: int = constr["decimal_places"]
152
+
153
+ if dec_places == 0:
154
+ current_set[var_name] = float(
155
+ random.randint(int(min_val), int(max_val))
156
+ )
157
+ else:
158
+ current_set[var_name] = round(
159
+ random.uniform(min_val, max_val), dec_places
160
+ )
161
+ if self._check_rules(current_set, rules):
162
+ logger.info("Found matching set after %s attemps", attempts)
163
+ return current_set
164
+ attempts += 1
165
+ msg = f"Could not generate a valid set after {maxAttempts} attempts."
166
+ raise ValueError(msg)
167
+
168
+ def _check_rules(
169
+ self, varSet: dict[str, float], rules: list[str], show_error: bool = True
170
+ ) -> bool:
171
+ # Create a local scope for evaluating rules
172
+ self.aeval.symtable.update(varSet)
173
+
174
+ for rule in rules:
175
+ # Evaluate the rule using asteval
176
+ res = self.aeval(rule)
177
+ if res is None:
178
+ msg = f"Error evaluating rule '{rule}'"
179
+ raise IndexError(msg)
180
+ if res is False:
181
+ return False
182
+ return True
183
+
184
+ def _save_variables_and_close(self) -> None:
185
+ """Format variables set to fit `Parametrics`."""
186
+ logger.info("Saving new variables to the question")
187
+ newVars = self.origParametrics.variables.copy()
188
+ for varName in newVars:
189
+ newVars[varName].extend(self._generatedParametrics.variables[varName])
190
+ self.origParametrics.variableRules = [
191
+ self.ui.listWidget_rules.item(i).text()
192
+ for i in range(self.ui.listWidget_rules.count())
193
+ ]
194
+ self.origParametrics.variables = newVars
195
+ logger.info("Rules saved to Parametrics.")
196
+ self.accept()
197
+
198
+
199
+ def populateDataSetTable(
200
+ tableWidget: QTableWidget,
201
+ parametrics: Parametrics | None = None,
202
+ ) -> None:
203
+ """Insert all Variables with their values into `tableWidget`."""
204
+ if parametrics is None:
205
+ return
206
+ variables = parametrics.variables
207
+ variants = parametrics.variants
208
+ tableWidget.setRowCount(len(variables) + len(parametrics.results))
209
+ tableWidget.setColumnCount(variants + 1) # Variable Name + Variants
210
+ headers = ["Variable"] + [f"Set {i + 1}" for i in range(variants)]
211
+ tableWidget.setHorizontalHeaderLabels(headers)
212
+ for row, (var, values) in enumerate(variables.items()):
213
+ tableWidget.setItem(row, 0, QTableWidgetItem(var))
214
+ for col, value in enumerate(values):
215
+ tableWidget.setItem(row, col + 1, QTableWidgetItem(str(value)))
216
+ for row, results in parametrics.results.items():
217
+ logger.debug("adding Results to the DataSetTable: %s", results)
218
+ tableWidget.setItem(
219
+ len(variables) + row - 1, 0, QTableWidgetItem(f"Results: {row}")
220
+ )
221
+ for variant, res in enumerate(results):
222
+ tableWidget.setItem(
223
+ len(variables) + row - 1,
224
+ variant + 1,
225
+ QTableWidgetItem(str(res)),
226
+ )
227
+ tableWidget.resizeColumnsToContents()
228
+
229
+
230
+ # This part is for testing the UI independently
231
+ if __name__ == "__main__":
232
+ import sys
233
+
234
+ from PySide6.QtWidgets import QApplication
235
+
236
+ # Mock ParametricQuestion for testing
237
+ class MockParametricQuestion:
238
+ def __init__(self) -> None:
239
+ self.origParametrics.variables = {
240
+ "a": [1.0, 2.0, 3.0],
241
+ "b": [10, 20, 30],
242
+ "c": [0.5, 1.5, 2.5],
243
+ }
244
+
245
+ app = QApplication(sys.argv)
246
+ mock_question = MockParametricQuestion()
247
+ dialog = VariableGeneratorDialog(paramQuestion=mock_question)
248
+ if dialog.exec():
249
+ print("Generated Sets:", dialog.generatedVarSets())
250
+ sys.exit(app.exec())
@@ -5,7 +5,6 @@ All Answers are calculated off an equation using the same variables.
5
5
  """
6
6
 
7
7
  import logging
8
- import math
9
8
  import re
10
9
  from typing import Literal, overload
11
10
 
@@ -16,8 +15,12 @@ from excel2moodle.core.globals import (
16
15
  Tags,
17
16
  TextElements,
18
17
  )
19
- from excel2moodle.core.question import ParametricQuestion
18
+ from excel2moodle.core.question import (
19
+ ParametricQuestion,
20
+ Parametrics,
21
+ )
20
22
  from excel2moodle.core.settings import Tags
23
+ from excel2moodle.logger import LogAdapterQuestionID
21
24
  from excel2moodle.question_types.nfm import NFMQuestionParser
22
25
 
23
26
  logger = logging.getLogger(__name__)
@@ -28,12 +31,72 @@ class ClozePart:
28
31
  self,
29
32
  question: ParametricQuestion,
30
33
  text: list[str],
34
+ number: int,
31
35
  ) -> None:
32
36
  self.question = question
33
- self.text: list[ET.Element] = self._setupText(text)
37
+ self.text: ET.Element = self._setupText(text)
38
+ self.num: int = number
34
39
  if not self.text:
35
40
  msg = f"Answer part for cloze question {self.question.id} is invalid without partText"
36
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}"
37
100
 
38
101
  @property
39
102
  def points(self) -> float:
@@ -47,12 +110,6 @@ class ClozePart:
47
110
  def points(self, points: float) -> None:
48
111
  self._points = points if points > 0 else 0.0
49
112
 
50
- @property
51
- def typ(self) -> Literal["MC", "NFM"] | None:
52
- if hasattr(self, "_typ"):
53
- return self._typ
54
- return None
55
-
56
113
  @property
57
114
  def mcAnswerString(self) -> str:
58
115
  if hasattr(self, "_mcAnswer"):
@@ -65,44 +122,14 @@ class ClozePart:
65
122
  self._mcAnswer: str = answerString
66
123
 
67
124
  def _setupText(self, text: list[str]) -> ET.Element:
68
- textList: list[ET.Element] = []
125
+ textItem: ET.Element = TextElements.LISTITEM.create()
69
126
  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
127
+ textItem.append(TextElements.PLEFT.create())
128
+ textItem[-1].text = t
129
+ return textItem
98
130
 
99
131
  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}'"
132
+ return f"Cloze Part {self.id}-{self.typ}"
106
133
 
107
134
 
108
135
  class ClozeQuestion(ParametricQuestion):
@@ -112,6 +139,7 @@ class ClozeQuestion(ParametricQuestion):
112
139
  super().__init__(*args, **kwargs)
113
140
  self.questionParts: dict[int, ClozePart] = {}
114
141
  self.questionTexts: list[ET.Element] = []
142
+ self.parametrics: Parametrics
115
143
 
116
144
  @property
117
145
  def partsNum(self) -> int:
@@ -127,37 +155,16 @@ class ClozeQuestion(ParametricQuestion):
127
155
  pts = self.rawData.get(Tags.POINTS)
128
156
  return pts
129
157
 
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)
158
+ def getUpdatedElement(self, variant: int = 0) -> ET.Element:
159
+ """Update and get the Question Elements to reflect the version.
155
160
 
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
+ `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)
161
168
 
162
169
 
163
170
  class ClozeQuestionParser(NFMQuestionParser):
@@ -176,21 +183,53 @@ class ClozeQuestionParser(NFMQuestionParser):
176
183
  self._parseAnswerParts()
177
184
 
178
185
  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
- }
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
+ )
184
193
  partsNum = len(parts)
185
194
  equations: dict[int, str] = self._getPartValues(Tags.RESULT)
186
195
  trueAnsws: dict[int, list[str]] = self._getPartValues(Tags.TRUE)
187
196
  falseAnsws: dict[int, list[str]] = self._getPartValues(Tags.FALSE)
188
197
  points: dict[int, float] = self._getPartValues(Tags.POINTS)
198
+ firstResult: dict[int, float] = self._getPartValues(Tags.FIRSTRESULT)
189
199
  for num, part in parts.items():
200
+ loclogger = LogAdapterQuestionID(
201
+ logger, {"qID": f"{self.question.id}-{num}"}
202
+ )
190
203
  eq = equations.get(num)
191
- true = trueAnsws.get(num)
192
- false = falseAnsws.get(num)
193
- part.setAnswer(equation=eq, trueAns=true, falseAns=false)
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
+ )
220
+ else:
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)
194
233
  if len(points) == 0:
195
234
  pts = round(self.rawInput.get(Tags.POINTS) / partsNum, 3)
196
235
  for part in parts.values():
@@ -202,13 +241,14 @@ class ClozeQuestionParser(NFMQuestionParser):
202
241
  for num, part in parts.items():
203
242
  p = points.get(num)
204
243
  part.points = p if p is not None else self.rawInput.get(Tags.POINTS)
205
-
206
244
  self.question.questionParts = parts
207
245
 
208
246
  @overload
209
247
  def _getPartValues(self, Tag: Literal[Tags.RESULT]) -> dict[int, str]: ...
210
248
  @overload
211
- def _getPartValues(self, Tag: Literal[Tags.POINTS]) -> dict[int, float]: ...
249
+ def _getPartValues(
250
+ self, Tag: Literal[Tags.POINTS, Tags.FIRSTRESULT]
251
+ ) -> dict[int, float]: ...
212
252
  @overload
213
253
  def _getPartValues(
214
254
  self, Tag: Literal[Tags.TRUE, Tags.FALSE]
@@ -219,57 +259,48 @@ class ClozeQuestionParser(NFMQuestionParser):
219
259
  for key in self.rawInput
220
260
  if key.startswith(Tag)
221
261
  }
222
- self.logger.warning("Found part data %s: %s", Tag, tagValues)
223
262
  return tagValues
224
263
 
225
264
  def _parseAnswerParts(self) -> None:
226
265
  """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",
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(
262
274
  result,
263
- firstResult,
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
264
284
  )
265
- return result
266
- msg = f"The expression {part.equation} could not be evaluated."
267
- raise QNotParsedException(msg, self.question.id)
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)
268
299
 
269
300
  def getPartNumber(self, indexKey: str) -> int:
270
301
  """Return the number of the question Part.
271
302
 
272
- The number should be given after the `@` sign.
303
+ The number should be given after the `:` colon.
273
304
  This is number is used, to reference the question Text
274
305
  and the expected answer fields together.
275
306
  """