excel2moodle 0.5.1__tar.gz → 0.5.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.5.1/excel2moodle.egg-info → excel2moodle-0.5.2}/PKG-INFO +41 -1
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/README.md +40 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/dataStructure.py +3 -3
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/question.py +1 -1
- excel2moodle-0.5.2/excel2moodle/question_types/cloze.py +342 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/question_types/nfm.py +5 -4
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/appUi.py +18 -22
- {excel2moodle-0.5.1 → excel2moodle-0.5.2/excel2moodle.egg-info}/PKG-INFO +41 -1
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle.egg-info/SOURCES.txt +0 -11
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/pyproject.toml +10 -1
- excel2moodle-0.5.2/test/test_nfmParsing.py +85 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/test/test_picture.py +16 -14
- excel2moodle-0.5.1/docs/_build/html/exampleQuestions.html +0 -599
- excel2moodle-0.5.1/docs/_build/html/excel2moodle.core.html +0 -1687
- excel2moodle-0.5.1/docs/_build/html/excel2moodle.extra.html +0 -237
- excel2moodle-0.5.1/docs/_build/html/excel2moodle.html +0 -467
- excel2moodle-0.5.1/docs/_build/html/excel2moodle.ui.html +0 -667
- excel2moodle-0.5.1/docs/_build/html/genindex.html +0 -908
- excel2moodle-0.5.1/docs/_build/html/howto.html +0 -384
- excel2moodle-0.5.1/docs/_build/html/index.html +0 -228
- excel2moodle-0.5.1/docs/_build/html/py-modindex.html +0 -228
- excel2moodle-0.5.1/docs/_build/html/search.html +0 -133
- excel2moodle-0.5.1/docs/_build/html/userReference.html +0 -444
- excel2moodle-0.5.1/excel2moodle/question_types/cloze.py +0 -207
- excel2moodle-0.5.1/test/test_nfmParsing.py +0 -30
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/LICENSE +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/MANIFEST.in +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/__init__.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/__main__.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/__init__.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/category.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/etHelpers.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/exceptions.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/globals.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/numericMultiQ.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/parser.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/settings.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/stringHelpers.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/core/validator.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/extra/__init__.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/extra/equationVerification.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/logger.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/question_types/__init__.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/question_types/mc.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/question_types/nf.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/UI_equationChecker.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/UI_exportSettingsDialog.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/UI_mainWindow.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/UI_variantDialog.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/__init__.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/dialogs.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/equationChecker.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/treewidget.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle/ui/windowDoc.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle.egg-info/dependency_links.txt +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle.egg-info/entry_points.txt +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle.egg-info/requires.txt +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/excel2moodle.egg-info/top_level.txt +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/setup.cfg +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/test/test_parseQuestion.py +0 -0
- {excel2moodle-0.5.1 → excel2moodle-0.5.2}/test/test_questionDataGet.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: excel2moodle
|
3
|
-
Version: 0.5.
|
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
|
|
@@ -60,6 +60,46 @@ If You want to support my work as well, you can by me a [coffee](https://ko-fi.c
|
|
60
60
|
|
61
61
|
# Changelogs
|
62
62
|
|
63
|
+
## 0.5.2 (2025-06-30)
|
64
|
+
Extended Documentation and bugfix for import Module
|
65
|
+
|
66
|
+
### bugfix (2 changes)
|
67
|
+
|
68
|
+
- [Default question variant saved and reused.](https://gitlab.com/jbosse3/excel2moodle/-/commit/097705ba83727463a9b27cd76e99814a7ecf28df)
|
69
|
+
- [bugfix: Import module working again](https://gitlab.com/jbosse3/excel2moodle/-/commit/5f293970bcdac3858911cdcc102b72714af057bd)
|
70
|
+
|
71
|
+
### documentation (1 change)
|
72
|
+
|
73
|
+
- [documentation: Added how to build question database](https://gitlab.com/jbosse3/excel2moodle/-/commit/71ceb122aa37e8bf2735b659359ae37d81017599)
|
74
|
+
|
75
|
+
### feature (1 change)
|
76
|
+
|
77
|
+
- [Implemented MC question string method](https://gitlab.com/jbosse3/excel2moodle/-/commit/c4f2081d0000ee60322fe8eec8468fa3317ce7be)
|
78
|
+
|
79
|
+
### improvement (1 change)
|
80
|
+
|
81
|
+
- [Implemented ClozePart object](https://gitlab.com/jbosse3/excel2moodle/-/commit/878f90f45e37421384c4f8f602115e7596b4ceb9)
|
82
|
+
|
83
|
+
## 0.5.2 (2025-06-30)
|
84
|
+
Extended Documentation and bugfix for import Module
|
85
|
+
|
86
|
+
### bugfix (2 changes)
|
87
|
+
|
88
|
+
- [Default question variant saved and reused.](https://gitlab.com/jbosse3/excel2moodle/-/commit/097705ba83727463a9b27cd76e99814a7ecf28df)
|
89
|
+
- [bugfix: Import module working again](https://gitlab.com/jbosse3/excel2moodle/-/commit/5f293970bcdac3858911cdcc102b72714af057bd)
|
90
|
+
|
91
|
+
### documentation (1 change)
|
92
|
+
|
93
|
+
- [documentation: Added how to build question database](https://gitlab.com/jbosse3/excel2moodle/-/commit/71ceb122aa37e8bf2735b659359ae37d81017599)
|
94
|
+
|
95
|
+
### feature (1 change)
|
96
|
+
|
97
|
+
- [Implemented MC question string method](https://gitlab.com/jbosse3/excel2moodle/-/commit/c4f2081d0000ee60322fe8eec8468fa3317ce7be)
|
98
|
+
|
99
|
+
### improvement (1 change)
|
100
|
+
|
101
|
+
- [Implemented ClozePart object](https://gitlab.com/jbosse3/excel2moodle/-/commit/878f90f45e37421384c4f8f602115e7596b4ceb9)
|
102
|
+
|
63
103
|
## 0.5.1 (2025-06-24)
|
64
104
|
Minor docs improvement and question variant bugfix
|
65
105
|
|
@@ -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
|
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
|
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.
|
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:
|
@@ -0,0 +1,342 @@
|
|
1
|
+
"""Implementation of tde cloze question type.
|
2
|
+
|
3
|
+
This question type is like the NFM but supports multiple fields of answers.
|
4
|
+
All Answers are calculated off an equation using the same variables.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
import math
|
9
|
+
import re
|
10
|
+
from typing import Literal, overload
|
11
|
+
|
12
|
+
import lxml.etree as ET
|
13
|
+
|
14
|
+
from excel2moodle.core.exceptions import QNotParsedException
|
15
|
+
from excel2moodle.core.globals import (
|
16
|
+
Tags,
|
17
|
+
TextElements,
|
18
|
+
)
|
19
|
+
from excel2moodle.core.question import ParametricQuestion
|
20
|
+
from excel2moodle.core.settings import Tags
|
21
|
+
from excel2moodle.question_types.nfm import NFMQuestionParser
|
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
|
+
|
107
|
+
|
108
|
+
class ClozeQuestion(ParametricQuestion):
|
109
|
+
"""Cloze Question Type."""
|
110
|
+
|
111
|
+
def __init__(self, *args, **kwargs) -> None:
|
112
|
+
super().__init__(*args, **kwargs)
|
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
|
129
|
+
|
130
|
+
def _assembleAnswer(self, variant: int = 1) -> None:
|
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]
|
137
|
+
ansStr = ClozeQuestionParser.getNumericAnsStr(
|
138
|
+
result,
|
139
|
+
self.rawData.get(Tags.TOLERANCE),
|
140
|
+
wrongSignCount=self.rawData.get(Tags.WRONGSIGNPERCENT),
|
141
|
+
points=part.points,
|
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)
|
147
|
+
ul = TextElements.ULIST.create()
|
148
|
+
item = TextElements.LISTITEM.create()
|
149
|
+
item.text = ansStr
|
150
|
+
ul.append(item)
|
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)
|
155
|
+
|
156
|
+
def _assembleText(self, variant=0) -> list[ET.Element]:
|
157
|
+
textParts = super()._assembleText(variant=variant)
|
158
|
+
self.logger.debug("Appending QuestionParts to main text")
|
159
|
+
textParts.extend(self.questionTexts)
|
160
|
+
return textParts
|
161
|
+
|
162
|
+
|
163
|
+
class ClozeQuestionParser(NFMQuestionParser):
|
164
|
+
"""Parser for the cloze question type."""
|
165
|
+
|
166
|
+
def __init__(self, *args, **kwargs) -> None:
|
167
|
+
super().__init__(*args, **kwargs)
|
168
|
+
self.question: ClozeQuestion
|
169
|
+
|
170
|
+
def setup(self, question: ClozeQuestion) -> None:
|
171
|
+
self.question: ClozeQuestion = question
|
172
|
+
super().setup(question)
|
173
|
+
|
174
|
+
def _parseAnswers(self) -> None:
|
175
|
+
self._setupParts()
|
176
|
+
self._parseAnswerParts()
|
177
|
+
|
178
|
+
def _setupParts(self) -> None:
|
179
|
+
parts: dict[int, ClozePart] = {
|
180
|
+
self.getPartNumber(key): ClozePart(self.question, self.rawInput[key])
|
181
|
+
for key in self.rawInput
|
182
|
+
if key.startswith(Tags.QUESTIONPART)
|
183
|
+
}
|
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 = {
|
218
|
+
self.getPartNumber(key): self.rawInput[key]
|
219
|
+
for key in self.rawInput
|
220
|
+
if key.startswith(Tag)
|
221
|
+
}
|
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)
|
248
|
+
self._setVariants(number)
|
249
|
+
|
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)
|
268
|
+
|
269
|
+
def getPartNumber(self, indexKey: str) -> int:
|
270
|
+
"""Return the number of the question Part.
|
271
|
+
|
272
|
+
The number should be given after the `@` sign.
|
273
|
+
This is number is used, to reference the question Text
|
274
|
+
and the expected answer fields together.
|
275
|
+
"""
|
276
|
+
try:
|
277
|
+
num = re.findall(r":(\d+)$", indexKey)[0]
|
278
|
+
except IndexError:
|
279
|
+
msg = f"No :i question Part value given for {indexKey}"
|
280
|
+
raise QNotParsedException(msg, self.question.id)
|
281
|
+
else:
|
282
|
+
return int(num)
|
283
|
+
|
284
|
+
@staticmethod
|
285
|
+
def getNumericAnsStr(
|
286
|
+
result: float,
|
287
|
+
tolerance: float,
|
288
|
+
points: float = 1,
|
289
|
+
wrongSignCount: int = 50,
|
290
|
+
wrongSignFeedback: str = "your result has the wrong sign (+-)",
|
291
|
+
) -> str:
|
292
|
+
"""Generate the answer string from `result`.
|
293
|
+
|
294
|
+
Parameters.
|
295
|
+
----------
|
296
|
+
wrongSignCount:
|
297
|
+
If the wrong sign `+` or `-` is given, how much of the points should be given.
|
298
|
+
Interpreted as percent.
|
299
|
+
tolerance:
|
300
|
+
The relative tolerance, as fraction
|
301
|
+
|
302
|
+
"""
|
303
|
+
absTol = f":{round(result * tolerance, 3)}"
|
304
|
+
answerParts: list[str | float] = [
|
305
|
+
"{",
|
306
|
+
points,
|
307
|
+
":NUMERICAL:=",
|
308
|
+
round(result, 3),
|
309
|
+
absTol,
|
310
|
+
"~%",
|
311
|
+
wrongSignCount,
|
312
|
+
"%",
|
313
|
+
round(result * (-1), 3),
|
314
|
+
absTol,
|
315
|
+
f"#{wrongSignFeedback}",
|
316
|
+
"}",
|
317
|
+
]
|
318
|
+
answerPStrings = [str(part) for part in answerParts]
|
319
|
+
return "".join(answerPStrings)
|
320
|
+
|
321
|
+
@staticmethod
|
322
|
+
def getMCAnsStr(
|
323
|
+
true: list[str],
|
324
|
+
false: list[str],
|
325
|
+
points: float = 1,
|
326
|
+
) -> str:
|
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]
|
332
|
+
answerParts: list[str | float] = [
|
333
|
+
"{",
|
334
|
+
points,
|
335
|
+
":MULTIRESPONSE:",
|
336
|
+
]
|
337
|
+
answerParts.extend(trueList)
|
338
|
+
answerParts.extend(falseList)
|
339
|
+
answerParts.append("}")
|
340
|
+
|
341
|
+
answerPStrings = [str(part) for part in answerParts]
|
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)
|
@@ -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.
|
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
|
|
@@ -2,17 +2,6 @@ LICENSE
|
|
2
2
|
MANIFEST.in
|
3
3
|
README.md
|
4
4
|
pyproject.toml
|
5
|
-
docs/_build/html/exampleQuestions.html
|
6
|
-
docs/_build/html/excel2moodle.core.html
|
7
|
-
docs/_build/html/excel2moodle.extra.html
|
8
|
-
docs/_build/html/excel2moodle.html
|
9
|
-
docs/_build/html/excel2moodle.ui.html
|
10
|
-
docs/_build/html/genindex.html
|
11
|
-
docs/_build/html/howto.html
|
12
|
-
docs/_build/html/index.html
|
13
|
-
docs/_build/html/py-modindex.html
|
14
|
-
docs/_build/html/search.html
|
15
|
-
docs/_build/html/userReference.html
|
16
5
|
excel2moodle/__init__.py
|
17
6
|
excel2moodle/__main__.py
|
18
7
|
excel2moodle/logger.py
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "excel2moodle"
|
7
|
-
version = "0.5.
|
7
|
+
version = "0.5.2"
|
8
8
|
authors = [
|
9
9
|
{ name="Jakob Bosse" },
|
10
10
|
]
|
@@ -45,6 +45,15 @@ pythonpath = ["."]
|
|
45
45
|
dev = [
|
46
46
|
"ty>=0.0.1a10",
|
47
47
|
]
|
48
|
+
doc = [
|
49
|
+
"asteval>=1.0.6",
|
50
|
+
"lxml>=5.4.0",
|
51
|
+
"sphinx>=7.4.7",
|
52
|
+
"sphinx-autodoc-typehints>=3.0.1",
|
53
|
+
"sphinx-copybutton>=0.5.2",
|
54
|
+
"sphinx-rtd-theme>=2.0.0",
|
55
|
+
"toml>=0.10.2",
|
56
|
+
]
|
48
57
|
|
49
58
|
[project.urls]
|
50
59
|
Repository = "https://gitlab.com/jbosse3/excel2moodle.git"
|