excel2moodle 0.4.0__py3-none-any.whl → 0.4.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.
Files changed (33) hide show
  1. excel2moodle/__main__.py +8 -9
  2. excel2moodle/core/__init__.py +0 -10
  3. excel2moodle/core/category.py +4 -3
  4. excel2moodle/core/dataStructure.py +85 -57
  5. excel2moodle/core/etHelpers.py +2 -2
  6. excel2moodle/core/exceptions.py +2 -2
  7. excel2moodle/core/globals.py +10 -27
  8. excel2moodle/core/parser.py +24 -30
  9. excel2moodle/core/question.py +147 -63
  10. excel2moodle/core/settings.py +73 -45
  11. excel2moodle/core/validator.py +36 -55
  12. excel2moodle/logger.py +3 -3
  13. excel2moodle/question_types/__init__.py +2 -0
  14. excel2moodle/question_types/cloze.py +207 -0
  15. excel2moodle/question_types/mc.py +26 -16
  16. excel2moodle/question_types/nf.py +17 -3
  17. excel2moodle/question_types/nfm.py +60 -17
  18. excel2moodle/ui/{windowEquationChecker.py → UI_equationChecker.py} +98 -78
  19. excel2moodle/ui/{exportSettingsDialog.py → UI_exportSettingsDialog.py} +55 -4
  20. excel2moodle/ui/{windowMain.py → UI_mainWindow.py} +32 -39
  21. excel2moodle/ui/appUi.py +35 -66
  22. excel2moodle/ui/dialogs.py +40 -2
  23. excel2moodle/ui/equationChecker.py +70 -0
  24. excel2moodle/ui/treewidget.py +4 -4
  25. {excel2moodle-0.4.0.dist-info → excel2moodle-0.4.2.dist-info}/METADATA +2 -3
  26. excel2moodle-0.4.2.dist-info/RECORD +38 -0
  27. {excel2moodle-0.4.0.dist-info → excel2moodle-0.4.2.dist-info}/entry_points.txt +0 -3
  28. excel2moodle/ui/questionPreviewDialog.py +0 -115
  29. excel2moodle-0.4.0.dist-info/RECORD +0 -37
  30. /excel2moodle/ui/{variantDialog.py → UI_variantDialog.py} +0 -0
  31. {excel2moodle-0.4.0.dist-info → excel2moodle-0.4.2.dist-info}/WHEEL +0 -0
  32. {excel2moodle-0.4.0.dist-info → excel2moodle-0.4.2.dist-info}/licenses/LICENSE +0 -0
  33. {excel2moodle-0.4.0.dist-info → excel2moodle-0.4.2.dist-info}/top_level.txt +0 -0
@@ -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,154 @@ 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
+ textParts = self._assembleMainTextParts()
143
+ self.logger.debug("Starting assembly")
144
+ if hasattr(self, "picture") and self.picture.ready:
145
+ self._appendPicture(textParts, mainText)
146
+ self.logger.debug("inserted MainText to element")
147
+ self._setAnswerElement(variant=variant)
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
+ textParts: list[ET.Element] = []
156
+ textParts.extend(self.qtextParagraphs)
157
+ bullets = self._getBPoints(variant=variant)
158
+ if bullets is not None:
159
+ textParts.append(bullets)
160
+ return textParts
161
+
162
+ def _getTextElement(self) -> ET.Element:
82
163
  if self.element is not None:
83
164
  mainText = self.element.find(XMLTags.QTEXT)
84
165
  self.logger.debug(f"found existing Text in element {mainText=}")
@@ -86,34 +167,40 @@ class Question:
86
167
  if txtele is not None:
87
168
  mainText.remove(txtele)
88
169
  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:
170
+ return mainText
171
+ msg = "Cant assamble, if element is none"
172
+ raise QNotParsedException(msg, self.id)
173
+
174
+ def _appendPicture(self, textParts: list[ET.Element], mainText: ET.Element) -> None:
175
+ textParts.append(self.picture.htmlTag)
176
+ mainText.append(self.picture.element)
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",
@@ -12,9 +12,10 @@ import excel2moodle
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
14
 
