excel2moodle 0.3.5__py3-none-any.whl → 0.3.7__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 +3 -9
- excel2moodle/__main__.py +8 -1
- excel2moodle/core/category.py +6 -44
- excel2moodle/core/dataStructure.py +242 -79
- excel2moodle/core/globals.py +0 -20
- excel2moodle/core/parser.py +15 -171
- excel2moodle/core/question.py +30 -13
- excel2moodle/core/stringHelpers.py +8 -32
- excel2moodle/core/{questionValidator.py → validator.py} +26 -31
- excel2moodle/logger.py +0 -1
- excel2moodle/question_types/__init__.py +33 -0
- excel2moodle/question_types/mc.py +93 -0
- excel2moodle/question_types/nf.py +30 -0
- excel2moodle/question_types/nfm.py +92 -0
- excel2moodle/ui/appUi.py +66 -51
- excel2moodle/ui/dialogs.py +8 -7
- excel2moodle/ui/settings.py +100 -36
- {excel2moodle-0.3.5.dist-info → excel2moodle-0.3.7.dist-info}/METADATA +2 -2
- excel2moodle-0.3.7.dist-info/RECORD +37 -0
- {excel2moodle-0.3.5.dist-info → excel2moodle-0.3.7.dist-info}/WHEEL +1 -1
- excel2moodle-0.3.5.dist-info/RECORD +0 -33
- {excel2moodle-0.3.5.dist-info → excel2moodle-0.3.7.dist-info}/entry_points.txt +0 -0
- {excel2moodle-0.3.5.dist-info → excel2moodle-0.3.7.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.3.5.dist-info → excel2moodle-0.3.7.dist-info}/top_level.txt +0 -0
excel2moodle/core/parser.py
CHANGED
@@ -3,7 +3,6 @@ import re
|
|
3
3
|
|
4
4
|
import lxml.etree as ET
|
5
5
|
import pandas as pd
|
6
|
-
from asteval import Interpreter
|
7
6
|
|
8
7
|
import excel2moodle.core.etHelpers as eth
|
9
8
|
from excel2moodle.core import stringHelpers
|
@@ -14,7 +13,6 @@ from excel2moodle.core.globals import (
|
|
14
13
|
XMLTags,
|
15
14
|
feedbackStr,
|
16
15
|
feedBElements,
|
17
|
-
parserSettings,
|
18
16
|
)
|
19
17
|
from excel2moodle.core.question import Picture, Question
|
20
18
|
from excel2moodle.logger import LogAdapterQuestionID
|
@@ -32,16 +30,19 @@ class QuestionParser:
|
|
32
30
|
Important to implement the answers methods.
|
33
31
|
"""
|
34
32
|
|
35
|
-
def __init__(self
|
33
|
+
def __init__(self) -> None:
|
36
34
|
"""Initialize the general Question parser."""
|
35
|
+
self.genFeedbacks: list[XMLTags] = []
|
36
|
+
self.logger: logging.LoggerAdapter
|
37
|
+
|
38
|
+
def setup(self, question: Question) -> None:
|
37
39
|
self.question: Question = question
|
38
|
-
self.rawInput =
|
40
|
+
self.rawInput = question.rawData
|
39
41
|
self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.question.id})
|
40
42
|
self.logger.debug(
|
41
43
|
"The following Data was provided: %s",
|
42
44
|
self.rawInput,
|
43
45
|
)
|
44
|
-
self.genFeedbacks: list[XMLTags] = []
|
45
46
|
|
46
47
|
def hasPicture(self) -> bool:
|
47
48
|
"""Create a ``Picture`` object ``question``if the question needs a pic."""
|
@@ -75,12 +76,12 @@ class QuestionParser:
|
|
75
76
|
bps: str = self.rawInput[DFIndex.BPOINTS]
|
76
77
|
try:
|
77
78
|
bulletList = self.formatBulletList(bps)
|
78
|
-
except IndexError
|
79
|
+
except IndexError:
|
79
80
|
msg = f"konnt Bullet Liste {self.question.id} nicht generieren"
|
80
81
|
raise QNotParsedException(
|
81
82
|
msg,
|
82
83
|
self.question.id,
|
83
|
-
exc_info=e,
|
84
|
+
# exc_info=e,
|
84
85
|
)
|
85
86
|
self.logger.debug(
|
86
87
|
"Generated BPoint List: \n %s",
|
@@ -90,7 +91,7 @@ class QuestionParser:
|
|
90
91
|
|
91
92
|
def formatBulletList(self, bps: str) -> ET.Element:
|
92
93
|
self.logger.debug("Formatting the bulletpoint list")
|
93
|
-
li: list[str] = stringHelpers.
|
94
|
+
li: list[str] = stringHelpers.getListFromStr(bps)
|
94
95
|
name = []
|
95
96
|
var = []
|
96
97
|
quant = []
|
@@ -138,24 +139,12 @@ class QuestionParser:
|
|
138
139
|
elif txtEle is True:
|
139
140
|
self.tmpEle.append(eth.getTextElement(eleName, t, **attribs))
|
140
141
|
|
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)
|
142
|
+
def _appendStandardTags(self) -> None:
|
143
|
+
"""Append the elements defined in the ``cls.standardTags``."""
|
144
|
+
for k, v in type(self.question).standardTags.items():
|
145
|
+
self.appendToTmpEle(k, text=v)
|
157
146
|
|
158
|
-
def parse(self
|
147
|
+
def parse(self) -> None:
|
159
148
|
"""Parse the Question.
|
160
149
|
|
161
150
|
Generates an new Question Element stored as ``self.tmpEle:ET.Element``
|
@@ -163,7 +152,6 @@ class QuestionParser:
|
|
163
152
|
"""
|
164
153
|
self.logger.info("Starting to parse")
|
165
154
|
self.tmpEle = ET.Element(XMLTags.QUESTION, type=self.question.moodleType)
|
166
|
-
# self.tmpEle.set(XMLTags.TYPE, self.question.moodleType)
|
167
155
|
self.appendToTmpEle(XMLTags.NAME, text=DFIndex.NAME, txtEle=True)
|
168
156
|
self.appendToTmpEle(XMLTags.ID, text=self.question.id)
|
169
157
|
if self.hasPicture():
|
@@ -171,11 +159,9 @@ class QuestionParser:
|
|
171
159
|
self.tmpEle.append(ET.Element(XMLTags.QTEXT, format="html"))
|
172
160
|
self.appendToTmpEle(XMLTags.POINTS, text=str(self.question.points))
|
173
161
|
self.appendToTmpEle(XMLTags.PENALTY, text="0.3333")
|
174
|
-
self.
|
162
|
+
self._appendStandardTags()
|
175
163
|
for feedb in self.genFeedbacks:
|
176
164
|
self.tmpEle.append(eth.getFeedBEle(feedb))
|
177
|
-
if xmlTree is not None:
|
178
|
-
xmlTree.append(self.tmpEle)
|
179
165
|
ansList = self.setAnswers()
|
180
166
|
self.setMainText()
|
181
167
|
self.setBPoints()
|
@@ -251,145 +237,3 @@ class QuestionParser:
|
|
251
237
|
tolerancePercent = 100 * tolerance if tolerance < 1 else tolerance
|
252
238
|
self.logger.debug("Using tolerance %s percent", tolerancePercent)
|
253
239
|
return int(tolerancePercent)
|
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))
|
268
|
-
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,14 +1,17 @@
|
|
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,
|
@@ -19,22 +22,37 @@ loggerObj = logging.getLogger(__name__)
|
|
19
22
|
|
20
23
|
|
21
24
|
class Question:
|
25
|
+
standardTags: typing.ClassVar[dict[str, str | float]] = {
|
26
|
+
"hidden": 0,
|
27
|
+
}
|
28
|
+
|
29
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
30
|
+
super().__init_subclass__(**kwargs)
|
31
|
+
subclassTags = getattr(cls, "standartTags", {})
|
32
|
+
superclassTags = super(cls, cls).standardTags
|
33
|
+
mergedTags = superclassTags.copy()
|
34
|
+
mergedTags.update(subclassTags)
|
35
|
+
cls.standardTags = mergedTags
|
36
|
+
|
37
|
+
@classmethod
|
38
|
+
def addStandardTags(cls, key, value) -> None:
|
39
|
+
cls.standardTags[key] = value
|
40
|
+
|
22
41
|
def __init__(
|
23
42
|
self,
|
24
|
-
category,
|
25
|
-
|
26
|
-
number: int,
|
43
|
+
category: Category,
|
44
|
+
rawData: dict[str, float | str | int | list[str]],
|
27
45
|
parent=None,
|
28
|
-
qtype: str = "type",
|
29
46
|
points: float = 0,
|
30
47
|
) -> None:
|
48
|
+
self.rawData = rawData
|
31
49
|
self.category = category
|
32
50
|
self.katName = self.category.name
|
33
|
-
self.name =
|
34
|
-
self.number =
|
51
|
+
self.name: str = self.rawData.get(DFIndex.NAME)
|
52
|
+
self.number: int = self.rawData.get(DFIndex.NUMBER)
|
35
53
|
self.parent = parent
|
36
|
-
self.qtype: str =
|
37
|
-
self.moodleType = questionTypes[qtype]
|
54
|
+
self.qtype: str = self.rawData.get(DFIndex.TYPE)
|
55
|
+
self.moodleType = questionTypes[self.qtype]
|
38
56
|
self.points = points if points != 0 else self.category.points
|
39
57
|
self.element: ET.Element | None = None
|
40
58
|
self.picture: Picture
|
@@ -45,7 +63,6 @@ class Question:
|
|
45
63
|
self.variants: int | None = None
|
46
64
|
self.variables: dict[str, list[float | int]] = {}
|
47
65
|
self.setID()
|
48
|
-
self.standardTags = {"hidden": "false"}
|
49
66
|
self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.id})
|
50
67
|
self.logger.debug("Sucess initializing")
|
51
68
|
|
@@ -143,11 +160,11 @@ class Picture:
|
|
143
160
|
exc_info=e,
|
144
161
|
)
|
145
162
|
|
146
|
-
def
|
163
|
+
def _getBase64Img(self, imgPath):
|
147
164
|
with open(imgPath, "rb") as img:
|
148
165
|
return base64.b64encode(img.read()).decode("utf-8")
|
149
166
|
|
150
|
-
def
|
167
|
+
def _setImgElement(self, dir: Path, picID: int) -> None:
|
151
168
|
"""Gibt das Bild im dirPath mit dir qID als base64 encodiert mit den entsprechenden XML-Tags zurück."""
|
152
169
|
self.path: Path = (dir / str(picID)).with_suffix(".svg")
|
153
170
|
self.element: ET.Element = ET.Element(
|
@@ -156,11 +173,11 @@ class Picture:
|
|
156
173
|
path="/",
|
157
174
|
encoding="base64",
|
158
175
|
)
|
159
|
-
self.element.text = self.
|
176
|
+
self.element.text = self._getBase64Img(self.path)
|
160
177
|
|
161
178
|
def __getImg(self) -> bool:
|
162
179
|
try:
|
163
|
-
self.
|
180
|
+
self._setImgElement(self.imgFolder, int(self.picID))
|
164
181
|
self.htmlTag = ET.Element(
|
165
182
|
"img",
|
166
183
|
src=f"@@PLUGINFILE@@/{self.path.name}",
|
@@ -6,9 +6,11 @@ from pathlib import Path
|
|
6
6
|
import lxml.etree as ET
|
7
7
|
|
8
8
|
|
9
|
-
def
|
10
|
-
|
11
|
-
|
9
|
+
def getListFromStr(stringList: str) -> list[str]:
|
10
|
+
"""Get a python List of strings from a semi-colon separated string."""
|
11
|
+
stripped: list[str] = []
|
12
|
+
li = stringList.split(";")
|
13
|
+
for i in li:
|
12
14
|
s = i.strip()
|
13
15
|
if s:
|
14
16
|
stripped.append(s)
|
@@ -20,32 +22,6 @@ def stringToFloat(string: str) -> float:
|
|
20
22
|
return float(string)
|
21
23
|
|
22
24
|
|
23
|
-
def get_bullet_string(s):
|
24
|
-
"""Formatiert die Angaben zum Statischen System hübsch."""
|
25
|
-
split = s.split(";")
|
26
|
-
s_spl = stripWhitespace(split)
|
27
|
-
name = []
|
28
|
-
var = []
|
29
|
-
quant = []
|
30
|
-
unit = []
|
31
|
-
for sc in s_spl:
|
32
|
-
sc_split = sc.split()
|
33
|
-
name.append(sc_split[0])
|
34
|
-
var.append(sc_split[1])
|
35
|
-
quant.append(sc_split[3])
|
36
|
-
unit.append(sc_split[4])
|
37
|
-
bulletString = ['</p><ul dir="ltr">']
|
38
|
-
for i in range(len(s_spl)):
|
39
|
-
num = quant[i].split(",")
|
40
|
-
num_s = f"{num[0]!s},\\!{num[1]!s}~" if len(num) == 2 else f"{num[0]!s},\\!0~"
|
41
|
-
bulletString.append('<li style="text-align: left;">')
|
42
|
-
bulletString.append(
|
43
|
-
f"{name[i]}: \\( {var[i]} = {num_s} \\mathrm{{ {unit[i]} }}\\) </li>\n",
|
44
|
-
)
|
45
|
-
bulletString.append("<br></ul>")
|
46
|
-
return "\n".join(bulletString)
|
47
|
-
|
48
|
-
|
49
25
|
def getBase64Img(imgPath):
|
50
26
|
with open(imgPath, "rb") as img:
|
51
27
|
return base64.b64encode(img.read()).decode("utf-8")
|
@@ -62,7 +38,7 @@ def getUnitsElementAsString(unit) -> None:
|
|
62
38
|
|
63
39
|
|
64
40
|
def printDom(xmlElement: ET.Element, file: Path | None = None) -> None:
|
65
|
-
"""Prints the document tree of ``xmlTree`` to
|
41
|
+
"""Prints the document tree of ``xmlTree`` to ``file``, if specified, else dumps to stdout."""
|
66
42
|
documentTree = ET.ElementTree(xmlElement)
|
67
43
|
if file is not None:
|
68
44
|
if file.parent.exists():
|
@@ -73,11 +49,11 @@ def printDom(xmlElement: ET.Element, file: Path | None = None) -> None:
|
|
73
49
|
pretty_print=True,
|
74
50
|
)
|
75
51
|
else:
|
76
|
-
|
52
|
+
print(xmlElement.tostring()) # noqa: T201
|
77
53
|
|
78
54
|
|
79
55
|
def texWrapper(text: str | list[str], style: str) -> list[str]:
|
80
|
-
r"""
|
56
|
+
r"""Put the strings inside ``text`` into a LaTex environment.
|
81
57
|
|
82
58
|
if ``style == unit``: inside ``\\mathrm{}``
|
83
59
|
if ``style == math``: inside ``\\( \\)``
|
@@ -16,11 +16,11 @@ import pandas as pd
|
|
16
16
|
|
17
17
|
from excel2moodle.core.exceptions import InvalidFieldException
|
18
18
|
from excel2moodle.core.globals import DFIndex
|
19
|
-
from excel2moodle.core.question import Question
|
20
19
|
|
21
20
|
if TYPE_CHECKING:
|
22
21
|
from types import UnionType
|
23
22
|
|
23
|
+
|
24
24
|
logger = logging.getLogger(__name__)
|
25
25
|
|
26
26
|
|
@@ -30,25 +30,25 @@ class Validator:
|
|
30
30
|
Creates a dictionary with the data, for easier access later.
|
31
31
|
"""
|
32
32
|
|
33
|
-
def __init__(self
|
34
|
-
self.
|
35
|
-
self.category = category
|
36
|
-
self.mandatory: dict[DFIndex, type | UnionType] = {
|
33
|
+
def __init__(self) -> None:
|
34
|
+
self.allMandatory: dict[DFIndex, type | UnionType] = {
|
37
35
|
DFIndex.TEXT: str,
|
38
36
|
DFIndex.NAME: str,
|
39
37
|
DFIndex.TYPE: str,
|
40
38
|
}
|
41
|
-
self.
|
42
|
-
DFIndex.BPOINTS: str,
|
39
|
+
self.allOptional: dict[DFIndex, type | UnionType] = {
|
43
40
|
DFIndex.PICTURE: int | str,
|
44
41
|
}
|
45
|
-
self.nfOpt: dict[DFIndex, type | UnionType] = {
|
42
|
+
self.nfOpt: dict[DFIndex, type | UnionType] = {
|
43
|
+
DFIndex.BPOINTS: str,
|
44
|
+
}
|
46
45
|
self.nfMand: dict[DFIndex, type | UnionType] = {
|
47
46
|
DFIndex.RESULT: float | int,
|
48
47
|
}
|
49
48
|
self.nfmOpt: dict[DFIndex, type | UnionType] = {}
|
50
49
|
self.nfmMand: dict[DFIndex, type | UnionType] = {
|
51
50
|
DFIndex.RESULT: str,
|
51
|
+
DFIndex.BPOINTS: str,
|
52
52
|
}
|
53
53
|
self.mcOpt: dict[DFIndex, type | UnionType] = {}
|
54
54
|
self.mcMand: dict[DFIndex, type | UnionType] = {
|
@@ -63,31 +63,35 @@ class Validator:
|
|
63
63
|
"NFM": (self.nfmOpt, self.nfmMand),
|
64
64
|
}
|
65
65
|
|
66
|
-
def setup(self, df: pd.Series, index: int) ->
|
66
|
+
def setup(self, df: pd.Series, index: int) -> None:
|
67
67
|
self.df = df
|
68
68
|
self.index = index
|
69
|
+
self.mandatory = {}
|
70
|
+
self.optional = {}
|
69
71
|
typ = self.df.loc[DFIndex.TYPE]
|
72
|
+
if typ not in self.mapper:
|
73
|
+
msg = f"No valid question type provided. {typ} is not a known type"
|
74
|
+
raise InvalidFieldException(msg, "index:02d", DFIndex.TYPE)
|
75
|
+
self.mandatory.update(self.allMandatory)
|
76
|
+
self.optional.update(self.allOptional)
|
70
77
|
self.mandatory.update(self.mapper[typ][1])
|
71
78
|
self.optional.update(self.mapper[typ][0])
|
72
|
-
return True
|
73
79
|
|
74
|
-
def validate(self) ->
|
75
|
-
|
80
|
+
def validate(self) -> None:
|
81
|
+
qid = f"{self.index:02d}"
|
76
82
|
checker, missing = self._mandatory()
|
77
83
|
if not checker:
|
78
|
-
msg = f"Question {
|
84
|
+
msg = f"Question {qid} misses the key {missing}"
|
79
85
|
if missing is not None:
|
80
|
-
raise InvalidFieldException(msg,
|
86
|
+
raise InvalidFieldException(msg, qid, missing)
|
81
87
|
checker, missing = self._typeCheck()
|
82
88
|
if not checker:
|
83
|
-
msg = f"Question {
|
89
|
+
msg = f"Question {qid} has wrong typed data {missing}"
|
84
90
|
if missing is not None:
|
85
|
-
raise InvalidFieldException(msg,
|
86
|
-
self._getQuestion()
|
87
|
-
self._getData()
|
88
|
-
return True
|
91
|
+
raise InvalidFieldException(msg, qid, missing)
|
89
92
|
|
90
|
-
def
|
93
|
+
def getQuestionRawData(self) -> dict[str, str | float | list[str]]:
|
94
|
+
"""Get the data from the spreadsheet as a dictionary."""
|
91
95
|
self.qdata: dict[str, str | float | int | list] = {}
|
92
96
|
for idx, val in self.df.items():
|
93
97
|
if not isinstance(idx, str):
|
@@ -100,6 +104,7 @@ class Validator:
|
|
100
104
|
self.qdata[idx] = [existing, val]
|
101
105
|
else:
|
102
106
|
self.qdata[idx] = val
|
107
|
+
return self.qdata
|
103
108
|
|
104
109
|
def _mandatory(self) -> tuple[bool, DFIndex | None]:
|
105
110
|
"""Detects if all keys of mandatory are filled with values."""
|
@@ -127,18 +132,8 @@ class Validator:
|
|
127
132
|
invalid.append(field)
|
128
133
|
for field, typ in self.optional.items():
|
129
134
|
if field in self.df:
|
130
|
-
if
|
135
|
+
if pd.notna(self.df[field]) and not isinstance(self.df[field], typ):
|
131
136
|
invalid.append(field)
|
132
137
|
if len(invalid) == 0:
|
133
138
|
return True, None
|
134
139
|
return False, invalid
|
135
|
-
|
136
|
-
def _getQuestion(self) -> None:
|
137
|
-
name = self.df[DFIndex.NAME]
|
138
|
-
qtype = self.df[DFIndex.TYPE]
|
139
|
-
self.question = Question(
|
140
|
-
self.category,
|
141
|
-
name=str(name),
|
142
|
-
number=self.index,
|
143
|
-
qtype=str(qtype),
|
144
|
-
)
|
excel2moodle/logger.py
CHANGED
@@ -90,7 +90,6 @@ class LogWindowHandler(logging.Handler):
|
|
90
90
|
log_message = self.format(record)
|
91
91
|
color = self.logLevelColors.get(record.levelname, "black")
|
92
92
|
prettyMessage = f'<span style="color:{color};">{log_message}</span>'
|
93
|
-
print("emitting new log signal") # noqa:T201
|
94
93
|
self.emitter.signal.emit(prettyMessage)
|
95
94
|
|
96
95
|
|
@@ -0,0 +1,33 @@
|
|
1
|
+
"""Implementations of all different question types.
|
2
|
+
|
3
|
+
For each question type supported by excel2moodle here are the implementations.
|
4
|
+
Two classes need to be defined for each type:
|
5
|
+
|
6
|
+
* The Question class subclassing core.Question()
|
7
|
+
* The Parser class subclassing core.Parser()
|
8
|
+
|
9
|
+
Both go into a module named ``excel2moodle.question_types.type.py``
|
10
|
+
"""
|
11
|
+
|
12
|
+
from enum import Enum
|
13
|
+
|
14
|
+
from excel2moodle.core.category import Category
|
15
|
+
from excel2moodle.question_types.mc import MCQuestion
|
16
|
+
from excel2moodle.question_types.nf import NFQuestion
|
17
|
+
from excel2moodle.question_types.nfm import NFMQuestion
|
18
|
+
|
19
|
+
|
20
|
+
class QuestionTypeMapping(Enum):
|
21
|
+
"""The Mapping between question-types name and the classes."""
|
22
|
+
|
23
|
+
MC = MCQuestion
|
24
|
+
NF = NFQuestion
|
25
|
+
NFM = NFMQuestion
|
26
|
+
|
27
|
+
def create(
|
28
|
+
self,
|
29
|
+
category: Category,
|
30
|
+
questionData: dict[str, str | int | float | list[str]],
|
31
|
+
**kwargs,
|
32
|
+
):
|
33
|
+
return self.value(category, questionData, **kwargs)
|
@@ -0,0 +1,93 @@
|
|
1
|
+
"""Multiple choice Question implementation."""
|
2
|
+
|
3
|
+
from typing import ClassVar
|
4
|
+
|
5
|
+
import lxml.etree as ET
|
6
|
+
|
7
|
+
import excel2moodle.core.etHelpers as eth
|
8
|
+
from excel2moodle.core import stringHelpers
|
9
|
+
from excel2moodle.core.globals import (
|
10
|
+
DFIndex,
|
11
|
+
TextElements,
|
12
|
+
XMLTags,
|
13
|
+
feedbackStr,
|
14
|
+
)
|
15
|
+
from excel2moodle.core.parser import QuestionParser
|
16
|
+
from excel2moodle.core.question import Question
|
17
|
+
|
18
|
+
|
19
|
+
class MCQuestion(Question):
|
20
|
+
"""Multiple-choice Question Implementation."""
|
21
|
+
|
22
|
+
standardTags: ClassVar[dict[str, str | float]] = {
|
23
|
+
"single": "false",
|
24
|
+
"shuffleanswers": "true",
|
25
|
+
"answernumbering": "abc",
|
26
|
+
"showstandardinstruction": "0",
|
27
|
+
"shownumcorrect": "",
|
28
|
+
}
|
29
|
+
|
30
|
+
def __init__(self, *args, **kwargs) -> None:
|
31
|
+
super().__init__(*args, **kwargs)
|
32
|
+
|
33
|
+
|
34
|
+
class MCQuestionParser(QuestionParser):
|
35
|
+
"""Parser for the multiple choice Question."""
|
36
|
+
|
37
|
+
def __init__(self) -> None:
|
38
|
+
super().__init__()
|
39
|
+
self.genFeedbacks = [
|
40
|
+
XMLTags.CORFEEDB,
|
41
|
+
XMLTags.PCORFEEDB,
|
42
|
+
XMLTags.INCORFEEDB,
|
43
|
+
]
|
44
|
+
|
45
|
+
def getAnsElementsList(
|
46
|
+
self,
|
47
|
+
answerList: list,
|
48
|
+
fraction: float = 50,
|
49
|
+
format="html",
|
50
|
+
) -> list[ET.Element]:
|
51
|
+
elementList: list[ET.Element] = []
|
52
|
+
for ans in answerList:
|
53
|
+
p = TextElements.PLEFT.create()
|
54
|
+
p.text = str(ans)
|
55
|
+
text = eth.getCdatTxtElement(p)
|
56
|
+
elementList.append(
|
57
|
+
ET.Element(XMLTags.ANSWER, fraction=str(fraction), format=format),
|
58
|
+
)
|
59
|
+
elementList[-1].append(text)
|
60
|
+
if fraction < 0:
|
61
|
+
elementList[-1].append(
|
62
|
+
eth.getFeedBEle(
|
63
|
+
XMLTags.ANSFEEDBACK,
|
64
|
+
text=feedbackStr["wrong"],
|
65
|
+
style=TextElements.SPANRED,
|
66
|
+
),
|
67
|
+
)
|
68
|
+
elif fraction > 0:
|
69
|
+
elementList[-1].append(
|
70
|
+
eth.getFeedBEle(
|
71
|
+
XMLTags.ANSFEEDBACK,
|
72
|
+
text=feedbackStr["right"],
|
73
|
+
style=TextElements.SPANGREEN,
|
74
|
+
),
|
75
|
+
)
|
76
|
+
return elementList
|
77
|
+
|
78
|
+
def setAnswers(self) -> list[ET.Element]:
|
79
|
+
ansStyle = self.rawInput[DFIndex.ANSTYPE]
|
80
|
+
true = stringHelpers.getListFromStr(self.rawInput[DFIndex.TRUE])
|
81
|
+
trueAnsList = stringHelpers.texWrapper(true, style=ansStyle)
|
82
|
+
self.logger.debug(f"got the following true answers \n {trueAnsList=}")
|
83
|
+
false = stringHelpers.getListFromStr(self.rawInput[DFIndex.FALSE])
|
84
|
+
falseAnsList = stringHelpers.texWrapper(false, style=ansStyle)
|
85
|
+
self.logger.debug(f"got the following false answers \n {falseAnsList=}")
|
86
|
+
truefrac = 1 / len(trueAnsList) * 100
|
87
|
+
falsefrac = 1 / len(trueAnsList) * (-100)
|
88
|
+
self.tmpEle.find(XMLTags.PENALTY).text = str(round(truefrac / 100, 4))
|
89
|
+
ansList = self.getAnsElementsList(trueAnsList, fraction=round(truefrac, 4))
|
90
|
+
ansList.extend(
|
91
|
+
self.getAnsElementsList(falseAnsList, fraction=round(falsefrac, 4)),
|
92
|
+
)
|
93
|
+
return ansList
|