excel2moodle 0.7.0__tar.gz → 0.7.2__tar.gz
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-0.7.0 → excel2moodle-0.7.2}/PKG-INFO +25 -1
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/README.md +24 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/core/bullets.py +25 -9
- excel2moodle-0.7.2/excel2moodle/core/globals.py +161 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/core/parser.py +14 -14
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/core/question.py +3 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/core/settings.py +2 -93
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/question_types/cloze.py +9 -9
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/question_types/mc.py +52 -14
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/question_types/nf.py +4 -4
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/question_types/nfm.py +6 -6
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle.egg-info/PKG-INFO +25 -1
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/pyproject.toml +1 -1
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/test/test_bullets.py +20 -0
- excel2moodle-0.7.0/excel2moodle/core/globals.py +0 -91
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/LICENSE +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/__init__.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/__main__.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/core/__init__.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/core/category.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/core/dataStructure.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/core/etHelpers.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/core/exceptions.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/core/stringHelpers.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/core/validator.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/extra/__init__.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/extra/equationChecker.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/extra/scriptCaller.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/extra/updateQuery.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/extra/variableGenerator.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/logger.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/question_types/__init__.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/ui/UI_equationChecker.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/ui/UI_exportSettingsDialog.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/ui/UI_mainWindow.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/ui/UI_updateDlg.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/ui/UI_variableGenerator.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/ui/UI_variantDialog.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/ui/__init__.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/ui/appUi.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/ui/dialogs.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle/ui/treewidget.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle.egg-info/SOURCES.txt +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle.egg-info/dependency_links.txt +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle.egg-info/entry_points.txt +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle.egg-info/requires.txt +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/excel2moodle.egg-info/top_level.txt +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/setup.cfg +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/test/test_feedbacking.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/test/test_nfmParsing.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/test/test_parseQuestion.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/test/test_picture.py +0 -0
- {excel2moodle-0.7.0 → excel2moodle-0.7.2}/test/test_questionDataGet.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: excel2moodle
|
3
|
-
Version: 0.7.
|
3
|
+
Version: 0.7.2
|
4
4
|
Summary: A package for converting questions from a spreadsheet, to valid moodle-xml
|
5
5
|
Author: Jakob Bosse
|
6
6
|
License-Expression: GPL-3.0-or-later
|
@@ -90,6 +90,30 @@ If You want to support my work as well, you can by me a [coffee](https://ko-fi.c
|
|
90
90
|
|
91
91
|
# Changelogs
|
92
92
|
|
93
|
+
## 0.7.2 (2025-10-11)
|
94
|
+
small important bugfixes
|
95
|
+
|
96
|
+
### improvement (2 changes)
|
97
|
+
|
98
|
+
- [BulletPoints are decoded using regex to allow multi word names](https://gitlab.com/jbosse3/excel2moodle/-/commit/7e32e9817323054d84a0dfb9a0a241c702fd096d)
|
99
|
+
- [Restructured globals, renamed rawInput to rawData](https://gitlab.com/jbosse3/excel2moodle/-/commit/effd9c3cd196b36d49204fe715acc1ffb124549c)
|
100
|
+
|
101
|
+
### bugfix (2 changes)
|
102
|
+
|
103
|
+
- [Added the number do the mandatory tags because it is](https://gitlab.com/jbosse3/excel2moodle/-/commit/56e9b69d71504dffe7c235d69ff44dec6931db28)
|
104
|
+
- [fixed assigning the first result to all clozes](https://gitlab.com/jbosse3/excel2moodle/-/commit/09a281c253502adc23442892be03aac36e6ea720)
|
105
|
+
|
106
|
+
## 0.7.1 (2025-10-04)
|
107
|
+
feedbacking improved
|
108
|
+
|
109
|
+
### documentation (1 change)
|
110
|
+
|
111
|
+
- [documentation improvement](https://gitlab.com/jbosse3/excel2moodle/-/commit/1a1110d05b49175e049a9ca18a027216a765e277)
|
112
|
+
|
113
|
+
### feature (1 change)
|
114
|
+
|
115
|
+
- [Added MC answer feedback support](https://gitlab.com/jbosse3/excel2moodle/-/commit/4f5fe550786cf29839ba54fbdfedbf03c72d3009)
|
116
|
+
|
93
117
|
## 0.7.0 (2025-09-30)
|
94
118
|
Rework of the equation checker done!
|
95
119
|
|
@@ -68,6 +68,30 @@ If You want to support my work as well, you can by me a [coffee](https://ko-fi.c
|
|
68
68
|
|
69
69
|
# Changelogs
|
70
70
|
|
71
|
+
## 0.7.2 (2025-10-11)
|
72
|
+
small important bugfixes
|
73
|
+
|
74
|
+
### improvement (2 changes)
|
75
|
+
|
76
|
+
- [BulletPoints are decoded using regex to allow multi word names](https://gitlab.com/jbosse3/excel2moodle/-/commit/7e32e9817323054d84a0dfb9a0a241c702fd096d)
|
77
|
+
- [Restructured globals, renamed rawInput to rawData](https://gitlab.com/jbosse3/excel2moodle/-/commit/effd9c3cd196b36d49204fe715acc1ffb124549c)
|
78
|
+
|
79
|
+
### bugfix (2 changes)
|
80
|
+
|
81
|
+
- [Added the number do the mandatory tags because it is](https://gitlab.com/jbosse3/excel2moodle/-/commit/56e9b69d71504dffe7c235d69ff44dec6931db28)
|
82
|
+
- [fixed assigning the first result to all clozes](https://gitlab.com/jbosse3/excel2moodle/-/commit/09a281c253502adc23442892be03aac36e6ea720)
|
83
|
+
|
84
|
+
## 0.7.1 (2025-10-04)
|
85
|
+
feedbacking improved
|
86
|
+
|
87
|
+
### documentation (1 change)
|
88
|
+
|
89
|
+
- [documentation improvement](https://gitlab.com/jbosse3/excel2moodle/-/commit/1a1110d05b49175e049a9ca18a027216a765e277)
|
90
|
+
|
91
|
+
### feature (1 change)
|
92
|
+
|
93
|
+
- [Added MC answer feedback support](https://gitlab.com/jbosse3/excel2moodle/-/commit/4f5fe550786cf29839ba54fbdfedbf03c72d3009)
|
94
|
+
|
71
95
|
## 0.7.0 (2025-09-30)
|
72
96
|
Rework of the equation checker done!
|
73
97
|
|
@@ -57,18 +57,33 @@ class BulletList:
|
|
57
57
|
|
58
58
|
def _setupBullets(self, bps: list[str]) -> ET.Element:
|
59
59
|
self.logger.debug("Formatting the bulletpoint list")
|
60
|
-
varFinder = re.compile(r"
|
60
|
+
varFinder = re.compile(r"\{(\w+)\}")
|
61
|
+
bulletFinder = re.compile(
|
62
|
+
r"^\s?(?P<desc>.*?)"
|
63
|
+
r"(?:\s+(?P<var>[\w+\{\\/\}^_-]+)\s*=\s*)"
|
64
|
+
r"(?P<val>[.,\{\w+\}]+)"
|
65
|
+
r"(?:\s+(?P<unit>[\w/\\^²³⁴⁵⁶]+)\s*$)"
|
66
|
+
)
|
61
67
|
for i, item in enumerate(bps):
|
62
|
-
|
63
|
-
name = sc_split[0]
|
64
|
-
var = sc_split[1]
|
65
|
-
quant = sc_split[3]
|
66
|
-
unit = sc_split[4]
|
67
|
-
|
68
|
-
match = re.search(varFinder, item)
|
68
|
+
match = re.search(bulletFinder, item)
|
69
69
|
if match is None:
|
70
|
+
self.logger.error("Couldn't find any bullets")
|
71
|
+
msg = f"Couldn't decode the bullet point: {item}"
|
72
|
+
raise ValueError(msg)
|
73
|
+
name = match.group("desc")
|
74
|
+
var = match.group("var")
|
75
|
+
unit = match.group("unit")
|
76
|
+
value = match.group("val")
|
77
|
+
self.logger.info(
|
78
|
+
"Decoded bulletPoint: name: %s, var: %s, - value: %s, - unit: %s.",
|
79
|
+
name,
|
80
|
+
var,
|
81
|
+
value,
|
82
|
+
unit,
|
83
|
+
)
|
84
|
+
if (match := re.search(varFinder, value)) is None:
|
70
85
|
self.logger.debug("Got a normal bulletItem")
|
71
|
-
num: float = float(
|
86
|
+
num: float = float(value.replace(",", "."))
|
72
87
|
bulletName = i + 1
|
73
88
|
else:
|
74
89
|
bulletName = match.group(1)
|
@@ -86,6 +101,7 @@ class BulletP:
|
|
86
101
|
self.var: str = var
|
87
102
|
self.unit: str = unit
|
88
103
|
self.element: ET.Element
|
104
|
+
self.value: float = value
|
89
105
|
self.update(value=value)
|
90
106
|
|
91
107
|
def update(self, value: float = 1) -> None:
|
@@ -0,0 +1,161 @@
|
|
1
|
+
import logging
|
2
|
+
from enum import Enum, StrEnum
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
import lxml.etree as ET
|
6
|
+
|
7
|
+
logger = logging.getLogger(__name__)
|
8
|
+
|
9
|
+
|
10
|
+
QUESTION_TYPES = {
|
11
|
+
"NF": "numerical",
|
12
|
+
"NFM": "numerical",
|
13
|
+
"MC": "multichoice",
|
14
|
+
"CLOZE": "cloze",
|
15
|
+
}
|
16
|
+
|
17
|
+
|
18
|
+
class Tags(StrEnum):
|
19
|
+
"""Tags and Settings Keys are needed to always acess the correct Value.
|
20
|
+
|
21
|
+
The Tags can be used to acess the settings or the QuestionData respectively.
|
22
|
+
As the QSettings settings are accesed via strings, which could easily gotten wrong.
|
23
|
+
Further, this Enum defines, which type a setting has to be.
|
24
|
+
"""
|
25
|
+
|
26
|
+
QUESTIONVARIANT = "defaultquestionvariant", int, 1, "testgen"
|
27
|
+
INCLUDEINCATS = "includecats", bool, False, "testgen"
|
28
|
+
GENEXPORTREPORT = "exportreport", bool, False, "testgen"
|
29
|
+
TOLERANCE = "tolerance", float, 0.01, "parser/nf"
|
30
|
+
PICTUREFOLDER = "pictureFolder", Path, None, "core"
|
31
|
+
PICTURESUBFOLDER = "imgfolder", str, "Abbildungen", "project"
|
32
|
+
SPREADSHEETPATH = "spreadsheetFolder", Path, None, "core"
|
33
|
+
LOGLEVEL = "loglevel", str, "INFO", "core"
|
34
|
+
LOGFILE = "logfile", str, "excel2moodleLogFile.log", "core"
|
35
|
+
CATEGORIESSHEET = "categoriessheet", str, "Kategorien", "core"
|
36
|
+
|
37
|
+
IMPORTMODULE = "importmodule", str, None
|
38
|
+
TEXT = "text", list, None
|
39
|
+
BPOINTS = "bulletpoint", list, None
|
40
|
+
TRUE = "true", list, None
|
41
|
+
FALSE = "false", list, None
|
42
|
+
TYPE = "type", str, None
|
43
|
+
NAME = "name", str, None
|
44
|
+
RESULT = "result", float, None
|
45
|
+
EQUATION = "formula", str, None
|
46
|
+
PICTURE = "picture", str, None
|
47
|
+
NUMBER = "number", int, None
|
48
|
+
ANSTYPE = "answertype", str, None
|
49
|
+
QUESTIONPART = "part", list, None
|
50
|
+
PARTTYPE = "parttype", str, None
|
51
|
+
POINTS = "points", float, 1.0
|
52
|
+
PICTUREWIDTH = "imgwidth", int, 500
|
53
|
+
ANSPICWIDTH = "answerimgwidth", int, 120
|
54
|
+
FIRSTRESULT = "firstresult", float, 0
|
55
|
+
WRONGSIGNPERCENT = "wrongsignpercent", int, 50
|
56
|
+
WRONGSIGNFB = "wrongsignfeedback", str, "your result has the wrong sign (+-)"
|
57
|
+
TRUEFB = "truefeedback", str, "congratulations!!! your answer is right."
|
58
|
+
FALSEFB = "falsefeedback", str, "Your answer is sadly wrong, try again!!!"
|
59
|
+
PCORRECFB = "partialcorrectfeedback", str, "Your answer is partially right."
|
60
|
+
GENERALFB = "feedback", str, "You answered this question."
|
61
|
+
TRUEANSFB = "trueanswerfeedback", list, None
|
62
|
+
FALSEANSFB = "falseanswerfeedback", list, None
|
63
|
+
|
64
|
+
MEDIASCRIPTS = "mediascripts", list, None
|
65
|
+
MEDIACALL = "parametricmedia", str, None
|
66
|
+
|
67
|
+
def __new__(
|
68
|
+
cls,
|
69
|
+
key: str,
|
70
|
+
typ: type,
|
71
|
+
default: str | float | Path | bool | None,
|
72
|
+
place: str = "project",
|
73
|
+
) -> object:
|
74
|
+
"""Define new settings class."""
|
75
|
+
obj = str.__new__(cls, key)
|
76
|
+
obj._value_ = key
|
77
|
+
obj._typ_ = typ
|
78
|
+
obj._default_ = default
|
79
|
+
obj._place_ = place
|
80
|
+
return obj
|
81
|
+
|
82
|
+
def __init__(
|
83
|
+
self,
|
84
|
+
_,
|
85
|
+
typ: type,
|
86
|
+
default: str | float | Path | None,
|
87
|
+
place: str = "project",
|
88
|
+
) -> None:
|
89
|
+
self._typ_: type = typ
|
90
|
+
self._place_: str = place
|
91
|
+
self._default_ = default
|
92
|
+
self._full_ = f"{self._place_}/{self._value_}"
|
93
|
+
|
94
|
+
@property
|
95
|
+
def default(self) -> str | int | float | Path | bool | None:
|
96
|
+
"""Get default value for the key."""
|
97
|
+
return self._default_
|
98
|
+
|
99
|
+
@property
|
100
|
+
def place(self) -> str:
|
101
|
+
return self._place_
|
102
|
+
|
103
|
+
@property
|
104
|
+
def full(self) -> str:
|
105
|
+
return self._full_
|
106
|
+
|
107
|
+
def typ(self) -> type:
|
108
|
+
"""Get type of the keys data."""
|
109
|
+
return self._typ_
|
110
|
+
|
111
|
+
|
112
|
+
class TextElements(Enum):
|
113
|
+
PLEFT = "p", "text-align: left;"
|
114
|
+
SPANRED = "span", "color: rgb(239, 69, 64)"
|
115
|
+
SPANGREEN = "span", "color: rgb(152, 202, 62)"
|
116
|
+
SPANORANGE = "span", "color: rgb(152, 100, 100)"
|
117
|
+
ULIST = "ul", ""
|
118
|
+
LISTITEM = "li", "text-align: left;"
|
119
|
+
DIV = "div", ""
|
120
|
+
|
121
|
+
def create(self, tag: str | None = None):
|
122
|
+
if tag is None:
|
123
|
+
tag, style = self.value
|
124
|
+
else:
|
125
|
+
style = self.value[1]
|
126
|
+
return ET.Element(tag, dir="ltr", style=style)
|
127
|
+
|
128
|
+
@property
|
129
|
+
def style(
|
130
|
+
self,
|
131
|
+
) -> str:
|
132
|
+
return self.value[1]
|
133
|
+
|
134
|
+
|
135
|
+
class XMLTags(StrEnum):
|
136
|
+
NAME = "name"
|
137
|
+
QTEXT = "questiontext"
|
138
|
+
QUESTION = "question"
|
139
|
+
TEXT = "text"
|
140
|
+
PICTURE = "file"
|
141
|
+
GENFEEDB = "generalfeedback"
|
142
|
+
CORFEEDB = "correctfeedback"
|
143
|
+
PCORFEEDB = "partialcorrectfeedback"
|
144
|
+
INCORFEEDB = "incorrectfeedback"
|
145
|
+
ANSFEEDBACK = "feedback"
|
146
|
+
POINTS = "defaultgrade"
|
147
|
+
PENALTY = "penalty"
|
148
|
+
HIDE = "hidden"
|
149
|
+
ID = "idnumber"
|
150
|
+
TYPE = "type"
|
151
|
+
ANSWER = "answer"
|
152
|
+
TOLERANCE = "tolerance"
|
153
|
+
|
154
|
+
|
155
|
+
feedBElements = {
|
156
|
+
XMLTags.CORFEEDB: TextElements.SPANGREEN.create(),
|
157
|
+
XMLTags.PCORFEEDB: TextElements.SPANORANGE.create(),
|
158
|
+
XMLTags.INCORFEEDB: TextElements.SPANRED.create(),
|
159
|
+
XMLTags.ANSFEEDBACK: TextElements.SPANGREEN.create(),
|
160
|
+
XMLTags.GENFEEDB: TextElements.SPANGREEN.create(),
|
161
|
+
}
|
@@ -34,18 +34,18 @@ class QuestionParser:
|
|
34
34
|
|
35
35
|
def setup(self, question: Question) -> None:
|
36
36
|
self.question: Question = question
|
37
|
-
self.
|
37
|
+
self.rawData = question.rawData
|
38
38
|
self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.question.id})
|
39
39
|
self.logger.debug(
|
40
40
|
"The following Data was provided: %s",
|
41
|
-
self.
|
41
|
+
self.rawData,
|
42
42
|
)
|
43
43
|
|
44
44
|
def hasPicture(self) -> bool:
|
45
45
|
"""Create a ``Picture`` object ``question``if the question needs a pic."""
|
46
46
|
if hasattr(self, "picture") and self.question.picture.ready:
|
47
47
|
return True
|
48
|
-
picKey = self.
|
48
|
+
picKey = self.rawData.get(Tags.PICTURE)
|
49
49
|
f = self.settings.get(Tags.PICTUREFOLDER)
|
50
50
|
svgFolder = (f / self.question.katName).resolve()
|
51
51
|
if not hasattr(self.question, "picture"):
|
@@ -53,7 +53,7 @@ class QuestionParser:
|
|
53
53
|
picKey,
|
54
54
|
svgFolder,
|
55
55
|
self.question.id,
|
56
|
-
width=self.
|
56
|
+
width=self.rawData.get(Tags.PICTUREWIDTH),
|
57
57
|
)
|
58
58
|
return bool(self.question.picture.ready)
|
59
59
|
|
@@ -63,7 +63,7 @@ class QuestionParser:
|
|
63
63
|
ET.SubElement(
|
64
64
|
ET.SubElement(textHTMLroot, "p"), "b"
|
65
65
|
).text = f"ID {self.question.id}"
|
66
|
-
text = self.
|
66
|
+
text = self.rawData[Tags.TEXT]
|
67
67
|
for t in text:
|
68
68
|
par = TextElements.PLEFT.create()
|
69
69
|
par.text = t
|
@@ -83,7 +83,7 @@ class QuestionParser:
|
|
83
83
|
It uses the data from ``self.rawInput`` if ``text`` is type``DFIndex``
|
84
84
|
Otherwise the value of ``text`` will be inserted.
|
85
85
|
"""
|
86
|
-
t = self.
|
86
|
+
t = self.rawData.get(text) if isinstance(text, Tags) else text
|
87
87
|
if txtEle is False:
|
88
88
|
self.tmpEle.append(eth.getElement(eleName, t, **attribs))
|
89
89
|
elif txtEle is True:
|
@@ -118,8 +118,8 @@ class QuestionParser:
|
|
118
118
|
|
119
119
|
self.htmlRoot = ET.Element("div")
|
120
120
|
self.htmlRoot.append(self.getMainTextElement())
|
121
|
-
if Tags.BPOINTS in self.
|
122
|
-
bps: list[str] = self.
|
121
|
+
if Tags.BPOINTS in self.rawData:
|
122
|
+
bps: list[str] = self.rawData[Tags.BPOINTS]
|
123
123
|
try:
|
124
124
|
bullets: BulletList = BulletList(bps, self.question.id)
|
125
125
|
except IndexError:
|
@@ -135,7 +135,7 @@ class QuestionParser:
|
|
135
135
|
if self.hasPicture():
|
136
136
|
self.htmlRoot.append(self.question.picture.htmlTag)
|
137
137
|
textRootElem.append(self.question.picture.element)
|
138
|
-
if Tags.MEDIACALL in self.
|
138
|
+
if Tags.MEDIACALL in self.rawData:
|
139
139
|
self.insertScriptedMedia()
|
140
140
|
ansList = self._parseAnswers()
|
141
141
|
if ansList is not None:
|
@@ -163,7 +163,7 @@ class QuestionParser:
|
|
163
163
|
span = feedBElements[feedback] if style is None else style.create()
|
164
164
|
if text is None:
|
165
165
|
self.logger.error("Giving a feedback without providing text is nonsens")
|
166
|
-
text = self.
|
166
|
+
text = self.rawData.get(Tags.GENERALFB)
|
167
167
|
ele = ET.Element(feedback, format="html")
|
168
168
|
par = TextElements.PLEFT.create()
|
169
169
|
span.text = text
|
@@ -173,13 +173,13 @@ class QuestionParser:
|
|
173
173
|
|
174
174
|
def insertScriptedMedia(self) -> None:
|
175
175
|
"""Load the scripts, insert the div and call a Function."""
|
176
|
-
for script in self.
|
176
|
+
for script in self.rawData.get(Tags.MEDIASCRIPTS):
|
177
177
|
ET.SubElement(
|
178
178
|
self.htmlRoot, "script", type="text/javascript", src=script
|
179
179
|
).text = ""
|
180
180
|
divId = f"scriptedMedia-{self.question.id}"
|
181
181
|
ET.SubElement(self.htmlRoot, "div", id=divId).text = ""
|
182
|
-
scriptCall = MediaCall(self.
|
182
|
+
scriptCall = MediaCall(self.rawData.get(Tags.MEDIACALL), divId=divId)
|
183
183
|
self.htmlRoot.append(scriptCall.element)
|
184
184
|
if isinstance(self.question, ParametricQuestion):
|
185
185
|
self.question.updateQue.append(scriptCall)
|
@@ -209,7 +209,7 @@ class QuestionParser:
|
|
209
209
|
format=format,
|
210
210
|
)
|
211
211
|
if feedback is None:
|
212
|
-
feedback = self.
|
212
|
+
feedback = self.rawData.get(Tags.TRUEFB)
|
213
213
|
ansEle.append(
|
214
214
|
self.getFeedBEle(
|
215
215
|
feedback=XMLTags.ANSFEEDBACK,
|
@@ -217,6 +217,6 @@ class QuestionParser:
|
|
217
217
|
style=TextElements.SPANGREEN,
|
218
218
|
),
|
219
219
|
)
|
220
|
-
absTolerance = round(result * self.
|
220
|
+
absTolerance = round(result * self.rawData.get(Tags.TOLERANCE), 4)
|
221
221
|
ansEle.append(eth.getElement(XMLTags.TOLERANCE, text=str(absTolerance)))
|
222
222
|
return ansEle
|
@@ -60,6 +60,8 @@ class QuestionData(dict):
|
|
60
60
|
Tags.QUESTIONPART,
|
61
61
|
Tags.TEXT,
|
62
62
|
Tags.MEDIASCRIPTS,
|
63
|
+
Tags.TRUEANSFB,
|
64
|
+
Tags.FALSEANSFB,
|
63
65
|
],
|
64
66
|
default: object = None,
|
65
67
|
) -> list: ...
|
@@ -118,6 +120,7 @@ class Question:
|
|
118
120
|
mandatoryTags: ClassVar[dict[Tags, type | UnionType]] = {
|
119
121
|
Tags.TEXT: str,
|
120
122
|
Tags.NAME: str,
|
123
|
+
Tags.NUMBER: int,
|
121
124
|
Tags.TYPE: str,
|
122
125
|
}
|
123
126
|
optionalTags: ClassVar[dict[Tags, type | UnionType]] = {
|
@@ -1,103 +1,12 @@
|
|
1
1
|
"""Settings module provides the adjusted subclass of ``PySide6.QtCore.QSettings``."""
|
2
2
|
|
3
3
|
import logging
|
4
|
-
from enum import StrEnum
|
5
4
|
from pathlib import Path
|
6
5
|
from typing import ClassVar, Literal, overload
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
class Tags(StrEnum):
|
12
|
-
"""Tags and Settings Keys are needed to always acess the correct Value.
|
13
|
-
|
14
|
-
The Tags can be used to acess the settings or the QuestionData respectively.
|
15
|
-
As the QSettings settings are accesed via strings, which could easily gotten wrong.
|
16
|
-
Further, this Enum defines, which type a setting has to be.
|
17
|
-
"""
|
18
|
-
|
19
|
-
def __new__(
|
20
|
-
cls,
|
21
|
-
key: str,
|
22
|
-
typ: type,
|
23
|
-
default: str | float | Path | bool | None,
|
24
|
-
place: str = "project",
|
25
|
-
) -> object:
|
26
|
-
"""Define new settings class."""
|
27
|
-
obj = str.__new__(cls, key)
|
28
|
-
obj._value_ = key
|
29
|
-
obj._typ_ = typ
|
30
|
-
obj._default_ = default
|
31
|
-
obj._place_ = place
|
32
|
-
return obj
|
33
|
-
|
34
|
-
def __init__(
|
35
|
-
self,
|
36
|
-
_,
|
37
|
-
typ: type,
|
38
|
-
default: str | float | Path | None,
|
39
|
-
place: str = "project",
|
40
|
-
) -> None:
|
41
|
-
self._typ_: type = typ
|
42
|
-
self._place_: str = place
|
43
|
-
self._default_ = default
|
44
|
-
self._full_ = f"{self._place_}/{self._value_}"
|
7
|
+
from excel2moodle.core.globals import Tags
|
45
8
|
|
46
|
-
|
47
|
-
def default(self) -> str | int | float | Path | bool | None:
|
48
|
-
"""Get default value for the key."""
|
49
|
-
return self._default_
|
50
|
-
|
51
|
-
@property
|
52
|
-
def place(self) -> str:
|
53
|
-
return self._place_
|
54
|
-
|
55
|
-
@property
|
56
|
-
def full(self) -> str:
|
57
|
-
return self._full_
|
58
|
-
|
59
|
-
def typ(self) -> type:
|
60
|
-
"""Get type of the keys data."""
|
61
|
-
return self._typ_
|
62
|
-
|
63
|
-
QUESTIONVARIANT = "defaultquestionvariant", int, 1, "testgen"
|
64
|
-
INCLUDEINCATS = "includecats", bool, False, "testgen"
|
65
|
-
GENEXPORTREPORT = "exportreport", bool, False, "testgen"
|
66
|
-
TOLERANCE = "tolerance", float, 0.01, "parser/nf"
|
67
|
-
PICTUREFOLDER = "pictureFolder", Path, None, "core"
|
68
|
-
PICTURESUBFOLDER = "imgfolder", str, "Abbildungen", "project"
|
69
|
-
SPREADSHEETPATH = "spreadsheetFolder", Path, None, "core"
|
70
|
-
LOGLEVEL = "loglevel", str, "INFO", "core"
|
71
|
-
LOGFILE = "logfile", str, "excel2moodleLogFile.log", "core"
|
72
|
-
CATEGORIESSHEET = "categoriessheet", str, "Kategorien", "core"
|
73
|
-
|
74
|
-
IMPORTMODULE = "importmodule", str, None
|
75
|
-
TEXT = "text", list, None
|
76
|
-
BPOINTS = "bulletpoint", list, None
|
77
|
-
TRUE = "true", list, None
|
78
|
-
FALSE = "false", list, None
|
79
|
-
TYPE = "type", str, None
|
80
|
-
NAME = "name", str, None
|
81
|
-
RESULT = "result", float, None
|
82
|
-
EQUATION = "formula", str, None
|
83
|
-
PICTURE = "picture", str, None
|
84
|
-
NUMBER = "number", int, None
|
85
|
-
ANSTYPE = "answertype", str, None
|
86
|
-
QUESTIONPART = "part", list, None
|
87
|
-
PARTTYPE = "parttype", str, None
|
88
|
-
POINTS = "points", float, 1.0
|
89
|
-
PICTUREWIDTH = "imgwidth", int, 500
|
90
|
-
ANSPICWIDTH = "answerimgwidth", int, 120
|
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."
|
98
|
-
|
99
|
-
MEDIASCRIPTS = "mediascripts", list, None
|
100
|
-
MEDIACALL = "parametricmedia", str, None
|
9
|
+
logger = logging.getLogger(__name__)
|
101
10
|
|
102
11
|
|
103
12
|
class Settings:
|
@@ -1,4 +1,4 @@
|
|
1
|
-
"""Implementation of
|
1
|
+
"""Implementation of the cloze question type.
|
2
2
|
|
3
3
|
This question type is like the NFM but supports multiple fields of answers.
|
4
4
|
All Answers are calculated off an equation using the same variables.
|
@@ -68,7 +68,7 @@ class ClozePart:
|
|
68
68
|
self.logger.debug("MC Answer Part already up to date.")
|
69
69
|
return
|
70
70
|
if self.typ == "NFM":
|
71
|
-
result = self.result.getResult(variant)
|
71
|
+
result = self.result.getResult(number=variant, equation=self.num)
|
72
72
|
self._element.text = self.getNumericAnsStr(
|
73
73
|
self.question.rawData,
|
74
74
|
result,
|
@@ -263,11 +263,11 @@ class ClozeQuestionParser(NFMQuestionParser):
|
|
263
263
|
|
264
264
|
def _setupParts(self) -> None:
|
265
265
|
parts: dict[int, ClozePart] = {}
|
266
|
-
for key in self.
|
266
|
+
for key in self.rawData:
|
267
267
|
if key.startswith(Tags.QUESTIONPART):
|
268
268
|
partNumber = self.getPartNumber(key)
|
269
269
|
parts[partNumber] = ClozePart(
|
270
|
-
self.question, self.
|
270
|
+
self.question, self.rawData[key], partNumber
|
271
271
|
)
|
272
272
|
partsNum = len(parts)
|
273
273
|
equations: dict[int, str] = self._getPartValues(Tags.RESULT)
|
@@ -310,7 +310,7 @@ class ClozeQuestionParser(NFMQuestionParser):
|
|
310
310
|
msg = f"Unclear Parts are defined. Either define `true:{num}` and `false:{num}` or `result:{num}` "
|
311
311
|
raise QNotParsedException(msg, self.question.id)
|
312
312
|
if len(points) == 0:
|
313
|
-
pts = round(self.
|
313
|
+
pts = round(self.rawData.get(Tags.POINTS) / partsNum, 3)
|
314
314
|
point = self._roundClozePartPoints(pts)
|
315
315
|
for part in parts.values():
|
316
316
|
part.points = point
|
@@ -325,7 +325,7 @@ class ClozeQuestionParser(NFMQuestionParser):
|
|
325
325
|
def _roundClozePartPoints(self, points: float | None = None) -> int:
|
326
326
|
"""Get the integer points for the cloze part."""
|
327
327
|
if points is None:
|
328
|
-
points = self.
|
328
|
+
points = self.rawData.get(Tags.POINTS)
|
329
329
|
corrPoints: int = round(points)
|
330
330
|
if not math.isclose(corrPoints, points):
|
331
331
|
self.logger.warning(
|
@@ -347,8 +347,8 @@ class ClozeQuestionParser(NFMQuestionParser):
|
|
347
347
|
) -> dict[int, list[str]]: ...
|
348
348
|
def _getPartValues(self, Tag):
|
349
349
|
tagValues: dict = {
|
350
|
-
self.getPartNumber(key): self.
|
351
|
-
for key in self.
|
350
|
+
self.getPartNumber(key): self.rawData[key]
|
351
|
+
for key in self.rawData
|
352
352
|
if key.startswith(Tag)
|
353
353
|
}
|
354
354
|
return tagValues
|
@@ -363,7 +363,7 @@ class ClozeQuestionParser(NFMQuestionParser):
|
|
363
363
|
if part.typ == "NFM":
|
364
364
|
result = self.question.parametrics.getResult(1, partNum)
|
365
365
|
ansStr = ClozePart.getNumericAnsStr(
|
366
|
-
self.
|
366
|
+
self.rawData,
|
367
367
|
result=result,
|
368
368
|
points=part.points,
|
369
369
|
)
|
@@ -28,8 +28,8 @@ class MCQuestion(Question):
|
|
28
28
|
"showstandardinstruction": "0",
|
29
29
|
"shownumcorrect": "",
|
30
30
|
}
|
31
|
-
|
32
|
-
|
31
|
+
optionalTags: ClassVar[dict[Tags, type | UnionType]] = {}
|
32
|
+
mandatoryTags: ClassVar[dict[Tags, type | UnionType]] = {
|
33
33
|
Tags.TRUE: str,
|
34
34
|
Tags.FALSE: str,
|
35
35
|
Tags.ANSTYPE: str,
|
@@ -52,11 +52,21 @@ class MCQuestionParser(QuestionParser):
|
|
52
52
|
|
53
53
|
def getAnsElementsList(
|
54
54
|
self,
|
55
|
-
answerList: list,
|
55
|
+
answerList: list[str],
|
56
|
+
feedbackList: list[str],
|
56
57
|
fraction: float = 50,
|
57
58
|
format="html",
|
58
59
|
) -> list[ET.Element]:
|
60
|
+
"""Take the answer Strings and format them each into a xml-tree.
|
61
|
+
|
62
|
+
If a feedbackList is given it is used. Otherwise the standard feedbacks are used.
|
63
|
+
"""
|
59
64
|
elementList: list[ET.Element] = []
|
65
|
+
if feedbackList is None:
|
66
|
+
if fraction > 0:
|
67
|
+
feedbackList = [self.rawData.get(Tags.FALSEFB) for _ in answerList]
|
68
|
+
else:
|
69
|
+
feedbackList = [self.rawData.get(Tags.TRUEFB) for _ in answerList]
|
60
70
|
for i, ans in enumerate(answerList):
|
61
71
|
p = TextElements.PLEFT.create()
|
62
72
|
if self.answerType == "picture":
|
@@ -72,7 +82,7 @@ class MCQuestionParser(QuestionParser):
|
|
72
82
|
elementList[-1].append(
|
73
83
|
self.getFeedBEle(
|
74
84
|
XMLTags.ANSFEEDBACK,
|
75
|
-
text=
|
85
|
+
text=feedbackList[i],
|
76
86
|
style=TextElements.SPANRED,
|
77
87
|
),
|
78
88
|
)
|
@@ -82,7 +92,7 @@ class MCQuestionParser(QuestionParser):
|
|
82
92
|
elementList[-1].append(
|
83
93
|
self.getFeedBEle(
|
84
94
|
XMLTags.ANSFEEDBACK,
|
85
|
-
text=
|
95
|
+
text=feedbackList[i],
|
86
96
|
style=TextElements.SPANGREEN,
|
87
97
|
),
|
88
98
|
)
|
@@ -91,21 +101,21 @@ class MCQuestionParser(QuestionParser):
|
|
91
101
|
return elementList
|
92
102
|
|
93
103
|
def _parseAnswers(self) -> list[ET.Element]:
|
94
|
-
self.answerType = self.
|
104
|
+
self.answerType = self.rawData.get(Tags.ANSTYPE)
|
95
105
|
if self.answerType not in self.question.AnsStyles:
|
96
106
|
msg = f"The Answer style: {self.answerType} is not supported"
|
97
107
|
raise InvalidFieldException(msg, self.question.id, Tags.ANSTYPE)
|
98
108
|
if self.answerType == "picture":
|
99
109
|
f = self.settings.get(Tags.PICTUREFOLDER)
|
100
110
|
imgFolder = (f / self.question.katName).resolve()
|
101
|
-
width = self.
|
111
|
+
width = self.rawData.get(Tags.ANSPICWIDTH)
|
102
112
|
self.trueImgs: list[Picture] = [
|
103
113
|
Picture(t, imgFolder, self.question.id, width=width)
|
104
|
-
for t in self.
|
114
|
+
for t in self.rawData.get(Tags.TRUE)
|
105
115
|
]
|
106
116
|
self.falseImgs: list[Picture] = [
|
107
117
|
Picture(t, imgFolder, self.question.id, width=width)
|
108
|
-
for t in self.
|
118
|
+
for t in self.rawData.get(Tags.FALSE)
|
109
119
|
]
|
110
120
|
trueAnsList: list[str] = [pic.htmlTag for pic in self.trueImgs if pic.ready]
|
111
121
|
falseAList: list[str] = [pic.htmlTag for pic in self.falseImgs if pic.ready]
|
@@ -114,19 +124,47 @@ class MCQuestionParser(QuestionParser):
|
|
114
124
|
raise QNotParsedException(msg, self.question.id)
|
115
125
|
else:
|
116
126
|
trueAnsList: list[str] = stringHelpers.texWrapper(
|
117
|
-
self.
|
127
|
+
self.rawData.get(Tags.TRUE), style=self.answerType
|
118
128
|
)
|
119
129
|
self.logger.debug(f"got the following true answers \n {trueAnsList=}")
|
120
130
|
falseAList: list[str] = stringHelpers.texWrapper(
|
121
|
-
self.
|
131
|
+
self.rawData.get(Tags.FALSE), style=self.answerType
|
122
132
|
)
|
123
133
|
self.logger.debug(f"got the following false answers \n {falseAList=}")
|
134
|
+
trueFbs = self.rawData.get(Tags.TRUEANSFB)
|
135
|
+
falseFbs = self.rawData.get(Tags.FALSEANSFB)
|
136
|
+
if trueFbs is None:
|
137
|
+
trueFbs = [self.rawData.get(Tags.FALSEFB) for _ in trueAnsList]
|
138
|
+
if falseFbs is None:
|
139
|
+
falseFbs = [self.rawData.get(Tags.TRUEFB) for _ in falseAList]
|
140
|
+
if len(trueFbs) < len(trueAnsList):
|
141
|
+
self.logger.warning(
|
142
|
+
"There are less true-feedbacks than true-answers given. Using fallback feedback"
|
143
|
+
)
|
144
|
+
delta = len(trueAnsList) - len(trueFbs)
|
145
|
+
while delta > 0:
|
146
|
+
trueFbs.append(self.rawData.get(Tags.TRUEFB))
|
147
|
+
delta -= 1
|
148
|
+
if len(falseFbs) < len(falseAList):
|
149
|
+
self.logger.warning(
|
150
|
+
"There are less false-feedbacks than false-answers given. Using fallback feedback"
|
151
|
+
)
|
152
|
+
delta = len(falseAList) - len(falseFbs)
|
153
|
+
while delta > 0:
|
154
|
+
falseFbs.append(self.rawData.get(Tags.TRUEFB))
|
155
|
+
delta -= 1
|
124
156
|
truefrac = 1 / len(trueAnsList) * 100
|
125
157
|
falsefrac = 1 / len(falseAList) * (-100)
|
126
158
|
self.tmpEle.find(XMLTags.PENALTY).text = str(round(truefrac / 100, 4))
|
127
|
-
ansList = self.getAnsElementsList(
|
159
|
+
ansList = self.getAnsElementsList(
|
160
|
+
answerList=trueAnsList, feedbackList=trueFbs, fraction=round(truefrac, 4)
|
161
|
+
)
|
128
162
|
ansList.extend(
|
129
|
-
self.getAnsElementsList(
|
163
|
+
self.getAnsElementsList(
|
164
|
+
answerList=falseAList,
|
165
|
+
feedbackList=falseFbs,
|
166
|
+
fraction=round(falsefrac, 4),
|
167
|
+
),
|
130
168
|
)
|
131
169
|
return ansList
|
132
170
|
|
@@ -138,5 +176,5 @@ class MCQuestionParser(QuestionParser):
|
|
138
176
|
XMLTags.INCORFEEDB: Tags.FALSEFB,
|
139
177
|
}
|
140
178
|
for feedb, tag in feedBacks.items():
|
141
|
-
self.tmpEle.append(self.getFeedBEle(feedb, text=self.
|
179
|
+
self.tmpEle.append(self.getFeedBEle(feedb, text=self.rawData.get(tag)))
|
142
180
|
self._finalizeParsing()
|
@@ -17,10 +17,10 @@ class NFQuestion(Question):
|
|
17
17
|
def __init__(self, *args, **kwargs) -> None:
|
18
18
|
super().__init__(*args, **kwargs)
|
19
19
|
|
20
|
-
|
20
|
+
optionalTags: ClassVar[dict[Tags, type | UnionType]] = {
|
21
21
|
Tags.BPOINTS: str,
|
22
22
|
}
|
23
|
-
|
23
|
+
mandatoryTags: ClassVar[dict[Tags, type | UnionType]] = {
|
24
24
|
Tags.RESULT: float | int,
|
25
25
|
}
|
26
26
|
|
@@ -37,13 +37,13 @@ class NFQuestionParser(QuestionParser):
|
|
37
37
|
super().setup(question)
|
38
38
|
|
39
39
|
def _parseAnswers(self) -> list[ET.Element]:
|
40
|
-
result: float = self.
|
40
|
+
result: float = self.rawData.get(Tags.RESULT)
|
41
41
|
ansEle: list[ET.Element] = []
|
42
42
|
ansEle.append(self.getNumericAnsElement(result=result))
|
43
43
|
return ansEle
|
44
44
|
|
45
45
|
def _finalizeParsing(self) -> None:
|
46
46
|
self.tmpEle.append(
|
47
|
-
self.getFeedBEle(XMLTags.GENFEEDB, text=self.
|
47
|
+
self.getFeedBEle(XMLTags.GENFEEDB, text=self.rawData.get(Tags.GENERALFB))
|
48
48
|
)
|
49
49
|
return super()._finalizeParsing()
|
@@ -14,7 +14,7 @@ from excel2moodle.core.question import ParametricQuestion, Parametrics
|
|
14
14
|
|
15
15
|
|
16
16
|
class NFMQuestion(ParametricQuestion):
|
17
|
-
|
17
|
+
mandatoryTags: ClassVar[dict[Tags, type | UnionType]] = {
|
18
18
|
Tags.RESULT: str,
|
19
19
|
Tags.BPOINTS: str,
|
20
20
|
}
|
@@ -58,20 +58,20 @@ class NFMQuestionParser(QuestionParser):
|
|
58
58
|
def _parseAnswers(self) -> list[ET.Element]:
|
59
59
|
variables = self.question.bulletList.getVariablesDict(self.question)
|
60
60
|
self.question.parametrics = Parametrics(
|
61
|
-
self.
|
62
|
-
self.
|
61
|
+
self.rawData.get(Tags.EQUATION),
|
62
|
+
self.rawData.get(Tags.FIRSTRESULT),
|
63
63
|
self.question.id,
|
64
64
|
)
|
65
65
|
self.question.parametrics.variables = variables
|
66
66
|
self.question.answerElement = self.getNumericAnsElement()
|
67
67
|
self.question.answerElementWrongSign = self.getNumericAnsElement(
|
68
|
-
fraction=self.
|
69
|
-
feedback=self.
|
68
|
+
fraction=self.rawData.get(Tags.WRONGSIGNPERCENT),
|
69
|
+
feedback=self.rawData.get(Tags.WRONGSIGNFB),
|
70
70
|
)
|
71
71
|
return [self.question.answerElement, self.question.answerElementWrongSign]
|
72
72
|
|
73
73
|
def _finalizeParsing(self) -> None:
|
74
74
|
self.tmpEle.append(
|
75
|
-
self.getFeedBEle(XMLTags.GENFEEDB, text=self.
|
75
|
+
self.getFeedBEle(XMLTags.GENFEEDB, text=self.rawData.get(Tags.GENERALFB))
|
76
76
|
)
|
77
77
|
return super()._finalizeParsing()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: excel2moodle
|
3
|
-
Version: 0.7.
|
3
|
+
Version: 0.7.2
|
4
4
|
Summary: A package for converting questions from a spreadsheet, to valid moodle-xml
|
5
5
|
Author: Jakob Bosse
|
6
6
|
License-Expression: GPL-3.0-or-later
|
@@ -90,6 +90,30 @@ If You want to support my work as well, you can by me a [coffee](https://ko-fi.c
|
|
90
90
|
|
91
91
|
# Changelogs
|
92
92
|
|
93
|
+
## 0.7.2 (2025-10-11)
|
94
|
+
small important bugfixes
|
95
|
+
|
96
|
+
### improvement (2 changes)
|
97
|
+
|
98
|
+
- [BulletPoints are decoded using regex to allow multi word names](https://gitlab.com/jbosse3/excel2moodle/-/commit/7e32e9817323054d84a0dfb9a0a241c702fd096d)
|
99
|
+
- [Restructured globals, renamed rawInput to rawData](https://gitlab.com/jbosse3/excel2moodle/-/commit/effd9c3cd196b36d49204fe715acc1ffb124549c)
|
100
|
+
|
101
|
+
### bugfix (2 changes)
|
102
|
+
|
103
|
+
- [Added the number do the mandatory tags because it is](https://gitlab.com/jbosse3/excel2moodle/-/commit/56e9b69d71504dffe7c235d69ff44dec6931db28)
|
104
|
+
- [fixed assigning the first result to all clozes](https://gitlab.com/jbosse3/excel2moodle/-/commit/09a281c253502adc23442892be03aac36e6ea720)
|
105
|
+
|
106
|
+
## 0.7.1 (2025-10-04)
|
107
|
+
feedbacking improved
|
108
|
+
|
109
|
+
### documentation (1 change)
|
110
|
+
|
111
|
+
- [documentation improvement](https://gitlab.com/jbosse3/excel2moodle/-/commit/1a1110d05b49175e049a9ca18a027216a765e277)
|
112
|
+
|
113
|
+
### feature (1 change)
|
114
|
+
|
115
|
+
- [Added MC answer feedback support](https://gitlab.com/jbosse3/excel2moodle/-/commit/4f5fe550786cf29839ba54fbdfedbf03c72d3009)
|
116
|
+
|
93
117
|
## 0.7.0 (2025-09-30)
|
94
118
|
Rework of the equation checker done!
|
95
119
|
|
@@ -2,6 +2,7 @@ from pathlib import Path
|
|
2
2
|
|
3
3
|
import pytest
|
4
4
|
|
5
|
+
from excel2moodle.core.bullets import BulletList
|
5
6
|
from excel2moodle.core.dataStructure import QuestionDB
|
6
7
|
from excel2moodle.core.settings import Settings, Tags
|
7
8
|
|
@@ -43,3 +44,22 @@ def test_bulletVariants(variant, bulletName, bulletStr) -> None:
|
|
43
44
|
question.getUpdatedElement(variant=variant)
|
44
45
|
for bullet in question.bulletList.bullets[bulletName].element:
|
45
46
|
assert bullet == bulletStr
|
47
|
+
|
48
|
+
|
49
|
+
@pytest.mark.parametrize(
|
50
|
+
("bulletString", "name", "var", "value", "unit"),
|
51
|
+
[
|
52
|
+
(r"Große Kraft F = 25,0 kN", "Große Kraft", "F", 25.0, "kN"),
|
53
|
+
(r"Lange Kraft F_l = 22,0 kN", "Lange Kraft", "F_l", 22.0, "kN"),
|
54
|
+
(r"Streckenlast p = 15,0 kN/m", "Streckenlast", "p", 15, "kN/m"),
|
55
|
+
(r"Längste Strecke l_{max} = 15.0 km", "Längste Strecke", "l_{max}", 15, "km"),
|
56
|
+
(r"Strng Var q_{st} = 33,339 kN/m²", "Strng Var", "q_{st}", 33.339, "kN/m²"),
|
57
|
+
(r"Max Sp \sigma_{max} = 33 kN/m^2", "Max Sp", r"\sigma_{max}", 33, "kN/m^2"),
|
58
|
+
],
|
59
|
+
)
|
60
|
+
def test_paresBPointNames(bulletString, name, var, value, unit) -> None:
|
61
|
+
bList: BulletList = BulletList([bulletString], "0000")
|
62
|
+
assert bList.bullets.get(1).name == name
|
63
|
+
assert bList.bullets.get(1).value == value
|
64
|
+
assert bList.bullets.get(1).var == var
|
65
|
+
assert bList.bullets.get(1).unit == unit
|
@@ -1,91 +0,0 @@
|
|
1
|
-
from enum import Enum, StrEnum
|
2
|
-
|
3
|
-
import lxml.etree as ET
|
4
|
-
|
5
|
-
from excel2moodle.core.settings import Tags
|
6
|
-
|
7
|
-
QUESTION_TYPES = {
|
8
|
-
"NF": "numerical",
|
9
|
-
"NFM": "numerical",
|
10
|
-
"MC": "multichoice",
|
11
|
-
"CLOZE": "cloze",
|
12
|
-
}
|
13
|
-
|
14
|
-
|
15
|
-
class TextElements(Enum):
|
16
|
-
PLEFT = "p", "text-align: left;"
|
17
|
-
SPANRED = "span", "color: rgb(239, 69, 64)"
|
18
|
-
SPANGREEN = "span", "color: rgb(152, 202, 62)"
|
19
|
-
SPANORANGE = "span", "color: rgb(152, 100, 100)"
|
20
|
-
ULIST = "ul", ""
|
21
|
-
LISTITEM = "li", "text-align: left;"
|
22
|
-
DIV = "div", ""
|
23
|
-
|
24
|
-
def create(self, tag: str | None = None):
|
25
|
-
if tag is None:
|
26
|
-
tag, style = self.value
|
27
|
-
else:
|
28
|
-
style = self.value[1]
|
29
|
-
return ET.Element(tag, dir="ltr", style=style)
|
30
|
-
|
31
|
-
@property
|
32
|
-
def style(
|
33
|
-
self,
|
34
|
-
) -> str:
|
35
|
-
return self.value[1]
|
36
|
-
|
37
|
-
|
38
|
-
class XMLTags(StrEnum):
|
39
|
-
def __new__(cls, value: str, dfkey: Tags | None = None):
|
40
|
-
obj = str.__new__(cls, value)
|
41
|
-
obj._value_ = value
|
42
|
-
if dfkey is not None:
|
43
|
-
obj._dfkey_ = dfkey
|
44
|
-
return obj
|
45
|
-
|
46
|
-
def __init__(self, _: str, dfkey: Tags | None = None, getEle=None) -> None:
|
47
|
-
if isinstance(dfkey, Tags):
|
48
|
-
self._dfkey_: str = dfkey
|
49
|
-
if getEle:
|
50
|
-
self._getEle_: object = getEle
|
51
|
-
|
52
|
-
@property
|
53
|
-
def dfkey(self) -> str:
|
54
|
-
return self._dfkey_
|
55
|
-
|
56
|
-
def set(self, getEle) -> None:
|
57
|
-
self._getEle_ = getEle
|
58
|
-
|
59
|
-
def __repr__(self) -> str:
|
60
|
-
msg = []
|
61
|
-
msg.append(f"XML Tag {self.value=}")
|
62
|
-
if hasattr(self, "_dfkey_"):
|
63
|
-
msg.append(f"Df Key {self.dfkey=}")
|
64
|
-
return "\n".join(msg)
|
65
|
-
|
66
|
-
NAME = "name", Tags.NAME
|
67
|
-
QTEXT = "questiontext", Tags.TEXT
|
68
|
-
QUESTION = "question"
|
69
|
-
TEXT = "text"
|
70
|
-
PICTURE = "file", Tags.PICTURE
|
71
|
-
GENFEEDB = "generalfeedback"
|
72
|
-
CORFEEDB = "correctfeedback"
|
73
|
-
PCORFEEDB = "partialcorrectfeedback"
|
74
|
-
INCORFEEDB = "incorrectfeedback"
|
75
|
-
ANSFEEDBACK = "feedback"
|
76
|
-
POINTS = "defaultgrade"
|
77
|
-
PENALTY = "penalty"
|
78
|
-
HIDE = "hidden"
|
79
|
-
ID = "idnumber"
|
80
|
-
TYPE = "type"
|
81
|
-
ANSWER = "answer"
|
82
|
-
TOLERANCE = "tolerance"
|
83
|
-
|
84
|
-
|
85
|
-
feedBElements = {
|
86
|
-
XMLTags.CORFEEDB: TextElements.SPANGREEN.create(),
|
87
|
-
XMLTags.PCORFEEDB: TextElements.SPANORANGE.create(),
|
88
|
-
XMLTags.INCORFEEDB: TextElements.SPANRED.create(),
|
89
|
-
XMLTags.ANSFEEDBACK: TextElements.SPANGREEN.create(),
|
90
|
-
XMLTags.GENFEEDB: TextElements.SPANGREEN.create(),
|
91
|
-
}
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|