excel2moodle 0.5.2__tar.gz → 0.6.0__tar.gz
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-0.5.2/excel2moodle.egg-info → excel2moodle-0.6.0}/PKG-INFO +27 -2
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/README.md +25 -0
- excel2moodle-0.6.0/excel2moodle/core/bullets.py +98 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/core/dataStructure.py +3 -4
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/core/globals.py +3 -8
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/core/parser.py +37 -65
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/core/question.py +144 -76
- excel2moodle-0.6.0/excel2moodle/extra/variableGenerator.py +250 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/question_types/cloze.py +156 -125
- excel2moodle-0.6.0/excel2moodle/question_types/nfm.py +73 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/ui/UI_mainWindow.py +63 -36
- excel2moodle-0.6.0/excel2moodle/ui/UI_variableGenerator.py +197 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/ui/appUi.py +90 -23
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/ui/dialogs.py +44 -77
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/ui/equationChecker.py +2 -2
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/ui/treewidget.py +9 -24
- {excel2moodle-0.5.2 → excel2moodle-0.6.0/excel2moodle.egg-info}/PKG-INFO +27 -2
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle.egg-info/SOURCES.txt +4 -2
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle.egg-info/requires.txt +1 -1
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/pyproject.toml +2 -2
- excel2moodle-0.6.0/test/test_bullets.py +45 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/test/test_nfmParsing.py +9 -11
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/test/test_parseQuestion.py +0 -1
- excel2moodle-0.5.2/excel2moodle/core/numericMultiQ.py +0 -80
- excel2moodle-0.5.2/excel2moodle/question_types/nfm.py +0 -135
- excel2moodle-0.5.2/excel2moodle/ui/windowDoc.py +0 -27
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/LICENSE +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/MANIFEST.in +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/__init__.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/__main__.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/core/__init__.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/core/category.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/core/etHelpers.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/core/exceptions.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/core/settings.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/core/stringHelpers.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/core/validator.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/extra/__init__.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/extra/equationVerification.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/logger.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/question_types/__init__.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/question_types/mc.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/question_types/nf.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/ui/UI_equationChecker.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/ui/UI_exportSettingsDialog.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/ui/UI_variantDialog.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle/ui/__init__.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle.egg-info/dependency_links.txt +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle.egg-info/entry_points.txt +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/excel2moodle.egg-info/top_level.txt +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/setup.cfg +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/test/test_picture.py +0 -0
- {excel2moodle-0.5.2 → excel2moodle-0.6.0}/test/test_questionDataGet.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: excel2moodle
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.6.0
|
4
4
|
Summary: A package for converting questions from a spreadsheet, to valid moodle-xml
|
5
5
|
Author: Jakob Bosse
|
6
6
|
License-Expression: GPL-3.0-or-later
|
@@ -12,11 +12,11 @@ Classifier: Operating System :: OS Independent
|
|
12
12
|
Requires-Python: >=3.10
|
13
13
|
Description-Content-Type: text/markdown
|
14
14
|
License-File: LICENSE
|
15
|
-
Requires-Dist: pyside6>=6.8.0
|
16
15
|
Requires-Dist: pandas>=2.1.3
|
17
16
|
Requires-Dist: lxml>=5.4.0
|
18
17
|
Requires-Dist: asteval>=1.0.6
|
19
18
|
Requires-Dist: python-calamine>=0.3.2
|
19
|
+
Requires-Dist: pyside6-essentials>=6.8.0
|
20
20
|
Dynamic: license-file
|
21
21
|
|
22
22
|
# excel 2 Moodle
|
@@ -81,6 +81,31 @@ If You want to support my work as well, you can by me a [coffee](https://ko-fi.c
|
|
81
81
|
|
82
82
|
# Changelogs
|
83
83
|
|
84
|
+
## 0.6.0 (2025-07-12)
|
85
|
+
Added variable generator and other architechtural improvements
|
86
|
+
|
87
|
+
### documentation (1 change)
|
88
|
+
|
89
|
+
- [Documenting variable generator usage](https://gitlab.com/jbosse3/excel2moodle/-/commit/3e4d3019b29872b5cfddf5539d5ebe7638bca049)
|
90
|
+
|
91
|
+
### feature (5 changes)
|
92
|
+
|
93
|
+
- [Opening spreadsheet file works from within excel2moodle](https://gitlab.com/jbosse3/excel2moodle/-/commit/9470f12ea5f098745a3210b281a5144a938ae8b5)
|
94
|
+
- [Variables are copied to clipboard](https://gitlab.com/jbosse3/excel2moodle/-/commit/87a7e5ec75f899b293e89ad3c1742567e3ec1c29)
|
95
|
+
- [Removed dependence on pyside6-addons](https://gitlab.com/jbosse3/excel2moodle/-/commit/2b3a7cf48581c14bd9cb570cd61d1d41aa410e11)
|
96
|
+
- [Var Generator ready](https://gitlab.com/jbosse3/excel2moodle/-/commit/ea97f0639dc35a4c99a64ae3976ccc8a0ac5d109)
|
97
|
+
- [Merge development of BulletsObj, Parametrization and VarGenerator](https://gitlab.com/jbosse3/excel2moodle/-/commit/40b46f3c143e082f1bb985d6c8c4e68bb6b6a7a8)
|
98
|
+
|
99
|
+
### improvement (7 changes)
|
100
|
+
|
101
|
+
- [Adapted Param. Parser to use bullet Obj](https://gitlab.com/jbosse3/excel2moodle/-/commit/194cab7cc6aecb2d25d1cb9c1538ed7d607dd9e1)
|
102
|
+
- [Added bulleList Object](https://gitlab.com/jbosse3/excel2moodle/-/commit/4ea982b8d8dc270675d2cb059c59fa980ce38894)
|
103
|
+
- [Parametrics in beta stage](https://gitlab.com/jbosse3/excel2moodle/-/commit/7d04d8ef2fc603c1b12b6934c827ce079df5d540)
|
104
|
+
- [Refactor parse() method, to construct complete xml-Tree](https://gitlab.com/jbosse3/excel2moodle/-/commit/8dc4bea9aa0673d39357115254dd55b02c04114e)
|
105
|
+
- [Refactored question assembly to only update fields.](https://gitlab.com/jbosse3/excel2moodle/-/commit/d7accb69be3b4a1e65f59eeecfb463f2663fabd4)
|
106
|
+
- [Adapted NFM Question to parametricResult](https://gitlab.com/jbosse3/excel2moodle/-/commit/fe552cd2b538ca8886415c200e4a2a3ecc1fbb2f) ([merge request](https://gitlab.com/jbosse3/excel2moodle/-/merge_requests/5))
|
107
|
+
- [Implemented ParametricResult Object](https://gitlab.com/jbosse3/excel2moodle/-/commit/e36d025955f1cab8e0542d66263ab70e3d8980df) ([merge request](https://gitlab.com/jbosse3/excel2moodle/-/merge_requests/5))
|
108
|
+
|
84
109
|
## 0.5.2 (2025-06-30)
|
85
110
|
Extended Documentation and bugfix for import Module
|
86
111
|
|
@@ -60,6 +60,31 @@ If You want to support my work as well, you can by me a [coffee](https://ko-fi.c
|
|
60
60
|
|
61
61
|
# Changelogs
|
62
62
|
|
63
|
+
## 0.6.0 (2025-07-12)
|
64
|
+
Added variable generator and other architechtural improvements
|
65
|
+
|
66
|
+
### documentation (1 change)
|
67
|
+
|
68
|
+
- [Documenting variable generator usage](https://gitlab.com/jbosse3/excel2moodle/-/commit/3e4d3019b29872b5cfddf5539d5ebe7638bca049)
|
69
|
+
|
70
|
+
### feature (5 changes)
|
71
|
+
|
72
|
+
- [Opening spreadsheet file works from within excel2moodle](https://gitlab.com/jbosse3/excel2moodle/-/commit/9470f12ea5f098745a3210b281a5144a938ae8b5)
|
73
|
+
- [Variables are copied to clipboard](https://gitlab.com/jbosse3/excel2moodle/-/commit/87a7e5ec75f899b293e89ad3c1742567e3ec1c29)
|
74
|
+
- [Removed dependence on pyside6-addons](https://gitlab.com/jbosse3/excel2moodle/-/commit/2b3a7cf48581c14bd9cb570cd61d1d41aa410e11)
|
75
|
+
- [Var Generator ready](https://gitlab.com/jbosse3/excel2moodle/-/commit/ea97f0639dc35a4c99a64ae3976ccc8a0ac5d109)
|
76
|
+
- [Merge development of BulletsObj, Parametrization and VarGenerator](https://gitlab.com/jbosse3/excel2moodle/-/commit/40b46f3c143e082f1bb985d6c8c4e68bb6b6a7a8)
|
77
|
+
|
78
|
+
### improvement (7 changes)
|
79
|
+
|
80
|
+
- [Adapted Param. Parser to use bullet Obj](https://gitlab.com/jbosse3/excel2moodle/-/commit/194cab7cc6aecb2d25d1cb9c1538ed7d607dd9e1)
|
81
|
+
- [Added bulleList Object](https://gitlab.com/jbosse3/excel2moodle/-/commit/4ea982b8d8dc270675d2cb059c59fa980ce38894)
|
82
|
+
- [Parametrics in beta stage](https://gitlab.com/jbosse3/excel2moodle/-/commit/7d04d8ef2fc603c1b12b6934c827ce079df5d540)
|
83
|
+
- [Refactor parse() method, to construct complete xml-Tree](https://gitlab.com/jbosse3/excel2moodle/-/commit/8dc4bea9aa0673d39357115254dd55b02c04114e)
|
84
|
+
- [Refactored question assembly to only update fields.](https://gitlab.com/jbosse3/excel2moodle/-/commit/d7accb69be3b4a1e65f59eeecfb463f2663fabd4)
|
85
|
+
- [Adapted NFM Question to parametricResult](https://gitlab.com/jbosse3/excel2moodle/-/commit/fe552cd2b538ca8886415c200e4a2a3ecc1fbb2f) ([merge request](https://gitlab.com/jbosse3/excel2moodle/-/merge_requests/5))
|
86
|
+
- [Implemented ParametricResult Object](https://gitlab.com/jbosse3/excel2moodle/-/commit/e36d025955f1cab8e0542d66263ab70e3d8980df) ([merge request](https://gitlab.com/jbosse3/excel2moodle/-/merge_requests/5))
|
87
|
+
|
63
88
|
## 0.5.2 (2025-06-30)
|
64
89
|
Extended Documentation and bugfix for import Module
|
65
90
|
|
@@ -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() ==
|
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.
|
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
|
-
|
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
|
62
|
-
|
63
|
-
ET.
|
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
|
-
|
67
|
-
|
68
|
-
|
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 ``
|
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
|
-
|
160
|
-
|
161
|
-
self.tmpEle.append(
|
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.
|
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.
|
134
|
+
self._element: ET.Element = None
|
134
135
|
self.isParsed: bool = False
|
135
136
|
self.picture: Picture
|
136
137
|
self.id: str
|
137
|
-
self.
|
138
|
-
self.bulletList:
|
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
|
162
|
-
"""
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
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.
|
178
|
-
|
179
|
-
|
180
|
-
|
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.
|
219
|
-
self.
|
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
|
-
|
222
|
-
|
223
|
-
|
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
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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:
|