excel2moodle 0.6.1__py3-none-any.whl → 0.6.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/core/category.py +24 -6
- excel2moodle/core/dataStructure.py +116 -27
- 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 +11 -4
- excel2moodle/extra/equationVerification.py +0 -2
- excel2moodle/question_types/cloze.py +116 -83
- 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/appUi.py +21 -7
- excel2moodle/ui/dialogs.py +12 -0
- {excel2moodle-0.6.1.dist-info → excel2moodle-0.6.2.dist-info}/METADATA +46 -19
- excel2moodle-0.6.2.dist-info/RECORD +39 -0
- excel2moodle-0.6.1.dist-info/RECORD +0 -39
- {excel2moodle-0.6.1.dist-info → excel2moodle-0.6.2.dist-info}/WHEEL +0 -0
- {excel2moodle-0.6.1.dist-info → excel2moodle-0.6.2.dist-info}/entry_points.txt +0 -0
- {excel2moodle-0.6.1.dist-info → excel2moodle-0.6.2.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.6.1.dist-info → excel2moodle-0.6.2.dist-info}/top_level.txt +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
|
|
@@ -30,7 +29,7 @@ class Category:
|
|
30
29
|
self.desc = str(description)
|
31
30
|
self.dataframe: pd.DataFrame = dataframe
|
32
31
|
self.settings: dict[str, float | str] = settings if settings else {}
|
33
|
-
self.
|
32
|
+
self._questions: dict[int, Question]
|
34
33
|
self.maxVariants: int | None = None
|
35
34
|
loggerObj.info("initializing Category %s", self.NAME)
|
36
35
|
|
@@ -50,6 +49,18 @@ class Category:
|
|
50
49
|
def id(self) -> str:
|
51
50
|
return f"{self.n:02d}"
|
52
51
|
|
52
|
+
@property
|
53
|
+
def questions(self) -> dict:
|
54
|
+
if not hasattr(self, "_questions"):
|
55
|
+
msg = "Category question are not yet initialized"
|
56
|
+
raise ValueError(msg)
|
57
|
+
return self._questions
|
58
|
+
|
59
|
+
def appendQuestion(self, questionNumber: int, question) -> None:
|
60
|
+
if not hasattr(self, "_questions"):
|
61
|
+
self._questions: dict[int, Question] = {}
|
62
|
+
self._questions[questionNumber] = question
|
63
|
+
|
53
64
|
def __hash__(self) -> int:
|
54
65
|
return hash(self.NAME)
|
55
66
|
|
@@ -58,13 +69,20 @@ class Category:
|
|
58
69
|
return self.NAME == other.NAME
|
59
70
|
return False
|
60
71
|
|
61
|
-
def getCategoryHeader(self) -> ET.Element:
|
72
|
+
def getCategoryHeader(self, subCategory: str | None = None) -> ET.Element:
|
62
73
|
"""Insert an <question type='category'> before all Questions of this Category."""
|
63
74
|
header = ET.Element("question", type="category")
|
64
75
|
cat = ET.SubElement(header, "category")
|
65
76
|
info = ET.SubElement(header, "info", format="html")
|
66
|
-
|
77
|
+
catStr = (
|
78
|
+
f"$module$/top/{self.NAME}"
|
79
|
+
if subCategory is None
|
80
|
+
else f"$module$/top/{self.NAME}/Question-{subCategory[2:]}_Variants"
|
81
|
+
)
|
82
|
+
ET.SubElement(cat, "text").text = catStr
|
67
83
|
ET.SubElement(info, "text").text = str(self.desc)
|
68
|
-
ET.SubElement(header, "idnumber").text =
|
84
|
+
ET.SubElement(header, "idnumber").text = (
|
85
|
+
f"cat-{self.id}" if subCategory is None else f"variants-{subCategory}"
|
86
|
+
)
|
69
87
|
ET.indent(header)
|
70
88
|
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,22 +98,39 @@ 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
|
+
|
109
|
+
Before raising it logges the exceptions with a meaningful message.
|
110
|
+
|
97
111
|
"""
|
98
112
|
sheetPath = sheetPath if sheetPath else self.spreadsheet
|
99
113
|
logger.info("Start Parsing the Excel Metadata Sheet\n")
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
114
|
+
try:
|
115
|
+
with Path(sheetPath).open("rb") as f:
|
116
|
+
settingDf = pd.read_excel(
|
117
|
+
f,
|
118
|
+
sheet_name="settings",
|
119
|
+
index_col=0,
|
120
|
+
engine="calamine",
|
121
|
+
)
|
122
|
+
except ValueError:
|
123
|
+
logger.exception(
|
124
|
+
"Did you forget to specify a 'settings' sheet in the file?"
|
106
125
|
)
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
126
|
+
raise
|
127
|
+
logger.debug("Found the settings: \n\t%s", settingDf)
|
128
|
+
settingDf = self.harmonizeDFIndex(settingDf)
|
129
|
+
for tag, value in settingDf.iterrows():
|
130
|
+
val = value.iloc[0]
|
131
|
+
if pd.notna(val):
|
132
|
+
self.settings.set(tag, val)
|
133
|
+
|
113
134
|
try:
|
114
135
|
self._validateProjectSettings(sheetPath=sheetPath)
|
115
136
|
except InvalidFieldException:
|
@@ -342,13 +363,18 @@ class QuestionDB:
|
|
342
363
|
else:
|
343
364
|
msg = "couldn't setup Parser"
|
344
365
|
raise QNotParsedException(msg, question.id)
|
345
|
-
category.
|
366
|
+
category.appendQuestion(qNumber, question)
|
346
367
|
return question
|
347
368
|
|
348
369
|
def appendQuestions(
|
349
|
-
self,
|
370
|
+
self,
|
371
|
+
questions: list[QuestionItem],
|
372
|
+
file: Path | None = None,
|
373
|
+
pCount: int = 0,
|
374
|
+
qCount: int = 0,
|
350
375
|
) -> None:
|
351
376
|
"""Append selected question Elements to the tree."""
|
377
|
+
self._exportedQuestions.clear()
|
352
378
|
tree = ET.Element("quiz")
|
353
379
|
catdict: dict[Category, list[Question]] = {}
|
354
380
|
for q in questions:
|
@@ -365,6 +391,8 @@ class QuestionDB:
|
|
365
391
|
includeHeader=self.settings.get(Tags.INCLUDEINCATS),
|
366
392
|
)
|
367
393
|
stringHelpers.printDom(tree, file=file)
|
394
|
+
if self.settings.get(Tags.GENEXPORTREPORT):
|
395
|
+
self.generateExportReport(file, pCount=pCount, qCount=qCount)
|
368
396
|
|
369
397
|
def _appendQElements(
|
370
398
|
self,
|
@@ -373,17 +401,78 @@ class QuestionDB:
|
|
373
401
|
tree: ET.Element,
|
374
402
|
includeHeader: bool = True,
|
375
403
|
) -> None:
|
376
|
-
|
404
|
+
variant: int = self.settings.get(Tags.QUESTIONVARIANT)
|
405
|
+
if includeHeader or variant == -1:
|
377
406
|
tree.append(cat.getCategoryHeader())
|
378
407
|
logger.debug(f"Appended a new category item {cat=}")
|
379
|
-
|
408
|
+
self._exportedAll: bool = True
|
380
409
|
for q in qList:
|
381
|
-
if
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
410
|
+
if not isinstance(q, ParametricQuestion):
|
411
|
+
tree.append(q.getUpdatedElement())
|
412
|
+
self._exportedQuestions.append(q)
|
413
|
+
continue
|
414
|
+
if variant == -1:
|
415
|
+
tree.append(cat.getCategoryHeader(subCategory=q.id))
|
416
|
+
for var in range(q.parametrics.variants):
|
417
|
+
tree.append(q.getUpdatedElement(variant=var))
|
418
|
+
elif variant == 0 or variant > q.parametrics.variants:
|
419
|
+
dialog = QuestionVariantDialog(self.window, q)
|
420
|
+
if dialog.exec() == QDialog.Accepted:
|
421
|
+
variant = dialog.variant
|
422
|
+
logger.debug("Die Fragen-Variante %s wurde gewählt", variant)
|
423
|
+
else:
|
424
|
+
logger.warning("Keine Fragenvariante wurde gewählt.")
|
425
|
+
tree.append(q.getUpdatedElement(variant=variant))
|
426
|
+
self._exportedQuestions.append(q)
|
427
|
+
|
428
|
+
def generateExportReport(
|
429
|
+
self, file: Path | None = None, pCount: int = 0, qCount: int = 0
|
430
|
+
) -> None:
|
431
|
+
"""Generate a YAML report of the exported questions."""
|
432
|
+
if not self._exportedQuestions:
|
433
|
+
return
|
434
|
+
if file:
|
435
|
+
base_path = file.with_name(f"{file.stem}_export_report.yaml")
|
436
|
+
else:
|
437
|
+
base_path = self.spreadsheet.parent / "export_report.yaml"
|
438
|
+
|
439
|
+
for i in range(99):
|
440
|
+
report_path = base_path.with_name(f"{base_path.stem}-{i:02d}.yaml")
|
441
|
+
if not report_path.resolve().exists():
|
442
|
+
break
|
443
|
+
|
444
|
+
report_data = {
|
445
|
+
"export_metadata": {
|
446
|
+
"export_time": dt.datetime.now(tz=None).strftime("%Y-%m-%d %H:%M:%S"),
|
447
|
+
"excel2moodle_version": __version__,
|
448
|
+
},
|
449
|
+
"categories": {},
|
450
|
+
}
|
451
|
+
if qCount != 0:
|
452
|
+
report_data["export_metadata"]["question_count"] = qCount
|
453
|
+
if pCount != 0:
|
454
|
+
report_data["export_metadata"]["total_point_count"] = pCount
|
455
|
+
|
456
|
+
sorted_questions = sorted(
|
457
|
+
self._exportedQuestions, key=lambda q: (q.category.name, q.id)
|
458
|
+
)
|
459
|
+
|
460
|
+
for question in sorted_questions:
|
461
|
+
category_name = question.category.name
|
462
|
+
if category_name not in report_data["categories"]:
|
463
|
+
report_data["categories"][category_name] = {
|
464
|
+
"description": question.category.desc,
|
465
|
+
"questions": [],
|
466
|
+
}
|
467
|
+
|
468
|
+
question_data = {"id": question.id, "name": question.name}
|
469
|
+
if isinstance(question, ParametricQuestion) and question.currentVariant > 0:
|
470
|
+
if self._exportedAll:
|
471
|
+
question_data["exported_variant"] = "all"
|
472
|
+
else:
|
473
|
+
question_data["exported_variant"] = question.currentVariant + 1
|
474
|
+
|
475
|
+
report_data["categories"][category_name]["questions"].append(question_data)
|
476
|
+
|
477
|
+
with report_path.open("w") as f:
|
478
|
+
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:
|
excel2moodle/core/settings.py
CHANGED
@@ -60,8 +60,9 @@ class Tags(StrEnum):
|
|
60
60
|
"""Get type of the keys data."""
|
61
61
|
return self._typ_
|
62
62
|
|
63
|
-
QUESTIONVARIANT = "
|
64
|
-
INCLUDEINCATS = "
|
63
|
+
QUESTIONVARIANT = "defaultquestionvariant", int, 1, "testgen"
|
64
|
+
INCLUDEINCATS = "includecats", bool, False, "testgen"
|
65
|
+
GENEXPORTREPORT = "exportreport", bool, False, "testgen"
|
65
66
|
TOLERANCE = "tolerance", float, 0.01, "parser/nf"
|
66
67
|
PICTUREFOLDER = "pictureFolder", Path, None, "core"
|
67
68
|
PICTURESUBFOLDER = "imgfolder", str, "Abbildungen", "project"
|
@@ -87,8 +88,13 @@ class Tags(StrEnum):
|
|
87
88
|
POINTS = "points", float, 1.0
|
88
89
|
PICTUREWIDTH = "imgwidth", int, 500
|
89
90
|
ANSPICWIDTH = "answerimgwidth", int, 120
|
90
|
-
WRONGSIGNPERCENT = "wrongsignpercent", int, 50
|
91
91
|
FIRSTRESULT = "firstresult", float, 0
|
92
|
+
WRONGSIGNPERCENT = "wrongsignpercent", int, 50
|
93
|
+
WRONGSIGNFB = "wrongsignfeedback", str, "your result has the wrong sign (+-)"
|
94
|
+
TRUEFB = "truefeedback", str, "congratulations!!! your answer is right."
|
95
|
+
FALSEFB = "falsefeedback", str, "Your answer is sadly wrong, try again!!!"
|
96
|
+
PCORRECFB = "partialcorrectfeedback", str, "Your answer is partially right."
|
97
|
+
GENERALFB = "feedback", str, "You answered this question."
|
92
98
|
|
93
99
|
|
94
100
|
class Settings:
|
@@ -121,7 +127,7 @@ class Settings:
|
|
121
127
|
) -> int: ...
|
122
128
|
@overload
|
123
129
|
@classmethod
|
124
|
-
def get(cls, key: Literal[Tags.INCLUDEINCATS]) -> bool: ...
|
130
|
+
def get(cls, key: Literal[Tags.INCLUDEINCATS, Tags.GENEXPORTREPORT]) -> bool: ...
|
125
131
|
@overload
|
126
132
|
@classmethod
|
127
133
|
def get(
|
@@ -132,6 +138,7 @@ class Settings:
|
|
132
138
|
Tags.LOGFILE,
|
133
139
|
Tags.CATEGORIESSHEET,
|
134
140
|
Tags.IMPORTMODULE,
|
141
|
+
Tags.WRONGSIGNFB,
|
135
142
|
],
|
136
143
|
) -> str: ...
|
137
144
|
@overload
|