excel2moodle 0.5.2__py3-none-any.whl → 0.6.0__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,98 @@
1
+ import logging
2
+ import re
3
+
4
+ import lxml.etree as ET
5
+
6
+ from excel2moodle.core import stringHelpers
7
+ from excel2moodle.core.globals import TextElements
8
+ from excel2moodle.core.question import ParametricQuestion
9
+ from excel2moodle.logger import LogAdapterQuestionID
10
+
11
+ loggerObj = logging.getLogger(__name__)
12
+
13
+
14
+ class BulletList:
15
+ def __init__(self, rawBullets: list[str], qID: str) -> None:
16
+ self.rawBullets: list[str] = rawBullets
17
+ self.element: ET.Element = ET.Element("ul")
18
+ self.bullets: dict[str, BulletP] = {}
19
+ self.id = qID
20
+ self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.id})
21
+ self._setupBullets(rawBullets)
22
+
23
+ def updateBullets(
24
+ self, variables: dict[str, list[float]], variant: int = 1
25
+ ) -> None:
26
+ for var, bullet in self.bullets.items():
27
+ bullet.update(value=variables[var][variant - 1])
28
+
29
+ def getVariablesDict(self, question: ParametricQuestion) -> dict[str, list[float]]:
30
+ """Read variabel values for vars in `question.rawData`.
31
+
32
+ Returns
33
+ -------
34
+ A dictionary containing a list of values for each variable name
35
+
36
+ """
37
+ keyList = self.varNames
38
+ dic: dict = {}
39
+ for k in keyList:
40
+ val = question.rawData[k.lower()]
41
+ if isinstance(val, str):
42
+ li = stringHelpers.getListFromStr(val)
43
+ variables: list[float] = [float(i.replace(",", ".")) for i in li]
44
+ dic[str(k)] = variables
45
+ else:
46
+ dic[str(k)] = [str(val)]
47
+ loggerObj.debug("The following variables were provided: %s", dic)
48
+ return dic
49
+
50
+ @property
51
+ def varNames(self) -> list[str]:
52
+ names = [i for i in self.bullets if isinstance(i, str)]
53
+ if len(names) > 0:
54
+ self.logger.debug("returning Var names: %s", names)
55
+ return names
56
+ msg = "Bullet variable names not given."
57
+ raise ValueError(msg)
58
+
59
+ def _setupBullets(self, bps: list[str]) -> ET.Element:
60
+ self.logger.debug("Formatting the bulletpoint list")
61
+ varFinder = re.compile(r"=\s*\{(\w+)\}")
62
+ for i, item in enumerate(bps):
63
+ sc_split = item.split()
64
+ name = sc_split[0]
65
+ var = sc_split[1]
66
+ quant = sc_split[3]
67
+ unit = sc_split[4]
68
+
69
+ match = re.search(varFinder, item)
70
+ if match is None:
71
+ self.logger.debug("Got a normal bulletItem")
72
+ num: float = float(quant.replace(",", "."))
73
+ bulletName: str = str(i + 1)
74
+ else:
75
+ bulletName = match.group(1)
76
+ num: float = 0.0
77
+ self.logger.debug("Got an variable bulletItem, match: %s", match)
78
+
79
+ self.bullets[bulletName] = BulletP(name=name, var=var, unit=unit, value=num)
80
+ self.element.append(self.bullets[bulletName].element)
81
+ return self.element
82
+
83
+
84
+ class BulletP:
85
+ def __init__(self, name: str, var: str, unit: str, value: float = 0.0) -> None:
86
+ self.name: str = name
87
+ self.var: str = var
88
+ self.unit: str = unit
89
+ self.element: ET.Element
90
+ self.update(value=value)
91
+
92
+ def update(self, value: float = 1) -> None:
93
+ if not hasattr(self, "element"):
94
+ self.element = TextElements.LISTITEM.create()
95
+ valuestr = str(value).replace(".", r",\!")
96
+ self.element.text = (
97
+ f"{self.name} \\( {self.var} = {valuestr} \\mathrm{{ {self.unit} }} \\)"
98
+ )
@@ -11,8 +11,8 @@ from typing import TYPE_CHECKING
11
11
 
