excel2moodle 0.3.1__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 +113 -0
- excel2moodle/__main__.py +31 -0
- excel2moodle/core/__init__.py +13 -0
- excel2moodle/core/category.py +108 -0
- excel2moodle/core/dataStructure.py +140 -0
- excel2moodle/core/etHelpers.py +67 -0
- excel2moodle/core/exceptions.py +20 -0
- excel2moodle/core/globals.py +128 -0
- excel2moodle/core/numericMultiQ.py +76 -0
- excel2moodle/core/parser.py +322 -0
- excel2moodle/core/question.py +106 -0
- excel2moodle/core/questionValidator.py +124 -0
- excel2moodle/core/questionWriter.py +174 -0
- excel2moodle/core/stringHelpers.py +94 -0
- excel2moodle/extra/__init__.py +9 -0
- excel2moodle/extra/equationVerification.py +124 -0
- excel2moodle/ui/__init__.py +1 -0
- excel2moodle/ui/appUi.py +243 -0
- excel2moodle/ui/dialogs.py +80 -0
- excel2moodle/ui/questionPreviewDialog.py +115 -0
- excel2moodle/ui/settings.py +34 -0
- excel2moodle/ui/treewidget.py +65 -0
- excel2moodle/ui/variantDialog.py +132 -0
- excel2moodle/ui/windowDoc.py +35 -0
- excel2moodle/ui/windowEquationChecker.py +187 -0
- excel2moodle-0.3.1.dist-info/METADATA +63 -0
- excel2moodle-0.3.1.dist-info/RECORD +30 -0
- excel2moodle-0.3.1.dist-info/WHEEL +5 -0
- excel2moodle-0.3.1.dist-info/licenses/LICENSE +674 -0
- excel2moodle-0.3.1.dist-info/top_level.txt +1 -0
excel2moodle/__init__.py
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
"""This Python program helps to create Moodle questions in less time.
|
2
|
+
|
3
|
+
The aim is to put alle the information for the questions into a spreadsheet file, and then parse it, to generate Moodle compliant XML-Files.
|
4
|
+
Furthermore this program lets you create a single ``.xml``-File with a selection of questions, that then can be imported to a Moodle-Test.
|
5
|
+
|
6
|
+
Concept
|
7
|
+
=========
|
8
|
+
The concept is, to store the different questions into categories of similar types and difficulties of questions, for each of which, a separated sheet in the Spreadsheet document should be created.
|
9
|
+
|
10
|
+
There Should be a sheet called *"Kategorien"*, where an overview over the different categories is stored.
|
11
|
+
This sheet stores The names and descriptions, for all categories. The name have to be the same as the actual sheet names with the questions.
|
12
|
+
Furthermore the points used for grading, are set in the "Kategorien" sheet
|
13
|
+
|
14
|
+
Functionality
|
15
|
+
===============
|
16
|
+
* Parse multiple Choice Questions, each into one XML file
|
17
|
+
* Parse Numeric Questions, each into one XML file
|
18
|
+
* create single XML File from a selection of questions
|
19
|
+
"""
|
20
|
+
from importlib.metadata import version
|
21
|
+
try:
|
22
|
+
__version__ = version("excel2moodle")
|
23
|
+
except Exception:
|
24
|
+
__version__ = "unknown"
|
25
|
+
|
26
|
+
# from excel2moodle.core import klausurGenerator
|
27
|
+
# from excel2moodle.core import numericMultiQ
|
28
|
+
# from excel2moodle.core import questionWriter
|
29
|
+
# from excel2moodle.core import questionParser
|
30
|
+
# from excel2moodle.core import stringHelpers
|
31
|
+
# from excel2moodle.core import globals
|
32
|
+
#
|
33
|
+
# from excel2moodle.ui import kGeneratorQt
|
34
|
+
from excel2moodle.ui import settings
|
35
|
+
|
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
|
+
loggerConfig = {
|
42
|
+
'version': 1,
|
43
|
+
'disable_existing_loggers': False,
|
44
|
+
'formatters': {
|
45
|
+
'standard': {
|
46
|
+
'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
|
47
|
+
},
|
48
|
+
},
|
49
|
+
'handlers': {
|
50
|
+
'default': {
|
51
|
+
'level': 'DEBUG',
|
52
|
+
'formatter': 'standard',
|
53
|
+
'class': 'logging.StreamHandler',
|
54
|
+
'stream': 'ext://sys.stdout', # Default is stderr
|
55
|
+
},
|
56
|
+
},
|
57
|
+
'loggers': {
|
58
|
+
'': { # root logger
|
59
|
+
'handlers': ['default'],
|
60
|
+
'level': 'DEBUG',
|
61
|
+
'propagate': True
|
62
|
+
},
|
63
|
+
'excel2moodle.questionParser': {
|
64
|
+
'handlers': ['default'],
|
65
|
+
'level': 'DEBUG',
|
66
|
+
'propagate': True
|
67
|
+
},
|
68
|
+
'__main__': { # if __name__ == '__main__'
|
69
|
+
'handlers': ['default'],
|
70
|
+
'level': 'DEBUG',
|
71
|
+
'propagate': True
|
72
|
+
},
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
class QSignaler(QObject):
|
77
|
+
signal = Signal(str)
|
78
|
+
|
79
|
+
class LogHandler(logging.Handler):
|
80
|
+
def __init__(self, *args, **kwargs)->None:
|
81
|
+
super().__init__(*args,**kwargs)
|
82
|
+
self.emitter = QSignaler()
|
83
|
+
# Define a formatter with log level and module
|
84
|
+
log_format = "[%(levelname)s] %(module)s: %(message)s"
|
85
|
+
self.formatter = logging.Formatter(log_format)
|
86
|
+
self.setFormatter(self.formatter)
|
87
|
+
self.logLevelColors = {
|
88
|
+
'DEBUG': 'gray',
|
89
|
+
'INFO': 'green',
|
90
|
+
'WARNING': 'orange',
|
91
|
+
'ERROR': 'red',
|
92
|
+
'CRITICAL': 'pink'
|
93
|
+
}
|
94
|
+
|
95
|
+
def emit(self, record)->None:
|
96
|
+
log_message = self.format(record)
|
97
|
+
color = self.logLevelColors.get(record.levelname, 'black')
|
98
|
+
prettyMessage = f'<span style="color:{color};">{log_message}</span>'
|
99
|
+
self.emitter.signal.emit(prettyMessage)
|
100
|
+
return None
|
101
|
+
|
102
|
+
|
103
|
+
settings = settings.Settings()
|
104
|
+
|
105
|
+
logger = logging.getLogger(__name__)
|
106
|
+
logging.config.dictConfig(config=loggerConfig)
|
107
|
+
|
108
|
+
qSignalLogger = LogHandler()
|
109
|
+
logger.addHandler(qSignalLogger)
|
110
|
+
|
111
|
+
p = Path(__file__).parent
|
112
|
+
dirProjectRoot = p.parent.resolve()
|
113
|
+
dirDocumentation = (dirProjectRoot / "docs/_build/html")
|
excel2moodle/__main__.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
"""Main Function to make the Package executable"""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from PySide6 import QtWidgets, sys
|
6
|
+
import xml.etree.ElementTree as xmlET
|
7
|
+
|
8
|
+
from excel2moodle.core import dataStructure
|
9
|
+
from excel2moodle.ui import appUi as ui
|
10
|
+
from excel2moodle.ui.settings import Settings
|
11
|
+
|
12
|
+
import logging
|
13
|
+
|
14
|
+
katOutPath = None
|
15
|
+
excelFile = None
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
def main()->None:
|
21
|
+
app = QtWidgets.QApplication(sys.argv)
|
22
|
+
settings = Settings()
|
23
|
+
database:dataStructure.QuestionDB = dataStructure.QuestionDB(settings)
|
24
|
+
window = ui.MainWindow(settings, database)
|
25
|
+
database.window = window
|
26
|
+
window.show()
|
27
|
+
sys.exit(app.exec())
|
28
|
+
|
29
|
+
if __name__ =="__main__":
|
30
|
+
main()
|
31
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"""
|
2
|
+
These Modules are the heart of the excel2moodle Package
|
3
|
+
"""
|
4
|
+
|
5
|
+
# from pandas._config import config
|
6
|
+
# from excel2moodle.core import questionWriter
|
7
|
+
# from excel2moodle.core import stringHelpers
|
8
|
+
# from excel2moodle.core import numericMultiQ
|
9
|
+
# from excel2moodle.core import globals
|
10
|
+
# from PySide6.QtCore import QObject, Signal
|
11
|
+
# import logging as logging
|
12
|
+
# from logging import config as logConfig
|
13
|
+
#
|
@@ -0,0 +1,108 @@
|
|
1
|
+
|
2
|
+
import logging
|
3
|
+
import pandas as pd
|
4
|
+
import lxml.etree as ET
|
5
|
+
|
6
|
+
from excel2moodle.core.exceptions import InvalidFieldException
|
7
|
+
from excel2moodle.core.questionValidator import Validator
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
from excel2moodle.core.question import Question
|
10
|
+
|
11
|
+
from excel2moodle.core.globals import XMLTags, TextElements, DFIndex, questionTypes, parserSettings
|
12
|
+
from excel2moodle.core.parser import NFMQuestionParser, NFQuestionParser, MCQuestionParser, QNotParsedException
|
13
|
+
|
14
|
+
class Category():
|
15
|
+
def __init__(self, n:int, name:str, description:str, dataframe: pd.DataFrame, points:float=0, version:int=0)->None:
|
16
|
+
self.n = n
|
17
|
+
self.NAME = name
|
18
|
+
self.desc = str(description)
|
19
|
+
self.dataframe:pd.DataFrame = dataframe
|
20
|
+
self.points = points
|
21
|
+
self.version = int(version)
|
22
|
+
self.questions:dict[int,Question] = {}
|
23
|
+
self.maxVariants:int|None = None
|
24
|
+
logger.info(f"initializing {self.NAME =}")
|
25
|
+
|
26
|
+
@property
|
27
|
+
def name(self):
|
28
|
+
return self.NAME
|
29
|
+
|
30
|
+
@property
|
31
|
+
def id(self):
|
32
|
+
return f"{self.version}{self.n:02d}"
|
33
|
+
|
34
|
+
def __hash__(self) -> int:
|
35
|
+
return hash(self.NAME)
|
36
|
+
|
37
|
+
def __eq__(self, other: object, /) -> bool:
|
38
|
+
if isinstance(other, Category):
|
39
|
+
return self.NAME == other.NAME
|
40
|
+
return False
|
41
|
+
|
42
|
+
def _getQuestions(self)->None:
|
43
|
+
self.questions:dict[int,Question] = {}
|
44
|
+
validator = Validator(self)
|
45
|
+
for q in self.dataframe.columns:
|
46
|
+
logger.debug(f"Starting to check Validity of {q}")
|
47
|
+
qdat = self.dataframe[q]
|
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:
|
71
|
+
if q.element is not None:
|
72
|
+
logger.info(f"Question {q.id} is already parsed")
|
73
|
+
return True
|
74
|
+
else:
|
75
|
+
series = self.dataframe[q.number]
|
76
|
+
if q.qtype == "NF":
|
77
|
+
parser = NFQuestionParser( q, series)
|
78
|
+
logger.debug(f"setup a new NF parser ")
|
79
|
+
elif q.qtype == "MC":
|
80
|
+
parser = MCQuestionParser( q, series)
|
81
|
+
logger.debug(f"setup a new MC parser ")
|
82
|
+
elif q.qtype == "NFM":
|
83
|
+
parser = NFMQuestionParser( q, series)
|
84
|
+
logger.debug(f"setup a new NFM parser ")
|
85
|
+
else:
|
86
|
+
logger.error(f"ERROR, couldn't setup Parser")
|
87
|
+
return False
|
88
|
+
try:
|
89
|
+
parser.parse(xmlTree=xmlTree)
|
90
|
+
return True
|
91
|
+
except QNotParsedException as e:
|
92
|
+
logger.critical(f"The Question {q.id} couldn't be parsed", exc_info=e, stack_info=True)
|
93
|
+
return False
|
94
|
+
finally:
|
95
|
+
del parser
|
96
|
+
|
97
|
+
def getCategoryHeader(self)->ET.Element:
|
98
|
+
"""vor den Fragen einer Kategorie wird ein <question type='category'> eingefügt mit Name und Beschreibung"""
|
99
|
+
header = ET.Element('question', type='category')
|
100
|
+
cat = ET.SubElement(header,"category")
|
101
|
+
info = ET.SubElement(header,"info", format='html')
|
102
|
+
ET.SubElement(cat,"text").text = f"$module$/top/{self.NAME}"
|
103
|
+
ET.SubElement(info,"text").text = str(self.desc)
|
104
|
+
ET.SubElement(header,"idnumber").text = str(self.n)
|
105
|
+
ET.indent(header)
|
106
|
+
return header
|
107
|
+
|
108
|
+
|
@@ -0,0 +1,140 @@
|
|
1
|
+
"""Main Module which does the heavy lifting
|
2
|
+
|
3
|
+
At the heart is the class ``xmlTest``
|
4
|
+
"""
|
5
|
+
|
6
|
+
from pathlib import Path
|
7
|
+
from PySide6 import QtCore
|
8
|
+
from PySide6 import QtWidgets
|
9
|
+
from PySide6.QtCore import Signal
|
10
|
+
from PySide6.QtWidgets import QMainWindow, QMessageBox, QTreeWidget
|
11
|
+
import pandas as pd
|
12
|
+
import lxml.etree as ET
|
13
|
+
import logging
|
14
|
+
|
15
|
+
from excel2moodle import QSignaler
|
16
|
+
from excel2moodle.core import stringHelpers
|
17
|
+
from excel2moodle.core.category import Category
|
18
|
+
from excel2moodle.ui.dialogs import QuestionVariantDialog
|
19
|
+
from excel2moodle.ui.treewidget import QuestionItem, CategoryItem
|
20
|
+
from excel2moodle.core.questionValidator import Validator
|
21
|
+
from excel2moodle.ui.settings import Settings
|
22
|
+
from excel2moodle.core.question import Question
|
23
|
+
from excel2moodle.core.exceptions import InvalidFieldException, QNotParsedException
|
24
|
+
|
25
|
+
|
26
|
+
logger = logging.getLogger(__name__)
|
27
|
+
|
28
|
+
|
29
|
+
class QuestionDB():
|
30
|
+
"""oberste Klasse für den Test"""
|
31
|
+
dataChanged = QSignaler()
|
32
|
+
|
33
|
+
def __init__(self, settings:Settings):
|
34
|
+
self.settings = settings
|
35
|
+
self.spreadSheetPath = Path()
|
36
|
+
self.mainPath = Path()
|
37
|
+
self.window:QMainWindow|None = None
|
38
|
+
self.version = None
|
39
|
+
self.categoriesMetaData = pd.DataFrame()
|
40
|
+
self.categories:dict[str,Category]={}
|
41
|
+
self.settings.shPathChanged.connect(self.onSheetPathChanged)
|
42
|
+
|
43
|
+
@QtCore.Slot(Path)
|
44
|
+
def onSheetPathChanged(self, sheet:Path)->None:
|
45
|
+
logger.debug("Slot, new Spreadsheet triggered")
|
46
|
+
self.spreadSheetPath = sheet
|
47
|
+
self.svgFolder = (self.spreadSheetPath.parent / 'Abbildungen_SVG')
|
48
|
+
self.retrieveCategoriesData()
|
49
|
+
|
50
|
+
def retrieveCategoriesData(self)->None:
|
51
|
+
"""Scans through the sheet with the metadata for all the question categories
|
52
|
+
|
53
|
+
The information that will be shown in the UI like description and points is retrieved from one spreadsheet sheet.
|
54
|
+
This method gathers this information and stores it in the ``categoriesMetaData`` dataframe
|
55
|
+
"""
|
56
|
+
|
57
|
+
logger.info("Start Parsing the Excel Metadata Sheet\n")
|
58
|
+
with open(self.spreadSheetPath, 'rb') as f:
|
59
|
+
excelFile = pd.ExcelFile(f)
|
60
|
+
self.categoriesMetaData = pd.read_excel(f, sheet_name="Kategorien",
|
61
|
+
usecols=["Kategorie", "Beschreibung", "Punkte", "Version"],
|
62
|
+
index_col=0)
|
63
|
+
logger.info("Sucessfully read categoriesMetaData")
|
64
|
+
print(self.categoriesMetaData)
|
65
|
+
for sh in excelFile.sheet_names:
|
66
|
+
if sh.startswith("KAT"):
|
67
|
+
n = int(sh[4:])
|
68
|
+
katDf = pd.read_excel(f, sheet_name=str(sh), index_col=0, header=None)
|
69
|
+
if not katDf.empty:
|
70
|
+
p = self.categoriesMetaData["Punkte"].iloc[n-1]
|
71
|
+
points = (p if not pd.isna(p) else 1)
|
72
|
+
v = self.categoriesMetaData["Version"].iloc[n-1]
|
73
|
+
version = (v if not pd.isna(v) else 0)
|
74
|
+
self.categories[sh] = Category(n,sh,
|
75
|
+
self.categoriesMetaData["Beschreibung"].iloc[n-1],
|
76
|
+
dataframe=katDf,
|
77
|
+
points=points,
|
78
|
+
version=version)
|
79
|
+
self.dataChanged.signal.emit("whoo")
|
80
|
+
return None
|
81
|
+
|
82
|
+
|
83
|
+
def parseAll(self):
|
84
|
+
self.mainTree = ET.Element("quiz")
|
85
|
+
for c in self.categories.values():
|
86
|
+
validator = Validator(c)
|
87
|
+
for q in c.dataframe.columns:
|
88
|
+
logger.debug(f"Starting to check Validity of {q}")
|
89
|
+
qdat = c.dataframe[q]
|
90
|
+
if isinstance(qdat, pd.Series):
|
91
|
+
validator.setup(qdat, q)
|
92
|
+
check = False
|
93
|
+
try:
|
94
|
+
check = validator.validate()
|
95
|
+
except InvalidFieldException as e:
|
96
|
+
logger.error(f"Question {c.id}{q:02d} is invalid.", exc_info=e)
|
97
|
+
if check:
|
98
|
+
c.questions[q]=validator.question
|
99
|
+
try:
|
100
|
+
c.parseQ(c.questions[q])
|
101
|
+
except QNotParsedException as e:
|
102
|
+
logger.error(f"Frage {c.questions[q].id} konnte nicht erstellt werden", exc_info=e)
|
103
|
+
|
104
|
+
def appendQuestions(self, questions:list[QuestionItem], file:Path|None=None):
|
105
|
+
tree = ET.Element("quiz")
|
106
|
+
catdict:dict[Category,list[Question]]={}
|
107
|
+
for q in questions:
|
108
|
+
logger.debug(f"got a question to append {q=}")
|
109
|
+
cat = q.parent().getCategory()
|
110
|
+
if cat not in catdict:
|
111
|
+
catdict[cat] = []
|
112
|
+
print(f"Category is parent of Q {cat=}")
|
113
|
+
catdict[cat].append(q.getQuestion())
|
114
|
+
for cat, qlist in catdict.items():
|
115
|
+
print(f"{cat = }, mit fragen {qlist = }")
|
116
|
+
self.appendQElements(cat, qlist, tree=tree, includeHeader=self.settings.value("testGen/includeCats"))
|
117
|
+
stringHelpers.printDom(tree, file=file)
|
118
|
+
|
119
|
+
def appendQElements(self,cat:Category, qList:list[Question], tree:ET.Element, includeHeader:bool=True)->None:
|
120
|
+
if includeHeader:
|
121
|
+
tree.append( cat.getCategoryHeader())
|
122
|
+
sameVariant = False
|
123
|
+
variant = 1
|
124
|
+
for q in qList:
|
125
|
+
if cat.parseQ(q):
|
126
|
+
if q.variants is not None:
|
127
|
+
if sameVariant is False:
|
128
|
+
dialog = QuestionVariantDialog(self.window, q)
|
129
|
+
if dialog.exec() == QtWidgets.QDialog.Accepted:
|
130
|
+
variant = dialog.variant
|
131
|
+
sameVariant = dialog.categoryWide
|
132
|
+
logger.debug(f"Die Fragen-Variante {variant} wurde gewählt")
|
133
|
+
q.assemble(variant)
|
134
|
+
else: print("skipping this question")
|
135
|
+
q.assemble
|
136
|
+
tree.append(q.element)
|
137
|
+
else: logger.warning(f"Frage {q} wurde nicht erstellt")
|
138
|
+
return None
|
139
|
+
|
140
|
+
|
@@ -0,0 +1,67 @@
|
|
1
|
+
"""Helper Module which aids in creating XML-Elements for the Questions
|
2
|
+
|
3
|
+
This module host different functions. All of them will return an ``lxml.etree.Element``
|
4
|
+
"""
|
5
|
+
|
6
|
+
import lxml.etree as ET
|
7
|
+
from .globals import DFIndex, XMLTags
|
8
|
+
import pandas as pd
|
9
|
+
from excel2moodle.core.globals import feedbackStr, TextElements, feedBElements
|
10
|
+
import excel2moodle.core.etHelpers as eth
|
11
|
+
|
12
|
+
def getElement( eleName : str, text: str, **attribs)->ET.Element:
|
13
|
+
"""Creates an XML-Element with text
|
14
|
+
|
15
|
+
If ``type(text)``is a ``QuestionFields``, the specific field is directly read.
|
16
|
+
Otherwise it will include whatever is ``text`` as a string
|
17
|
+
:param **kwargs: are treated as attributes for the Element
|
18
|
+
raises:
|
19
|
+
NanException if the spreadsheet cell of text:QuestionFields is ``nan``
|
20
|
+
"""
|
21
|
+
|
22
|
+
toEle = ET.Element(eleName)
|
23
|
+
toEle.text = str(text)
|
24
|
+
for k, v in attribs.items():
|
25
|
+
toEle.set(k,v)
|
26
|
+
return toEle
|
27
|
+
|
28
|
+
|
29
|
+
def getTextElement( eleName:str, text:str|DFIndex, **attribs)->ET.Element:
|
30
|
+
"""Creates two nested elements: ``eleName`` with child ``text`` which holds the text"""
|
31
|
+
|
32
|
+
toEle = ET.Element(eleName, **attribs)
|
33
|
+
child = getElement("text", text)
|
34
|
+
toEle.append(child)
|
35
|
+
return toEle
|
36
|
+
|
37
|
+
def getCdatTxtElement(subEle: ET._Element|list[ET._Element])->ET.Element:
|
38
|
+
"""Puts all ``subEle`` as ``str`` into a ``<text><![CDATA[...subEle...]]</text>`` element"""
|
39
|
+
|
40
|
+
textEle = ET.Element(XMLTags.TEXT)
|
41
|
+
if isinstance(subEle, list):
|
42
|
+
elementString : list= []
|
43
|
+
for i in subEle:
|
44
|
+
elementString.append(ET.tostring(i, encoding="unicode", pretty_print=True))
|
45
|
+
textEle.text = ET.CDATA("".join(elementString))
|
46
|
+
return textEle
|
47
|
+
else:
|
48
|
+
textEle.text = ET.CDATA(ET.tostring(subEle, encoding="unicode", pretty_print=True))
|
49
|
+
return textEle
|
50
|
+
|
51
|
+
|
52
|
+
def getFeedBEle(feedback:XMLTags,
|
53
|
+
text:str|None=None, style: TextElements | None = None
|
54
|
+
)->ET.Element:
|
55
|
+
"""Gets ET Elements with the feedback for the question."""
|
56
|
+
if style is None:
|
57
|
+
span = feedBElements[feedback]
|
58
|
+
else:
|
59
|
+
span = style.create()
|
60
|
+
if text is None:
|
61
|
+
text = feedbackStr[feedback]
|
62
|
+
ele = ET.Element(feedback, format="html")
|
63
|
+
par = TextElements.PLEFT.create()
|
64
|
+
span.text = text
|
65
|
+
par.append(span)
|
66
|
+
ele.append(eth.getCdatTxtElement(par))
|
67
|
+
return ele
|
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
from excel2moodle.core.globals import DFIndex
|
3
|
+
|
4
|
+
|
5
|
+
class QNotParsedException(Exception):
|
6
|
+
def __init__(self, message:str, qID:str|int, *args, **kwargs)->None:
|
7
|
+
super().__init__(message, *args, **kwargs)
|
8
|
+
self.qID = qID
|
9
|
+
|
10
|
+
class NanException(QNotParsedException):
|
11
|
+
def __init__(self, message, qID, field, *args, **kwargs):
|
12
|
+
super().__init__(message, qID, *args, **kwargs)
|
13
|
+
self.field = field
|
14
|
+
|
15
|
+
class InvalidFieldException(Exception):
|
16
|
+
def __init__(self, message:str, qID:str, field:DFIndex|list[DFIndex], *args: object, **kwargs) -> None:
|
17
|
+
super().__init__(message, *args, **kwargs)
|
18
|
+
self.field = field
|
19
|
+
self.index = qID
|
20
|
+
|
@@ -0,0 +1,128 @@
|
|
1
|
+
from enum import Enum, StrEnum
|
2
|
+
import lxml.etree as ET
|
3
|
+
|
4
|
+
questionTypes = {
|
5
|
+
"NF": "numerical",
|
6
|
+
"NFM": "numerical",
|
7
|
+
"MC": "multichoice",
|
8
|
+
}
|
9
|
+
|
10
|
+
class DFIndex(StrEnum):
|
11
|
+
"""This Enum holds the identifier string for for the spreadsheet and the string for the xml-tag
|
12
|
+
|
13
|
+
Each enum corresponds to a list of two values.
|
14
|
+
The first Value is the index in the spreadsheet, the second is the name of the xml-tag
|
15
|
+
"""
|
16
|
+
|
17
|
+
TEXT = "text"
|
18
|
+
BPOINTS = "bulletPoints"
|
19
|
+
TRUE = "true"
|
20
|
+
FALSE = "false"
|
21
|
+
TYPE = "type"
|
22
|
+
NAME = "name"
|
23
|
+
RESULT = "result"
|
24
|
+
PICTURE = "picture"
|
25
|
+
NUMBER = "number"
|
26
|
+
ANSTYPE = "answerType"
|
27
|
+
|
28
|
+
class TextElements(Enum):
|
29
|
+
PLEFT ="p","text-align: left;"
|
30
|
+
SPANRED = "span", "color: rgb(239, 69, 64)"
|
31
|
+
SPANGREEN = "span", "color: rgb(152, 202, 62)"
|
32
|
+
SPANORANGE = "span","color: rgb(152, 100, 100)"
|
33
|
+
ULIST = "ul", "",
|
34
|
+
LISTITEM = "li", "text-align: left;",
|
35
|
+
|
36
|
+
def create(self, tag:str|None=None):
|
37
|
+
if tag is None:
|
38
|
+
tag, style = self.value
|
39
|
+
else: style = self.value[1]
|
40
|
+
return ET.Element(tag, dir="ltr", style=style)
|
41
|
+
|
42
|
+
@property
|
43
|
+
def style(self,)->str:
|
44
|
+
return self.value[1]
|
45
|
+
|
46
|
+
class XMLTags(StrEnum):
|
47
|
+
def __new__(cls, value:str, dfkey: DFIndex|None = None):
|
48
|
+
obj = str.__new__(cls, value)
|
49
|
+
obj._value_ = value
|
50
|
+
if dfkey is not None:
|
51
|
+
obj._dfkey_ = dfkey
|
52
|
+
return obj
|
53
|
+
|
54
|
+
def __init__(self,_:str, dfkey:DFIndex|None =None, getEle=None )->None:
|
55
|
+
if isinstance(dfkey, DFIndex):
|
56
|
+
self._dfkey_:str=dfkey
|
57
|
+
if getEle:
|
58
|
+
self._getEle_:object=getEle
|
59
|
+
|
60
|
+
@property
|
61
|
+
def dfkey(self)->str:
|
62
|
+
return self._dfkey_
|
63
|
+
|
64
|
+
def set(self,getEle)->None:
|
65
|
+
self._getEle_ = getEle
|
66
|
+
|
67
|
+
def __repr__(self) -> str:
|
68
|
+
msg = []
|
69
|
+
msg.append(f"XML Tag {self.value = }")
|
70
|
+
if hasattr(self, "_dfkey_"):
|
71
|
+
msg.append(f"Df Key {self.dfkey =}")
|
72
|
+
return "\n".join(msg)
|
73
|
+
|
74
|
+
NAME = "name", DFIndex.NAME
|
75
|
+
QTEXT = "questiontext", DFIndex.TEXT
|
76
|
+
QUESTION = "question"
|
77
|
+
TEXT = "text"
|
78
|
+
PICTURE = "file", DFIndex.PICTURE
|
79
|
+
GENFEEDB = "generalfeedback"
|
80
|
+
CORFEEDB = "correctfeedback"
|
81
|
+
PCORFEEDB = "partialcorrectfeedback"
|
82
|
+
INCORFEEDB = "incorrectfeedback"
|
83
|
+
ANSFEEDBACK = "feedback"
|
84
|
+
POINTS = "defaultgrade"
|
85
|
+
PENALTY = "penalty"
|
86
|
+
HIDE = "hidden"
|
87
|
+
ID = "idnumber"
|
88
|
+
TYPE = "type"
|
89
|
+
ANSWER = "answer"
|
90
|
+
TOLERANCE = "tolerance"
|
91
|
+
|
92
|
+
feedBElements = {
|
93
|
+
XMLTags.CORFEEDB:TextElements.SPANGREEN.create(),
|
94
|
+
XMLTags.PCORFEEDB: TextElements.SPANORANGE.create(),
|
95
|
+
XMLTags.INCORFEEDB: TextElements.SPANRED.create(),
|
96
|
+
XMLTags.ANSFEEDBACK: TextElements.SPANGREEN.create(),
|
97
|
+
XMLTags.GENFEEDB: TextElements.SPANGREEN.create(),
|
98
|
+
}
|
99
|
+
feedbackStr = {
|
100
|
+
XMLTags.CORFEEDB:"Die Frage wurde richtig beantwortet",
|
101
|
+
XMLTags.PCORFEEDB: "Die Frage wurde teilweise richtig beantwortet",
|
102
|
+
XMLTags.INCORFEEDB: "Die Frage wurde Falsch beantwortet",
|
103
|
+
XMLTags.GENFEEDB: "Sie haben eine Antwort abgegeben",
|
104
|
+
"right": "richtig",
|
105
|
+
"wrong": "falsch",
|
106
|
+
"right1Percent": "Gratultaion, die Frage wurde im Rahmen der Toleranz richtig beantwortet"
|
107
|
+
}
|
108
|
+
|
109
|
+
parserSettings = {
|
110
|
+
"Parser" : {
|
111
|
+
"standards": {
|
112
|
+
"hidden": 0,
|
113
|
+
},
|
114
|
+
},
|
115
|
+
"MCParser" : {
|
116
|
+
"standards" : {
|
117
|
+
"single": "false",
|
118
|
+
"shuffleanswers": "true",
|
119
|
+
"answernumbering": "abc",
|
120
|
+
"showstandardinstruction": "0",
|
121
|
+
"shownumcorrect": "",
|
122
|
+
},
|
123
|
+
},
|
124
|
+
"NFParser" : {
|
125
|
+
"standards": {},
|
126
|
+
},
|
127
|
+
}
|
128
|
+
|