excel2moodle 0.6.1__py3-none-any.whl → 0.6.3__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 +2 -0
- excel2moodle/__main__.py +18 -3
- excel2moodle/core/bullets.py +2 -2
- excel2moodle/core/category.py +27 -8
- excel2moodle/core/dataStructure.py +122 -32
- excel2moodle/core/etHelpers.py +0 -20
- excel2moodle/core/globals.py +0 -9
- excel2moodle/core/parser.py +21 -13
- excel2moodle/core/question.py +43 -23
- excel2moodle/core/settings.py +16 -5
- excel2moodle/extra/equationVerification.py +0 -2
- excel2moodle/extra/updateQuery.py +48 -0
- excel2moodle/extra/variableGenerator.py +73 -49
- excel2moodle/question_types/cloze.py +119 -87
- excel2moodle/question_types/mc.py +15 -10
- excel2moodle/question_types/nf.py +7 -1
- excel2moodle/question_types/nfm.py +14 -10
- excel2moodle/ui/UI_exportSettingsDialog.py +60 -14
- excel2moodle/ui/UI_updateDlg.py +106 -0
- excel2moodle/ui/appUi.py +66 -24
- excel2moodle/ui/dialogs.py +30 -1
- excel2moodle/ui/treewidget.py +30 -10
- {excel2moodle-0.6.1.dist-info → excel2moodle-0.6.3.dist-info}/METADATA +66 -19
- excel2moodle-0.6.3.dist-info/RECORD +41 -0
- excel2moodle-0.6.1.dist-info/RECORD +0 -39
- {excel2moodle-0.6.1.dist-info → excel2moodle-0.6.3.dist-info}/WHEEL +0 -0
- {excel2moodle-0.6.1.dist-info → excel2moodle-0.6.3.dist-info}/entry_points.txt +0 -0
- {excel2moodle-0.6.1.dist-info → excel2moodle-0.6.3.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.6.1.dist-info → excel2moodle-0.6.3.dist-info}/top_level.txt +0 -0
excel2moodle/__init__.py
CHANGED
@@ -45,6 +45,8 @@ if __package__ is not None:
|
|
45
45
|
"documentation": "https://jbosse3.gitlab.io/excel2moodle",
|
46
46
|
"homepage": meta["project-url"].split()[1],
|
47
47
|
"issues": "https://gitlab.com/jbosse3/excel2moodle/issues",
|
48
|
+
"funding": "https://ko-fi.com/jbosse3",
|
49
|
+
"API_id": "jbosse3%2Fexcel2moodle",
|
48
50
|
}
|
49
51
|
|
50
52
|
|
excel2moodle/__main__.py
CHANGED
@@ -5,17 +5,20 @@ import signal
|
|
5
5
|
import sys
|
6
6
|
from pathlib import Path
|
7
7
|
|
8
|
+
from PySide6.QtCore import QTimer
|
8
9
|
from PySide6.QtWidgets import QApplication
|
9
10
|
|
10
11
|
import excel2moodle
|
11
|
-
from excel2moodle import e2mMetadata, mainLogger
|
12
|
+
from excel2moodle import __version__, e2mMetadata, mainLogger
|
12
13
|
from excel2moodle.core import dataStructure
|
13
14
|
from excel2moodle.core.settings import Settings, Tags
|
15
|
+
from excel2moodle.extra import updateQuery
|
14
16
|
from excel2moodle.logger import loggerConfig
|
15
17
|
from excel2moodle.ui import appUi as ui
|
16
18
|
|
17
19
|
|
18
20
|
def main() -> None:
|
21
|
+
app = QApplication(sys.argv)
|
19
22
|
excel2moodle.isMain = True
|
20
23
|
settings = Settings()
|
21
24
|
logfile = Path(settings.get(Tags.LOGFILE)).resolve()
|
@@ -24,12 +27,24 @@ def main() -> None:
|
|
24
27
|
logfile.replace(f"{logfile}.old")
|
25
28
|
logConfig.dictConfig(config=loggerConfig)
|
26
29
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
27
|
-
app = QApplication(sys.argv)
|
28
|
-
settings = Settings()
|
29
30
|
database: dataStructure.QuestionDB = dataStructure.QuestionDB(settings)
|
30
31
|
window = ui.MainWindow(settings, database)
|
31
32
|
database.window = window
|
32
33
|
window.show()
|
34
|
+
# Update check
|
35
|
+
latestTag = updateQuery.get_latest_tag(e2mMetadata.get("API_id"))
|
36
|
+
if latestTag is None:
|
37
|
+
mainLogger.warning(
|
38
|
+
"Couldn't check for Updates, maybe there is no internet connection"
|
39
|
+
)
|
40
|
+
if latestTag is not None and latestTag.strip("v") != __version__:
|
41
|
+
mainLogger.warning(
|
42
|
+
"A new Update is available: %s --> %s ", __version__, latestTag
|
43
|
+
)
|
44
|
+
changelog = updateQuery.get_changelog(e2mMetadata.get("API_id"))
|
45
|
+
# Delay showing the update dialog slightly
|
46
|
+
QTimer.singleShot(100, lambda: window.showUpdateDialog(changelog, latestTag))
|
47
|
+
|
33
48
|
for k, v in e2mMetadata.items():
|
34
49
|
msg = f"{k:^14s}: {v}"
|
35
50
|
mainLogger.info(msg)
|
excel2moodle/core/bullets.py
CHANGED
@@ -15,7 +15,7 @@ class BulletList:
|
|
15
15
|
def __init__(self, rawBullets: list[str], qID: str) -> None:
|
16
16
|
self.rawBullets: list[str] = rawBullets
|
17
17
|
self.element: ET.Element = ET.Element("ul")
|
18
|
-
self.bullets: dict[str, BulletP] = {}
|
18
|
+
self.bullets: dict[str | int, BulletP] = {}
|
19
19
|
self.id = qID
|
20
20
|
self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.id})
|
21
21
|
self._setupBullets(rawBullets)
|
@@ -70,7 +70,7 @@ class BulletList:
|
|
70
70
|
if match is None:
|
71
71
|
self.logger.debug("Got a normal bulletItem")
|
72
72
|
num: float = float(quant.replace(",", "."))
|
73
|
-
bulletName
|
73
|
+
bulletName = i + 1
|
74
74
|
else:
|
75
75
|
bulletName = match.group(1)
|
76
76
|
num: float = 0.0
|
excel2moodle/core/category.py
CHANGED
@@ -5,10 +5,9 @@ from typing import TYPE_CHECKING
|
|
5
5
|
import lxml.etree as ET
|
6
6
|
import pandas as pd
|
7
7
|
|
8
|
-
from excel2moodle.core.settings import Tags
|
9
|
-
|
10
8
|
if TYPE_CHECKING:
|
11
9
|
from excel2moodle.core.question import Question
|
10
|
+
from excel2moodle.core.settings import Tags
|
12
11
|
|
13
12
|
loggerObj = logging.getLogger(__name__)
|
14
13
|
|
@@ -25,12 +24,13 @@ class Category:
|
|
25
24
|
) -> None:
|
26
25
|
"""Instantiate a new Category object."""
|
27
26
|
self.NAME = name
|
28
|
-
match = re.search(r"\d
|
29
|
-
|
27
|
+
match = re.search(r"\d+", str(self.NAME))
|
28
|
+
n = int(match.group(0)) if match else 99
|
29
|
+
self.n: int = n if n <= 99 and n >= 0 else 99
|
30
30
|
self.desc = str(description)
|
31
31
|
self.dataframe: pd.DataFrame = dataframe
|
32
32
|
self.settings: dict[str, float | str] = settings if settings else {}
|
33
|
-
self.
|
33
|
+
self._questions: dict[int, Question]
|
34
34
|
self.maxVariants: int | None = None
|
35
35
|
loggerObj.info("initializing Category %s", self.NAME)
|
36
36
|
|
@@ -50,6 +50,18 @@ class Category:
|
|
50
50
|
def id(self) -> str:
|
51
51
|
return f"{self.n:02d}"
|
52
52
|
|
53
|
+
@property
|
54
|
+
def questions(self) -> dict:
|
55
|
+
if not hasattr(self, "_questions"):
|
56
|
+
msg = f"Category {self.id} doesn't contain any valid questions."
|
57
|
+
raise ValueError(msg)
|
58
|
+
return self._questions
|
59
|
+
|
60
|
+
def appendQuestion(self, questionNumber: int, question) -> None:
|
61
|
+
if not hasattr(self, "_questions"):
|
62
|
+
self._questions: dict[int, Question] = {}
|
63
|
+
self._questions[questionNumber] = question
|
64
|
+
|
53
65
|
def __hash__(self) -> int:
|
54
66
|
return hash(self.NAME)
|
55
67
|
|
@@ -58,13 +70,20 @@ class Category:
|
|
58
70
|
return self.NAME == other.NAME
|
59
71
|
return False
|
60
72
|
|
61
|
-
def getCategoryHeader(self) -> ET.Element:
|
73
|
+
def getCategoryHeader(self, subCategory: str | None = None) -> ET.Element:
|
62
74
|
"""Insert an <question type='category'> before all Questions of this Category."""
|
63
75
|
header = ET.Element("question", type="category")
|
64
76
|
cat = ET.SubElement(header, "category")
|
65
77
|
info = ET.SubElement(header, "info", format="html")
|
66
|
-
|
78
|
+
catStr = (
|
79
|
+
f"$module$/top/{self.NAME}"
|
80
|
+
if subCategory is None
|
81
|
+
else f"$module$/top/{self.NAME}/Question-{subCategory[2:]}_Variants"
|
82
|
+
)
|
83
|
+
ET.SubElement(cat, "text").text = catStr
|
67
84
|
ET.SubElement(info, "text").text = str(self.desc)
|
68
|
-
ET.SubElement(header, "idnumber").text =
|
85
|
+
ET.SubElement(header, "idnumber").text = (
|
86
|
+
f"cat-{self.id}" if subCategory is None else f"variants-{subCategory}"
|
87
|
+
)
|
69
88
|
ET.indent(header)
|
70
89
|
return header
|
@@ -3,6 +3,7 @@
|
|
3
3
|
At the heart is the class ``xmlTest``
|
4
4
|
"""
|
5
5
|
|
6
|
+
import datetime as dt
|
6
7
|
import logging
|
7
8
|
import sys
|
8
9
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
@@ -11,14 +12,16 @@ from typing import TYPE_CHECKING
|
|
11
12
|
|
12
13
|
import lxml.etree as ET # noqa: N812
|
13
14
|
import pandas as pd
|
15
|
+
import yaml
|
14
16
|
from PySide6.QtCore import QObject, Signal
|
15
17
|
from PySide6.QtWidgets import QDialog
|
16
18
|
|
19
|
+
from excel2moodle import __version__
|
17
20
|
from excel2moodle.core import stringHelpers
|
18
21
|
from excel2moodle.core.category import Category
|
19
22
|
from excel2moodle.core.exceptions import InvalidFieldException, QNotParsedException
|
20
23
|
from excel2moodle.core.globals import Tags
|
21
|
-
from excel2moodle.core.question import Question
|
24
|
+
from excel2moodle.core.question import ParametricQuestion, Question
|
22
25
|
from excel2moodle.core.settings import Settings
|
23
26
|
from excel2moodle.core.validator import Validator
|
24
27
|
from excel2moodle.logger import LogAdapterQuestionID
|
@@ -72,9 +75,10 @@ class QuestionDB:
|
|
72
75
|
def __init__(self, settings: Settings) -> None:
|
73
76
|
self.settings = settings
|
74
77
|
self.window: QMainWindow | None = None
|
75
|
-
self.version = None
|
76
78
|
self.categoriesMetaData: pd.DataFrame
|
77
79
|
self.categories: dict[str, Category]
|
80
|
+
self._exportedQuestions: list[Question] = []
|
81
|
+
self._exportedAll: bool = False
|
78
82
|
|
79
83
|
@property
|
80
84
|
def spreadsheet(self) -> Path:
|
@@ -94,6 +98,17 @@ class QuestionDB:
|
|
94
98
|
``categoriesMetaData`` dataframe
|
95
99
|
Setup the categories and store them in ``self.categories = {}``
|
96
100
|
Pass the question data to the categories.
|
101
|
+
|
102
|
+
Raises
|
103
|
+
------
|
104
|
+
ValueError
|
105
|
+
When there is no 'seetings' worksheet in the file.
|
106
|
+
InvalidFieldException
|
107
|
+
When the settings are invalid
|
108
|
+
Or When the categories Sheet doesn't provide the necessary keys.
|
109
|
+
|
110
|
+
Before raising it logges the exceptions with a meaningful message.
|
111
|
+
|
97
112
|
"""
|
98
113
|
sheetPath = sheetPath if sheetPath else self.spreadsheet
|
99
114
|
logger.info("Start Parsing the Excel Metadata Sheet\n")
|
@@ -104,19 +119,14 @@ class QuestionDB:
|
|
104
119
|
index_col=0,
|
105
120
|
engine="calamine",
|
106
121
|
)
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
except InvalidFieldException:
|
116
|
-
logger.exception(
|
117
|
-
"Can not create the database with invalid project settings."
|
118
|
-
)
|
119
|
-
raise
|
122
|
+
logger.debug("Found the settings: \n\t%s", settingDf)
|
123
|
+
settingDf = self.harmonizeDFIndex(settingDf)
|
124
|
+
for tag, value in settingDf.iterrows():
|
125
|
+
val = value.iloc[0]
|
126
|
+
if pd.notna(val):
|
127
|
+
self.settings.set(tag, val)
|
128
|
+
|
129
|
+
self._validateProjectSettings(sheetPath=sheetPath)
|
120
130
|
with Path(sheetPath).open("rb") as f:
|
121
131
|
self.categoriesMetaData = pd.read_excel(
|
122
132
|
f,
|
@@ -124,10 +134,20 @@ class QuestionDB:
|
|
124
134
|
index_col=0,
|
125
135
|
engine="calamine",
|
126
136
|
)
|
127
|
-
|
137
|
+
if "description" not in self.categoriesMetaData.columns:
|
138
|
+
msg = f"You need to specify the 'description' tag for each category in the sheet '{self.settings.get(Tags.CATEGORIESSHEET)}'."
|
139
|
+
raise InvalidFieldException(msg, "0000", "description")
|
140
|
+
logger.info("Sucessfully read categoriesMetaData")
|
128
141
|
return self.categoriesMetaData
|
129
142
|
|
130
143
|
def _validateProjectSettings(self, sheetPath: Path) -> None:
|
144
|
+
if Tags.LOGLEVEL in self.settings:
|
145
|
+
level: str = self.settings.get(Tags.LOGLEVEL)
|
146
|
+
if level.upper() not in ("DEBUG", "INFO", "WARNING", "ERROR"):
|
147
|
+
self.settings.pop(Tags.LOGLEVEL)
|
148
|
+
logger.warning("You specified an unsupported Loglevel: %s", level)
|
149
|
+
if self.window is not None:
|
150
|
+
self.window.logHandler.setLevel(self.settings.get(Tags.LOGLEVEL).upper())
|
131
151
|
if Tags.IMPORTMODULE in self.settings:
|
132
152
|
logger.warning(
|
133
153
|
"Appending: %s to sys.path. All names defined by it will be usable",
|
@@ -276,7 +296,7 @@ class QuestionDB:
|
|
276
296
|
for qNum in category.dataframe.columns:
|
277
297
|
try:
|
278
298
|
self.setupAndParseQuestion(category, qNum)
|
279
|
-
except (InvalidFieldException, QNotParsedException):
|
299
|
+
except (InvalidFieldException, QNotParsedException, AttributeError):
|
280
300
|
logger.exception(
|
281
301
|
"Question %s%02d couldn't be parsed. The Question Data: \n %s",
|
282
302
|
category.id,
|
@@ -342,21 +362,26 @@ class QuestionDB:
|
|
342
362
|
else:
|
343
363
|
msg = "couldn't setup Parser"
|
344
364
|
raise QNotParsedException(msg, question.id)
|
345
|
-
category.
|
365
|
+
category.appendQuestion(qNumber, question)
|
346
366
|
return question
|
347
367
|
|
348
368
|
def appendQuestions(
|
349
|
-
self,
|
369
|
+
self,
|
370
|
+
questions: list[QuestionItem],
|
371
|
+
file: Path | None = None,
|
372
|
+
pCount: int = 0,
|
373
|
+
qCount: int = 0,
|
350
374
|
) -> None:
|
351
375
|
"""Append selected question Elements to the tree."""
|
376
|
+
self._exportedQuestions.clear()
|
352
377
|
tree = ET.Element("quiz")
|
353
378
|
catdict: dict[Category, list[Question]] = {}
|
354
379
|
for q in questions:
|
355
380
|
logger.debug(f"got a question to append {q=}")
|
356
|
-
cat = q.parent().
|
381
|
+
cat = q.parent().category
|
357
382
|
if cat not in catdict:
|
358
383
|
catdict[cat] = []
|
359
|
-
catdict[cat].append(q.
|
384
|
+
catdict[cat].append(q.question)
|
360
385
|
for cat, qlist in catdict.items():
|
361
386
|
self._appendQElements(
|
362
387
|
cat,
|
@@ -365,6 +390,8 @@ class QuestionDB:
|
|
365
390
|
includeHeader=self.settings.get(Tags.INCLUDEINCATS),
|
366
391
|
)
|
367
392
|
stringHelpers.printDom(tree, file=file)
|
393
|
+
if self.settings.get(Tags.GENEXPORTREPORT):
|
394
|
+
self.generateExportReport(file, pCount=pCount, qCount=qCount)
|
368
395
|
|
369
396
|
def _appendQElements(
|
370
397
|
self,
|
@@ -373,17 +400,80 @@ class QuestionDB:
|
|
373
400
|
tree: ET.Element,
|
374
401
|
includeHeader: bool = True,
|
375
402
|
) -> None:
|
376
|
-
|
403
|
+
variant: int = self.settings.get(Tags.QUESTIONVARIANT)
|
404
|
+
if includeHeader or variant == -1:
|
377
405
|
tree.append(cat.getCategoryHeader())
|
378
406
|
logger.debug(f"Appended a new category item {cat=}")
|
379
|
-
|
407
|
+
self._exportedAll: bool = True
|
380
408
|
for q in qList:
|
381
|
-
if
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
409
|
+
if not isinstance(q, ParametricQuestion):
|
410
|
+
tree.append(q.getUpdatedElement())
|
411
|
+
self._exportedQuestions.append(q)
|
412
|
+
continue
|
413
|
+
if variant == -1:
|
414
|
+
tree.append(cat.getCategoryHeader(subCategory=q.id))
|
415
|
+
for var in range(q.parametrics.variants):
|
416
|
+
tree.append(q.getUpdatedElement(variant=var))
|
417
|
+
elif variant == 0 or variant > q.parametrics.variants:
|
418
|
+
dialog = QuestionVariantDialog(self.window, q)
|
419
|
+
if dialog.exec() == QDialog.Accepted:
|
420
|
+
variant = dialog.variant
|
421
|
+
logger.debug("Die Fragen-Variante %s wurde gewählt", variant)
|
422
|
+
else:
|
423
|
+
logger.warning("Keine Fragenvariante wurde gewählt.")
|
424
|
+
tree.append(q.getUpdatedElement(variant=variant))
|
425
|
+
else:
|
426
|
+
tree.append(q.getUpdatedElement(variant=variant))
|
427
|
+
self._exportedQuestions.append(q)
|
428
|
+
|
429
|
+
def generateExportReport(
|
430
|
+
self, file: Path | None = None, pCount: int = 0, qCount: int = 0
|
431
|
+
) -> None:
|
432
|
+
"""Generate a YAML report of the exported questions."""
|
433
|
+
if not self._exportedQuestions:
|
434
|
+
return
|
435
|
+
if file:
|
436
|
+
base_path = file.with_name(f"{file.stem}_export_report.yaml")
|
437
|
+
else:
|
438
|
+
base_path = self.spreadsheet.parent / "export_report.yaml"
|
439
|
+
|
440
|
+
for i in range(99):
|
441
|
+
report_path = base_path.with_name(f"{base_path.stem}-{i:02d}.yaml")
|
442
|
+
if not report_path.resolve().exists():
|
443
|
+
break
|
444
|
+
|
445
|
+
report_data = {
|
446
|
+
"export_metadata": {
|
447
|
+
"export_time": dt.datetime.now(tz=None).strftime("%Y-%m-%d %H:%M:%S"),
|
448
|
+
"excel2moodle_version": __version__,
|
449
|
+
},
|
450
|
+
"categories": {},
|
451
|
+
}
|
452
|
+
if qCount != 0:
|
453
|
+
report_data["export_metadata"]["question_count"] = qCount
|
454
|
+
if pCount != 0:
|
455
|
+
report_data["export_metadata"]["total_point_count"] = pCount
|
456
|
+
|
457
|
+
sorted_questions = sorted(
|
458
|
+
self._exportedQuestions, key=lambda q: (q.category.name, q.id)
|
459
|
+
)
|
460
|
+
|
461
|
+
for question in sorted_questions:
|
462
|
+
category_name = question.category.name
|
463
|
+
if category_name not in report_data["categories"]:
|
464
|
+
report_data["categories"][category_name] = {
|
465
|
+
"description": question.category.desc,
|
466
|
+
"questions": [],
|
467
|
+
}
|
468
|
+
|
469
|
+
question_data = {"id": question.id, "name": question.name}
|
470
|
+
if isinstance(question, ParametricQuestion) and question.currentVariant > 0:
|
471
|
+
if self._exportedAll:
|
472
|
+
question_data["exported_variant"] = "all"
|
473
|
+
else:
|
474
|
+
question_data["exported_variant"] = question.currentVariant + 1
|
475
|
+
|
476
|
+
report_data["categories"][category_name]["questions"].append(question_data)
|
477
|
+
|
478
|
+
with report_path.open("w") as f:
|
479
|
+
yaml.dump(report_data, f, sort_keys=False)
|
excel2moodle/core/etHelpers.py
CHANGED
@@ -5,9 +5,6 @@ This module host different functions. All of them will return an ``lxml.etree.El
|
|
5
5
|
|
6
6
|
import lxml.etree as ET
|
7
7
|
|
8
|
-
import excel2moodle.core.etHelpers as eth
|
9
|
-
from excel2moodle.core.globals import TextElements, feedbackStr, feedBElements
|
10
|
-
|
11
8
|
from .globals import Tags, XMLTags
|
12
9
|
|
13
10
|
|
@@ -48,20 +45,3 @@ def getCdatTxtElement(subEle: ET._Element | list[ET._Element]) -> ET.Element:
|
|
48
45
|
ET.tostring(subEle, encoding="unicode", pretty_print=True),
|
49
46
|
)
|
50
47
|
return textEle
|
51
|
-
|
52
|
-
|
53
|
-
def getFeedBEle(
|
54
|
-
feedback: XMLTags,
|
55
|
-
text: str | None = None,
|
56
|
-
style: TextElements | None = None,
|
57
|
-
) -> ET.Element:
|
58
|
-
"""Gets ET Elements with the feedback for the question."""
|
59
|
-
span = feedBElements[feedback] if style is None else 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
|
excel2moodle/core/globals.py
CHANGED
@@ -89,12 +89,3 @@ feedBElements = {
|
|
89
89
|
XMLTags.ANSFEEDBACK: TextElements.SPANGREEN.create(),
|
90
90
|
XMLTags.GENFEEDB: TextElements.SPANGREEN.create(),
|
91
91
|
}
|
92
|
-
feedbackStr = {
|
93
|
-
XMLTags.CORFEEDB: "Die Frage wurde richtig beantwortet",
|
94
|
-
XMLTags.PCORFEEDB: "Die Frage wurde teilweise richtig beantwortet",
|
95
|
-
XMLTags.INCORFEEDB: "Die Frage wurde Falsch beantwortet",
|
96
|
-
XMLTags.GENFEEDB: "Sie haben eine Antwort abgegeben",
|
97
|
-
"right": "richtig",
|
98
|
-
"wrong": "falsch",
|
99
|
-
"right1Percent": "Gratultaion, die Frage wurde im Rahmen der Toleranz richtig beantwortet",
|
100
|
-
}
|
excel2moodle/core/parser.py
CHANGED
@@ -9,7 +9,6 @@ from excel2moodle.core.globals import (
|
|
9
9
|
Tags,
|
10
10
|
TextElements,
|
11
11
|
XMLTags,
|
12
|
-
feedbackStr,
|
13
12
|
feedBElements,
|
14
13
|
)
|
15
14
|
from excel2moodle.core.question import Picture, Question
|
@@ -30,7 +29,6 @@ class QuestionParser:
|
|
30
29
|
|
31
30
|
def __init__(self) -> None:
|
32
31
|
"""Initialize the general Question parser."""
|
33
|
-
self.genFeedbacks: list[XMLTags] = []
|
34
32
|
self.logger: logging.LoggerAdapter
|
35
33
|
|
36
34
|
def setup(self, question: Question) -> None:
|
@@ -84,7 +82,7 @@ class QuestionParser:
|
|
84
82
|
It uses the data from ``self.rawInput`` if ``text`` is type``DFIndex``
|
85
83
|
Otherwise the value of ``text`` will be inserted.
|
86
84
|
"""
|
87
|
-
t = self.rawInput
|
85
|
+
t = self.rawInput.get(text) if isinstance(text, Tags) else text
|
88
86
|
if txtEle is False:
|
89
87
|
self.tmpEle.append(eth.getElement(eleName, t, **attribs))
|
90
88
|
elif txtEle is True:
|
@@ -111,12 +109,11 @@ class QuestionParser:
|
|
111
109
|
self.appendToTmpEle(XMLTags.NAME, text=Tags.NAME, txtEle=True)
|
112
110
|
self.appendToTmpEle(XMLTags.ID, text=self.question.id)
|
113
111
|
textRootElem = ET.Element(XMLTags.QTEXT, format="html")
|
114
|
-
mainTextEle = ET.SubElement(textRootElem, "text")
|
112
|
+
self.mainTextEle = ET.SubElement(textRootElem, "text")
|
115
113
|
self.tmpEle.append(textRootElem)
|
116
|
-
|
114
|
+
if self.question.qtype != "CLOZE":
|
115
|
+
self.appendToTmpEle(XMLTags.POINTS, text=str(self.question.points))
|
117
116
|
self._appendStandardTags()
|
118
|
-
for feedb in self.genFeedbacks:
|
119
|
-
self.tmpEle.append(eth.getFeedBEle(feedb))
|
120
117
|
|
121
118
|
self.htmlRoot = ET.Element("div")
|
122
119
|
self.htmlRoot.append(self.getMainTextElement())
|
@@ -139,10 +136,17 @@ class QuestionParser:
|
|
139
136
|
if ansList is not None:
|
140
137
|
for ele in ansList:
|
141
138
|
self.tmpEle.append(ele)
|
139
|
+
self._finalizeParsing()
|
140
|
+
|
141
|
+
def _finalizeParsing(self) -> None:
|
142
|
+
"""Pass the parsed element trees to the question.
|
143
|
+
|
144
|
+
Intended for the subclasses to do extra stuff.
|
145
|
+
"""
|
142
146
|
self.question._element = self.tmpEle
|
143
147
|
self.question.htmlRoot = self.htmlRoot
|
144
148
|
self.question.isParsed = True
|
145
|
-
self.question.textElement = mainTextEle
|
149
|
+
self.question.textElement = self.mainTextEle
|
146
150
|
self.logger.info("Sucessfully parsed")
|
147
151
|
|
148
152
|
def getFeedBEle(
|
@@ -153,7 +157,8 @@ class QuestionParser:
|
|
153
157
|
) -> ET.Element:
|
154
158
|
span = feedBElements[feedback] if style is None else style.create()
|
155
159
|
if text is None:
|
156
|
-
text
|
160
|
+
self.logger.error("Giving a feedback without providing text is nonsens")
|
161
|
+
text = self.rawInput.get(Tags.GENERALFB)
|
157
162
|
ele = ET.Element(feedback, format="html")
|
158
163
|
par = TextElements.PLEFT.create()
|
159
164
|
span.text = text
|
@@ -169,6 +174,7 @@ class QuestionParser:
|
|
169
174
|
self,
|
170
175
|
result: float = 0.0,
|
171
176
|
fraction: float = 100,
|
177
|
+
feedback: str | None = None,
|
172
178
|
format: str = "moodle_auto_format",
|
173
179
|
) -> ET.Element:
|
174
180
|
"""Get ``<answer/>`` Element specific for the numerical Question.
|
@@ -184,11 +190,13 @@ class QuestionParser:
|
|
184
190
|
fraction=str(fraction),
|
185
191
|
format=format,
|
186
192
|
)
|
193
|
+
if feedback is None:
|
194
|
+
feedback = self.rawInput.get(Tags.TRUEFB)
|
187
195
|
ansEle.append(
|
188
|
-
|
189
|
-
XMLTags.ANSFEEDBACK,
|
190
|
-
|
191
|
-
TextElements.SPANGREEN,
|
196
|
+
self.getFeedBEle(
|
197
|
+
feedback=XMLTags.ANSFEEDBACK,
|
198
|
+
text=feedback,
|
199
|
+
style=TextElements.SPANGREEN,
|
192
200
|
),
|
193
201
|
)
|
194
202
|
absTolerance = round(result * self.rawInput.get(Tags.TOLERANCE), 4)
|
excel2moodle/core/question.py
CHANGED
@@ -2,6 +2,7 @@ import base64
|
|
2
2
|
import logging
|
3
3
|
import math
|
4
4
|
import re
|
5
|
+
from copy import deepcopy
|
5
6
|
from pathlib import Path
|
6
7
|
from types import UnionType
|
7
8
|
from typing import TYPE_CHECKING, ClassVar, Literal, overload
|
@@ -37,12 +38,23 @@ class QuestionData(dict):
|
|
37
38
|
@overload
|
38
39
|
def get(
|
39
40
|
self,
|
40
|
-
key: Literal[
|
41
|
+
key: Literal[
|
42
|
+
Tags.NAME,
|
43
|
+
Tags.ANSTYPE,
|
44
|
+
Tags.PICTURE,
|
45
|
+
Tags.EQUATION,
|
46
|
+
Tags.TRUEFB,
|
47
|
+
Tags.FALSEFB,
|
48
|
+
Tags.GENERALFB,
|
49
|
+
Tags.WRONGSIGNFB,
|
50
|
+
],
|
51
|
+
default: object = None,
|
41
52
|
) -> str: ...
|
42
53
|
@overload
|
43
54
|
def get(
|
44
55
|
self,
|
45
56
|
key: Literal[Tags.BPOINTS, Tags.TRUE, Tags.FALSE, Tags.QUESTIONPART, Tags.TEXT],
|
57
|
+
default: object = None,
|
46
58
|
) -> list: ...
|
47
59
|
@overload
|
48
60
|
def get(
|
@@ -53,19 +65,22 @@ class QuestionData(dict):
|
|
53
65
|
Tags.ANSPICWIDTH,
|
54
66
|
Tags.WRONGSIGNPERCENT,
|
55
67
|
],
|
68
|
+
default: object = None,
|
56
69
|
) -> int: ...
|
57
70
|
@overload
|
58
71
|
def get(
|
59
|
-
self, key: Literal[Tags.PARTTYPE, Tags.TYPE]
|
72
|
+
self, key: Literal[Tags.PARTTYPE, Tags.TYPE], default: object = None
|
60
73
|
) -> Literal["MC", "NFM", "CLOZE"]: ...
|
61
74
|
@overload
|
62
75
|
def get(
|
63
|
-
self,
|
76
|
+
self,
|
77
|
+
key: Literal[Tags.TOLERANCE, Tags.POINTS, Tags.FIRSTRESULT],
|
78
|
+
default: object = None,
|
64
79
|
) -> float: ...
|
65
80
|
@overload
|
66
|
-
def get(self, key: Literal[Tags.RESULT]) -> float | str: ...
|
81
|
+
def get(self, key: Literal[Tags.RESULT], default: object = None) -> float | str: ...
|
67
82
|
|
68
|
-
def get(self, key
|
83
|
+
def get(self, key, default=None):
|
69
84
|
"""Get the value for `key` with correct type.
|
70
85
|
|
71
86
|
If `key == Tags.TOLERANCE` the tolerance is checked to be a perc. fraction
|
@@ -172,7 +187,7 @@ class Question:
|
|
172
187
|
self.textElement.text = ET.CDATA(
|
173
188
|
ET.tostring(self.htmlRoot, encoding="unicode", pretty_print=True)
|
174
189
|
)
|
175
|
-
return self._element
|
190
|
+
return deepcopy(self._element)
|
176
191
|
|
177
192
|
def _setID(self, id=0) -> None:
|
178
193
|
if id == 0:
|
@@ -186,6 +201,11 @@ class ParametricQuestion(Question):
|
|
186
201
|
super().__init__(*args, **kwargs)
|
187
202
|
self.rules: list[str] = []
|
188
203
|
self.parametrics: Parametrics
|
204
|
+
self._variant: int
|
205
|
+
|
206
|
+
@property
|
207
|
+
def currentVariant(self) -> int:
|
208
|
+
return self._variant
|
189
209
|
|
190
210
|
def getUpdatedElement(self, variant: int = 0) -> ET.Element:
|
191
211
|
"""Update the bulletItem.text With the values for variant.
|
@@ -201,6 +221,7 @@ class ParametricQuestion(Question):
|
|
201
221
|
self.bulletList.updateBullets(
|
202
222
|
variables=self.parametrics.variables, variant=variant
|
203
223
|
)
|
224
|
+
self._variant = variant
|
204
225
|
return super().getUpdatedElement(variant)
|
205
226
|
|
206
227
|
|
@@ -272,32 +293,31 @@ class Parametrics:
|
|
272
293
|
self.results[num] = []
|
273
294
|
for variant in range(self.variants):
|
274
295
|
type(self).setupAstIntprt(self._variables, variant)
|
275
|
-
self.logger.debug("Setup The interpreter for variant: %s", variant)
|
296
|
+
self.logger.debug("Setup The interpreter for variant: %s", variant + 1)
|
276
297
|
for num, eq in self.equations.items():
|
277
298
|
result = type(self).astEval(eq)
|
299
|
+
if not isinstance(result, float | int):
|
300
|
+
msg = f"The expression: '{eq}' = {result} could not be evaluated."
|
301
|
+
raise QNotParsedException(msg, self.id)
|
278
302
|
self.logger.info(
|
279
303
|
"Calculated expr. %s (variant %s): %s = %.3f ",
|
280
304
|
num,
|
281
|
-
variant,
|
305
|
+
variant + 1,
|
282
306
|
eq,
|
283
307
|
result,
|
284
308
|
)
|
285
|
-
if
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
self._resultChecker,
|
293
|
-
)
|
294
|
-
self.logger.debug(
|
295
|
-
"Calculated result %s for variant %s", result, variant
|
309
|
+
if variant == 0 and not math.isclose(
|
310
|
+
result, self._resultChecker[num], rel_tol=0.01
|
311
|
+
):
|
312
|
+
self.logger.warning(
|
313
|
+
"The calculated result %s differs from given firstResult: %s",
|
314
|
+
result,
|
315
|
+
self._resultChecker,
|
296
316
|
)
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
317
|
+
self.logger.debug(
|
318
|
+
"Calculated result %s for variant %s", result, variant
|
319
|
+
)
|
320
|
+
self.results[num].append(round(result, 3))
|
301
321
|
return self.results
|
302
322
|
|
303
323
|
def resetVariables(self) -> None:
|