12
12
  import lxml.etree as ET # noqa: N812
13
13
  import pandas as pd
14
- from PySide6 import QtWidgets
15
14
  from PySide6.QtCore import QObject, Signal
15
+ from PySide6.QtWidgets import QDialog
16
16
 
17
17
  from excel2moodle.core import stringHelpers
18
18
  from excel2moodle.core.category import Category
@@ -381,10 +381,9 @@ class QuestionDB:
381
381
  if hasattr(q, "variants") and q.variants is not None:
382
382
  if variant == 0 or variant > q.variants:
383
383
  dialog = QuestionVariantDialog(self.window, q)
384
- if dialog.exec() == QtWidgets.QDialog.Accepted:
384
+ if dialog.exec() == QDialog.Accepted:
385
385
  variant = dialog.variant
386
386
  logger.debug("Die Fragen-Variante %s wurde gewählt", variant)
387
387
  else:
388
388
  logger.warning("Keine Fragenvariante wurde gewählt.")
389
- q.assemble(variant=variant)
390
- tree.append(q.element)
389
+ tree.append(q.getUpdatedElement(variant=variant))
@@ -17,14 +17,9 @@ class TextElements(Enum):
17
17
  SPANRED = "span", "color: rgb(239, 69, 64)"
18
18
  SPANGREEN = "span", "color: rgb(152, 202, 62)"
19
19
  SPANORANGE = "span", "color: rgb(152, 100, 100)"
20
- ULIST = (
21
- "ul",
22
- "",
23
- )
24
- LISTITEM = (
25
- "li",
26
- "text-align: left;",
27
- )
20
+ ULIST = "ul", ""
21
+ LISTITEM = "li", "text-align: left;"
22
+ DIV = "div", ""
28
23
 
29
24
  def create(self, tag: str | None = None):
30
25
  if tag is None:
@@ -1,9 +1,9 @@
1
1
  import logging
2
- import re
3
2
 
4
3
  import lxml.etree as ET
5
4
 
6
5
  import excel2moodle.core.etHelpers as eth
6
+ from excel2moodle.core.bullets import BulletList
7
7
  from excel2moodle.core.exceptions import QNotParsedException
8
8
  from excel2moodle.core.globals import (
9
9
  Tags,
@@ -58,65 +58,19 @@ class QuestionParser:
58
58
  )
59
59
  return bool(self.question.picture.ready)
60
60
 
61
- def setMainText(self) -> None:
62
- paragraphs: list[ET._Element] = [TextElements.PLEFT.create()]
63
- ET.SubElement(paragraphs[0], "b").text = f"ID {self.question.id}"
61
+ def getMainTextElement(self) -> ET.Element:
62
+ """Get the root question Text with the question paragraphs."""
63
+ textHTMLroot: ET._Element = ET.Element("div")
64
+ ET.SubElement(
65
+ ET.SubElement(textHTMLroot, "p"), "b"
66
+ ).text = f"ID {self.question.id}"
64
67
  text = self.rawInput[Tags.TEXT]
65
68
  for t in text:
66
- paragraphs.append(TextElements.PLEFT.create())
67
- paragraphs[-1].text = t
68
- self.question.qtextParagraphs = paragraphs
69
+ par = TextElements.PLEFT.create()
70
+ par.text = t
71
+ textHTMLroot.append(par)
69
72
  self.logger.debug("Created main Text with: %s paragraphs", len(text))
