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.
- excel2moodle/__init__.py +72 -49
- excel2moodle/__main__.py +5 -15
- excel2moodle/core/category.py +53 -62
- excel2moodle/core/dataStructure.py +87 -52
- excel2moodle/core/etHelpers.py +21 -16
- excel2moodle/core/exceptions.py +11 -4
- excel2moodle/core/globals.py +41 -28
- excel2moodle/core/numericMultiQ.py +18 -13
- excel2moodle/core/parser.py +202 -149
- excel2moodle/core/question.py +120 -51
- excel2moodle/core/questionValidator.py +48 -37
- excel2moodle/core/stringHelpers.py +41 -23
- excel2moodle/extra/__init__.py +1 -3
- excel2moodle/extra/equationVerification.py +40 -22
- excel2moodle/ui/appUi.py +113 -69
- excel2moodle/ui/dialogs.py +36 -19
- excel2moodle/ui/settings.py +48 -10
- {excel2moodle-0.3.2.dist-info → excel2moodle-0.3.4.dist-info}/METADATA +7 -6
- excel2moodle-0.3.4.dist-info/RECORD +32 -0
- {excel2moodle-0.3.2.dist-info → excel2moodle-0.3.4.dist-info}/WHEEL +1 -1
- excel2moodle-0.3.4.dist-info/entry_points.txt +2 -0
- excel2moodle-0.3.2.dist-info/RECORD +0 -31
- {excel2moodle-0.3.2.dist-info → excel2moodle-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.3.2.dist-info → excel2moodle-0.3.4.dist-info}/top_level.txt +0 -0
excel2moodle/__init__.py
CHANGED
@@ -1,14 +1,20 @@
|
|
1
1
|
"""This Python program helps to create Moodle questions in less time.
|
2
2
|
|
3
|
-
The aim is to put alle the information for the questions into a spreadsheet
|
4
|
-
|
3
|
+
The aim is to put alle the information for the questions into a spreadsheet
|
4
|
+
file, and then parse it, to generate Moodle compliant XML-Files.
|
5
|
+
Furthermore this program lets you create a single ``.xml``-File with a selection
|
6
|
+
of questions, that then can be imported to a Moodle-Test.
|
5
7
|
|
6
8
|
Concept
|
7
9
|
=========
|
8
|
-
The concept is, to store the different questions into categories of similar
|
10
|
+
The concept is, to store the different questions into categories of similar
|
11
|
+
types and difficulties of questions, for each of which, a separated sheet in
|
12
|
+
the Spreadsheet document should be created.
|
9
13
|
|
10
|
-
There Should be a sheet called *"Kategorien"*, where an overview over the
|
11
|
-
|
14
|
+
There Should be a sheet called *"Kategorien"*, where an overview over the
|
15
|
+
different categories is stored.
|
16
|
+
This sheet stores The names and descriptions, for all categories.
|
17
|
+
The name have to be the same as the actual sheet names with the questions.
|
12
18
|
Furthermore the points used for grading, are set in the "Kategorien" sheet
|
13
19
|
|
14
20
|
Functionality
|
@@ -17,12 +23,34 @@ Functionality
|
|
17
23
|
* Parse Numeric Questions, each into one XML file
|
18
24
|
* create single XML File from a selection of questions
|
19
25
|
"""
|
26
|
+
|
27
|
+
from importlib import metadata
|
20
28
|
from importlib.metadata import version
|
29
|
+
|
21
30
|
try:
|
22
31
|
__version__ = version("excel2moodle")
|
23
32
|
except Exception:
|
24
33
|
__version__ = "unknown"
|
25
34
|
|
35
|
+
|
36
|
+
if __package__ is not None:
|
37
|
+
meta = metadata.metadata(__package__)
|
38
|
+
e2mMetadata: dict = {
|
39
|
+
"version": __version__,
|
40
|
+
"name": meta["name"],
|
41
|
+
"description": meta["summary"],
|
42
|
+
"author": meta["author"],
|
43
|
+
"license": meta["license-expression"],
|
44
|
+
"documentation": "https://jbosse3.gitlab.io/excel2moodle",
|
45
|
+
"homepage": meta["project-url"].split()[1],
|
46
|
+
"issues": "https://gitlab.com/jbosse3/excel2moodle/issues",
|
47
|
+
}
|
48
|
+
|
49
|
+
import logging as logging
|
50
|
+
from logging import config as logConfig
|
51
|
+
|
52
|
+
from PySide6.QtCore import QObject, Signal
|
53
|
+
|
26
54
|
# from excel2moodle.core import klausurGenerator
|
27
55
|
# from excel2moodle.core import numericMultiQ
|
28
56
|
# from excel2moodle.core import questionWriter
|
@@ -33,68 +61,63 @@ except Exception:
|
|
33
61
|
# from excel2moodle.ui import kGeneratorQt
|
34
62
|
from excel2moodle.ui import settings
|
35
63
|
|
36
|
-
from PySide6.QtCore import QObject, Signal
|
37
|
-
import logging as logging
|
38
|
-
from logging import config as logConfig
|
39
|
-
from pathlib import Path
|
40
|
-
|
41
64
|
loggerConfig = {
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
|
47
|
-
},
|
65
|
+
"version": 1,
|
66
|
+
"disable_existing_loggers": False,
|
67
|
+
"formatters": {
|
68
|
+
"standard": {"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"},
|
48
69
|
},
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
70
|
+
"handlers": {
|
71
|
+
"default": {
|
72
|
+
"level": "DEBUG",
|
73
|
+
"formatter": "standard",
|
74
|
+
"class": "logging.StreamHandler",
|
75
|
+
"stream": "ext://sys.stdout", # Default is stderr
|
55
76
|
},
|
56
77
|
},
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
78
|
+
"loggers": {
|
79
|
+
"": { # root logger
|
80
|
+
"handlers": ["default"],
|
81
|
+
"level": "DEBUG",
|
82
|
+
"propagate": True,
|
62
83
|
},
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
84
|
+
"excel2moodle.questionParser": {
|
85
|
+
"handlers": ["default"],
|
86
|
+
"level": "DEBUG",
|
87
|
+
"propagate": True,
|
67
88
|
},
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
89
|
+
"__main__": { # if __name__ == '__main__'
|
90
|
+
"handlers": ["default"],
|
91
|
+
"level": "DEBUG",
|
92
|
+
"propagate": True,
|
72
93
|
},
|
73
|
-
}
|
94
|
+
},
|
74
95
|
}
|
75
96
|
|
97
|
+
|
76
98
|
class QSignaler(QObject):
|
77
99
|
signal = Signal(str)
|
78
100
|
|
101
|
+
|
79
102
|
class LogHandler(logging.Handler):
|
80
|
-
def __init__(self, *args, **kwargs)->None:
|
81
|
-
super().__init__(*args
|
103
|
+
def __init__(self, *args, **kwargs) -> None:
|
104
|
+
super().__init__(*args, **kwargs)
|
82
105
|
self.emitter = QSignaler()
|
83
|
-
|
106
|
+
# Define a formatter with log level and module
|
84
107
|
log_format = "[%(levelname)s] %(module)s: %(message)s"
|
85
108
|
self.formatter = logging.Formatter(log_format)
|
86
109
|
self.setFormatter(self.formatter)
|
87
110
|
self.logLevelColors = {
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
111
|
+
"DEBUG": "gray",
|
112
|
+
"INFO": "green",
|
113
|
+
"WARNING": "orange",
|
114
|
+
"ERROR": "red",
|
115
|
+
"CRITICAL": "pink",
|
93
116
|
}
|
94
117
|
|
95
|
-
def emit(self, record)->None:
|
118
|
+
def emit(self, record) -> None:
|
96
119
|
log_message = self.format(record)
|
97
|
-
color = self.logLevelColors.get(record.levelname,
|
120
|
+
color = self.logLevelColors.get(record.levelname, "black")
|
98
121
|
prettyMessage = f'<span style="color:{color};">{log_message}</span>'
|
99
122
|
self.emitter.signal.emit(prettyMessage)
|
100
123
|
return None
|
@@ -108,6 +131,6 @@ logging.config.dictConfig(config=loggerConfig)
|
|
108
131
|
qSignalLogger = LogHandler()
|
109
132
|
logger.addHandler(qSignalLogger)
|
110
133
|
|
111
|
-
|
112
|
-
|
113
|
-
|
134
|
+
|
135
|
+
for k, v in e2mMetadata.items():
|
136
|
+
print(f"{k}: \t {v}\n")
|
excel2moodle/__main__.py
CHANGED
@@ -1,31 +1,21 @@
|
|
1
1
|
"""Main Function to make the Package executable"""
|
2
2
|
|
3
|
-
from pathlib import Path
|
4
|
-
|
5
3
|
from PySide6 import QtWidgets, sys
|
6
|
-
import xml.etree.ElementTree as xmlET
|
7
4
|
|
8
|
-
from
|
5
|
+
from excel2moodle.core import dataStructure
|
9
6
|
from excel2moodle.ui import appUi as ui
|
10
7
|
from excel2moodle.ui.settings import Settings
|
11
8
|
|
12
|
-
import logging
|
13
|
-
|
14
|
-
katOutPath = None
|
15
|
-
excelFile = None
|
16
|
-
|
17
|
-
logger = logging.getLogger(__name__)
|
18
9
|
|
19
|
-
|
20
|
-
def main()->None:
|
10
|
+
def main() -> None:
|
21
11
|
app = QtWidgets.QApplication(sys.argv)
|
22
12
|
settings = Settings()
|
23
|
-
database:dataStructure.QuestionDB = dataStructure.QuestionDB(settings)
|
13
|
+
database: dataStructure.QuestionDB = dataStructure.QuestionDB(settings)
|
24
14
|
window = ui.MainWindow(settings, database)
|
25
15
|
database.window = window
|
26
16
|
window.show()
|
27
17
|
sys.exit(app.exec())
|
28
18
|
|
29
|
-
if __name__ =="__main__":
|
30
|
-
main()
|
31
19
|
|
20
|
+
if __name__ == "__main__":
|
21
|
+
main()
|
excel2moodle/core/category.py
CHANGED
@@ -1,27 +1,38 @@
|
|
1
|
-
|
2
1
|
import logging
|
3
|
-
|
2
|
+
|
4
3
|
import lxml.etree as ET
|
4
|
+
import pandas as pd
|
5
5
|
|
6
|
-
from excel2moodle.core.
|
7
|
-
|
8
|
-
|
6
|
+
from excel2moodle.core.parser import (
|
7
|
+
MCQuestionParser,
|
8
|
+
NFMQuestionParser,
|
9
|
+
NFQuestionParser,
|
10
|
+
QNotParsedException,
|
11
|
+
)
|
9
12
|
from excel2moodle.core.question import Question
|
10
13
|
|
11
|
-
|
12
|
-
from excel2moodle.core.parser import NFMQuestionParser, NFQuestionParser, MCQuestionParser, QNotParsedException
|
14
|
+
logger = logging.getLogger(__name__)
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
+
|
17
|
+
class Category:
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
n: int,
|
21
|
+
name: str,
|
22
|
+
description: str,
|
23
|
+
dataframe: pd.DataFrame,
|
24
|
+
points: float = 0,
|
25
|
+
version: int = 0,
|
26
|
+
) -> None:
|
16
27
|
self.n = n
|
17
28
|
self.NAME = name
|
18
29
|
self.desc = str(description)
|
19
|
-
self.dataframe:pd.DataFrame = dataframe
|
30
|
+
self.dataframe: pd.DataFrame = dataframe
|
20
31
|
self.points = points
|
21
32
|
self.version = int(version)
|
22
|
-
self.questions:dict[int,Question] = {}
|
23
|
-
self.maxVariants:int|None = None
|
24
|
-
logger.info(f"initializing {self.NAME
|
33
|
+
self.questions: dict[int, Question] = {}
|
34
|
+
self.maxVariants: int | None = None
|
35
|
+
logger.info(f"initializing {self.NAME=}")
|
25
36
|
|
26
37
|
@property
|
27
38
|
def name(self):
|
@@ -39,70 +50,50 @@ class Category():
|
|
39
50
|
return self.NAME == other.NAME
|
40
51
|
return False
|
41
52
|
|
42
|
-
def
|
43
|
-
self
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
if isinstance(qdat, pd.Series):
|
49
|
-
validator.setup(qdat, q)
|
50
|
-
check = False
|
51
|
-
try:
|
52
|
-
check = validator.validate()
|
53
|
-
except InvalidFieldException as e:
|
54
|
-
logger.error(f"Frage {self.id}{q:02d} ist invalid.", exc_info=e)
|
55
|
-
if check:
|
56
|
-
self.questions[q]=validator.question
|
57
|
-
try:
|
58
|
-
self.parseQ(self.questions[q])
|
59
|
-
except QNotParsedException as e:
|
60
|
-
logger.error(f"Frage {self.questions[q].id} konnte nicht erstellt werden", exc_info=e)
|
61
|
-
return None
|
62
|
-
|
63
|
-
def parsAll(self, tree: ET.Element = None)->None:
|
64
|
-
if tree is None:
|
65
|
-
tree = ET.Element("quiz")
|
66
|
-
tree.append(self.getCategoryHeader())
|
67
|
-
for q in self.questions.values():
|
68
|
-
self.parseQ(q, tree)
|
69
|
-
|
70
|
-
def parseQ(self, q:Question, xmlTree:ET._Element|None=None)->bool:
|
53
|
+
def parseQ(
|
54
|
+
self,
|
55
|
+
q: Question,
|
56
|
+
questionData: dict | None = None,
|
57
|
+
xmlTree: ET._Element | None = None,
|
58
|
+
) -> bool:
|
71
59
|
if q.element is not None:
|
72
60
|
logger.info(f"Question {q.id} is already parsed")
|
73
61
|
return True
|
74
62
|
else:
|
75
|
-
series = self.dataframe[q.number]
|
76
63
|
if q.qtype == "NF":
|
77
|
-
parser = NFQuestionParser(
|
78
|
-
logger.debug(
|
64
|
+
parser = NFQuestionParser(q, questionData)
|
65
|
+
logger.debug("setup a new NF parser ")
|
79
66
|
elif q.qtype == "MC":
|
80
|
-
parser = MCQuestionParser(
|
81
|
-
logger.debug(
|
67
|
+
parser = MCQuestionParser(q, questionData)
|
68
|
+
logger.debug("setup a new MC parser ")
|
82
69
|
elif q.qtype == "NFM":
|
83
|
-
parser = NFMQuestionParser(
|
84
|
-
logger.debug(
|
85
|
-
else:
|
86
|
-
logger.error(
|
70
|
+
parser = NFMQuestionParser(q, questionData)
|
71
|
+
logger.debug("setup a new NFM parser ")
|
72
|
+
else:
|
73
|
+
logger.error("ERROR, couldn't setup Parser")
|
87
74
|
return False
|
88
75
|
try:
|
89
76
|
parser.parse(xmlTree=xmlTree)
|
90
77
|
return True
|
91
78
|
except QNotParsedException as e:
|
92
|
-
logger.critical(
|
79
|
+
logger.critical(
|
80
|
+
f"The Question {q.id} couldn't be parsed",
|
81
|
+
exc_info=e,
|
82
|
+
stack_info=True,
|
83
|
+
)
|
93
84
|
return False
|
94
85
|
finally:
|
95
86
|
del parser
|
96
87
|
|
97
|
-
def getCategoryHeader(self)->ET.Element:
|
98
|
-
"""vor den Fragen einer Kategorie wird ein
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
ET.SubElement(
|
103
|
-
ET.SubElement(
|
104
|
-
ET.SubElement(
|
88
|
+
def getCategoryHeader(self) -> ET.Element:
|
89
|
+
"""vor den Fragen einer Kategorie wird ein
|
90
|
+
<question type='category'> eingefügt mit Name und Beschreibung
|
91
|
+
"""
|
92
|
+
header = ET.Element("question", type="category")
|
93
|
+
cat = ET.SubElement(header, "category")
|
94
|
+
info = ET.SubElement(header, "info", format="html")
|
95
|
+
ET.SubElement(cat, "text").text = f"$module$/top/{self.NAME}"
|
96
|
+
ET.SubElement(info, "text").text = str(self.desc)
|
97
|
+
ET.SubElement(header, "idnumber").text = str(self.n)
|
105
98
|
ET.indent(header)
|
106
99
|
return header
|
107
|
-
|
108
|
-
|
@@ -1,86 +1,100 @@
|
|
1
|
-
"""Main Module which does the heavy lifting
|
1
|
+
"""Main Module which does the heavy lifting
|
2
2
|
|
3
3
|
At the heart is the class ``xmlTest``
|
4
4
|
"""
|
5
5
|
|
6
|
+
import logging
|
6
7
|
from pathlib import Path
|
7
|
-
|
8
|
-
|
8
|
+
|
9
|
+
import lxml.etree as ET
|
10
|
+
import pandas as pd
|
11
|
+
from PySide6 import QtCore, QtWidgets
|
9
12
|
from PySide6.QtCore import Signal
|
10
13
|
from PySide6.QtWidgets import QMainWindow, QMessageBox, QTreeWidget
|
11
|
-
import pandas as pd
|
12
|
-
import lxml.etree as ET
|
13
|
-
import logging
|
14
14
|
|
15
15
|
from excel2moodle import QSignaler
|
16
16
|
from excel2moodle.core import stringHelpers
|
17
17
|
from excel2moodle.core.category import Category
|
18
|
-
from excel2moodle.
|
19
|
-
from excel2moodle.
|
18
|
+
from excel2moodle.core.exceptions import InvalidFieldException, QNotParsedException
|
19
|
+
from excel2moodle.core.question import Question
|
20
20
|
from excel2moodle.core.questionValidator import Validator
|
21
|
+
from excel2moodle.ui.dialogs import QuestionVariantDialog
|
21
22
|
from excel2moodle.ui.settings import Settings
|
22
|
-
from excel2moodle.
|
23
|
-
from excel2moodle.core.exceptions import InvalidFieldException, QNotParsedException
|
24
|
-
|
23
|
+
from excel2moodle.ui.treewidget import CategoryItem, QuestionItem
|
25
24
|
|
26
25
|
logger = logging.getLogger(__name__)
|
27
26
|
|
28
27
|
|
29
|
-
class QuestionDB
|
28
|
+
class QuestionDB:
|
30
29
|
"""oberste Klasse für den Test"""
|
30
|
+
|
31
31
|
dataChanged = QSignaler()
|
32
32
|
|
33
|
-
def __init__(self, settings:Settings):
|
33
|
+
def __init__(self, settings: Settings):
|
34
34
|
self.settings = settings
|
35
35
|
self.spreadSheetPath = Path()
|
36
36
|
self.mainPath = Path()
|
37
|
-
self.window:QMainWindow|None = None
|
37
|
+
self.window: QMainWindow | None = None
|
38
38
|
self.version = None
|
39
39
|
self.categoriesMetaData = pd.DataFrame()
|
40
|
-
self.categories:dict[str,Category]={}
|
40
|
+
self.categories: dict[str, Category] = {}
|
41
41
|
self.settings.shPathChanged.connect(self.onSheetPathChanged)
|
42
42
|
|
43
43
|
@QtCore.Slot(Path)
|
44
|
-
def onSheetPathChanged(self, sheet:Path)->None:
|
44
|
+
def onSheetPathChanged(self, sheet: Path) -> None:
|
45
45
|
logger.debug("Slot, new Spreadsheet triggered")
|
46
46
|
self.spreadSheetPath = sheet
|
47
|
-
|
47
|
+
svgFolder = self.spreadSheetPath.parent / str(
|
48
|
+
self.settings.get("core/pictureSubFolder", default="Abbildungen_SVG")
|
49
|
+
)
|
50
|
+
svgFolder.resolve()
|
51
|
+
self.settings.set("core/pictureFolder", svgFolder)
|
48
52
|
self.retrieveCategoriesData()
|
49
53
|
self.parseAll()
|
50
54
|
|
51
|
-
def retrieveCategoriesData(self)->None:
|
52
|
-
"""Scans through the sheet with the metadata for all
|
55
|
+
def retrieveCategoriesData(self) -> None:
|
56
|
+
"""Scans through the sheet with the metadata for all categories
|
53
57
|
|
54
|
-
The information that will be shown in the UI like description
|
55
|
-
|
58
|
+
The information that will be shown in the UI like description
|
59
|
+
and points is retrieved from one spreadsheet sheet.
|
60
|
+
This method gathers this information and stores it in the
|
61
|
+
``categoriesMetaData`` dataframe
|
56
62
|
"""
|
57
63
|
|
58
64
|
logger.info("Start Parsing the Excel Metadata Sheet\n")
|
59
|
-
with open(self.spreadSheetPath,
|
65
|
+
with open(self.spreadSheetPath, "rb") as f:
|
60
66
|
excelFile = pd.ExcelFile(f)
|
61
|
-
self.categoriesMetaData = pd.read_excel(
|
62
|
-
|
63
|
-
|
67
|
+
self.categoriesMetaData = pd.read_excel(
|
68
|
+
f,
|
69
|
+
sheet_name="Kategorien",
|
70
|
+
usecols=["Kategorie", "Beschreibung", "Punkte", "Version"],
|
71
|
+
index_col=0,
|
72
|
+
)
|
64
73
|
logger.info("Sucessfully read categoriesMetaData")
|
65
74
|
print(self.categoriesMetaData)
|
75
|
+
self.categories = {}
|
66
76
|
for sh in excelFile.sheet_names:
|
67
77
|
if sh.startswith("KAT"):
|
68
78
|
n = int(sh[4:])
|
69
|
-
katDf = pd.read_excel(
|
79
|
+
katDf = pd.read_excel(
|
80
|
+
f, sheet_name=str(sh), index_col=0, header=None
|
81
|
+
)
|
70
82
|
if not katDf.empty:
|
71
|
-
p = self.categoriesMetaData["Punkte"].iloc[n-1]
|
72
|
-
points =
|
73
|
-
v = self.categoriesMetaData["Version"].iloc[n-1]
|
74
|
-
version =
|
75
|
-
self.categories[sh] = Category(
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
83
|
+
p = self.categoriesMetaData["Punkte"].iloc[n - 1]
|
84
|
+
points = p if not pd.isna(p) else 1
|
85
|
+
v = self.categoriesMetaData["Version"].iloc[n - 1]
|
86
|
+
version = v if not pd.isna(v) else 0
|
87
|
+
self.categories[sh] = Category(
|
88
|
+
n,
|
89
|
+
sh,
|
90
|
+
self.categoriesMetaData["Beschreibung"].iloc[n - 1],
|
91
|
+
dataframe=katDf,
|
92
|
+
points=points,
|
93
|
+
version=version,
|
94
|
+
)
|
80
95
|
self.dataChanged.signal.emit("whoo")
|
81
96
|
return None
|
82
97
|
|
83
|
-
|
84
98
|
def parseAll(self):
|
85
99
|
self.mainTree = ET.Element("quiz")
|
86
100
|
for c in self.categories.values():
|
@@ -94,17 +108,25 @@ class QuestionDB():
|
|
94
108
|
try:
|
95
109
|
check = validator.validate()
|
96
110
|
except InvalidFieldException as e:
|
97
|
-
logger.error(
|
98
|
-
|
99
|
-
|
111
|
+
logger.error(
|
112
|
+
f"Question {c.id}{
|
113
|
+
q:02d} is invalid.",
|
114
|
+
exc_info=e,
|
115
|
+
)
|
116
|
+
if check:
|
117
|
+
c.questions[q] = validator.question
|
100
118
|
try:
|
101
|
-
c.parseQ(c.questions[q])
|
119
|
+
c.parseQ(c.questions[q], validator.qdata)
|
102
120
|
except QNotParsedException as e:
|
103
|
-
logger.error(
|
121
|
+
logger.error(
|
122
|
+
f"Frage {
|
123
|
+
c.questions[q].id} konnte nicht erstellt werden",
|
124
|
+
exc_info=e,
|
125
|
+
)
|
104
126
|
|
105
|
-
def appendQuestions(self, questions:list[QuestionItem], file:Path|None=None):
|
127
|
+
def appendQuestions(self, questions: list[QuestionItem], file: Path | None = None):
|
106
128
|
tree = ET.Element("quiz")
|
107
|
-
catdict:dict[Category,list[Question]]={}
|
129
|
+
catdict: dict[Category, list[Question]] = {}
|
108
130
|
for q in questions:
|
109
131
|
logger.debug(f"got a question to append {q=}")
|
110
132
|
cat = q.parent().getCategory()
|
@@ -113,13 +135,25 @@ class QuestionDB():
|
|
113
135
|
print(f"Category is parent of Q {cat=}")
|
114
136
|
catdict[cat].append(q.getQuestion())
|
115
137
|
for cat, qlist in catdict.items():
|
116
|
-
print(f"{cat
|
117
|
-
self.appendQElements(
|
138
|
+
print(f"{cat=}, mit fragen {qlist=}")
|
139
|
+
self.appendQElements(
|
140
|
+
cat,
|
141
|
+
qlist,
|
142
|
+
tree=tree,
|
143
|
+
includeHeader=self.settings.value("testGen/includeCats"),
|
144
|
+
)
|
118
145
|
stringHelpers.printDom(tree, file=file)
|
119
146
|
|
120
|
-
def appendQElements(
|
147
|
+
def appendQElements(
|
148
|
+
self,
|
149
|
+
cat: Category,
|
150
|
+
qList: list[Question],
|
151
|
+
tree: ET.Element,
|
152
|
+
includeHeader: bool = True,
|
153
|
+
) -> None:
|
121
154
|
if includeHeader:
|
122
|
-
tree.append(
|
155
|
+
tree.append(cat.getCategoryHeader())
|
156
|
+
logger.debug(f"Appended a new category item {cat=}")
|
123
157
|
sameVariant = False
|
124
158
|
variant = 1
|
125
159
|
for q in qList:
|
@@ -132,10 +166,11 @@ class QuestionDB():
|
|
132
166
|
sameVariant = dialog.categoryWide
|
133
167
|
logger.debug(f"Die Fragen-Variante {variant} wurde gewählt")
|
134
168
|
q.assemble(variant)
|
135
|
-
else:
|
136
|
-
|
169
|
+
else:
|
170
|
+
print("skipping this question")
|
171
|
+
else:
|
172
|
+
q.assemble()
|
137
173
|
tree.append(q.element)
|
138
|
-
else:
|
174
|
+
else:
|
175
|
+
logger.warning(f"Frage {q} wurde nicht erstellt")
|
139
176
|
return None
|
140
|
-
|
141
|
-
|
excel2moodle/core/etHelpers.py
CHANGED
@@ -4,29 +4,31 @@ This module host different functions. All of them will return an ``lxml.etree.El
|
|
4
4
|
"""
|
5
5
|
|
6
6
|
import lxml.etree as ET
|
7
|
-
|
8
|
-
import pandas as pd
|
9
|
-
from excel2moodle.core.globals import feedbackStr, TextElements, feedBElements
|
7
|
+
|
10
8
|
import excel2moodle.core.etHelpers as eth
|
9
|
+
from excel2moodle.core.globals import TextElements, feedbackStr, feedBElements
|
10
|
+
|
11
|
+
from .globals import DFIndex, XMLTags
|
12
|
+
|
11
13
|
|
12
|
-
def getElement(
|
14
|
+
def getElement(eleName: str, text: str, **attribs) -> ET.Element:
|
13
15
|
"""Creates an XML-Element with text
|
14
|
-
|
16
|
+
|
15
17
|
If ``type(text)``is a ``QuestionFields``, the specific field is directly read.
|
16
18
|
Otherwise it will include whatever is ``text`` as a string
|
17
19
|
:param **kwargs: are treated as attributes for the Element
|
18
|
-
raises:
|
20
|
+
raises:
|
19
21
|
NanException if the spreadsheet cell of text:QuestionFields is ``nan``
|
20
22
|
"""
|
21
23
|
|
22
24
|
toEle = ET.Element(eleName)
|
23
25
|
toEle.text = str(text)
|
24
26
|
for k, v in attribs.items():
|
25
|
-
toEle.set(k,v)
|
27
|
+
toEle.set(k, v)
|
26
28
|
return toEle
|
27
29
|
|
28
30
|
|
29
|
-
def getTextElement(
|
31
|
+
def getTextElement(eleName: str, text: str | DFIndex, **attribs) -> ET.Element:
|
30
32
|
"""Creates two nested elements: ``eleName`` with child ``text`` which holds the text"""
|
31
33
|
|
32
34
|
toEle = ET.Element(eleName, **attribs)
|
@@ -34,28 +36,31 @@ def getTextElement( eleName:str, text:str|DFIndex, **attribs)->ET.Element:
|
|
34
36
|
toEle.append(child)
|
35
37
|
return toEle
|
36
38
|
|
37
|
-
|
39
|
+
|
40
|
+
def getCdatTxtElement(subEle: ET._Element | list[ET._Element]) -> ET.Element:
|
38
41
|
"""Puts all ``subEle`` as ``str`` into a ``<text><![CDATA[...subEle...]]</text>`` element"""
|
39
42
|
|
40
43
|
textEle = ET.Element(XMLTags.TEXT)
|
41
44
|
if isinstance(subEle, list):
|
42
|
-
elementString
|
45
|
+
elementString: list = []
|
43
46
|
for i in subEle:
|
44
47
|
elementString.append(ET.tostring(i, encoding="unicode", pretty_print=True))
|
45
48
|
textEle.text = ET.CDATA("".join(elementString))
|
46
49
|
return textEle
|
47
50
|
else:
|
48
|
-
textEle.text = ET.CDATA(
|
49
|
-
|
51
|
+
textEle.text = ET.CDATA(
|
52
|
+
ET.tostring(subEle, encoding="unicode", pretty_print=True)
|
53
|
+
)
|
54
|
+
return textEle
|
50
55
|
|
51
56
|
|
52
|
-
def getFeedBEle(
|
53
|
-
|
54
|
-
|
57
|
+
def getFeedBEle(
|
58
|
+
feedback: XMLTags, text: str | None = None, style: TextElements | None = None
|
59
|
+
) -> ET.Element:
|
55
60
|
"""Gets ET Elements with the feedback for the question."""
|
56
61
|
if style is None:
|
57
62
|
span = feedBElements[feedback]
|
58
|
-
else:
|
63
|
+
else:
|
59
64
|
span = style.create()
|
60
65
|
if text is None:
|
61
66
|
text = feedbackStr[feedback]
|