excel2moodle 0.3.6__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 -12
- excel2moodle/__main__.py +14 -3
- excel2moodle/core/category.py +6 -44
- excel2moodle/core/dataStructure.py +249 -82
- excel2moodle/core/globals.py +1 -21
- excel2moodle/core/parser.py +34 -204
- excel2moodle/core/question.py +108 -57
- excel2moodle/core/settings.py +201 -0
- excel2moodle/core/stringHelpers.py +10 -33
- excel2moodle/core/{questionValidator.py → validator.py} +32 -34
- excel2moodle/logger.py +1 -1
- excel2moodle/question_types/__init__.py +33 -0
- excel2moodle/question_types/mc.py +127 -0
- excel2moodle/question_types/nf.py +29 -0
- excel2moodle/question_types/nfm.py +91 -0
- excel2moodle/ui/appUi.py +67 -47
- excel2moodle/ui/dialogs.py +43 -34
- excel2moodle/ui/exportSettingsDialog.py +79 -0
- excel2moodle/ui/windowDoc.py +9 -17
- excel2moodle/ui/windowMain.py +220 -225
- {excel2moodle-0.3.6.dist-info → excel2moodle-0.4.0.dist-info}/METADATA +2 -2
- excel2moodle-0.4.0.dist-info/RECORD +37 -0
- {excel2moodle-0.3.6.dist-info → excel2moodle-0.4.0.dist-info}/WHEEL +1 -1
- {excel2moodle-0.3.6.dist-info → excel2moodle-0.4.0.dist-info}/entry_points.txt +3 -0
- excel2moodle/core/questionWriter.py +0 -267
- excel2moodle/ui/settings.py +0 -123
- excel2moodle-0.3.6.dist-info/RECORD +0 -33
- {excel2moodle-0.3.6.dist-info → excel2moodle-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.3.6.dist-info → excel2moodle-0.4.0.dist-info}/top_level.txt +0 -0
excel2moodle/core/parser.py
CHANGED
@@ -2,8 +2,6 @@ import logging
|
|
2
2
|
import re
|
3
3
|
|
4
4
|
import lxml.etree as ET
|
5
|
-
import pandas as pd
|
6
|
-
from asteval import Interpreter
|
7
5
|
|
8
6
|
import excel2moodle.core.etHelpers as eth
|
9
7
|
from excel2moodle.core import stringHelpers
|
@@ -14,16 +12,13 @@ from excel2moodle.core.globals import (
|
|
14
12
|
XMLTags,
|
15
13
|
feedbackStr,
|
16
14
|
feedBElements,
|
17
|
-
parserSettings,
|
18
15
|
)
|
19
16
|
from excel2moodle.core.question import Picture, Question
|
17
|
+
from excel2moodle.core.settings import Settings, SettingsKey
|
20
18
|
from excel2moodle.logger import LogAdapterQuestionID
|
21
|
-
from excel2moodle.ui.settings import Settings, SettingsKey
|
22
19
|
|
23
20
|
loggerObj = logging.getLogger(__name__)
|
24
21
|
|
25
|
-
settings = Settings()
|
26
|
-
|
27
22
|
|
28
23
|
class QuestionParser:
|
29
24
|
"""Setup the Parser Object.
|
@@ -32,42 +27,42 @@ class QuestionParser:
|
|
32
27
|
Important to implement the answers methods.
|
33
28
|
"""
|
34
29
|
|
35
|
-
|
30
|
+
settings = Settings()
|
31
|
+
|
32
|
+
def __init__(self) -> None:
|
36
33
|
"""Initialize the general Question parser."""
|
34
|
+
self.genFeedbacks: list[XMLTags] = []
|
35
|
+
self.logger: logging.LoggerAdapter
|
36
|
+
|
37
|
+
def setup(self, question: Question) -> None:
|
37
38
|
self.question: Question = question
|
38
|
-
self.rawInput =
|
39
|
+
self.rawInput = question.rawData
|
39
40
|
self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.question.id})
|
40
41
|
self.logger.debug(
|
41
42
|
"The following Data was provided: %s",
|
42
43
|
self.rawInput,
|
43
44
|
)
|
44
|
-
self.genFeedbacks: list[XMLTags] = []
|
45
45
|
|
46
46
|
def hasPicture(self) -> bool:
|
47
47
|
"""Create a ``Picture`` object ``question``if the question needs a pic."""
|
48
48
|
if hasattr(self, "picture") and self.question.picture.ready:
|
49
49
|
return True
|
50
|
-
picKey = self.rawInput
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
return True
|
57
|
-
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)
|
58
56
|
|
59
57
|
def setMainText(self) -> None:
|
60
58
|
paragraphs: list[ET._Element] = [TextElements.PLEFT.create()]
|
61
59
|
ET.SubElement(paragraphs[0], "b").text = f"ID {self.question.id}"
|
62
60
|
text = self.rawInput[DFIndex.TEXT]
|
63
|
-
pcount = 0
|
64
61
|
for t in text:
|
65
|
-
|
66
|
-
|
67
|
-
paragraphs.append(TextElements.PLEFT.create())
|
68
|
-
paragraphs[-1].text = t
|
62
|
+
paragraphs.append(TextElements.PLEFT.create())
|
63
|
+
paragraphs[-1].text = t
|
69
64
|
self.question.qtextParagraphs = paragraphs
|
70
|
-
self.logger.debug("Created main Text with: %s paragraphs",
|
65
|
+
self.logger.debug("Created main Text with: %s paragraphs", len(text))
|
71
66
|
|
72
67
|
def setBPoints(self) -> None:
|
73
68
|
"""If there bulletPoints are set in the Spreadsheet it creates an unordered List-Element in ``Question.bulletList``."""
|
@@ -75,12 +70,12 @@ class QuestionParser:
|
|
75
70
|
bps: str = self.rawInput[DFIndex.BPOINTS]
|
76
71
|
try:
|
77
72
|
bulletList = self.formatBulletList(bps)
|
78
|
-
except IndexError
|
73
|
+
except IndexError:
|
79
74
|
msg = f"konnt Bullet Liste {self.question.id} nicht generieren"
|
80
75
|
raise QNotParsedException(
|
81
76
|
msg,
|
82
77
|
self.question.id,
|
83
|
-
exc_info=e,
|
78
|
+
# exc_info=e,
|
84
79
|
)
|
85
80
|
self.logger.debug(
|
86
81
|
"Generated BPoint List: \n %s",
|
@@ -90,7 +85,7 @@ class QuestionParser:
|
|
90
85
|
|
91
86
|
def formatBulletList(self, bps: str) -> ET.Element:
|
92
87
|
self.logger.debug("Formatting the bulletpoint list")
|
93
|
-
li: list[str] = stringHelpers.
|
88
|
+
li: list[str] = stringHelpers.getListFromStr(bps)
|
94
89
|
name = []
|
95
90
|
var = []
|
96
91
|
quant = []
|
@@ -138,24 +133,12 @@ class QuestionParser:
|
|
138
133
|
elif txtEle is True:
|
139
134
|
self.tmpEle.append(eth.getTextElement(eleName, t, **attribs))
|
140
135
|
|
141
|
-
def
|
142
|
-
"""
|
143
|
-
|
144
|
-
|
145
|
-
parser.append("MCParser")
|
146
|
-
elif isinstance(self, NFQuestionParser):
|
147
|
-
parser.append("NFParser")
|
148
|
-
for p in parser:
|
149
|
-
try:
|
150
|
-
for k, v in parserSettings[p][key].items():
|
151
|
-
self.appendToTmpEle(k, text=v)
|
152
|
-
except KeyError as e:
|
153
|
-
msg = f"Invalider Input aus den Einstellungen Parser: {
|
154
|
-
type(p) =}"
|
155
|
-
self.logger.exception(msg, exc_info=e)
|
156
|
-
raise QNotParsedException(msg, self.question.id, exc_info=e)
|
136
|
+
def _appendStandardTags(self) -> None:
|
137
|
+
"""Append the elements defined in the ``cls.standardTags``."""
|
138
|
+
for k, v in type(self.question).standardTags.items():
|
139
|
+
self.appendToTmpEle(k, text=v)
|
157
140
|
|
158
|
-
def parse(self
|
141
|
+
def parse(self) -> None:
|
159
142
|
"""Parse the Question.
|
160
143
|
|
161
144
|
Generates an new Question Element stored as ``self.tmpEle:ET.Element``
|
@@ -163,7 +146,6 @@ class QuestionParser:
|
|
163
146
|
"""
|
164
147
|
self.logger.info("Starting to parse")
|
165
148
|
self.tmpEle = ET.Element(XMLTags.QUESTION, type=self.question.moodleType)
|
166
|
-
# self.tmpEle.set(XMLTags.TYPE, self.question.moodleType)
|
167
149
|
self.appendToTmpEle(XMLTags.NAME, text=DFIndex.NAME, txtEle=True)
|
168
150
|
self.appendToTmpEle(XMLTags.ID, text=self.question.id)
|
169
151
|
if self.hasPicture():
|
@@ -171,11 +153,9 @@ class QuestionParser:
|
|
171
153
|
self.tmpEle.append(ET.Element(XMLTags.QTEXT, format="html"))
|
172
154
|
self.appendToTmpEle(XMLTags.POINTS, text=str(self.question.points))
|
173
155
|
self.appendToTmpEle(XMLTags.PENALTY, text="0.3333")
|
174
|
-
self.
|
156
|
+
self._appendStandardTags()
|
175
157
|
for feedb in self.genFeedbacks:
|
176
158
|
self.tmpEle.append(eth.getFeedBEle(feedb))
|
177
|
-
if xmlTree is not None:
|
178
|
-
xmlTree.append(self.tmpEle)
|
179
159
|
ansList = self.setAnswers()
|
180
160
|
self.setMainText()
|
181
161
|
self.setBPoints()
|
@@ -208,7 +188,6 @@ class QuestionParser:
|
|
208
188
|
def getNumericAnsElement(
|
209
189
|
self,
|
210
190
|
result: float,
|
211
|
-
tolerance: float = 0,
|
212
191
|
fraction: float = 100,
|
213
192
|
format: str = "moodle_auto_format",
|
214
193
|
) -> ET.Element:
|
@@ -232,164 +211,15 @@ class QuestionParser:
|
|
232
211
|
TextElements.SPANGREEN,
|
233
212
|
),
|
234
213
|
)
|
235
|
-
tolerance = self.
|
236
|
-
|
237
|
-
|
238
|
-
return ansEle
|
239
|
-
|
240
|
-
def getTolerancePercent(self, tolerance: float) -> int:
|
241
|
-
"""Get the correct tolerance.
|
242
|
-
If ``tolerance < 1``: it is interpreted as the fraction.
|
243
|
-
If ``tolerance >= 1``: it is interpreted as percentage.
|
244
|
-
"""
|
245
|
-
if tolerance == 0 or pd.isna(tolerance) or tolerance >= 100:
|
246
|
-
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)
|
247
217
|
self.logger.info(
|
248
218
|
"Using default tolerance %s percent from settings",
|
249
219
|
tolerance,
|
250
220
|
)
|
251
|
-
|
252
|
-
self.logger.debug("Using tolerance %s percent",
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
class NFQuestionParser(QuestionParser):
|
257
|
-
"""Subclass for parsing numeric questions."""
|
258
|
-
|
259
|
-
def __init__(self, *args) -> None:
|
260
|
-
super().__init__(*args)
|
261
|
-
self.genFeedbacks = [XMLTags.GENFEEDB]
|
262
|
-
|
263
|
-
def setAnswers(self) -> list[ET.Element]:
|
264
|
-
result = self.rawInput[DFIndex.RESULT]
|
265
|
-
ansEle: list[ET.Element] = []
|
266
|
-
tol = self.rawInput[DFIndex.TOLERANCE]
|
267
|
-
ansEle.append(self.getNumericAnsElement(result=result, tolerance=tol))
|
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)))
|
268
225
|
return ansEle
|
269
|
-
|
270
|
-
|
271
|
-
class NFMQuestionParser(QuestionParser):
|
272
|
-
def __init__(self, *args) -> None:
|
273
|
-
super().__init__(*args)
|
274
|
-
self.genFeedbacks = [XMLTags.GENFEEDB]
|
275
|
-
self.astEval = Interpreter()
|
276
|
-
|
277
|
-
def setAnswers(self) -> None:
|
278
|
-
equation = self.rawInput[DFIndex.RESULT]
|
279
|
-
bps = str(self.rawInput[DFIndex.BPOINTS])
|
280
|
-
ansElementsList: list[ET.Element] = []
|
281
|
-
varNames: list[str] = self._getVarsList(bps)
|
282
|
-
self.question.variables, number = self._getVariablesDict(varNames)
|
283
|
-
for n in range(number):
|
284
|
-
self._setupAstIntprt(self.question.variables, n)
|
285
|
-
result = self.astEval(equation)
|
286
|
-
if isinstance(result, float):
|
287
|
-
tol = self.rawInput[DFIndex.TOLERANCE]
|
288
|
-
ansElementsList.append(
|
289
|
-
self.getNumericAnsElement(result=round(result, 3), tolerance=tol),
|
290
|
-
)
|
291
|
-
self.question.answerVariants = ansElementsList
|
292
|
-
self.setVariants(len(ansElementsList))
|
293
|
-
|
294
|
-
def setVariants(self, number: int) -> None:
|
295
|
-
self.question.variants = number
|
296
|
-
mvar = self.question.category.maxVariants
|
297
|
-
if mvar is None:
|
298
|
-
self.question.category.maxVariants = number
|
299
|
-
else:
|
300
|
-
self.question.category.maxVariants = min(number, mvar)
|
301
|
-
|
302
|
-
def _setupAstIntprt(self, var: dict[str, list[float | int]], index: int) -> None:
|
303
|
-
"""Setup the asteval Interpreter with the variables."""
|
304
|
-
for name, value in var.items():
|
305
|
-
self.astEval.symtable[name] = value[index]
|
306
|
-
|
307
|
-
def _getVariablesDict(self, keyList: list) -> tuple[dict[str, list[float]], int]:
|
308
|
-
"""Liest alle Variablen-Listen deren Name in ``keyList`` ist aus dem DataFrame im Column[index]."""
|
309
|
-
dic: dict = {}
|
310
|
-
num: int = 0
|
311
|
-
for k in keyList:
|
312
|
-
val = self.rawInput[k]
|
313
|
-
if isinstance(val, str):
|
314
|
-
li = stringHelpers.stripWhitespace(val.split(";"))
|
315
|
-
num = len(li)
|
316
|
-
vars: list[float] = [float(i.replace(",", ".")) for i in li]
|
317
|
-
dic[str(k)] = vars
|
318
|
-
else:
|
319
|
-
dic[str(k)] = [str(val)]
|
320
|
-
num = 1
|
321
|
-
return dic, num
|
322
|
-
|
323
|
-
@staticmethod
|
324
|
-
def _getVarsList(bps: str | list[str]) -> list:
|
325
|
-
"""Durchsucht den bulletPoints String nach den Variablen, die als "{var}" gekennzeichnet sind."""
|
326
|
-
vars = []
|
327
|
-
if isinstance(bps, list):
|
328
|
-
for _p in bps:
|
329
|
-
vars.extend(re.findall(r"\{\w\}", str(bps)))
|
330
|
-
else:
|
331
|
-
vars = re.findall(r"\{\w\}", str(bps))
|
332
|
-
variablen = []
|
333
|
-
for v in vars:
|
334
|
-
variablen.append(v.strip("{}"))
|
335
|
-
return variablen
|
336
|
-
|
337
|
-
|
338
|
-
class MCQuestionParser(QuestionParser):
|
339
|
-
def __init__(self, *args) -> None:
|
340
|
-
super().__init__(*args)
|
341
|
-
self.genFeedbacks = [
|
342
|
-
XMLTags.CORFEEDB,
|
343
|
-
XMLTags.PCORFEEDB,
|
344
|
-
XMLTags.INCORFEEDB,
|
345
|
-
]
|
346
|
-
|
347
|
-
def getAnsElementsList(
|
348
|
-
self,
|
349
|
-
answerList: list,
|
350
|
-
fraction: float = 50,
|
351
|
-
format="html",
|
352
|
-
) -> list[ET.Element]:
|
353
|
-
elementList: list[ET.Element] = []
|
354
|
-
for ans in answerList:
|
355
|
-
p = TextElements.PLEFT.create()
|
356
|
-
p.text = str(ans)
|
357
|
-
text = eth.getCdatTxtElement(p)
|
358
|
-
elementList.append(
|
359
|
-
ET.Element(XMLTags.ANSWER, fraction=str(fraction), format=format),
|
360
|
-
)
|
361
|
-
elementList[-1].append(text)
|
362
|
-
if fraction < 0:
|
363
|
-
elementList[-1].append(
|
364
|
-
eth.getFeedBEle(
|
365
|
-
XMLTags.ANSFEEDBACK,
|
366
|
-
text=feedbackStr["wrong"],
|
367
|
-
style=TextElements.SPANRED,
|
368
|
-
),
|
369
|
-
)
|
370
|
-
elif fraction > 0:
|
371
|
-
elementList[-1].append(
|
372
|
-
eth.getFeedBEle(
|
373
|
-
XMLTags.ANSFEEDBACK,
|
374
|
-
text=feedbackStr["right"],
|
375
|
-
style=TextElements.SPANGREEN,
|
376
|
-
),
|
377
|
-
)
|
378
|
-
return elementList
|
379
|
-
|
380
|
-
def setAnswers(self) -> list[ET.Element]:
|
381
|
-
ansStyle = self.rawInput[DFIndex.ANSTYPE]
|
382
|
-
true = stringHelpers.stripWhitespace(self.rawInput[DFIndex.TRUE].split(";"))
|
383
|
-
trueAnsList = stringHelpers.texWrapper(true, style=ansStyle)
|
384
|
-
self.logger.debug(f"got the following true answers \n {trueAnsList=}")
|
385
|
-
false = stringHelpers.stripWhitespace(self.rawInput[DFIndex.FALSE].split(";"))
|
386
|
-
falseAnsList = stringHelpers.texWrapper(false, style=ansStyle)
|
387
|
-
self.logger.debug(f"got the following false answers \n {falseAnsList=}")
|
388
|
-
truefrac = 1 / len(trueAnsList) * 100
|
389
|
-
falsefrac = 1 / len(trueAnsList) * (-100)
|
390
|
-
self.tmpEle.find(XMLTags.PENALTY).text = str(round(truefrac / 100, 4))
|
391
|
-
ansList = self.getAnsElementsList(trueAnsList, fraction=round(truefrac, 4))
|
392
|
-
ansList.extend(
|
393
|
-
self.getAnsElementsList(falseAnsList, fraction=round(falsefrac, 4)),
|
394
|
-
)
|
395
|
-
return ansList
|
excel2moodle/core/question.py
CHANGED
@@ -1,40 +1,60 @@
|
|
1
1
|
import base64
|
2
2
|
import logging
|
3
3
|
import re
|
4
|
+
import typing
|
4
5
|
from pathlib import Path
|
5
6
|
from re import Match
|
6
7
|
|
7
8
|
import lxml.etree as ET
|
8
9
|
|
9
10
|
from excel2moodle.core import etHelpers
|
11
|
+
from excel2moodle.core.category import Category
|
10
12
|
from excel2moodle.core.exceptions import QNotParsedException
|
11
13
|
from excel2moodle.core.globals import (
|
14
|
+
DFIndex,
|
12
15
|
TextElements,
|
13
16
|
XMLTags,
|
14
17
|
questionTypes,
|
15
18
|
)
|
19
|
+
from excel2moodle.core.settings import Settings, SettingsKey
|
16
20
|
from excel2moodle.logger import LogAdapterQuestionID
|
17
21
|
|
18
22
|
loggerObj = logging.getLogger(__name__)
|
23
|
+
settings = Settings()
|
19
24
|
|
20
25
|
|
21
26
|
class Question:
|
27
|
+
standardTags: typing.ClassVar[dict[str, str | float]] = {
|
28
|
+
"hidden": 0,
|
29
|
+
}
|
30
|
+
|
31
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
32
|
+
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
|
38
|
+
|
39
|
+
@classmethod
|
40
|
+
def addStandardTags(cls, key, value) -> None:
|
41
|
+
cls.standardTags[key] = value
|
42
|
+
|
22
43
|
def __init__(
|
23
44
|
self,
|
24
|
-
category,
|
25
|
-
|
26
|
-
number: int,
|
45
|
+
category: Category,
|
46
|
+
rawData: dict[str, float | str | int | list[str]],
|
27
47
|
parent=None,
|
28
|
-
qtype: str = "type",
|
29
48
|
points: float = 0,
|
30
49
|
) -> None:
|
50
|
+
self.rawData = rawData
|
31
51
|
self.category = category
|
32
52
|
self.katName = self.category.name
|
33
|
-
self.name =
|
34
|
-
self.number =
|
53
|
+
self.name: str = self.rawData.get(DFIndex.NAME)
|
54
|
+
self.number: int = self.rawData.get(DFIndex.NUMBER)
|
35
55
|
self.parent = parent
|
36
|
-
self.qtype: str =
|
37
|
-
self.moodleType = questionTypes[qtype]
|
56
|
+
self.qtype: str = self.rawData.get(DFIndex.TYPE)
|
57
|
+
self.moodleType = questionTypes[self.qtype]
|
38
58
|
self.points = points if points != 0 else self.category.points
|
39
59
|
self.element: ET.Element | None = None
|
40
60
|
self.picture: Picture
|
@@ -45,21 +65,20 @@ class Question:
|
|
45
65
|
self.variants: int | None = None
|
46
66
|
self.variables: dict[str, list[float | int]] = {}
|
47
67
|
self.setID()
|
48
|
-
self.standardTags = {"hidden": "false"}
|
49
68
|
self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.id})
|
50
69
|
self.logger.debug("Sucess initializing")
|
51
70
|
|
52
71
|
def __repr__(self) -> str:
|
53
72
|
li: list[str] = []
|
54
|
-
li.append(f"Question v{self.
|
55
|
-
li.append(f"{self.
|
73
|
+
li.append(f"Question v{self.id}")
|
74
|
+
li.append(f"{self.qtype}")
|
56
75
|
li.append(f"{self.parent=}")
|
57
|
-
return "\
|
76
|
+
return "\t".join(li)
|
58
77
|
|
59
78
|
def assemble(self, variant: int = 1) -> None:
|
60
79
|
textElements: list[ET.Element] = []
|
61
80
|
textElements.extend(self.qtextParagraphs)
|
62
|
-
self.logger.debug("Starting assembly")
|
81
|
+
self.logger.debug("Starting assembly, (variant %s)", variant)
|
63
82
|
if self.element is not None:
|
64
83
|
mainText = self.element.find(XMLTags.QTEXT)
|
65
84
|
self.logger.debug(f"found existing Text in element {mainText=}")
|
@@ -78,7 +97,6 @@ class Question:
|
|
78
97
|
textElements.append(self.picture.htmlTag)
|
79
98
|
mainText.append(self.picture.element)
|
80
99
|
mainText.append(etHelpers.getCdatTxtElement(textElements))
|
81
|
-
# self.element.insert(3, mainText)
|
82
100
|
self.logger.debug("inserted MainText to element")
|
83
101
|
if len(self.answerVariants) > 0:
|
84
102
|
ans = self.element.find(XMLTags.ANSWER)
|
@@ -117,61 +135,94 @@ class Question:
|
|
117
135
|
|
118
136
|
|
119
137
|
class Picture:
|
120
|
-
def __init__(
|
121
|
-
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)}
|
122
145
|
self.ready: bool = False
|
123
|
-
self.
|
124
|
-
self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.question.id})
|
125
|
-
self.imgFolder = (imgFolder / question.katName).resolve()
|
146
|
+
self.imgFolder = imgFolder
|
126
147
|
self.htmlTag: ET.Element
|
127
148
|
self.path: Path
|
128
|
-
self.
|
129
|
-
|
149
|
+
self.questionId: str = questionId
|
150
|
+
self.logger.debug("Instantiating a new picture in %s", picKey)
|
151
|
+
if self.getImgId(picKey):
|
130
152
|
self.ready = self.__getImg()
|
131
|
-
|
132
|
-
def _setPath(self) -> None:
|
133
|
-
if self.pic == 1:
|
134
|
-
self.picID = self.question.id
|
135
153
|
else:
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
149
190
|
|
150
|
-
def
|
151
|
-
""
|
152
|
-
|
153
|
-
self.element: ET.Element = ET.Element(
|
154
|
-
"file",
|
155
|
-
name=f"{self.path.name}",
|
156
|
-
path="/",
|
157
|
-
encoding="base64",
|
158
|
-
)
|
159
|
-
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")
|
160
194
|
|
161
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)
|
162
203
|
try:
|
163
|
-
self.
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
alt=f"Bild {self.path.name}",
|
168
|
-
width="500",
|
169
|
-
)
|
170
|
-
return True
|
171
|
-
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)
|
172
208
|
self.logger.warning(
|
173
209
|
msg=f"Bild {self.picID} konnte nicht gefunden werden ",
|
174
210
|
exc_info=e,
|
175
211
|
)
|
176
212
|
self.element = None
|
177
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
|