70
-
71
- def setBPoints(self) -> None:
72
- """If there bulletPoints are set in the Spreadsheet it creates an unordered List-Element in ``Question.bulletList``."""
73
- if Tags.BPOINTS in self.rawInput:
74
- bps: list[str] = self.rawInput[Tags.BPOINTS]
75
- try:
76
- bulletList = self.formatBulletList(bps)
77
- except IndexError:
78
- msg = f"konnt Bullet Liste {self.question.id} nicht generieren"
79
- raise QNotParsedException(
80
- msg,
81
- self.question.id,
82
- # exc_info=e,
83
- )
84
- self.logger.debug(
85
- "Generated BPoint List: \n %s",
86
- ET.tostring(bulletList, encoding="unicode"),
87
- )
88
- self.question.bulletList = bulletList
89
-
90
- def formatBulletList(self, bps: list[str]) -> ET.Element:
91
- self.logger.debug("Formatting the bulletpoint list")
92
- name = []
93
- var = []
94
- quant = []
95
- unit = []
96
- unorderedList = TextElements.ULIST.create()
97
- for item in bps:
98
- sc_split = item.split()
99
- name.append(sc_split[0])
100
- var.append(sc_split[1])
101
- quant.append(sc_split[3])
102
- unit.append(sc_split[4])
103
- for i in range(len(name)):
104
- if re.fullmatch(r"{\w+}", quant[i]):
105
- self.logger.debug("Got an variable bulletItem")
106
- num_s = quant[i]
107
- else:
108
- self.logger.debug("Got a normal bulletItem")
109
- num = quant[i].split(",")
110
- if len(num) == 2:
111
- num_s = f"{num[0]!s},\\!{num[1]!s}~"
112
- else:
113
- num_s = f"{num[0]!s},\\!0~"
114
- bullet = TextElements.LISTITEM.create()
115
- bullet.text = (
116
- f"{name[i]}: \\( {var[i]} = {num_s} \\mathrm{{ {unit[i]} }}\\)\n"
117
- )
118
- unorderedList.append(bullet)
119
- return unorderedList
73
+ return textHTMLroot
120
74
 
