excel2moodle 0.3.4__py3-none-any.whl → 0.3.5__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 +16 -83
- excel2moodle/core/__init__.py +1 -3
- excel2moodle/core/category.py +32 -32
- excel2moodle/core/dataStructure.py +34 -50
- excel2moodle/core/etHelpers.py +12 -17
- excel2moodle/core/exceptions.py +1 -1
- excel2moodle/core/globals.py +2 -1
- excel2moodle/core/numericMultiQ.py +11 -12
- excel2moodle/core/parser.py +121 -102
- excel2moodle/core/question.py +32 -30
- excel2moodle/core/questionValidator.py +28 -19
- excel2moodle/core/questionWriter.py +232 -139
- excel2moodle/core/stringHelpers.py +16 -23
- excel2moodle/extra/equationVerification.py +13 -27
- excel2moodle/logger.py +102 -0
- excel2moodle/ui/appUi.py +76 -91
- excel2moodle/ui/dialogs.py +40 -4
- excel2moodle/ui/settings.py +104 -54
- excel2moodle/ui/treewidget.py +13 -10
- excel2moodle/ui/windowMain.py +18 -57
- {excel2moodle-0.3.4.dist-info → excel2moodle-0.3.5.dist-info}/METADATA +1 -1
- excel2moodle-0.3.5.dist-info/RECORD +33 -0
- {excel2moodle-0.3.4.dist-info → excel2moodle-0.3.5.dist-info}/WHEEL +1 -1
- excel2moodle-0.3.4.dist-info/RECORD +0 -32
- {excel2moodle-0.3.4.dist-info → excel2moodle-0.3.5.dist-info}/entry_points.txt +0 -0
- {excel2moodle-0.3.4.dist-info → excel2moodle-0.3.5.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.3.4.dist-info → excel2moodle-0.3.5.dist-info}/top_level.txt +0 -0
excel2moodle/core/parser.py
CHANGED
@@ -1,44 +1,55 @@
|
|
1
|
-
import
|
2
|
-
import
|
3
|
-
import re as re
|
4
|
-
from pathlib import Path
|
1
|
+
import logging
|
2
|
+
import re
|
5
3
|
|
6
4
|
import lxml.etree as ET
|
7
5
|
import pandas as pd
|
8
6
|
from asteval import Interpreter
|
9
7
|
|
10
8
|
import excel2moodle.core.etHelpers as eth
|
11
|
-
from excel2moodle import settings
|
12
9
|
from excel2moodle.core import stringHelpers
|
13
|
-
from excel2moodle.core.exceptions import
|
14
|
-
from excel2moodle.core.globals import (
|
15
|
-
|
16
|
-
|
10
|
+
from excel2moodle.core.exceptions import QNotParsedException
|
11
|
+
from excel2moodle.core.globals import (
|
12
|
+
DFIndex,
|
13
|
+
TextElements,
|
14
|
+
XMLTags,
|
15
|
+
feedbackStr,
|
16
|
+
feedBElements,
|
17
|
+
parserSettings,
|
18
|
+
)
|
17
19
|
from excel2moodle.core.question import Picture, Question
|
20
|
+
from excel2moodle.logger import LogAdapterQuestionID
|
21
|
+
from excel2moodle.ui.settings import Settings, SettingsKey
|
18
22
|
|
19
|
-
|
23
|
+
loggerObj = logging.getLogger(__name__)
|
24
|
+
|
25
|
+
settings = Settings()
|
20
26
|
|
21
27
|
|
22
28
|
class QuestionParser:
|
23
|
-
|
29
|
+
"""Setup the Parser Object.
|
30
|
+
|
31
|
+
This is the superclass which implements the general Behaviour of he Parser.
|
32
|
+
Important to implement the answers methods.
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(self, question: Question, data: dict) -> None:
|
36
|
+
"""Initialize the general Question parser."""
|
24
37
|
self.question: Question = question
|
25
38
|
self.rawInput = data
|
26
|
-
logger.
|
27
|
-
|
28
|
-
|
39
|
+
self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.question.id})
|
40
|
+
self.logger.debug(
|
41
|
+
"The following Data was provided: %s",
|
42
|
+
self.rawInput,
|
29
43
|
)
|
30
44
|
self.genFeedbacks: list[XMLTags] = []
|
31
45
|
|
32
46
|
def hasPicture(self) -> bool:
|
33
|
-
"""
|
34
|
-
if
|
35
|
-
|
47
|
+
"""Create a ``Picture`` object ``question``if the question needs a pic."""
|
48
|
+
if hasattr(self, "picture") and self.question.picture.ready:
|
49
|
+
return True
|
36
50
|
picKey = self.rawInput[DFIndex.PICTURE]
|
37
|
-
svgFolder = settings.get(
|
38
|
-
|
39
|
-
default=Path("../Fragensammlung/Abbildungen_SVG").resolve(),
|
40
|
-
)
|
41
|
-
if picKey != 0 and picKey != "nan":
|
51
|
+
svgFolder = settings.get(SettingsKey.PICTUREFOLDER)
|
52
|
+
if picKey != 0 and pd.notna(picKey):
|
42
53
|
if not hasattr(self.question, "picture"):
|
43
54
|
self.question.picture = Picture(picKey, svgFolder, self.question)
|
44
55
|
if self.question.picture.ready:
|
@@ -56,33 +67,29 @@ class QuestionParser:
|
|
56
67
|
paragraphs.append(TextElements.PLEFT.create())
|
57
68
|
paragraphs[-1].text = t
|
58
69
|
self.question.qtextParagraphs = paragraphs
|
59
|
-
logger.debug(
|
60
|
-
f"Created main Text {
|
61
|
-
self.question.id} with:{pcount} paragraphs"
|
62
|
-
)
|
63
|
-
return None
|
70
|
+
self.logger.debug("Created main Text with: %s paragraphs", pcount)
|
64
71
|
|
65
72
|
def setBPoints(self) -> None:
|
66
|
-
"""If there bulletPoints are set in the Spreadsheet it creates an unordered List-Element in ``Question.bulletList
|
73
|
+
"""If there bulletPoints are set in the Spreadsheet it creates an unordered List-Element in ``Question.bulletList``."""
|
67
74
|
if DFIndex.BPOINTS in self.rawInput:
|
68
75
|
bps: str = self.rawInput[DFIndex.BPOINTS]
|
69
76
|
try:
|
70
77
|
bulletList = self.formatBulletList(bps)
|
71
78
|
except IndexError as e:
|
79
|
+
msg = f"konnt Bullet Liste {self.question.id} nicht generieren"
|
72
80
|
raise QNotParsedException(
|
73
|
-
|
81
|
+
msg,
|
74
82
|
self.question.id,
|
75
83
|
exc_info=e,
|
76
84
|
)
|
77
|
-
logger.debug(
|
78
|
-
|
79
|
-
|
85
|
+
self.logger.debug(
|
86
|
+
"Generated BPoint List: \n %s",
|
87
|
+
ET.tostring(bulletList, encoding="unicode"),
|
80
88
|
)
|
81
89
|
self.question.bulletList = bulletList
|
82
|
-
return None
|
83
90
|
|
84
91
|
def formatBulletList(self, bps: str) -> ET.Element:
|
85
|
-
logger.debug("Formatting the bulletpoint list")
|
92
|
+
self.logger.debug("Formatting the bulletpoint list")
|
86
93
|
li: list[str] = stringHelpers.stripWhitespace(bps.split(";"))
|
87
94
|
name = []
|
88
95
|
var = []
|
@@ -95,27 +102,36 @@ class QuestionParser:
|
|
95
102
|
var.append(sc_split[1])
|
96
103
|
quant.append(sc_split[3])
|
97
104
|
unit.append(sc_split[4])
|
98
|
-
for i in range(
|
105
|
+
for i in range(len(name)):
|
99
106
|
if re.fullmatch(r"{\w+}", quant[i]):
|
100
|
-
logger.debug(
|
107
|
+
self.logger.debug("Got an variable bulletItem")
|
101
108
|
num_s = quant[i]
|
102
109
|
else:
|
103
|
-
logger.debug(
|
110
|
+
self.logger.debug("Got a normal bulletItem")
|
104
111
|
num = quant[i].split(",")
|
105
112
|
if len(num) == 2:
|
106
|
-
num_s = f"{
|
113
|
+
num_s = f"{num[0]!s},\\!{num[1]!s}~"
|
107
114
|
else:
|
108
|
-
num_s = f"{
|
115
|
+
num_s = f"{num[0]!s},\\!0~"
|
109
116
|
bullet = TextElements.LISTITEM.create()
|
110
|
-
bullet.text =
|
111
|
-
num_s} \\mathrm{{ {unit[i]} }}\\)\n"
|
117
|
+
bullet.text = (
|
118
|
+
f"{name[i]}: \\( {var[i]} = {num_s} \\mathrm{{ {unit[i]} }}\\)\n"
|
119
|
+
)
|
112
120
|
unorderedList.append(bullet)
|
113
121
|
return unorderedList
|
114
122
|
|
115
123
|
def appendToTmpEle(
|
116
|
-
self,
|
117
|
-
|
118
|
-
|
124
|
+
self,
|
125
|
+
eleName: str,
|
126
|
+
text: str | DFIndex,
|
127
|
+
txtEle=False,
|
128
|
+
**attribs,
|
129
|
+
) -> None:
|
130
|
+
"""Append ``text`` to the temporary Element.
|
131
|
+
|
132
|
+
It uses the data from ``self.rawInput`` if ``text`` is type``DFIndex``
|
133
|
+
Otherwise the value of ``text`` will be inserted.
|
134
|
+
"""
|
119
135
|
t = self.rawInput[text] if isinstance(text, DFIndex) else text
|
120
136
|
if txtEle is False:
|
121
137
|
self.tmpEle.append(eth.getElement(eleName, t, **attribs))
|
@@ -123,7 +139,7 @@ class QuestionParser:
|
|
123
139
|
self.tmpEle.append(eth.getTextElement(eleName, t, **attribs))
|
124
140
|
|
125
141
|
def appendFromSettings(self, key="standards") -> None:
|
126
|
-
"""Appends 1 to 1 mapped Elements defined in the parserSettings to the element"""
|
142
|
+
"""Appends 1 to 1 mapped Elements defined in the parserSettings to the element."""
|
127
143
|
parser = ["Parser"]
|
128
144
|
if isinstance(self, MCQuestionParser):
|
129
145
|
parser.append("MCParser")
|
@@ -136,17 +152,16 @@ class QuestionParser:
|
|
136
152
|
except KeyError as e:
|
137
153
|
msg = f"Invalider Input aus den Einstellungen Parser: {
|
138
154
|
type(p) =}"
|
139
|
-
logger.
|
155
|
+
self.logger.exception(msg, exc_info=e)
|
140
156
|
raise QNotParsedException(msg, self.question.id, exc_info=e)
|
141
|
-
return None
|
142
157
|
|
143
158
|
def parse(self, xmlTree: ET._Element | None = None) -> None:
|
144
|
-
"""
|
159
|
+
"""Parse the Question.
|
145
160
|
|
146
161
|
Generates an new Question Element stored as ``self.tmpEle:ET.Element``
|
147
162
|
if no Exceptions are raised, ``self.tmpEle`` is passed to ``self.question.element``
|
148
163
|
"""
|
149
|
-
logger.info(
|
164
|
+
self.logger.info("Starting to parse")
|
150
165
|
self.tmpEle = ET.Element(XMLTags.QUESTION, type=self.question.moodleType)
|
151
166
|
# self.tmpEle.set(XMLTags.TYPE, self.question.moodleType)
|
152
167
|
self.appendToTmpEle(XMLTags.NAME, text=DFIndex.NAME, txtEle=True)
|
@@ -167,9 +182,8 @@ class QuestionParser:
|
|
167
182
|
if ansList is not None:
|
168
183
|
for ele in ansList:
|
169
184
|
self.tmpEle.append(ele)
|
170
|
-
logger.info(
|
185
|
+
self.logger.info("Sucessfully parsed")
|
171
186
|
self.question.element = self.tmpEle
|
172
|
-
return None
|
173
187
|
|
174
188
|
def getFeedBEle(
|
175
189
|
self,
|
@@ -177,10 +191,7 @@ class QuestionParser:
|
|
177
191
|
text: str | None = None,
|
178
192
|
style: TextElements | None = None,
|
179
193
|
) -> ET.Element:
|
180
|
-
if style is None
|
181
|
-
span = feedBElements[feedback]
|
182
|
-
else:
|
183
|
-
span = style.create()
|
194
|
+
span = feedBElements[feedback] if style is None else style.create()
|
184
195
|
if text is None:
|
185
196
|
text = feedbackStr[feedback]
|
186
197
|
ele = ET.Element(feedback, format="html")
|
@@ -191,49 +202,60 @@ class QuestionParser:
|
|
191
202
|
return ele
|
192
203
|
|
193
204
|
def setAnswers(self) -> list[ET.Element] | None:
|
194
|
-
"""Needs to be implemented in the type-specific subclasses"""
|
205
|
+
"""Needs to be implemented in the type-specific subclasses."""
|
195
206
|
return None
|
196
207
|
|
197
|
-
@staticmethod
|
198
208
|
def getNumericAnsElement(
|
199
|
-
|
200
|
-
|
201
|
-
|
209
|
+
self,
|
210
|
+
result: float,
|
211
|
+
tolerance: float = 0,
|
212
|
+
fraction: float = 100,
|
202
213
|
format: str = "moodle_auto_format",
|
203
214
|
) -> ET.Element:
|
204
|
-
"""
|
205
|
-
|
215
|
+
"""Get ``<answer/>`` Element specific for the numerical Question.
|
216
|
+
|
217
|
+
The element contains those children:
|
206
218
|
``<text/>`` which holds the value of the answer
|
207
|
-
``<
|
208
|
-
``<feedback/>`` with general feedback for a true answer
|
219
|
+
``<tolerance/>`` with the *relative* tolerance for the result in percent
|
220
|
+
``<feedback/>`` with general feedback for a true answer.
|
209
221
|
"""
|
210
|
-
|
211
222
|
ansEle: ET.Element = eth.getTextElement(
|
212
|
-
XMLTags.ANSWER,
|
223
|
+
XMLTags.ANSWER,
|
224
|
+
text=str(result),
|
225
|
+
fraction=str(fraction),
|
226
|
+
format=format,
|
213
227
|
)
|
214
228
|
ansEle.append(
|
215
229
|
eth.getFeedBEle(
|
216
230
|
XMLTags.ANSFEEDBACK,
|
217
231
|
feedbackStr["right1Percent"],
|
218
232
|
TextElements.SPANGREEN,
|
219
|
-
)
|
233
|
+
),
|
220
234
|
)
|
221
|
-
|
222
|
-
|
223
|
-
tolerance = int(settings.value(
|
224
|
-
"parser/nf/tolerance"))
|
225
|
-
except ValueError as e:
|
226
|
-
logger.error(
|
227
|
-
f"The tolerance Setting is invalid {e} \n using 1% tolerance",
|
228
|
-
exc_info=e)
|
229
|
-
tolerance = 1
|
230
|
-
logger.debug(f"using tolerance of {tolerance} %")
|
231
|
-
tol = abs(round(result * tolerance, 3))
|
235
|
+
tolerance = self.getTolerancePercent(tolerance)
|
236
|
+
tol = abs(round(result * (tolerance / 100), 3))
|
232
237
|
ansEle.append(eth.getElement(XMLTags.TOLERANCE, text=str(tol)))
|
233
238
|
return ansEle
|
234
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)
|
247
|
+
self.logger.info(
|
248
|
+
"Using default tolerance %s percent from settings",
|
249
|
+
tolerance,
|
250
|
+
)
|
251
|
+
tolerancePercent = 100 * tolerance if tolerance < 1 else tolerance
|
252
|
+
self.logger.debug("Using tolerance %s percent", tolerancePercent)
|
253
|
+
return int(tolerancePercent)
|
254
|
+
|
235
255
|
|
236
256
|
class NFQuestionParser(QuestionParser):
|
257
|
+
"""Subclass for parsing numeric questions."""
|
258
|
+
|
237
259
|
def __init__(self, *args) -> None:
|
238
260
|
super().__init__(*args)
|
239
261
|
self.genFeedbacks = [XMLTags.GENFEEDB]
|
@@ -241,12 +263,13 @@ class NFQuestionParser(QuestionParser):
|
|
241
263
|
def setAnswers(self) -> list[ET.Element]:
|
242
264
|
result = self.rawInput[DFIndex.RESULT]
|
243
265
|
ansEle: list[ET.Element] = []
|
244
|
-
|
266
|
+
tol = self.rawInput[DFIndex.TOLERANCE]
|
267
|
+
ansEle.append(self.getNumericAnsElement(result=result, tolerance=tol))
|
245
268
|
return ansEle
|
246
269
|
|
247
270
|
|
248
271
|
class NFMQuestionParser(QuestionParser):
|
249
|
-
def __init__(self, *args):
|
272
|
+
def __init__(self, *args) -> None:
|
250
273
|
super().__init__(*args)
|
251
274
|
self.genFeedbacks = [XMLTags.GENFEEDB]
|
252
275
|
self.astEval = Interpreter()
|
@@ -261,32 +284,28 @@ class NFMQuestionParser(QuestionParser):
|
|
261
284
|
self._setupAstIntprt(self.question.variables, n)
|
262
285
|
result = self.astEval(equation)
|
263
286
|
if isinstance(result, float):
|
287
|
+
tol = self.rawInput[DFIndex.TOLERANCE]
|
264
288
|
ansElementsList.append(
|
265
|
-
self.getNumericAnsElement(result=round(result, 3))
|
289
|
+
self.getNumericAnsElement(result=round(result, 3), tolerance=tol),
|
266
290
|
)
|
267
291
|
self.question.answerVariants = ansElementsList
|
268
292
|
self.setVariants(len(ansElementsList))
|
269
|
-
return None
|
270
293
|
|
271
|
-
def setVariants(self, number: int):
|
294
|
+
def setVariants(self, number: int) -> None:
|
272
295
|
self.question.variants = number
|
273
296
|
mvar = self.question.category.maxVariants
|
274
297
|
if mvar is None:
|
275
298
|
self.question.category.maxVariants = number
|
276
299
|
else:
|
277
|
-
self.question.category.maxVariants = number
|
300
|
+
self.question.category.maxVariants = min(number, mvar)
|
278
301
|
|
279
302
|
def _setupAstIntprt(self, var: dict[str, list[float | int]], index: int) -> None:
|
280
|
-
"""
|
281
|
-
|
282
|
-
Dann kann dieser die equation lesen.
|
283
|
-
"""
|
303
|
+
"""Setup the asteval Interpreter with the variables."""
|
284
304
|
for name, value in var.items():
|
285
305
|
self.astEval.symtable[name] = value[index]
|
286
|
-
return None
|
287
306
|
|
288
307
|
def _getVariablesDict(self, keyList: list) -> tuple[dict[str, list[float]], int]:
|
289
|
-
"""Liest alle Variablen-Listen deren Name in ``keyList`` ist aus dem DataFrame im Column[index]"""
|
308
|
+
"""Liest alle Variablen-Listen deren Name in ``keyList`` ist aus dem DataFrame im Column[index]."""
|
290
309
|
dic: dict = {}
|
291
310
|
num: int = 0
|
292
311
|
for k in keyList:
|
@@ -299,17 +318,14 @@ class NFMQuestionParser(QuestionParser):
|
|
299
318
|
else:
|
300
319
|
dic[str(k)] = [str(val)]
|
301
320
|
num = 1
|
302
|
-
print(f"Folgende Variablen wurden gefunden:\n{dic}\n")
|
303
321
|
return dic, num
|
304
322
|
|
305
323
|
@staticmethod
|
306
324
|
def _getVarsList(bps: str | list[str]) -> list:
|
307
|
-
"""
|
308
|
-
Durchsucht den bulletPoints String nach den Variablen, die als "{var}" gekennzeichnet sind
|
309
|
-
"""
|
325
|
+
"""Durchsucht den bulletPoints String nach den Variablen, die als "{var}" gekennzeichnet sind."""
|
310
326
|
vars = []
|
311
327
|
if isinstance(bps, list):
|
312
|
-
for
|
328
|
+
for _p in bps:
|
313
329
|
vars.extend(re.findall(r"\{\w\}", str(bps)))
|
314
330
|
else:
|
315
331
|
vars = re.findall(r"\{\w\}", str(bps))
|
@@ -329,7 +345,10 @@ class MCQuestionParser(QuestionParser):
|
|
329
345
|
]
|
330
346
|
|
331
347
|
def getAnsElementsList(
|
332
|
-
self,
|
348
|
+
self,
|
349
|
+
answerList: list,
|
350
|
+
fraction: float = 50,
|
351
|
+
format="html",
|
333
352
|
) -> list[ET.Element]:
|
334
353
|
elementList: list[ET.Element] = []
|
335
354
|
for ans in answerList:
|
@@ -337,7 +356,7 @@ class MCQuestionParser(QuestionParser):
|
|
337
356
|
p.text = str(ans)
|
338
357
|
text = eth.getCdatTxtElement(p)
|
339
358
|
elementList.append(
|
340
|
-
ET.Element(XMLTags.ANSWER, fraction=str(fraction), format=format)
|
359
|
+
ET.Element(XMLTags.ANSWER, fraction=str(fraction), format=format),
|
341
360
|
)
|
342
361
|
elementList[-1].append(text)
|
343
362
|
if fraction < 0:
|
@@ -346,7 +365,7 @@ class MCQuestionParser(QuestionParser):
|
|
346
365
|
XMLTags.ANSFEEDBACK,
|
347
366
|
text=feedbackStr["wrong"],
|
348
367
|
style=TextElements.SPANRED,
|
349
|
-
)
|
368
|
+
),
|
350
369
|
)
|
351
370
|
elif fraction > 0:
|
352
371
|
elementList[-1].append(
|
@@ -354,7 +373,7 @@ class MCQuestionParser(QuestionParser):
|
|
354
373
|
XMLTags.ANSFEEDBACK,
|
355
374
|
text=feedbackStr["right"],
|
356
375
|
style=TextElements.SPANGREEN,
|
357
|
-
)
|
376
|
+
),
|
358
377
|
)
|
359
378
|
return elementList
|
360
379
|
|
@@ -362,15 +381,15 @@ class MCQuestionParser(QuestionParser):
|
|
362
381
|
ansStyle = self.rawInput[DFIndex.ANSTYPE]
|
363
382
|
true = stringHelpers.stripWhitespace(self.rawInput[DFIndex.TRUE].split(";"))
|
364
383
|
trueAnsList = stringHelpers.texWrapper(true, style=ansStyle)
|
365
|
-
logger.debug(f"got the following true answers \n {trueAnsList=}")
|
384
|
+
self.logger.debug(f"got the following true answers \n {trueAnsList=}")
|
366
385
|
false = stringHelpers.stripWhitespace(self.rawInput[DFIndex.FALSE].split(";"))
|
367
386
|
falseAnsList = stringHelpers.texWrapper(false, style=ansStyle)
|
368
|
-
logger.debug(f"got the following false answers \n {falseAnsList=}")
|
387
|
+
self.logger.debug(f"got the following false answers \n {falseAnsList=}")
|
369
388
|
truefrac = 1 / len(trueAnsList) * 100
|
370
389
|
falsefrac = 1 / len(trueAnsList) * (-100)
|
371
390
|
self.tmpEle.find(XMLTags.PENALTY).text = str(round(truefrac / 100, 4))
|
372
391
|
ansList = self.getAnsElementsList(trueAnsList, fraction=round(truefrac, 4))
|
373
392
|
ansList.extend(
|
374
|
-
self.getAnsElementsList(falseAnsList, fraction=round(falsefrac, 4))
|
393
|
+
self.getAnsElementsList(falseAnsList, fraction=round(falsefrac, 4)),
|
375
394
|
)
|
376
395
|
return ansList
|
excel2moodle/core/question.py
CHANGED
@@ -1,22 +1,21 @@
|
|
1
|
-
import base64
|
1
|
+
import base64
|
2
2
|
import logging
|
3
|
-
import re
|
3
|
+
import re
|
4
4
|
from pathlib import Path
|
5
|
-
from
|
5
|
+
from re import Match
|
6
6
|
|
7
7
|
import lxml.etree as ET
|
8
8
|
|
9
9
|
from excel2moodle.core import etHelpers
|
10
10
|
from excel2moodle.core.exceptions import QNotParsedException
|
11
11
|
from excel2moodle.core.globals import (
|
12
|
-
DFIndex,
|
13
12
|
TextElements,
|
14
13
|
XMLTags,
|
15
|
-
parserSettings,
|
16
14
|
questionTypes,
|
17
15
|
)
|
16
|
+
from excel2moodle.logger import LogAdapterQuestionID
|
18
17
|
|
19
|
-
|
18
|
+
loggerObj = logging.getLogger(__name__)
|
20
19
|
|
21
20
|
|
22
21
|
class Question:
|
@@ -28,7 +27,7 @@ class Question:
|
|
28
27
|
parent=None,
|
29
28
|
qtype: str = "type",
|
30
29
|
points: float = 0,
|
31
|
-
):
|
30
|
+
) -> None:
|
32
31
|
self.category = category
|
33
32
|
self.katName = self.category.name
|
34
33
|
self.name = name
|
@@ -47,7 +46,8 @@ class Question:
|
|
47
46
|
self.variables: dict[str, list[float | int]] = {}
|
48
47
|
self.setID()
|
49
48
|
self.standardTags = {"hidden": "false"}
|
50
|
-
logger
|
49
|
+
self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.id})
|
50
|
+
self.logger.debug("Sucess initializing")
|
51
51
|
|
52
52
|
def __repr__(self) -> str:
|
53
53
|
li: list[str] = []
|
@@ -59,16 +59,17 @@ class Question:
|
|
59
59
|
def assemble(self, variant: int = 1) -> None:
|
60
60
|
textElements: list[ET.Element] = []
|
61
61
|
textElements.extend(self.qtextParagraphs)
|
62
|
-
logger.debug(
|
62
|
+
self.logger.debug("Starting assembly")
|
63
63
|
if self.element is not None:
|
64
64
|
mainText = self.element.find(XMLTags.QTEXT)
|
65
|
-
logger.debug(f"found existing Text in element {mainText=}")
|
65
|
+
self.logger.debug(f"found existing Text in element {mainText=}")
|
66
66
|
txtele = mainText.find("text")
|
67
67
|
if txtele is not None:
|
68
68
|
mainText.remove(txtele)
|
69
|
-
logger.debug(
|
69
|
+
self.logger.debug("removed previously existing questiontext")
|
70
70
|
else:
|
71
|
-
|
71
|
+
msg = "Cant assamble, if element is none"
|
72
|
+
raise QNotParsedException(msg, self.id)
|
72
73
|
if self.variants is not None:
|
73
74
|
textElements.append(self.getBPointVariant(variant - 1))
|
74
75
|
elif self.bulletList is not None:
|
@@ -78,14 +79,13 @@ class Question:
|
|
78
79
|
mainText.append(self.picture.element)
|
79
80
|
mainText.append(etHelpers.getCdatTxtElement(textElements))
|
80
81
|
# self.element.insert(3, mainText)
|
81
|
-
logger.debug(
|
82
|
+
self.logger.debug("inserted MainText to element")
|
82
83
|
if len(self.answerVariants) > 0:
|
83
84
|
ans = self.element.find(XMLTags.ANSWER)
|
84
85
|
if ans is not None:
|
85
86
|
self.element.remove(ans)
|
86
|
-
logger.debug("removed previous answer element")
|
87
|
+
self.logger.debug("removed previous answer element")
|
87
88
|
self.element.insert(5, self.answerVariants[variant - 1])
|
88
|
-
return None
|
89
89
|
|
90
90
|
def setID(self, id=0) -> None:
|
91
91
|
if id == 0:
|
@@ -111,16 +111,17 @@ class Question:
|
|
111
111
|
listItemText = li.text or ""
|
112
112
|
bullet = TextElements.LISTITEM.create()
|
113
113
|
bullet.text = varPlaceholder.sub(replaceMatch, listItemText)
|
114
|
-
logger.debug(f"Inserted Variables into List: {bullet}")
|
114
|
+
self.logger.debug(f"Inserted Variables into List: {bullet}")
|
115
115
|
unorderedList.append(bullet)
|
116
116
|
return unorderedList
|
117
117
|
|
118
118
|
|
119
119
|
class Picture:
|
120
|
-
def __init__(self, picKey: str, imgFolder: Path, question: Question):
|
120
|
+
def __init__(self, picKey: str, imgFolder: Path, question: Question) -> None:
|
121
121
|
self.pic = picKey
|
122
122
|
self.ready: bool = False
|
123
123
|
self.question = question
|
124
|
+
self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.question.id})
|
124
125
|
self.imgFolder = (imgFolder / question.katName).resolve()
|
125
126
|
self.htmlTag: ET.Element
|
126
127
|
self.path: Path
|
@@ -128,32 +129,32 @@ class Picture:
|
|
128
129
|
if hasattr(self, "picID"):
|
129
130
|
self.ready = self.__getImg()
|
130
131
|
|
131
|
-
def _setPath(self):
|
132
|
+
def _setPath(self) -> None:
|
132
133
|
if self.pic == 1:
|
133
134
|
self.picID = self.question.id
|
134
135
|
else:
|
135
136
|
selectedPic = self.pic[2:]
|
136
|
-
logger.debug(
|
137
|
+
self.logger.debug("Got the picture key: %s", selectedPic)
|
137
138
|
try:
|
138
|
-
self.picID = f"{self.question.category.id}{
|
139
|
-
int(selectedPic):02d}"
|
139
|
+
self.picID = f"{self.question.category.id}{int(selectedPic):02d}"
|
140
140
|
except ValueError as e:
|
141
|
-
logger.warning(
|
142
|
-
msg=f"Bild-ID konnte aus dem Key: {
|
143
|
-
self.pic=}nicht festgestellt werden",
|
141
|
+
self.logger.warning(
|
142
|
+
msg=f"Bild-ID konnte aus dem Key: {self.pic} nicht festgestellt werden",
|
144
143
|
exc_info=e,
|
145
144
|
)
|
146
145
|
|
147
146
|
def __getBase64Img(self, imgPath):
|
148
147
|
with open(imgPath, "rb") as img:
|
149
|
-
|
150
|
-
return img64
|
148
|
+
return base64.b64encode(img.read()).decode("utf-8")
|
151
149
|
|
152
150
|
def __setImgElement(self, dir: Path, picID: int) -> None:
|
153
|
-
"""
|
151
|
+
"""Gibt das Bild im dirPath mit dir qID als base64 encodiert mit den entsprechenden XML-Tags zurück."""
|
154
152
|
self.path: Path = (dir / str(picID)).with_suffix(".svg")
|
155
153
|
self.element: ET.Element = ET.Element(
|
156
|
-
"file",
|
154
|
+
"file",
|
155
|
+
name=f"{self.path.name}",
|
156
|
+
path="/",
|
157
|
+
encoding="base64",
|
157
158
|
)
|
158
159
|
self.element.text = self.__getBase64Img(self.path)
|
159
160
|
|
@@ -168,8 +169,9 @@ class Picture:
|
|
168
169
|
)
|
169
170
|
return True
|
170
171
|
except FileNotFoundError as e:
|
171
|
-
logger.warning(
|
172
|
-
msg=f"Bild {self.picID} konnte nicht gefunden werden ",
|
172
|
+
self.logger.warning(
|
173
|
+
msg=f"Bild {self.picID} konnte nicht gefunden werden ",
|
174
|
+
exc_info=e,
|
173
175
|
)
|
174
176
|
self.element = None
|
175
177
|
return False
|