excel2moodle 0.7.0__py3-none-any.whl → 0.7.2__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.
@@ -57,18 +57,33 @@ class BulletList:
57
57
 
58
58
  def _setupBullets(self, bps: list[str]) -> ET.Element:
59
59
  self.logger.debug("Formatting the bulletpoint list")
60
- varFinder = re.compile(r"=\s*\{(\w+)\}")
60
+ varFinder = re.compile(r"\{(\w+)\}")
61
+ bulletFinder = re.compile(
62
+ r"^\s?(?P<desc>.*?)"
63
+ r"(?:\s+(?P<var>[\w+\{\\/\}^_-]+)\s*=\s*)"
64
+ r"(?P<val>[.,\{\w+\}]+)"
65
+ r"(?:\s+(?P<unit>[\w/\\^²³⁴⁵⁶]+)\s*$)"
66
+ )
61
67
  for i, item in enumerate(bps):
62
- sc_split = item.split()
63
- name = sc_split[0]
64
- var = sc_split[1]
65
- quant = sc_split[3]
66
- unit = sc_split[4]
67
-
68
- match = re.search(varFinder, item)
68
+ match = re.search(bulletFinder, item)
69
69
  if match is None:
70
+ self.logger.error("Couldn't find any bullets")
71
+ msg = f"Couldn't decode the bullet point: {item}"
72
+ raise ValueError(msg)
73
+ name = match.group("desc")
74
+ var = match.group("var")
75
+ unit = match.group("unit")
76
+ value = match.group("val")
77
+ self.logger.info(
78
+ "Decoded bulletPoint: name: %s, var: %s, - value: %s, - unit: %s.",
79
+ name,
80
+ var,
81
+ value,
82
+ unit,
83
+ )
84
+ if (match := re.search(varFinder, value)) is None:
70
85
  self.logger.debug("Got a normal bulletItem")
71
- num: float = float(quant.replace(",", "."))
86
+ num: float = float(value.replace(",", "."))
72
87
  bulletName = i + 1
73
88
  else:
74
89
  bulletName = match.group(1)
@@ -86,6 +101,7 @@ class BulletP:
86
101
  self.var: str = var
87
102
  self.unit: str = unit
88
103
  self.element: ET.Element
104
+ self.value: float = value
89
105
  self.update(value=value)
90
106
 
91
107
  def update(self, value: float = 1) -> None:
@@ -1,8 +1,11 @@
1
+ import logging
1
2
  from enum import Enum, StrEnum
3
+ from pathlib import Path
2
4
 
3
5
  import lxml.etree as ET
4
6
 
5
- from excel2moodle.core.settings import Tags
7
+ logger = logging.getLogger(__name__)
8
+
6
9
 
