excel2moodle 0.4.1__py3-none-any.whl → 0.4.2__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/__main__.py +2 -2
- excel2moodle/core/__init__.py +0 -10
- excel2moodle/core/category.py +4 -3
- excel2moodle/core/dataStructure.py +85 -57
- excel2moodle/core/etHelpers.py +2 -2
- excel2moodle/core/exceptions.py +2 -2
- excel2moodle/core/globals.py +10 -27
- excel2moodle/core/parser.py +24 -30
- excel2moodle/core/question.py +147 -63
- excel2moodle/core/settings.py +73 -45
- excel2moodle/core/validator.py +36 -55
- excel2moodle/logger.py +3 -3
- excel2moodle/question_types/__init__.py +2 -0
- excel2moodle/question_types/cloze.py +207 -0
- excel2moodle/question_types/mc.py +26 -16
- excel2moodle/question_types/nf.py +17 -3
- excel2moodle/question_types/nfm.py +60 -17
- excel2moodle/ui/{windowEquationChecker.py → UI_equationChecker.py} +98 -78
- excel2moodle/ui/{exportSettingsDialog.py → UI_exportSettingsDialog.py} +55 -4
- excel2moodle/ui/{windowMain.py → UI_mainWindow.py} +32 -39
- excel2moodle/ui/appUi.py +35 -66
- excel2moodle/ui/dialogs.py +40 -2
- excel2moodle/ui/equationChecker.py +70 -0
- excel2moodle/ui/treewidget.py +4 -4
- {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.2.dist-info}/METADATA +2 -3
- excel2moodle-0.4.2.dist-info/RECORD +38 -0
- {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.2.dist-info}/entry_points.txt +0 -3
- excel2moodle/ui/questionPreviewDialog.py +0 -115
- excel2moodle-0.4.1.dist-info/RECORD +0 -37
- /excel2moodle/ui/{variantDialog.py → UI_variantDialog.py} +0 -0
- {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.2.dist-info}/WHEEL +0 -0
- {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.2.dist-info}/top_level.txt +0 -0
excel2moodle/__main__.py
CHANGED
@@ -9,7 +9,7 @@ from PySide6 import QtWidgets, sys
|
|
9
9
|
import excel2moodle
|
10
10
|
from excel2moodle import e2mMetadata, mainLogger
|
11
11
|
from excel2moodle.core import dataStructure
|
12
|
-
from excel2moodle.core.settings import Settings,
|
12
|
+
from excel2moodle.core.settings import Settings, Tags
|
13
13
|
from excel2moodle.logger import loggerConfig
|
14
14
|
from excel2moodle.ui import appUi as ui
|
15
15
|
|
@@ -17,7 +17,7 @@ from excel2moodle.ui import appUi as ui
|
|
17
17
|
def main() -> None:
|
18
18
|
excel2moodle.isMain = True
|
19
19
|
settings = Settings()
|
20
|
-
logfile = Path(settings.get(
|
20
|
+
logfile = Path(settings.get(Tags.LOGFILE)).resolve()
|
21
21
|
e2mMetadata["logfile"] = logfile
|
22
22
|
if logfile.exists() and logfile.is_file():
|
23
23
|
logfile.replace(f"{logfile}.old")
|
excel2moodle/core/__init__.py
CHANGED
@@ -1,11 +1 @@
|
|
1
1
|
"""These Modules are the heart of the excel2moodle Package."""
|
2
|
-
|
3
|
-
# from pandas._config import config
|
4
|
-
# from excel2moodle.core import questionWriter
|
5
|
-
# from excel2moodle.core import stringHelpers
|
6
|
-
# from excel2moodle.core import numericMultiQ
|
7
|
-
# from excel2moodle.core import globals
|
8
|
-
# from PySide6.QtCore import QObject, Signal
|
9
|
-
# import logging as logging
|
10
|
-
# from logging import config as logConfig
|
11
|
-
#
|
excel2moodle/core/category.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import logging
|
2
|
+
import re
|
2
3
|
from typing import TYPE_CHECKING
|
3
4
|
|
4
5
|
import lxml.etree as ET
|
@@ -15,7 +16,6 @@ class Category:
|
|
15
16
|
|
16
17
|
def __init__(
|
17
18
|
self,
|
18
|
-
n: int,
|
19
19
|
name: str,
|
20
20
|
description: str,
|
21
21
|
dataframe: pd.DataFrame,
|
@@ -23,8 +23,9 @@ class Category:
|
|
23
23
|
version: int = 0,
|
24
24
|
) -> None:
|
25
25
|
"""Instantiate a new Category object."""
|
26
|
-
self.n = n
|
27
26
|
self.NAME = name
|
27
|
+
match = re.search(r"\d+$", self.NAME)
|
28
|
+
self.n: int = int(match.group(0)) if match else 99
|
28
29
|
self.desc = str(description)
|
29
30
|
self.dataframe: pd.DataFrame = dataframe
|
30
31
|
self.points = points
|
@@ -56,6 +57,6 @@ class Category:
|
|
56
57
|
info = ET.SubElement(header, "info", format="html")
|
57
58
|
ET.SubElement(cat, "text").text = f"$module$/top/{self.NAME}"
|
58
59
|
ET.SubElement(info, "text").text = str(self.desc)
|
59
|
-
ET.SubElement(header, "idnumber").text =
|
60
|
+
ET.SubElement(header, "idnumber").text = self.id
|
60
61
|
ET.indent(header)
|
61
62
|
return header
|
@@ -4,6 +4,7 @@ At the heart is the class ``xmlTest``
|
|
4
4
|
"""
|
5
5
|
|
6
6
|
import logging
|
7
|
+
import sys
|
7
8
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
8
9
|
from pathlib import Path
|
9
10
|
from typing import TYPE_CHECKING
|
@@ -16,12 +17,13 @@ from PySide6.QtCore import QObject, Signal
|
|
16
17
|
from excel2moodle.core import stringHelpers
|
17
18
|
from excel2moodle.core.category import Category
|
18
19
|
from excel2moodle.core.exceptions import InvalidFieldException, QNotParsedException
|
19
|
-
from excel2moodle.core.globals import
|
20
|
+
from excel2moodle.core.globals import Tags
|
20
21
|
from excel2moodle.core.question import Question
|
21
|
-
from excel2moodle.core.settings import Settings
|
22
|
+
from excel2moodle.core.settings import Settings
|
22
23
|
from excel2moodle.core.validator import Validator
|
23
24
|
from excel2moodle.logger import LogAdapterQuestionID
|
24
25
|
from excel2moodle.question_types import QuestionTypeMapping
|
26
|
+
from excel2moodle.question_types.cloze import ClozeQuestion, ClozeQuestionParser
|
25
27
|
from excel2moodle.question_types.mc import MCQuestion, MCQuestionParser
|
26
28
|
from excel2moodle.question_types.nf import NFQuestion, NFQuestionParser
|
27
29
|
from excel2moodle.question_types.nfm import NFMQuestion, NFMQuestionParser
|
@@ -49,6 +51,7 @@ def processSheet(sheetPath: str, categoryName: str) -> pd.DataFrame:
|
|
49
51
|
sheet_name=str(categoryName),
|
50
52
|
index_col=0,
|
51
53
|
header=None,
|
54
|
+
engine="calamine",
|
52
55
|
)
|
53
56
|
|
54
57
|
|
@@ -64,6 +67,7 @@ class QuestionDB:
|
|
64
67
|
nfParser: NFQuestionParser = NFQuestionParser()
|
65
68
|
nfmParser: NFMQuestionParser = NFMQuestionParser()
|
66
69
|
mcParser: MCQuestionParser = MCQuestionParser()
|
70
|
+
clozeParser: ClozeQuestionParser = ClozeQuestionParser()
|
67
71
|
|
68
72
|
def __init__(self, settings: Settings) -> None:
|
69
73
|
self.settings = settings
|
@@ -72,7 +76,7 @@ class QuestionDB:
|
|
72
76
|
self.categoriesMetaData: pd.DataFrame
|
73
77
|
self.categories: dict[str, Category]
|
74
78
|
|
75
|
-
def readCategoriesMetadata(self, sheetPath: Path) ->
|
79
|
+
def readCategoriesMetadata(self, sheetPath: Path) -> pd.DataFrame:
|
76
80
|
"""Read the metadata and questions from the spreadsheet.
|
77
81
|
|
78
82
|
Get the category data from the spreadsheet and stores it in the
|
@@ -86,22 +90,35 @@ class QuestionDB:
|
|
86
90
|
f,
|
87
91
|
sheet_name="settings",
|
88
92
|
index_col=0,
|
93
|
+
engine="calamine",
|
89
94
|
)
|
90
95
|
logger.debug("Found the settings: \n\t%s", settingDf)
|
91
|
-
self._setProjectSettings(settingDf)
|
96
|
+
self._setProjectSettings(settingDf, mainPath=sheetPath.parent)
|
92
97
|
with Path(sheetPath).open("rb") as f:
|
93
98
|
self.categoriesMetaData = pd.read_excel(
|
94
99
|
f,
|
95
|
-
sheet_name=self.settings.get(
|
100
|
+
sheet_name=self.settings.get(Tags.CATEGORIESSHEET),
|
96
101
|
index_col=0,
|
102
|
+
engine="calamine",
|
97
103
|
)
|
98
104
|
logger.info(
|
99
105
|
"Sucessfully read categoriesMetaData \n %s", self.categoriesMetaData
|
100
106
|
)
|
107
|
+
return self.categoriesMetaData
|
101
108
|
|
102
|
-
def _setProjectSettings(
|
109
|
+
def _setProjectSettings(
|
110
|
+
self, settings: pd.DataFrame, mainPath: Path | None = None
|
111
|
+
) -> None:
|
112
|
+
settings = self.harmonizeDFIndex(settings)
|
103
113
|
for tag, value in settings.iterrows():
|
104
114
|
self.settings.set(tag, value.iloc[0], local=True)
|
115
|
+
extraModule = self.settings.get(Tags.IMPORTMODULE)
|
116
|
+
if extraModule:
|
117
|
+
logger.warning(
|
118
|
+
"Appending: %s to sys.path. All names defined by it will be usable",
|
119
|
+
mainPath,
|
120
|
+
)
|
121
|
+
sys.path.append(str(mainPath))
|
105
122
|
|
106
123
|
def initAllCategories(self, sheetPath: Path) -> None:
|
107
124
|
"""Read all category sheets and initialize all Categories."""
|
@@ -112,11 +129,10 @@ class QuestionDB:
|
|
112
129
|
self.categories.clear()
|
113
130
|
else:
|
114
131
|
self.categories: dict[str, Category] = {}
|
115
|
-
with
|
116
|
-
excelFile = pd.ExcelFile(f)
|
132
|
+
with pd.ExcelFile(sheetPath, engine="calamine") as excelFile:
|
117
133
|
for categoryName in excelFile.sheet_names:
|
118
134
|
logger.debug("Starting to read category %s", categoryName)
|
119
|
-
if categoryName.
|
135
|
+
if categoryName in self.categoriesMetaData.index:
|
120
136
|
self.initCategory(sheetPath, categoryName)
|
121
137
|
|
122
138
|
def asyncInitAllCategories(self, sheetPath: Path) -> None:
|
@@ -132,71 +148,83 @@ class QuestionDB:
|
|
132
148
|
self.categories.clear()
|
133
149
|
else:
|
134
150
|
self.categories: dict[str, Category] = {}
|
135
|
-
|
136
|
-
with
|
137
|
-
|
138
|
-
|
139
|
-
|
151
|
+
sheetNames = []
|
152
|
+
with pd.ExcelFile(sheetPath, engine="calamine") as excelFile:
|
153
|
+
sheetNames = [
|
154
|
+
name
|
155
|
+
for name in excelFile.sheet_names
|
156
|
+
if name in self.categoriesMetaData.index
|
140
157
|
]
|
141
|
-
logger.debug("found those
|
158
|
+
logger.debug("found those category sheets: \n %s ", sheetNames)
|
142
159
|
with ProcessPoolExecutor() as executor:
|
143
160
|
futures = {
|
144
161
|
executor.submit(processSheet, str(sheetPath), sheet): sheet
|
145
|
-
for sheet in
|
162
|
+
for sheet in sheetNames
|
146
163
|
}
|
147
164
|
for future in as_completed(futures):
|
148
165
|
categoryName = futures[future]
|
149
166
|
try:
|
150
167
|
categoryDataF = future.result()
|
151
|
-
|
152
|
-
self._setupCategory(categoryDataF, categoryName, categoryNumber)
|
168
|
+
self._setupCategory(categoryDataF, categoryName)
|
153
169
|
logger.debug("Finished processing %s", categoryName)
|
154
170
|
except Exception as e:
|
155
171
|
logger.exception("Error processing sheet %s: %s", categoryName, e)
|
156
172
|
logger.debug("Future exception: %s", future.exception())
|
157
173
|
|
158
|
-
def initCategory(self, sheetPath: Path, categoryName: str) ->
|
159
|
-
"""Read `categoryName` from the
|
160
|
-
|
174
|
+
def initCategory(self, sheetPath: Path, categoryName: str) -> bool | Category:
|
175
|
+
"""Read `categoryName` from the ``sheetPath`` and initialize the category.
|
176
|
+
Returns the Category and appends it to `self.categories`.
|
177
|
+
"""
|
161
178
|
katDf = pd.read_excel(
|
162
179
|
sheetPath,
|
163
180
|
sheet_name=str(categoryName),
|
164
181
|
index_col=0,
|
165
182
|
header=None,
|
183
|
+
engine="calamine",
|
166
184
|
)
|
167
185
|
if not katDf.empty:
|
168
186
|
logger.debug("Sucessfully read the Dataframe for cat %s", categoryName)
|
169
|
-
self._setupCategory(katDf, categoryName
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
) ->
|
187
|
+
return self._setupCategory(katDf, categoryName)
|
188
|
+
return False
|
189
|
+
|
190
|
+
@staticmethod
|
191
|
+
def harmonizeDFIndex(dataframe: pd.DataFrame) -> pd.DataFrame:
|
192
|
+
"""Convert the index strings to lowercase without whitespace."""
|
193
|
+
index = dataframe.index.str.lower()
|
194
|
+
harmonizedIdx = ["".join(i.split()) if pd.notna(i) else i for i in index]
|
195
|
+
dataframe.index = pd.Index(harmonizedIdx)
|
196
|
+
return dataframe
|
197
|
+
|
198
|
+
def _setupCategory(self, categoryDf: pd.DataFrame, categoryName: str) -> Category:
|
174
199
|
"""Setup the category from the ``dataframe``.
|
200
|
+
|
175
201
|
:emits: categoryReady(self) Signal.
|
176
|
-
"""
|
202
|
+
"""
|
203
|
+
categoryDf = self.harmonizeDFIndex(categoryDf)
|
177
204
|
points = (
|
178
|
-
self.categoriesMetaData["points"].
|
205
|
+
self.categoriesMetaData["points"].loc[categoryName]
|
179
206
|
if "points" in self.categoriesMetaData
|
180
|
-
and not pd.isna(self.categoriesMetaData["points"]).
|
181
|
-
else self.settings.get(
|
207
|
+
and not pd.isna(self.categoriesMetaData["points"]).loc[categoryName]
|
208
|
+
else self.settings.get(Tags.POINTS)
|
182
209
|
)
|
183
210
|
version = (
|
184
|
-
self.categoriesMetaData["version"].
|
211
|
+
self.categoriesMetaData["version"].loc[categoryName]
|
185
212
|
if "version" in self.categoriesMetaData
|
186
|
-
and not pd.isna(self.categoriesMetaData["version"].
|
187
|
-
else self.settings.get(
|
213
|
+
and not pd.isna(self.categoriesMetaData["version"].loc[categoryName])
|
214
|
+
else self.settings.get(Tags.VERSION)
|
188
215
|
)
|
189
216
|
category = Category(
|
190
|
-
categoryNumber,
|
191
217
|
categoryName,
|
192
|
-
self.categoriesMetaData["description"].
|
218
|
+
self.categoriesMetaData["description"].loc[categoryName],
|
193
219
|
dataframe=categoryDf,
|
194
220
|
points=points,
|
195
221
|
version=version,
|
196
222
|
)
|
197
|
-
self
|
223
|
+
if hasattr(self, "categories"):
|
224
|
+
self.categories[categoryName] = category
|
225
|
+
self.signals.categoryReady.emit(category)
|
198
226
|
logger.debug("Category %s is initialized", categoryName)
|
199
|
-
|
227
|
+
return category
|
200
228
|
|
201
229
|
def parseAllQuestions(self) -> None:
|
202
230
|
"""Parse all question from all categories.
|
@@ -214,13 +242,12 @@ class QuestionDB:
|
|
214
242
|
for qNum in category.dataframe.columns:
|
215
243
|
try:
|
216
244
|
self.setupAndParseQuestion(category, qNum)
|
217
|
-
except (InvalidFieldException, QNotParsedException
|
245
|
+
except (InvalidFieldException, QNotParsedException):
|
218
246
|
logger.exception(
|
219
247
|
"Question %s%02d couldn't be parsed. The Question Data: \n %s",
|
220
248
|
category.id,
|
221
249
|
qNum,
|
222
250
|
category.dataframe[qNum],
|
223
|
-
exc_info=e,
|
224
251
|
)
|
225
252
|
self.signals.categoryQuestionsReady.emit(category)
|
226
253
|
|
@@ -255,30 +282,33 @@ class QuestionDB:
|
|
255
282
|
raise QNotParsedException(msg, f"{category.id}{qNumber}")
|
256
283
|
cls.validator.setup(qdat, qNumber)
|
257
284
|
cls.validator.validate()
|
258
|
-
validData = cls.validator.
|
259
|
-
qtype: str =
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
question = category.questions[qNumber]
|
264
|
-
if question.element is not None:
|
285
|
+
validData = cls.validator.getQuestionData()
|
286
|
+
qtype: str = validData.get(Tags.TYPE)
|
287
|
+
logger.debug("Question type is: %s", qtype)
|
288
|
+
question = QuestionTypeMapping[qtype].create(category, validData)
|
289
|
+
if question.isParsed:
|
265
290
|
locallogger.info("Question already parsed")
|
266
291
|
return
|
267
292
|
if isinstance(question, NFQuestion):
|
268
293
|
cls.nfParser.setup(question)
|
269
|
-
cls.nfParser.parse()
|
270
294
|
locallogger.debug("setup a new NF parser ")
|
295
|
+
cls.nfParser.parse()
|
271
296
|
elif isinstance(question, MCQuestion):
|
272
297
|
cls.mcParser.setup(question)
|
273
|
-
cls.mcParser.parse()
|
274
298
|
locallogger.debug("setup a new MC parser ")
|
299
|
+
cls.mcParser.parse()
|
275
300
|
elif isinstance(question, NFMQuestion):
|
276
301
|
cls.nfmParser.setup(question)
|
277
|
-
cls.nfmParser.parse()
|
278
302
|
locallogger.debug("setup a new NFM parser ")
|
303
|
+
cls.nfmParser.parse()
|
304
|
+
elif isinstance(question, ClozeQuestion):
|
305
|
+
cls.clozeParser.setup(question)
|
306
|
+
locallogger.debug("setup a new CLOZE parser")
|
307
|
+
cls.clozeParser.parse()
|
279
308
|
else:
|
280
309
|
msg = "couldn't setup Parser"
|
281
310
|
raise QNotParsedException(msg, question.id)
|
311
|
+
category.questions[qNumber] = question
|
282
312
|
|
283
313
|
def appendQuestions(
|
284
314
|
self, questions: list[QuestionItem], file: Path | None = None
|
@@ -293,15 +323,15 @@ class QuestionDB:
|
|
293
323
|
catdict[cat] = []
|
294
324
|
catdict[cat].append(q.getQuestion())
|
295
325
|
for cat, qlist in catdict.items():
|
296
|
-
self.
|
326
|
+
self._appendQElements(
|
297
327
|
cat,
|
298
328
|
qlist,
|
299
329
|
tree=tree,
|
300
|
-
includeHeader=self.settings.get(
|
330
|
+
includeHeader=self.settings.get(Tags.INCLUDEINCATS),
|
301
331
|
)
|
302
332
|
stringHelpers.printDom(tree, file=file)
|
303
333
|
|
304
|
-
def
|
334
|
+
def _appendQElements(
|
305
335
|
self,
|
306
336
|
cat: Category,
|
307
337
|
qList: list[Question],
|
@@ -311,9 +341,9 @@ class QuestionDB:
|
|
311
341
|
if includeHeader:
|
312
342
|
tree.append(cat.getCategoryHeader())
|
313
343
|
logger.debug(f"Appended a new category item {cat=}")
|
314
|
-
variant: int = self.settings.get(
|
344
|
+
variant: int = self.settings.get(Tags.QUESTIONVARIANT)
|
315
345
|
for q in qList:
|
316
|
-
if q.variants is not None:
|
346
|
+
if hasattr(q, "variants") and q.variants is not None:
|
317
347
|
if variant == 0 or variant > q.variants:
|
318
348
|
dialog = QuestionVariantDialog(self.window, q)
|
319
349
|
if dialog.exec() == QtWidgets.QDialog.Accepted:
|
@@ -321,7 +351,5 @@ class QuestionDB:
|
|
321
351
|
logger.debug("Die Fragen-Variante %s wurde gewählt", variant)
|
322
352
|
else:
|
323
353
|
logger.warning("Keine Fragenvariante wurde gewählt.")
|
324
|
-
|
325
|
-
else:
|
326
|
-
q.assemble()
|
354
|
+
q.assemble(variant=variant)
|
327
355
|
tree.append(q.element)
|
excel2moodle/core/etHelpers.py
CHANGED
@@ -8,7 +8,7 @@ import lxml.etree as ET
|
|
8
8
|
import excel2moodle.core.etHelpers as eth
|
9
9
|
from excel2moodle.core.globals import TextElements, feedbackStr, feedBElements
|
10
10
|
|
11
|
-
from .globals import
|
11
|
+
from .globals import Tags, XMLTags
|
12
12
|
|
13
13
|
|
14
14
|
def getElement(eleName: str, text: str, **attribs) -> ET.Element:
|
@@ -27,7 +27,7 @@ def getElement(eleName: str, text: str, **attribs) -> ET.Element:
|
|
27
27
|
return toEle
|
28
28
|
|
29
29
|
|
30
|
-
def getTextElement(eleName: str, text: str |
|
30
|
+
def getTextElement(eleName: str, text: str | Tags, **attribs) -> ET.Element:
|
31
31
|
"""Creates two nested elements: ``eleName`` with child ``text`` which holds the text."""
|
32
32
|
toEle = ET.Element(eleName, **attribs)
|
33
33
|
child = getElement("text", text)
|
excel2moodle/core/exceptions.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from excel2moodle.core.globals import
|
1
|
+
from excel2moodle.core.globals import Tags
|
2
2
|
|
3
3
|
|
4
4
|
class QNotParsedException(Exception):
|
@@ -18,7 +18,7 @@ class InvalidFieldException(Exception):
|
|
18
18
|
self,
|
19
19
|
message: str,
|
20
20
|
qID: str,
|
21
|
-
field:
|
21
|
+
field: Tags | list[Tags],
|
22
22
|
*args: object,
|
23
23
|
**kwargs,
|
24
24
|
) -> None:
|
excel2moodle/core/globals.py
CHANGED
@@ -2,33 +2,16 @@ from enum import Enum, StrEnum
|
|
2
2
|
|
3
3
|
import lxml.etree as ET
|
4
4
|
|
5
|
-
|
5
|
+
from excel2moodle.core.settings import Tags
|
6
|
+
|
7
|
+
QUESTION_TYPES = {
|
6
8
|
"NF": "numerical",
|
7
9
|
"NFM": "numerical",
|
8
10
|
"MC": "multichoice",
|
11
|
+
"CLOZE": "cloze",
|
9
12
|
}
|
10
13
|
|
11
14
|
|
12
|
-
class DFIndex(StrEnum):
|
13
|
-
"""The identifier string for for the spreadsheet and the string for the xml-tag.
|
14
|
-
|
15
|
-
Each enum corresponds to a list of two values.
|
16
|
-
The first Value is the index in the spreadsheet, the second is the name of the xml-tag
|
17
|
-
"""
|
18
|
-
|
19
|
-
TEXT = "text"
|
20
|
-
BPOINTS = "bulletPoint"
|
21
|
-
TRUE = "true"
|
22
|
-
FALSE = "false"
|
23
|
-
TYPE = "type"
|
24
|
-
NAME = "name"
|
25
|
-
RESULT = "result"
|
26
|
-
PICTURE = "picture"
|
27
|
-
NUMBER = "number"
|
28
|
-
ANSTYPE = "answerType"
|
29
|
-
TOLERANCE = "tolerance"
|
30
|
-
|
31
|
-
|
32
15
|
class TextElements(Enum):
|
33
16
|
PLEFT = "p", "text-align: left;"
|
34
17
|
SPANRED = "span", "color: rgb(239, 69, 64)"
|
@@ -58,15 +41,15 @@ class TextElements(Enum):
|
|
58
41
|
|
59
42
|
|
60
43
|
class XMLTags(StrEnum):
|
61
|
-
def __new__(cls, value: str, dfkey:
|
44
|
+
def __new__(cls, value: str, dfkey: Tags | None = None):
|
62
45
|
obj = str.__new__(cls, value)
|
63
46
|
obj._value_ = value
|
64
47
|
if dfkey is not None:
|
65
48
|
obj._dfkey_ = dfkey
|
66
49
|
return obj
|
67
50
|
|
68
|
-
def __init__(self, _: str, dfkey:
|
69
|
-
if isinstance(dfkey,
|
51
|
+
def __init__(self, _: str, dfkey: Tags | None = None, getEle=None) -> None:
|
52
|
+
if isinstance(dfkey, Tags):
|
70
53
|
self._dfkey_: str = dfkey
|
71
54
|
if getEle:
|
72
55
|
self._getEle_: object = getEle
|
@@ -85,11 +68,11 @@ class XMLTags(StrEnum):
|
|
85
68
|
msg.append(f"Df Key {self.dfkey=}")
|
86
69
|
return "\n".join(msg)
|
87
70
|
|
88
|
-
NAME = "name",
|
89
|
-
QTEXT = "questiontext",
|
71
|
+
NAME = "name", Tags.NAME
|
72
|
+
QTEXT = "questiontext", Tags.TEXT
|
90
73
|
QUESTION = "question"
|
91
74
|
TEXT = "text"
|
92
|
-
PICTURE = "file",
|
75
|
+
PICTURE = "file", Tags.PICTURE
|
93
76
|
GENFEEDB = "generalfeedback"
|
94
77
|
CORFEEDB = "correctfeedback"
|
95
78
|
PCORFEEDB = "partialcorrectfeedback"
|
excel2moodle/core/parser.py
CHANGED
@@ -4,17 +4,16 @@ import re
|
|
4
4
|
import lxml.etree as ET
|
5
5
|
|
6
6
|
import excel2moodle.core.etHelpers as eth
|
7
|
-
from excel2moodle.core import stringHelpers
|
8
7
|
from excel2moodle.core.exceptions import QNotParsedException
|
9
8
|
from excel2moodle.core.globals import (
|
10
|
-
|
9
|
+
Tags,
|
11
10
|
TextElements,
|
12
11
|
XMLTags,
|
13
12
|
feedbackStr,
|
14
13
|
feedBElements,
|
15
14
|
)
|
16
15
|
from excel2moodle.core.question import Picture, Question
|
17
|
-
from excel2moodle.core.settings import Settings,
|
16
|
+
from excel2moodle.core.settings import Settings, Tags
|
18
17
|
from excel2moodle.logger import LogAdapterQuestionID
|
19
18
|
|
20
19
|
loggerObj = logging.getLogger(__name__)
|
@@ -47,8 +46,8 @@ class QuestionParser:
|
|
47
46
|
"""Create a ``Picture`` object ``question``if the question needs a pic."""
|
48
47
|
if hasattr(self, "picture") and self.question.picture.ready:
|
49
48
|
return True
|
50
|
-
picKey = self.rawInput.get(
|
51
|
-
f = self.settings.get(
|
49
|
+
picKey = self.rawInput.get(Tags.PICTURE)
|
50
|
+
f = self.settings.get(Tags.PICTUREFOLDER)
|
52
51
|
svgFolder = (f / self.question.katName).resolve()
|
53
52
|
if not hasattr(self.question, "picture"):
|
54
53
|
self.question.picture = Picture(picKey, svgFolder, self.question.id)
|
@@ -57,7 +56,7 @@ class QuestionParser:
|
|
57
56
|
def setMainText(self) -> None:
|
58
57
|
paragraphs: list[ET._Element] = [TextElements.PLEFT.create()]
|
59
58
|
ET.SubElement(paragraphs[0], "b").text = f"ID {self.question.id}"
|
60
|
-
text = self.rawInput[
|
59
|
+
text = self.rawInput[Tags.TEXT]
|
61
60
|
for t in text:
|
62
61
|
paragraphs.append(TextElements.PLEFT.create())
|
63
62
|
paragraphs[-1].text = t
|
@@ -66,8 +65,8 @@ class QuestionParser:
|
|
66
65
|
|
67
66
|
def setBPoints(self) -> None:
|
68
67
|
"""If there bulletPoints are set in the Spreadsheet it creates an unordered List-Element in ``Question.bulletList``."""
|
69
|
-
if
|
70
|
-
bps: str = self.rawInput[
|
68
|
+
if Tags.BPOINTS in self.rawInput:
|
69
|
+
bps: list[str] = self.rawInput[Tags.BPOINTS]
|
71
70
|
try:
|
72
71
|
bulletList = self.formatBulletList(bps)
|
73
72
|
except IndexError:
|
@@ -83,15 +82,14 @@ class QuestionParser:
|
|
83
82
|
)
|
84
83
|
self.question.bulletList = bulletList
|
85
84
|
|
86
|
-
def formatBulletList(self, bps: str) -> ET.Element:
|
85
|
+
def formatBulletList(self, bps: list[str]) -> ET.Element:
|
87
86
|
self.logger.debug("Formatting the bulletpoint list")
|
88
|
-
li: list[str] = stringHelpers.getListFromStr(bps)
|
89
87
|
name = []
|
90
88
|
var = []
|
91
89
|
quant = []
|
92
90
|
unit = []
|
93
91
|
unorderedList = TextElements.ULIST.create()
|
94
|
-
for item in
|
92
|
+
for item in bps:
|
95
93
|
sc_split = item.split()
|
96
94
|
name.append(sc_split[0])
|
97
95
|
var.append(sc_split[1])
|
@@ -118,7 +116,7 @@ class QuestionParser:
|
|
118
116
|
def appendToTmpEle(
|
119
117
|
self,
|
120
118
|
eleName: str,
|
121
|
-
text: str |
|
119
|
+
text: str | Tags,
|
122
120
|
txtEle=False,
|
123
121
|
**attribs,
|
124
122
|
) -> None:
|
@@ -127,7 +125,7 @@ class QuestionParser:
|
|
127
125
|
It uses the data from ``self.rawInput`` if ``text`` is type``DFIndex``
|
128
126
|
Otherwise the value of ``text`` will be inserted.
|
129
127
|
"""
|
130
|
-
t = self.rawInput[text] if isinstance(text,
|
128
|
+
t = self.rawInput[text] if isinstance(text, Tags) else text
|
131
129
|
if txtEle is False:
|
132
130
|
self.tmpEle.append(eth.getElement(eleName, t, **attribs))
|
133
131
|
elif txtEle is True:
|
@@ -135,6 +133,9 @@ class QuestionParser:
|
|
135
133
|
|
136
134
|
def _appendStandardTags(self) -> None:
|
137
135
|
"""Append the elements defined in the ``cls.standardTags``."""
|
136
|
+
self.logger.debug(
|
137
|
+
"Appending the Standard Tags %s", type(self.question).standardTags.items()
|
138
|
+
)
|
138
139
|
for k, v in type(self.question).standardTags.items():
|
139
140
|
self.appendToTmpEle(k, text=v)
|
140
141
|
|
@@ -145,25 +146,27 @@ class QuestionParser:
|
|
145
146
|
if no Exceptions are raised, ``self.tmpEle`` is passed to ``self.question.element``
|
146
147
|
"""
|
147
148
|
self.logger.info("Starting to parse")
|
148
|
-
self.tmpEle = ET.Element(
|
149
|
-
|
149
|
+
self.tmpEle: ET.Elemnt = ET.Element(
|
150
|
+
XMLTags.QUESTION, type=self.question.moodleType
|
151
|
+
)
|
152
|
+
self.appendToTmpEle(XMLTags.NAME, text=Tags.NAME, txtEle=True)
|
150
153
|
self.appendToTmpEle(XMLTags.ID, text=self.question.id)
|
151
154
|
if self.hasPicture():
|
152
155
|
self.tmpEle.append(self.question.picture.element)
|
153
156
|
self.tmpEle.append(ET.Element(XMLTags.QTEXT, format="html"))
|
154
157
|
self.appendToTmpEle(XMLTags.POINTS, text=str(self.question.points))
|
155
|
-
self.appendToTmpEle(XMLTags.PENALTY, text="0.3333")
|
156
158
|
self._appendStandardTags()
|
157
159
|
for feedb in self.genFeedbacks:
|
158
160
|
self.tmpEle.append(eth.getFeedBEle(feedb))
|
159
|
-
ansList = self.
|
161
|
+
ansList = self._parseAnswers()
|
160
162
|
self.setMainText()
|
161
163
|
self.setBPoints()
|
162
164
|
if ansList is not None:
|
163
165
|
for ele in ansList:
|
164
166
|
self.tmpEle.append(ele)
|
165
|
-
self.logger.info("Sucessfully parsed")
|
166
167
|
self.question.element = self.tmpEle
|
168
|
+
self.question.isParsed = True
|
169
|
+
self.logger.info("Sucessfully parsed")
|
167
170
|
|
168
171
|
def getFeedBEle(
|
169
172
|
self,
|
@@ -181,7 +184,7 @@ class QuestionParser:
|
|
181
184
|
ele.append(eth.getCdatTxtElement(par))
|
182
185
|
return ele
|
183
186
|
|
184
|
-
def
|
187
|
+
def _parseAnswers(self) -> list[ET.Element] | None:
|
185
188
|
"""Needs to be implemented in the type-specific subclasses."""
|
186
189
|
return None
|
187
190
|
|
@@ -211,15 +214,6 @@ class QuestionParser:
|
|
211
214
|
TextElements.SPANGREEN,
|
212
215
|
),
|
213
216
|
)
|
214
|
-
|
215
|
-
|
216
|
-
tolerance = self.settings.get(SettingsKey.TOLERANCE)
|
217
|
-
self.logger.info(
|
218
|
-
"Using default tolerance %s percent from settings",
|
219
|
-
tolerance,
|
220
|
-
)
|
221
|
-
tolPercent = 100 * tolerance if tolerance < 1 else tolerance
|
222
|
-
self.logger.debug("Using tolerance %s percent", tolPercent)
|
223
|
-
relTolerance = abs(round(result * (tolerance / 100), 3))
|
224
|
-
ansEle.append(eth.getElement(XMLTags.TOLERANCE, text=str(relTolerance)))
|
217
|
+
absTolerance = round(result * self.rawInput.get(Tags.TOLERANCE), 4)
|
218
|
+
ansEle.append(eth.getElement(XMLTags.TOLERANCE, text=str(absTolerance)))
|
225
219
|
return ansEle
|