excel2moodle 0.4.1__py3-none-any.whl → 0.4.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.
Files changed (34) hide show
  1. excel2moodle/__init__.py +0 -7
  2. excel2moodle/__main__.py +2 -2
  3. excel2moodle/core/__init__.py +0 -10
  4. excel2moodle/core/category.py +4 -3
  5. excel2moodle/core/dataStructure.py +116 -61
  6. excel2moodle/core/etHelpers.py +2 -2
  7. excel2moodle/core/exceptions.py +2 -2
  8. excel2moodle/core/globals.py +10 -27
  9. excel2moodle/core/parser.py +24 -30
  10. excel2moodle/core/question.py +147 -63
  11. excel2moodle/core/settings.py +107 -111
  12. excel2moodle/core/validator.py +36 -55
  13. excel2moodle/logger.py +7 -4
  14. excel2moodle/question_types/__init__.py +2 -0
  15. excel2moodle/question_types/cloze.py +207 -0
  16. excel2moodle/question_types/mc.py +26 -16
  17. excel2moodle/question_types/nf.py +17 -3
  18. excel2moodle/question_types/nfm.py +60 -17
  19. excel2moodle/ui/{windowEquationChecker.py → UI_equationChecker.py} +98 -78
  20. excel2moodle/ui/{exportSettingsDialog.py → UI_exportSettingsDialog.py} +55 -4
  21. excel2moodle/ui/{windowMain.py → UI_mainWindow.py} +32 -39
  22. excel2moodle/ui/appUi.py +66 -86
  23. excel2moodle/ui/dialogs.py +40 -2
  24. excel2moodle/ui/equationChecker.py +70 -0
  25. excel2moodle/ui/treewidget.py +4 -4
  26. {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.3.dist-info}/METADATA +2 -3
  27. excel2moodle-0.4.3.dist-info/RECORD +38 -0
  28. {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.3.dist-info}/entry_points.txt +0 -3
  29. excel2moodle/ui/questionPreviewDialog.py +0 -115
  30. excel2moodle-0.4.1.dist-info/RECORD +0 -37
  31. /excel2moodle/ui/{variantDialog.py → UI_variantDialog.py} +0 -0
  32. {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.3.dist-info}/WHEEL +0 -0
  33. {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.3.dist-info}/licenses/LICENSE +0 -0
  34. {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.3.dist-info}/top_level.txt +0 -0
@@ -4,17 +4,16 @@ import re
4
4
  import lxml.etree as ET
5
5
 
6
6
  import excel2moodle.core.etHelpers as eth
7
- from excel2moodle.core import stringHelpers
8
7
  from excel2moodle.core.exceptions import QNotParsedException
9
8
  from excel2moodle.core.globals import (
10
- DFIndex,
9
+ Tags,
11
10
  TextElements,
12
11
  XMLTags,
13
12
  feedbackStr,
14
13
  feedBElements,
15
14
  )
16
15
  from excel2moodle.core.question import Picture, Question
17
- from excel2moodle.core.settings import Settings, SettingsKey
16
+ from excel2moodle.core.settings import Settings, Tags
18
17
  from excel2moodle.logger import LogAdapterQuestionID
19
18
 
20
19
  loggerObj = logging.getLogger(__name__)
@@ -47,8 +46,8 @@ class QuestionParser:
47
46
  """Create a ``Picture`` object ``question``if the question needs a pic."""
48
47
  if hasattr(self, "picture") and self.question.picture.ready:
49
48
  return True
50
- picKey = self.rawInput.get(DFIndex.PICTURE, False)
51
- f = self.settings.get(SettingsKey.PICTUREFOLDER)
49
+ picKey = self.rawInput.get(Tags.PICTURE)
50
+ f = self.settings.get(Tags.PICTUREFOLDER)
52
51
  svgFolder = (f / self.question.katName).resolve()
53
52
  if not hasattr(self.question, "picture"):
54
53
  self.question.picture = Picture(picKey, svgFolder, self.question.id)
@@ -57,7 +56,7 @@ class QuestionParser:
57
56
  def setMainText(self) -> None:
58
57
  paragraphs: list[ET._Element] = [TextElements.PLEFT.create()]
59
58
  ET.SubElement(paragraphs[0], "b").text = f"ID {self.question.id}"
60
- text = self.rawInput[DFIndex.TEXT]
59
+ text = self.rawInput[Tags.TEXT]
61
60
  for t in text:
62
61
  paragraphs.append(TextElements.PLEFT.create())
63
62
  paragraphs[-1].text = t
@@ -66,8 +65,8 @@ class QuestionParser:
66
65
 
67
66
  def setBPoints(self) -> None:
68
67
  """If there bulletPoints are set in the Spreadsheet it creates an unordered List-Element in ``Question.bulletList``."""
69
- if DFIndex.BPOINTS in self.rawInput:
70
- bps: str = self.rawInput[DFIndex.BPOINTS]
68
+ if Tags.BPOINTS in self.rawInput:
69
+ bps: list[str] = self.rawInput[Tags.BPOINTS]
71
70
  try:
72
71
  bulletList = self.formatBulletList(bps)
73
72
  except IndexError:
@@ -83,15 +82,14 @@ class QuestionParser:
83
82
  )
84
83
  self.question.bulletList = bulletList
85
84
 
86
- def formatBulletList(self, bps: str) -> ET.Element:
85
+ def formatBulletList(self, bps: list[str]) -> ET.Element:
87
86
  self.logger.debug("Formatting the bulletpoint list")
88
- li: list[str] = stringHelpers.getListFromStr(bps)
89
87
  name = []
90
88
  var = []
91
89
  quant = []
92
90
  unit = []
93
91
  unorderedList = TextElements.ULIST.create()
94
- for item in li:
92
+ for item in bps:
95
93
  sc_split = item.split()
96
94
  name.append(sc_split[0])
97
95
  var.append(sc_split[1])
@@ -118,7 +116,7 @@ class QuestionParser:
118
116
  def appendToTmpEle(
119
117
  self,
120
118
  eleName: str,
121
- text: str | DFIndex,
119
+ text: str | Tags,
122
120
  txtEle=False,
123
121
  **attribs,
124
122
  ) -> None:
@@ -127,7 +125,7 @@ class QuestionParser:
127
125
  It uses the data from ``self.rawInput`` if ``text`` is type``DFIndex``
128
126
  Otherwise the value of ``text`` will be inserted.
129
127
  """
130
- t = self.rawInput[text] if isinstance(text, DFIndex) else text
128
+ t = self.rawInput[text] if isinstance(text, Tags) else text
131
129
  if txtEle is False:
132
130
  self.tmpEle.append(eth.getElement(eleName, t, **attribs))
133
131
  elif txtEle is True:
@@ -135,6 +133,9 @@ class QuestionParser:
135
133
 
136
134
  def _appendStandardTags(self) -> None:
137
135
  """Append the elements defined in the ``cls.standardTags``."""
136
+ self.logger.debug(
137
+ "Appending the Standard Tags %s", type(self.question).standardTags.items()
138
+ )
138
139
  for k, v in type(self.question).standardTags.items():
139
140
  self.appendToTmpEle(k, text=v)
140
141
 
@@ -145,25 +146,27 @@ class QuestionParser:
145
146
  if no Exceptions are raised, ``self.tmpEle`` is passed to ``self.question.element``
146
147
  """
147
148
  self.logger.info("Starting to parse")
148
- self.tmpEle = ET.Element(XMLTags.QUESTION, type=self.question.moodleType)
149
- self.appendToTmpEle(XMLTags.NAME, text=DFIndex.NAME, txtEle=True)
149
+ self.tmpEle: ET.Elemnt = ET.Element(
150
+ XMLTags.QUESTION, type=self.question.moodleType
151
+ )
152
+ self.appendToTmpEle(XMLTags.NAME, text=Tags.NAME, txtEle=True)
150
153
  self.appendToTmpEle(XMLTags.ID, text=self.question.id)
151
154
  if self.hasPicture():
152
155
  self.tmpEle.append(self.question.picture.element)