7
10
  QUESTION_TYPES = {
8
11
  "NF": "numerical",
@@ -12,6 +15,100 @@ QUESTION_TYPES = {
12
15
  }
13
16
 
14
17
 
18
+ class Tags(StrEnum):
19
+ """Tags and Settings Keys are needed to always acess the correct Value.
20
+
21
+ The Tags can be used to acess the settings or the QuestionData respectively.
22
+ As the QSettings settings are accesed via strings, which could easily gotten wrong.
23
+ Further, this Enum defines, which type a setting has to be.
24
+ """
25
+
26
+ QUESTIONVARIANT = "defaultquestionvariant", int, 1, "testgen"
27
+ INCLUDEINCATS = "includecats", bool, False, "testgen"
28
+ GENEXPORTREPORT = "exportreport", bool, False, "testgen"
29
+ TOLERANCE = "tolerance", float, 0.01, "parser/nf"
30
+ PICTUREFOLDER = "pictureFolder", Path, None, "core"
31
+ PICTURESUBFOLDER = "imgfolder", str, "Abbildungen", "project"
32
+ SPREADSHEETPATH = "spreadsheetFolder", Path, None, "core"
33
+ LOGLEVEL = "loglevel", str, "INFO", "core"
34
+ LOGFILE = "logfile", str, "excel2moodleLogFile.log", "core"
35
+ CATEGORIESSHEET = "categoriessheet", str, "Kategorien", "core"
36
+
37
+ IMPORTMODULE = "importmodule", str, None
38
+ TEXT = "text", list, None
39
+ BPOINTS = "bulletpoint", list, None
40
+ TRUE = "true", list, None
41
+ FALSE = "false", list, None
42
+ TYPE = "type", str, None
43
+ NAME = "name", str, None
44
+ RESULT = "result", float, None
45
+ EQUATION = "formula", str, None
46
+ PICTURE = "picture", str, None
47
+ NUMBER = "number", int, None
48
+ ANSTYPE = "answertype", str, None
49
+ QUESTIONPART = "part", list, None
50
+ PARTTYPE = "parttype", str, None
51
+ POINTS = "points", float, 1.0
52
+ PICTUREWIDTH = "imgwidth", int, 500
53
+ ANSPICWIDTH = "answerimgwidth", int, 120
54
+ FIRSTRESULT = "firstresult", float, 0
55
+ WRONGSIGNPERCENT = "wrongsignpercent", int, 50
56
+ WRONGSIGNFB = "wrongsignfeedback", str, "your result has the wrong sign (+-)"
57
+ TRUEFB = "truefeedback", str, "congratulations!!! your answer is right."
58
+ FALSEFB = "falsefeedback", str, "Your answer is sadly wrong, try again!!!"
59
+ PCORRECFB = "partialcorrectfeedback", str, "Your answer is partially right."
60
+ GENERALFB = "feedback", str, "You answered this question."
61
+ TRUEANSFB = "trueanswerfeedback", list, None
62
+ FALSEANSFB = "falseanswerfeedback", list, None
63
+
64
+ MEDIASCRIPTS = "mediascripts", list, None
65
+ MEDIACALL = "parametricmedia", str, None
66
+
67
+ def __new__(
68
+ cls,
69
+ key: str,
70
+ typ: type,
71
+ default: str | float | Path | bool | None,
72
+ place: str = "project",
73
+ ) -> object:
74
+ """Define new settings class."""
75
+ obj = str.__new__(cls, key)
76
+ obj._value_ = key
77
+ obj._typ_ = typ
78
+ obj._default_ = default
79
+ obj._place_ = place
80
+ return obj
81
+
82
+ def __init__(
83
+ self,
84
+ _,
85
+ typ: type,
86
+ default: str | float | Path | None,
87
+ place: str = "project",
88
+ ) -> None:
89
+ self._typ_: type = typ
90
+ self._place_: str = place
91
+ self._default_ = default
92
+ self._full_ = f"{self._place_}/{self._value_}"
93
+
94
+ @property
95
+ def default(self) -> str | int | float | Path | bool | None:
96
+ """Get default value for the key."""
97
+ return self._default_
98
+
99
+ @property
100
+ def place(self) -> str:
101
+ return self._place_
102
+
103
+ @property
104
+ def full(self) -> str:
105
+ return self._full_
106
+
107
+ def typ(self) -> type:
108
+ """Get type of the keys data."""
109
+ return self._typ_
110
+
111
+
15
112
  class TextElements(Enum):
16
113
  PLEFT = "p", "text-align: left;"
17
114
  SPANRED = "span", "color: rgb(239, 69, 64)"
@@ -36,38 +133,11 @@ class TextElements(Enum):
36
133
 
37
134
 
38
135
  class XMLTags(StrEnum):
39
- def __new__(cls, value: str, dfkey: Tags | None = None):
40
- obj = str.__new__(cls, value)
41
- obj._value_ = value
42
- if dfkey is not None:
43
- obj._dfkey_ = dfkey
44
- return obj
45
-
46
- def __init__(self, _: str, dfkey: Tags | None = None, getEle=None) -> None:
47
- if isinstance(dfkey, Tags):
48
- self._dfkey_: str = dfkey
49
- if getEle:
50
- self._getEle_: object = getEle
51
-
52
- @property
53
- def dfkey(self) -> str:
54
- return self._dfkey_
55
-
56
- def set(self, getEle) -> None:
57
- self._getEle_ = getEle
58
-
59
- def __repr__(self) -> str:
60
- msg = []
61
- msg.append(f"XML Tag {self.value=}")
62
- if hasattr(self, "_dfkey_"):
63
- msg.append(f"Df Key {self.dfkey=}")
64
- return "\n".join(msg)
65
-
66
- NAME = "name", Tags.NAME
67
- QTEXT = "questiontext", Tags.TEXT
136
+ NAME = "name"
137
+ QTEXT = "questiontext"
68
138
  QUESTION = "question"
69
139
  TEXT = "text"
70
- PICTURE = "file", Tags.PICTURE
140
+ PICTURE = "file"
71
141
  GENFEEDB = "generalfeedback"
72
142
  CORFEEDB = "correctfeedback"
73
143
  PCORFEEDB = "partialcorrectfeedback"
@@ -34,18 +34,18 @@ class QuestionParser:
34
34
 
35
35
  def setup(self, question: Question) -> None:
36
36
  self.question: Question = question
37
- self.rawInput = question.rawData
37
+ self.rawData = question.rawData
38
38
  self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.question.id})
39
39
  self.logger.debug(
40
40
  "The following Data was provided: %s",
41
- self.rawInput,
41
+ self.rawData,
42
42
  )
43
43
 
44
44
  def hasPicture(self) -> bool:
45
45
  """Create a ``Picture`` object ``question``if the question needs a pic."""
46
46
  if hasattr(self, "picture") and self.question.picture.ready:
47
47
  return True
48
- picKey = self.rawInput.get(Tags.PICTURE)
48
+ picKey = self.rawData.get(Tags.PICTURE)
49
49
  f = self.settings.get(Tags.PICTUREFOLDER)
50
50
  svgFolder = (f / self.question.katName).resolve()
51
51
  if not hasattr(self.question, "picture"):
@@ -53,7 +53,7 @@ class QuestionParser:
53
53
  picKey,
54
54
  svgFolder,
55
55
  self.question.id,
56
- width=self.rawInput.get(Tags.PICTUREWIDTH),
56
+ width=self.rawData.get(Tags.PICTUREWIDTH),
57
57
  )
58
58
  return bool(self.question.picture.ready)
59
59
 
@@ -63,7 +63,7 @@ class QuestionParser:
63
63
  ET.SubElement(
64
64
  ET.SubElement(textHTMLroot, "p"), "b"
65
65
  ).text = f"ID {self.question.id}"
66
- text = self.rawInput[Tags.TEXT]
66
+ text = self.rawData[Tags.TEXT]
67
67
  for t in text:
68
68
  par = TextElements.PLEFT.create()
69
69
  par.text = t
@@ -83,7 +83,7 @@ class QuestionParser:
83
83
  It uses the data from ``self.rawInput`` if ``text`` is type``DFIndex``
84
84
  Otherwise the value of ``text`` will be inserted.