15
- class SettingsKey(StrEnum):
16
- """Settings Keys are needed to always acess the correct Value.
15
+ class Tags(StrEnum):
16
+ """Tags and Settings Keys are needed to always acess the correct Value.
17
17
 
18
+ The Tags can be used to acess the settings or the QuestionData respectively.
18
19
  As the QSettings settings are accesed via strings, which could easily gotten wrong.
19
20
  Further, this Enum defines, which type a setting has to be.
20
21
  """
@@ -22,23 +23,27 @@ class SettingsKey(StrEnum):
22
23
  def __new__(
23
24
  cls,
24
25
  key: str,
25
- place: str,
26
26
  typ: type,
27
27
  default: str | float | Path | bool | None,
28
+ place: str = "project",
28
29
  ):
29
30
  """Define new settings class."""
30
31
  obj = str.__new__(cls, key)
31
32
  obj._value_ = key
32
- obj._place_ = place
33
- obj._default_ = default
34
33
  obj._typ_ = typ
34
+ obj._default_ = default
35
+ obj._place_ = place
35
36
  return obj
36
37
 
37
38
  def __init__(
38
- self, _, place: str, typ: type, default: str | float | Path | None
39
+ self,
40
+ _,
41
+ typ: type,
42
+ default: str | float | Path | None,
43
+ place: str = "project",
39
44
  ) -> None:
40
- self._typ_ = typ
41
- self._place_ = place
45
+ self._typ_: type = typ
46
+ self._place_: str = place
42
47
  self._default_ = default
43
48
  self._full_ = f"{self._place_}/{self._value_}"
44
49
 
@@ -56,22 +61,39 @@ class SettingsKey(StrEnum):
56
61
  return self._full_
57
62
 
58
63
  def typ(self) -> type:
59
- """Get default value for the key."""
64
+ """Get type of the keys data."""
60
65
  return self._typ_
61
66
 
62
- QUESTIONVARIANT = "defaultQuestionVariant", "testgen", int, 0
63
- INCLUDEINCATS = "includeCats", "testgen", bool, False
64
- TOLERANCE = "tolerance", "parser/nf", int, 1
65
- PICTUREFOLDER = "pictureFolder", "core", Path, None
66
- SPREADSHEETFOLDER = "spreadsheetFolder", "core", Path, None
67
- LOGLEVEL = "loglevel", "core", str, "INFO"
68
- LOGFILE = "logfile", "core", str, "excel2moodleLogFile.log"
69
- CATEGORIESSHEET = "categoriesSheet", "core", str, "Kategorien"
70
- VERSION = "version", "project", int, 1
71
- POINTS = "points", "project", float, 1.0
72
- PICTURESUBFOLDER = "imgFolder", "project", str, "Abbildungen"
73
- PICTUREWIDTH = "imgWidth", "project", int, 500
74
- ANSPICWIDTH = "answerImgWidth", "project", int, 120
67
+ QUESTIONVARIANT = "defaultQuestionVariant", int, 0, "testgen"
68
+ INCLUDEINCATS = "includeCats", bool, False, "testgen"
69
+ TOLERANCE = "tolerance", float, 0.01, "parser/nf"
70
+ PICTUREFOLDER = "pictureFolder", Path, None, "core"
71
+ PICTURESUBFOLDER = "imgfolder", str, "Abbildungen", "project"
72
+ SPREADSHEETPATH = "spreadsheetFolder", Path, None, "core"
73
+ LOGLEVEL = "loglevel", str, "INFO", "core"
74
+ LOGFILE = "logfile", str, "excel2moodleLogFile.log", "core"
75
+ CATEGORIESSHEET = "categoriessheet", str, "Kategorien", "core"
76
+
77
+ IMPORTMODULE = "importmodule", str, None
78
+ TEXT = "text", list, None
79
+ BPOINTS = "bulletpoint", list, None
80
+ TRUE = "true", list, None
81
+ FALSE = "false", list, None
82
+ TYPE = "type", str, None
83
+ NAME = "name", str, None
84
+ RESULT = "result", float, None
85
+ EQUATION = "formula", str, None
86
+ PICTURE = "picture", str, False
87
+ NUMBER = "number", int, None
88
+ ANSTYPE = "answertype", str, None
89
+ QUESTIONPART = "part", list, None
90
+ PARTTYPE = "parttype", str, None
91
+ VERSION = "version", int, 1
92
+ POINTS = "points", float, 1.0
93
+ PICTUREWIDTH = "imgwidth", int, 500
94
+ ANSPICWIDTH = "answerimgwidth", int, 120
95
+ WRONGSIGNPERCENT = "wrongsignpercentage", int, 50
96
+ FIRSTRESULT = "firstresult", float, None
75
97
 
76
98
 
77
99
  class Settings(QSettings):
@@ -85,52 +107,58 @@ class Settings(QSettings):
85
107
  super().__init__("jbosse3", "excel2moodle")
86
108
  if excel2moodle.isMainState():
87
109
  logger.info("Settings are stored under: %s", self.fileName())
88
- if self.contains(SettingsKey.SPREADSHEETFOLDER.full):
89
- self.sheet = self.get(SettingsKey.SPREADSHEETFOLDER)
110
+ if self.contains(Tags.SPREADSHEETPATH.full):
111
+ self.sheet = self.get(Tags.SPREADSHEETPATH)
90
112
  if self.sheet.is_file():
91
113
  QTimer.singleShot(300, self._emitSpreadsheetChanged)
92
114
 
93
115
  def _emitSpreadsheetChanged(self) -> None:
94
116
  self.shPathChanged.emit(self.sheet)
95
117
 
118
+ @overload
119
+ def get(
120
+ self,
121
+ key: Literal[Tags.POINTS],
122
+ ) -> float: ...
96
123
  @overload
97
124
  def get(
98
125
  self,
99
126
  key: Literal[
100
- SettingsKey.QUESTIONVARIANT,
101
- SettingsKey.TOLERANCE,
102
- SettingsKey.VERSION,
103
- SettingsKey.POINTS,
104
- SettingsKey.PICTUREWIDTH,
105
- SettingsKey.ANSPICWIDTH,
127
+ Tags.QUESTIONVARIANT,
128
+ Tags.TOLERANCE,
129
+ Tags.VERSION,
130
+ Tags.PICTUREWIDTH,
131
+ Tags.ANSPICWIDTH,
132
+ Tags.WRONGSIGNPERCENT,
106
133
  ],
107
134
  ) -> int: ...
108
135
  @overload
109
- def get(self, key: Literal[SettingsKey.INCLUDEINCATS]) -> bool: ...
136
+ def get(self, key: Literal[Tags.INCLUDEINCATS]) -> bool: ...
110
137
  @overload
111
138
  def get(
112
139
  self,
113
140
  key: Literal[
114
- SettingsKey.PICTURESUBFOLDER,
115
- SettingsKey.LOGLEVEL,
116
- SettingsKey.LOGFILE,
117
- SettingsKey.CATEGORIESSHEET,
141
+ Tags.PICTURESUBFOLDER,
142
+ Tags.LOGLEVEL,
143
+ Tags.LOGFILE,
144
+ Tags.CATEGORIESSHEET,
145
+ Tags.IMPORTMODULE,
118
146
  ],
119
147
  ) -> str: ...
120
148
  @overload
121
149
  def get(
122
150
  self,
123
- key: Literal[SettingsKey.PICTUREFOLDER, SettingsKey.SPREADSHEETFOLDER],
151
+ key: Literal[Tags.PICTUREFOLDER, Tags.SPREADSHEETPATH],
124
152
  ) -> Path: ...
125
153
 
126
- def get(self, key: SettingsKey):
154
+ def get(self, key: Tags):
127
155
  """Get the typesafe settings value.
128
156
 
129
157
  If local Settings are stored, they are returned.
130
158
  If no setting is made, the default value is returned.
131
159
  """
132
- if key in self.localSettings:
133
- val = key.typ()(self.localSettings[key])
160
+ if key in type(self).localSettings:
161
+ val = key.typ()(type(self).localSettings[key])
134
162
  logger.debug("Returning project setting: %s = %s", key, val)
135
163
  return val
136
164
  if not excel2moodle.isMainState():
@@ -160,7 +188,7 @@ class Settings(QSettings):
160
188
 
161
189
  def set(
162
190
  self,
163
- key: SettingsKey | str,
191
+ key: Tags | str,
164
192
  value: float | bool | Path | str,
165
193
  local: bool = False,
166
194
  ) -> None:
@@ -177,13 +205,13 @@ class Settings(QSettings):
177
205
  if not excel2moodle.isMainState():
178
206
  local = True
179
207
  if local:
180
- if key in SettingsKey:
181
- self.localSettings[key] = value
208
+ if key in Tags:
209
+ type(self).localSettings[key] = value
182
210
  logger.info("Saved the project setting %s = %s", key, value)
183
211
  else:
184
212
  logger.warning("got invalid local Setting %s = %s", key, value)
185
213
  return
186
- if not local and isinstance(key, SettingsKey):
214
+ if not local and isinstance(key, Tags):
187
215
  if not isinstance(value, key.typ()):
188
216
  logger.error("trying to save setting with wrong type not possible")
189
217
  return
@@ -195,7 +223,7 @@ class Settings(QSettings):
195
223
  if isinstance(sheet, Path):
196
224
  self.sheet = sheet.resolve(strict=True)
197
225
  logpath = str(self.sheet.parent / "excel2moodleLogFile.log")
198
- self.set(SettingsKey.LOGFILE, logpath)
199
- self.set(SettingsKey.SPREADSHEETFOLDER, self.sheet)
226
+ self.set(Tags.LOGFILE, logpath)
227
+ self.set(Tags.SPREADSHEETPATH, self.sheet)
200
228
  self.shPathChanged.emit(sheet)
201
229
  return
@@ -10,19 +10,20 @@ which can be accessed via ``Validator.question``
10
10
  """
