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