excel2moodle 0.6.2__tar.gz → 0.6.3__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.
Files changed (52) hide show
  1. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/PKG-INFO +21 -1
  2. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/README.md +20 -0
  3. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/__init__.py +2 -0
  4. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/__main__.py +18 -3
  5. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/core/bullets.py +2 -2
  6. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/core/category.py +4 -3
  7. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/core/dataStructure.py +24 -23
  8. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/core/settings.py +5 -1
  9. excel2moodle-0.6.3/excel2moodle/extra/updateQuery.py +48 -0
  10. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/extra/variableGenerator.py +73 -49
  11. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/question_types/cloze.py +5 -6
  12. excel2moodle-0.6.3/excel2moodle/ui/UI_updateDlg.py +106 -0
  13. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/ui/appUi.py +45 -17
  14. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/ui/dialogs.py +18 -1
  15. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/ui/treewidget.py +30 -10
  16. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle.egg-info/PKG-INFO +21 -1
  17. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle.egg-info/SOURCES.txt +2 -0
  18. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/pyproject.toml +10 -1
  19. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/LICENSE +0 -0
  20. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/core/__init__.py +0 -0
  21. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/core/etHelpers.py +0 -0
  22. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/core/exceptions.py +0 -0
  23. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/core/globals.py +0 -0
  24. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/core/parser.py +0 -0
  25. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/core/question.py +0 -0
  26. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/core/stringHelpers.py +0 -0
  27. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/core/validator.py +0 -0
  28. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/extra/__init__.py +0 -0
  29. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/extra/equationVerification.py +0 -0
  30. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/logger.py +0 -0
  31. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/question_types/__init__.py +0 -0
  32. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/question_types/mc.py +0 -0
  33. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/question_types/nf.py +0 -0
  34. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/question_types/nfm.py +0 -0
  35. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/ui/UI_equationChecker.py +0 -0
  36. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/ui/UI_exportSettingsDialog.py +0 -0
  37. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/ui/UI_mainWindow.py +0 -0
  38. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/ui/UI_variableGenerator.py +0 -0
  39. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/ui/UI_variantDialog.py +0 -0
  40. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/ui/__init__.py +0 -0
  41. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle/ui/equationChecker.py +0 -0
  42. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle.egg-info/dependency_links.txt +0 -0
  43. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle.egg-info/entry_points.txt +0 -0
  44. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle.egg-info/requires.txt +0 -0
  45. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/excel2moodle.egg-info/top_level.txt +0 -0
  46. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/setup.cfg +0 -0
  47. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/test/test_bullets.py +0 -0
  48. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/test/test_feedbacking.py +0 -0
  49. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/test/test_nfmParsing.py +0 -0
  50. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/test/test_parseQuestion.py +0 -0
  51. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/test/test_picture.py +0 -0
  52. {excel2moodle-0.6.2 → excel2moodle-0.6.3}/test/test_questionDataGet.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: excel2moodle
3
- Version: 0.6.2
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
 