153
156
  self.tmpEle.append(ET.Element(XMLTags.QTEXT, format="html"))
154
157
  self.appendToTmpEle(XMLTags.POINTS, text=str(self.question.points))
155
- self.appendToTmpEle(XMLTags.PENALTY, text="0.3333")
156
158
  self._appendStandardTags()
157
159
  for feedb in self.genFeedbacks:
158
160
  self.tmpEle.append(eth.getFeedBEle(feedb))
159
- ansList = self.setAnswers()
161
+ ansList = self._parseAnswers()
160
162
  self.setMainText()
161
163
  self.setBPoints()
162
164
  if ansList is not None:
163
165
  for ele in ansList:
164
166
  self.tmpEle.append(ele)
165
- self.logger.info("Sucessfully parsed")
166
167
  self.question.element = self.tmpEle
168
+ self.question.isParsed = True
169
+ self.logger.info("Sucessfully parsed")
167
170
 
168
171
  def getFeedBEle(
169
172
  self,
@@ -181,7 +184,7 @@ class QuestionParser:
181
184
  ele.append(eth.getCdatTxtElement(par))
182
185
  return ele
183
186
 
184
- def setAnswers(self) -> list[ET.Element] | None:
187
+ def _parseAnswers(self) -> list[ET.Element] | None:
185
188
  """Needs to be implemented in the type-specific subclasses."""
186
189
  return None
187
190
 
@@ -211,15 +214,6 @@ class QuestionParser:
211
214
  TextElements.SPANGREEN,
212
215
  ),
213
216
  )
214
- tolerance = float(self.rawInput.get(DFIndex.TOLERANCE, 0))
215
- if tolerance == 0 or tolerance >= 100:
216
- tolerance = self.settings.get(SettingsKey.TOLERANCE)
217
- self.logger.info(
218
- "Using default tolerance %s percent from settings",
219
- tolerance,
220
- )
221
- tolPercent = 100 * tolerance if tolerance < 1 else tolerance
222
- self.logger.debug("Using tolerance %s percent", tolPercent)
223
- relTolerance = abs(round(result * (tolerance / 100), 3))
224
- ansEle.append(eth.getElement(XMLTags.TOLERANCE, text=str(relTolerance)))
217
+ absTolerance = round(result * self.rawInput.get(Tags.TOLERANCE), 4)
218
+ ansEle.append(eth.getElement(XMLTags.TOLERANCE, text=str(absTolerance)))
225
219
  return ansEle
@@ -1,9 +1,10 @@
1
1
  import base64
2
2
  import logging
3
3
  import re
4
- import typing
5
4
  from pathlib import Path
6
5
  from re import Match
6
+ from types import UnionType
7
+ from typing import ClassVar, Literal, overload
7
8
 
8
9
  import lxml.etree as ET
9
10
 
@@ -11,74 +12,158 @@ from excel2moodle.core import etHelpers
11
12
  from excel2moodle.core.category import Category
12
13
  from excel2moodle.core.exceptions import QNotParsedException
13
14
  from excel2moodle.core.globals import (
14
- DFIndex,
15
+ QUESTION_TYPES,
16
+ Tags,
15
17
  TextElements,
16
18
  XMLTags,
17
- questionTypes,
18
19
  )
19
- from excel2moodle.core.settings import Settings, SettingsKey
20
+ from excel2moodle.core.settings import Settings, Tags
20
21
  from excel2moodle.logger import LogAdapterQuestionID
21
22
 
22
23
  loggerObj = logging.getLogger(__name__)
23
24
  settings = Settings()
24
25
 
25
26
 
27
+ class QuestionData(dict):
28
+ @overload
29
+ def get(
30
+ self,
31
+ key: Literal[Tags.NAME, Tags.ANSTYPE, Tags.PICTURE, Tags.EQUATION],
32
+ ) -> str: ...
33
+ @overload
34
+ def get(
35
+ self,
36
+ key: Literal[Tags.BPOINTS, Tags.TRUE, Tags.FALSE, Tags.QUESTIONPART, Tags.TEXT],
37
+ ) -> list: ...
38
+ @overload
39
+ def get(
40
+ self,
41
+ key: Literal[
42
+ Tags.VERSION,
43
+ Tags.NUMBER,
44
+ Tags.PICTUREWIDTH,
45
+ Tags.ANSPICWIDTH,
46
+ Tags.WRONGSIGNPERCENT,
47
+ ],
48
+ ) -> int: ...
49
+ @overload
50
+ def get(
51
+ self, key: Literal[Tags.PARTTYPE, Tags.TYPE]
52
+ ) -> Literal["MC", "NFM", "CLOZE"]: ...
53
+ @overload
54
+ def get(
55
+ self, key: Literal[Tags.TOLERANCE, Tags.POINTS, Tags.FIRSTRESULT]
56
+ ) -> float: ...
57
+ @overload
58
+ def get(self, key: Literal[Tags.RESULT]) -> float | str: ...
59
+
60
+ def get(self, key: Tags, default=None):
61
+ if key in self:
62
+ val = self[key]
63
+ try:
64
+ typed = key.typ()(val)
65
+ except ValueError:
66
+ return None
67
+ return typed
68
+ return settings.get(key)
69
+
70
+
26
71
  class Question:
27
- standardTags: typing.ClassVar[dict[str, str | float]] = {
72
+ standardTags: ClassVar[dict[str, str | float]] = {
28
73
  "hidden": 0,
74
+ "penalty": 0.33333,
75
+ }
76
+ mandatoryTags: ClassVar[dict[Tags, type | UnionType]] = {
77
+ Tags.TEXT: str,
78
+ Tags.NAME: str,
79
+ Tags.TYPE: str,
80
+ }
81
+ optionalTags: ClassVar[dict[Tags, type | UnionType]] = {
82
+ Tags.PICTURE: int | str,
29
83
  }
30
84
 
31
85
  def __init_subclass__(cls, **kwargs) -> None:
32
86
  super().__init_subclass__(**kwargs)
33
- subclassTags = getattr(cls, "standartTags", {})
34
- superclassTags = super(cls, cls).standardTags
35
- mergedTags = superclassTags.copy()
36
- mergedTags.update(subclassTags)
37
- cls.standardTags = mergedTags
87
+ dictionaries = ("standardTags", "mandatoryTags", "optionalTags")
88
+ for dic in dictionaries:
89
+ cls._mergeDicts(dic)
38
90
 
39
91
  @classmethod
40
- def addStandardTags(cls, key, value) -> None:
92
+ def _mergeDicts(cls, dictName) -> None:
93
+ superDict = getattr(super(cls, cls), dictName)
94
+ subDict = getattr(cls, dictName, {})
95
+ mergedDict = superDict.copy()
96
+ mergedDict.update(subDict)
97
+ setattr(cls, dictName, mergedDict)
98
+
99
+ @classmethod
100
+ def addStandardTag(cls, key, value) -> None:
41
101
  cls.standardTags[key] = value
42
102
 
43
103
  def __init__(
44
104
  self,
45
105
  category: Category,
46
- rawData: dict[str, float | str | int | list[str]],
106
+ rawData: QuestionData,
47
107
  parent=None,
48
108
  points: float = 0,
49
109
  ) -> None:
50
- self.rawData = rawData
110
+ self.rawData: QuestionData = rawData
51
111
  self.category = category
52
112
  self.katName = self.category.name
53
- self.name: str = self.rawData.get(DFIndex.NAME)
54
- self.number: int = self.rawData.get(DFIndex.NUMBER)
55
- self.parent = parent
56
- self.qtype: str = self.rawData.get(DFIndex.TYPE)
57
- self.moodleType = questionTypes[self.qtype]
113
+ self.moodleType = QUESTION_TYPES[self.qtype]
58
114
  self.points = points if points != 0 else self.category.points
59
- self.element: ET.Element | None = None
115
+ self.element: ET.Element = None
116
+ self.isParsed: bool = False
60
117
  self.picture: Picture
61
118
  self.id: str
62
119
  self.qtextParagraphs: list[ET.Element] = []
63
- self.bulletList: ET.Element | None = None
64
- self.answerVariants: list[ET.Element] = []
65
- self.variants: int | None = None
66
- self.variables: dict[str, list[float | int]] = {}
67
- self.setID()
120
+ self.bulletList: ET.Element = None
121
+ self._setID()
68
122
  self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.id})