11
11
 
12
12
  import logging
13
- from typing import TYPE_CHECKING
14
13
 
15
14
  import pandas as pd
16
15
 
16
+ from excel2moodle.core import stringHelpers
17
17
  from excel2moodle.core.exceptions import InvalidFieldException
18
- from excel2moodle.core.globals import DFIndex
19
-
20
- if TYPE_CHECKING:
21
- from types import UnionType
22
-
18
+ from excel2moodle.core.globals import QUESTION_TYPES, Tags
19
+ from excel2moodle.core.question import QuestionData
20
+ from excel2moodle.core.settings import Settings
21
+ from excel2moodle.question_types import QuestionTypeMapping
23
22
 
24
23
  logger = logging.getLogger(__name__)
25
24
 
25
+ settings = Settings()
26
+
26
27
 
27
28
  class Validator:
28
29
  """Validate the question data from the spreadsheet.
@@ -30,52 +31,15 @@ class Validator:
30
31
  Creates a dictionary with the data, for easier access later.
31
32
  """
32
33
 
33
- def __init__(self) -> None:
34
- self.allMandatory: dict[DFIndex, type | UnionType] = {
35
- DFIndex.TEXT: str,
36
- DFIndex.NAME: str,
37
- DFIndex.TYPE: str,
38
- }
39
- self.allOptional: dict[DFIndex, type | UnionType] = {
40
- DFIndex.PICTURE: int | str,
41
- }
42
- self.nfOpt: dict[DFIndex, type | UnionType] = {
43
- DFIndex.BPOINTS: str,
44
- }
45
- self.nfMand: dict[DFIndex, type | UnionType] = {
46
- DFIndex.RESULT: float | int,
47
- }
48
- self.nfmOpt: dict[DFIndex, type | UnionType] = {}
49
- self.nfmMand: dict[DFIndex, type | UnionType] = {
50
- DFIndex.RESULT: str,
51
- DFIndex.BPOINTS: str,
52
- }
53
- self.mcOpt: dict[DFIndex, type | UnionType] = {}
54
- self.mcMand: dict[DFIndex, type | UnionType] = {
55
- DFIndex.TRUE: str,
56
- DFIndex.FALSE: str,
57
- DFIndex.ANSTYPE: str,
58
- }
59
-
60
- self.mapper: dict = {
61
- "NF": (self.nfOpt, self.nfMand),
62
- "MC": (self.mcOpt, self.mcMand),
63
- "NFM": (self.nfmOpt, self.nfmMand),
64
- }
65
-
66
34
  def setup(self, df: pd.Series, index: int) -> None:
67
35
  self.df = df
68
36
  self.index = index
69
- self.mandatory = {}
70
- self.optional = {}
71
- typ = self.df.loc[DFIndex.TYPE]
72
- if typ not in self.mapper:
37
+ typ = self.df.loc[Tags.TYPE]
38
+ if typ not in QUESTION_TYPES:
73
39
  msg = f"No valid question type provided. {typ} is not a known type"
74
- raise InvalidFieldException(msg, "index:02d", DFIndex.TYPE)
75
- self.mandatory.update(self.allMandatory)
76
- self.optional.update(self.allOptional)
77
- self.mandatory.update(self.mapper[typ][1])
78
- self.optional.update(self.mapper[typ][0])
40
+ raise InvalidFieldException(msg, "index:02d", Tags.TYPE)
41
+ self.mandatory = QuestionTypeMapping[typ].value.mandatoryTags
42
+ self.optional = QuestionTypeMapping[typ].value.optionalTags
79
43
 
80
44
  def validate(self) -> None:
81
45
  qid = f"{self.index:02d}"
@@ -90,9 +54,9 @@ class Validator:
90
54
  if missing is not None:
91
55
  raise InvalidFieldException(msg, qid, missing)
92
56
 
93
- def getQuestionRawData(self) -> dict[str, str | float | list[str]]:
57
+ def getQuestionData(self) -> QuestionData:
94
58
  """Get the data from the spreadsheet as a dictionary."""
95
- self.qdata: dict[str, str | float | int | list] = {}
59
+ self.qdata: dict[str, int | float | list[str] | str] = {}
96
60
  for idx, val in self.df.items():
97
61
  if not isinstance(idx, str):
98
62
  continue
@@ -106,9 +70,26 @@ class Validator:
106
70
  self.qdata[idx] = [existing, val]
107
71
  else:
108
72
  self.qdata[idx] = val
109
- return self.qdata
110
-
111
- def _mandatory(self) -> tuple[bool, DFIndex | None]:
73
+ return self.formatQData()
74
+
75
+ def formatQData(self) -> QuestionData:
76
+ """Format the dictionary to The types for QuestionData."""
77
+ listTags = (Tags.BPOINTS, Tags.TRUE, Tags.FALSE, Tags.TEXT, Tags.QUESTIONPART)
78
+ for tag in listTags:
79
+ for key in self.qdata:
80
+ if key.startswith(tag) and not isinstance(self.qdata[key], list):
81
+ self.qdata[key] = stringHelpers.getListFromStr(self.qdata[key])
82
+ tol = float(self.qdata.get(Tags.TOLERANCE, 0))
83
+ if tol <= 0 or tol > 99:
84
+ self.qdata[Tags.TOLERANCE] = settings.get(Tags.TOLERANCE)
85
+ else:
86
+ self.qdata[Tags.TOLERANCE] = tol if tol < 1 else tol / 100
87
+
88
+ if self.qdata[Tags.TYPE] == "NFM":
89
+ self.qdata[Tags.EQUATION] = str(self.qdata[Tags.RESULT])
90
+ return QuestionData(self.qdata)
91
+
92
+ def _mandatory(self) -> tuple[bool, Tags | None]:
112
93
  """Detects if all keys of mandatory are filled with values."""
113
94
  checker = pd.Series.notna(self.df)
114
95
  for k in self.mandatory:
@@ -123,8 +104,8 @@ class Validator:
123
104
  return False, k
124
105
  return True, None
125
106
 
126
- def _typeCheck(self) -> tuple[bool, list[DFIndex] | None]:
127
- invalid: list[DFIndex] = []
107
+ def _typeCheck(self) -> tuple[bool, list[Tags] | None]:
108
+ invalid: list[Tags] = []
128
109
  for field, typ in self.mandatory.items():
129
110
  if field in self.df and isinstance(self.df[field], pd.Series):
130
111
  for f in self.df[field]: