excel2moodle 0.5.1__py3-none-any.whl → 0.5.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -131,7 +131,7 @@ class QuestionDB:
131
131
  if Tags.IMPORTMODULE in self.settings:
132
132
  logger.warning(
133
133
  "Appending: %s to sys.path. All names defined by it will be usable",
134
- sheetPath,
134
+ sheetPath.parent,
135
135
  )
136
136
  sys.path.append(str(sheetPath.parent))
137
137
  if Tags.PICTURESUBFOLDER not in self.settings:
@@ -286,7 +286,7 @@ class QuestionDB:
286
286
  self.signals.categoryQuestionsReady.emit(category)
287
287
 
288
288
  @classmethod
289
- def setupAndParseQuestion(cls, category: Category, qNumber: int) -> Question | None:
289
+ def setupAndParseQuestion(cls, category: Category, qNumber: int) -> Question:
290
290
  """Check if the Question Data is valid. Then parse it.
291
291
 
292
292
  The Question data is accessed from `category.dataframe` via its number
@@ -322,7 +322,7 @@ class QuestionDB:
322
322
  question = QuestionTypeMapping[qtype].create(category, validData)
323
323
  if question.isParsed:
324
324
  locallogger.info("Question already parsed")
325
- return None
325
+ return question
326
326
  if isinstance(question, NFQuestion):
327
327
  cls.nfParser.setup(question)
328
328
  locallogger.debug("setup a new NF parser ")
@@ -161,7 +161,7 @@ class Question:
161
161
  def assemble(self, variant=0) -> None:
162
162
  """Assemble the question to the valid xml Tree."""
163
163
  mainText = self._getTextElement()
164
- self.logger.debug("Starting assembly")
164
+ self.logger.info("Starting assembly variant: %s", variant)
165
165
  self._assembleAnswer(variant=variant)
166
166
  textParts = self._assembleText(variant=variant)
167
167
  if hasattr(self, "picture") and self.picture.ready:
@@ -1,11 +1,13 @@
1
- """Implementation of the cloze question type.
1
+ """Implementation of tde 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.
5
5
  """
6
6
 
7
+ import logging
7
8
  import math
8
9
  import re
10
+ from typing import Literal, overload
9
11
 
10
12
  import lxml.etree as ET
11
13
 
@@ -18,43 +20,143 @@ from excel2moodle.core.question import ParametricQuestion
18
20
  from excel2moodle.core.settings import Tags
19
21
  from excel2moodle.question_types.nfm import NFMQuestionParser
20
22
 
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class ClozePart:
27
+ def __init__(
28
+ self,
29
+ question: ParametricQuestion,
30
+ text: list[str],
31
+ ) -> None:
32
+ self.question = question
33
+ self.text: list[ET.Element] = self._setupText(text)
34
+ if not self.text:
35
+ msg = f"Answer part for cloze question {self.question.id} is invalid without partText"
36
+ raise ValueError(msg)
37
+
38
+ @property
39
+ def points(self) -> float:
40
+ if hasattr(self, "_points"):
41
+ return self._points
42
+ return 0.0
43
+ self.question.logger.error("Invalid call to points of unparsed cloze part")
44
+ return 0.0
45
+
46
+ @points.setter
47
+ def points(self, points: float) -> None:
48
+ self._points = points if points > 0 else 0.0
49
+
50
+ @property
51
+ def typ(self) -> Literal["MC", "NFM"] | None:
52
+ if hasattr(self, "_typ"):
53
+ return self._typ
54
+ return None
55
+
56
+ @property
57
+ def mcAnswerString(self) -> str:
58
+ if hasattr(self, "_mcAnswer"):
59
+ return self._mcAnswer
60
+ msg = "No MC Answer was set"
61
+ raise ValueError(msg)
62
+
63
+ @mcAnswerString.setter
64
+ def mcAnswerString(self, answerString: str) -> None:
65
+ self._mcAnswer: str = answerString
66
+
67
+ def _setupText(self, text: list[str]) -> ET.Element:
68
+ textList: list[ET.Element] = []
69
+ for t in text:
70
+ textList.append(TextElements.PLEFT.create())
71
+ textList[-1].text = t
72
+ return textList
73
+
74
+ def setAnswer(
75
+ self,
76
+ equation: str | None = None,
77
+ trueAns: list[str] | None = None,
78
+ falseAns: list[str] | None = None,
79
+ ) -> bool:
80
+ if falseAns is not None:
81
+ self.falseAnswers: list[str] = falseAns
82
+ if trueAns is not None:
83
+ self.trueAnswers: list[str] = trueAns
84
+ if equation is not None:
85
+ self.equation: str = equation
86
+ check = False
87
+ t = hasattr(self, "trueAnswers")
88
+ f = hasattr(self, "falseAnswers")
89
+ eq = hasattr(self, "equation")
90
+ if t and f and not eq:
91
+ self._typ: Literal["MC", "NFM"] = "MC"
92
+ return True
93
+ if eq and not t and not f:
94
+ self._typ: Literal["MC", "NFM"] = "NFM"
95
+ self.nfResults: list[float] = []
96
+ return True
97
+ return False
98
+
99
+ def __repr__(self) -> str:
100
+ answers: str = (
101
+ self.equation
102
+ if self.typ == "NFM"
103
+ else f"{self.trueAnswers}\n {self.falseAnswers}"
104
+ )
105
+ return f"Cloze Part {self.typ}\n Answers: '{answers}'"
106
+
21
107
 
22
108
  class ClozeQuestion(ParametricQuestion):
23
109
  """Cloze Question Type."""
24
110
 
25
111
  def __init__(self, *args, **kwargs) -> None:
26
112
  super().__init__(*args, **kwargs)
27
- self.answerVariants: dict[int, list[float]] = {}
28
- self.answerStrings: dict[int, list[str]] = {}
29
- self.answerTypes: dict[int, str] = {}
30
- self.questionTexts: dict[int, list[ET.Element]] = {}
31
- self.partsNum: int = 0
113
+ self.questionParts: dict[int, ClozePart] = {}
114
+ self.questionTexts: list[ET.Element] = []
115
+
116
+ @property
117
+ def partsNum(self) -> int:
118
+ return len(self.questionParts)
119
+
120
+ @property
121
+ def points(self) -> float:
122
+ pts: float = 0
123
+ if self.isParsed:
124
+ for p in self.questionParts.values():
125
+ pts = pts + p.points
126
+ else:
127
+ pts = self.rawData.get(Tags.POINTS)
128
+ return pts
32
129
 
33
130
  def _assembleAnswer(self, variant: int = 1) -> None:
34
- for part, ans in self.answerVariants.items():
35
- result = ans[variant - 1]
36
- if self.answerTypes.get(part, None) == "MC":
37
- ansStr = ClozeQuestionParser.getMCAnsStr(answers)
38
- else:
131
+ for partNum, part in self.questionParts.items():
132
+ if part.typ == "MC":
133
+ ansStr = part.mcAnswerString
134
+ self.logger.info("MC answer part: %s ", ansStr)
135
+ elif part.typ == "NFM":
136
+ result = part.nfResults[variant - 1]
39
137
  ansStr = ClozeQuestionParser.getNumericAnsStr(
40
138
  result,
41
139
  self.rawData.get(Tags.TOLERANCE),
42
140
  wrongSignCount=self.rawData.get(Tags.WRONGSIGNPERCENT),
141
+ points=part.points,
43
142
  )
143
+ self.logger.info("NF answer part: %s ", ansStr)
144
+ else:
145
+ msg = "Type of the answer part is invalid"
146
+ raise QNotParsedException(msg, self.id)
44
147
  ul = TextElements.ULIST.create()
45
148
  item = TextElements.LISTITEM.create()
46
149
  item.text = ansStr
47
150
  ul.append(item)
48
- self.questionTexts[part].append(ul)
49
- self.logger.debug("Appended Question Parts %s to main text", part)
50
- self.questionTexts[part].append(ET.Element("hr"))
151
+ part.text.append(ul)
152
+ self.logger.debug("Appended part %s %s to main text", partNum, part)
153
+ part.text.append(ET.Element("hr"))
154
+ self.questionTexts.extend(part.text)
51
155
 
52
156
  def _assembleText(self, variant=0) -> list[ET.Element]:
53
157
  textParts = super()._assembleText(variant=variant)
54
158
  self.logger.debug("Appending QuestionParts to main text")
55
- for paragraphs in self.questionTexts.values():
56
- for par in paragraphs:
57
- textParts.append(par)
159
+ textParts.extend(self.questionTexts)
58
160
  return textParts
59
161
 
60
162
 
