excel2moodle 0.3.7__py3-none-any.whl → 0.4.0__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/__init__.py +6 -11
- excel2moodle/__main__.py +13 -1
- excel2moodle/core/dataStructure.py +23 -19
- excel2moodle/core/globals.py +1 -1
- excel2moodle/core/parser.py +20 -34
- excel2moodle/core/question.py +82 -48
- excel2moodle/{ui → core}/settings.py +28 -13
- excel2moodle/core/stringHelpers.py +4 -3
- excel2moodle/core/validator.py +7 -4
- excel2moodle/logger.py +1 -1
- excel2moodle/question_types/mc.py +44 -10
- excel2moodle/question_types/nf.py +1 -2
- excel2moodle/question_types/nfm.py +1 -2
- excel2moodle/ui/appUi.py +30 -15
- excel2moodle/ui/dialogs.py +41 -33
- excel2moodle/ui/exportSettingsDialog.py +79 -0
- excel2moodle/ui/windowDoc.py +9 -17
- excel2moodle/ui/windowMain.py +220 -225
- {excel2moodle-0.3.7.dist-info → excel2moodle-0.4.0.dist-info}/METADATA +1 -1
- excel2moodle-0.4.0.dist-info/RECORD +37 -0
- {excel2moodle-0.3.7.dist-info → excel2moodle-0.4.0.dist-info}/WHEEL +1 -1
- {excel2moodle-0.3.7.dist-info → excel2moodle-0.4.0.dist-info}/entry_points.txt +3 -0
- excel2moodle/core/questionWriter.py +0 -267
- excel2moodle-0.3.7.dist-info/RECORD +0 -37
- {excel2moodle-0.3.7.dist-info → excel2moodle-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.3.7.dist-info → excel2moodle-0.4.0.dist-info}/top_level.txt +0 -0
excel2moodle/__init__.py
CHANGED
@@ -25,13 +25,8 @@ Functionality
|
|
25
25
|
"""
|
26
26
|
|
27
27
|
import logging
|
28
|
-
import logging.config as logConfig
|
29
28
|
from importlib import metadata
|
30
29
|
from importlib.metadata import version
|
31
|
-
from pathlib import Path
|
32
|
-
|
33
|
-
from excel2moodle.logger import LogWindowHandler, loggerConfig
|
34
|
-
from excel2moodle.ui.settings import Settings, SettingsKey
|
35
30
|
|
36
31
|
try:
|
37
32
|
__version__ = version("excel2moodle")
|
@@ -53,11 +48,11 @@ if __package__ is not None:
|
|
53
48
|
}
|
54
49
|
|
55
50
|
|
56
|
-
|
57
|
-
logfile = Path(settings.get(SettingsKey.LOGFILE)).resolve()
|
58
|
-
e2mMetadata["logfile"] = logfile
|
59
|
-
if logfile.exists() and logfile.is_file():
|
60
|
-
logfile.replace(f"{logfile}.old")
|
51
|
+
isMain: bool = False
|
61
52
|
|
62
53
|
mainLogger = logging.getLogger(__name__)
|
63
|
-
|
54
|
+
|
55
|
+
|
56
|
+
def isMainState() -> bool:
|
57
|
+
mainLogger.debug("Returning mainState %s", isMain)
|
58
|
+
return isMain
|
excel2moodle/__main__.py
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
"""Main Function to make the Package executable."""
|
2
2
|
|
3
|
+
import logging.config as logConfig
|
3
4
|
import signal
|
5
|
+
from pathlib import Path
|
4
6
|
|
5
7
|
from PySide6 import QtWidgets, sys
|
6
8
|
|
9
|
+
import excel2moodle
|
7
10
|
from excel2moodle import e2mMetadata, mainLogger
|
8
11
|
from excel2moodle.core import dataStructure
|
12
|
+
from excel2moodle.core.settings import Settings, SettingsKey
|
13
|
+
from excel2moodle.logger import loggerConfig
|
9
14
|
from excel2moodle.ui import appUi as ui
|
10
|
-
from excel2moodle.ui.settings import Settings
|
11
15
|
|
12
16
|
|
13
17
|
def main() -> None:
|
@@ -25,4 +29,12 @@ def main() -> None:
|
|
25
29
|
|
26
30
|
|
27
31
|
if __name__ == "__main__":
|
32
|
+
excel2moodle.isMain = True
|
33
|
+
settings = Settings()
|
34
|
+
logfile = Path(settings.get(SettingsKey.LOGFILE)).resolve()
|
35
|
+
e2mMetadata["logfile"] = logfile
|
36
|
+
if logfile.exists() and logfile.is_file():
|
37
|
+
logfile.replace(f"{logfile}.old")
|
38
|
+
|
39
|
+
logConfig.dictConfig(config=loggerConfig)
|
28
40
|
main()
|
@@ -18,6 +18,7 @@ from excel2moodle.core.category import Category
|
|
18
18
|
from excel2moodle.core.exceptions import InvalidFieldException, QNotParsedException
|
19
19
|
from excel2moodle.core.globals import DFIndex
|
20
20
|
from excel2moodle.core.question import Question
|
21
|
+
from excel2moodle.core.settings import Settings, SettingsKey
|
21
22
|
from excel2moodle.core.validator import Validator
|
22
23
|
from excel2moodle.logger import LogAdapterQuestionID
|
23
24
|
from excel2moodle.question_types import QuestionTypeMapping
|
@@ -25,7 +26,6 @@ from excel2moodle.question_types.mc import MCQuestion, MCQuestionParser
|
|
25
26
|
from excel2moodle.question_types.nf import NFQuestion, NFQuestionParser
|
26
27
|
from excel2moodle.question_types.nfm import NFMQuestion, NFMQuestionParser
|
27
28
|
from excel2moodle.ui.dialogs import QuestionVariantDialog
|
28
|
-
from excel2moodle.ui.settings import Settings, SettingsKey
|
29
29
|
from excel2moodle.ui.treewidget import QuestionItem
|
30
30
|
|
31
31
|
if TYPE_CHECKING:
|
@@ -93,10 +93,11 @@ class QuestionDB:
|
|
93
93
|
self.categoriesMetaData = pd.read_excel(
|
94
94
|
f,
|
95
95
|
sheet_name=self.settings.get(SettingsKey.CATEGORIESSHEET),
|
96
|
-
usecols=["Kategorie", "Beschreibung", "Punkte", "Version"],
|
97
96
|
index_col=0,
|
98
97
|
)
|
99
|
-
logger.info(
|
98
|
+
logger.info(
|
99
|
+
"Sucessfully read categoriesMetaData \n %s", self.categoriesMetaData
|
100
|
+
)
|
100
101
|
|
101
102
|
def _setProjectSettings(self, settings: pd.DataFrame) -> None:
|
102
103
|
for tag, value in settings.iterrows():
|
@@ -173,14 +174,22 @@ class QuestionDB:
|
|
173
174
|
"""Setup the category from the ``dataframe``.
|
174
175
|
:emits: categoryReady(self) Signal.
|
175
176
|
""" # noqa: D401
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
177
|
+
points = (
|
178
|
+
self.categoriesMetaData["points"].iloc[categoryNumber - 1]
|
179
|
+
if "points" in self.categoriesMetaData
|
180
|
+
and not pd.isna(self.categoriesMetaData["points"]).iloc[categoryNumber - 1]
|
181
|
+
else self.settings.get(SettingsKey.POINTS)
|
182
|
+
)
|
183
|
+
version = (
|
184
|
+
self.categoriesMetaData["version"].iloc[categoryNumber - 1]
|
185
|
+
if "version" in self.categoriesMetaData
|
186
|
+
and not pd.isna(self.categoriesMetaData["version"].iloc[categoryNumber - 1])
|
187
|
+
else self.settings.get(SettingsKey.VERSION)
|
188
|
+
)
|
180
189
|
category = Category(
|
181
190
|
categoryNumber,
|
182
191
|
categoryName,
|
183
|
-
self.categoriesMetaData["
|
192
|
+
self.categoriesMetaData["description"].iloc[categoryNumber - 1],
|
184
193
|
dataframe=categoryDf,
|
185
194
|
points=points,
|
186
195
|
version=version,
|
@@ -204,24 +213,19 @@ class QuestionDB:
|
|
204
213
|
"""
|
205
214
|
for qNum in category.dataframe.columns:
|
206
215
|
try:
|
207
|
-
self.
|
208
|
-
except InvalidFieldException as e:
|
216
|
+
self.setupAndParseQuestion(category, qNum)
|
217
|
+
except (InvalidFieldException, QNotParsedException, ValueError) as e:
|
209
218
|
logger.exception(
|
210
|
-
"Question %s%02d
|
219
|
+
"Question %s%02d couldn't be parsed. The Question Data: \n %s",
|
211
220
|
category.id,
|
212
221
|
qNum,
|
213
|
-
|
214
|
-
)
|
215
|
-
except QNotParsedException as e:
|
216
|
-
logger.exception(
|
217
|
-
"Frage %s konnte nicht erstellt werden",
|
218
|
-
category.questions[qNum].id,
|
222
|
+
category.dataframe[qNum],
|
219
223
|
exc_info=e,
|
220
224
|
)
|
221
225
|
self.signals.categoryQuestionsReady.emit(category)
|
222
226
|
|
223
227
|
@classmethod
|
224
|
-
def
|
228
|
+
def setupAndParseQuestion(cls, category: Category, qNumber: int) -> None:
|
225
229
|
"""Check if the Question Data is valid. Then parse it.
|
226
230
|
|
227
231
|
The Question data is accessed from `category.dataframe` via its number
|
@@ -315,9 +319,9 @@ class QuestionDB:
|
|
315
319
|
if dialog.exec() == QtWidgets.QDialog.Accepted:
|
316
320
|
variant = dialog.variant
|
317
321
|
logger.debug("Die Fragen-Variante %s wurde gewählt", variant)
|
318
|
-
q.assemble(variant)
|
319
322
|
else:
|
320
323
|
logger.warning("Keine Fragenvariante wurde gewählt.")
|
324
|
+
q.assemble(variant)
|
321
325
|
else:
|
322
326
|
q.assemble()
|
323
327
|
tree.append(q.element)
|
excel2moodle/core/globals.py
CHANGED
excel2moodle/core/parser.py
CHANGED
@@ -2,7 +2,6 @@ import logging
|
|
2
2
|
import re
|
3
3
|
|
4
4
|
import lxml.etree as ET
|
5
|
-
import pandas as pd
|
6
5
|
|
7
6
|
import excel2moodle.core.etHelpers as eth
|
8
7
|
from excel2moodle.core import stringHelpers
|
@@ -15,13 +14,11 @@ from excel2moodle.core.globals import (
|
|
15
14
|
feedBElements,
|
16
15
|
)
|
17
16
|
from excel2moodle.core.question import Picture, Question
|
17
|
+
from excel2moodle.core.settings import Settings, SettingsKey
|
18
18
|
from excel2moodle.logger import LogAdapterQuestionID
|
19
|
-
from excel2moodle.ui.settings import Settings, SettingsKey
|
20
19
|
|
21
20
|
loggerObj = logging.getLogger(__name__)
|
22
21
|
|
23
|
-
settings = Settings()
|
24
|
-
|
25
22
|
|
26
23
|
class QuestionParser:
|
27
24
|
"""Setup the Parser Object.
|
@@ -30,6 +27,8 @@ class QuestionParser:
|
|
30
27
|
Important to implement the answers methods.
|
31
28
|
"""
|
32
29
|
|
30
|
+
settings = Settings()
|
31
|
+
|
33
32
|
def __init__(self) -> None:
|
34
33
|
"""Initialize the general Question parser."""
|
35
34
|
self.genFeedbacks: list[XMLTags] = []
|
@@ -48,27 +47,22 @@ class QuestionParser:
|
|
48
47
|
"""Create a ``Picture`` object ``question``if the question needs a pic."""
|
49
48
|
if hasattr(self, "picture") and self.question.picture.ready:
|
50
49
|
return True
|
51
|
-
picKey = self.rawInput
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
return True
|
58
|
-
return False
|
50
|
+
picKey = self.rawInput.get(DFIndex.PICTURE, False)
|
51
|
+
f = self.settings.get(SettingsKey.PICTUREFOLDER)
|
52
|
+
svgFolder = (f / self.question.katName).resolve()
|
53
|
+
if not hasattr(self.question, "picture"):
|
54
|
+
self.question.picture = Picture(picKey, svgFolder, self.question.id)
|
55
|
+
return bool(self.question.picture.ready)
|
59
56
|
|
60
57
|
def setMainText(self) -> None:
|
61
58
|
paragraphs: list[ET._Element] = [TextElements.PLEFT.create()]
|
62
59
|
ET.SubElement(paragraphs[0], "b").text = f"ID {self.question.id}"
|
63
60
|
text = self.rawInput[DFIndex.TEXT]
|
64
|
-
pcount = 0
|
65
61
|
for t in text:
|
66
|
-
|
67
|
-
|
68
|
-
paragraphs.append(TextElements.PLEFT.create())
|
69
|
-
paragraphs[-1].text = t
|
62
|
+
paragraphs.append(TextElements.PLEFT.create())
|
63
|
+
paragraphs[-1].text = t
|
70
64
|
self.question.qtextParagraphs = paragraphs
|
71
|
-
self.logger.debug("Created main Text with: %s paragraphs",
|
65
|
+
self.logger.debug("Created main Text with: %s paragraphs", len(text))
|
72
66
|
|
73
67
|
def setBPoints(self) -> None:
|
74
68
|
"""If there bulletPoints are set in the Spreadsheet it creates an unordered List-Element in ``Question.bulletList``."""
|
@@ -194,7 +188,6 @@ class QuestionParser:
|
|
194
188
|
def getNumericAnsElement(
|
195
189
|
self,
|
196
190
|
result: float,
|
197
|
-
tolerance: float = 0,
|
198
191
|
fraction: float = 100,
|
199
192
|
format: str = "moodle_auto_format",
|
200
193
|
) -> ET.Element:
|
@@ -218,22 +211,15 @@ class QuestionParser:
|
|
218
211
|
TextElements.SPANGREEN,
|
219
212
|
),
|
220
213
|
)
|
221
|
-
tolerance = self.
|
222
|
-
|
223
|
-
|
224
|
-
return ansEle
|
225
|
-
|
226
|
-
def getTolerancePercent(self, tolerance: float) -> int:
|
227
|
-
"""Get the correct tolerance.
|
228
|
-
If ``tolerance < 1``: it is interpreted as the fraction.
|
229
|
-
If ``tolerance >= 1``: it is interpreted as percentage.
|
230
|
-
"""
|
231
|
-
if tolerance == 0 or pd.isna(tolerance) or tolerance >= 100:
|
232
|
-
tolerance = settings.get(SettingsKey.PARSERNF_TOLERANCE)
|
214
|
+
tolerance = float(self.rawInput.get(DFIndex.TOLERANCE, 0))
|
215
|
+
if tolerance == 0 or tolerance >= 100:
|
216
|
+
tolerance = self.settings.get(SettingsKey.TOLERANCE)
|
233
217
|
self.logger.info(
|
234
218
|
"Using default tolerance %s percent from settings",
|
235
219
|
tolerance,
|
236
220
|
)
|
237
|
-
|
238
|
-
self.logger.debug("Using tolerance %s percent",
|
239
|
-
|
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)))
|
225
|
+
return ansEle
|
excel2moodle/core/question.py
CHANGED
@@ -16,9 +16,11 @@ from excel2moodle.core.globals import (
|
|
16
16
|
XMLTags,
|
17
17
|
questionTypes,
|
18
18
|
)
|
19
|
+
from excel2moodle.core.settings import Settings, SettingsKey
|
19
20
|
from excel2moodle.logger import LogAdapterQuestionID
|
20
21
|
|
21
22
|
loggerObj = logging.getLogger(__name__)
|
23
|
+
settings = Settings()
|
22
24
|
|
23
25
|
|
24
26
|
class Question:
|
@@ -68,15 +70,15 @@ class Question:
|
|
68
70
|
|
69
71
|
def __repr__(self) -> str:
|
70
72
|
li: list[str] = []
|
71
|
-
li.append(f"Question v{self.
|
72
|
-
li.append(f"{self.
|
73
|
+
li.append(f"Question v{self.id}")
|
74
|
+
li.append(f"{self.qtype}")
|
73
75
|
li.append(f"{self.parent=}")
|
74
|
-
return "\
|
76
|
+
return "\t".join(li)
|
75
77
|
|
76
78
|
def assemble(self, variant: int = 1) -> None:
|
77
79
|
textElements: list[ET.Element] = []
|
78
80
|
textElements.extend(self.qtextParagraphs)
|
79
|
-
self.logger.debug("Starting assembly")
|
81
|
+
self.logger.debug("Starting assembly, (variant %s)", variant)
|
80
82
|
if self.element is not None:
|
81
83
|
mainText = self.element.find(XMLTags.QTEXT)
|
82
84
|
self.logger.debug(f"found existing Text in element {mainText=}")
|
@@ -95,7 +97,6 @@ class Question:
|
|
95
97
|
textElements.append(self.picture.htmlTag)
|
96
98
|
mainText.append(self.picture.element)
|
97
99
|
mainText.append(etHelpers.getCdatTxtElement(textElements))
|
98
|
-
# self.element.insert(3, mainText)
|
99
100
|
self.logger.debug("inserted MainText to element")
|
100
101
|
if len(self.answerVariants) > 0:
|
101
102
|
ans = self.element.find(XMLTags.ANSWER)
|
@@ -134,61 +135,94 @@ class Question:
|
|
134
135
|
|
135
136
|
|
136
137
|
class Picture:
|
137
|
-
def __init__(
|
138
|
-
self
|
138
|
+
def __init__(
|
139
|
+
self, picKey: str, imgFolder: Path, questionId: str, width: int = 0
|
140
|
+
) -> None:
|
141
|
+
self.logger = LogAdapterQuestionID(loggerObj, {"qID": questionId})
|
142
|
+
self.picID: str
|
143
|
+
w: int = width if width > 0 else settings.get(SettingsKey.PICTUREWIDTH)
|
144
|
+
self.size: dict[str, str] = {"width": str(w)}
|
139
145
|
self.ready: bool = False
|
140
|
-
self.
|
141
|
-
self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.question.id})
|
142
|
-
self.imgFolder = (imgFolder / question.katName).resolve()
|
146
|
+
self.imgFolder = imgFolder
|
143
147
|
self.htmlTag: ET.Element
|
144
148
|
self.path: Path
|
145
|
-
self.
|
146
|
-
|
149
|
+
self.questionId: str = questionId
|
150
|
+
self.logger.debug("Instantiating a new picture in %s", picKey)
|
151
|
+
if self.getImgId(picKey):
|
147
152
|
self.ready = self.__getImg()
|
148
|
-
|
149
|
-
def _setPath(self) -> None:
|
150
|
-
if self.pic == 1:
|
151
|
-
self.picID = self.question.id
|
152
153
|
else:
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
154
|
+
self.ready = False
|
155
|
+
|
156
|
+
def getImgId(self, imgKey: str) -> bool:
|
157
|
+
"""Get the image ID and width based on the given key.
|
158
|
+
The key should either be the full ID (as the question) or only the question Num.
|
159
|
+
If only the number is given, the category.id is prepended.
|
160
|
+
The width should be specified by `ID:width:XX`. where xx is the px value.
|
161
|
+
"""
|
162
|
+
width = re.findall(r"\:width\:(\d+)", str(imgKey))
|
163
|
+
height = re.findall(r"\:height\:(\d+)", str(imgKey))
|
164
|
+
if len(width) > 0 and width[0]:
|
165
|
+
self.size["width"] = width[0]
|
166
|
+
elif len(height) > 0 and height[0]:
|
167
|
+
self.size["height"] = height[0]
|
168
|
+
self.size.pop("width")
|
169
|
+
self.logger.debug("Size of picture is %s", self.size)
|
170
|
+
if imgKey in ("true", "True", "yes"):
|
171
|
+
self.picID = self.questionId
|
172
|
+
return True
|
173
|
+
num: list[int | str] = re.findall(r"^\d+", str(imgKey))
|
174
|
+
app: list[int | str] = re.findall(r"^\d+([A-Za-z_\-]+)", str(imgKey))
|
175
|
+
if imgKey in ("false", "nan", False) or len(num) == 0:
|
176
|
+
return False
|
177
|
+
imgID: int = int(num[0])
|
178
|
+
if imgID < 10:
|
179
|
+
picID = f"{self.questionId[:3]}{imgID:02d}"
|
180
|
+
elif imgID < 10000:
|
181
|
+
picID = f"{self.questionId[:1]}{imgID:04d}"
|
182
|
+
elif imgID <= 100000:
|
183
|
+
picID = str(imgID)
|
184
|
+
if len(app) > 0 and app[0]:
|
185
|
+
self.picID = f"{picID}{app[0]}"
|
186
|
+
else:
|
187
|
+
self.picID = str(picID)
|
188
|
+
self.logger.debug("Evaluated the imgID %s from %s", self.picID, imgKey)
|
189
|
+
return True
|
166
190
|
|
167
|
-
def
|
168
|
-
""
|
169
|
-
|
170
|
-
self.element: ET.Element = ET.Element(
|
171
|
-
"file",
|
172
|
-
name=f"{self.path.name}",
|
173
|
-
path="/",
|
174
|
-
encoding="base64",
|
175
|
-
)
|
176
|
-
self.element.text = self._getBase64Img(self.path)
|
191
|
+
def _getBase64Img(self, imgPath: Path):
|
192
|
+
with imgPath.open("rb") as img:
|
193
|
+
return base64.b64encode(img.read()).decode("utf-8")
|
177
194
|
|
178
195
|
def __getImg(self) -> bool:
|
196
|
+
suffixes = ["png", "svg", "jpeg", "jpg"]
|
197
|
+
paths = [
|
198
|
+
path
|
199
|
+
for suf in suffixes
|
200
|
+
for path in self.imgFolder.glob(f"{self.picID}.{suf}")
|
201
|
+
]
|
202
|
+
self.logger.debug("Found the following paths %s", paths)
|
179
203
|
try:
|
180
|
-
self.
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
alt=f"Bild {self.path.name}",
|
185
|
-
width="500",
|
186
|
-
)
|
187
|
-
return True
|
188
|
-
except FileNotFoundError as e:
|
204
|
+
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)
|
189
208
|
self.logger.warning(
|
190
209
|
msg=f"Bild {self.picID} konnte nicht gefunden werden ",
|
191
210
|
exc_info=e,
|
192
211
|
)
|
193
212
|
self.element = None
|
194
213
|
return False
|
214
|
+
base64Img = self._getBase64Img(self.path)
|
215
|
+
self.element: ET.Element = ET.Element(
|
216
|
+
"file",
|
217
|
+
name=f"{self.path.name}",
|
218
|
+
path="/",
|
219
|
+
encoding="base64",
|
220
|
+
)
|
221
|
+
self.element.text = base64Img
|
222
|
+
self.htmlTag = ET.Element(
|
223
|
+
"img",
|
224
|
+
src=f"@@PLUGINFILE@@/{self.path.name}",
|
225
|
+
alt=f"Bild {self.path.name}",
|
226
|
+
**self.size,
|
227
|
+
)
|
228
|
+
return True
|
@@ -7,6 +7,8 @@ from typing import ClassVar, Literal, overload
|
|
7
7
|
|
8
8
|
from PySide6.QtCore import QSettings, QTimer, Signal
|
9
9
|
|
10
|
+
import excel2moodle
|
11
|
+
|
10
12
|
logger = logging.getLogger(__name__)
|
11
13
|
|
12
14
|
|
@@ -58,16 +60,18 @@ class SettingsKey(StrEnum):
|
|
58
60
|
return self._typ_
|
59
61
|
|
60
62
|
QUESTIONVARIANT = "defaultQuestionVariant", "testgen", int, 0
|
61
|
-
INCLUDEINCATS = "includeCats", "
|
62
|
-
|
63
|
-
PICTURESUBFOLDER = "imgSubFolder", "core", str, "Abbildungen"
|
63
|
+
INCLUDEINCATS = "includeCats", "testgen", bool, False
|
64
|
+
TOLERANCE = "tolerance", "parser/nf", int, 1
|
64
65
|
PICTUREFOLDER = "pictureFolder", "core", Path, None
|
65
66
|
SPREADSHEETFOLDER = "spreadsheetFolder", "core", Path, None
|
66
67
|
LOGLEVEL = "loglevel", "core", str, "INFO"
|
67
68
|
LOGFILE = "logfile", "core", str, "excel2moodleLogFile.log"
|
68
|
-
CATEGORIESSHEET = "
|
69
|
+
CATEGORIESSHEET = "categoriesSheet", "core", str, "Kategorien"
|
69
70
|
VERSION = "version", "project", int, 1
|
70
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
|
71
75
|
|
72
76
|
|
73
77
|
class Settings(QSettings):
|
@@ -79,11 +83,12 @@ class Settings(QSettings):
|
|
79
83
|
def __init__(self) -> None:
|
80
84
|
"""Instantiate the settings."""
|
81
85
|
super().__init__("jbosse3", "excel2moodle")
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
86
|
+
if excel2moodle.isMainState():
|
87
|
+
logger.info("Settings are stored under: %s", self.fileName())
|
88
|
+
if self.contains(SettingsKey.SPREADSHEETFOLDER.full):
|
89
|
+
self.sheet = self.get(SettingsKey.SPREADSHEETFOLDER)
|
90
|
+
if self.sheet.is_file():
|
91
|
+
QTimer.singleShot(300, self._emitSpreadsheetChanged)
|
87
92
|
|
88
93
|
def _emitSpreadsheetChanged(self) -> None:
|
89
94
|
self.shPathChanged.emit(self.sheet)
|
@@ -93,9 +98,11 @@ class Settings(QSettings):
|
|
93
98
|
self,
|
94
99
|
key: Literal[
|
95
100
|
SettingsKey.QUESTIONVARIANT,
|
96
|
-
SettingsKey.
|
101
|
+
SettingsKey.TOLERANCE,
|
97
102
|
SettingsKey.VERSION,
|
98
103
|
SettingsKey.POINTS,
|
104
|
+
SettingsKey.PICTUREWIDTH,
|
105
|
+
SettingsKey.ANSPICWIDTH,
|
99
106
|
],
|
100
107
|
) -> int: ...
|
101
108
|
@overload
|
@@ -117,12 +124,18 @@ class Settings(QSettings):
|
|
117
124
|
) -> Path: ...
|
118
125
|
|
119
126
|
def get(self, key: SettingsKey):
|
120
|
-
"""Get the typesafe settings value.
|
121
|
-
|
127
|
+
"""Get the typesafe settings value.
|
128
|
+
|
129
|
+
If local Settings are stored, they are returned.
|
130
|
+
If no setting is made, the default value is returned.
|
131
|
+
"""
|
122
132
|
if key in self.localSettings:
|
123
133
|
val = key.typ()(self.localSettings[key])
|
124
134
|
logger.debug("Returning project setting: %s = %s", key, val)
|
125
135
|
return val
|
136
|
+
if not excel2moodle.isMainState():
|
137
|
+
logger.warning("No GUI: Returning default value.")
|
138
|
+
return key.default
|
126
139
|
if key.typ() is Path:
|
127
140
|
path: Path = self.value(key.full, defaultValue=key.default)
|
128
141
|
try:
|
@@ -158,9 +171,11 @@ class Settings(QSettings):
|
|
158
171
|
local
|
159
172
|
True saves local project specific settings.
|
160
173
|
Defaults to False
|
161
|
-
The local settings are meant to be set in the first sheet
|
174
|
+
The local settings are meant to be set in the first sheet `settings`
|
162
175
|
|
163
176
|
"""
|
177
|
+
if not excel2moodle.isMainState():
|
178
|
+
local = True
|
164
179
|
if local:
|
165
180
|
if key in SettingsKey:
|
166
181
|
self.localSettings[key] = value
|
@@ -4,14 +4,15 @@ import base64
|
|
4
4
|
from pathlib import Path
|
5
5
|
|
6
6
|
import lxml.etree as ET
|
7
|
+
from pandas import pandas
|
7
8
|
|
8
9
|
|
9
|
-
def getListFromStr(stringList: str) -> list[str]:
|
10
|
+
def getListFromStr(stringList: str | list[str]) -> list[str]:
|
10
11
|
"""Get a python List of strings from a semi-colon separated string."""
|
11
12
|
stripped: list[str] = []
|
12
|
-
li = stringList.split(";")
|
13
|
+
li = stringList if isinstance(stringList, list) else stringList.split(";")
|
13
14
|
for i in li:
|
14
|
-
s = i.strip()
|
15
|
+
s = i.strip() if not pandas.isna(i) else None
|
15
16
|
if s:
|
16
17
|
stripped.append(s)
|
17
18
|
return stripped
|
excel2moodle/core/validator.py
CHANGED
@@ -96,6 +96,8 @@ class Validator:
|
|
96
96
|
for idx, val in self.df.items():
|
97
97
|
if not isinstance(idx, str):
|
98
98
|
continue
|
99
|
+
if pd.isna(val):
|
100
|
+
continue
|
99
101
|
if idx in self.qdata:
|
100
102
|
if isinstance(self.qdata[idx], list):
|
101
103
|
self.qdata[idx].append(val)
|
@@ -124,16 +126,17 @@ class Validator:
|
|
124
126
|
def _typeCheck(self) -> tuple[bool, list[DFIndex] | None]:
|
125
127
|
invalid: list[DFIndex] = []
|
126
128
|
for field, typ in self.mandatory.items():
|
127
|
-
if isinstance(self.df[field], pd.Series):
|
129
|
+
if field in self.df and isinstance(self.df[field], pd.Series):
|
128
130
|
for f in self.df[field]:
|
129
131
|
if pd.notna(f) and not isinstance(f, typ):
|
130
132
|
invalid.append(field)
|
131
133
|
elif not isinstance(self.df[field], typ):
|
132
134
|
invalid.append(field)
|
133
135
|
for field, typ in self.optional.items():
|
134
|
-
if field in self.df:
|
135
|
-
|
136
|
-
|
136
|
+
if field in self.df and isinstance(self.df[field], pd.Series):
|
137
|
+
for f in self.df[field]:
|
138
|
+
if pd.notna(f) and not isinstance(f, typ):
|
139
|
+
invalid.append(field)
|
137
140
|
if len(invalid) == 0:
|
138
141
|
return True, None
|
139
142
|
return False, invalid
|