excel2moodle 0.6.1__py3-none-any.whl → 0.6.3__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.
@@ -60,8 +60,9 @@ class Tags(StrEnum):
60
60
  """Get type of the keys data."""
61
61
  return self._typ_
62
62
 
63
- QUESTIONVARIANT = "defaultQuestionVariant", int, 1, "testgen"
64
- INCLUDEINCATS = "includeCats", bool, False, "testgen"
63
+ QUESTIONVARIANT = "defaultquestionvariant", int, 1, "testgen"
64
+ INCLUDEINCATS = "includecats", bool, False, "testgen"
65
+ GENEXPORTREPORT = "exportreport", bool, False, "testgen"
65
66
  TOLERANCE = "tolerance", float, 0.01, "parser/nf"
66
67
  PICTUREFOLDER = "pictureFolder", Path, None, "core"
67
68
  PICTURESUBFOLDER = "imgfolder", str, "Abbildungen", "project"
@@ -87,8 +88,13 @@ class Tags(StrEnum):
87
88
  POINTS = "points", float, 1.0
88
89
  PICTUREWIDTH = "imgwidth", int, 500
89
90
  ANSPICWIDTH = "answerimgwidth", int, 120
90
- WRONGSIGNPERCENT = "wrongsignpercent", int, 50
91
91
  FIRSTRESULT = "firstresult", float, 0
92
+ WRONGSIGNPERCENT = "wrongsignpercent", int, 50
93
+ WRONGSIGNFB = "wrongsignfeedback", str, "your result has the wrong sign (+-)"
94
+ TRUEFB = "truefeedback", str, "congratulations!!! your answer is right."
95
+ FALSEFB = "falsefeedback", str, "Your answer is sadly wrong, try again!!!"
96
+ PCORRECFB = "partialcorrectfeedback", str, "Your answer is partially right."
97
+ GENERALFB = "feedback", str, "You answered this question."
92
98
 
93
99
 
94
100
  class Settings:
@@ -101,6 +107,10 @@ class Settings:
101
107
  def clear(cls) -> None:
102
108
  cls.values.clear()
103
109
 
110
+ @classmethod
111
+ def pop(cls, key: str):
112
+ return cls.values.pop(key)
113
+
104
114
  @overload
105
115
  @classmethod
106
116
  def get(
@@ -121,7 +131,7 @@ class Settings:
121
131
  ) -> int: ...
122
132
  @overload
123
133
  @classmethod
124
- def get(cls, key: Literal[Tags.INCLUDEINCATS]) -> bool: ...
134
+ def get(cls, key: Literal[Tags.INCLUDEINCATS, Tags.GENEXPORTREPORT]) -> bool: ...
125
135
  @overload
126
136
  @classmethod
127
137
  def get(
@@ -132,6 +142,7 @@ class Settings:
132
142
  Tags.LOGFILE,
133
143
  Tags.CATEGORIESSHEET,
134
144
  Tags.IMPORTMODULE,
145
+ Tags.WRONGSIGNFB,
135
146
  ],
136
147
  ) -> str: ...
137
148
  @overload
@@ -153,7 +164,7 @@ class Settings:
153
164
  default = key.default
154
165
  if default is None:
155
166
  return None
156
- logger.info("Returning the default value for %s", key)
167
+ logger.debug("Returning the default value for %s", key)
157
168
  return default
158
169
  if key.typ() is Path:
159
170
  path: Path = Path(raw)
@@ -32,8 +32,6 @@ from pathlib import Path
32
32
 
33
33
  import pandas as pd
34
34
 
35
- from excel2moodle.core import numericMultiQ as nmq
36
-
37
35
  # Hier Bitte die Frage angeben, die getestet Werden soll:
38
36
 
39
37
  # ===========================================================
@@ -0,0 +1,48 @@
1
+ """This module provides functions to query the GitLab API for project information."""
2
+
3
+ import json
4
+ import sys
5
+ import urllib.request
6
+
7
+
8
+ def get_latest_tag(project_id: str) -> str | None:
9
+ """Queries the GitLab API for the latest tag of a project.
10
+
11
+ Args:
12
+ project_id: The URL-encoded project ID (e.g., 'jbosse3%2Fexcel2moodle').
13
+
14
+ Returns:
15
+ The name of the latest tag.
16
+
17
+ """
18
+ url = f"https://gitlab.com/api/v4/projects/{project_id}/repository/tags"
19
+ try:
20
+ with urllib.request.urlopen(url) as response:
21
+ if response.status == 200:
22
+ data = json.loads(response.read().decode())
23
+ if data:
24
+ return data[0]["name"]
25
+ except urllib.error.URLError as e:
26
+ print(f"Error fetching latest tag: {e}", file=sys.stderr)
27
+ return None
28
+
29
+
30
+ def get_changelog(project_id: str, branch: str = "master") -> str:
31
+ """Queries the GitLab API for the content of the CHANGELOG.md file.
32
+
33
+ Args:
34
+ project_id: The URL-encoded project ID (e.g., 'jbosse3%2Fexcel2moodle').
35
+ branch: The branch to get the file from.
36
+
37
+ Returns:
38
+ The content of the CHANGELOG.md file.
39
+
40
+ """
41
+ url = f"https://gitlab.com/api/v4/projects/{project_id}/repository/files/CHANGELOG.md/raw?ref={branch}"
42
+ try:
43
+ with urllib.request.urlopen(url) as response:
44
+ if response.status == 200:
45
+ return response.read().decode()
46
+ except urllib.error.URLError as e:
47
+ print(f"Error fetching changelog: {e}", file=sys.stderr)
48
+ return ""
@@ -2,9 +2,11 @@ import logging
2
2
  import random
3
3
 
4
4
  from asteval import Interpreter
5
+ from PySide6.QtCore import Qt, Slot
5
6
  from PySide6.QtWidgets import (
6
7
  QDialog,
7
8
  QLineEdit,
9
+ QListWidgetItem,
8
10
  QMainWindow,
9
11
  QMessageBox,
10
12
  QTableWidget,
@@ -52,7 +54,7 @@ class VariableGeneratorDialog(QDialog):
52
54
  # Add QLineEdit for Min, Max, and Decimal Places
53
55
  min_le = QLineEdit(str(min(values)) if values else "0")
54
56
  max_le = QLineEdit(str(max(values)) if values else "100")
55
- dec_le = QLineEdit("0") # Default to 0 decimal places
57
+ dec_le = QLineEdit("1") # Default to 0 decimal places
56
58
 
57
59
  self.ui.tableWidget_variables.setCellWidget(row, 1, min_le)
58
60
  self.ui.tableWidget_variables.setCellWidget(row, 2, max_le)
@@ -67,11 +69,20 @@ class VariableGeneratorDialog(QDialog):
67
69
  self.ui.groupBox_existing_variables.toggled.connect(
68
70
  self.ui.tableWidget_existing_variables.setVisible
69
71
  )
72
+ self.ui.listWidget_rules.itemDoubleClicked.connect(self._edit_rule)
73
+
74
+ @Slot(QListWidgetItem)
75
+ def _edit_rule(self, item) -> None:
76
+ """Move the double-clicked rule into the line edit and remove it from the list."""
77
+ self.ui.lineEdit_newRule.setText(item.text())
78
+ self.ui.listWidget_rules.takeItem(self.ui.listWidget_rules.row(item))
70
79
 
71
80
  def _add_rule(self) -> None:
72
81
  rule_text = self.ui.lineEdit_newRule.text().strip()
73
82
  if rule_text:
74
- self.ui.listWidget_rules.addItem(rule_text)
83
+ # Check if the rule already exists. If so, do nothing.
84
+ if not self.ui.listWidget_rules.findItems(rule_text, Qt.MatchExactly):
85
+ self.ui.listWidget_rules.addItem(rule_text)
75
86
  self.ui.lineEdit_newRule.clear()
76
87
 
77
88
  def _remove_rule(self) -> None:
@@ -100,40 +111,70 @@ class VariableGeneratorDialog(QDialog):
100
111
 
101
112
  num_sets = self.ui.spinBox_numSets.value()
102
113
 
114
+ # Build a set of existing variable combinations to ensure we don't generate duplicates of them.
115
+ unique_sets_tracker = set()
116
+ if self.origParametrics.variables:
117
+ var_names = list(self.origParametrics.variables.keys())
118
+ if var_names:
119
+ # Assuming all variable lists have the same length
120
+ num_variants = len(self.origParametrics.variables[var_names[0]])
121
+ for i in range(num_variants):
122
+ existing_set = {
123
+ var: self.origParametrics.variables[var][i] for var in var_names
124
+ }
125
+ unique_sets_tracker.add(frozenset(existing_set.items()))
126
+
127
+ generated_sets = [] # This will be a list of dicts
103
128
  try:
104
- generated_sets = [
105
- self._findSet(varConstraints, rules) for _ in range(num_sets)
106
- ]
129
+ while len(generated_sets) < num_sets:
130
+ new_set = self._findSet(varConstraints, rules, unique_sets_tracker)
131
+ generated_sets.append(new_set)
132
+ unique_sets_tracker.add(frozenset(new_set.items()))
133
+
107
134
  except IndexError as e:
108
135
  logger.exception("Invalid variables in Rule:")
109
136
  QMessageBox.critical(self, "Rule Error", f"{e}")
137
+ return # Stop generation
110
138
  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
- )
139
+ logger.warning("Failed to generate a new unique set: %s", e)
140
+ if len(generated_sets) < num_sets:
141
+ QMessageBox.warning(
142
+ self,
143
+ "Generation Incomplete",
144
+ f"Could only generate {len(generated_sets)} unique sets out of the requested {num_sets}. "
145
+ "The space of possible unique combinations may be exhausted.",
146
+ )
147
+
148
+ if not generated_sets:
149
+ logger.info("No new variable sets were generated.")
150
+ if not self._rule_error_occurred:
151
+ QMessageBox.information(
152
+ self,
153
+ "No Sets Generated",
154
+ "No new unique variable sets could be generated with the given constraints and rules.",
155
+ )
156
+ return
157
+
158
+ # convert the generated_sets list[dict[str, float]] into dict[str, list[float]]
159
+ # [{A:7, B:8}, {A:11, B:9}] -> {A: [7, 11], B: [8, 9]}
160
+ newVariables = {}
161
+ for var in self.origParametrics.variables:
162
+ newVariables[var] = [dataSet[var] for dataSet in generated_sets]
163
+ self._generatedParametrics.variables = newVariables
164
+ self.ui.groupBox_generated_variables.show()
165
+ populateDataSetTable(
166
+ self.ui.tableWidget_generated_variables,
167
+ parametrics=self._generatedParametrics,
168
+ )
129
169
 
130
170
  def _findSet(
131
171
  self,
132
172
  constraints: dict[str, dict[str, float | int]],
133
173
  rules: list[str],
174
+ existing_sets: set[frozenset],
134
175
  maxAttempts: int = 1000,
135
176
  ) -> dict[str, float]:
136
- """Generate Random numbers for each variable and check if the rules apply.
177
+ """Generate a random set of variables that satisfies the rules and is not in existing_sets.
137
178
 
138
179
  Raises
139
180
  ------
@@ -143,6 +184,7 @@ class VariableGeneratorDialog(QDialog):
143
184
  """
144
185
  attempts = 0
145
186
  while attempts < maxAttempts:
187
+ attempts += 1
146
188
  current_set: dict[str, float] = {}
147
189
  # Generate initial values based on min/max constraints
148
190
  for var_name, constr in constraints.items():
@@ -158,11 +200,15 @@ class VariableGeneratorDialog(QDialog):
158
200
  current_set[var_name] = round(
159
201
  random.uniform(min_val, max_val), dec_places
160
202
  )
203
+
204
+ # Check for uniqueness first, as it's a cheaper check than evaluating rules.
205
+ if frozenset(current_set.items()) in existing_sets:
206
+ continue # It's a duplicate, try again.
207
+
161
208
  if self._check_rules(current_set, rules):
162
- logger.info("Found matching set after %s attemps", attempts)
209
+ logger.info("Found matching unique set after %s attempts", attempts)
163
210
  return current_set
164
- attempts += 1
165
- msg = f"Could not generate a valid set after {maxAttempts} attempts."
211
+ msg = f"Could not generate a valid unique set after {maxAttempts} attempts."
166
212
  raise ValueError(msg)
167
213
 
168
214
  def _check_rules(
@@ -226,25 +272,3 @@ def populateDataSetTable(
226
272
  )
227
273
  tableWidget.resizeColumnsToContents()
228
274
 
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,6 +5,7 @@ All Answers are calculated off an equation using the same variables.
5
5
  """
6
6
 
7
7
  import logging
8
+ import math
8
9
  import re
9
10
  from typing import Literal, overload
10
11
 
@@ -18,6 +19,7 @@ from excel2moodle.core.globals import (
18
19
  from excel2moodle.core.question import (
19
20
  ParametricQuestion,
20
21
  Parametrics,
22
+ QuestionData,
21
23
  )
22
24
  from excel2moodle.core.settings import Tags
23
25
  from excel2moodle.logger import LogAdapterQuestionID
@@ -67,7 +69,8 @@ class ClozePart:
67
69
  return
68
70
  if self.typ == "NFM":
69
71
  result = self.result.getResult(variant)
70
- self._element.text = ClozeQuestionParser.getNumericAnsStr(
72
+ self._element.text = self.getNumericAnsStr(
73
+ self.question.rawData,
71
74
  result,
72
75
  self.question.rawData.get(Tags.TOLERANCE),
73
76
  wrongSignCount=self.question.rawData.get(Tags.WRONGSIGNPERCENT),
@@ -99,16 +102,20 @@ class ClozePart:
99
102
  return f"{self.question.id}-{self.num}"
100
103
 
101
104
  @property
102
- def points(self) -> float:
105
+ def points(self) -> int:
106
+ """Points of clozes can be only integers.
107
+
108
+ Otherwise the moodle import fails.
109
+ """
103
110
  if hasattr(self, "_points"):
104
111
  return self._points
105
- return 0.0
112
+ return 0
106
113
  self.question.logger.error("Invalid call to points of unparsed cloze part")
107
- return 0.0
114
+ return 0
108
115
 
109
116
  @points.setter
110
- def points(self, points: float) -> None:
111
- self._points = points if points > 0 else 0.0
117
+ def points(self, points: int) -> None:
118
+ self._points = max(0, points)
112
119
 
113
120
  @property
114
121
  def mcAnswerString(self) -> str:
@@ -131,6 +138,73 @@ class ClozePart:
131
138
  def __repr__(self) -> str:
132
139
  return f"Cloze Part {self.id}-{self.typ}"
133
140
 
141
+ @staticmethod
142
+ def getNumericAnsStr(
143
+ questionData: QuestionData,
144
+ result: float,
145
+ tolerance: float = 0.0,
146
+ points: int = 1,
147
+ wrongSignCount: int = 0,
148
+ wrongSignFeedback: str | None = None,
149
+ ) -> str:
150
+ """Generate the answer string from `result`.
151
+
152
+ Parameters.
153
+ ----------
154
+ wrongSignCount:
155
+ If the wrong sign `+` or `-` is given, how much of the points should be given.
156
+ Interpreted as percent.
157
+ tolerance:
158
+ The relative tolerance, as fraction
159
+
160
+ """
161
+ if wrongSignFeedback is None:
162
+ wrongSignFeedback = questionData.get(Tags.WRONGSIGNFB)
163
+ if wrongSignCount == 0:
164
+ wrongSignCount = questionData.get(Tags.WRONGSIGNPERCENT)
165
+ if tolerance == 0.0:
166
+ tolerance = questionData.get(Tags.TOLERANCE)
167
+ absTol = f":{round(result * tolerance, 3)}"
168
+ answerParts: list[str | float] = [
169
+ "{",
170
+ points,
171
+ ":NUMERICAL:=",
172
+ round(result, 3),
173
+ absTol,
174
+ "~%",
175
+ wrongSignCount,
176
+ "%",
177
+ round(result * (-1), 3),
178
+ absTol,
179
+ f"#{wrongSignFeedback}",
180
+ "}",
181
+ ]
182
+ answerPStrings = [str(part) for part in answerParts]
183
+ return "".join(answerPStrings)
184
+
185
+ @staticmethod
186
+ def getMCAnsStr(
187
+ true: list[str],
188
+ false: list[str],
189
+ points: int = 1,
190
+ ) -> str:
191
+ """Generate the answer string for the MC answers."""
192
+ truePercent: float = round(100 / len(true), 1)
193
+ falsePercent: float = round(100 / len(false), 1)
194
+ falseList: list[str] = [f"~%-{falsePercent}%{ans}" for ans in false]
195
+ trueList: list[str] = [f"~%{truePercent}%{ans}" for ans in true]
196
+ answerParts: list[str | float] = [
197
+ "{",
198
+ points,
199
+ ":MULTIRESPONSE:",
200
+ ]
201
+ answerParts.extend(trueList)
202
+ answerParts.extend(falseList)
203
+ answerParts.append("}")
204
+
205
+ answerPStrings = [str(part) for part in answerParts]
206
+ return "".join(answerPStrings)
207
+
134
208
 
135
209
  class ClozeQuestion(ParametricQuestion):
136
210
  """Cloze Question Type."""
@@ -146,13 +220,18 @@ class ClozeQuestion(ParametricQuestion):
146
220
  return len(self.questionParts)
147
221
 
148
222
  @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)
223
+ def points(self) -> int:
224
+ """Points for the cloze question. The sum of all its parts points.
225
+
226
+ Returns only integer values.
227
+ """
228
+ pts: int = 0
229
+ if not self.isParsed:
230
+ msg = "The Cloze question has no points because it is not yet parsed"
231
+ self.logger.warning(msg)
232
+ return pts
233
+ for p in self.questionParts.values():
234
+ pts = pts + p.points
156
235
  return pts
157
236
 
158
237
  def getUpdatedElement(self, variant: int = 0) -> ET.Element:
@@ -232,17 +311,30 @@ class ClozeQuestionParser(NFMQuestionParser):
232
311
  raise QNotParsedException(msg, self.question.id)
233
312
  if len(points) == 0:
234
313
  pts = round(self.rawInput.get(Tags.POINTS) / partsNum, 3)
314
+ point = self._roundClozePartPoints(pts)
235
315
  for part in parts.values():
236
- part.points = pts
237
- elif len(points) != partsNum:
238
- logger.warning(
316
+ part.points = point
317
+ else:
318
+ loclogger.warning(
239
319
  "Some Answer parts are missing the points, they will get the standard points"
240
320
  )
241
321
  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)
322
+ part.points = self._roundClozePartPoints(points=points.get(num))
244
323
  self.question.questionParts = parts
245
324
 
325
+ def _roundClozePartPoints(self, points: float | None = None) -> int:
326
+ """Get the integer points for the cloze part."""
327
+ if points is None:
328
+ points = self.rawInput.get(Tags.POINTS)
329
+ corrPoints: int = round(points)
330
+ if not math.isclose(corrPoints, points):
331
+ self.logger.warning(
332
+ "Type cloze supports only integers as points. %s was round to %s",
333
+ points,
334
+ corrPoints,
335
+ )
336
+ return corrPoints
337
+
246
338
  @overload
247
339
  def _getPartValues(self, Tag: Literal[Tags.RESULT]) -> dict[int, str]: ...
248
340
  @overload
@@ -270,20 +362,20 @@ class ClozeQuestionParser(NFMQuestionParser):
270
362
  for partNum, part in self.question.questionParts.items():
271
363
  if part.typ == "NFM":
272
364
  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),
365
+ ansStr = ClozePart.getNumericAnsStr(
366
+ self.rawInput,
367
+ result=result,
277
368
  points=part.points,
278
369
  )
279
- self.logger.info("NF answer part: %s ", ansStr)
280
- logger.debug("Appended NF part %s result", partNum)
370
+ self.logger.debug("Generated %s answer part: %s ", partNum, ansStr)
281
371
  elif part.typ == "MC":
282
- ansStr = self.getMCAnsStr(
283
- part.trueAnswers, part.falseAnswers, points=part.points
372
+ ansStr = ClozePart.getMCAnsStr(
373
+ part.trueAnswers,
374
+ part.falseAnswers,
375
+ points=part.points,
284
376
  )
285
377
  part.mcAnswerString = ansStr
286
- logger.debug("Appended MC part %s: %s", partNum, ansStr)
378
+ self.logger.debug("Appended MC part %s: %s", partNum, ansStr)
287
379
  else:
288
380
  msg = "Type of the answer part is invalid"
289
381
  raise QNotParsedException(msg, self.id)
@@ -311,63 +403,3 @@ class ClozeQuestionParser(NFMQuestionParser):
311
403
  raise QNotParsedException(msg, self.question.id)
312
404
  else:
313
405
  return int(num)
314
-
315
- @staticmethod
316
- def getNumericAnsStr(
317
- result: float,
318
- tolerance: float,
319
- points: float = 1,
320
- wrongSignCount: int = 50,
321
- wrongSignFeedback: str = "your result has the wrong sign (+-)",
322
- ) -> str:
323
- """Generate the answer string from `result`.
324
-
325
- Parameters.
326
- ----------
327
- wrongSignCount:
328
- If the wrong sign `+` or `-` is given, how much of the points should be given.
329
- Interpreted as percent.
330
- tolerance:
331
- The relative tolerance, as fraction
332
-
333
- """
334
- absTol = f":{round(result * tolerance, 3)}"
335
- answerParts: list[str | float] = [
336
- "{",
337
- points,
338
- ":NUMERICAL:=",
339
- round(result, 3),
340
- absTol,
341
- "~%",
342
- wrongSignCount,
343
- "%",
344
- round(result * (-1), 3),
345
- absTol,
346
- f"#{wrongSignFeedback}",
347
- "}",
348
- ]
349
- answerPStrings = [str(part) for part in answerParts]
350
- return "".join(answerPStrings)
351
-
352
- @staticmethod
353
- def getMCAnsStr(
354
- true: list[str],
355
- false: list[str],
356
- points: float = 1,
357
- ) -> str:
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]
363
- answerParts: list[str | float] = [
364
- "{",
365
- points,
366
- ":MULTIRESPONSE:",
367
- ]
368
- answerParts.extend(trueList)
369
- answerParts.extend(falseList)
370
- answerParts.append("}")
371
-
372
- answerPStrings = [str(part) for part in answerParts]
373
- return "".join(answerPStrings)
@@ -12,7 +12,6 @@ from excel2moodle.core.globals import (
12
12
  Tags,
13
13
  TextElements,
14
14
  XMLTags,
15
- feedbackStr,
16
15
  )
17
16
  from excel2moodle.core.parser import QuestionParser
18
17
  from excel2moodle.core.question import Picture, Question
@@ -46,11 +45,6 @@ class MCQuestionParser(QuestionParser):
46
45
 
47
46
  def __init__(self) -> None:
48
47
  super().__init__()
49
- self.genFeedbacks = [
50
- XMLTags.CORFEEDB,
51
- XMLTags.PCORFEEDB,
52
- XMLTags.INCORFEEDB,
53
- ]
54
48
 
55
49
  def setup(self, question: MCQuestion) -> None:
56
50
  self.question: MCQuestion = question
@@ -76,9 +70,9 @@ class MCQuestionParser(QuestionParser):
76
70
  elementList[-1].append(text)
77
71
  if fraction < 0:
78
72
  elementList[-1].append(
79
- eth.getFeedBEle(
73
+ self.getFeedBEle(
80
74
  XMLTags.ANSFEEDBACK,
81
- text=feedbackStr["wrong"],
75
+ text=self.rawInput.get(Tags.FALSEFB),
82
76
  style=TextElements.SPANRED,
83
77
  ),
84
78
  )
@@ -86,9 +80,9 @@ class MCQuestionParser(QuestionParser):
86
80
  elementList[-1].append(self.falseImgs[i].element)
87
81
  elif fraction > 0:
88
82
  elementList[-1].append(
89
- eth.getFeedBEle(
83
+ self.getFeedBEle(
90
84
  XMLTags.ANSFEEDBACK,
91
- text=feedbackStr["right"],
85
+ text=self.rawInput.get(Tags.TRUEFB),
92
86
  style=TextElements.SPANGREEN,
93
87
  ),
94
88
  )
@@ -135,3 +129,14 @@ class MCQuestionParser(QuestionParser):
135
129
  self.getAnsElementsList(falseAList, fraction=round(falsefrac, 4)),
136
130
  )
137
131
  return ansList
132
+
133
+ def parse(self) -> None:
134
+ super().parse()
135
+ feedBacks = {
136
+ XMLTags.CORFEEDB: Tags.TRUEFB,
137
+ XMLTags.PCORFEEDB: Tags.PCORRECFB,
138
+ XMLTags.INCORFEEDB: Tags.FALSEFB,
139
+ }
140
+ for feedb, tag in feedBacks.items():
141
+ self.tmpEle.append(self.getFeedBEle(feedb, text=self.rawInput.get(tag)))
142
+ self._finalizeParsing()
@@ -30,7 +30,7 @@ class NFQuestionParser(QuestionParser):
30
30
 
31
31
  def __init__(self) -> None:
32
32
  super().__init__()
33
- self.genFeedbacks = [XMLTags.GENFEEDB]
33
+ self.feedBackList = {XMLTags.GENFEEDB: Tags.GENERALFB}
34
34
 
35
35
  def setup(self, question: NFQuestion) -> None:
36
36
  self.question: NFQuestion = question
@@ -41,3 +41,9 @@ class NFQuestionParser(QuestionParser):
41
41
  ansEle: list[ET.Element] = []
42
42
  ansEle.append(self.getNumericAnsElement(result=result))
43
43
  return ansEle
44
+
45
+ def _finalizeParsing(self) -> None:
46
+ self.tmpEle.append(
47
+ self.getFeedBEle(XMLTags.GENFEEDB, text=self.rawInput.get(Tags.GENERALFB))
48
+ )
49
+ return super()._finalizeParsing()