85
85
  """
86
- t = self.rawInput.get(text) if isinstance(text, Tags) else text
86
+ t = self.rawData.get(text) if isinstance(text, Tags) else text
87
87
  if txtEle is False:
88
88
  self.tmpEle.append(eth.getElement(eleName, t, **attribs))
89
89
  elif txtEle is True:
@@ -118,8 +118,8 @@ class QuestionParser:
118
118
 
119
119
  self.htmlRoot = ET.Element("div")
120
120
  self.htmlRoot.append(self.getMainTextElement())
121
- if Tags.BPOINTS in self.rawInput:
122
- bps: list[str] = self.rawInput[Tags.BPOINTS]
121
+ if Tags.BPOINTS in self.rawData:
122
+ bps: list[str] = self.rawData[Tags.BPOINTS]
123
123
  try:
124
124
  bullets: BulletList = BulletList(bps, self.question.id)
125
125
  except IndexError:
@@ -135,7 +135,7 @@ class QuestionParser:
135
135
  if self.hasPicture():
136
136
  self.htmlRoot.append(self.question.picture.htmlTag)
137
137
  textRootElem.append(self.question.picture.element)
138
- if Tags.MEDIACALL in self.rawInput:
138
+ if Tags.MEDIACALL in self.rawData:
139
139
  self.insertScriptedMedia()
140
140
  ansList = self._parseAnswers()
141
141
  if ansList is not None:
@@ -163,7 +163,7 @@ class QuestionParser:
163
163
  span = feedBElements[feedback] if style is None else style.create()
164
164
  if text is None:
165
165
  self.logger.error("Giving a feedback without providing text is nonsens")
166
- text = self.rawInput.get(Tags.GENERALFB)
166
+ text = self.rawData.get(Tags.GENERALFB)
167
167
  ele = ET.Element(feedback, format="html")
168
168
  par = TextElements.PLEFT.create()
169
169
  span.text = text
@@ -173,13 +173,13 @@ class QuestionParser:
173
173
 
174
174
  def insertScriptedMedia(self) -> None:
175
175
  """Load the scripts, insert the div and call a Function."""
176
- for script in self.rawInput.get(Tags.MEDIASCRIPTS):
176
+ for script in self.rawData.get(Tags.MEDIASCRIPTS):
177
177
  ET.SubElement(
178
178
  self.htmlRoot, "script", type="text/javascript", src=script
179
179
  ).text = ""
180
180
  divId = f"scriptedMedia-{self.question.id}"
181
181
  ET.SubElement(self.htmlRoot, "div", id=divId).text = ""
182
- scriptCall = MediaCall(self.rawInput.get(Tags.MEDIACALL), divId=divId)
182
+ scriptCall = MediaCall(self.rawData.get(Tags.MEDIACALL), divId=divId)
183
183
  self.htmlRoot.append(scriptCall.element)
184
184
  if isinstance(self.question, ParametricQuestion):
185
185
  self.question.updateQue.append(scriptCall)
@@ -209,7 +209,7 @@ class QuestionParser:
209
209
  format=format,
210
210
  )
211
211
  if feedback is None:
212
- feedback = self.rawInput.get(Tags.TRUEFB)
212
+ feedback = self.rawData.get(Tags.TRUEFB)
213
213
  ansEle.append(
214
214
  self.getFeedBEle(
215
215
  feedback=XMLTags.ANSFEEDBACK,
@@ -217,6 +217,6 @@ class QuestionParser:
217
217
  style=TextElements.SPANGREEN,
218
218
  ),
219
219
  )
220
- absTolerance = round(result * self.rawInput.get(Tags.TOLERANCE), 4)
220
+ absTolerance = round(result * self.rawData.get(Tags.TOLERANCE), 4)
221
221
  ansEle.append(eth.getElement(XMLTags.TOLERANCE, text=str(absTolerance)))
222
222
  return ansEle
@@ -60,6 +60,8 @@ class QuestionData(dict):
60
60
  Tags.QUESTIONPART,
61
61
  Tags.TEXT,
62
62
  Tags.MEDIASCRIPTS,
63
+ Tags.TRUEANSFB,
64
+ Tags.FALSEANSFB,
63
65
  ],
64
66
  default: object = None,
65
67
  ) -> list: ...
@@ -118,6 +120,7 @@ class Question:
118
120
  mandatoryTags: ClassVar[dict[Tags, type | UnionType]] = {
119
121
  Tags.TEXT: str,
120
122
  Tags.NAME: str,
123
+ Tags.NUMBER: int,
121
124
  Tags.TYPE: str,
122
125
  }
123
126
  optionalTags: ClassVar[dict[Tags, type | UnionType]] = {
@@ -1,103 +1,12 @@
1
1
  """Settings module provides the adjusted subclass of ``PySide6.QtCore.QSettings``."""
2
2
 
3
3
  import logging
4
- from enum import StrEnum
5
4
  from pathlib import Path
6
5
  from typing import ClassVar, Literal, overload
7
6
 
8
- logger = logging.getLogger(__name__)
9
-
10
-
11
- class Tags(StrEnum):
12
- """Tags and Settings Keys are needed to always acess the correct Value.
13
-
14
- The Tags can be used to acess the settings or the QuestionData respectively.
15
- As the QSettings settings are accesed via strings, which could easily gotten wrong.
16
- Further, this Enum defines, which type a setting has to be.
17
- """
18
-
19
- def __new__(
20
- cls,
21
- key: str,
22
- typ: type,
23
- default: str | float | Path | bool | None,
24
- place: str = "project",
25
- ) -> object:
26
- """Define new settings class."""
27
- obj = str.__new__(cls, key)
28
- obj._value_ = key
29
- obj._typ_ = typ
30
- obj._default_ = default
31
- obj._place_ = place
32
- return obj
33
-
34
- def __init__(
35
- self,
36
- _,
37
- typ: type,
38
- default: str | float | Path | None,
39
- place: str = "project",
40
- ) -> None:
41
- self._typ_: type = typ
42
- self._place_: str = place
43
- self._default_ = default
44
- self._full_ = f"{self._place_}/{self._value_}"
7
+ from excel2moodle.core.globals import Tags
45
8
 
46
- @property
47
- def default(self) -> str | int | float | Path | bool | None:
48
- """Get default value for the key."""
49
- return self._default_
50
-
51
- @property
52
- def place(self) -> str:
53
- return self._place_
54
-
55
- @property
56
- def full(self) -> str:
57
- return self._full_
58
-
59
- def typ(self) -> type:
60
- """Get type of the keys data."""
61
- return self._typ_
62
-
63
- QUESTIONVARIANT = "defaultquestionvariant", int, 1, "testgen"
64
- INCLUDEINCATS = "includecats", bool, False, "testgen"
65
- GENEXPORTREPORT = "exportreport", bool, False, "testgen"
66
- TOLERANCE = "tolerance", float, 0.01, "parser/nf"
67
- PICTUREFOLDER = "pictureFolder", Path, None, "core"
68
- PICTURESUBFOLDER = "imgfolder", str, "Abbildungen", "project"
69
- SPREADSHEETPATH = "spreadsheetFolder", Path, None, "core"
70
- LOGLEVEL = "loglevel", str, "INFO", "core"
71
- LOGFILE = "logfile", str, "excel2moodleLogFile.log", "core"
72
- CATEGORIESSHEET = "categoriessheet", str, "Kategorien", "core"
73
-
74
- IMPORTMODULE = "importmodule", str, None
75
- TEXT = "text", list, None
76
- BPOINTS = "bulletpoint", list, None
77
- TRUE = "true", list, None
78
- FALSE = "false", list, None
79
- TYPE = "type", str, None
80
- NAME = "name", str, None
81
- RESULT = "result", float, None
82
- EQUATION = "formula", str, None
83
- PICTURE = "picture", str, None
84
- NUMBER = "number", int, None
85
- ANSTYPE = "answertype", str, None
86
- QUESTIONPART = "part", list, None
87
- PARTTYPE = "parttype", str, None
88
- POINTS = "points", float, 1.0
89
- PICTUREWIDTH = "imgwidth", int, 500
90
- ANSPICWIDTH = "answerimgwidth", int, 120
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."
98
-
99
- MEDIASCRIPTS = "mediascripts", list, None
100
- MEDIACALL = "parametricmedia", str, None
9
+ logger = logging.getLogger(__name__)
101
10
 
102
11
 
103
12
  class Settings:
@@ -1,4 +1,4 @@
1
- """Implementation of tde cloze question type.
1
+ """Implementation of the 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.
@@ -68,7 +68,7 @@ class ClozePart:
68
68
  self.logger.debug("MC Answer Part already up to date.")
69
69
  return
70
70
  if self.typ == "NFM":
71
- result = self.result.getResult(variant)
71
+ result = self.result.getResult(number=variant, equation=self.num)
72
72
  self._element.text = self.getNumericAnsStr(
73
73
  self.question.rawData,
74
74
  result,
@@ -263,11 +263,11 @@ class ClozeQuestionParser(NFMQuestionParser):
263
263
 
264
264
  def _setupParts(self) -> None:
265
265
  parts: dict[int, ClozePart] = {}
266
- for key in self.rawInput:
266
+ for key in self.rawData:
267
267
  if key.startswith(Tags.QUESTIONPART):
268
268
  partNumber = self.getPartNumber(key)
269
269
  parts[partNumber] = ClozePart(
270
- self.question, self.rawInput[key], partNumber
270
+ self.question, self.rawData[key], partNumber
271
271
  )
272
272
  partsNum = len(parts)
273
273
  equations: dict[int, str] = self._getPartValues(Tags.RESULT)
@@ -310,7 +310,7 @@ class ClozeQuestionParser(NFMQuestionParser):
310
310
  msg = f"Unclear Parts are defined. Either define `true:{num}` and `false:{num}` or `result:{num}` "
311
311
  raise QNotParsedException(msg, self.question.id)
312
312
  if len(points) == 0:
313
- pts = round(self.rawInput.get(Tags.POINTS) / partsNum, 3)
313
+ pts = round(self.rawData.get(Tags.POINTS) / partsNum, 3)
314
314
  point = self._roundClozePartPoints(pts)
315
315
  for part in parts.values():
316
316
  part.points = point
@@ -325,7 +325,7 @@ class ClozeQuestionParser(NFMQuestionParser):
325
325
  def _roundClozePartPoints(self, points: float | None = None) -> int:
326
326
  """Get the integer points for the cloze part."""
327
327
  if points is None:
328
- points = self.rawInput.get(Tags.POINTS)
328
+ points = self.rawData.get(Tags.POINTS)
329
329
  corrPoints: int = round(points)
330
330
  if not math.isclose(corrPoints, points):
331
331
  self.logger.warning(
@@ -347,8 +347,8 @@ class ClozeQuestionParser(NFMQuestionParser):
347
347
  ) -> dict[int, list[str]]: ...
348
348
  def _getPartValues(self, Tag):
349
349
  tagValues: dict = {
350
- self.getPartNumber(key): self.rawInput[key]
351
- for key in self.rawInput
350
+ self.getPartNumber(key): self.rawData[key]
351
+ for key in self.rawData
352
352
  if key.startswith(Tag)
353
353
  }
354
354
  return tagValues
@@ -363,7 +363,7 @@ class ClozeQuestionParser(NFMQuestionParser):
363
363
  if part.typ == "NFM":
364
364
  result = self.question.parametrics.getResult(1, partNum)
365
365
  ansStr = ClozePart.getNumericAnsStr(
366
- self.rawInput,
366
+ self.rawData,
367
367
  result=result,
368
368
  points=part.points,
369
369
  )
@@ -28,8 +28,8 @@ class MCQuestion(Question):
28
28
  "showstandardinstruction": "0",
29
29
  "shownumcorrect": "",
30
30
  }
31
- mcOpt: ClassVar[dict[Tags, type | UnionType]] = {}
32
- mcMand: ClassVar[dict[Tags, type | UnionType]] = {
31
+ optionalTags: ClassVar[dict[Tags, type | UnionType]] = {}
32
+ mandatoryTags: ClassVar[dict[Tags, type | UnionType]] = {
33
33
  Tags.TRUE: str,
34
34
  Tags.FALSE: str,
35
35
  Tags.ANSTYPE: str,
@@ -52,11 +52,21 @@ class MCQuestionParser(QuestionParser):
52
52
 
53
53
  def getAnsElementsList(
54
54
  self,
55
- answerList: list,
55
+ answerList: list[str],
56
+ feedbackList: list[str],
56
57
  fraction: float = 50,
57
58
  format="html",
58
59
  ) -> list[ET.Element]:
60
+ """Take the answer Strings and format them each into a xml-tree.
61
+
62
+ If a feedbackList is given it is used. Otherwise the standard feedbacks are used.
63
+ """
59
64
  elementList: list[ET.Element] = []
65
+ if feedbackList is None:
66
+ if fraction > 0:
67
+ feedbackList = [self.rawData.get(Tags.FALSEFB) for _ in answerList]
68
+ else:
69
+ feedbackList = [self.rawData.get(Tags.TRUEFB) for _ in answerList]
60
70
  for i, ans in enumerate(answerList):
61
71
  p = TextElements.PLEFT.create()
62
72
  if self.answerType == "picture":
@@ -72,7 +82,7 @@ class MCQuestionParser(QuestionParser):
72
82
  elementList[-1].append(
73
83
  self.getFeedBEle(
74
84
  XMLTags.ANSFEEDBACK,
75
- text=self.rawInput.get(Tags.FALSEFB),
85
+ text=feedbackList[i],
76
86
  style=TextElements.SPANRED,
77
87
  ),
78
88
  )
@@ -82,7 +92,7 @@ class MCQuestionParser(QuestionParser):
82
92
  elementList[-1].append(
83
93
  self.getFeedBEle(
84
94
  XMLTags.ANSFEEDBACK,
85
- text=self.rawInput.get(Tags.TRUEFB),
95
+ text=feedbackList[i],
86
96
  style=TextElements.SPANGREEN,
87
97
  ),
88
98
  )
@@ -91,21 +101,21 @@ class MCQuestionParser(QuestionParser):
91
101
  return elementList
92
102
 
93
103
  def _parseAnswers(self) -> list[ET.Element]:
94
- self.answerType = self.rawInput.get(Tags.ANSTYPE)
104
+ self.answerType = self.rawData.get(Tags.ANSTYPE)
95
105
  if self.answerType not in self.question.AnsStyles:
96
106
  msg = f"The Answer style: {self.answerType} is not supported"
97
107
  raise InvalidFieldException(msg, self.question.id, Tags.ANSTYPE)
98
108
  if self.answerType == "picture":
99
109
  f = self.settings.get(Tags.PICTUREFOLDER)
100
110
  imgFolder = (f / self.question.katName).resolve()
101
- width = self.rawInput.get(Tags.ANSPICWIDTH)
111
+ width = self.rawData.get(Tags.ANSPICWIDTH)
102
112
  self.trueImgs: list[Picture] = [
103
113
  Picture(t, imgFolder, self.question.id, width=width)
104
- for t in self.rawInput.get(Tags.TRUE)
114
+ for t in self.rawData.get(Tags.TRUE)
105
115
  ]
106
116
  self.falseImgs: list[Picture] = [
107
117
  Picture(t, imgFolder, self.question.id, width=width)
108
- for t in self.rawInput.get(Tags.FALSE)
118
+ for t in self.rawData.get(Tags.FALSE)
109
119
  ]
110
120
  trueAnsList: list[str] = [pic.htmlTag for pic in self.trueImgs if pic.ready]
111
121
  falseAList: list[str] = [pic.htmlTag for pic in self.falseImgs if pic.ready]
@@ -114,19 +124,47 @@ class MCQuestionParser(QuestionParser):
114
124
  raise QNotParsedException(msg, self.question.id)
115
125
  else:
116
126
  trueAnsList: list[str] = stringHelpers.texWrapper(
117
- self.rawInput.get(Tags.TRUE), style=self.answerType
127
+ self.rawData.get(Tags.TRUE), style=self.answerType
118
128
  )
119
129
  self.logger.debug(f"got the following true answers \n {trueAnsList=}")
120
130
  falseAList: list[str] = stringHelpers.texWrapper(
121
- self.rawInput.get(Tags.FALSE), style=self.answerType
131
+ self.rawData.get(Tags.FALSE), style=self.answerType
122
132
  )
123
133
  self.logger.debug(f"got the following false answers \n {falseAList=}")
134
+ trueFbs = self.rawData.get(Tags.TRUEANSFB)
135
+ falseFbs = self.rawData.get(Tags.FALSEANSFB)
136
+ if trueFbs is None:
137
+ trueFbs = [self.rawData.get(Tags.FALSEFB) for _ in trueAnsList]
138
+ if falseFbs is None:
139
+ falseFbs = [self.rawData.get(Tags.TRUEFB) for _ in falseAList]
140
+ if len(trueFbs) < len(trueAnsList):
141
+ self.logger.warning(
142
+ "There are less true-feedbacks than true-answers given. Using fallback feedback"
143
+ )
144
+ delta = len(trueAnsList) - len(trueFbs)
145
+ while delta > 0:
146
+ trueFbs.append(self.rawData.get(Tags.TRUEFB))
147
+ delta -= 1
148
+ if len(falseFbs) < len(falseAList):
149
+ self.logger.warning(
150
+ "There are less false-feedbacks than false-answers given. Using fallback feedback"
151
+ )
152
+ delta = len(falseAList) - len(falseFbs)
153
+ while delta > 0:
154
+ falseFbs.append(self.rawData.get(Tags.TRUEFB))
155
+ delta -= 1
124
156
  truefrac = 1 / len(trueAnsList) * 100
125
157
  falsefrac = 1 / len(falseAList) * (-100)
126
158
  self.tmpEle.find(XMLTags.PENALTY).text = str(round(truefrac / 100, 4))
127
- ansList = self.getAnsElementsList(trueAnsList, fraction=round(truefrac, 4))
159
+ ansList = self.getAnsElementsList(
160
+ answerList=trueAnsList, feedbackList=trueFbs, fraction=round(truefrac, 4)
161
+ )
128
162
  ansList.extend(
129
- self.getAnsElementsList(falseAList, fraction=round(falsefrac, 4)),
163
+ self.getAnsElementsList(
164
+ answerList=falseAList,
165
+ feedbackList=falseFbs,
166
+ fraction=round(falsefrac, 4),
167
+ ),
130
168
  )
131
169
  return ansList
132
170
 
@@ -138,5 +176,5 @@ class MCQuestionParser(QuestionParser):
138
176
  XMLTags.INCORFEEDB: Tags.FALSEFB,
139
177
  }
140
178
  for feedb, tag in feedBacks.items():
141
- self.tmpEle.append(self.getFeedBEle(feedb, text=self.rawInput.get(tag)))
179
+ self.tmpEle.append(self.getFeedBEle(feedb, text=self.rawData.get(tag)))
142
180
  self._finalizeParsing()
@@ -17,10 +17,10 @@ class NFQuestion(Question):
17
17
  def __init__(self, *args, **kwargs) -> None:
18
18
  super().__init__(*args, **kwargs)
19
19
 
20
- nfOpt: ClassVar[dict[Tags, type | UnionType]] = {
20
+ optionalTags: ClassVar[dict[Tags, type | UnionType]] = {
21
21
  Tags.BPOINTS: str,
22
22
  }
23
- nfMand: ClassVar[dict[Tags, type | UnionType]] = {
23
+ mandatoryTags: ClassVar[dict[Tags, type | UnionType]] = {
24
24
  Tags.RESULT: float | int,
25
25
  }
26
26
 
@@ -37,13 +37,13 @@ class NFQuestionParser(QuestionParser):
37
37
  super().setup(question)
38
38
 
39
39
  def _parseAnswers(self) -> list[ET.Element]:
40
- result: float = self.rawInput.get(Tags.RESULT)
40
+ result: float = self.rawData.get(Tags.RESULT)
41
41
  ansEle: list[ET.Element] = []
42
42
  ansEle.append(self.getNumericAnsElement(result=result))
43
43
  return ansEle
44
44
 
45
45
  def _finalizeParsing(self) -> None:
46
46
  self.tmpEle.append(
47
- self.getFeedBEle(XMLTags.GENFEEDB, text=self.rawInput.get(Tags.GENERALFB))
47
+ self.getFeedBEle(XMLTags.GENFEEDB, text=self.rawData.get(Tags.GENERALFB))
48
48
  )
49
49
  return super()._finalizeParsing()
@@ -14,7 +14,7 @@ from excel2moodle.core.question import ParametricQuestion, Parametrics
14
14
 
15
15
 
16
16
  class NFMQuestion(ParametricQuestion):
17
- nfmMand: ClassVar[dict[Tags, type | UnionType]] = {
17
+ mandatoryTags: ClassVar[dict[Tags, type | UnionType]] = {
18
18
  Tags.RESULT: str,
19
19
  Tags.BPOINTS: str,
20
20
  }
@@ -58,20 +58,20 @@ class NFMQuestionParser(QuestionParser):
58
58
  def _parseAnswers(self) -> list[ET.Element]:
59
59
  variables = self.question.bulletList.getVariablesDict(self.question)
60
60
  self.question.parametrics = Parametrics(
61
- self.rawInput.get(Tags.EQUATION),
62
- self.rawInput.get(Tags.FIRSTRESULT),
61
+ self.rawData.get(Tags.EQUATION),
62
+ self.rawData.get(Tags.FIRSTRESULT),
63
63
  self.question.id,
64
64
  )
65
65
  self.question.parametrics.variables = variables
66
66
  self.question.answerElement = self.getNumericAnsElement()
67
67
  self.question.answerElementWrongSign = self.getNumericAnsElement(
68
- fraction=self.rawInput.get(Tags.WRONGSIGNPERCENT),
69
- feedback=self.rawInput.get(Tags.WRONGSIGNFB),
68
+ fraction=self.rawData.get(Tags.WRONGSIGNPERCENT),
69
+ feedback=self.rawData.get(Tags.WRONGSIGNFB),
70
70
  )
71
71
  return [self.question.answerElement, self.question.answerElementWrongSign]
72
72
 
73
73
  def _finalizeParsing(self) -> None:
74
74
  self.tmpEle.append(
75
- self.getFeedBEle(XMLTags.GENFEEDB, text=self.rawInput.get(Tags.GENERALFB))
75
+ self.getFeedBEle(XMLTags.GENFEEDB, text=self.rawData.get(Tags.GENERALFB))
76
76
  )
77
77
  return super()._finalizeParsing()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: excel2moodle
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: A package for converting questions from a spreadsheet, to valid moodle-xml
5
5
  Author: Jakob Bosse
6
6
  License-Expression: GPL-3.0-or-later
@@ -90,6 +90,30 @@ If You want to support my work as well, you can by me a [coffee](https://ko-fi.c
90
90
 
91
91
  # Changelogs
92
92
 
93
+ ## 0.7.2 (2025-10-11)
94
+ small important bugfixes
95
+
96
+ ### improvement (2 changes)
97
+
98
+ - [BulletPoints are decoded using regex to allow multi word names](https://gitlab.com/jbosse3/excel2moodle/-/commit/7e32e9817323054d84a0dfb9a0a241c702fd096d)
99
+ - [Restructured globals, renamed rawInput to rawData](https://gitlab.com/jbosse3/excel2moodle/-/commit/effd9c3cd196b36d49204fe715acc1ffb124549c)
100
+
101
+ ### bugfix (2 changes)
102
+
103
+ - [Added the number do the mandatory tags because it is](https://gitlab.com/jbosse3/excel2moodle/-/commit/56e9b69d71504dffe7c235d69ff44dec6931db28)
104
+ - [fixed assigning the first result to all clozes](https://gitlab.com/jbosse3/excel2moodle/-/commit/09a281c253502adc23442892be03aac36e6ea720)
105
+
106
+ ## 0.7.1 (2025-10-04)
107
+ feedbacking improved
108
+
109
+ ### documentation (1 change)
110
+
111
+ - [documentation improvement](https://gitlab.com/jbosse3/excel2moodle/-/commit/1a1110d05b49175e049a9ca18a027216a765e277)
112
+
113
+ ### feature (1 change)
114
+
115
+ - [Added MC answer feedback support](https://gitlab.com/jbosse3/excel2moodle/-/commit/4f5fe550786cf29839ba54fbdfedbf03c72d3009)
116
+
93
117
  ## 0.7.0 (2025-09-30)
94
118
  Rework of the equation checker done!
95
119
 
@@ -2,15 +2,15 @@ excel2moodle/__init__.py,sha256=W05Gsm3IOcxJnp4C-TPvxRiO3NR2L9g8PSIHDoRJua0,1893
2
2
  excel2moodle/__main__.py,sha256=B55ZK25z-HzIIox2xLYkJXMUwYzITPKGCi9fELMFGaM,1877
3
3
  excel2moodle/logger.py,sha256=fq8ZOkCI1wj38v8IyrZsUlpt16onlSH_phqbVvYUwBQ,3725
4
4
  excel2moodle/core/__init__.py,sha256=87BwhtZse72Tk17Ib-V9X2k9wkhmtVnEj2ZmJ9JBAnI,63
5
- excel2moodle/core/bullets.py,sha256=TiRf2EfhsryE-KBfvo43dMtsMWuZi9L9H7TuwXAZ1rg,3550
5
+ excel2moodle/core/bullets.py,sha256=Gpe6vO-XZqqW_vknlvinbbrL_YtsB4ZoYVtfBv98564,4251
6
6
  excel2moodle/core/category.py,sha256=fOMj2ynoAy6tXwmFhJ9uST9BQHiRJeU2BrkK1r57ek4,2897
7
7
  excel2moodle/core/dataStructure.py,sha256=f3aqSPSIQxspYf1FmhFnlr4H1tc1gpVG_DQBdD0bZQk,19858
8
8
  excel2moodle/core/etHelpers.py,sha256=LzimWGuX6RH2TbfEnWUoAXT2Tr0z6P7bCANjxuANSX0,1667
9
9
  excel2moodle/core/exceptions.py,sha256=9xfsaIcm6Yej6QAZga0d3DK3jLQejdfgJARuAaG-uZY,739
10
- excel2moodle/core/globals.py,sha256=gvkl8Obq4XBW40B1L68Ewg06sonK27l-KIiodwFv8ic,2393
11
- excel2moodle/core/parser.py,sha256=hjbA0i7N1oHoJHhOmvEtVl4Ryaqd0eqUS26bXS47CBo,8467
12
- excel2moodle/core/question.py,sha256=bRQhWyJPwvNMOABlT-WCngTx-Bt8AuOoQXgyPTQh_aQ,14528
13
- excel2moodle/core/settings.py,sha256=_H3TJ67-4Q0hFi8g3JWFILJ6q3mfcEsBRaV74jhbko8,6531
10
+ excel2moodle/core/globals.py,sha256=Ua0ptvVfga-WSimEaf3YI3-9fMcOE81MuThRIxVcL0U,4906
11
+ excel2moodle/core/parser.py,sha256=l7fsDTPskbTZurdpg-rMqfDXZwlZ3CVfJTbdmOeNg2o,8453
12
+ excel2moodle/core/question.py,sha256=hwQ0_p_NKWn6OPi5kPO_y-2Hl0F3CDON7J87L3GjUzU,14611
13
+ excel2moodle/core/settings.py,sha256=A_kdbvpLPWLQGKCytNrK_KEjy3HfGnQU4Fz-a39WzHs,3362
14
14
  excel2moodle/core/stringHelpers.py,sha256=OzFZ6Eu3PeBLKb61K-aeVfUZmVuBerr9KfyOsuNRd7Y,2403
15
15
  excel2moodle/core/validator.py,sha256=6nZIyTwcXPT2jgi31QrBGur3Cq7A3Q9btLp5ALFEsKw,4998
16
16
  excel2moodle/extra/__init__.py,sha256=PM-id60HD21A3IcGC_fCYFihS8osBGZMIJCcN-ZRsIM,293
@@ -19,10 +19,10 @@ excel2moodle/extra/scriptCaller.py,sha256=nJsUxz0EzoChobCJbAbfS8NSMghdUFV6oEPdO6
19
19
  excel2moodle/extra/updateQuery.py,sha256=kD_L23Qea9Cx4zUwfQVNJXXFbybd9cwE8sSbZrz7VF8,1554
20
20
  excel2moodle/extra/variableGenerator.py,sha256=jcXbyA1B3-uZHsovU9QhZ879FoemeHqAe_BxPT4ZXoY,11484
21
21
  excel2moodle/question_types/__init__.py,sha256=81mss0g7SVtnlb-WkydE28G_dEAAf6oT1uB8lpK2-II,1041
22
- excel2moodle/question_types/cloze.py,sha256=WGeA37Dq405Zda_WNU2SrAhfW9DNjkdnLBfM-JFM4lw,14656
23
- excel2moodle/question_types/mc.py,sha256=nx6PsbfLLH_4H5eCSjGcfgEC6EEVgseI7xy15jg5JmA,5482
24
- excel2moodle/question_types/nf.py,sha256=HAolGy13-FbLVJskAUXCFy76NJu91IG9wtq6OI05Igw,1374
25
- excel2moodle/question_types/nfm.py,sha256=D5-aE4C7TAuwHFidXR15DLWNZ4JT-HVbPXI0CzGWOS0,3013
22
+ excel2moodle/question_types/cloze.py,sha256=vcG1XCVsnncrb8Sm6O_zNMuTdYtE_5boWzgpkU1r1tA,14675
23
+ excel2moodle/question_types/mc.py,sha256=pXhVw79GxRVJOeKVp7-p0NdzD1t92qPDnTkB0hrOs_I,7102
24
+ excel2moodle/question_types/nf.py,sha256=Urw970Y-YhISSakonBWRiQYheHgASkJkHLhAtwWrS9M,1386
25
+ excel2moodle/question_types/nfm.py,sha256=hRpVlaGdW3VaHITMFiVhMIjGwtBnnI17PX1d10JL3MI,3014
26
26
  excel2moodle/ui/UI_equationChecker.py,sha256=jA5gKemrgeertbp2iip-icQCRFKnV-dR-f0eODBBAp0,8910
27
27
  excel2moodle/ui/UI_exportSettingsDialog.py,sha256=I0Vqw2TCWoUhDKxTgLoGaAo4_L77vfN8G7_zi7b_5lY,8254
28
28
  excel2moodle/ui/UI_mainWindow.py,sha256=9w8bRgOrVEX7BRGQvMuVhPCiSOsXYkMb4rxLDeRErII,21544
@@ -33,9 +33,9 @@ excel2moodle/ui/__init__.py,sha256=4EdGtpzwH3rgw4xW9E5x9kdPQYwKbo9rehHRZTNxCrQ,4
33
33
  excel2moodle/ui/appUi.py,sha256=jlurK6sb_CVEV1vNAC_mL8lmX9H9Ex-QZ3BQXgAisJM,14887
34
34
  excel2moodle/ui/dialogs.py,sha256=du6v17lh6LhgDDK0QltPzD-z8wUn3aD4QzaAQBmiTBQ,7314
35
35
  excel2moodle/ui/treewidget.py,sha256=3hZRLlrhp4FMXFyNY0LGDy7k1RSuKH87QyqB1N4qOqg,2335
36
- excel2moodle-0.7.0.dist-info/licenses/LICENSE,sha256=ywQqe6Sitymkf2lV2NRcx_aGsaC-KbSl_EfEsRXmNRw,35135
37
- excel2moodle-0.7.0.dist-info/METADATA,sha256=f2ZbXDgR3FOXRwhJ7I2eUgCzsIaEVfuJdBkcYuAF_z0,12716
38
- excel2moodle-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
39
- excel2moodle-0.7.0.dist-info/entry_points.txt,sha256=myfMLDThuGgWHMJDPPfILiZqo_7D3fhmDdJGqWOAjPw,60
40
- excel2moodle-0.7.0.dist-info/top_level.txt,sha256=5V1xRUQ9o7UmOCmNoWCZPAuy5nXp3Qbzyqch8fUGT_c,13
41
- excel2moodle-0.7.0.dist-info/RECORD,,
36
+ excel2moodle-0.7.2.dist-info/licenses/LICENSE,sha256=ywQqe6Sitymkf2lV2NRcx_aGsaC-KbSl_EfEsRXmNRw,35135
37
+ excel2moodle-0.7.2.dist-info/METADATA,sha256=wTdBTBoHroAy5ARxV1Uh3FspriO2TE_A_UzmtXmpuJY,13761
38
+ excel2moodle-0.7.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
39
+ excel2moodle-0.7.2.dist-info/entry_points.txt,sha256=myfMLDThuGgWHMJDPPfILiZqo_7D3fhmDdJGqWOAjPw,60
40
+ excel2moodle-0.7.2.dist-info/top_level.txt,sha256=5V1xRUQ9o7UmOCmNoWCZPAuy5nXp3Qbzyqch8fUGT_c,13
41
+ excel2moodle-0.7.2.dist-info/RECORD,,