69
123
  self.logger.debug("Sucess initializing")
70
124
 
125
+ @property
126
+ def name(self) -> str:
127
+ return self.rawData.get(Tags.NAME)
128
+
129
+ @property
130
+ def qtype(self) -> str:
131
+ return self.rawData.get(Tags.TYPE)
132
+
71
133
  def __repr__(self) -> str:
72
134
  li: list[str] = []
73
135
  li.append(f"Question v{self.id}")
74
136
  li.append(f"{self.qtype}")
75
- li.append(f"{self.parent=}")
76
137
  return "\t".join(li)
77
138
 
78
- def assemble(self, variant: int = 1) -> None:
79
- textElements: list[ET.Element] = []
80
- textElements.extend(self.qtextParagraphs)
81
- self.logger.debug("Starting assembly, (variant %s)", variant)
139
+ def assemble(self, variant=0) -> None:
140
+ """Assemble the question to the valid xml Tree."""
141
+ mainText = self._getTextElement()
142
+ self.logger.debug("Starting assembly")
143
+ self._setAnswerElement(variant=variant)
144
+ textParts = self._assembleMainTextParts()
145
+ if hasattr(self, "picture") and self.picture.ready:
146
+ mainText.append(self.picture.element)
147
+ self.logger.debug("Appended Picture element to text")
148
+ mainText.append(etHelpers.getCdatTxtElement(textParts))
149
+
150
+ def _assembleMainTextParts(self, variant=0) -> list[ET.Element]:
151
+ """Assemble the Question Text.
152
+
153
+ Intended for the cloze question, where the answers parts are part of the text.
154
+ """
155
+ self.logger.debug("inserting MainText to element")
156
+ textParts: list[ET.Element] = []
157
+ textParts.extend(self.qtextParagraphs)
158
+ bullets = self._getBPoints(variant=variant)
159
+ if bullets is not None:
160
+ textParts.append(bullets)
161
+ if hasattr(self, "picture") and self.picture.ready:
162
+ textParts.append(self.picture.htmlTag)
163
+ self.logger.debug("Appended Picture html to text")
164
+ return textParts
165
+
166
+ def _getTextElement(self) -> ET.Element:
82
167
  if self.element is not None:
83
168
  mainText = self.element.find(XMLTags.QTEXT)
84
169
  self.logger.debug(f"found existing Text in element {mainText=}")
@@ -86,34 +171,36 @@ class Question:
86
171
  if txtele is not None:
87
172
  mainText.remove(txtele)
88
173
  self.logger.debug("removed previously existing questiontext")
89
- else:
90
- msg = "Cant assamble, if element is none"
91
- raise QNotParsedException(msg, self.id)
92
- if self.variants is not None:
93
- textElements.append(self.getBPointVariant(variant - 1))
94
- elif self.bulletList is not None:
95
- textElements.append(self.bulletList)
96
- if hasattr(self, "picture") and self.picture.ready:
97
- textElements.append(self.picture.htmlTag)
98
- mainText.append(self.picture.element)
99
- mainText.append(etHelpers.getCdatTxtElement(textElements))
100
- self.logger.debug("inserted MainText to element")
101
- if len(self.answerVariants) > 0:
102
- ans = self.element.find(XMLTags.ANSWER)
103
- if ans is not None:
104
- self.element.remove(ans)
105
- self.logger.debug("removed previous answer element")
106
- self.element.insert(5, self.answerVariants[variant - 1])
107
-
108
- def setID(self, id=0) -> None:
174
+ return mainText
175
+ msg = "Cant assamble, if element is none"
176
+ raise QNotParsedException(msg, self.id)
177
+
178
+ def _getBPoints(self, variant: int = 0) -> ET.Element:
179
+ if hasattr(self, "bulletList"):
180
+ return self.bulletList
181
+ return None
182
+
183
+ def _setAnswerElement(self, variant: int = 0) -> None:
184
+ pass
185
+
186
+ def _setID(self, id=0) -> None:
109
187
  if id == 0:
110
- self.id: str = f"{self.category.id}{self.number:02d}"
188
+ self.id: str = f"{self.category.id}{self.rawData.get(Tags.NUMBER):02d}"
111
189
  else:
112
190
  self.id: str = str(id)
113
191
 
114
- def getBPointVariant(self, variant: int) -> ET.Element:
192
+
193
+ class ParametricQuestion(Question):
194
+ def __init__(self, *args, **kwargs) -> None:
195
+ super().__init__(*args, **kwargs)
196
+ self.variants: int = 0
197
+ self.variables: dict[str, list[float | int]] = {}
198
+
199
+ def _getBPoints(self, variant: int = 1) -> ET.Element:
200
+ """Get the bullet points with the variable set for `variant`."""
115
201
  if self.bulletList is None:
116
- return None
202
+ msg = "Can't assemble a parametric question, without the bulletPoints variables"
203
+ raise QNotParsedException(msg, self.id)
117
204
  # matches {a}, {some_var}, etc.
118
205
  varPlaceholder = re.compile(r"{(\w+)}")
119
206
 
@@ -129,8 +216,8 @@ class Question:
129
216
  listItemText = li.text or ""
130
217
  bullet = TextElements.LISTITEM.create()
131
218
  bullet.text = varPlaceholder.sub(replaceMatch, listItemText)
132
- self.logger.debug(f"Inserted Variables into List: {bullet}")
133
219
  unorderedList.append(bullet)
220
+ self.logger.debug("Inserted Variables into List: %s", bullet.text)
134
221
  return unorderedList
135
222
 
136
223
 
@@ -140,7 +227,7 @@ class Picture:
140
227
  ) -> None:
141
228
  self.logger = LogAdapterQuestionID(loggerObj, {"qID": questionId})
142
229
  self.picID: str
143
- w: int = width if width > 0 else settings.get(SettingsKey.PICTUREWIDTH)
230
+ w: int = width if width > 0 else settings.get(Tags.PICTUREWIDTH)
144
231
  self.size: dict[str, str] = {"width": str(w)}
145
232
  self.ready: bool = False
146
233
  self.imgFolder = imgFolder
@@ -149,7 +236,7 @@ class Picture:
149
236
  self.questionId: str = questionId
150
237
  self.logger.debug("Instantiating a new picture in %s", picKey)
151
238
  if self.getImgId(picKey):
152
- self.ready = self.__getImg()
239
+ self.ready = self._getImg()
153
240
  else:
154
241
  self.ready = False
155
242
 
@@ -192,7 +279,7 @@ class Picture:
192
279
  with imgPath.open("rb") as img:
193
280
  return base64.b64encode(img.read()).decode("utf-8")
194
281
 
195
- def __getImg(self) -> bool:
282
+ def _getImg(self) -> bool:
196
283
  suffixes = ["png", "svg", "jpeg", "jpg"]
197
284
  paths = [
198
285
  path
@@ -202,15 +289,12 @@ class Picture:
202
289
  self.logger.debug("Found the following paths %s", paths)
203
290
  try:
204
291
  self.path = paths[0]
205
- except IndexError as e:
206
- msg = f"The Picture from key {self.picID} is not found"
207
- # raise InvalidFieldException(msg, self.questionId, DFIndex.PICTURE)
208
- self.logger.warning(
209
- msg=f"Bild {self.picID} konnte nicht gefunden werden ",
210
- exc_info=e,
211
- )
292
+ except IndexError:
293
+ msg = f"The Picture {self.imgFolder}/{self.picID} is not found"
294
+ self.logger.warning(msg=msg)
212
295
  self.element = None
213
296
  return False
297
+ # raise FileNotFoundError(msg)
214
298
  base64Img = self._getBase64Img(self.path)
215
299
  self.element: ET.Element = ET.Element(
216
300
  "file",