@@ -68,6 +68,26 @@ If You want to support my work as well, you can by me a [coffee](https://ko-fi.c
68
68
 
69
69
  # Changelogs
70
70
 
71
+ ## 0.6.3 (2025-08-03)
72
+ Lots of small improvements made
73
+
74
+ ### improvement (3 changes)
75
+
76
+ - [small logging improvements and error handling](https://gitlab.com/jbosse3/excel2moodle/-/commit/149f8e923a06d9d7077fe90c7005a3e1d5d2d42f)
77
+ - [Make variable generator rules editable](https://gitlab.com/jbosse3/excel2moodle/-/commit/80ea32d97bdec16b77100bc870a0e0272a739dd4)
78
+ - [Variable generator only generates unique sets.](https://gitlab.com/jbosse3/excel2moodle/-/commit/d347c91bbac66de1da157fee4f76faf8d4636557)
79
+
80
+ ### bugfix (3 changes)
81
+
82
+ - [mixed parametric and non parametric Bullets are working now](https://gitlab.com/jbosse3/excel2moodle/-/commit/f094b13dffd4b6b7ac1a03fc7e34eec6e8d1bfa7)
83
+ - [Loglevel setting is respected in spreadsheet file](https://gitlab.com/jbosse3/excel2moodle/-/commit/d6ef89beeec94f24782a00b7564883074badf72d)
84
+ - [Treewidget variants count updated after variable generation](https://gitlab.com/jbosse3/excel2moodle/-/commit/c48a0d093a0cce85fd3e9c3c091eef936739c02b)
85
+
86
+ ### feature (2 changes)
87
+
88
+ - [Category ID taken from any number in its name](https://gitlab.com/jbosse3/excel2moodle/-/commit/ac7e19af5f25ac2e576b63c478e7b07153e782ef)
89
+ - [Implemented Update Check on Startup](https://gitlab.com/jbosse3/excel2moodle/-/commit/a143edd47f566c5e731c05612f4ac21dc7728eb7)
90
+
71
91
  ## 0.6.2 (2025-08-02)
72
92
  Adding export options and fixing cloze points bug
73
93
 
@@ -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
 
@@ -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)
@@ -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: str = str(i + 1)
73
+ bulletName = i + 1
74
74
  else:
75
75
  bulletName = match.group(1)
76
76
  num: float = 0.0
@@ -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+$", str(self.NAME))
28
- self.n: int = int(match.group(0)) if match else 99
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 question are not yet initialized"
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
- try:
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",
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
- try:
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
- logger.info("Sucessfully read categoriesMetaData")
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().getCategory()
381
+ cat = q.parent().category
383
382
  if cat not in catdict:
384
383
  catdict[cat] = []
385
- catdict[cat].append(q.getQuestion())
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(
@@ -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.info("Returning the default value for %s", key)
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("0") # Default to 0 decimal places
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
- self.ui.listWidget_rules.addItem(rule_text)
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) for _ in range(num_sets)
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("No variable set found:")
112
- QMessageBox.warning(
113
- self,
114
- "Generation Failed",
115
- f"{e} Consider relaxing your rules or increasing the number of attempts.",
116
- )
117
- else:
118
- # convert the generated_sets list[dict[str, float]] into dict[str, list[float]]
119
- # [{A:7, B:8}, {A:11, B:9}] -> {A: [7, 11], B: [8, 9]}
120
- newVariables = {}
121
- for var in self.origParametrics.variables:
122
- newVariables[var] = [dataSet[var] for dataSet in generated_sets]
123
- self._generatedParametrics.variables = newVariables
124
- self.ui.groupBox_generated_variables.show()
125
- populateDataSetTable(
126
- self.ui.tableWidget_generated_variables,
127
- parametrics=self._generatedParametrics,
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 Random numbers for each variable and check if the rules apply.
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 attemps", attempts)
209
+ logger.info("Found matching unique set after %s attempts", attempts)
163
210
  return current_set
164
- attempts += 1
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
- logger.warning(
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.info("NF answer part: %s ", ansStr)
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
+
@@ -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
- loggerSignal.emitter.signal.connect(self.updateLog)
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.getQuestion().points
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
- for q in cat.questions.values():
228
- QuestionItem(catItem, q)
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.getQuestion())
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.getQuestion()
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.getQuestion())
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.getQuestion()
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.getQuestion()
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
- self.testDB.readCategoriesMetadata()
339
- self.testDB.asyncInitAllCategories(self.mainApp.excelPath)
340
- self.mainApp.setStatus("[OK] Tabellen wurde eingelesen")
341
- self.testDB.parseAllQuestions()
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()
@@ -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)
@@ -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 hasattr(question, "variants") and question.variants is not None:
22
- self.setText(3, str(question.variants))
29
+ if isinstance(question, ParametricQuestion):
30
+ self.setText(3, str(question.parametrics.variants))
23
31
 
24
- def getQuestion(self) -> Question | ParametricQuestion:
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.setText(0, category.NAME)
34
- self.setText(1, category.desc)
35
- self.setText(2, str(category.points))
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.getQuestion()
48
- if hasattr(q, "variants") and q.variants is not None:
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
- def getCategory(self) -> Category:
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.2
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
 
@@ -24,6 +24,7 @@ excel2moodle/core/stringHelpers.py
24
24
  excel2moodle/core/validator.py
25
25
  excel2moodle/extra/__init__.py
26
26
  excel2moodle/extra/equationVerification.py
27
+ excel2moodle/extra/updateQuery.py
27
28
  excel2moodle/extra/variableGenerator.py
28
29
  excel2moodle/question_types/__init__.py
29
30
  excel2moodle/question_types/cloze.py
@@ -33,6 +34,7 @@ excel2moodle/question_types/nfm.py
33
34
  excel2moodle/ui/UI_equationChecker.py
34
35
  excel2moodle/ui/UI_exportSettingsDialog.py
35
36
  excel2moodle/ui/UI_mainWindow.py
37
+ excel2moodle/ui/UI_updateDlg.py
36
38
  excel2moodle/ui/UI_variableGenerator.py
37
39
  excel2moodle/ui/UI_variantDialog.py
38
40
  excel2moodle/ui/__init__.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "excel2moodle"
7
- version = "0.6.2"
7
+ version = "0.6.3"
8
8
  authors = [
9
9
  { name="Jakob Bosse" },
10
10
  ]
@@ -78,3 +78,12 @@ max-doc-length = 88
78
78
  [tool.ruff.format]
79
79
  quote-style = "double"
80
80
 
81
+ [tool.ty.src]
82
+ exclude = [
83
+ "build/**"
84
+ ]
85
+
86
+ [tool.pyright]
87
+ exclude = [
88
+ "build/**"
89
+ ]
File without changes
File without changes