excel2moodle 0.4.1__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.
- excel2moodle/__main__.py +2 -2
- excel2moodle/core/__init__.py +0 -10
- excel2moodle/core/category.py +4 -3
- excel2moodle/core/dataStructure.py +85 -57
- excel2moodle/core/etHelpers.py +2 -2
- excel2moodle/core/exceptions.py +2 -2
- excel2moodle/core/globals.py +10 -27
- excel2moodle/core/parser.py +24 -30
- excel2moodle/core/question.py +147 -63
- excel2moodle/core/settings.py +73 -45
- excel2moodle/core/validator.py +36 -55
- excel2moodle/logger.py +3 -3
- excel2moodle/question_types/__init__.py +2 -0
- excel2moodle/question_types/cloze.py +207 -0
- excel2moodle/question_types/mc.py +26 -16
- excel2moodle/question_types/nf.py +17 -3
- excel2moodle/question_types/nfm.py +60 -17
- excel2moodle/ui/{windowEquationChecker.py → UI_equationChecker.py} +98 -78
- excel2moodle/ui/{exportSettingsDialog.py → UI_exportSettingsDialog.py} +55 -4
- excel2moodle/ui/{windowMain.py → UI_mainWindow.py} +32 -39
- excel2moodle/ui/appUi.py +35 -66
- excel2moodle/ui/dialogs.py +40 -2
- excel2moodle/ui/equationChecker.py +70 -0
- excel2moodle/ui/treewidget.py +4 -4
- {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.2.dist-info}/METADATA +2 -3
- excel2moodle-0.4.2.dist-info/RECORD +38 -0
- {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.2.dist-info}/entry_points.txt +0 -3
- excel2moodle/ui/questionPreviewDialog.py +0 -115
- excel2moodle-0.4.1.dist-info/RECORD +0 -37
- /excel2moodle/ui/{variantDialog.py → UI_variantDialog.py} +0 -0
- {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.2.dist-info}/WHEEL +0 -0
- {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.2.dist-info}/top_level.txt +0 -0
excel2moodle/core/question.py
CHANGED
@@ -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
|
-
|
15
|
+
QUESTION_TYPES,
|
16
|
+
Tags,
|
15
17
|
TextElements,
|
16
18
|
XMLTags,
|
17
|
-
questionTypes,
|
18
19
|
)
|
19
|
-
from excel2moodle.core.settings import Settings,
|
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:
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
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:
|
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.
|
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
|
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
|
64
|
-
self.
|
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
|
79
|
-
|
80
|
-
|
81
|
-
self.
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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.
|
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
|
-
|
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
|
-
|
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(
|
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.
|
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
|
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
|
206
|
-
msg = f"The Picture
|
207
|
-
|
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",
|
excel2moodle/core/settings.py
CHANGED
@@ -12,9 +12,10 @@ import excel2moodle
|
|
12
12
|
logger = logging.getLogger(__name__)
|
13
13
|
|
14
14
|
|
15
|
-
class
|
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,
|
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
|
64
|
+
"""Get type of the keys data."""
|
60
65
|
return self._typ_
|
61
66
|
|
62
|
-
QUESTIONVARIANT = "defaultQuestionVariant",
|
63
|
-
INCLUDEINCATS = "includeCats",
|
64
|
-
TOLERANCE = "tolerance", "parser/nf"
|
65
|
-
PICTUREFOLDER = "pictureFolder",
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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(
|
89
|
-
self.sheet = self.get(
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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[
|
136
|
+
def get(self, key: Literal[Tags.INCLUDEINCATS]) -> bool: ...
|
110
137
|
@overload
|
111
138
|
def get(
|
112
139
|
self,
|
113
140
|
key: Literal[
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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[
|
151
|
+
key: Literal[Tags.PICTUREFOLDER, Tags.SPREADSHEETPATH],
|
124
152
|
) -> Path: ...
|
125
153
|
|
126
|
-
def get(self, key:
|
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:
|
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
|
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,
|
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(
|
199
|
-
self.set(
|
226
|
+
self.set(Tags.LOGFILE, logpath)
|
227
|
+
self.set(Tags.SPREADSHEETPATH, self.sheet)
|
200
228
|
self.shPathChanged.emit(sheet)
|
201
229
|
return
|
excel2moodle/core/validator.py
CHANGED
@@ -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
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
70
|
-
|
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",
|
75
|
-
self.mandatory.
|
76
|
-
self.optional.
|
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
|
57
|
+
def getQuestionData(self) -> QuestionData:
|
94
58
|
"""Get the data from the spreadsheet as a dictionary."""
|
95
|
-
self.qdata: dict[str,
|
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.
|
110
|
-
|
111
|
-
def
|
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[
|
127
|
-
invalid: list[
|
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]:
|