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