excel2moodle 0.6.2__py3-none-any.whl → 0.6.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- excel2moodle/__init__.py +2 -0
- excel2moodle/__main__.py +18 -3
- excel2moodle/core/bullets.py +2 -2
- excel2moodle/core/category.py +4 -3
- excel2moodle/core/dataStructure.py +24 -23
- excel2moodle/core/settings.py +5 -1
- excel2moodle/extra/updateQuery.py +48 -0
- excel2moodle/extra/variableGenerator.py +73 -49
- excel2moodle/question_types/cloze.py +5 -6
- excel2moodle/ui/UI_updateDlg.py +106 -0
- excel2moodle/ui/appUi.py +45 -17
- excel2moodle/ui/dialogs.py +18 -1
- excel2moodle/ui/treewidget.py +30 -10
- {excel2moodle-0.6.2.dist-info → excel2moodle-0.6.3.dist-info}/METADATA +21 -1
- {excel2moodle-0.6.2.dist-info → excel2moodle-0.6.3.dist-info}/RECORD +19 -17
- {excel2moodle-0.6.2.dist-info → excel2moodle-0.6.3.dist-info}/WHEEL +0 -0
- {excel2moodle-0.6.2.dist-info → excel2moodle-0.6.3.dist-info}/entry_points.txt +0 -0
- {excel2moodle-0.6.2.dist-info → excel2moodle-0.6.3.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.6.2.dist-info → excel2moodle-0.6.3.dist-info}/top_level.txt +0 -0
excel2moodle/__init__.py
CHANGED
@@ -45,6 +45,8 @@ if __package__ is not None:
|
|
45
45
|
"documentation": "https://jbosse3.gitlab.io/excel2moodle",
|
46
46
|
"homepage": meta["project-url"].split()[1],
|
47
47
|
"issues": "https://gitlab.com/jbosse3/excel2moodle/issues",
|
48
|
+
"funding": "https://ko-fi.com/jbosse3",
|
49
|
+
"API_id": "jbosse3%2Fexcel2moodle",
|
48
50
|
}
|
49
51
|
|
50
52
|
|
excel2moodle/__main__.py
CHANGED
@@ -5,17 +5,20 @@ import signal
|
|
5
5
|
import sys
|
6
6
|
from pathlib import Path
|
7
7
|
|
8
|
+
from PySide6.QtCore import QTimer
|
8
9
|
from PySide6.QtWidgets import QApplication
|
9
10
|
|
10
11
|
import excel2moodle
|
11
|
-
from excel2moodle import e2mMetadata, mainLogger
|
12
|
+
from excel2moodle import __version__, e2mMetadata, mainLogger
|
12
13
|
from excel2moodle.core import dataStructure
|
13
14
|
from excel2moodle.core.settings import Settings, Tags
|
15
|
+
from excel2moodle.extra import updateQuery
|
14
16
|
from excel2moodle.logger import loggerConfig
|
15
17
|
from excel2moodle.ui import appUi as ui
|
16
18
|
|
17
19
|
|
18
20
|
def main() -> None:
|
21
|
+
app = QApplication(sys.argv)
|
19
22
|
excel2moodle.isMain = True
|
20
23
|
settings = Settings()
|
21
24
|
logfile = Path(settings.get(Tags.LOGFILE)).resolve()
|
@@ -24,12 +27,24 @@ def main() -> None:
|
|
24
27
|
logfile.replace(f"{logfile}.old")
|
25
28
|
logConfig.dictConfig(config=loggerConfig)
|
26
29
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
27
|
-
app = QApplication(sys.argv)
|
28
|
-
settings = Settings()
|
29
30
|
database: dataStructure.QuestionDB = dataStructure.QuestionDB(settings)
|
30
31
|
window = ui.MainWindow(settings, database)
|
31
32
|
database.window = window
|
32
33
|
window.show()
|
34
|
+
# Update check
|
35
|
+
latestTag = updateQuery.get_latest_tag(e2mMetadata.get("API_id"))
|
36
|
+
if latestTag is None:
|
37
|
+
mainLogger.warning(
|
38
|
+
"Couldn't check for Updates, maybe there is no internet connection"
|
39
|
+
)
|
40
|
+
if latestTag is not None and latestTag.strip("v") != __version__:
|
41
|
+
mainLogger.warning(
|
42
|
+
"A new Update is available: %s --> %s ", __version__, latestTag
|
43
|
+
)
|
44
|
+
changelog = updateQuery.get_changelog(e2mMetadata.get("API_id"))
|
45
|
+
# Delay showing the update dialog slightly
|
46
|
+
QTimer.singleShot(100, lambda: window.showUpdateDialog(changelog, latestTag))
|
47
|
+
|
33
48
|
for k, v in e2mMetadata.items():
|
34
49
|
msg = f"{k:^14s}: {v}"
|
35
50
|
mainLogger.info(msg)
|
excel2moodle/core/bullets.py
CHANGED
@@ -15,7 +15,7 @@ class BulletList:
|
|
15
15
|
def __init__(self, rawBullets: list[str], qID: str) -> None:
|
16
16
|
self.rawBullets: list[str] = rawBullets
|
17
17
|
self.element: ET.Element = ET.Element("ul")
|
18
|
-
self.bullets: dict[str, BulletP] = {}
|
18
|
+
self.bullets: dict[str | int, BulletP] = {}
|
19
19
|
self.id = qID
|
20
20
|
self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.id})
|
21
21
|
self._setupBullets(rawBullets)
|
@@ -70,7 +70,7 @@ class BulletList:
|
|
70
70
|
if match is None:
|
71
71
|
self.logger.debug("Got a normal bulletItem")
|
72
72
|
num: float = float(quant.replace(",", "."))
|
73
|
-
bulletName
|
73
|
+
bulletName = i + 1
|
74
74
|
else:
|
75
75
|
bulletName = match.group(1)
|
76
76
|
num: float = 0.0
|
excel2moodle/core/category.py
CHANGED
@@ -24,8 +24,9 @@ class Category:
|
|
24
24
|
) -> None:
|
25
25
|
"""Instantiate a new Category object."""
|
26
26
|
self.NAME = name
|
27
|
-
match = re.search(r"\d
|
28
|
-
|
27
|
+
match = re.search(r"\d+", str(self.NAME))
|
28
|
+
n = int(match.group(0)) if match else 99
|
29
|
+
self.n: int = n if n <= 99 and n >= 0 else 99
|
29
30
|
self.desc = str(description)
|
30
31
|
self.dataframe: pd.DataFrame = dataframe
|
31
32
|
self.settings: dict[str, float | str] = settings if settings else {}
|
@@ -52,7 +53,7 @@ class Category:
|
|
52
53
|
@property
|
53
54
|
def questions(self) -> dict:
|
54
55
|
if not hasattr(self, "_questions"):
|
55
|
-
msg = "Category
|
56
|
+
msg = f"Category {self.id} doesn't contain any valid questions."
|
56
57
|
raise ValueError(msg)
|
57
58
|
return self._questions
|
58
59
|
|
@@ -105,25 +105,20 @@ class QuestionDB:
|
|
105
105
|
When there is no 'seetings' worksheet in the file.
|
106
106
|
InvalidFieldException
|
107
107
|
When the settings are invalid
|
108
|
+
Or When the categories Sheet doesn't provide the necessary keys.
|
108
109
|
|
109
110
|
Before raising it logges the exceptions with a meaningful message.
|
110
111
|
|
111
112
|
"""
|
112
113
|
sheetPath = sheetPath if sheetPath else self.spreadsheet
|
113
114
|
logger.info("Start Parsing the Excel Metadata Sheet\n")
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
engine="calamine",
|
121
|
-
)
|
122
|
-
except ValueError:
|
123
|
-
logger.exception(
|
124
|
-
"Did you forget to specify a 'settings' sheet in the file?"
|
115
|
+
with Path(sheetPath).open("rb") as f:
|
116
|
+
settingDf = pd.read_excel(
|
117
|
+
f,
|
118
|
+
sheet_name="settings",
|
119
|
+
index_col=0,
|
120
|
+
engine="calamine",
|
125
121
|
)
|
126
|
-
raise
|
127
122
|
logger.debug("Found the settings: \n\t%s", settingDf)
|
128
123
|
settingDf = self.harmonizeDFIndex(settingDf)
|
129
124
|
for tag, value in settingDf.iterrows():
|
@@ -131,13 +126,7 @@ class QuestionDB:
|
|
131
126
|
if pd.notna(val):
|
132
127
|
self.settings.set(tag, val)
|
133
128
|
|
134
|
-
|
135
|
-
self._validateProjectSettings(sheetPath=sheetPath)
|
136
|
-
except InvalidFieldException:
|
137
|
-
logger.exception(
|
138
|
-
"Can not create the database with invalid project settings."
|
139
|
-
)
|
140
|
-
raise
|
129
|
+
self._validateProjectSettings(sheetPath=sheetPath)
|
141
130
|
with Path(sheetPath).open("rb") as f:
|
142
131
|
self.categoriesMetaData = pd.read_excel(
|
143
132
|
f,
|
@@ -145,10 +134,20 @@ class QuestionDB:
|
|
145
134
|
index_col=0,
|
146
135
|
engine="calamine",
|
147
136
|
)
|
148
|
-
|
137
|
+
if "description" not in self.categoriesMetaData.columns:
|
138
|
+
msg = f"You need to specify the 'description' tag for each category in the sheet '{self.settings.get(Tags.CATEGORIESSHEET)}'."
|
139
|
+
raise InvalidFieldException(msg, "0000", "description")
|
140
|
+
logger.info("Sucessfully read categoriesMetaData")
|
149
141
|
return self.categoriesMetaData
|
150
142
|
|
151
143
|
def _validateProjectSettings(self, sheetPath: Path) -> None:
|
144
|
+
if Tags.LOGLEVEL in self.settings:
|
145
|
+
level: str = self.settings.get(Tags.LOGLEVEL)
|
146
|
+
if level.upper() not in ("DEBUG", "INFO", "WARNING", "ERROR"):
|
147
|
+
self.settings.pop(Tags.LOGLEVEL)
|
148
|
+
logger.warning("You specified an unsupported Loglevel: %s", level)
|
149
|
+
if self.window is not None:
|
150
|
+
self.window.logHandler.setLevel(self.settings.get(Tags.LOGLEVEL).upper())
|
152
151
|
if Tags.IMPORTMODULE in self.settings:
|
153
152
|
logger.warning(
|
154
153
|
"Appending: %s to sys.path. All names defined by it will be usable",
|
@@ -297,7 +296,7 @@ class QuestionDB:
|
|
297
296
|
for qNum in category.dataframe.columns:
|
298
297
|
try:
|
299
298
|
self.setupAndParseQuestion(category, qNum)
|
300
|
-
except (InvalidFieldException, QNotParsedException):
|
299
|
+
except (InvalidFieldException, QNotParsedException, AttributeError):
|
301
300
|
logger.exception(
|
302
301
|
"Question %s%02d couldn't be parsed. The Question Data: \n %s",
|
303
302
|
category.id,
|
@@ -379,10 +378,10 @@ class QuestionDB:
|
|
379
378
|
catdict: dict[Category, list[Question]] = {}
|
380
379
|
for q in questions:
|
381
380
|
logger.debug(f"got a question to append {q=}")
|
382
|
-
cat = q.parent().
|
381
|
+
cat = q.parent().category
|
383
382
|
if cat not in catdict:
|
384
383
|
catdict[cat] = []
|
385
|
-
catdict[cat].append(q.
|
384
|
+
catdict[cat].append(q.question)
|
386
385
|
for cat, qlist in catdict.items():
|
387
386
|
self._appendQElements(
|
388
387
|
cat,
|
@@ -423,6 +422,8 @@ class QuestionDB:
|
|
423
422
|
else:
|
424
423
|
logger.warning("Keine Fragenvariante wurde gewählt.")
|
425
424
|
tree.append(q.getUpdatedElement(variant=variant))
|
425
|
+
else:
|
426
|
+
tree.append(q.getUpdatedElement(variant=variant))
|
426
427
|
self._exportedQuestions.append(q)
|
427
428
|
|
428
429
|
def generateExportReport(
|
excel2moodle/core/settings.py
CHANGED
@@ -107,6 +107,10 @@ class Settings:
|
|
107
107
|
def clear(cls) -> None:
|
108
108
|
cls.values.clear()
|
109
109
|
|
110
|
+
@classmethod
|
111
|
+
def pop(cls, key: str):
|
112
|
+
return cls.values.pop(key)
|
113
|
+
|
110
114
|
@overload
|
111
115
|
@classmethod
|
112
116
|
def get(
|
@@ -160,7 +164,7 @@ class Settings:
|
|
160
164
|
default = key.default
|
161
165
|
if default is None:
|
162
166
|
return None
|
163
|
-
logger.
|
167
|
+
logger.debug("Returning the default value for %s", key)
|
164
168
|
return default
|
165
169
|
if key.typ() is Path:
|
166
170
|
path: Path = Path(raw)
|
@@ -0,0 +1,48 @@
|
|
1
|
+
"""This module provides functions to query the GitLab API for project information."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import sys
|
5
|
+
import urllib.request
|
6
|
+
|
7
|
+
|
8
|
+
def get_latest_tag(project_id: str) -> str | None:
|
9
|
+
"""Queries the GitLab API for the latest tag of a project.
|
10
|
+
|
11
|
+
Args:
|
12
|
+
project_id: The URL-encoded project ID (e.g., 'jbosse3%2Fexcel2moodle').
|
13
|
+
|
14
|
+
Returns:
|
15
|
+
The name of the latest tag.
|
16
|
+
|
17
|
+
"""
|
18
|
+
url = f"https://gitlab.com/api/v4/projects/{project_id}/repository/tags"
|
19
|
+
try:
|
20
|
+
with urllib.request.urlopen(url) as response:
|
21
|
+
if response.status == 200:
|
22
|
+
data = json.loads(response.read().decode())
|
23
|
+
if data:
|
24
|
+
return data[0]["name"]
|
25
|
+
except urllib.error.URLError as e:
|
26
|
+
print(f"Error fetching latest tag: {e}", file=sys.stderr)
|
27
|
+
return None
|
28
|
+
|
29
|
+
|
30
|
+
def get_changelog(project_id: str, branch: str = "master") -> str:
|
31
|
+
"""Queries the GitLab API for the content of the CHANGELOG.md file.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
project_id: The URL-encoded project ID (e.g., 'jbosse3%2Fexcel2moodle').
|
35
|
+
branch: The branch to get the file from.
|
36
|
+
|
37
|
+
Returns:
|
38
|
+
The content of the CHANGELOG.md file.
|
39
|
+
|
40
|
+
"""
|
41
|
+
url = f"https://gitlab.com/api/v4/projects/{project_id}/repository/files/CHANGELOG.md/raw?ref={branch}"
|
42
|
+
try:
|
43
|
+
with urllib.request.urlopen(url) as response:
|
44
|
+
if response.status == 200:
|
45
|
+
return response.read().decode()
|
46
|
+
except urllib.error.URLError as e:
|
47
|
+
print(f"Error fetching changelog: {e}", file=sys.stderr)
|
48
|
+
return ""
|
@@ -2,9 +2,11 @@ import logging
|
|
2
2
|
import random
|
3
3
|
|
4
4
|
from asteval import Interpreter
|
5
|
+
from PySide6.QtCore import Qt, Slot
|
5
6
|
from PySide6.QtWidgets import (
|
6
7
|
QDialog,
|
7
8
|
QLineEdit,
|
9
|
+
QListWidgetItem,
|
8
10
|
QMainWindow,
|
9
11
|
QMessageBox,
|
10
12
|
QTableWidget,
|
@@ -52,7 +54,7 @@ class VariableGeneratorDialog(QDialog):
|
|
52
54
|
# Add QLineEdit for Min, Max, and Decimal Places
|
53
55
|
min_le = QLineEdit(str(min(values)) if values else "0")
|
54
56
|
max_le = QLineEdit(str(max(values)) if values else "100")
|
55
|
-
dec_le = QLineEdit("
|
57
|
+
dec_le = QLineEdit("1") # Default to 0 decimal places
|
56
58
|
|
57
59
|
self.ui.tableWidget_variables.setCellWidget(row, 1, min_le)
|
58
60
|
self.ui.tableWidget_variables.setCellWidget(row, 2, max_le)
|
@@ -67,11 +69,20 @@ class VariableGeneratorDialog(QDialog):
|
|
67
69
|
self.ui.groupBox_existing_variables.toggled.connect(
|
68
70
|
self.ui.tableWidget_existing_variables.setVisible
|
69
71
|
)
|
72
|
+
self.ui.listWidget_rules.itemDoubleClicked.connect(self._edit_rule)
|
73
|
+
|
74
|
+
@Slot(QListWidgetItem)
|
75
|
+
def _edit_rule(self, item) -> None:
|
76
|
+
"""Move the double-clicked rule into the line edit and remove it from the list."""
|
77
|
+
self.ui.lineEdit_newRule.setText(item.text())
|
78
|
+
self.ui.listWidget_rules.takeItem(self.ui.listWidget_rules.row(item))
|
70
79
|
|
71
80
|
def _add_rule(self) -> None:
|
72
81
|
rule_text = self.ui.lineEdit_newRule.text().strip()
|
73
82
|
if rule_text:
|
74
|
-
|
83
|
+
# Check if the rule already exists. If so, do nothing.
|
84
|
+
if not self.ui.listWidget_rules.findItems(rule_text, Qt.MatchExactly):
|
85
|
+
self.ui.listWidget_rules.addItem(rule_text)
|
75
86
|
self.ui.lineEdit_newRule.clear()
|
76
87
|
|
77
88
|
def _remove_rule(self) -> None:
|
@@ -100,40 +111,70 @@ class VariableGeneratorDialog(QDialog):
|
|
100
111
|
|
101
112
|
num_sets = self.ui.spinBox_numSets.value()
|
102
113
|
|
114
|
+
# Build a set of existing variable combinations to ensure we don't generate duplicates of them.
|
115
|
+
unique_sets_tracker = set()
|
116
|
+
if self.origParametrics.variables:
|
117
|
+
var_names = list(self.origParametrics.variables.keys())
|
118
|
+
if var_names:
|
119
|
+
# Assuming all variable lists have the same length
|
120
|
+
num_variants = len(self.origParametrics.variables[var_names[0]])
|
121
|
+
for i in range(num_variants):
|
122
|
+
existing_set = {
|
123
|
+
var: self.origParametrics.variables[var][i] for var in var_names
|
124
|
+
}
|
125
|
+
unique_sets_tracker.add(frozenset(existing_set.items()))
|
126
|
+
|
127
|
+
generated_sets = [] # This will be a list of dicts
|
103
128
|
try:
|
104
|
-
generated_sets
|
105
|
-
self._findSet(varConstraints, rules
|
106
|
-
|
129
|
+
while len(generated_sets) < num_sets:
|
130
|
+
new_set = self._findSet(varConstraints, rules, unique_sets_tracker)
|
131
|
+
generated_sets.append(new_set)
|
132
|
+
unique_sets_tracker.add(frozenset(new_set.items()))
|
133
|
+
|
107
134
|
except IndexError as e:
|
108
135
|
logger.exception("Invalid variables in Rule:")
|
109
136
|
QMessageBox.critical(self, "Rule Error", f"{e}")
|
137
|
+
return # Stop generation
|
110
138
|
except ValueError as e:
|
111
|
-
logger.warning("
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
139
|
+
logger.warning("Failed to generate a new unique set: %s", e)
|
140
|
+
if len(generated_sets) < num_sets:
|
141
|
+
QMessageBox.warning(
|
142
|
+
self,
|
143
|
+
"Generation Incomplete",
|
144
|
+
f"Could only generate {len(generated_sets)} unique sets out of the requested {num_sets}. "
|
145
|
+
"The space of possible unique combinations may be exhausted.",
|
146
|
+
)
|
147
|
+
|
148
|
+
if not generated_sets:
|
149
|
+
logger.info("No new variable sets were generated.")
|
150
|
+
if not self._rule_error_occurred:
|
151
|
+
QMessageBox.information(
|
152
|
+
self,
|
153
|
+
"No Sets Generated",
|
154
|
+
"No new unique variable sets could be generated with the given constraints and rules.",
|
155
|
+
)
|
156
|
+
return
|
157
|
+
|
158
|
+
# convert the generated_sets list[dict[str, float]] into dict[str, list[float]]
|
159
|
+
# [{A:7, B:8}, {A:11, B:9}] -> {A: [7, 11], B: [8, 9]}
|
160
|
+
newVariables = {}
|
161
|
+
for var in self.origParametrics.variables:
|
162
|
+
newVariables[var] = [dataSet[var] for dataSet in generated_sets]
|
163
|
+
self._generatedParametrics.variables = newVariables
|
164
|
+
self.ui.groupBox_generated_variables.show()
|
165
|
+
populateDataSetTable(
|
166
|
+
self.ui.tableWidget_generated_variables,
|
167
|
+
parametrics=self._generatedParametrics,
|
168
|
+
)
|
129
169
|
|
130
170
|
def _findSet(
|
131
171
|
self,
|
132
172
|
constraints: dict[str, dict[str, float | int]],
|
133
173
|
rules: list[str],
|
174
|
+
existing_sets: set[frozenset],
|
134
175
|
maxAttempts: int = 1000,
|
135
176
|
) -> dict[str, float]:
|
136
|
-
"""Generate
|
177
|
+
"""Generate a random set of variables that satisfies the rules and is not in existing_sets.
|
137
178
|
|
138
179
|
Raises
|
139
180
|
------
|
@@ -143,6 +184,7 @@ class VariableGeneratorDialog(QDialog):
|
|
143
184
|
"""
|
144
185
|
attempts = 0
|
145
186
|
while attempts < maxAttempts:
|
187
|
+
attempts += 1
|
146
188
|
current_set: dict[str, float] = {}
|
147
189
|
# Generate initial values based on min/max constraints
|
148
190
|
for var_name, constr in constraints.items():
|
@@ -158,11 +200,15 @@ class VariableGeneratorDialog(QDialog):
|
|
158
200
|
current_set[var_name] = round(
|
159
201
|
random.uniform(min_val, max_val), dec_places
|
160
202
|
)
|
203
|
+
|
204
|
+
# Check for uniqueness first, as it's a cheaper check than evaluating rules.
|
205
|
+
if frozenset(current_set.items()) in existing_sets:
|
206
|
+
continue # It's a duplicate, try again.
|
207
|
+
|
161
208
|
if self._check_rules(current_set, rules):
|
162
|
-
logger.info("Found matching set after %s
|
209
|
+
logger.info("Found matching unique set after %s attempts", attempts)
|
163
210
|
return current_set
|
164
|
-
|
165
|
-
msg = f"Could not generate a valid set after {maxAttempts} attempts."
|
211
|
+
msg = f"Could not generate a valid unique set after {maxAttempts} attempts."
|
166
212
|
raise ValueError(msg)
|
167
213
|
|
168
214
|
def _check_rules(
|
@@ -226,25 +272,3 @@ def populateDataSetTable(
|
|
226
272
|
)
|
227
273
|
tableWidget.resizeColumnsToContents()
|
228
274
|
|
229
|
-
|
230
|
-
# This part is for testing the UI independently
|
231
|
-
if __name__ == "__main__":
|
232
|
-
import sys
|
233
|
-
|
234
|
-
from PySide6.QtWidgets import QApplication
|
235
|
-
|
236
|
-
# Mock ParametricQuestion for testing
|
237
|
-
class MockParametricQuestion:
|
238
|
-
def __init__(self) -> None:
|
239
|
-
self.origParametrics.variables = {
|
240
|
-
"a": [1.0, 2.0, 3.0],
|
241
|
-
"b": [10, 20, 30],
|
242
|
-
"c": [0.5, 1.5, 2.5],
|
243
|
-
}
|
244
|
-
|
245
|
-
app = QApplication(sys.argv)
|
246
|
-
mock_question = MockParametricQuestion()
|
247
|
-
dialog = VariableGeneratorDialog(paramQuestion=mock_question)
|
248
|
-
if dialog.exec():
|
249
|
-
print("Generated Sets:", dialog.generatedVarSets())
|
250
|
-
sys.exit(app.exec())
|
@@ -228,7 +228,7 @@ class ClozeQuestion(ParametricQuestion):
|
|
228
228
|
pts: int = 0
|
229
229
|
if not self.isParsed:
|
230
230
|
msg = "The Cloze question has no points because it is not yet parsed"
|
231
|
-
logger.warning(msg)
|
231
|
+
self.logger.warning(msg)
|
232
232
|
return pts
|
233
233
|
for p in self.questionParts.values():
|
234
234
|
pts = pts + p.points
|
@@ -315,7 +315,7 @@ class ClozeQuestionParser(NFMQuestionParser):
|
|
315
315
|
for part in parts.values():
|
316
316
|
part.points = point
|
317
317
|
else:
|
318
|
-
|
318
|
+
loclogger.warning(
|
319
319
|
"Some Answer parts are missing the points, they will get the standard points"
|
320
320
|
)
|
321
321
|
for num, part in parts.items():
|
@@ -328,7 +328,7 @@ class ClozeQuestionParser(NFMQuestionParser):
|
|
328
328
|
points = self.rawInput.get(Tags.POINTS)
|
329
329
|
corrPoints: int = round(points)
|
330
330
|
if not math.isclose(corrPoints, points):
|
331
|
-
logger.warning(
|
331
|
+
self.logger.warning(
|
332
332
|
"Type cloze supports only integers as points. %s was round to %s",
|
333
333
|
points,
|
334
334
|
corrPoints,
|
@@ -367,8 +367,7 @@ class ClozeQuestionParser(NFMQuestionParser):
|
|
367
367
|
result=result,
|
368
368
|
points=part.points,
|
369
369
|
)
|
370
|
-
self.logger.
|
371
|
-
logger.debug("Appended NF part %s result", partNum)
|
370
|
+
self.logger.debug("Generated %s answer part: %s ", partNum, ansStr)
|
372
371
|
elif part.typ == "MC":
|
373
372
|
ansStr = ClozePart.getMCAnsStr(
|
374
373
|
part.trueAnswers,
|
@@ -376,7 +375,7 @@ class ClozeQuestionParser(NFMQuestionParser):
|
|
376
375
|
points=part.points,
|
377
376
|
)
|
378
377
|
part.mcAnswerString = ansStr
|
379
|
-
logger.debug("Appended MC part %s: %s", partNum, ansStr)
|
378
|
+
self.logger.debug("Appended MC part %s: %s", partNum, ansStr)
|
380
379
|
else:
|
381
380
|
msg = "Type of the answer part is invalid"
|
382
381
|
raise QNotParsedException(msg, self.id)
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
################################################################################
|
4
|
+
## Form generated from reading UI file 'UI_updateDlg.ui'
|
5
|
+
##
|
6
|
+
## Created by: Qt User Interface Compiler version 6.9.1
|
7
|
+
##
|
8
|
+
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
9
|
+
################################################################################
|
10
|
+
|
11
|
+
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
12
|
+
QMetaObject, QObject, QPoint, QRect,
|
13
|
+
QSize, QTime, QUrl, Qt)
|
14
|
+
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
15
|
+
QFont, QFontDatabase, QGradient, QIcon,
|
16
|
+
QImage, QKeySequence, QLinearGradient, QPainter,
|
17
|
+
QPalette, QPixmap, QRadialGradient, QTransform)
|
18
|
+
from PySide6.QtWidgets import (QAbstractButton, QAbstractScrollArea, QApplication, QDialog,
|
19
|
+
QDialogButtonBox, QFrame, QLabel, QSizePolicy,
|
20
|
+
QTextBrowser, QVBoxLayout, QWidget)
|
21
|
+
|
22
|
+
class Ui_UpdateDialog(object):
|
23
|
+
def setupUi(self, UpdateDialog):
|
24
|
+
if not UpdateDialog.objectName():
|
25
|
+
UpdateDialog.setObjectName(u"UpdateDialog")
|
26
|
+
UpdateDialog.setWindowModality(Qt.WindowModality.NonModal)
|
27
|
+
UpdateDialog.resize(534, 512)
|
28
|
+
UpdateDialog.setModal(True)
|
29
|
+
self.verticalLayout = QVBoxLayout(UpdateDialog)
|
30
|
+
self.verticalLayout.setObjectName(u"verticalLayout")
|
31
|
+
self.titleLabel = QLabel(UpdateDialog)
|
32
|
+
self.titleLabel.setObjectName(u"titleLabel")
|
33
|
+
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
34
|
+
sizePolicy.setHorizontalStretch(0)
|
35
|
+
sizePolicy.setVerticalStretch(0)
|
36
|
+
sizePolicy.setHeightForWidth(self.titleLabel.sizePolicy().hasHeightForWidth())
|
37
|
+
self.titleLabel.setSizePolicy(sizePolicy)
|
38
|
+
|
39
|
+
self.verticalLayout.addWidget(self.titleLabel)
|
40
|
+
|
41
|
+
self.fundingLabel = QLabel(UpdateDialog)
|
42
|
+
self.fundingLabel.setObjectName(u"fundingLabel")
|
43
|
+
sizePolicy.setHeightForWidth(self.fundingLabel.sizePolicy().hasHeightForWidth())
|
44
|
+
self.fundingLabel.setSizePolicy(sizePolicy)
|
45
|
+
self.fundingLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
46
|
+
self.fundingLabel.setOpenExternalLinks(True)
|
47
|
+
|
48
|
+
self.verticalLayout.addWidget(self.fundingLabel)
|
49
|
+
|
50
|
+
self.line = QFrame(UpdateDialog)
|
51
|
+
self.line.setObjectName(u"line")
|
52
|
+
self.line.setFrameShape(QFrame.Shape.HLine)
|
53
|
+
self.line.setFrameShadow(QFrame.Shadow.Sunken)
|
54
|
+
|
55
|
+
self.verticalLayout.addWidget(self.line)
|
56
|
+
|
57
|
+
self.changelogLabel = QLabel(UpdateDialog)
|
58
|
+
self.changelogLabel.setObjectName(u"changelogLabel")
|
59
|
+
sizePolicy.setHeightForWidth(self.changelogLabel.sizePolicy().hasHeightForWidth())
|
60
|
+
self.changelogLabel.setSizePolicy(sizePolicy)
|
61
|
+
|
62
|
+
self.verticalLayout.addWidget(self.changelogLabel)
|
63
|
+
|
64
|
+
self.changelogBrowser = QTextBrowser(UpdateDialog)
|
65
|
+
self.changelogBrowser.setObjectName(u"changelogBrowser")
|
66
|
+
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
67
|
+
sizePolicy1.setHorizontalStretch(0)
|
68
|
+
sizePolicy1.setVerticalStretch(0)
|
69
|
+
sizePolicy1.setHeightForWidth(self.changelogBrowser.sizePolicy().hasHeightForWidth())
|
70
|
+
self.changelogBrowser.setSizePolicy(sizePolicy1)
|
71
|
+
self.changelogBrowser.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
72
|
+
self.changelogBrowser.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
73
|
+
self.changelogBrowser.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents)
|
74
|
+
|
75
|
+
self.verticalLayout.addWidget(self.changelogBrowser)
|
76
|
+
|
77
|
+
self.label = QLabel(UpdateDialog)
|
78
|
+
self.label.setObjectName(u"label")
|
79
|
+
sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
|
80
|
+
self.label.setSizePolicy(sizePolicy)
|
81
|
+
|
82
|
+
self.verticalLayout.addWidget(self.label)
|
83
|
+
|
84
|
+
self.buttonBox = QDialogButtonBox(UpdateDialog)
|
85
|
+
self.buttonBox.setObjectName(u"buttonBox")
|
86
|
+
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
|
87
|
+
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Ok)
|
88
|
+
|
89
|
+
self.verticalLayout.addWidget(self.buttonBox)
|
90
|
+
|
91
|
+
|
92
|
+
self.retranslateUi(UpdateDialog)
|
93
|
+
self.buttonBox.accepted.connect(UpdateDialog.accept)
|
94
|
+
self.buttonBox.rejected.connect(UpdateDialog.reject)
|
95
|
+
|
96
|
+
QMetaObject.connectSlotsByName(UpdateDialog)
|
97
|
+
# setupUi
|
98
|
+
|
99
|
+
def retranslateUi(self, UpdateDialog):
|
100
|
+
UpdateDialog.setWindowTitle(QCoreApplication.translate("UpdateDialog", u"Dialog", None))
|
101
|
+
self.titleLabel.setText(QCoreApplication.translate("UpdateDialog", u"<h2>A new <i>excel2moodle</i> version is available!</h2>", None))
|
102
|
+
self.fundingLabel.setText(QCoreApplication.translate("UpdateDialog", u"If you find this project useful, please consider supporting its development.", None))
|
103
|
+
self.changelogLabel.setText(QCoreApplication.translate("UpdateDialog", u"<h3>Changelog:</h3>", None))
|
104
|
+
self.label.setText(QCoreApplication.translate("UpdateDialog", u"To install the update run: 'uv tool upgrade excel2moodle'", None))
|
105
|
+
# retranslateUi
|
106
|
+
|
excel2moodle/ui/appUi.py
CHANGED
@@ -21,6 +21,7 @@ from PySide6.QtWidgets import (
|
|
21
21
|
from excel2moodle import e2mMetadata, mainLogger
|
22
22
|
from excel2moodle.core.category import Category
|
23
23
|
from excel2moodle.core.dataStructure import QuestionDB
|
24
|
+
from excel2moodle.core.exceptions import InvalidFieldException
|
24
25
|
from excel2moodle.core.question import ParametricQuestion
|
25
26
|
from excel2moodle.core.settings import Settings, Tags
|
26
27
|
from excel2moodle.extra.variableGenerator import VariableGeneratorDialog
|
@@ -34,15 +35,14 @@ from excel2moodle.ui.UI_mainWindow import Ui_MoodleTestGenerator
|
|
34
35
|
|
35
36
|
logger = logging.getLogger(__name__)
|
36
37
|
|
37
|
-
loggerSignal = LogWindowHandler()
|
38
|
-
mainLogger.addHandler(loggerSignal)
|
39
|
-
|
40
38
|
|
41
39
|
class MainWindow(QMainWindow):
|
42
40
|
def __init__(self, settings: Settings, testDB: QuestionDB) -> None:
|
43
41
|
super().__init__()
|
44
42
|
self.settings = settings
|
45
43
|
self.qSettings = QSettings("jbosse3", "excel2moodle")
|
44
|
+
self.logHandler = LogWindowHandler()
|
45
|
+
mainLogger.addHandler(self.logHandler)
|
46
46
|
logger.info("Settings are stored under: %s", self.qSettings.fileName())
|
47
47
|
|
48
48
|
self.excelPath: Path | None = None
|
@@ -93,7 +93,7 @@ class MainWindow(QMainWindow):
|
|
93
93
|
self.ui.checkBoxQuestionListSelectAll.checkStateChanged.connect(
|
94
94
|
self.toggleQuestionSelectionState,
|
95
95
|
)
|
96
|
-
|
96
|
+
self.logHandler.emitter.signal.connect(self.updateLog)
|
97
97
|
self.ui.actionEquationChecker.triggered.connect(self.openEqCheckerDlg)
|
98
98
|
self.ui.actionParseAll.triggered.connect(self.parseSpreadsheetAll)
|
99
99
|
self.testDB.signals.categoryQuestionsReady.connect(self.treeRefreshCategory)
|
@@ -110,6 +110,10 @@ class MainWindow(QMainWindow):
|
|
110
110
|
self.openSpreadsheetExternally
|
111
111
|
)
|
112
112
|
|
113
|
+
def showUpdateDialog(self, changelog, version) -> None:
|
114
|
+
dialog = dialogs.UpdateDialog(self, changelog=changelog, version=version)
|
115
|
+
dialog.exec()
|
116
|
+
|
113
117
|
@Slot()
|
114
118
|
def parseSpreadsheetAll(self) -> None:
|
115
119
|
"""Event triggered by the *Tools/Parse all Questions* Event.
|
@@ -159,7 +163,7 @@ class MainWindow(QMainWindow):
|
|
159
163
|
selection = self.ui.treeWidget.selectedItems()
|
160
164
|
for q in selection:
|
161
165
|
questions += 1
|
162
|
-
count += q.
|
166
|
+
count += q.question.points
|
163
167
|
self.ui.pointCounter.setValue(count)
|
164
168
|
self.ui.questionCounter.setValue(questions)
|
165
169
|
if self.eqChecker.isVisible():
|
@@ -221,18 +225,33 @@ class MainWindow(QMainWindow):
|
|
221
225
|
|
222
226
|
@Slot(Category)
|
223
227
|
def treeRefreshCategory(self, cat: Category) -> None:
|
224
|
-
"""Append Category with its Questions to the treewidget.
|
228
|
+
"""Append Category with its Questions to the treewidget.
|
229
|
+
|
230
|
+
If the category already has an item, refresh it.
|
231
|
+
"""
|
232
|
+
# Find existing item
|
233
|
+
for i in range(self.ui.treeWidget.topLevelItemCount()):
|
234
|
+
item = self.ui.treeWidget.topLevelItem(i)
|
235
|
+
# The top level items are categories
|
236
|
+
if isinstance(item, CategoryItem) and item.category.NAME == cat.NAME:
|
237
|
+
item.refresh()
|
238
|
+
return
|
239
|
+
|
225
240
|
catItem = CategoryItem(self.ui.treeWidget, cat)
|
226
241
|
catItem.setFlags(catItem.flags() & ~Qt.ItemIsSelectable)
|
227
|
-
|
228
|
-
|
242
|
+
try:
|
243
|
+
for q in cat.questions.values():
|
244
|
+
QuestionItem(catItem, q)
|
245
|
+
except ValueError:
|
246
|
+
logger.exception("No Questions to update.")
|
247
|
+
catItem.updateVariantCount()
|
229
248
|
self.ui.treeWidget.sortItems(0, Qt.SortOrder.AscendingOrder)
|
230
249
|
|
231
250
|
@Slot()
|
232
251
|
def updateQuestionPreview(self) -> None:
|
233
252
|
item = self.ui.treeWidget.currentItem()
|
234
253
|
if isinstance(item, QuestionItem):
|
235
|
-
self.questionPreview.setupQuestion(item.
|
254
|
+
self.questionPreview.setupQuestion(item.question)
|
236
255
|
else:
|
237
256
|
logger.info("current Item is not a Question, can't preview")
|
238
257
|
|
@@ -244,12 +263,12 @@ class MainWindow(QMainWindow):
|
|
244
263
|
def openEqCheckerDlg(self) -> None:
|
245
264
|
item = self.ui.treeWidget.currentItem()
|
246
265
|
if isinstance(item, QuestionItem):
|
247
|
-
question = item.
|
266
|
+
question = item.question
|
248
267
|
if isinstance(question, (NFQuestion, MCQuestion)):
|
249
268
|
logger.debug("Can't check an MC or NF Question")
|
250
269
|
else:
|
251
270
|
logger.debug("opening wEquationChecker \n")
|
252
|
-
self.eqChecker.setup(item.
|
271
|
+
self.eqChecker.setup(item.question)
|
253
272
|
self.eqChecker.show()
|
254
273
|
else:
|
255
274
|
logger.debug("No Question Item selected: %s", type(item))
|
@@ -269,11 +288,12 @@ class MainWindow(QMainWindow):
|
|
269
288
|
def openVariableGeneratorDlg(self) -> None:
|
270
289
|
item = self.ui.treeWidget.currentItem()
|
271
290
|
if isinstance(item, QuestionItem):
|
272
|
-
question = item.
|
291
|
+
question = item.question
|
273
292
|
if isinstance(question, ParametricQuestion):
|
274
293
|
dialog = VariableGeneratorDialog(self, parametrics=question.parametrics)
|
275
294
|
if dialog.exec():
|
276
295
|
self.questionPreview.setupQuestion(question)
|
296
|
+
self.treeRefreshCategory(question.category)
|
277
297
|
logger.info("Updated QuestionItem display for %s", question.id)
|
278
298
|
self.copyVariablesToClipboard(
|
279
299
|
variables=question.parametrics.variables
|
@@ -294,7 +314,7 @@ class MainWindow(QMainWindow):
|
|
294
314
|
if not variables:
|
295
315
|
item = self.ui.treeWidget.currentItem()
|
296
316
|
if isinstance(item, QuestionItem):
|
297
|
-
question = item.
|
317
|
+
question = item.question
|
298
318
|
if isinstance(question, ParametricQuestion):
|
299
319
|
variables = question.parametrics.variables
|
300
320
|
varsList = [
|
@@ -335,7 +355,15 @@ class ParseAllThread(QRunnable):
|
|
335
355
|
|
336
356
|
@Slot()
|
337
357
|
def run(self) -> None:
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
358
|
+
try:
|
359
|
+
self.testDB.readCategoriesMetadata()
|
360
|
+
except InvalidFieldException:
|
361
|
+
logger.exception("Youre spreadsheet questionbank isn't correctly setup.")
|
362
|
+
except ValueError:
|
363
|
+
logger.exception(
|
364
|
+
"Did you forget to specify a 'settings' sheet in the file?"
|
365
|
+
)
|
366
|
+
else:
|
367
|
+
self.testDB.asyncInitAllCategories(self.mainApp.excelPath)
|
368
|
+
self.mainApp.setStatus("[OK] Tabellen wurde eingelesen")
|
369
|
+
self.testDB.parseAllQuestions()
|
excel2moodle/ui/dialogs.py
CHANGED
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
|
|
6
6
|
|
7
7
|
import lxml.etree as ET
|
8
8
|
from PySide6.QtCore import Slot
|
9
|
-
from PySide6.QtWidgets import QDialog, QFileDialog, QMessageBox, QWidget
|
9
|
+
from PySide6.QtWidgets import QDialog, QFileDialog, QMainWindow, QMessageBox, QWidget
|
10
10
|
|
11
11
|
from excel2moodle import e2mMetadata
|
12
12
|
from excel2moodle.core.globals import XMLTags
|
@@ -14,6 +14,7 @@ from excel2moodle.core.question import ParametricQuestion, Question
|
|
14
14
|
from excel2moodle.core.settings import Tags
|
15
15
|
from excel2moodle.extra import variableGenerator
|
16
16
|
from excel2moodle.ui.UI_exportSettingsDialog import Ui_ExportDialog
|
17
|
+
from excel2moodle.ui.UI_updateDlg import Ui_UpdateDialog
|
17
18
|
from excel2moodle.ui.UI_variantDialog import Ui_Dialog
|
18
19
|
|
19
20
|
if TYPE_CHECKING:
|
@@ -22,6 +23,22 @@ if TYPE_CHECKING:
|
|
22
23
|
logger = logging.getLogger(__name__)
|
23
24
|
|
24
25
|
|
26
|
+
class UpdateDialog(QDialog):
|
27
|
+
def __init__(
|
28
|
+
self, parent: QMainWindow, changelog: str = "", version: str = ""
|
29
|
+
) -> None:
|
30
|
+
super().__init__(parent)
|
31
|
+
self.ui = Ui_UpdateDialog()
|
32
|
+
self.ui.setupUi(self)
|
33
|
+
self.ui.changelogBrowser.setMarkdown(changelog)
|
34
|
+
self.ui.titleLabel.setText(
|
35
|
+
f"<h2>New Version {version} of <i>exel2moodle</i> just dropped!!</h2>"
|
36
|
+
)
|
37
|
+
self.ui.fundingLabel.setText(
|
38
|
+
f'If you find this project useful, please consider supporting its development. <br> <a href="{e2mMetadata["funding"]}">Buy jbosse3 a coffee</a>, so he stays caffeinated during coding.',
|
39
|
+
)
|
40
|
+
|
41
|
+
|
25
42
|
class QuestionVariantDialog(QDialog):
|
26
43
|
def __init__(self, parent, question: ParametricQuestion) -> None:
|
27
44
|
super().__init__(parent)
|
excel2moodle/ui/treewidget.py
CHANGED
@@ -4,24 +4,33 @@ Those two are subclasses of `QTreeWidgetItem`, to provide an easy interface
|
|
4
4
|
of accessing the corresponding questions from the items.
|
5
5
|
"""
|
6
6
|
|
7
|
+
import logging
|
8
|
+
|
7
9
|
from PySide6.QtCore import Qt
|
8
10
|
from PySide6.QtWidgets import QTreeWidgetItem
|
9
11
|
|
10
12
|
from excel2moodle.core.dataStructure import Category
|
11
13
|
from excel2moodle.core.question import ParametricQuestion, Question
|
12
14
|
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
13
17
|
|
14
18
|
class QuestionItem(QTreeWidgetItem):
|
15
19
|
def __init__(self, parent, question: Question | ParametricQuestion) -> None:
|
16
20
|
super().__init__(parent)
|
17
21
|
self.setData(2, Qt.UserRole, question)
|
22
|
+
self.refresh()
|
23
|
+
|
24
|
+
def refresh(self) -> None:
|
25
|
+
question = self.question
|
18
26
|
self.setText(0, question.id)
|
19
27
|
self.setText(1, question.name)
|
20
28
|
self.setText(2, str(question.points))
|
21
|
-
if
|
22
|
-
self.setText(3, str(question.variants))
|
29
|
+
if isinstance(question, ParametricQuestion):
|
30
|
+
self.setText(3, str(question.parametrics.variants))
|
23
31
|
|
24
|
-
|
32
|
+
@property
|
33
|
+
def question(self) -> Question | ParametricQuestion:
|
25
34
|
"""Return the question Object the QTreeWidgetItem represents."""
|
26
35
|
return self.data(2, Qt.UserRole)
|
27
36
|
|
@@ -30,9 +39,9 @@ class CategoryItem(QTreeWidgetItem):
|
|
30
39
|
def __init__(self, parent, category: Category) -> None:
|
31
40
|
super().__init__(parent)
|
32
41
|
self.setData(2, Qt.UserRole, category)
|
33
|
-
self.
|
34
|
-
|
35
|
-
|
42
|
+
self.refresh()
|
43
|
+
|
44
|
+
def updateVariantCount(self) -> None:
|
36
45
|
var = self.getMaxVariants()
|
37
46
|
if var != 0:
|
38
47
|
self.setText(3, str(var))
|
@@ -44,10 +53,21 @@ class CategoryItem(QTreeWidgetItem):
|
|
44
53
|
def getMaxVariants(self) -> int:
|
45
54
|
count: int = 0
|
46
55
|
for child in self.iterateChildren():
|
47
|
-
q = child.
|
48
|
-
if
|
49
|
-
count = max(q.variants, count)
|
56
|
+
q = child.question
|
57
|
+
if isinstance(q, ParametricQuestion):
|
58
|
+
count = max(q.parametrics.variants, count)
|
50
59
|
return count
|
51
60
|
|
52
|
-
|
61
|
+
@property
|
62
|
+
def category(self) -> Category:
|
53
63
|
return self.data(2, Qt.UserRole)
|
64
|
+
|
65
|
+
def refresh(self) -> None:
|
66
|
+
for child in self.iterateChildren():
|
67
|
+
child.refresh()
|
68
|
+
# Update category data, as it might have changed
|
69
|
+
cat = self.category
|
70
|
+
self.setText(0, cat.NAME)
|
71
|
+
self.setText(1, cat.desc)
|
72
|
+
self.setText(2, str(cat.points))
|
73
|
+
self.updateVariantCount()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: excel2moodle
|
3
|
-
Version: 0.6.
|
3
|
+
Version: 0.6.3
|
4
4
|
Summary: A package for converting questions from a spreadsheet, to valid moodle-xml
|
5
5
|
Author: Jakob Bosse
|
6
6
|
License-Expression: GPL-3.0-or-later
|
@@ -90,6 +90,26 @@ If You want to support my work as well, you can by me a [coffee](https://ko-fi.c
|
|
90
90
|
|
91
91
|
# Changelogs
|
92
92
|
|
93
|
+
## 0.6.3 (2025-08-03)
|
94
|
+
Lots of small improvements made
|
95
|
+
|
96
|
+
### improvement (3 changes)
|
97
|
+
|
98
|
+
- [small logging improvements and error handling](https://gitlab.com/jbosse3/excel2moodle/-/commit/149f8e923a06d9d7077fe90c7005a3e1d5d2d42f)
|
99
|
+
- [Make variable generator rules editable](https://gitlab.com/jbosse3/excel2moodle/-/commit/80ea32d97bdec16b77100bc870a0e0272a739dd4)
|
100
|
+
- [Variable generator only generates unique sets.](https://gitlab.com/jbosse3/excel2moodle/-/commit/d347c91bbac66de1da157fee4f76faf8d4636557)
|
101
|
+
|
102
|
+
### bugfix (3 changes)
|
103
|
+
|
104
|
+
- [mixed parametric and non parametric Bullets are working now](https://gitlab.com/jbosse3/excel2moodle/-/commit/f094b13dffd4b6b7ac1a03fc7e34eec6e8d1bfa7)
|
105
|
+
- [Loglevel setting is respected in spreadsheet file](https://gitlab.com/jbosse3/excel2moodle/-/commit/d6ef89beeec94f24782a00b7564883074badf72d)
|
106
|
+
- [Treewidget variants count updated after variable generation](https://gitlab.com/jbosse3/excel2moodle/-/commit/c48a0d093a0cce85fd3e9c3c091eef936739c02b)
|
107
|
+
|
108
|
+
### feature (2 changes)
|
109
|
+
|
110
|
+
- [Category ID taken from any number in its name](https://gitlab.com/jbosse3/excel2moodle/-/commit/ac7e19af5f25ac2e576b63c478e7b07153e782ef)
|
111
|
+
- [Implemented Update Check on Startup](https://gitlab.com/jbosse3/excel2moodle/-/commit/a143edd47f566c5e731c05612f4ac21dc7728eb7)
|
112
|
+
|
93
113
|
## 0.6.2 (2025-08-02)
|
94
114
|
Adding export options and fixing cloze points bug
|
95
115
|
|
@@ -1,39 +1,41 @@
|
|
1
|
-
excel2moodle/__init__.py,sha256=
|
2
|
-
excel2moodle/__main__.py,sha256=
|
1
|
+
excel2moodle/__init__.py,sha256=W05Gsm3IOcxJnp4C-TPvxRiO3NR2L9g8PSIHDoRJua0,1893
|
2
|
+
excel2moodle/__main__.py,sha256=B55ZK25z-HzIIox2xLYkJXMUwYzITPKGCi9fELMFGaM,1877
|
3
3
|
excel2moodle/logger.py,sha256=fq8ZOkCI1wj38v8IyrZsUlpt16onlSH_phqbVvYUwBQ,3725
|
4
4
|
excel2moodle/core/__init__.py,sha256=87BwhtZse72Tk17Ib-V9X2k9wkhmtVnEj2ZmJ9JBAnI,63
|
5
|
-
excel2moodle/core/bullets.py,sha256=
|
6
|
-
excel2moodle/core/category.py,sha256=
|
7
|
-
excel2moodle/core/dataStructure.py,sha256=
|
5
|
+
excel2moodle/core/bullets.py,sha256=KwlxGOWbWexMkfXkY4Zg-gmxpzQCNZ09kSo8kPffGQU,3501
|
6
|
+
excel2moodle/core/category.py,sha256=fOMj2ynoAy6tXwmFhJ9uST9BQHiRJeU2BrkK1r57ek4,2897
|
7
|
+
excel2moodle/core/dataStructure.py,sha256=BzBKg_fCT1FHuJum2aGLK_4RqmA2qluODvg1GwiEA7E,19797
|
8
8
|
excel2moodle/core/etHelpers.py,sha256=LzimWGuX6RH2TbfEnWUoAXT2Tr0z6P7bCANjxuANSX0,1667
|
9
9
|
excel2moodle/core/exceptions.py,sha256=9xfsaIcm6Yej6QAZga0d3DK3jLQejdfgJARuAaG-uZY,739
|
10
10
|
excel2moodle/core/globals.py,sha256=gvkl8Obq4XBW40B1L68Ewg06sonK27l-KIiodwFv8ic,2393
|
11
11
|
excel2moodle/core/parser.py,sha256=wth3SPjlbWnS1zUVPap6PsJeVGCRatLuJZe6U7KgXBg,7540
|
12
12
|
excel2moodle/core/question.py,sha256=eylkSKYQYo0uRxzfTZqoQNFAXq94mzzxbRWyknstupg,14187
|
13
|
-
excel2moodle/core/settings.py,sha256=
|
13
|
+
excel2moodle/core/settings.py,sha256=zqtA1fcQeO3bELLvS8bzP9TSZT7RksFxMaqnYYBXQCQ,6432
|
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=oQpk-4cM0x_vKGEexC0Z1UyVoGG7w3V3RydtkM0MV_Y,3869
|
18
|
-
excel2moodle/extra/
|
18
|
+
excel2moodle/extra/updateQuery.py,sha256=kD_L23Qea9Cx4zUwfQVNJXXFbybd9cwE8sSbZrz7VF8,1554
|
19
|
+
excel2moodle/extra/variableGenerator.py,sha256=qYY9i872yJAE4_nrxy_M5jCdMtGgVKB7yz1Th31bySY,11486
|
19
20
|
excel2moodle/question_types/__init__.py,sha256=81mss0g7SVtnlb-WkydE28G_dEAAf6oT1uB8lpK2-II,1041
|
20
|
-
excel2moodle/question_types/cloze.py,sha256=
|
21
|
+
excel2moodle/question_types/cloze.py,sha256=xxhsqSNYqt878bjZ8MfGr-Oe7jLy9FdSTIUZ_GPE9PY,14657
|
21
22
|
excel2moodle/question_types/mc.py,sha256=nx6PsbfLLH_4H5eCSjGcfgEC6EEVgseI7xy15jg5JmA,5482
|
22
23
|
excel2moodle/question_types/nf.py,sha256=HAolGy13-FbLVJskAUXCFy76NJu91IG9wtq6OI05Igw,1374
|
23
24
|
excel2moodle/question_types/nfm.py,sha256=D5-aE4C7TAuwHFidXR15DLWNZ4JT-HVbPXI0CzGWOS0,3013
|
24
25
|
excel2moodle/ui/UI_equationChecker.py,sha256=evQDlqCHeooJcAnYjhFCyjlPhfknr7ULGKQwMmqQeJ4,8947
|
25
26
|
excel2moodle/ui/UI_exportSettingsDialog.py,sha256=I0Vqw2TCWoUhDKxTgLoGaAo4_L77vfN8G7_zi7b_5lY,8254
|
26
27
|
excel2moodle/ui/UI_mainWindow.py,sha256=9w8bRgOrVEX7BRGQvMuVhPCiSOsXYkMb4rxLDeRErII,21544
|
28
|
+
excel2moodle/ui/UI_updateDlg.py,sha256=uabFUsK0O6cRSzNcM84mPntuwdXZCNkqsRDJxAOxG4Q,5144
|
27
29
|
excel2moodle/ui/UI_variableGenerator.py,sha256=DjpZnBELSqyOjJdwjXNP2V71rCPm3tr6_XkNT9F3e34,11205
|
28
30
|
excel2moodle/ui/UI_variantDialog.py,sha256=snVaF3_YAc7NWjMRg7NzbjL_PzNbOpt4eiqElkE46io,5414
|
29
31
|
excel2moodle/ui/__init__.py,sha256=4EdGtpzwH3rgw4xW9E5x9kdPQYwKbo9rehHRZTNxCrQ,44
|
30
|
-
excel2moodle/ui/appUi.py,sha256=
|
31
|
-
excel2moodle/ui/dialogs.py,sha256=
|
32
|
+
excel2moodle/ui/appUi.py,sha256=y5SWTNo45tIZ-HekdZkR7fPOD0QSP66uB_wlrk-b4YM,15046
|
33
|
+
excel2moodle/ui/dialogs.py,sha256=du6v17lh6LhgDDK0QltPzD-z8wUn3aD4QzaAQBmiTBQ,7314
|
32
34
|
excel2moodle/ui/equationChecker.py,sha256=RII9DlZAlHqe5udBWzeUaozhtyi3ZkCZs8h3-oO6pEw,2700
|
33
|
-
excel2moodle/ui/treewidget.py,sha256=
|
34
|
-
excel2moodle-0.6.
|
35
|
-
excel2moodle-0.6.
|
36
|
-
excel2moodle-0.6.
|
37
|
-
excel2moodle-0.6.
|
38
|
-
excel2moodle-0.6.
|
39
|
-
excel2moodle-0.6.
|
35
|
+
excel2moodle/ui/treewidget.py,sha256=3hZRLlrhp4FMXFyNY0LGDy7k1RSuKH87QyqB1N4qOqg,2335
|
36
|
+
excel2moodle-0.6.3.dist-info/licenses/LICENSE,sha256=ywQqe6Sitymkf2lV2NRcx_aGsaC-KbSl_EfEsRXmNRw,35135
|
37
|
+
excel2moodle-0.6.3.dist-info/METADATA,sha256=gspbMXBm2lHiIkV-_-8XL45Q7ItMAEZV-7YFaDGJhZc,11891
|
38
|
+
excel2moodle-0.6.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
39
|
+
excel2moodle-0.6.3.dist-info/entry_points.txt,sha256=myfMLDThuGgWHMJDPPfILiZqo_7D3fhmDdJGqWOAjPw,60
|
40
|
+
excel2moodle-0.6.3.dist-info/top_level.txt,sha256=5V1xRUQ9o7UmOCmNoWCZPAuy5nXp3Qbzyqch8fUGT_c,13
|
41
|
+
excel2moodle-0.6.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|