excel2moodle 0.4.4__py3-none-any.whl → 0.5.1__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 +14 -6
- excel2moodle/core/dataStructure.py +46 -39
- excel2moodle/core/parser.py +6 -1
- excel2moodle/core/question.py +44 -20
- excel2moodle/core/settings.py +6 -5
- excel2moodle/logger.py +16 -1
- excel2moodle/question_types/cloze.py +3 -3
- excel2moodle/question_types/mc.py +3 -3
- excel2moodle/question_types/nfm.py +1 -1
- excel2moodle/ui/appUi.py +1 -1
- excel2moodle/ui/dialogs.py +33 -18
- excel2moodle-0.5.1.dist-info/METADATA +106 -0
- {excel2moodle-0.4.4.dist-info → excel2moodle-0.5.1.dist-info}/RECORD +17 -17
- excel2moodle-0.4.4.dist-info/METADATA +0 -63
- {excel2moodle-0.4.4.dist-info → excel2moodle-0.5.1.dist-info}/WHEEL +0 -0
- {excel2moodle-0.4.4.dist-info → excel2moodle-0.5.1.dist-info}/entry_points.txt +0 -0
- {excel2moodle-0.4.4.dist-info → excel2moodle-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.4.4.dist-info → excel2moodle-0.5.1.dist-info}/top_level.txt +0 -0
excel2moodle/core/category.py
CHANGED
@@ -5,6 +5,8 @@ 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
|
+
|
8
10
|
if TYPE_CHECKING:
|
9
11
|
from excel2moodle.core.question import Question
|
10
12
|
|
@@ -19,17 +21,15 @@ class Category:
|
|
19
21
|
name: str,
|
20
22
|
description: str,
|
21
23
|
dataframe: pd.DataFrame,
|
22
|
-
|
23
|
-
version: int = 0,
|
24
|
+
settings: dict[str, float | str],
|
24
25
|
) -> None:
|
25
26
|
"""Instantiate a new Category object."""
|
26
27
|
self.NAME = name
|
27
|
-
match = re.search(r"\d+$", self.NAME)
|
28
|
+
match = re.search(r"\d+$", str(self.NAME))
|
28
29
|
self.n: int = int(match.group(0)) if match else 99
|
29
30
|
self.desc = str(description)
|
30
31
|
self.dataframe: pd.DataFrame = dataframe
|
31
|
-
self.
|
32
|
-
self.version = int(version)
|
32
|
+
self.settings: dict[str, float | str] = settings if settings else {}
|
33
33
|
self.questions: dict[int, Question] = {}
|
34
34
|
self.maxVariants: int | None = None
|
35
35
|
loggerObj.info("initializing Category %s", self.NAME)
|
@@ -38,9 +38,17 @@ class Category:
|
|
38
38
|
def name(self) -> str:
|
39
39
|
return self.NAME
|
40
40
|
|
41
|
+
@property
|
42
|
+
def points(self) -> float:
|
43
|
+
return self.settings.get(Tags.POINTS)
|
44
|
+
|
45
|
+
@points.setter
|
46
|
+
def points(self, points: float) -> None:
|
47
|
+
self.settings[Tags.POINTS] = points
|
48
|
+
|
41
49
|
@property
|
42
50
|
def id(self) -> str:
|
43
|
-
return f"{self.
|
51
|
+
return f"{self.n:02d}"
|
44
52
|
|
45
53
|
def __hash__(self) -> int:
|
46
54
|
return hash(self.NAME)
|
@@ -105,7 +105,18 @@ class QuestionDB:
|
|
105
105
|
engine="calamine",
|
106
106
|
)
|
107
107
|
logger.debug("Found the settings: \n\t%s", settingDf)
|
108
|
-
self.
|
108
|
+
settingDf = self.harmonizeDFIndex(settingDf)
|
109
|
+
for tag, value in settingDf.iterrows():
|
110
|
+
val = value.iloc[0]
|
111
|
+
if pd.notna(val):
|
112
|
+
self.settings.set(tag, val)
|
113
|
+
try:
|
114
|
+
self._validateProjectSettings(sheetPath=sheetPath)
|
115
|
+
except InvalidFieldException:
|
116
|
+
logger.exception(
|
117
|
+
"Can not create the database with invalid project settings."
|
118
|
+
)
|
119
|
+
raise
|
109
120
|
with Path(sheetPath).open("rb") as f:
|
110
121
|
self.categoriesMetaData = pd.read_excel(
|
111
122
|
f,
|
@@ -113,35 +124,33 @@ class QuestionDB:
|
|
113
124
|
index_col=0,
|
114
125
|
engine="calamine",
|
115
126
|
)
|
116
|
-
logger.info(
|
117
|
-
"Sucessfully read categoriesMetaData \n %s", self.categoriesMetaData
|
118
|
-
)
|
127
|
+
logger.info("Sucessfully read categoriesMetaData")
|
119
128
|
return self.categoriesMetaData
|
120
129
|
|
121
|
-
def
|
122
|
-
|
123
|
-
) -> None:
|
124
|
-
settings = self.harmonizeDFIndex(settings)
|
125
|
-
for tag, value in settings.iterrows():
|
126
|
-
value = value.iloc[0]
|
127
|
-
if tag == Tags.TOLERANCE:
|
128
|
-
tol = value if value < 1 else value / 100
|
129
|
-
if tol > 0 and tol < 1:
|
130
|
-
self.settings.set(Tags.TOLERANCE, tol)
|
131
|
-
else:
|
132
|
-
self.settings.set(tag, value)
|
133
|
-
if Tags.IMPORTMODULE in settings.index:
|
130
|
+
def _validateProjectSettings(self, sheetPath: Path) -> None:
|
131
|
+
if Tags.IMPORTMODULE in self.settings:
|
134
132
|
logger.warning(
|
135
133
|
"Appending: %s to sys.path. All names defined by it will be usable",
|
136
|
-
|
134
|
+
sheetPath,
|
137
135
|
)
|
138
|
-
sys.path.append(str(
|
136
|
+
sys.path.append(str(sheetPath.parent))
|
137
|
+
if Tags.PICTURESUBFOLDER not in self.settings:
|
138
|
+
logger.warning("You didn't specify an image Folder. This may cause errors.")
|
139
139
|
imgFolder = self.settings.get(Tags.SPREADSHEETPATH).parent / self.settings.get(
|
140
140
|
Tags.PICTURESUBFOLDER
|
141
141
|
)
|
142
|
-
|
143
|
-
|
144
|
-
|
142
|
+
catSheet = self.settings.get(Tags.CATEGORIESSHEET)
|
143
|
+
if catSheet not in pd.ExcelFile(sheetPath, engine="calamine").sheet_names:
|
144
|
+
msg = f"The specified categories sheet: '{catSheet}' doesn't exist."
|
145
|
+
raise InvalidFieldException(msg, "00000", Tags.CATEGORIESSHEET)
|
146
|
+
try:
|
147
|
+
imgFolder.resolve(strict=True)
|
148
|
+
except FileNotFoundError:
|
149
|
+
msg = f"Img Path: {imgFolder} could not be found"
|
150
|
+
raise InvalidFieldException(msg, "00000", Tags.PICTURESUBFOLDER)
|
151
|
+
else:
|
152
|
+
self.settings.set(Tags.PICTUREFOLDER, imgFolder.resolve())
|
153
|
+
logger.info("Set up the project settings")
|
145
154
|
|
146
155
|
def initAllCategories(self, sheetPath: Path | None = None) -> None:
|
147
156
|
"""Read all category sheets and initialize all Categories."""
|
@@ -228,29 +237,26 @@ class QuestionDB:
|
|
228
237
|
|
229
238
|
:emits: categoryReady(self) Signal.
|
230
239
|
"""
|
231
|
-
categoryDf = self.harmonizeDFIndex(categoryDf)
|
232
|
-
|
233
|
-
self.categoriesMetaData
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
if "version" in self.categoriesMetaData
|
241
|
-
and not pd.isna(self.categoriesMetaData["version"].loc[categoryName])
|
242
|
-
else self.settings.get(Tags.VERSION)
|
243
|
-
)
|
240
|
+
categoryDf: pd.DataFrame = self.harmonizeDFIndex(categoryDf)
|
241
|
+
categorySettings: dict[str, float | int | str] = self.harmonizeDFIndex(
|
242
|
+
self.categoriesMetaData.loc[categoryName]
|
243
|
+
).to_dict()
|
244
|
+
nanSettings: list[str] = [
|
245
|
+
k for k, v in categorySettings.items() if pd.isna(v) or k not in Tags
|
246
|
+
]
|
247
|
+
for na in nanSettings:
|
248
|
+
categorySettings.pop(na)
|
244
249
|
category = Category(
|
245
250
|
categoryName,
|
246
251
|
self.categoriesMetaData["description"].loc[categoryName],
|
247
252
|
dataframe=categoryDf,
|
248
|
-
|
249
|
-
version=version,
|
253
|
+
settings=categorySettings,
|
250
254
|
)
|
251
255
|
if hasattr(self, "categories"):
|
252
256
|
self.categories[categoryName] = category
|
253
257
|
self.signals.categoryReady.emit(category)
|
258
|
+
else:
|
259
|
+
logger.warning("Can't append initialized category to the database")
|
254
260
|
logger.debug("Category %s is initialized", categoryName)
|
255
261
|
return category
|
256
262
|
|
@@ -280,7 +286,7 @@ class QuestionDB:
|
|
280
286
|
self.signals.categoryQuestionsReady.emit(category)
|
281
287
|
|
282
288
|
@classmethod
|
283
|
-
def setupAndParseQuestion(cls, category: Category, qNumber: int) -> None:
|
289
|
+
def setupAndParseQuestion(cls, category: Category, qNumber: int) -> Question | None:
|
284
290
|
"""Check if the Question Data is valid. Then parse it.
|
285
291
|
|
286
292
|
The Question data is accessed from `category.dataframe` via its number
|
@@ -316,7 +322,7 @@ class QuestionDB:
|
|
316
322
|
question = QuestionTypeMapping[qtype].create(category, validData)
|
317
323
|
if question.isParsed:
|
318
324
|
locallogger.info("Question already parsed")
|
319
|
-
return
|
325
|
+
return None
|
320
326
|
if isinstance(question, NFQuestion):
|
321
327
|
cls.nfParser.setup(question)
|
322
328
|
locallogger.debug("setup a new NF parser ")
|
@@ -337,6 +343,7 @@ class QuestionDB:
|
|
337
343
|
msg = "couldn't setup Parser"
|
338
344
|
raise QNotParsedException(msg, question.id)
|
339
345
|
category.questions[qNumber] = question
|
346
|
+
return question
|
340
347
|
|
341
348
|
def appendQuestions(
|
342
349
|
self, questions: list[QuestionItem], file: Path | None = None
|
excel2moodle/core/parser.py
CHANGED
@@ -50,7 +50,12 @@ class QuestionParser:
|
|
50
50
|
f = self.settings.get(Tags.PICTUREFOLDER)
|
51
51
|
svgFolder = (f / self.question.katName).resolve()
|
52
52
|
if not hasattr(self.question, "picture"):
|
53
|
-
self.question.picture = Picture(
|
53
|
+
self.question.picture = Picture(
|
54
|
+
picKey,
|
55
|
+
svgFolder,
|
56
|
+
self.question.id,
|
57
|
+
width=self.rawInput.get(Tags.PICTUREWIDTH),
|
58
|
+
)
|
54
59
|
return bool(self.question.picture.ready)
|
55
60
|
|
56
61
|
def setMainText(self) -> None:
|
excel2moodle/core/question.py
CHANGED
@@ -25,6 +25,14 @@ settings = Settings()
|
|
25
25
|
|
26
26
|
|
27
27
|
class QuestionData(dict):
|
28
|
+
@property
|
29
|
+
def categoryFallbacks(self) -> dict[str, float | str]:
|
30
|
+
return self._categoryFallbacks
|
31
|
+
|
32
|
+
@categoryFallbacks.setter
|
33
|
+
def categoryFallbacks(self, fallbacks: dict) -> None:
|
34
|
+
self._categoryFallbacks: dict[str, float | str] = fallbacks
|
35
|
+
|
28
36
|
@overload
|
29
37
|
def get(
|
30
38
|
self,
|
@@ -39,7 +47,6 @@ class QuestionData(dict):
|
|
39
47
|
def get(
|
40
48
|
self,
|
41
49
|
key: Literal[
|
42
|
-
Tags.VERSION,
|
43
50
|
Tags.NUMBER,
|
44
51
|
Tags.PICTUREWIDTH,
|
45
52
|
Tags.ANSPICWIDTH,
|
@@ -58,14 +65,26 @@ class QuestionData(dict):
|
|
58
65
|
def get(self, key: Literal[Tags.RESULT]) -> float | str: ...
|
59
66
|
|
60
67
|
def get(self, key: Tags, default=None):
|
68
|
+
"""Get the value for `key` with correct type.
|
69
|
+
|
70
|
+
If `key == Tags.TOLERANCE` the tolerance is checked to be a perc. fraction
|
71
|
+
"""
|
61
72
|
if key in self:
|
62
73
|
val = self[key]
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
74
|
+
elif key in self.categoryFallbacks:
|
75
|
+
val = self.categoryFallbacks.get(key)
|
76
|
+
else:
|
77
|
+
val = settings.get(key)
|
78
|
+
try:
|
79
|
+
typed = key.typ()(val)
|
80
|
+
except (TypeError, ValueError):
|
81
|
+
return None
|
82
|
+
if key == Tags.TOLERANCE:
|
83
|
+
loggerObj.debug("Verifying Tolerance")
|
84
|
+
if typed <= 0 or typed >= 100:
|
85
|
+
typed = settings.get(Tags.TOLERANCE)
|
86
|
+
return typed if typed < 1 else typed / 100
|
87
|
+
return typed
|
69
88
|
|
70
89
|
|
71
90
|
class Question:
|
@@ -105,13 +124,12 @@ class Question:
|
|
105
124
|
category: Category,
|
106
125
|
rawData: QuestionData,
|
107
126
|
parent=None,
|
108
|
-
points: float = 0,
|
109
127
|
) -> None:
|
110
128
|
self.rawData: QuestionData = rawData
|
129
|
+
self.rawData.categoryFallbacks = category.settings
|
111
130
|
self.category = category
|
112
131
|
self.katName = self.category.name
|
113
132
|
self.moodleType = QUESTION_TYPES[self.qtype]
|
114
|
-
self.points = points if points != 0 else self.category.points
|
115
133
|
self.element: ET.Element = None
|
116
134
|
self.isParsed: bool = False
|
117
135
|
self.picture: Picture
|
@@ -122,6 +140,10 @@ class Question:
|
|
122
140
|
self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.id})
|
123
141
|
self.logger.debug("Sucess initializing")
|
124
142
|
|
143
|
+
@property
|
144
|
+
def points(self) -> float:
|
145
|
+
return self.rawData.get(Tags.POINTS)
|
146
|
+
|
125
147
|
@property
|
126
148
|
def name(self) -> str:
|
127
149
|
return self.rawData.get(Tags.NAME)
|
@@ -140,14 +162,14 @@ class Question:
|
|
140
162
|
"""Assemble the question to the valid xml Tree."""
|
141
163
|
mainText = self._getTextElement()
|
142
164
|
self.logger.debug("Starting assembly")
|
143
|
-
self.
|
144
|
-
textParts = self.
|
165
|
+
self._assembleAnswer(variant=variant)
|
166
|
+
textParts = self._assembleText(variant=variant)
|
145
167
|
if hasattr(self, "picture") and self.picture.ready:
|
146
168
|
mainText.append(self.picture.element)
|
147
169
|
self.logger.debug("Appended Picture element to text")
|
148
170
|
mainText.append(etHelpers.getCdatTxtElement(textParts))
|
149
171
|
|
150
|
-
def
|
172
|
+
def _assembleText(self, variant=0) -> list[ET.Element]:
|
151
173
|
"""Assemble the Question Text.
|
152
174
|
|
153
175
|
Intended for the cloze question, where the answers parts are part of the text.
|
@@ -180,7 +202,7 @@ class Question:
|
|
180
202
|
return self.bulletList
|
181
203
|
return None
|
182
204
|
|
183
|
-
def
|
205
|
+
def _assembleAnswer(self, variant: int = 0) -> None:
|
184
206
|
pass
|
185
207
|
|
186
208
|
def _setID(self, id=0) -> None:
|
@@ -207,7 +229,7 @@ class ParametricQuestion(Question):
|
|
207
229
|
def replaceMatch(match: Match[str]) -> str | int | float:
|
208
230
|
key = match.group(1)
|
209
231
|
if key in self.variables:
|
210
|
-
value = self.variables[key][variant]
|
232
|
+
value = self.variables[key][variant - 1]
|
211
233
|
return f"{value}".replace(".", ",\\!")
|
212
234
|
return match.group(0) # keep original if no match
|
213
235
|
|
@@ -242,6 +264,7 @@ class Picture:
|
|
242
264
|
|
243
265
|
def getImgId(self, imgKey: str) -> bool:
|
244
266
|
"""Get the image ID and width based on the given key.
|
267
|
+
|
245
268
|
The key should either be the full ID (as the question) or only the question Num.
|
246
269
|
If only the number is given, the category.id is prepended.
|
247
270
|
The width should be specified by `ID:width:XX`. where xx is the px value.
|
@@ -262,12 +285,13 @@ class Picture:
|
|
262
285
|
if imgKey in ("false", "nan", False) or len(num) == 0:
|
263
286
|
return False
|
264
287
|
imgID: int = int(num[0])
|
265
|
-
if imgID <
|
266
|
-
picID = f"{self.questionId[:
|
288
|
+
if imgID < 100:
|
289
|
+
picID = f"{self.questionId[:2]}{imgID:02d}"
|
267
290
|
elif imgID < 10000:
|
268
|
-
picID = f"{
|
269
|
-
|
270
|
-
|
291
|
+
picID = f"{imgID:04d}"
|
292
|
+
else:
|
293
|
+
msg = f"The imgKey {imgKey} is invalid, it should be a 4 digit question ID with an optional suffix"
|
294
|
+
raise QNotParsedException(msg, self.questionId)
|
271
295
|
if len(app) > 0 and app[0]:
|
272
296
|
self.picID = f"{picID}{app[0]}"
|
273
297
|
else:
|
@@ -280,7 +304,7 @@ class Picture:
|
|
280
304
|
return base64.b64encode(img.read()).decode("utf-8")
|
281
305
|
|
282
306
|
def _getImg(self) -> bool:
|
283
|
-
suffixes = ["png", "svg", "jpeg", "jpg"]
|
307
|
+
suffixes = ["png", "svg", "jpeg", "jpg", "JPG", "jxl"]
|
284
308
|
paths = [
|
285
309
|
path
|
286
310
|
for suf in suffixes
|
excel2moodle/core/settings.py
CHANGED
@@ -22,7 +22,7 @@ class Tags(StrEnum):
|
|
22
22
|
typ: type,
|
23
23
|
default: str | float | Path | bool | None,
|
24
24
|
place: str = "project",
|
25
|
-
):
|
25
|
+
) -> object:
|
26
26
|
"""Define new settings class."""
|
27
27
|
obj = str.__new__(cls, key)
|
28
28
|
obj._value_ = key
|
@@ -84,17 +84,19 @@ class Tags(StrEnum):
|
|
84
84
|
ANSTYPE = "answertype", str, None
|
85
85
|
QUESTIONPART = "part", list, None
|
86
86
|
PARTTYPE = "parttype", str, None
|
87
|
-
VERSION = "version", int, 1
|
88
87
|
POINTS = "points", float, 1.0
|
89
88
|
PICTUREWIDTH = "imgwidth", int, 500
|
90
89
|
ANSPICWIDTH = "answerimgwidth", int, 120
|
91
|
-
WRONGSIGNPERCENT = "
|
90
|
+
WRONGSIGNPERCENT = "wrongsignpercent", int, 50
|
92
91
|
FIRSTRESULT = "firstresult", float, 0
|
93
92
|
|
94
93
|
|
95
94
|
class Settings:
|
96
95
|
values: ClassVar[dict[str, str | float | Path]] = {}
|
97
96
|
|
97
|
+
def __contains__(self, tag: Tags) -> bool:
|
98
|
+
return bool(tag in type(self).values)
|
99
|
+
|
98
100
|
@classmethod
|
99
101
|
def clear(cls) -> None:
|
100
102
|
cls.values.clear()
|
@@ -112,7 +114,6 @@ class Settings:
|
|
112
114
|
key: Literal[
|
113
115
|
Tags.QUESTIONVARIANT,
|
114
116
|
Tags.TOLERANCE,
|
115
|
-
Tags.VERSION,
|
116
117
|
Tags.PICTUREWIDTH,
|
117
118
|
Tags.ANSPICWIDTH,
|
118
119
|
Tags.WRONGSIGNPERCENT,
|
@@ -192,6 +193,6 @@ class Settings:
|
|
192
193
|
type(value),
|
193
194
|
)
|
194
195
|
return
|
195
|
-
logger.info("Saved %s = %s
|
196
|
+
logger.info("Saved %s = %s: %s", key, value, tag.typ().__name__)
|
196
197
|
else:
|
197
198
|
logger.warning("got invalid local Setting %s = %s", key, value)
|
excel2moodle/logger.py
CHANGED
@@ -88,11 +88,26 @@ class LogWindowHandler(logging.Handler):
|
|
88
88
|
"CRITICAL": "pink",
|
89
89
|
}
|
90
90
|
|
91
|
+
def handle(self, record) -> bool:
|
92
|
+
info = record.exc_info
|
93
|
+
if record.exc_info:
|
94
|
+
excType, excVal, excTraceB = record.exc_info
|
95
|
+
exc_info_msg = f"[{excType.__name__}]: <b>{excVal}</b>"
|
96
|
+
record.exc_text = exc_info_msg
|
97
|
+
record.exc_info = None
|
98
|
+
try:
|
99
|
+
super().handle(record)
|
100
|
+
finally:
|
101
|
+
record.exc_info = info
|
102
|
+
record.exc_text = None
|
103
|
+
return True
|
104
|
+
|
91
105
|
def emit(self, record: logging.LogRecord) -> None:
|
92
106
|
"""Emit the signal, with a new logging message."""
|
93
107
|
log_message = self.format(record)
|
108
|
+
msg = log_message.replace("\n", "<br> ")
|
94
109
|
color = self.logLevelColors.get(record.levelname, "black")
|
95
|
-
prettyMessage = f'<span style="color:{color};">{
|
110
|
+
prettyMessage = f'<span style="color:{color};">{msg}</span>'
|
96
111
|
self.emitter.signal.emit(prettyMessage)
|
97
112
|
|
98
113
|
|
@@ -30,7 +30,7 @@ class ClozeQuestion(ParametricQuestion):
|
|
30
30
|
self.questionTexts: dict[int, list[ET.Element]] = {}
|
31
31
|
self.partsNum: int = 0
|
32
32
|
|
33
|
-
def
|
33
|
+
def _assembleAnswer(self, variant: int = 1) -> None:
|
34
34
|
for part, ans in self.answerVariants.items():
|
35
35
|
result = ans[variant - 1]
|
36
36
|
if self.answerTypes.get(part, None) == "MC":
|
@@ -49,8 +49,8 @@ class ClozeQuestion(ParametricQuestion):
|
|
49
49
|
self.logger.debug("Appended Question Parts %s to main text", part)
|
50
50
|
self.questionTexts[part].append(ET.Element("hr"))
|
51
51
|
|
52
|
-
def
|
53
|
-
textParts = super().
|
52
|
+
def _assembleText(self, variant=0) -> list[ET.Element]:
|
53
|
+
textParts = super()._assembleText(variant=variant)
|
54
54
|
self.logger.debug("Appending QuestionParts to main text")
|
55
55
|
for paragraphs in self.questionTexts.values():
|
56
56
|
for par in paragraphs:
|
@@ -104,13 +104,13 @@ class MCQuestionParser(QuestionParser):
|
|
104
104
|
if self.answerType == "picture":
|
105
105
|
f = self.settings.get(Tags.PICTUREFOLDER)
|
106
106
|
imgFolder = (f / self.question.katName).resolve()
|
107
|
-
|
107
|
+
width = self.rawInput.get(Tags.ANSPICWIDTH)
|
108
108
|
self.trueImgs: list[Picture] = [
|
109
|
-
Picture(t, imgFolder, self.question.id, width=
|
109
|
+
Picture(t, imgFolder, self.question.id, width=width)
|
110
110
|
for t in self.rawInput.get(Tags.TRUE)
|
111
111
|
]
|
112
112
|
self.falseImgs: list[Picture] = [
|
113
|
-
Picture(t, imgFolder, self.question.id, width=
|
113
|
+
Picture(t, imgFolder, self.question.id, width=width)
|
114
114
|
for t in self.rawInput.get(Tags.FALSE)
|
115
115
|
]
|
116
116
|
trueAnsList: list[str] = [pic.htmlTag for pic in self.trueImgs if pic.ready]
|
@@ -30,7 +30,7 @@ class NFMQuestion(ParametricQuestion):
|
|
30
30
|
super().__init__(*args, **kwargs)
|
31
31
|
self.answerVariants: list[ET.Element]
|
32
32
|
|
33
|
-
def
|
33
|
+
def _assembleAnswer(self, variant: int = 1) -> None:
|
34
34
|
prevAnsElement = self.element.find(XMLTags.ANSWER)
|
35
35
|
if prevAnsElement is not None:
|
36
36
|
self.element.remove(prevAnsElement)
|
excel2moodle/ui/appUi.py
CHANGED
@@ -108,7 +108,6 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
108
108
|
If successful it prints out a list of all exported Questions
|
109
109
|
"""
|
110
110
|
self.ui.treeWidget.clear()
|
111
|
-
self.testDB.readCategoriesMetadata()
|
112
111
|
process = ParseAllThread(self.testDB, self)
|
113
112
|
self.threadPool.start(process)
|
114
113
|
|
@@ -259,6 +258,7 @@ class ParseAllThread(QRunnable):
|
|
259
258
|
|
260
259
|
@QtCore.Slot()
|
261
260
|
def run(self) -> None:
|
261
|
+
self.testDB.readCategoriesMetadata()
|
262
262
|
self.testDB.asyncInitAllCategories(self.mainApp.excelPath)
|
263
263
|
self.mainApp.setStatus("[OK] Tabellen wurde eingelesen")
|
264
264
|
self.testDB.parseAllQuestions()
|
excel2moodle/ui/dialogs.py
CHANGED
@@ -90,28 +90,39 @@ class QuestionPreview:
|
|
90
90
|
self.ui.graphicsView.setScene(self.picScene)
|
91
91
|
self.setText()
|
92
92
|
self.setAnswers()
|
93
|
-
if hasattr(self, "picItem"):
|
93
|
+
if hasattr(self, "picItem") and self.picItem.scene() == self.picScene:
|
94
|
+
logger.debug("removing Previous picture")
|
94
95
|
self.picScene.removeItem(self.picItem)
|
96
|
+
del self.picItem
|
95
97
|
self.setPicture()
|
96
98
|
|
97
99
|
def setPicture(self) -> None:
|
98
100
|
if hasattr(self.question, "picture") and self.question.picture.ready:
|
99
101
|
path = self.question.picture.path
|
100
102
|
if path.suffix == ".svg":
|
101
|
-
self.picItem = QGraphicsSvgItem(str(
|
103
|
+
self.picItem = QGraphicsSvgItem(str(path))
|
102
104
|
else:
|
103
|
-
pic = QtGui.QPixmap(
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
width, scaleHeight, QtGui.Qt.AspectRatioMode.KeepAspectRatio
|
110
|
-
)
|
111
|
-
)
|
105
|
+
pic = QtGui.QPixmap(str(path))
|
106
|
+
self.picItem = QtWidgets.QGraphicsPixmapItem(pic)
|
107
|
+
if pic.isNull():
|
108
|
+
logger.warning("Picture null")
|
109
|
+
scale = self._getImgFittingScale()
|
110
|
+
self.picItem.setScale(scale)
|
112
111
|
self.picScene.addItem(self.picItem)
|
113
|
-
|
114
|
-
|
112
|
+
|
113
|
+
def _getImgFittingScale(self) -> float:
|
114
|
+
view_size = self.ui.graphicsView.viewport().size()
|
115
|
+
view_width = view_size.width()
|
116
|
+
view_height = view_size.height()
|
117
|
+
if isinstance(self.picItem, QtWidgets.QGraphicsPixmapItem):
|
118
|
+
original_size = self.picItem.pixmap().size()
|
119
|
+
elif isinstance(self.picItem, QGraphicsSvgItem):
|
120
|
+
original_size = self.picItem.renderer().defaultSize()
|
121
|
+
else:
|
122
|
+
return 1 # Unknown item type
|
123
|
+
scale_x = view_width / original_size.width()
|
124
|
+
scale_y = view_height / original_size.height()
|
125
|
+
return min(scale_x, scale_y)
|
115
126
|
|
116
127
|
def setText(self) -> None:
|
117
128
|
t = []
|
@@ -153,24 +164,28 @@ class AboutDialog(QtWidgets.QMessageBox):
|
|
153
164
|
self.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Close)
|
154
165
|
|
155
166
|
self.aboutMessage: str = f"""
|
156
|
-
<h1> About {e2mMetadata["name"]} v
|
167
|
+
<h1> About {e2mMetadata["name"]} v{e2mMetadata["version"]}</h1><br>
|
157
168
|
<p style="text-align:center">
|
158
169
|
|
159
170
|
<b><a href="{e2mMetadata["homepage"]}">{e2mMetadata["name"]}</a> - {e2mMetadata["description"]}</b>
|
160
171
|
</p>
|
161
172
|
<p style="text-align:center">
|
162
|
-
|
163
|
-
|
164
|
-
|
173
|
+
If you need help you can find some <a href="https://gitlab.com/jbosse3/excel2moodle/-/example/"> examples.</a>
|
174
|
+
</br>
|
175
|
+
A Documentation can be viewed by clicking "F1",
|
176
|
+
or onto the documentation button.
|
165
177
|
</br>
|
166
178
|
</p>
|
167
179
|
<p style="text-align:center">
|
180
|
+
To see whats new in version {e2mMetadata["version"]} see the <a href="https://gitlab.com/jbosse3/excel2moodle#changelogs"> changelogs.</a>
|
181
|
+
</p>
|
182
|
+
<p style="text-align:center">
|
168
183
|
This project is maintained by {e2mMetadata["author"]}.
|
169
184
|
<br>
|
170
185
|
Development takes place at <a href="{e2mMetadata["homepage"]}"> GitLab: {e2mMetadata["homepage"]}</a>
|
171
186
|
contributions are very welcome
|
172
187
|
</br>
|
173
|
-
If you encounter any issues please report them under the repositories issues page
|
188
|
+
If you encounter any issues please report them under the <a href="https://gitlab.com/jbosse3/excel2moodle/-/issues/"> repositories issues page </a>.
|
174
189
|
</br>
|
175
190
|
</p>
|
176
191
|
<p style="text-align:center">
|
@@ -0,0 +1,106 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: excel2moodle
|
3
|
+
Version: 0.5.1
|
4
|
+
Summary: A package for converting questions from a spreadsheet, to valid moodle-xml
|
5
|
+
Author: Jakob Bosse
|
6
|
+
License-Expression: GPL-3.0-or-later
|
7
|
+
Project-URL: Repository, https://gitlab.com/jbosse3/excel2moodle.git
|
8
|
+
Project-URL: Documentation, https://jbosse3.gitlab.io/excel2moodle
|
9
|
+
Keywords: moodle,XML,teaching,question,converter,open educational Ressource
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: Operating System :: OS Independent
|
12
|
+
Requires-Python: >=3.10
|
13
|
+
Description-Content-Type: text/markdown
|
14
|
+
License-File: LICENSE
|
15
|
+
Requires-Dist: pyside6>=6.8.0
|
16
|
+
Requires-Dist: pandas>=2.1.3
|
17
|
+
Requires-Dist: lxml>=5.4.0
|
18
|
+
Requires-Dist: asteval>=1.0.6
|
19
|
+
Requires-Dist: python-calamine>=0.3.2
|
20
|
+
Dynamic: license-file
|
21
|
+
|
22
|
+
# excel 2 Moodle
|
23
|
+
{width=35%}
|
24
|
+
|
25
|
+
This Python program helps to create Moodle questions in less time.
|
26
|
+
The idea is to write the questions data into a spreadsheet file, from which the program generates the moodle compliant xml Files.
|
27
|
+
All questions or a selection of questions can be exported into one xml file to be imported into moodle.
|
28
|
+
|
29
|
+
## Concept
|
30
|
+
The concept is, to store the different questions into categories of similar types and difficulties of questions, for each of which, a separated sheet in the Spreadsheet document should be created.
|
31
|
+
|
32
|
+
A `settings` sheet contains global settings to be used for all questions and categories.
|
33
|
+
Another sheet stores metadata for the different categories of questions.
|
34
|
+
And each category lives inside a separate sheet inside the spreadsheet document.
|
35
|
+
|
36
|
+
## Getting Started
|
37
|
+
|
38
|
+
### Installation
|
39
|
+
To get started with excel2moodle first have a look at the [installation](https://jbosse3.gitlab.io/excel2moodle/howto.html#excel2moodle-unter-windows-installieren)
|
40
|
+
If you already have python and uv installed, it is as easy as running `uv tool install excel2moodle`.
|
41
|
+
|
42
|
+
### [ Documentation ](https://jbosse3.gitlab.io/excel2moodle/index.html)
|
43
|
+
Once excel2moodle is installed you can checkout the [example question sheet](https://gitlab.com/jbosse3/excel2moodle/-/tree/master/example?ref_type=heads)
|
44
|
+
in the repository.
|
45
|
+
|
46
|
+
Some steps are already documented as [ tutorials ](https://jbosse3.gitlab.io/excel2moodle/howto.html)
|
47
|
+
you can follow along.
|
48
|
+
|
49
|
+
And please have a look into the [**user Reference**](https://jbosse3.gitlab.io/excel2moodle/userReference.html)
|
50
|
+
of the documentation.
|
51
|
+
That part explains each part of defining a question.
|
52
|
+
|
53
|
+
|
54
|
+
## Functionality
|
55
|
+
* Equation Verification:
|
56
|
+
+ this tool helps you to validate the correct equation for the parametrized Questions.
|
57
|
+
* Question Preview:
|
58
|
+
+ This helps you when selecting the correct questions for the export.
|
59
|
+
* Export Options:
|
60
|
+
+ you can export the questions preserving the categories in moodle
|
61
|
+
|
62
|
+
### Question Types
|
63
|
+
* Generate multiple Choice Questions:
|
64
|
+
+ The answers can be pictures or normal text
|
65
|
+
* Generate Numeric Questions
|
66
|
+
* Generate parametrized numeric Questions
|
67
|
+
* Generate parametrized cloze Questions
|
68
|
+
|
69
|
+
|
70
|
+
{width=80%}
|
71
|
+
|
72
|
+
## Licensing and authorship
|
73
|
+
excel2moodle is lincensed under the latest [GNU GPL license](https://gitlab.com/jbosse3/excel2moodle/-/blob/master/LICENSE)
|
74
|
+
Initial development was made by Richard Lorenz, and later taken over by Jakob Bosse
|
75
|
+
|
76
|
+
## Supporting
|
77
|
+
A special thanks goes to the [Civil Engineering Departement of the Fachhochschule Potsdam](https://www.fh-potsdam.de/en/study-further-education/departments/civil-engineering-department)
|
78
|
+
where i was employed as a student associate to work on this project.
|
79
|
+
|
80
|
+
If You want to support my work as well, you can by me a [coffee](https://ko-fi.com/jbosse3)
|
81
|
+
|
82
|
+
# Changelogs
|
83
|
+
|
84
|
+
## 0.5.1 (2025-06-24)
|
85
|
+
Minor docs improvement and question variant bugfix
|
86
|
+
|
87
|
+
### bugfix (1 change)
|
88
|
+
|
89
|
+
- [Bullet points variant didn't get updated](https://gitlab.com/jbosse3/excel2moodle/-/commit/7b4ad9e9c8a4216167ae019859ebaa8def81d57f)
|
90
|
+
|
91
|
+
## 0.5.0 (2025-06-20)
|
92
|
+
settings handling improved
|
93
|
+
|
94
|
+
### feature (2 changes)
|
95
|
+
|
96
|
+
- [Pixmaps and vector graphics scaled to fit in preview](https://gitlab.com/jbosse3/excel2moodle/-/commit/00a6ef13fb2a0046d7641e24af6cf6f08642390e)
|
97
|
+
- [feature: category Settings implemented](https://gitlab.com/jbosse3/excel2moodle/-/commit/d673cc3f5ba06051aa37bc17a3ef0161121cb730)
|
98
|
+
|
99
|
+
### improvement (1 change)
|
100
|
+
|
101
|
+
- [Tolerance is harmonized by questionData.get()](https://gitlab.com/jbosse3/excel2moodle/-/commit/8d1724f4877e1584cc531b6b3f278bdea68b5831)
|
102
|
+
|
103
|
+
### Settings Errors are logged (1 change)
|
104
|
+
|
105
|
+
- [Log Errors in settings Sheet](https://gitlab.com/jbosse3/excel2moodle/-/commit/07e58f957c69ea818db1c5679cf89e287817ced3)
|
106
|
+
|
@@ -1,38 +1,38 @@
|
|
1
1
|
excel2moodle/__init__.py,sha256=mnb-RWgmWIPSBk4S65a_jP6rxntAkTeYxN0ObUJalbQ,1801
|
2
2
|
excel2moodle/__main__.py,sha256=sG4ygwfVFskLQorBn-v98SvasNcPmwl_vLYpruT5Hk8,1175
|
3
|
-
excel2moodle/logger.py,sha256=
|
3
|
+
excel2moodle/logger.py,sha256=fq8ZOkCI1wj38v8IyrZsUlpt16onlSH_phqbVvYUwBQ,3725
|
4
4
|
excel2moodle/core/__init__.py,sha256=87BwhtZse72Tk17Ib-V9X2k9wkhmtVnEj2ZmJ9JBAnI,63
|
5
|
-
excel2moodle/core/category.py,sha256=
|
6
|
-
excel2moodle/core/dataStructure.py,sha256=
|
5
|
+
excel2moodle/core/category.py,sha256=wLzpbweQbzaItdbp2NCPI_Zmk94fy1EDOwEEN8zPvkU,2123
|
6
|
+
excel2moodle/core/dataStructure.py,sha256=sDdum2I0EZRuXTgbVSsLX4aBINfzkmNXBCHzUVBhxH8,16022
|
7
7
|
excel2moodle/core/etHelpers.py,sha256=G37qplp8tPJxqHNCBrf2Wo0jJZ0aDbxE9slQavqYqd8,2293
|
8
8
|
excel2moodle/core/exceptions.py,sha256=9xfsaIcm6Yej6QAZga0d3DK3jLQejdfgJARuAaG-uZY,739
|
9
9
|
excel2moodle/core/globals.py,sha256=Zm1wcrzQTRnhjrkwgBvo7VjKCFdPMjh-VLSSI5_QCO8,2837
|
10
10
|
excel2moodle/core/numericMultiQ.py,sha256=vr-gYogu2sf2a_Bhvhnu1ZSZFZXM32MfhJesjTkoOQM,2618
|
11
|
-
excel2moodle/core/parser.py,sha256=
|
12
|
-
excel2moodle/core/question.py,sha256=
|
13
|
-
excel2moodle/core/settings.py,sha256=
|
11
|
+
excel2moodle/core/parser.py,sha256=y0BXXt5j-4gRZO8otmEZ1Rmb0DW7hziesUoZ2kVpo9Y,8235
|
12
|
+
excel2moodle/core/question.py,sha256=vtrYq0J5D7PivkVo4eMUx_5l5jCyunmjThij38QopyY,11706
|
13
|
+
excel2moodle/core/settings.py,sha256=27D-P44rYk-DMrwI1dNpxHcznpFQf1W3XZrOc8e6rX4,5855
|
14
14
|
excel2moodle/core/stringHelpers.py,sha256=OzFZ6Eu3PeBLKb61K-aeVfUZmVuBerr9KfyOsuNRd7Y,2403
|
15
15
|
excel2moodle/core/validator.py,sha256=ssgkyUwrR-0AGPX1cUqvRwZsGja13J7HQ2W72ltqN-Y,4683
|
16
16
|
excel2moodle/extra/__init__.py,sha256=PM-id60HD21A3IcGC_fCYFihS8osBGZMIJCcN-ZRsIM,293
|
17
17
|
excel2moodle/extra/equationVerification.py,sha256=GLJl1r90d8AAiNy0H2hooZrg3D6aEwNfifYKAe3aGxM,3921
|
18
18
|
excel2moodle/question_types/__init__.py,sha256=81mss0g7SVtnlb-WkydE28G_dEAAf6oT1uB8lpK2-II,1041
|
19
|
-
excel2moodle/question_types/cloze.py,sha256=
|
20
|
-
excel2moodle/question_types/mc.py,sha256=
|
19
|
+
excel2moodle/question_types/cloze.py,sha256=N-0fDSWfpxYN0YZmPQPEaNHpxqW1OOC32WNseYmy6zM,7657
|
20
|
+
excel2moodle/question_types/mc.py,sha256=2kn6dPjFVg97H8SlUBFbcPjzDk84vgDGCMOtSABseu0,5225
|
21
21
|
excel2moodle/question_types/nf.py,sha256=bMP4IXrhnXmAI0NmjEc7DtX4xGaUbxzLicE2LjeaUho,1150
|
22
|
-
excel2moodle/question_types/nfm.py,sha256=
|
22
|
+
excel2moodle/question_types/nfm.py,sha256=03-aihk9HtzRCgpCGz_5WOR4JOqdJlkafyKprgLrosI,4948
|
23
23
|
excel2moodle/ui/UI_equationChecker.py,sha256=evQDlqCHeooJcAnYjhFCyjlPhfknr7ULGKQwMmqQeJ4,8947
|
24
24
|
excel2moodle/ui/UI_exportSettingsDialog.py,sha256=71xxXEqtewN0ReMfJ5t4gbrX_Bf0VEuxJ_DIV7ZtH94,6045
|
25
25
|
excel2moodle/ui/UI_mainWindow.py,sha256=asWUmKIYqufKUvRuCuA1JoMyv4qfRXyoR70F0331lww,19291
|
26
26
|
excel2moodle/ui/UI_variantDialog.py,sha256=snVaF3_YAc7NWjMRg7NzbjL_PzNbOpt4eiqElkE46io,5414
|
27
27
|
excel2moodle/ui/__init__.py,sha256=4EdGtpzwH3rgw4xW9E5x9kdPQYwKbo9rehHRZTNxCrQ,44
|
28
|
-
excel2moodle/ui/appUi.py,sha256=
|
29
|
-
excel2moodle/ui/dialogs.py,sha256=
|
28
|
+
excel2moodle/ui/appUi.py,sha256=88WODtEWqX1oQJebbPhlQChKM5N_9BH0ZuOpKVYrKm0,10863
|
29
|
+
excel2moodle/ui/dialogs.py,sha256=0h6aD4tguph1P07dorkn1A5B7_Z5SJZQ2_8xBYWK6MU,7689
|
30
30
|
excel2moodle/ui/equationChecker.py,sha256=ANpN7S0llkp6pGL1sKHII1Jc8YUvgDR458UnGVnZZOo,2702
|
31
31
|
excel2moodle/ui/treewidget.py,sha256=az64swVj1yQUsioeaZys32AauvQDdC4EKcqdbbWgL6s,2489
|
32
32
|
excel2moodle/ui/windowDoc.py,sha256=WvzHj6F4JvHP82WlTsyFeOXW024Xq3BUqtp--T4twuI,661
|
33
|
-
excel2moodle-0.
|
34
|
-
excel2moodle-0.
|
35
|
-
excel2moodle-0.
|
36
|
-
excel2moodle-0.
|
37
|
-
excel2moodle-0.
|
38
|
-
excel2moodle-0.
|
33
|
+
excel2moodle-0.5.1.dist-info/licenses/LICENSE,sha256=ywQqe6Sitymkf2lV2NRcx_aGsaC-KbSl_EfEsRXmNRw,35135
|
34
|
+
excel2moodle-0.5.1.dist-info/METADATA,sha256=H0kM9fFOx63yWYuKrq6ffdZglqWWCxdrxpKNdop3C0E,4614
|
35
|
+
excel2moodle-0.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
36
|
+
excel2moodle-0.5.1.dist-info/entry_points.txt,sha256=myfMLDThuGgWHMJDPPfILiZqo_7D3fhmDdJGqWOAjPw,60
|
37
|
+
excel2moodle-0.5.1.dist-info/top_level.txt,sha256=5V1xRUQ9o7UmOCmNoWCZPAuy5nXp3Qbzyqch8fUGT_c,13
|
38
|
+
excel2moodle-0.5.1.dist-info/RECORD,,
|
@@ -1,63 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: excel2moodle
|
3
|
-
Version: 0.4.4
|
4
|
-
Summary: A package for converting questions from a spreadsheet, to valid moodle-xml
|
5
|
-
Author: Jakob Bosse
|
6
|
-
License-Expression: GPL-3.0-or-later
|
7
|
-
Project-URL: Repository, https://gitlab.com/jbosse3/excel2moodle.git
|
8
|
-
Project-URL: Documentation, https://jbosse3.gitlab.io/excel2moodle
|
9
|
-
Keywords: moodle,XML,teaching,question,converter,open educational Ressource
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
11
|
-
Classifier: Operating System :: OS Independent
|
12
|
-
Requires-Python: >=3.10
|
13
|
-
Description-Content-Type: text/markdown
|
14
|
-
License-File: LICENSE
|
15
|
-
Requires-Dist: pyside6>=6.8.0
|
16
|
-
Requires-Dist: pandas>=2.1.3
|
17
|
-
Requires-Dist: lxml>=5.4.0
|
18
|
-
Requires-Dist: asteval>=1.0.6
|
19
|
-
Requires-Dist: python-calamine>=0.3.2
|
20
|
-
Dynamic: license-file
|
21
|
-
|
22
|
-
# excel 2 Moodle
|
23
|
-
[Deutsche README](https://gitlab.com/jbosse3/excel2moodle/-/blob/master/README.de.md)
|
24
|
-
|
25
|
-
{width=50%}
|
26
|
-
|
27
|
-
This Python program helps to create Moodle questions in less time.
|
28
|
-
The aim is to put alle the information for the questions into a spreadsheet file, and then parse it, to generate Moodle compliant XML-Files.
|
29
|
-
|
30
|
-
Furthermore this program lets you create a single XML-File with a selection of questions, that then can be imported to a Moodle-Test.
|
31
|
-
|
32
|
-
## Concept
|
33
|
-
The concept is, to store the different questions into categories of similar types and difficulties of questions, for each of which, a separated sheet in the Spreadsheet document should be created.
|
34
|
-
|
35
|
-
There Should be a sheet called "Kategorien", where an overview over the different categories is stored.
|
36
|
-
This sheet stores The names and descriptions, for all categories. The name have to be the same as the actual sheet names with the questions.
|
37
|
-
Furthermore the points used for grading, are set in the "Kategorien" sheet
|
38
|
-
|
39
|
-
|
40
|
-
## Development State
|
41
|
-
This program is still quite rough, with very litte robustness against faulty user input inside the Spreadsheet.
|
42
|
-
|
43
|
-
## Functionality
|
44
|
-
* Parse multiple Choice Questions, each into one XML file
|
45
|
-
* Parse Numeric Questions, each into one XML file
|
46
|
-
* create single XML File from a selection of questions
|
47
|
-
|
48
|
-
## Development Goals
|
49
|
-
* [X] creating an example spreadsheet
|
50
|
-
* [X] Export function, to create numerical Question version from a matrix of variables and corresponding correct Answers:
|
51
|
-
* similar to the calculated question Type, but with the benefit, of serving all students the same exact question
|
52
|
-
* [.] making it more robust:
|
53
|
-
* [X] Adding Error Messages when exporting
|
54
|
-
* [X] Creating logging
|
55
|
-
* [ ] Logging Errors to File
|
56
|
-
* [ ] making it Image File-Type agnostic
|
57
|
-
* [ ] Creating a Settings Menu
|
58
|
-
* [ ] Making keys in spreadsheet selectable in the Settings
|
59
|
-
* [ ] Setting image folder
|
60
|
-
|
61
|
-
## Licensing and authorship
|
62
|
-
excel2moodle is lincensed under the latest [GNU GPL license](https://gitlab.com/jbosse3/excel2moodle/-/blob/master/LICENSE)
|
63
|
-
Initial development was made by Richard Lorenz, and later taken over by Jakob Bosse
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|