121
75
  def appendToTmpEle(
122
76
  self,
@@ -148,7 +102,7 @@ class QuestionParser:
148
102
  """Parse the Question.
149
103
 
150
104
  Generates an new Question Element stored as ``self.tmpEle:ET.Element``
151
- if no Exceptions are raised, ``self.tmpEle`` is passed to ``self.question.element``
105
+ if no Exceptions are raised, ``self.tmpEle`` is passed to ``question.element``
152
106
  """
153
107
  self.logger.info("Starting to parse")
154
108
  self.tmpEle: ET.Elemnt = ET.Element(
@@ -156,21 +110,39 @@ class QuestionParser:
156
110
  )
157
111
  self.appendToTmpEle(XMLTags.NAME, text=Tags.NAME, txtEle=True)
158
112
  self.appendToTmpEle(XMLTags.ID, text=self.question.id)
159
- if self.hasPicture():
160
- self.tmpEle.append(self.question.picture.element)
161
- self.tmpEle.append(ET.Element(XMLTags.QTEXT, format="html"))
113
+ textRootElem = ET.Element(XMLTags.QTEXT, format="html")
114
+ mainTextEle = ET.SubElement(textRootElem, "text")
115
+ self.tmpEle.append(textRootElem)
162
116
  self.appendToTmpEle(XMLTags.POINTS, text=str(self.question.points))
163
117
  self._appendStandardTags()
164
118
  for feedb in self.genFeedbacks:
165
119
  self.tmpEle.append(eth.getFeedBEle(feedb))
120
+
121
+ self.htmlRoot = ET.Element("div")
122
+ self.htmlRoot.append(self.getMainTextElement())
123
+ if Tags.BPOINTS in self.rawInput:
124
+ bps: list[str] = self.rawInput[Tags.BPOINTS]
125
+ try:
126
+ bullets: BulletList = BulletList(bps, self.question.id)
127
+ except IndexError:
128
+ msg = f"konnt Bullet Liste {self.question.id} nicht generieren"
129
+ raise QNotParsedException(
130
+ msg,
131
+ self.question.id,
132
+ )
133
+ self.htmlRoot.append(bullets.element)
134
+ self.question.bulletList = bullets
135
+ if self.hasPicture():
136
+ self.htmlRoot.append(self.question.picture.htmlTag)
137
+ textRootElem.append(self.question.picture.element)
166
138
  ansList = self._parseAnswers()
167
- self.setMainText()
168
- self.setBPoints()
169
139
  if ansList is not None:
170
140
  for ele in ansList:
171
141
  self.tmpEle.append(ele)
172
- self.question.element = self.tmpEle
142
+ self.question._element = self.tmpEle
143
+ self.question.htmlRoot = self.htmlRoot
173
144
  self.question.isParsed = True
145
+ self.question.textElement = mainTextEle
174
146
  self.logger.info("Sucessfully parsed")
175
147
 
176
148
  def getFeedBEle(
@@ -195,7 +167,7 @@ class QuestionParser:
195
167
 
196
168
  def getNumericAnsElement(
197
169
  self,
198
- result: float,
170
+ result: float = 0.0,
199
171
  fraction: float = 100,
200
172
  format: str = "moodle_auto_format",
201
173
  ) -> ET.Element:
@@ -1,25 +1,26 @@
1
1
  import base64
2
2
  import logging
3
+ import math
3
4
  import re
4
5
  from pathlib import Path
5
- from re import Match
6
6
  from types import UnionType
7
- from typing import ClassVar, Literal, overload
7
+ from typing import TYPE_CHECKING, ClassVar, Literal, overload
8
8
 
9
9
  import lxml.etree as ET
10
+ from asteval import Interpreter
10
11
 
11
- from excel2moodle.core import etHelpers
12
12
  from excel2moodle.core.category import Category
13
13
  from excel2moodle.core.exceptions import QNotParsedException
14
14
  from excel2moodle.core.globals import (
15
15
  QUESTION_TYPES,
16
16
  Tags,
17
- TextElements,
18
- XMLTags,
19
17
  )
20
18
  from excel2moodle.core.settings import Settings, Tags
21
19
  from excel2moodle.logger import LogAdapterQuestionID
22
20
 
21
+ if TYPE_CHECKING:
22
+ from excel2moodle.core.bullets import BulletList
23
+
23
24
  loggerObj = logging.getLogger(__name__)
24
25
  settings = Settings()
25
26
 
@@ -130,12 +131,13 @@ class Question:
130
131
  self.category = category
131
132
  self.katName = self.category.name
132
133
  self.moodleType = QUESTION_TYPES[self.qtype]
133
- self.element: ET.Element = None
134
+ self._element: ET.Element = None
134
135
  self.isParsed: bool = False
135
136
  self.picture: Picture
136
137
  self.id: str
137
- self.qtextParagraphs: list[ET.Element] = []
138
- self.bulletList: ET.Element = None
138
+ self.htmlRoot: ET.Element
139
+ self.bulletList: BulletList
140
+ self.textElement: ET.Element
139
141
  self._setID()
140
142
  self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.id})
141
143
  self.logger.debug("Sucess initializing")
@@ -158,52 +160,19 @@ class Question:
158
160
  li.append(f"{self.qtype}")
159
161
  return "\t".join(li)
160
162
 
161
- def assemble(self, variant=0) -> None:
162
- """Assemble the question to the valid xml Tree."""
163
- mainText = self._getTextElement()
164
- self.logger.info("Starting assembly variant: %s", variant)
165
- self._assembleAnswer(variant=variant)
166
- textParts = self._assembleText(variant=variant)
167
- if hasattr(self, "picture") and self.picture.ready:
168
- mainText.append(self.picture.element)
169
- self.logger.debug("Appended Picture element to text")
170
- mainText.append(etHelpers.getCdatTxtElement(textParts))
171
-
172
- def _assembleText(self, variant=0) -> list[ET.Element]:
173
- """Assemble the Question Text.
174
-
175
- Intended for the cloze question, where the answers parts are part of the text.
163
+ def getUpdatedElement(self, variant: int = 0) -> ET.Element:
164
+ """Update and get the Question Elements to reflect the version.
165
+
166
+ Each Subclass needs to implement its specific logic.
167
+ Things needed to be considered:
168
+ Question Text
169
+ Bullet Points
170
+ Answers
176
171
  """
177
- self.logger.debug("inserting MainText to element")
178
- textParts: list[ET.Element] = []
179
- textParts.extend(self.qtextParagraphs)
180
- bullets = self._getBPoints(variant=variant)
181
- if bullets is not None:
182
- textParts.append(bullets)
183
- if hasattr(self, "picture") and self.picture.ready:
184
- textParts.append(self.picture.htmlTag)
185
- self.logger.debug("Appended Picture html to text")
186
- return textParts
187
-
188
- def _getTextElement(self) -> ET.Element:
189
- if self.element is not None:
190
- mainText = self.element.find(XMLTags.QTEXT)
191
- self.logger.debug(f"found existing Text in element {mainText=}")
192
- txtele = mainText.find("text")
193
- if txtele is not None:
194
- mainText.remove(txtele)
195
- self.logger.debug("removed previously existing questiontext")
196
- return mainText
197
- msg = "Cant assamble, if element is none"
198
- raise QNotParsedException(msg, self.id)
199
-
200
- def _getBPoints(self, variant: int = 0) -> ET.Element:
201
- if hasattr(self, "bulletList"):
202
- return self.bulletList
203
- return None
204
-
205
- def _assembleAnswer(self, variant: int = 0) -> None:
206
- pass
172
+ self.textElement.text = ET.CDATA(
173
+ ET.tostring(self.htmlRoot, encoding="unicode", pretty_print=True)
174
+ )
175
+ return self._element
207
176
 
208
177
  def _setID(self, id=0) -> None:
209
178
  if id == 0:
@@ -215,32 +184,131 @@ class Question:
215
184
  class ParametricQuestion(Question):
216
185
  def __init__(self, *args, **kwargs) -> None:
217
186
  super().__init__(*args, **kwargs)
218
- self.variants: int = 0
219
- self.variables: dict[str, list[float | int]] = {}
187
+ self.rules: list[str] = []
188
+ self.parametrics: Parametrics
189
+
190
+ def getUpdatedElement(self, variant: int = 0) -> ET.Element:
191
+ """Update the bulletItem.text With the values for variant.
220
192
 
221
- def _getBPoints(self, variant: int = 1) -> ET.Element:
222
- """Get the bullet points with the variable set for `variant`."""
223
- if self.bulletList is None:
193
+ `ParametricQuestion` updates the bullet points.
194
+ `Question` returns the Element.
195
+
196
+ """
197
+ if not hasattr(self, "bulletList"):
224
198
  msg = "Can't assemble a parametric question, without the bulletPoints variables"
225
199
  raise QNotParsedException(msg, self.id)
226
- # matches {a}, {some_var}, etc.
227
- varPlaceholder = re.compile(r"{(\w+)}")
228
-
229
- def replaceMatch(match: Match[str]) -> str | int | float:
230
- key = match.group(1)
231
- if key in self.variables:
232
- value = self.variables[key][variant - 1]
233
- return f"{value}".replace(".", ",\\!")
234
- return match.group(0) # keep original if no match
235
-
236
- unorderedList = TextElements.ULIST.create()
237
- for li in self.bulletList:
238
- listItemText = li.text or ""
239
- bullet = TextElements.LISTITEM.create()
240
- bullet.text = varPlaceholder.sub(replaceMatch, listItemText)
241
- unorderedList.append(bullet)
242
- self.logger.debug("Inserted Variables into List: %s", bullet.text)
243
- return unorderedList
200
+
201
+ self.bulletList.updateBullets(
202
+ variables=self.parametrics.variables, variant=variant
203
+ )
204
+ return super().getUpdatedElement(variant)
205
+
206
+
207
+ class Parametrics:
208
+ """Object for parametrizing the numeric Questions.
209
+
210
+ Equation storing the variables, equation, and results.
211
+ """
212
+
213
+ astEval = Interpreter(with_import=True)
214
+
215
+ def __init__(
216
+ self,
217
+ equation: str | dict[int, str],
218
+ firstResult: float | dict[int, float] = 0.0,
219
+ identifier: str = "0000",
220
+ ) -> None:
221
+ self.equations: dict[int, str] = (
222
+ equation if isinstance(equation, dict) else {1: equation}
223
+ )
224
+ self._resultChecker: dict[int, float] = (
225
+ firstResult if isinstance(firstResult, dict) else {1: firstResult}
226
+ )
227
+ self.id = identifier
228
+ self._variables: dict[str, list[float | int]] = {}
229
+ self.results: dict[int, list[float]] = {}
230
+ self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.id})
231
+
232
+ @property
233
+ def variants(self) -> int:
234
+ number = 1000
235
+ for li in self._variables.values():
236
+ number = min(number, len(li))
237
+ return number
238
+
239
+ @property
240
+ def variableRules(self) -> list[str]:
241
+ if hasattr(self, "_rules"):
242
+ return self._rules
243
+ return []
244
+
245
+ @variableRules.setter
246
+ def variableRules(self, rules: list[str]) -> None:
247
+ self._rules = rules
248
+
249
+ def getResult(self, number: int = 1, equation: int = 1) -> float:
250
+ """Get the result for the variant `number` from the `equation` number."""
251
+ self.logger.debug(
252
+ "Returning result %s, variant: %s",
253
+ self.results[equation][number - 1],
254
+ number,
255
+ )
256
+ return self.results[equation][number - 1]
257
+
258
+ @property
259
+ def variables(self) -> dict[str, list[float | int]]:
260
+ return self._variables
261
+
262
+ @variables.setter
263
+ def variables(self, variables: dict[str, list[float | int]]) -> None:
264
+ self._variables: dict[str, list[float | int]] = variables
265
+ self._calculateResults()
266
+ self.logger.info("Updated parameters and results")
267
+
268
+ def _calculateResults(self) -> dict[int, list[float]]:
269
+ self.logger.info("Updating Results for new variables")
270
+ for num in self.equations:
271
+ # reset the old results, and set a list for each equation
272
+ self.results[num] = []
273
+ for variant in range(self.variants):
274
+ type(self).setupAstIntprt(self._variables, variant)
275
+ self.logger.debug("Setup The interpreter for variant: %s", variant)
276
+ for num, eq in self.equations.items():
277
+ result = type(self).astEval(eq)
278
+ self.logger.info(
279
+ "Calculated expr. %s (variant %s): %s = %.3f ",
280
+ num,
281
+ variant,
282
+ eq,
283
+ result,
284
+ )
285
+ if isinstance(result, float | int):
286
+ if variant == 0 and not math.isclose(
287
+ result, self._resultChecker[num], rel_tol=0.01
288
+ ):
289
+ self.logger.warning(
290
+ "The calculated result %s differs from given firstResult: %s",
291
+ result,
292
+ self._resultChecker,
293
+ )
294
+ self.logger.debug(
295
+ "Calculated result %s for variant %s", result, variant
296
+ )
297
+ self.results[num].append(round(result, 3))
298
+ else:
299
+ msg = f"The expression {eq} lead to: {result=} could not be evaluated."
300
+ raise QNotParsedException(msg, self.id)
301
+ return self.results
302
+
303
+ def resetVariables(self) -> None:
304
+ self._variables = {}
305
+ self.logger.info("Reset the variables")
306
+
307
+ @classmethod
308
+ def setupAstIntprt(cls, var: dict[str, list[float | int]], index: int) -> None:
309
+ """Setup the asteval Interpreter with the variables."""
310
+ for name, value in var.items():
311
+ cls.astEval.symtable[name] = value[index]
244
312
 
245
313
 
246
314
  class Picture: