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

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