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.
@@ -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
+