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