excel2moodle 0.3.6__py3-none-any.whl → 0.4.0__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 +6 -12
- excel2moodle/__main__.py +14 -3
- excel2moodle/core/category.py +6 -44
- excel2moodle/core/dataStructure.py +249 -82
- excel2moodle/core/globals.py +1 -21
- excel2moodle/core/parser.py +34 -204
- excel2moodle/core/question.py +108 -57
- excel2moodle/core/settings.py +201 -0
- excel2moodle/core/stringHelpers.py +10 -33
- excel2moodle/core/{questionValidator.py → validator.py} +32 -34
- excel2moodle/logger.py +1 -1
- excel2moodle/question_types/__init__.py +33 -0
- excel2moodle/question_types/mc.py +127 -0
- excel2moodle/question_types/nf.py +29 -0
- excel2moodle/question_types/nfm.py +91 -0
- excel2moodle/ui/appUi.py +67 -47
- excel2moodle/ui/dialogs.py +43 -34
- excel2moodle/ui/exportSettingsDialog.py +79 -0
- excel2moodle/ui/windowDoc.py +9 -17
- excel2moodle/ui/windowMain.py +220 -225
- {excel2moodle-0.3.6.dist-info → excel2moodle-0.4.0.dist-info}/METADATA +2 -2
- excel2moodle-0.4.0.dist-info/RECORD +37 -0
- {excel2moodle-0.3.6.dist-info → excel2moodle-0.4.0.dist-info}/WHEEL +1 -1
- {excel2moodle-0.3.6.dist-info → excel2moodle-0.4.0.dist-info}/entry_points.txt +3 -0
- excel2moodle/core/questionWriter.py +0 -267
- excel2moodle/ui/settings.py +0 -123
- excel2moodle-0.3.6.dist-info/RECORD +0 -33
- {excel2moodle-0.3.6.dist-info → excel2moodle-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.3.6.dist-info → excel2moodle-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,201 @@
|
|
1
|
+
"""Settings module provides the adjusted subclass of ``PySide6.QtCore.QSettings``."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from enum import StrEnum
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import ClassVar, Literal, overload
|
7
|
+
|
8
|
+
from PySide6.QtCore import QSettings, QTimer, Signal
|
9
|
+
|
10
|
+
import excel2moodle
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class SettingsKey(StrEnum):
|
16
|
+
"""Settings Keys are needed to always acess the correct Value.
|
17
|
+
|
18
|
+
As the QSettings settings are accesed via strings, which could easily gotten wrong.
|
19
|
+
Further, this Enum defines, which type a setting has to be.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __new__(
|
23
|
+
cls,
|
24
|
+
key: str,
|
25
|
+
place: str,
|
26
|
+
typ: type,
|
27
|
+
default: str | float | Path | bool | None,
|
28
|
+
):
|
29
|
+
"""Define new settings class."""
|
30
|
+
obj = str.__new__(cls, key)
|
31
|
+
obj._value_ = key
|
32
|
+
obj._place_ = place
|
33
|
+
obj._default_ = default
|
34
|
+
obj._typ_ = typ
|
35
|
+
return obj
|
36
|
+
|
37
|
+
def __init__(
|
38
|
+
self, _, place: str, typ: type, default: str | float | Path | None
|
39
|
+
) -> None:
|
40
|
+
self._typ_ = typ
|
41
|
+
self._place_ = place
|
42
|
+
self._default_ = default
|
43
|
+
self._full_ = f"{self._place_}/{self._value_}"
|
44
|
+
|
45
|
+
@property
|
46
|
+
def default(self) -> str | int | float | Path | bool | None:
|
47
|
+
"""Get default value for the key."""
|
48
|
+
return self._default_
|
49
|
+
|
50
|
+
@property
|
51
|
+
def place(self) -> str:
|
52
|
+
return self._place_
|
53
|
+
|
54
|
+
@property
|
55
|
+
def full(self) -> str:
|
56
|
+
return self._full_
|
57
|
+
|
58
|
+
def typ(self) -> type:
|
59
|
+
"""Get default value for the key."""
|
60
|
+
return self._typ_
|
61
|
+
|
62
|
+
QUESTIONVARIANT = "defaultQuestionVariant", "testgen", int, 0
|
63
|
+
INCLUDEINCATS = "includeCats", "testgen", bool, False
|
64
|
+
TOLERANCE = "tolerance", "parser/nf", int, 1
|
65
|
+
PICTUREFOLDER = "pictureFolder", "core", Path, None
|
66
|
+
SPREADSHEETFOLDER = "spreadsheetFolder", "core", Path, None
|
67
|
+
LOGLEVEL = "loglevel", "core", str, "INFO"
|
68
|
+
LOGFILE = "logfile", "core", str, "excel2moodleLogFile.log"
|
69
|
+
CATEGORIESSHEET = "categoriesSheet", "core", str, "Kategorien"
|
70
|
+
VERSION = "version", "project", int, 1
|
71
|
+
POINTS = "points", "project", float, 1.0
|
72
|
+
PICTURESUBFOLDER = "imgFolder", "project", str, "Abbildungen"
|
73
|
+
PICTUREWIDTH = "imgWidth", "project", int, 500
|
74
|
+
ANSPICWIDTH = "answerImgWidth", "project", int, 120
|
75
|
+
|
76
|
+
|
77
|
+
class Settings(QSettings):
|
78
|
+
"""Settings for Excel2moodle."""
|
79
|
+
|
80
|
+
shPathChanged = Signal(Path)
|
81
|
+
localSettings: ClassVar[dict[str, str | float | Path]] = {}
|
82
|
+
|
83
|
+
def __init__(self) -> None:
|
84
|
+
"""Instantiate the settings."""
|
85
|
+
super().__init__("jbosse3", "excel2moodle")
|
86
|
+
if excel2moodle.isMainState():
|
87
|
+
logger.info("Settings are stored under: %s", self.fileName())
|
88
|
+
if self.contains(SettingsKey.SPREADSHEETFOLDER.full):
|
89
|
+
self.sheet = self.get(SettingsKey.SPREADSHEETFOLDER)
|
90
|
+
if self.sheet.is_file():
|
91
|
+
QTimer.singleShot(300, self._emitSpreadsheetChanged)
|
92
|
+
|
93
|
+
def _emitSpreadsheetChanged(self) -> None:
|
94
|
+
self.shPathChanged.emit(self.sheet)
|
95
|
+
|
96
|
+
@overload
|
97
|
+
def get(
|
98
|
+
self,
|
99
|
+
key: Literal[
|
100
|
+
SettingsKey.QUESTIONVARIANT,
|
101
|
+
SettingsKey.TOLERANCE,
|
102
|
+
SettingsKey.VERSION,
|
103
|
+
SettingsKey.POINTS,
|
104
|
+
SettingsKey.PICTUREWIDTH,
|
105
|
+
SettingsKey.ANSPICWIDTH,
|
106
|
+
],
|
107
|
+
) -> int: ...
|
108
|
+
@overload
|
109
|
+
def get(self, key: Literal[SettingsKey.INCLUDEINCATS]) -> bool: ...
|
110
|
+
@overload
|
111
|
+
def get(
|
112
|
+
self,
|
113
|
+
key: Literal[
|
114
|
+
SettingsKey.PICTURESUBFOLDER,
|
115
|
+
SettingsKey.LOGLEVEL,
|
116
|
+
SettingsKey.LOGFILE,
|
117
|
+
SettingsKey.CATEGORIESSHEET,
|
118
|
+
],
|
119
|
+
) -> str: ...
|
120
|
+
@overload
|
121
|
+
def get(
|
122
|
+
self,
|
123
|
+
key: Literal[SettingsKey.PICTUREFOLDER, SettingsKey.SPREADSHEETFOLDER],
|
124
|
+
) -> Path: ...
|
125
|
+
|
126
|
+
def get(self, key: SettingsKey):
|
127
|
+
"""Get the typesafe settings value.
|
128
|
+
|
129
|
+
If local Settings are stored, they are returned.
|
130
|
+
If no setting is made, the default value is returned.
|
131
|
+
"""
|
132
|
+
if key in self.localSettings:
|
133
|
+
val = key.typ()(self.localSettings[key])
|
134
|
+
logger.debug("Returning project setting: %s = %s", key, val)
|
135
|
+
return val
|
136
|
+
if not excel2moodle.isMainState():
|
137
|
+
logger.warning("No GUI: Returning default value.")
|
138
|
+
return key.default
|
139
|
+
if key.typ() is Path:
|
140
|
+
path: Path = self.value(key.full, defaultValue=key.default)
|
141
|
+
try:
|
142
|
+
path.resolve(strict=True)
|
143
|
+
except ValueError:
|
144
|
+
logger.warning(
|
145
|
+
f"The settingsvalue {key} couldn't be fetched with correct typ",
|
146
|
+
)
|
147
|
+
return key.default
|
148
|
+
logger.debug("Returning path setting: %s = %s", key, path)
|
149
|
+
return path
|
150
|
+
raw = self.value(key.full, defaultValue=key.default, type=key.typ())
|
151
|
+
logger.debug("read a settings Value: %s of type: %s", key, key.typ())
|
152
|
+
try:
|
153
|
+
logger.debug("Returning global setting: %s = %s", key, raw)
|
154
|
+
return key.typ()(raw)
|
155
|
+
except (ValueError, TypeError):
|
156
|
+
logger.warning(
|
157
|
+
f"The settingsvalue {key} couldn't be fetched with correct typ",
|
158
|
+
)
|
159
|
+
return key.default
|
160
|
+
|
161
|
+
def set(
|
162
|
+
self,
|
163
|
+
key: SettingsKey | str,
|
164
|
+
value: float | bool | Path | str,
|
165
|
+
local: bool = False,
|
166
|
+
) -> None:
|
167
|
+
"""Set the setting to value.
|
168
|
+
|
169
|
+
Parameters
|
170
|
+
----------
|
171
|
+
local
|
172
|
+
True saves local project specific settings.
|
173
|
+
Defaults to False
|
174
|
+
The local settings are meant to be set in the first sheet `settings`
|
175
|
+
|
176
|
+
"""
|
177
|
+
if not excel2moodle.isMainState():
|
178
|
+
local = True
|
179
|
+
if local:
|
180
|
+
if key in SettingsKey:
|
181
|
+
self.localSettings[key] = value
|
182
|
+
logger.info("Saved the project setting %s = %s", key, value)
|
183
|
+
else:
|
184
|
+
logger.warning("got invalid local Setting %s = %s", key, value)
|
185
|
+
return
|
186
|
+
if not local and isinstance(key, SettingsKey):
|
187
|
+
if not isinstance(value, key.typ()):
|
188
|
+
logger.error("trying to save setting with wrong type not possible")
|
189
|
+
return
|
190
|
+
self.setValue(key.full, value)
|
191
|
+
logger.info("Saved the global setting %s = %s", key, value)
|
192
|
+
|
193
|
+
def setSpreadsheet(self, sheet: Path) -> None:
|
194
|
+
"""Save spreadsheet path and emit the changed event."""
|
195
|
+
if isinstance(sheet, Path):
|
196
|
+
self.sheet = sheet.resolve(strict=True)
|
197
|
+
logpath = str(self.sheet.parent / "excel2moodleLogFile.log")
|
198
|
+
self.set(SettingsKey.LOGFILE, logpath)
|
199
|
+
self.set(SettingsKey.SPREADSHEETFOLDER, self.sheet)
|
200
|
+
self.shPathChanged.emit(sheet)
|
201
|
+
return
|
@@ -4,12 +4,15 @@ import base64
|
|
4
4
|
from pathlib import Path
|
5
5
|
|
6
6
|
import lxml.etree as ET
|
7
|
+
from pandas import pandas
|
7
8
|
|
8
9
|
|
9
|
-
def
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
def getListFromStr(stringList: str | list[str]) -> list[str]:
|
11
|
+
"""Get a python List of strings from a semi-colon separated string."""
|
12
|
+
stripped: list[str] = []
|
13
|
+
li = stringList if isinstance(stringList, list) else stringList.split(";")
|
14
|
+
for i in li:
|
15
|
+
s = i.strip() if not pandas.isna(i) else None
|
13
16
|
if s:
|
14
17
|
stripped.append(s)
|
15
18
|
return stripped
|
@@ -20,32 +23,6 @@ def stringToFloat(string: str) -> float:
|
|
20
23
|
return float(string)
|
21
24
|
|
22
25
|
|
23
|
-
def get_bullet_string(s):
|
24
|
-
"""Formatiert die Angaben zum Statischen System hübsch."""
|
25
|
-
split = s.split(";")
|
26
|
-
s_spl = stripWhitespace(split)
|
27
|
-
name = []
|
28
|
-
var = []
|
29
|
-
quant = []
|
30
|
-
unit = []
|
31
|
-
for sc in s_spl:
|
32
|
-
sc_split = sc.split()
|
33
|
-
name.append(sc_split[0])
|
34
|
-
var.append(sc_split[1])
|
35
|
-
quant.append(sc_split[3])
|
36
|
-
unit.append(sc_split[4])
|
37
|
-
bulletString = ['</p><ul dir="ltr">']
|
38
|
-
for i in range(len(s_spl)):
|
39
|
-
num = quant[i].split(",")
|
40
|
-
num_s = f"{num[0]!s},\\!{num[1]!s}~" if len(num) == 2 else f"{num[0]!s},\\!0~"
|
41
|
-
bulletString.append('<li style="text-align: left;">')
|
42
|
-
bulletString.append(
|
43
|
-
f"{name[i]}: \\( {var[i]} = {num_s} \\mathrm{{ {unit[i]} }}\\) </li>\n",
|
44
|
-
)
|
45
|
-
bulletString.append("<br></ul>")
|
46
|
-
return "\n".join(bulletString)
|
47
|
-
|
48
|
-
|
49
26
|
def getBase64Img(imgPath):
|
50
27
|
with open(imgPath, "rb") as img:
|
51
28
|
return base64.b64encode(img.read()).decode("utf-8")
|
@@ -62,7 +39,7 @@ def getUnitsElementAsString(unit) -> None:
|
|
62
39
|
|
63
40
|
|
64
41
|
def printDom(xmlElement: ET.Element, file: Path | None = None) -> None:
|
65
|
-
"""Prints the document tree of ``xmlTree`` to
|
42
|
+
"""Prints the document tree of ``xmlTree`` to ``file``, if specified, else dumps to stdout."""
|
66
43
|
documentTree = ET.ElementTree(xmlElement)
|
67
44
|
if file is not None:
|
68
45
|
if file.parent.exists():
|
@@ -73,11 +50,11 @@ def printDom(xmlElement: ET.Element, file: Path | None = None) -> None:
|
|
73
50
|
pretty_print=True,
|
74
51
|
)
|
75
52
|
else:
|
76
|
-
|
53
|
+
print(xmlElement.tostring()) # noqa: T201
|
77
54
|
|
78
55
|
|
79
56
|
def texWrapper(text: str | list[str], style: str) -> list[str]:
|
80
|
-
r"""
|
57
|
+
r"""Put the strings inside ``text`` into a LaTex environment.
|
81
58
|
|
82
59
|
if ``style == unit``: inside ``\\mathrm{}``
|
83
60
|
if ``style == math``: inside ``\\( \\)``
|
@@ -16,11 +16,11 @@ import pandas as pd
|
|
16
16
|
|
17
17
|
from excel2moodle.core.exceptions import InvalidFieldException
|
18
18
|
from excel2moodle.core.globals import DFIndex
|
19
|
-
from excel2moodle.core.question import Question
|
20
19
|
|
21
20
|
if TYPE_CHECKING:
|
22
21
|
from types import UnionType
|
23
22
|
|
23
|
+
|
24
24
|
logger = logging.getLogger(__name__)
|
25
25
|
|
26
26
|
|
@@ -30,25 +30,25 @@ class Validator:
|
|
30
30
|
Creates a dictionary with the data, for easier access later.
|
31
31
|
"""
|
32
32
|
|
33
|
-
def __init__(self
|
34
|
-
self.
|
35
|
-
self.category = category
|
36
|
-
self.mandatory: dict[DFIndex, type | UnionType] = {
|
33
|
+
def __init__(self) -> None:
|
34
|
+
self.allMandatory: dict[DFIndex, type | UnionType] = {
|
37
35
|
DFIndex.TEXT: str,
|
38
36
|
DFIndex.NAME: str,
|
39
37
|
DFIndex.TYPE: str,
|
40
38
|
}
|
41
|
-
self.
|
42
|
-
DFIndex.BPOINTS: str,
|
39
|
+
self.allOptional: dict[DFIndex, type | UnionType] = {
|
43
40
|
DFIndex.PICTURE: int | str,
|
44
41
|
}
|
45
|
-
self.nfOpt: dict[DFIndex, type | UnionType] = {
|
42
|
+
self.nfOpt: dict[DFIndex, type | UnionType] = {
|
43
|
+
DFIndex.BPOINTS: str,
|
44
|
+
}
|
46
45
|
self.nfMand: dict[DFIndex, type | UnionType] = {
|
47
46
|
DFIndex.RESULT: float | int,
|
48
47
|
}
|
49
48
|
self.nfmOpt: dict[DFIndex, type | UnionType] = {}
|
50
49
|
self.nfmMand: dict[DFIndex, type | UnionType] = {
|
51
50
|
DFIndex.RESULT: str,
|
51
|
+
DFIndex.BPOINTS: str,
|
52
52
|
}
|
53
53
|
self.mcOpt: dict[DFIndex, type | UnionType] = {}
|
54
54
|
self.mcMand: dict[DFIndex, type | UnionType] = {
|
@@ -63,35 +63,41 @@ class Validator:
|
|
63
63
|
"NFM": (self.nfmOpt, self.nfmMand),
|
64
64
|
}
|
65
65
|
|
66
|
-
def setup(self, df: pd.Series, index: int) ->
|
66
|
+
def setup(self, df: pd.Series, index: int) -> None:
|
67
67
|
self.df = df
|
68
68
|
self.index = index
|
69
|
+
self.mandatory = {}
|
70
|
+
self.optional = {}
|
69
71
|
typ = self.df.loc[DFIndex.TYPE]
|
72
|
+
if typ not in self.mapper:
|
73
|
+
msg = f"No valid question type provided. {typ} is not a known type"
|
74
|
+
raise InvalidFieldException(msg, "index:02d", DFIndex.TYPE)
|
75
|
+
self.mandatory.update(self.allMandatory)
|
76
|
+
self.optional.update(self.allOptional)
|
70
77
|
self.mandatory.update(self.mapper[typ][1])
|
71
78
|
self.optional.update(self.mapper[typ][0])
|
72
|
-
return True
|
73
79
|
|
74
|
-
def validate(self) ->
|
75
|
-
|
80
|
+
def validate(self) -> None:
|
81
|
+
qid = f"{self.index:02d}"
|
76
82
|
checker, missing = self._mandatory()
|
77
83
|
if not checker:
|
78
|
-
msg = f"Question {
|
84
|
+
msg = f"Question {qid} misses the key {missing}"
|
79
85
|
if missing is not None:
|
80
|
-
raise InvalidFieldException(msg,
|
86
|
+
raise InvalidFieldException(msg, qid, missing)
|
81
87
|
checker, missing = self._typeCheck()
|
82
88
|
if not checker:
|
83
|
-
msg = f"Question {
|
89
|
+
msg = f"Question {qid} has wrong typed data {missing}"
|
84
90
|
if missing is not None:
|
85
|
-
raise InvalidFieldException(msg,
|
86
|
-
self._getQuestion()
|
87
|
-
self._getData()
|
88
|
-
return True
|
91
|
+
raise InvalidFieldException(msg, qid, missing)
|
89
92
|
|
90
|
-
def
|
93
|
+
def getQuestionRawData(self) -> dict[str, str | float | list[str]]:
|
94
|
+
"""Get the data from the spreadsheet as a dictionary."""
|
91
95
|
self.qdata: dict[str, str | float | int | list] = {}
|
92
96
|
for idx, val in self.df.items():
|
93
97
|
if not isinstance(idx, str):
|
94
98
|
continue
|
99
|
+
if pd.isna(val):
|
100
|
+
continue
|
95
101
|
if idx in self.qdata:
|
96
102
|
if isinstance(self.qdata[idx], list):
|
97
103
|
self.qdata[idx].append(val)
|
@@ -100,6 +106,7 @@ class Validator:
|
|
100
106
|
self.qdata[idx] = [existing, val]
|
101
107
|
else:
|
102
108
|
self.qdata[idx] = val
|
109
|
+
return self.qdata
|
103
110
|
|
104
111
|
def _mandatory(self) -> tuple[bool, DFIndex | None]:
|
105
112
|
"""Detects if all keys of mandatory are filled with values."""
|
@@ -119,26 +126,17 @@ class Validator:
|
|
119
126
|
def _typeCheck(self) -> tuple[bool, list[DFIndex] | None]:
|
120
127
|
invalid: list[DFIndex] = []
|
121
128
|
for field, typ in self.mandatory.items():
|
122
|
-
if isinstance(self.df[field], pd.Series):
|
129
|
+
if field in self.df and isinstance(self.df[field], pd.Series):
|
123
130
|
for f in self.df[field]:
|
124
131
|
if pd.notna(f) and not isinstance(f, typ):
|
125
132
|
invalid.append(field)
|
126
133
|
elif not isinstance(self.df[field], typ):
|
127
134
|
invalid.append(field)
|
128
135
|
for field, typ in self.optional.items():
|
129
|
-
if field in self.df:
|
130
|
-
|
131
|
-
|
136
|
+
if field in self.df and isinstance(self.df[field], pd.Series):
|
137
|
+
for f in self.df[field]:
|
138
|
+
if pd.notna(f) and not isinstance(f, typ):
|
139
|
+
invalid.append(field)
|
132
140
|
if len(invalid) == 0:
|
133
141
|
return True, None
|
134
142
|
return False, invalid
|
135
|
-
|
136
|
-
def _getQuestion(self) -> None:
|
137
|
-
name = self.df[DFIndex.NAME]
|
138
|
-
qtype = self.df[DFIndex.TYPE]
|
139
|
-
self.question = Question(
|
140
|
-
self.category,
|
141
|
-
name=str(name),
|
142
|
-
number=self.index,
|
143
|
-
qtype=str(qtype),
|
144
|
-
)
|
excel2moodle/logger.py
CHANGED
@@ -0,0 +1,33 @@
|
|
1
|
+
"""Implementations of all different question types.
|
2
|
+
|
3
|
+
For each question type supported by excel2moodle here are the implementations.
|
4
|
+
Two classes need to be defined for each type:
|
5
|
+
|
6
|
+
* The Question class subclassing core.Question()
|
7
|
+
* The Parser class subclassing core.Parser()
|
8
|
+
|
9
|
+
Both go into a module named ``excel2moodle.question_types.type.py``
|
10
|
+
"""
|
11
|
+
|
12
|
+
from enum import Enum
|
13
|
+
|
14
|
+
from excel2moodle.core.category import Category
|
15
|
+
from excel2moodle.question_types.mc import MCQuestion
|
16
|
+
from excel2moodle.question_types.nf import NFQuestion
|
17
|
+
from excel2moodle.question_types.nfm import NFMQuestion
|
18
|
+
|
19
|
+
|
20
|
+
class QuestionTypeMapping(Enum):
|
21
|
+
"""The Mapping between question-types name and the classes."""
|
22
|
+
|
23
|
+
MC = MCQuestion
|
24
|
+
NF = NFQuestion
|
25
|
+
NFM = NFMQuestion
|
26
|
+
|
27
|
+
def create(
|
28
|
+
self,
|
29
|
+
category: Category,
|
30
|
+
questionData: dict[str, str | int | float | list[str]],
|
31
|
+
**kwargs,
|
32
|
+
):
|
33
|
+
return self.value(category, questionData, **kwargs)
|
@@ -0,0 +1,127 @@
|
|
1
|
+
"""Multiple choice Question implementation."""
|
2
|
+
|
3
|
+
from typing import ClassVar
|
4
|
+
|
5
|
+
import lxml.etree as ET
|
6
|
+
|
7
|
+
import excel2moodle.core.etHelpers as eth
|
8
|
+
from excel2moodle.core import stringHelpers
|
9
|
+
from excel2moodle.core.exceptions import InvalidFieldException
|
10
|
+
from excel2moodle.core.globals import (
|
11
|
+
DFIndex,
|
12
|
+
TextElements,
|
13
|
+
XMLTags,
|
14
|
+
feedbackStr,
|
15
|
+
)
|
16
|
+
from excel2moodle.core.parser import QuestionParser
|
17
|
+
from excel2moodle.core.question import Picture, Question
|
18
|
+
from excel2moodle.core.settings import SettingsKey
|
19
|
+
|
20
|
+
|
21
|
+
class MCQuestion(Question):
|
22
|
+
"""Multiple-choice Question Implementation."""
|
23
|
+
|
24
|
+
standardTags: ClassVar[dict[str, str | float]] = {
|
25
|
+
"single": "false",
|
26
|
+
"shuffleanswers": "true",
|
27
|
+
"answernumbering": "abc",
|
28
|
+
"showstandardinstruction": "0",
|
29
|
+
"shownumcorrect": "",
|
30
|
+
}
|
31
|
+
|
32
|
+
def __init__(self, *args, **kwargs) -> None:
|
33
|
+
super().__init__(*args, **kwargs)
|
34
|
+
self.AnsStyles = ["math", "unit", "text", "picture"]
|
35
|
+
|
36
|
+
|
37
|
+
class MCQuestionParser(QuestionParser):
|
38
|
+
"""Parser for the multiple choice Question."""
|
39
|
+
|
40
|
+
def __init__(self) -> None:
|
41
|
+
super().__init__()
|
42
|
+
self.genFeedbacks = [
|
43
|
+
XMLTags.CORFEEDB,
|
44
|
+
XMLTags.PCORFEEDB,
|
45
|
+
XMLTags.INCORFEEDB,
|
46
|
+
]
|
47
|
+
|
48
|
+
def setup(self, question: MCQuestion) -> None:
|
49
|
+
self.question: MCQuestion = question
|
50
|
+
super().setup(question)
|
51
|
+
|
52
|
+
def getAnsElementsList(
|
53
|
+
self,
|
54
|
+
answerList: list,
|
55
|
+
fraction: float = 50,
|
56
|
+
format="html",
|
57
|
+
) -> list[ET.Element]:
|
58
|
+
elementList: list[ET.Element] = []
|
59
|
+
for i, ans in enumerate(answerList):
|
60
|
+
p = TextElements.PLEFT.create()
|
61
|
+
if self.answerType == "picture":
|
62
|
+
p.append(ans)
|
63
|
+
else:
|
64
|
+
p.text = str(ans)
|
65
|
+
text = eth.getCdatTxtElement(p)
|
66
|
+
elementList.append(
|
67
|
+
ET.Element(XMLTags.ANSWER, fraction=str(fraction), format=format),
|
68
|
+
)
|
69
|
+
elementList[-1].append(text)
|
70
|
+
if fraction < 0:
|
71
|
+
elementList[-1].append(
|
72
|
+
eth.getFeedBEle(
|
73
|
+
XMLTags.ANSFEEDBACK,
|
74
|
+
text=feedbackStr["wrong"],
|
75
|
+
style=TextElements.SPANRED,
|
76
|
+
),
|
77
|
+
)
|
78
|
+
if self.answerType == "picture":
|
79
|
+
elementList[-1].append(self.falseImgs[i].element)
|
80
|
+
elif fraction > 0:
|
81
|
+
elementList[-1].append(
|
82
|
+
eth.getFeedBEle(
|
83
|
+
XMLTags.ANSFEEDBACK,
|
84
|
+
text=feedbackStr["right"],
|
85
|
+
style=TextElements.SPANGREEN,
|
86
|
+
),
|
87
|
+
)
|
88
|
+
if self.answerType == "picture":
|
89
|
+
elementList[-1].append(self.trueImgs[i].element)
|
90
|
+
return elementList
|
91
|
+
|
92
|
+
def setAnswers(self) -> list[ET.Element]:
|
93
|
+
self.answerType = self.rawInput[DFIndex.ANSTYPE]
|
94
|
+
true = stringHelpers.getListFromStr(self.rawInput[DFIndex.TRUE])
|
95
|
+
false = stringHelpers.getListFromStr(self.rawInput[DFIndex.FALSE])
|
96
|
+
if self.answerType not in self.question.AnsStyles:
|
97
|
+
msg = f"The Answer style: {self.answerType} is not supported"
|
98
|
+
raise InvalidFieldException(msg, self.question.id, DFIndex.ANSTYPE)
|
99
|
+
if self.answerType == "picture":
|
100
|
+
f = self.settings.get(SettingsKey.PICTUREFOLDER)
|
101
|
+
imgFolder = (f / self.question.katName).resolve()
|
102
|
+
w = self.settings.get(SettingsKey.ANSPICWIDTH)
|
103
|
+
self.trueImgs: list[Picture] = [
|
104
|
+
Picture(t, imgFolder, self.question.id, width=w) for t in true
|
105
|
+
]
|
106
|
+
self.falseImgs: list[Picture] = [
|
107
|
+
Picture(t, imgFolder, self.question.id, width=w) for t in false
|
108
|
+
]
|
109
|
+
trueAnsList: list[str] = [pic.htmlTag for pic in self.trueImgs]
|
110
|
+
falseAList: list[str] = [pic.htmlTag for pic in self.falseImgs]
|
111
|
+
else:
|
112
|
+
trueAnsList: list[str] = stringHelpers.texWrapper(
|
113
|
+
true, style=self.answerType
|
114
|
+
)
|
115
|
+
self.logger.debug(f"got the following true answers \n {trueAnsList=}")
|
116
|
+
falseAList: list[str] = stringHelpers.texWrapper(
|
117
|
+
false, style=self.answerType
|
118
|
+
)
|
119
|
+
self.logger.debug(f"got the following false answers \n {falseAList=}")
|
120
|
+
truefrac = 1 / len(trueAnsList) * 100
|
121
|
+
falsefrac = 1 / len(falseAList) * (-100)
|
122
|
+
self.tmpEle.find(XMLTags.PENALTY).text = str(round(truefrac / 100, 4))
|
123
|
+
ansList = self.getAnsElementsList(trueAnsList, fraction=round(truefrac, 4))
|
124
|
+
ansList.extend(
|
125
|
+
self.getAnsElementsList(falseAList, fraction=round(falsefrac, 4)),
|
126
|
+
)
|
127
|
+
return ansList
|
@@ -0,0 +1,29 @@
|
|
1
|
+
"""Numerical question implementation."""
|
2
|
+
|
3
|
+
import lxml.etree as ET
|
4
|
+
|
5
|
+
from excel2moodle.core.globals import (
|
6
|
+
DFIndex,
|
7
|
+
XMLTags,
|
8
|
+
)
|
9
|
+
from excel2moodle.core.parser import QuestionParser
|
10
|
+
from excel2moodle.core.question import Question
|
11
|
+
|
12
|
+
|
13
|
+
class NFQuestion(Question):
|
14
|
+
def __init__(self, *args, **kwargs) -> None:
|
15
|
+
super().__init__(*args, **kwargs)
|
16
|
+
|
17
|
+
|
18
|
+
class NFQuestionParser(QuestionParser):
|
19
|
+
"""Subclass for parsing numeric questions."""
|
20
|
+
|
21
|
+
def __init__(self) -> None:
|
22
|
+
super().__init__()
|
23
|
+
self.genFeedbacks = [XMLTags.GENFEEDB]
|
24
|
+
|
25
|
+
def setAnswers(self) -> list[ET.Element]:
|
26
|
+
result = self.rawInput[DFIndex.RESULT]
|
27
|
+
ansEle: list[ET.Element] = []
|
28
|
+
ansEle.append(self.getNumericAnsElement(result=result))
|
29
|
+
return ansEle
|