@@ -70,67 +172,99 @@ class ClozeQuestionParser(NFMQuestionParser):
70
172
  super().setup(question)
71
173
 
72
174
  def _parseAnswers(self) -> None:
175
+ self._setupParts()
73
176
  self._parseAnswerParts()
74
- self._parseQuestionParts()
75
177
 
76
- def _parseAnswerParts(self) -> None:
77
- """Parse the numeric or MC result items."""
78
- self.question.answerTypes: dict[int, str] = {
79
- self.getPartNumber(key): self.rawInput[key]
178
+ def _setupParts(self) -> None:
179
+ parts: dict[int, ClozePart] = {
180
+ self.getPartNumber(key): ClozePart(self.question, self.rawInput[key])
80
181
  for key in self.rawInput
81
- if key.startswith(Tags.PARTTYPE)
182
+ if key.startswith(Tags.QUESTIONPART)
82
183
  }
83
- equations: dict[int, str] = {
184
+ partsNum = len(parts)
185
+ equations: dict[int, str] = self._getPartValues(Tags.RESULT)
186
+ trueAnsws: dict[int, list[str]] = self._getPartValues(Tags.TRUE)
187
+ falseAnsws: dict[int, list[str]] = self._getPartValues(Tags.FALSE)
188
+ points: dict[int, float] = self._getPartValues(Tags.POINTS)
189
+ for num, part in parts.items():
190
+ eq = equations.get(num)
191
+ true = trueAnsws.get(num)
192
+ false = falseAnsws.get(num)
193
+ part.setAnswer(equation=eq, trueAns=true, falseAns=false)
194
+ if len(points) == 0:
195
+ pts = round(self.rawInput.get(Tags.POINTS) / partsNum, 3)
196
+ for part in parts.values():
197
+ part.points = pts
198
+ elif len(points) != partsNum:
199
+ logger.warning(
200
+ "Some Answer parts are missing the points, they will get the standard points"
201
+ )
202
+ for num, part in parts.items():
203
+ p = points.get(num)
204
+ part.points = p if p is not None else self.rawInput.get(Tags.POINTS)
205
+
206
+ self.question.questionParts = parts
207
+
208
+ @overload
209
+ def _getPartValues(self, Tag: Literal[Tags.RESULT]) -> dict[int, str]: ...
210
+ @overload
211
+ def _getPartValues(self, Tag: Literal[Tags.POINTS]) -> dict[int, float]: ...
212
+ @overload
213
+ def _getPartValues(
214
+ self, Tag: Literal[Tags.TRUE, Tags.FALSE]
215
+ ) -> dict[int, list[str]]: ...
216
+ def _getPartValues(self, Tag):
217
+ tagValues: dict = {
84
218
  self.getPartNumber(key): self.rawInput[key]
85
219
  for key in self.rawInput
86
- if key.startswith(Tags.RESULT)
220
+ if key.startswith(Tag)
87
221
  }
88
- self.logger.debug("Got the following answers: %s", equations)
89
- bps = str(self.rawInput[Tags.BPOINTS])
90
- varNames: list[str] = self._getVarsList(bps)
91
- numericAnswers: dict[int, list[float]] = {key: [] for key in equations}
92
- self.question.variables, number = self._getVariablesDict(varNames)
93
- for n in range(number):
94
- self.setupAstIntprt(self.question.variables, n)
95
- for ansNum, eq in equations.items():
96
- result = self.astEval(eq)
97
- if isinstance(result, float):
98
- firstResult = self.rawInput.get(Tags.FIRSTRESULT)
99
- if n == 0 and not math.isclose(result, firstResult, rel_tol=0.01):
100
- self.logger.warning(
101
- "The calculated result %s differs from given firstResult: %s",
102
- result,
103
- firstResult,
104
- )
105
- numericAnswers[ansNum].append(result)
106
- else:
107
- msg = f"The expression {eq} could not be evaluated."
108
- raise QNotParsedException(msg, self.question.id)
109
-
110
- self.question.answerVariants = numericAnswers
222
+ self.logger.warning("Found part data %s: %s", Tag, tagValues)
223
+ return tagValues
224
+
225
+ def _parseAnswerParts(self) -> None:
226
+ """Parse the numeric or MC result items."""
227
+ try:
228
+ bps = str(self.rawInput[Tags.BPOINTS])
229
+ except KeyError:
230
+ bps = None
231
+ number = 1
232
+ else:
233
+ varNames: list[str] = self._getVarsList(bps)
234
+ self.question.variables, number = self._getVariablesDict(varNames)
235
+ for variant in range(number):
236
+ self.setupAstIntprt(self.question.variables, variant)
237
+ for partNum, part in self.question.questionParts.items():
238
+ if part.typ == "NFM":
239
+ result = self._calculateNFMPartResult(part, partNum, variant)
240
+ part.nfResults.append(result)
241
+ logger.debug("Appended NF part %s result: %s", partNum, result)
242
+ elif part.typ == "MC":
243
+ ansStr = self.getMCAnsStr(
244
+ part.trueAnswers, part.falseAnswers, points=part.points
245
+ )
246
+ part.mcAnswerString = ansStr
247
+ logger.debug("Appended MC part %s: %s", partNum, ansStr)
111
248
  self._setVariants(number)
112
249
 
113
- def _parseQuestionParts(self) -> None:
114
- """Generate the question parts aka the clozes."""
115
- parts: dict[int, list[str]] = {
116
- self.getPartNumber(key): self.rawInput[key]
117
- for key in self.rawInput
118
- if key.startswith(Tags.QUESTIONPART)
119
- }
120
- questionParts: dict[int, list[ET.Element]] = {}
121
- for number, text in parts.items():
122
- questionParts[number] = []
123
- for t in text:
124
- questionParts[number].append(TextElements.PLEFT.create())
125
- questionParts[number][-1].text = t
126
- self.logger.debug("The Question Parts are created:", questionParts)
127
- self.question.questionTexts = questionParts
128
- self.question.partsNum = len(questionParts)
129
- self.logger.info("The Question has %s parts", self.question.partsNum)
130
-
131
- # def setMainText(self) -> None:
132
- # super().setMainText()
133
- # self.question.qtextParagraphs
250
+ def _calculateNFMPartResult(
251
+ self, part: ClozePart, partNum: int, variant: int
252
+ ) -> float:
253
+ result = self.astEval(part.equation)
254
+ if isinstance(result, float):
255
+ try:
256
+ firstResult = self.rawInput[f"{Tags.FIRSTRESULT}:{partNum}"]
257
+ except KeyError:
258
+ firstResult = 0.0
259
+ if variant == 0 and not math.isclose(result, firstResult, rel_tol=0.002):
260
+ self.logger.warning(
261
+ "The calculated result %s differs from given firstResult: %s",
262
+ result,
263
+ firstResult,
264
+ )
265
+ return result
266
+ msg = f"The expression {part.equation} could not be evaluated."
267
+ raise QNotParsedException(msg, self.question.id)
134
268
 
135
269
  def getPartNumber(self, indexKey: str) -> int:
136
270
  """Return the number of the question Part.
@@ -151,7 +285,7 @@ class ClozeQuestionParser(NFMQuestionParser):
151
285
  def getNumericAnsStr(
152
286
  result: float,
153
287
  tolerance: float,
154
- weight: int = 1,
288
+ points: float = 1,
155
289
  wrongSignCount: int = 50,
156
290
  wrongSignFeedback: str = "your result has the wrong sign (+-)",
157
291
  ) -> str:
@@ -159,12 +293,6 @@ class ClozeQuestionParser(NFMQuestionParser):
159
293
 
160
294
  Parameters.
161
295
  ----------
162
- weight:
163
- The weight of the answer relative to the other answer elements.
164
- Of one answer has `weight=2` and two other answers `weight=1`,
165
- this answer will be counted as 50% of the questions points.
166
- The other two will counted as 25% of the questions points.
167
-
168
296
  wrongSignCount:
169
297
  If the wrong sign `+` or `-` is given, how much of the points should be given.
170
298
  Interpreted as percent.
@@ -175,7 +303,7 @@ class ClozeQuestionParser(NFMQuestionParser):
175
303
  absTol = f":{round(result * tolerance, 3)}"
176
304
  answerParts: list[str | float] = [
177
305
  "{",
178
- weight,
306
+ points,
179
307
  ":NUMERICAL:=",
180
308
  round(result, 3),
181
309
  absTol,
@@ -194,14 +322,21 @@ class ClozeQuestionParser(NFMQuestionParser):
194
322
  def getMCAnsStr(
195
323
  true: list[str],
196
324
  false: list[str],
197
- weight: int = 1,
325
+ points: float = 1,
198
326
  ) -> str:
199
327
  """Generate the answer string for the MC answers."""
328
+ truePercent: float = round(100 / len(true), 1)
329
+ falsePercent: float = round(100 / len(false), 1)
330
+ falseList: list[str] = [f"~%-{falsePercent}%{ans}" for ans in false]
331
+ trueList: list[str] = [f"~%{truePercent}%{ans}" for ans in true]
200
332
  answerParts: list[str | float] = [
201
333
  "{",
202
- weight,
203
- ":MC:",
204
- "}",
334
+ points,
335
+ ":MULTIRESPONSE:",
205
336
  ]
337
+ answerParts.extend(trueList)
338
+ answerParts.extend(falseList)
339
+ answerParts.append("}")
340
+
206
341
  answerPStrings = [str(part) for part in answerParts]
207
- return "".join(answerPString)
342
+ return "".join(answerPStrings)
@@ -45,14 +45,15 @@ class NFMQuestionParser(QuestionParser):
45
45
  super().__init__()
46
46
  self.genFeedbacks = [XMLTags.GENFEEDB]
47
47
  self.question: NFMQuestion
48
- module = self.settings.get(Tags.IMPORTMODULE)
49
- if module and not type(self).astEval.symtable.get(module):
50
- type(self).astEval(f"import {module}")
51
- self.logger.warning("Imported '%s' to Asteval symtable", module)
52
48
 
53
49
  def setup(self, question: NFMQuestion) -> None:
54
50
  self.question: NFMQuestion = question
55
51
  super().setup(question)
52
+ module = self.settings.get(Tags.IMPORTMODULE)
53
+ if module and type(self).astEval.symtable.get(module, None) is None:
54
+ type(self).astEval(f"import {module}")
55
+ imported = type(self).astEval.symtable.get(module)
56
+ self.logger.warning("Imported '%s' to Asteval symtable.", module)
56
57
 
57
58
  def _parseAnswers(self) -> None:
58
59
  equation = self.rawInput.get(Tags.EQUATION)
excel2moodle/ui/appUi.py CHANGED
@@ -51,23 +51,27 @@ class MainWindow(QtWidgets.QMainWindow):
51
51
  self.ui.treeWidget.header().setSectionResizeMode(
52
52
  QtWidgets.QHeaderView.ResizeToContents,
53
53
  )
54
+ self.ui.pointCounter.setReadOnly(True)
55
+ self.ui.questionCounter.setReadOnly(True)
56
+ self.setStatus(
57
+ "Wählen Sie eine Excel Tabelle mit den Fragen aus",
58
+ )
59
+ self.threadPool = QThreadPool()
60
+ self._restoreSettings()
61
+
62
+ def _restoreSettings(self) -> None:
63
+ """Restore the settings from the last session, if they exist."""
54
64
  self.exportDialog.ui.checkBoxIncludeCategories.setChecked(
55
65
  self.qSettings.value(Tags.INCLUDEINCATS, defaultValue=True, type=bool)
56
66
  )
57
67
  self.exportDialog.ui.spinBoxDefaultQVariant.setValue(
58
68
  self.qSettings.value(Tags.QUESTIONVARIANT, defaultValue=1, type=int)
59
69
  )
60
- self.ui.pointCounter.setReadOnly(True)
61
- self.ui.questionCounter.setReadOnly(True)
62
- self.setStatus(
63
- "Wählen Sie eine Excel Tabelle mit den Fragen aus",
64
- )
65
70
  try:
66
71
  self.resize(self.qSettings.value("windowSize"))
67
72
  self.move(self.qSettings.value("windowPosition"))
68
73
  except Exception:
69
74
  pass
70
- self.threadPool = QThreadPool()
71
75
  if self.qSettings.contains(Tags.SPREADSHEETPATH.full):
72
76
  sheet = self.qSettings.value(Tags.SPREADSHEETPATH.full)
73
77
  self.setSheetPath(sheet)
@@ -79,9 +83,6 @@ class MainWindow(QtWidgets.QMainWindow):
79
83
  )
80
84
  loggerSignal.emitter.signal.connect(self.updateLog)
81
85
  self.ui.actionEquationChecker.triggered.connect(self.openEqCheckerDlg)
82
- self.exportDialog.ui.checkBoxIncludeCategories.checkStateChanged.connect(
83
- self.setIncludeCategoriesSetting,
84
- )
85
86
  self.ui.actionParseAll.triggered.connect(self.parseSpreadsheetAll)
86
87
  self.testDB.signals.categoryQuestionsReady.connect(self.treeRefreshCategory)
87
88
  self.ui.actionSpreadsheet.triggered.connect(self.actionSpreadsheet)
@@ -91,13 +92,6 @@ class MainWindow(QtWidgets.QMainWindow):
91
92
  self.ui.treeWidget.itemClicked.connect(self.updateQuestionPreview)
92
93
  self.ui.actionAbout.triggered.connect(self.openAboutDlg)
93
94
  self.ui.actionDocumentation.triggered.connect(self.openDocumentation)
94
- self.exportDialog.ui.spinBoxDefaultQVariant.valueChanged.connect(
95
- self.setQVariantDefault
96
- )
97
-
98
- @QtCore.Slot()
99
- def setQVariantDefault(self, value: int) -> None:
100
- self.settings.set(Tags.QUESTIONVARIANT, value)
101
95
 
102
96
  @QtCore.Slot()
103
97
  def parseSpreadsheetAll(self) -> None:
@@ -128,12 +122,6 @@ class MainWindow(QtWidgets.QMainWindow):
128
122
  def updateLog(self, log) -> None:
129
123
  self.ui.loggerWindow.append(log)
130
124
 
131
- def setIncludeCategoriesSetting(self) -> None:
132
- if self.exportDialog.ui.checkBoxIncludeCategories.isChecked():
133
- self.settings.set(Tags.INCLUDEINCATS, True)
134
- else:
135
- self.settings.set(Tags.INCLUDEINCATS, False)
136
-
137
125
  def closeEvent(self, event) -> None:
138
126
  logger.info("Closing. Saving window stats.")
139
127
  self.qSettings.setValue("windowSize", self.size())
@@ -179,6 +167,14 @@ class MainWindow(QtWidgets.QMainWindow):
179
167
  self.exportDialog.ui.pointCount.setValue(self.ui.pointCounter.value())
180
168
  if self.exportDialog.exec():
181
169
  self.exportFile = self.exportDialog.exportFile
170
+ self.settings.set(
171
+ Tags.INCLUDEINCATS,
172
+ self.exportDialog.ui.checkBoxIncludeCategories.isChecked(),
173
+ )
174
+ self.settings.set(
175
+ Tags.QUESTIONVARIANT,
176
+ self.exportDialog.ui.spinBoxDefaultQVariant.value(),
177
+ )
182
178
  logger.info("New Export File is set %s", self.exportFile)
183
179
  self.testDB.appendQuestions(selection, self.exportFile)
184
180
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: excel2moodle
3
- Version: 0.5.1
3
+ Version: 0.5.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
@@ -81,6 +81,46 @@ If You want to support my work as well, you can by me a [coffee](https://ko-fi.c
81
81
 
82
82
  # Changelogs
83
83
 
84
+ ## 0.5.2 (2025-06-30)
85
+ Extended Documentation and bugfix for import Module
86
+
87
+ ### bugfix (2 changes)
88
+
89
+ - [Default question variant saved and reused.](https://gitlab.com/jbosse3/excel2moodle/-/commit/097705ba83727463a9b27cd76e99814a7ecf28df)
90
+ - [bugfix: Import module working again](https://gitlab.com/jbosse3/excel2moodle/-/commit/5f293970bcdac3858911cdcc102b72714af057bd)
91
+
92
+ ### documentation (1 change)
93
+
94
+ - [documentation: Added how to build question database](https://gitlab.com/jbosse3/excel2moodle/-/commit/71ceb122aa37e8bf2735b659359ae37d81017599)
95
+
96
+ ### feature (1 change)
97
+
98
+ - [Implemented MC question string method](https://gitlab.com/jbosse3/excel2moodle/-/commit/c4f2081d0000ee60322fe8eec8468fa3317ce7be)
99
+
100
+ ### improvement (1 change)
101
+
102
+ - [Implemented ClozePart object](https://gitlab.com/jbosse3/excel2moodle/-/commit/878f90f45e37421384c4f8f602115e7596b4ceb9)
103
+
104
+ ## 0.5.2 (2025-06-30)
105
+ Extended Documentation and bugfix for import Module
106
+
107
+ ### bugfix (2 changes)
108
+
109
+ - [Default question variant saved and reused.](https://gitlab.com/jbosse3/excel2moodle/-/commit/097705ba83727463a9b27cd76e99814a7ecf28df)
110
+ - [bugfix: Import module working again](https://gitlab.com/jbosse3/excel2moodle/-/commit/5f293970bcdac3858911cdcc102b72714af057bd)
111
+
112
+ ### documentation (1 change)
113
+
114
+ - [documentation: Added how to build question database](https://gitlab.com/jbosse3/excel2moodle/-/commit/71ceb122aa37e8bf2735b659359ae37d81017599)
115
+
116
+ ### feature (1 change)
117
+
118
+ - [Implemented MC question string method](https://gitlab.com/jbosse3/excel2moodle/-/commit/c4f2081d0000ee60322fe8eec8468fa3317ce7be)
119
+
120
+ ### improvement (1 change)
121
+
122
+ - [Implemented ClozePart object](https://gitlab.com/jbosse3/excel2moodle/-/commit/878f90f45e37421384c4f8f602115e7596b4ceb9)
123
+
84
124
  ## 0.5.1 (2025-06-24)
85
125
  Minor docs improvement and question variant bugfix
86
126
 
@@ -3,36 +3,36 @@ excel2moodle/__main__.py,sha256=sG4ygwfVFskLQorBn-v98SvasNcPmwl_vLYpruT5Hk8,1175
3
3
  excel2moodle/logger.py,sha256=fq8ZOkCI1wj38v8IyrZsUlpt16onlSH_phqbVvYUwBQ,3725
4
4
  excel2moodle/core/__init__.py,sha256=87BwhtZse72Tk17Ib-V9X2k9wkhmtVnEj2ZmJ9JBAnI,63
5
5
  excel2moodle/core/category.py,sha256=wLzpbweQbzaItdbp2NCPI_Zmk94fy1EDOwEEN8zPvkU,2123
6
- excel2moodle/core/dataStructure.py,sha256=sDdum2I0EZRuXTgbVSsLX4aBINfzkmNXBCHzUVBhxH8,16022
6
+ excel2moodle/core/dataStructure.py,sha256=0XVu6NqKKNrjL0B9UVD9di-uqriGIdF-57lwx0ckeDI,16026
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
11
  excel2moodle/core/parser.py,sha256=y0BXXt5j-4gRZO8otmEZ1Rmb0DW7hziesUoZ2kVpo9Y,8235
12
- excel2moodle/core/question.py,sha256=vtrYq0J5D7PivkVo4eMUx_5l5jCyunmjThij38QopyY,11706
12
+ excel2moodle/core/question.py,sha256=_BSaMdDB269Q4Ag7izegMiExjYRTYOuhYe-qo92EAAg,11726
13
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=N-0fDSWfpxYN0YZmPQPEaNHpxqW1OOC32WNseYmy6zM,7657
19
+ excel2moodle/question_types/cloze.py,sha256=-F2CozuNHRvTDQM69DgkKXTvw9wMLXfipkdhfTNWSSs,11969
20
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=03-aihk9HtzRCgpCGz_5WOR4JOqdJlkafyKprgLrosI,4948
22
+ excel2moodle/question_types/nfm.py,sha256=LTQ60qrhYUqKXoZW4AKz352rD4pgN0wAX_tm5FLFbV0,5022
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=88WODtEWqX1oQJebbPhlQChKM5N_9BH0ZuOpKVYrKm0,10863
28
+ excel2moodle/ui/appUi.py,sha256=caNdBb4_C2Ptl0He2iCQqoxw6Tm0wmJL9eOoKfjMah8,10681
29
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.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,,
33
+ excel2moodle-0.5.2.dist-info/licenses/LICENSE,sha256=ywQqe6Sitymkf2lV2NRcx_aGsaC-KbSl_EfEsRXmNRw,35135
34
+ excel2moodle-0.5.2.dist-info/METADATA,sha256=pskuhRVnt-PEmvxVxvPUvN03gHmilnZqeFluoPOkPrk,6330
35
+ excel2moodle-0.5.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
36
+ excel2moodle-0.5.2.dist-info/entry_points.txt,sha256=myfMLDThuGgWHMJDPPfILiZqo_7D3fhmDdJGqWOAjPw,60
37
+ excel2moodle-0.5.2.dist-info/top_level.txt,sha256=5V1xRUQ9o7UmOCmNoWCZPAuy5nXp3Qbzyqch8fUGT_c,13
38
+ excel2moodle-0.5.2.dist-info/RECORD,,