excel2moodle 0.3.1__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 +113 -0
- excel2moodle/__main__.py +31 -0
- excel2moodle/core/__init__.py +13 -0
- excel2moodle/core/category.py +108 -0
- excel2moodle/core/dataStructure.py +140 -0
- excel2moodle/core/etHelpers.py +67 -0
- excel2moodle/core/exceptions.py +20 -0
- excel2moodle/core/globals.py +128 -0
- excel2moodle/core/numericMultiQ.py +76 -0
- excel2moodle/core/parser.py +322 -0
- excel2moodle/core/question.py +106 -0
- excel2moodle/core/questionValidator.py +124 -0
- excel2moodle/core/questionWriter.py +174 -0
- excel2moodle/core/stringHelpers.py +94 -0
- excel2moodle/extra/__init__.py +9 -0
- excel2moodle/extra/equationVerification.py +124 -0
- excel2moodle/ui/__init__.py +1 -0
- excel2moodle/ui/appUi.py +243 -0
- excel2moodle/ui/dialogs.py +80 -0
- excel2moodle/ui/questionPreviewDialog.py +115 -0
- excel2moodle/ui/settings.py +34 -0
- excel2moodle/ui/treewidget.py +65 -0
- excel2moodle/ui/variantDialog.py +132 -0
- excel2moodle/ui/windowDoc.py +35 -0
- excel2moodle/ui/windowEquationChecker.py +187 -0
- excel2moodle-0.3.1.dist-info/METADATA +63 -0
- excel2moodle-0.3.1.dist-info/RECORD +30 -0
- excel2moodle-0.3.1.dist-info/WHEEL +5 -0
- excel2moodle-0.3.1.dist-info/licenses/LICENSE +674 -0
- excel2moodle-0.3.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
"""Numeric Multi Questions Module to calculate results from a formula
|
2
|
+
|
3
|
+
This module calculates a series of results from al matrix of variables.
|
4
|
+
For each column in the matrix there will be one result.
|
5
|
+
As well it returns a bullet points string that shows the numerical values corresponding to the set of variables
|
6
|
+
"""
|
7
|
+
|
8
|
+
import re as re
|
9
|
+
import pandas as pd
|
10
|
+
from asteval import Interpreter
|
11
|
+
|
12
|
+
astEval = Interpreter()
|
13
|
+
|
14
|
+
|
15
|
+
def getVariablesDict(df: pd.DataFrame, keyList: list, index:int)-> dict:
|
16
|
+
"""Liest alle Variablen-Listen deren Name in ``keyList`` ist aus dem DataFrame im Column[index]"""
|
17
|
+
dic = {}
|
18
|
+
for k in keyList:
|
19
|
+
val = df.loc[str(k)][index]
|
20
|
+
if type(val) == str and val != None :
|
21
|
+
li = val.split(";")
|
22
|
+
dic[str(k)] = li
|
23
|
+
else:
|
24
|
+
dic[str(k)] = [str(val)]
|
25
|
+
print(f"Folgende Variablen wurden gefunden:\n{dic}\n")
|
26
|
+
return dic
|
27
|
+
|
28
|
+
def setParameters(parameters: dict, index: int)->None:
|
29
|
+
"""Ubergibt die Parameter mit entsprechenden Variablen-Namen an den asteval-Interpreter.
|
30
|
+
|
31
|
+
Dann kann dieser die equation loesen.
|
32
|
+
"""
|
33
|
+
for k,v in parameters.items():
|
34
|
+
comma = re.compile(r",")
|
35
|
+
value = comma.sub(".",v[index])
|
36
|
+
astEval.symtable[k] = float(value)
|
37
|
+
return None
|
38
|
+
|
39
|
+
def insertVariablesToBPoints(varDict: dict, bulletPoints: str, index: int)-> str:
|
40
|
+
"""
|
41
|
+
Für jeden Eintrag im varDict, wird im bulletPoints String der Substring "{key}" durch value[index] ersetzt
|
42
|
+
"""
|
43
|
+
for k, v in varDict.items():
|
44
|
+
s = r"{" + str(k) + r"}"
|
45
|
+
matcher = re.compile(s)
|
46
|
+
bulletPoints = matcher.sub(str(v[index]), bulletPoints)
|
47
|
+
return bulletPoints
|
48
|
+
|
49
|
+
def getVarsList(bps: str)->list:
|
50
|
+
"""
|
51
|
+
Durchsucht den bulletPoints String nach den Variablen, die als "{var}" gekennzeichnet sind
|
52
|
+
"""
|
53
|
+
vars = re.findall(r"\{\w\}", str(bps))
|
54
|
+
variablen=[]
|
55
|
+
for v in vars:
|
56
|
+
variablen.append(v.strip("{}"))
|
57
|
+
return variablen
|
58
|
+
|
59
|
+
|
60
|
+
def parseNumericMultiQuestion(datFrame: pd.DataFrame, bulletPoints: str,
|
61
|
+
equation: str, questionIndex: int
|
62
|
+
)-> tuple[list[str],list[float]]:
|
63
|
+
"""Berechnet die Ergebnisse anhand der Variablen in *bulletPoints*
|
64
|
+
|
65
|
+
Gibt eine Liste mit allen Ergebnissen zurück und eine Liste mit den bulletPoints-Strings, die die Numerischen Variablen enthalten
|
66
|
+
"""
|
67
|
+
results = []
|
68
|
+
bps = []
|
69
|
+
varNames = getVarsList(bulletPoints)
|
70
|
+
variables = getVariablesDict(datFrame, varNames, questionIndex)
|
71
|
+
length = len(next(iter(variables.values())))
|
72
|
+
for n in range(length):
|
73
|
+
setParameters(variables, n)
|
74
|
+
results.append(astEval(equation))
|
75
|
+
bps.append(insertVariablesToBPoints(variables, bulletPoints, n))
|
76
|
+
return bps, results
|
@@ -0,0 +1,322 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
from unicodedata import category
|
4
|
+
from asteval import Interpreter
|
5
|
+
import lxml.etree as ET
|
6
|
+
# import xml.etree.ElementTree as ET
|
7
|
+
from typing import Union
|
8
|
+
from pathlib import Path
|
9
|
+
from numpy import isin
|
10
|
+
import pandas as pd
|
11
|
+
import base64 as base64
|
12
|
+
import logging as logging
|
13
|
+
|
14
|
+
from excel2moodle.core import question
|
15
|
+
from excel2moodle.core.exceptions import NanException, QNotParsedException
|
16
|
+
import excel2moodle.core.etHelpers as eth
|
17
|
+
|
18
|
+
from excel2moodle.core.globals import XMLTags, TextElements, DFIndex, questionTypes, parserSettings, feedbackStr, feedBElements
|
19
|
+
from excel2moodle.core import stringHelpers
|
20
|
+
from excel2moodle.core.question import Picture, Question
|
21
|
+
import re as re
|
22
|
+
|
23
|
+
|
24
|
+
logger = logging.getLogger(__name__)
|
25
|
+
svgFolder = Path("../Fragensammlung/Abbildungen_SVG/")
|
26
|
+
|
27
|
+
class QuestionParser():
|
28
|
+
def __init__(self, question:Question, dataframe:pd.Series):
|
29
|
+
self.question:Question = question
|
30
|
+
self.df = dataframe
|
31
|
+
self.genFeedbacks:list[XMLTags] = []
|
32
|
+
|
33
|
+
def hasPicture(self)->bool:
|
34
|
+
"""Creates a ``Picture`` object inside ``question``, if the question needs a pic"""
|
35
|
+
|
36
|
+
picKey = self.df.get(DFIndex.PICTURE)
|
37
|
+
if picKey != 0 and not pd.isna(picKey):
|
38
|
+
if not hasattr(self.question, 'picture'):
|
39
|
+
self.question.picture = Picture(picKey, svgFolder, self.question)
|
40
|
+
if self.question.picture.ready:
|
41
|
+
return True
|
42
|
+
return False
|
43
|
+
|
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}"
|
47
|
+
text = self.df.get(DFIndex.TEXT)
|
48
|
+
pcount = 0
|
49
|
+
for t in text:
|
50
|
+
if not pd.isna(t):
|
51
|
+
pcount +=1
|
52
|
+
paragraphs.append(TextElements.PLEFT.create())
|
53
|
+
paragraphs[-1].text = t
|
54
|
+
self.question.qtextElements = paragraphs
|
55
|
+
logger.debug(f"Created main Text {self.question.id} with:{pcount} paragraphs")
|
56
|
+
return None
|
57
|
+
|
58
|
+
def setBPoints(self)->None:
|
59
|
+
"""If there bulletPoints are set in the Spreadsheet it creates an unordered List-Element in ``Question.bulletList``"""
|
60
|
+
if DFIndex.BPOINTS in self.df.index:
|
61
|
+
bps = self.df.get(DFIndex.BPOINTS)
|
62
|
+
try:
|
63
|
+
bulletList = self.formatBulletList(bps)
|
64
|
+
except IndexError as e:
|
65
|
+
raise QNotParsedException(f"konnt Bullet Liste {self.question.id} nicht generieren", self.question.id, exc_info=e)
|
66
|
+
self.question.bulletList.append(bulletList)
|
67
|
+
logger.debug(f"appendet Bullet List {bulletList = }")
|
68
|
+
return None
|
69
|
+
|
70
|
+
def formatBulletList(self,bps:str)->ET.Element:
|
71
|
+
li:list[str] =stringHelpers.stripWhitespace( bps.split(';'))
|
72
|
+
name = []
|
73
|
+
var = []
|
74
|
+
quant = []
|
75
|
+
unit = []
|
76
|
+
unorderedList = TextElements.ULIST.create()
|
77
|
+
for item in li:
|
78
|
+
sc_split = item.split()
|
79
|
+
name.append(sc_split[0])
|
80
|
+
var.append(sc_split[1])
|
81
|
+
quant.append(sc_split[3])
|
82
|
+
unit.append(sc_split[4])
|
83
|
+
for i in range(0, len(name)):
|
84
|
+
num = quant[i].split(',')
|
85
|
+
if len(num)==2:
|
86
|
+
num_s = f"{str(num[0])},\\!{str(num[1])}~"
|
87
|
+
else: num_s = f"{str(num[0])},\\!0~"
|
88
|
+
bullet = TextElements.LISTITEM.create()
|
89
|
+
bullet.text=(f"{ name[i] }: \\( {var[i]} = {num_s} \\mathrm{{ {unit[i]} }}\\)\n")
|
90
|
+
unorderedList.append(bullet)
|
91
|
+
return unorderedList
|
92
|
+
|
93
|
+
def appendToQuestion(self, eleName: str, text:str|DFIndex, txtEle=False, **attribs ):
|
94
|
+
t = (self.df.get(text) if isinstance(text, DFIndex) else text)
|
95
|
+
if txtEle is False:
|
96
|
+
self.tmpEle.append(eth.getElement(eleName, t, **attribs))
|
97
|
+
elif txtEle is True:
|
98
|
+
self.tmpEle.append(eth.getTextElement(eleName, t, **attribs))
|
99
|
+
|
100
|
+
def appendFromSettings(self, key="standards")->None:
|
101
|
+
"""Appends 1 to 1 mapped Elements defined in the parserSettings to the element"""
|
102
|
+
parser = ["Parser"]
|
103
|
+
if isinstance(self, MCQuestionParser):
|
104
|
+
parser.append("MCParser")
|
105
|
+
elif isinstance(self, NFQuestionParser):
|
106
|
+
parser.append("NFParser")
|
107
|
+
for p in parser:
|
108
|
+
try:
|
109
|
+
for k, v in parserSettings[p][key].items():
|
110
|
+
self.appendToQuestion(k, text=v)
|
111
|
+
except KeyError as e:
|
112
|
+
msg = f"Invalider Input aus den Einstellungen Parser: {type(p) = }"
|
113
|
+
logger.error(msg, exc_info=e)
|
114
|
+
raise QNotParsedException(msg, self.question.id, exc_info=e)
|
115
|
+
return None
|
116
|
+
|
117
|
+
def parse(self, xmlTree: ET._Element|None=None)->None:
|
118
|
+
"""Parses the Question
|
119
|
+
|
120
|
+
Generates an new Question Element stored as ``self.tmpEle:ET.Element``
|
121
|
+
if no Exceptions are raised, ``self.tmpEle`` is passed to ``self.question.element``
|
122
|
+
"""
|
123
|
+
self.tmpEle = ET.Element(XMLTags.QUESTION, type = self.question.moodleType)
|
124
|
+
# self.tmpEle.set(XMLTags.TYPE, self.question.moodleType)
|
125
|
+
self.appendToQuestion(XMLTags.NAME, text=DFIndex.NAME, txtEle=True)
|
126
|
+
self.appendToQuestion(XMLTags.ID, text=self.question.id)
|
127
|
+
if self.hasPicture() :
|
128
|
+
self.tmpEle.append(self.question.picture.element)
|
129
|
+
self.tmpEle.append(ET.Element(XMLTags.QTEXT, format = "html"))
|
130
|
+
self.appendToQuestion(XMLTags.POINTS, text=str(self.question.points))
|
131
|
+
self.appendToQuestion(XMLTags.PENALTY, text="0.3333")
|
132
|
+
self.appendFromSettings()
|
133
|
+
for feedb in self.genFeedbacks:
|
134
|
+
self.tmpEle.append(eth.getFeedBEle(feedb))
|
135
|
+
if xmlTree is not None:
|
136
|
+
xmlTree.append(self.tmpEle)
|
137
|
+
ansList = self.setAnswers()
|
138
|
+
self.setMainText()
|
139
|
+
self.setBPoints()
|
140
|
+
if ansList is not None:
|
141
|
+
for ele in ansList:
|
142
|
+
self.tmpEle.append(ele)
|
143
|
+
logger.info(f"Sucessfully parsed {self.question.id}")
|
144
|
+
self.question.element = self.tmpEle
|
145
|
+
return None
|
146
|
+
|
147
|
+
def getFeedBEle(self, feedback:XMLTags, text:str|None=None, style: TextElements | None = None)->ET.Element:
|
148
|
+
if style is None:
|
149
|
+
span = feedBElements[feedback]
|
150
|
+
else:
|
151
|
+
span = style.create()
|
152
|
+
if text is None:
|
153
|
+
text = feedbackStr[feedback]
|
154
|
+
ele = ET.Element(feedback, format="html")
|
155
|
+
par = TextElements.PLEFT.create()
|
156
|
+
span.text = text
|
157
|
+
par.append(span)
|
158
|
+
ele.append(eth.getCdatTxtElement(par))
|
159
|
+
return ele
|
160
|
+
|
161
|
+
def setAnswers(self)->list[ET.Element]|None:
|
162
|
+
"""Needs to be implemented in the type-specific subclasses"""
|
163
|
+
return None
|
164
|
+
|
165
|
+
@staticmethod
|
166
|
+
def getNumericAnsElement(result:int|float,
|
167
|
+
tolerance:float = 0.01,
|
168
|
+
fraction:int|float = 100,
|
169
|
+
format:str = "moodle_auto_format")->ET.Element:
|
170
|
+
"""Returns an ``<answer/>`` Element specific for the numerical Question
|
171
|
+
The element contains those childs:
|
172
|
+
``<text/>`` which holds the value of the answer
|
173
|
+
``<tolerace/>`` with the *relative* tolerance for the result
|
174
|
+
``<feedback/>`` with general feedback for a true answer
|
175
|
+
"""
|
176
|
+
|
177
|
+
ansEle:ET.Element = eth.getTextElement(XMLTags.ANSWER, text = str(result), fraction = str(fraction), format = format)
|
178
|
+
ansEle.append(eth.getFeedBEle(XMLTags.ANSFEEDBACK, feedbackStr["right1Percent"], TextElements.SPANGREEN))
|
179
|
+
tol = abs(round(result*tolerance, 3))
|
180
|
+
ansEle.append(eth.getElement(XMLTags.TOLERANCE, text = str(tol)))
|
181
|
+
return ansEle
|
182
|
+
|
183
|
+
class NFQuestionParser(QuestionParser):
|
184
|
+
def __init__(self, *args)->None:
|
185
|
+
super().__init__(*args)
|
186
|
+
self.genFeedbacks=[XMLTags.GENFEEDB]
|
187
|
+
|
188
|
+
def setAnswers(self)->list[ET.Element]:
|
189
|
+
result = self.df.get(DFIndex.RESULT)
|
190
|
+
ansEle:list[ET.Element]=[]
|
191
|
+
ansEle.append(self.getNumericAnsElement( result = result ))
|
192
|
+
return ansEle
|
193
|
+
|
194
|
+
class NFMQuestionParser(QuestionParser):
|
195
|
+
def __init__(self, question: Question, dataframe: pd.Series):
|
196
|
+
super().__init__(question, dataframe)
|
197
|
+
self.genFeedbacks=[XMLTags.GENFEEDB]
|
198
|
+
self.astEval = Interpreter()
|
199
|
+
|
200
|
+
def setAnswers(self)->None:
|
201
|
+
equation = self.df.get(DFIndex.RESULT)
|
202
|
+
bps = self.df.get(DFIndex.BPOINTS)
|
203
|
+
ansElementsList:list[ET.Element]=[]
|
204
|
+
varNames:list[str]= self.getVarsList(bps)
|
205
|
+
varsDict, number = self.getVariablesDict(varNames)
|
206
|
+
bulletPoints:list[ET.Element] = []
|
207
|
+
for n in range(number):
|
208
|
+
self._setupAstIntprt(varsDict, n)
|
209
|
+
result = self.astEval(equation)
|
210
|
+
if isinstance(result, float):
|
211
|
+
ansElementsList.append(self.getNumericAnsElement( result = round(result,3) ))
|
212
|
+
bpli = self.insertVariablesToBPoints(varsDict, bps, n)
|
213
|
+
bulletPoints.append(self.formatBulletList(bpli))
|
214
|
+
self.question.answerVariants = ansElementsList
|
215
|
+
self.question.bulletList = bulletPoints
|
216
|
+
self.setVariants(len(bulletPoints))
|
217
|
+
return None
|
218
|
+
|
219
|
+
def setVariants(self, number:int):
|
220
|
+
self.question.variants = number
|
221
|
+
mvar = self.question.category.maxVariants
|
222
|
+
if mvar is None:
|
223
|
+
self.question.category.maxVariants = number
|
224
|
+
else:
|
225
|
+
self.question.category.maxVariants = number if number <= mvar else mvar
|
226
|
+
|
227
|
+
|
228
|
+
@staticmethod
|
229
|
+
def insertVariablesToBPoints(varDict: dict, bulletPoints: str, index: int)-> str:
|
230
|
+
"""
|
231
|
+
Für jeden Eintrag im varDict, wird im bulletPoints String der Substring "{key}" durch value[index] ersetzt
|
232
|
+
"""
|
233
|
+
for k, v in varDict.items():
|
234
|
+
s = r"{" + str(k) + r"}"
|
235
|
+
matcher = re.compile(s)
|
236
|
+
bulletPoints = matcher.sub(str(v[index]), bulletPoints)
|
237
|
+
return bulletPoints
|
238
|
+
|
239
|
+
def _setupAstIntprt(self, var:dict[str, list[str]], index:int)->None:
|
240
|
+
"""Ubergibt die Parameter mit entsprechenden Variablen-Namen an den asteval-Interpreter.
|
241
|
+
|
242
|
+
Dann kann dieser die equation lesen.
|
243
|
+
"""
|
244
|
+
for k,v in var.items():
|
245
|
+
comma = re.compile(r",")
|
246
|
+
value = comma.sub(".",v[index])
|
247
|
+
self.astEval.symtable[k] = float(value)
|
248
|
+
return None
|
249
|
+
|
250
|
+
def getVariablesDict(self, keyList: list)-> tuple[dict[str,list[str]],int]:
|
251
|
+
"""Liest alle Variablen-Listen deren Name in ``keyList`` ist aus dem DataFrame im Column[index]"""
|
252
|
+
dic:dict = {}
|
253
|
+
num:int = 0
|
254
|
+
for k in keyList:
|
255
|
+
val = self.df.get(k)
|
256
|
+
if isinstance(val, str) :
|
257
|
+
li = val.split(";")
|
258
|
+
num = len(li)
|
259
|
+
dic[str(k)] = li
|
260
|
+
else:
|
261
|
+
dic[str(k)] = [str(val)]
|
262
|
+
num = 1
|
263
|
+
print(f"Folgende Variablen wurden gefunden:\n{dic}\n")
|
264
|
+
return dic, num
|
265
|
+
|
266
|
+
@staticmethod
|
267
|
+
def getVarsList(bps: str|list[str])->list:
|
268
|
+
"""
|
269
|
+
Durchsucht den bulletPoints String nach den Variablen, die als "{var}" gekennzeichnet sind
|
270
|
+
"""
|
271
|
+
vars = []
|
272
|
+
if isinstance(bps, list):
|
273
|
+
for p in bps:
|
274
|
+
vars.extend(re.findall(r"\{\w\}", str(bps)))
|
275
|
+
else:
|
276
|
+
vars = re.findall(r"\{\w\}", str(bps))
|
277
|
+
variablen=[]
|
278
|
+
for v in vars:
|
279
|
+
variablen.append(v.strip("{}"))
|
280
|
+
return variablen
|
281
|
+
|
282
|
+
class MCQuestionParser(QuestionParser):
|
283
|
+
def __init__(self, *args)->None:
|
284
|
+
super().__init__(*args)
|
285
|
+
self.genFeedbacks=[
|
286
|
+
XMLTags.CORFEEDB,
|
287
|
+
XMLTags.PCORFEEDB,
|
288
|
+
XMLTags.INCORFEEDB,
|
289
|
+
]
|
290
|
+
|
291
|
+
def getAnsElementsList(self, answerList:list, fraction:float=50, format="html")->list[ET.Element]:
|
292
|
+
elementList: list[ET.Element] = []
|
293
|
+
for ans in answerList:
|
294
|
+
p = TextElements.PLEFT.create()
|
295
|
+
p.text = str(ans)
|
296
|
+
text = eth.getCdatTxtElement(p)
|
297
|
+
elementList.append(ET.Element(XMLTags.ANSWER, fraction=str(fraction), format=format))
|
298
|
+
elementList[-1].append(text)
|
299
|
+
if fraction < 0:
|
300
|
+
elementList[-1].append(eth.getFeedBEle(XMLTags.ANSFEEDBACK,
|
301
|
+
text = feedbackStr["wrong"],
|
302
|
+
style = TextElements.SPANRED))
|
303
|
+
elif fraction > 0:
|
304
|
+
elementList[-1].append(eth.getFeedBEle(XMLTags.ANSFEEDBACK,
|
305
|
+
text=feedbackStr["right"],
|
306
|
+
style=TextElements.SPANGREEN))
|
307
|
+
return elementList
|
308
|
+
|
309
|
+
|
310
|
+
def setAnswers(self)->list[ET.Element]:
|
311
|
+
ansStyle = self.df.get(DFIndex.ANSTYPE)
|
312
|
+
true = stringHelpers.stripWhitespace(self.df.get(DFIndex.TRUE).split(';'))
|
313
|
+
trueAnsList = stringHelpers.texWrapper(true, style=ansStyle)
|
314
|
+
false = stringHelpers.stripWhitespace(self.df.get(DFIndex.FALSE).split(';'))
|
315
|
+
falseAnsList= stringHelpers.texWrapper(false, style=ansStyle)
|
316
|
+
truefrac = 1/len(trueAnsList)*100
|
317
|
+
falsefrac = 1/len(trueAnsList)*(-100)
|
318
|
+
self.tmpEle.find(XMLTags.PENALTY).text=str(round(truefrac/100, 4))
|
319
|
+
ansList = self.getAnsElementsList(trueAnsList, fraction=round(truefrac, 4))
|
320
|
+
ansList.extend(self.getAnsElementsList(falseAnsList, fraction=round(falsefrac, 4)))
|
321
|
+
return ansList
|
322
|
+
|
@@ -0,0 +1,106 @@
|
|
1
|
+
import logging
|
2
|
+
import lxml.etree as ET
|
3
|
+
from pathlib import Path
|
4
|
+
import base64 as base64
|
5
|
+
|
6
|
+
from excel2moodle.core import category, etHelpers
|
7
|
+
from excel2moodle.core.globals import XMLTags, TextElements, DFIndex, questionTypes, parserSettings
|
8
|
+
from excel2moodle.core.exceptions import QNotParsedException
|
9
|
+
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
class Question():
|
14
|
+
def __init__(self, category,name:str, number:int, parent=None, qtype:str="type", points:float=0):
|
15
|
+
self.category = category
|
16
|
+
self.katName = self.category.name
|
17
|
+
self.name = name
|
18
|
+
self.number = number
|
19
|
+
self.parent = parent
|
20
|
+
self.qtype: str = qtype
|
21
|
+
self.moodleType = questionTypes[qtype]
|
22
|
+
self.points = ( points if points is not 0 else self.category.points)
|
23
|
+
self.element: ET.Element|None=None
|
24
|
+
self.picture:Picture
|
25
|
+
self.id:str
|
26
|
+
self.qtextElements: list[ET.Element] = []
|
27
|
+
self.bulletList: list[ET.Element] = []
|
28
|
+
self.answerVariants: list[ET.Element] = []
|
29
|
+
self.variants:int|None = None
|
30
|
+
self.setID()
|
31
|
+
self.standardTags = {
|
32
|
+
"hidden":"false"
|
33
|
+
}
|
34
|
+
logger.debug(f"Question {self.id} is initialized")
|
35
|
+
|
36
|
+
def __repr__(self)->str:
|
37
|
+
li:list[str] = []
|
38
|
+
li.append(f"Question v{self.category.version}")
|
39
|
+
li.append(f'{self.id=}')
|
40
|
+
li.append(f'{self.parent=}')
|
41
|
+
return "\n".join(li)
|
42
|
+
|
43
|
+
def assemble(self, variant:int=1)->None:
|
44
|
+
if self.element is not None:
|
45
|
+
mainText = self.element.find(XMLTags.QTEXT)
|
46
|
+
else: raise QNotParsedException("Cant assamble, if element is none", self.id)
|
47
|
+
if len(self.bulletList)>0:
|
48
|
+
self.qtextElements.append(self.bulletList[variant-1])
|
49
|
+
if hasattr(self, "picture") and self.picture.ready:
|
50
|
+
self.qtextElements.append(self.picture.htmlTag)
|
51
|
+
mainText.append(self.picture.element)
|
52
|
+
mainText.append(etHelpers.getCdatTxtElement(self.qtextElements))
|
53
|
+
self.element.insert(3, mainText)
|
54
|
+
if len( self.answerVariants ) > 0:
|
55
|
+
self.element.insert(5, self.answerVariants[variant-1])
|
56
|
+
return None
|
57
|
+
|
58
|
+
def setID(self, id = 0)->None:
|
59
|
+
if id == 0:
|
60
|
+
self.id: str = f"{self.category.id}{self.number:02d}"
|
61
|
+
else: self.id:str = str(id)
|
62
|
+
|
63
|
+
class Picture():
|
64
|
+
def __init__(self, picKey:str, imgFolder:Path, question:Question):
|
65
|
+
self.pic = picKey
|
66
|
+
self.ready:bool = False
|
67
|
+
self.question = question
|
68
|
+
self.imgFolder = (imgFolder / question.katName).resolve()
|
69
|
+
self.htmlTag:ET.Element
|
70
|
+
self.path:Path
|
71
|
+
self._setPath()
|
72
|
+
if hasattr(self, 'picID'):
|
73
|
+
self.ready = self.__getImg()
|
74
|
+
|
75
|
+
def _setPath(self):
|
76
|
+
if self.pic == 1:
|
77
|
+
self.picID = self.question.id
|
78
|
+
else:
|
79
|
+
selectedPic = self.pic[2:]
|
80
|
+
logger.debug(f"got a picture key {selectedPic =}")
|
81
|
+
try:
|
82
|
+
self.picID = f"{self.question.category.id}{int(selectedPic):02d}"
|
83
|
+
except ValueError as e:
|
84
|
+
logger.warning(msg=f"Bild-ID konnte aus dem Key: {self.pic = }nicht festgestellt werden", exc_info=e)
|
85
|
+
|
86
|
+
def __getBase64Img(self, imgPath):
|
87
|
+
with open(imgPath, 'rb') as img:
|
88
|
+
img64 = base64.b64encode(img.read()).decode('utf-8')
|
89
|
+
return img64
|
90
|
+
|
91
|
+
def __setImgElement(self, dir:Path, picID:int)->None:
|
92
|
+
"""gibt das Bild im dirPath mit dir qID als base64 encodiert mit den entsprechenden XML-Tags zurück"""
|
93
|
+
self.path:Path = ( dir/ str(picID) ).with_suffix('.svg')
|
94
|
+
self.element:ET.Element = ET.Element("file", name=f'{self.path.name}', path='/', encoding='base64')
|
95
|
+
self.element.text = self.__getBase64Img(self.path)
|
96
|
+
|
97
|
+
|
98
|
+
def __getImg(self)->bool:
|
99
|
+
try:
|
100
|
+
self.__setImgElement(self.imgFolder, int(self.picID))
|
101
|
+
self.htmlTag = ET.Element("img", src=f"@@PLUGINFILE@@/{self.path.name}", alt=f"Bild {self.path.name}", width="500")
|
102
|
+
return True
|
103
|
+
except FileNotFoundError as e:
|
104
|
+
logger.warning(msg=f"Bild {self.picID} konnte nicht gefunden werden ", exc_info=e)
|
105
|
+
self.element = None
|
106
|
+
return False
|
@@ -0,0 +1,124 @@
|
|
1
|
+
"""This Module checks if the data inside the Spreadsheet is valid
|
2
|
+
|
3
|
+
Those things are considered:
|
4
|
+
|
5
|
+
#. The mandatory entries must not be ``Nan``
|
6
|
+
#. All fields must have the right data-type
|
7
|
+
|
8
|
+
If Those checks pass, a question is created, which can be accessed via ``Validator.question``
|
9
|
+
"""
|
10
|
+
|
11
|
+
from types import UnionType
|
12
|
+
|
13
|
+
from pandas.core.series import notna
|
14
|
+
from excel2moodle.core.question import Question
|
15
|
+
from excel2moodle.core.globals import DFIndex
|
16
|
+
from excel2moodle.core.exceptions import InvalidFieldException, NanException
|
17
|
+
import pandas as pd
|
18
|
+
import logging
|
19
|
+
|
20
|
+
|
21
|
+
class Validator():
|
22
|
+
def __init__(self, category) -> None:
|
23
|
+
self.question:Question
|
24
|
+
self.category = category
|
25
|
+
self.mandatory: dict[DFIndex, type|UnionType] = {
|
26
|
+
DFIndex.TEXT: str,
|
27
|
+
DFIndex.NAME: str,
|
28
|
+
DFIndex.TYPE: str,
|
29
|
+
}
|
30
|
+
self.optional: dict[DFIndex, type|UnionType] = {
|
31
|
+
DFIndex.BPOINTS : str,
|
32
|
+
DFIndex.NAME: str,
|
33
|
+
DFIndex.PICTURE: int|str,
|
34
|
+
}
|
35
|
+
self.nfOpt: dict[DFIndex, type|UnionType] = {
|
36
|
+
DFIndex.RESULT: float|int,
|
37
|
+
}
|
38
|
+
self.nfMand: dict[DFIndex, type|UnionType] = {
|
39
|
+
}
|
40
|
+
self.nfmOpt: dict[DFIndex, type|UnionType] = {
|
41
|
+
}
|
42
|
+
self.nfmMand: dict[DFIndex, type|UnionType] = {
|
43
|
+
}
|
44
|
+
self.mcOpt: dict[DFIndex, type|UnionType] = {
|
45
|
+
}
|
46
|
+
self.mcMand: dict[DFIndex, type|UnionType] = {
|
47
|
+
DFIndex.TRUE: str,
|
48
|
+
DFIndex.FALSE: str,
|
49
|
+
DFIndex.ANSTYPE: str,
|
50
|
+
}
|
51
|
+
|
52
|
+
self.mapper: dict = {
|
53
|
+
"NF": (self.nfOpt, self.nfMand),
|
54
|
+
"MC": (self.mcOpt, self.mcMand),
|
55
|
+
"NFM": (self.nfmOpt, self.nfmMand),
|
56
|
+
}
|
57
|
+
|
58
|
+
def setup(self, df:pd.Series, index:int)->bool:
|
59
|
+
self.df = df
|
60
|
+
self.index = index
|
61
|
+
typ = self.df.loc[DFIndex.TYPE]
|
62
|
+
self.mandatory.update(self.mapper[typ][1])
|
63
|
+
self.optional.update(self.mapper[typ][0])
|
64
|
+
return True
|
65
|
+
|
66
|
+
def validate(self )->bool:
|
67
|
+
id = f"{self.category.id}{self.index:02d}"
|
68
|
+
checker, missing = self._mandatory()
|
69
|
+
if not checker:
|
70
|
+
msg = f"Question {id} misses the key {missing}"
|
71
|
+
if missing is not None:
|
72
|
+
raise InvalidFieldException(msg, id, missing)
|
73
|
+
checker, missing = self._typeCheck()
|
74
|
+
if not checker:
|
75
|
+
msg = f"Question {id} misses keys {missing}"
|
76
|
+
if missing is not None:
|
77
|
+
raise InvalidFieldException(msg, id, missing)
|
78
|
+
self._getQuestion()
|
79
|
+
return True
|
80
|
+
|
81
|
+
def _mandatory(self)->tuple[bool,DFIndex|None]:
|
82
|
+
"""detects if all keys of mandatory are filled with values"""
|
83
|
+
checker = pd.Series.notna(self.df)
|
84
|
+
for k in self.mandatory.keys():
|
85
|
+
try:
|
86
|
+
c = checker[k]
|
87
|
+
except KeyError:
|
88
|
+
return False, k
|
89
|
+
if isinstance(c, pd.Series):
|
90
|
+
if not c.any():
|
91
|
+
return False, k
|
92
|
+
elif not c:
|
93
|
+
return False, k,
|
94
|
+
return True, None
|
95
|
+
|
96
|
+
def _typeCheck(self)->tuple[bool, list[DFIndex]|None]:
|
97
|
+
invalid:list[DFIndex] = []
|
98
|
+
for field, typ in self.mandatory.items():
|
99
|
+
if isinstance(self.df[field], pd.Series):
|
100
|
+
for f in self.df[field]:
|
101
|
+
if pd.notna(f):
|
102
|
+
if not isinstance(f, typ):
|
103
|
+
invalid.append(field)
|
104
|
+
elif not isinstance(self.df[field], typ):
|
105
|
+
invalid.append(field)
|
106
|
+
for field, typ in self.optional.items():
|
107
|
+
if field in self.df:
|
108
|
+
if not isinstance(self.df[field], typ):
|
109
|
+
invalid.append(field)
|
110
|
+
if len(invalid) == 0:
|
111
|
+
return True, None
|
112
|
+
else:
|
113
|
+
return False, invalid
|
114
|
+
|
115
|
+
|
116
|
+
def _getQuestion(self)->None:
|
117
|
+
name = self.df[DFIndex.NAME]
|
118
|
+
qtype = self.df[DFIndex.TYPE]
|
119
|
+
self.question=Question(self.category,
|
120
|
+
name = str(name),
|
121
|
+
number = self.index,
|
122
|
+
qtype = str(qtype))
|
123
|
+
return None
|
124
|
+
|