excel2moodle 0.4.1__py3-none-any.whl → 0.4.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.
Files changed (34) hide show
  1. excel2moodle/__init__.py +0 -7
  2. excel2moodle/__main__.py +2 -2
  3. excel2moodle/core/__init__.py +0 -10
  4. excel2moodle/core/category.py +4 -3
  5. excel2moodle/core/dataStructure.py +116 -61
  6. excel2moodle/core/etHelpers.py +2 -2
  7. excel2moodle/core/exceptions.py +2 -2
  8. excel2moodle/core/globals.py +10 -27
  9. excel2moodle/core/parser.py +24 -30
  10. excel2moodle/core/question.py +147 -63
  11. excel2moodle/core/settings.py +107 -111
  12. excel2moodle/core/validator.py +36 -55
  13. excel2moodle/logger.py +7 -4
  14. excel2moodle/question_types/__init__.py +2 -0
  15. excel2moodle/question_types/cloze.py +207 -0
  16. excel2moodle/question_types/mc.py +26 -16
  17. excel2moodle/question_types/nf.py +17 -3
  18. excel2moodle/question_types/nfm.py +60 -17
  19. excel2moodle/ui/{windowEquationChecker.py → UI_equationChecker.py} +98 -78
  20. excel2moodle/ui/{exportSettingsDialog.py → UI_exportSettingsDialog.py} +55 -4
  21. excel2moodle/ui/{windowMain.py → UI_mainWindow.py} +32 -39
  22. excel2moodle/ui/appUi.py +66 -86
  23. excel2moodle/ui/dialogs.py +40 -2
  24. excel2moodle/ui/equationChecker.py +70 -0
  25. excel2moodle/ui/treewidget.py +4 -4
  26. {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.3.dist-info}/METADATA +2 -3
  27. excel2moodle-0.4.3.dist-info/RECORD +38 -0
  28. {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.3.dist-info}/entry_points.txt +0 -3
  29. excel2moodle/ui/questionPreviewDialog.py +0 -115
  30. excel2moodle-0.4.1.dist-info/RECORD +0 -37
  31. /excel2moodle/ui/{variantDialog.py → UI_variantDialog.py} +0 -0
  32. {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.3.dist-info}/WHEEL +0 -0
  33. {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.3.dist-info}/licenses/LICENSE +0 -0
  34. {excel2moodle-0.4.1.dist-info → excel2moodle-0.4.3.dist-info}/top_level.txt +0 -0
@@ -5,16 +5,13 @@ from enum import StrEnum
5
5
  from pathlib import Path
6
6
  from typing import ClassVar, Literal, overload
7
7
 
8
- from PySide6.QtCore import QSettings, QTimer, Signal
9
-
10
- import excel2moodle
11
-
12
8
  logger = logging.getLogger(__name__)
13
9
 
14
10
 
15
- class SettingsKey(StrEnum):
16
- """Settings Keys are needed to always acess the correct Value.
11
+ class Tags(StrEnum):
12
+ """Tags and Settings Keys are needed to always acess the correct Value.
17
13
 
14
+ The Tags can be used to acess the settings or the QuestionData respectively.
18
15
  As the QSettings settings are accesed via strings, which could easily gotten wrong.
19
16
  Further, this Enum defines, which type a setting has to be.
20
17
  """
@@ -22,23 +19,27 @@ class SettingsKey(StrEnum):
22
19
  def __new__(
23
20
  cls,
24
21
  key: str,
25
- place: str,
26
22
  typ: type,
27
23
  default: str | float | Path | bool | None,
24
+ place: str = "project",
28
25
  ):
29
26
  """Define new settings class."""
30
27
  obj = str.__new__(cls, key)
31
28
  obj._value_ = key
32
- obj._place_ = place
33
- obj._default_ = default
34
29
  obj._typ_ = typ
30
+ obj._default_ = default
31
+ obj._place_ = place
35
32
  return obj
36
33
 
37
34
  def __init__(
38
- self, _, place: str, typ: type, default: str | float | Path | None
35
+ self,
36
+ _,
37
+ typ: type,
38
+ default: str | float | Path | None,
39
+ place: str = "project",
39
40
  ) -> None:
40
- self._typ_ = typ
41
- self._place_ = place
41
+ self._typ_: type = typ
42
+ self._place_: str = place
42
43
  self._default_ = default
43
44
  self._full_ = f"{self._place_}/{self._value_}"
44
45
 
@@ -56,88 +57,105 @@ class SettingsKey(StrEnum):
56
57
  return self._full_
57
58
 
58
59
  def typ(self) -> type:
59
- """Get default value for the key."""
60
+ """Get type of the keys data."""
60
61
  return self._typ_
61
62
 
62
- QUESTIONVARIANT = "defaultQuestionVariant", "testgen", int, 0
63
- INCLUDEINCATS = "includeCats", "testgen", bool, False
64
- TOLERANCE = "tolerance", "parser/nf", int, 1
65
- PICTUREFOLDER = "pictureFolder", "core", Path, None
66
- SPREADSHEETFOLDER = "spreadsheetFolder", "core", Path, None
67
- LOGLEVEL = "loglevel", "core", str, "INFO"
68
- LOGFILE = "logfile", "core", str, "excel2moodleLogFile.log"
69
- CATEGORIESSHEET = "categoriesSheet", "core", str, "Kategorien"
70
- VERSION = "version", "project", int, 1
71
- POINTS = "points", "project", float, 1.0
72
- PICTURESUBFOLDER = "imgFolder", "project", str, "Abbildungen"
73
- PICTUREWIDTH = "imgWidth", "project", int, 500
74
- ANSPICWIDTH = "answerImgWidth", "project", int, 120
75
-
76
-
77
- class Settings(QSettings):
78
- """Settings for Excel2moodle."""
79
-
80
- shPathChanged = Signal(Path)
81
- localSettings: ClassVar[dict[str, str | float | Path]] = {}
82
-
83
- def __init__(self) -> None:
84
- """Instantiate the settings."""
85
- super().__init__("jbosse3", "excel2moodle")
86
- if excel2moodle.isMainState():
87
- logger.info("Settings are stored under: %s", self.fileName())
88
- if self.contains(SettingsKey.SPREADSHEETFOLDER.full):
89
- self.sheet = self.get(SettingsKey.SPREADSHEETFOLDER)
90
- if self.sheet.is_file():
91
- QTimer.singleShot(300, self._emitSpreadsheetChanged)
92
-
93
- def _emitSpreadsheetChanged(self) -> None:
94
- self.shPathChanged.emit(self.sheet)
63
+ QUESTIONVARIANT = "defaultQuestionVariant", int, 1, "testgen"
64
+ INCLUDEINCATS = "includeCats", bool, False, "testgen"
65
+ TOLERANCE = "tolerance", float, 0.01, "parser/nf"
66
+ PICTUREFOLDER = "pictureFolder", Path, None, "core"
67
+ PICTURESUBFOLDER = "imgfolder", str, "Abbildungen", "project"
68
+ SPREADSHEETPATH = "spreadsheetFolder", Path, None, "core"
69
+ LOGLEVEL = "loglevel", str, "INFO", "core"
70
+ LOGFILE = "logfile", str, "excel2moodleLogFile.log", "core"
71
+ CATEGORIESSHEET = "categoriessheet", str, "Kategorien", "core"
72
+
73
+ IMPORTMODULE = "importmodule", str, None
74
+ TEXT = "text", list, None
75
+ BPOINTS = "bulletpoint", list, None
76
+ TRUE = "true", list, None
77
+ FALSE = "false", list, None
78
+ TYPE = "type", str, None
79
+ NAME = "name", str, None
80
+ RESULT = "result", float, None
81
+ EQUATION = "formula", str, None
82
+ PICTURE = "picture", str, None
83
+ NUMBER = "number", int, None
84
+ ANSTYPE = "answertype", str, None
85
+ QUESTIONPART = "part", list, None
86
+ PARTTYPE = "parttype", str, None
87
+ VERSION = "version", int, 1
88
+ POINTS = "points", float, 1.0
89
+ PICTUREWIDTH = "imgwidth", int, 500
90
+ ANSPICWIDTH = "answerimgwidth", int, 120
91
+ WRONGSIGNPERCENT = "wrongsignpercentage", int, 50
92
+ FIRSTRESULT = "firstresult", float, 0
93
+
94
+
95
+ class Settings:
96
+ values: ClassVar[dict[str, str | float | Path]] = {}
97
+
98
+ @classmethod
99
+ def clear(cls) -> None:
100
+ cls.values.clear()
95
101
 
96
102
  @overload
103
+ @classmethod
97
104
  def get(
98
- self,
105
+ cls,
106
+ key: Literal[Tags.POINTS],
107
+ ) -> float: ...
108
+ @overload
109
+ @classmethod
110
+ def get(
111
+ cls,
99
112
  key: Literal[
100
- SettingsKey.QUESTIONVARIANT,
101
- SettingsKey.TOLERANCE,
102
- SettingsKey.VERSION,
103
- SettingsKey.POINTS,
104
- SettingsKey.PICTUREWIDTH,
105
- SettingsKey.ANSPICWIDTH,
113
+ Tags.QUESTIONVARIANT,
114
+ Tags.TOLERANCE,
115
+ Tags.VERSION,
116
+ Tags.PICTUREWIDTH,
117
+ Tags.ANSPICWIDTH,
118
+ Tags.WRONGSIGNPERCENT,
106
119
  ],
107
120
  ) -> int: ...
108
121
  @overload
109
- def get(self, key: Literal[SettingsKey.INCLUDEINCATS]) -> bool: ...
122
+ @classmethod
123
+ def get(cls, key: Literal[Tags.INCLUDEINCATS]) -> bool: ...
110
124
  @overload
125
+ @classmethod
111
126
  def get(
112
- self,
127
+ cls,
113
128
  key: Literal[
114
- SettingsKey.PICTURESUBFOLDER,
115
- SettingsKey.LOGLEVEL,
116
- SettingsKey.LOGFILE,
117
- SettingsKey.CATEGORIESSHEET,
129
+ Tags.PICTURESUBFOLDER,
130
+ Tags.LOGLEVEL,
131
+ Tags.LOGFILE,
132
+ Tags.CATEGORIESSHEET,
133
+ Tags.IMPORTMODULE,
118
134
  ],
119
135
  ) -> str: ...
120
136
  @overload
137
+ @classmethod
121
138
  def get(
122
- self,
123
- key: Literal[SettingsKey.PICTUREFOLDER, SettingsKey.SPREADSHEETFOLDER],
139
+ cls,
140
+ key: Literal[Tags.PICTUREFOLDER, Tags.SPREADSHEETPATH],
124
141
  ) -> Path: ...
125
142
 
126
- def get(self, key: SettingsKey):
143
+ @classmethod
144
+ def get(cls, key: Tags):
127
145
  """Get the typesafe settings value.
128
146
 
129
- If local Settings are stored, they are returned.
130
147
  If no setting is made, the default value is returned.
131
148
  """
132
- if key in self.localSettings:
133
- val = key.typ()(self.localSettings[key])
134
- logger.debug("Returning project setting: %s = %s", key, val)
135
- return val
136
- if not excel2moodle.isMainState():
137
- logger.warning("No GUI: Returning default value.")
138
- return key.default
149
+ try:
150
+ raw = cls.values[key]
151
+ except KeyError:
152
+ default = key.default
153
+ if default is None:
154
+ return None
155
+ logger.info("Returning the default value for %s", key)
156
+ return default
139
157
  if key.typ() is Path:
140
- path: Path = self.value(key.full, defaultValue=key.default)
158
+ path: Path = Path(raw)
141
159
  try:
142
160
  path.resolve(strict=True)
143
161
  except ValueError:
@@ -147,10 +165,7 @@ class Settings(QSettings):
147
165
  return key.default
148
166
  logger.debug("Returning path setting: %s = %s", key, path)
149
167
  return path
150
- raw = self.value(key.full, defaultValue=key.default, type=key.typ())
151
- logger.debug("read a settings Value: %s of type: %s", key, key.typ())
152
168
  try:
153
- logger.debug("Returning global setting: %s = %s", key, raw)
154
169
  return key.typ()(raw)
155
170
  except (ValueError, TypeError):
156
171
  logger.warning(
@@ -158,44 +173,25 @@ class Settings(QSettings):
158
173
  )
159
174
  return key.default
160
175
 
176
+ @classmethod
161
177
  def set(
162
- self,
163
- key: SettingsKey | str,
178
+ cls,
179
+ key: Tags | str,
164
180
  value: float | bool | Path | str,
165
- local: bool = False,
166
181
  ) -> None:
167
- """Set the setting to value.
168
-
169
- Parameters
170
- ----------
171
- local
172
- True saves local project specific settings.
173
- Defaults to False
174
- The local settings are meant to be set in the first sheet `settings`
175
-
176
- """
177
- if not excel2moodle.isMainState():
178
- local = True
179
- if local:
180
- if key in SettingsKey:
181
- self.localSettings[key] = value
182
- logger.info("Saved the project setting %s = %s", key, value)
183
- else:
184
- logger.warning("got invalid local Setting %s = %s", key, value)
185
- return
186
- if not local and isinstance(key, SettingsKey):
187
- if not isinstance(value, key.typ()):
188
- logger.error("trying to save setting with wrong type not possible")
182
+ """Set the setting to value."""
183
+ if key in Tags:
184
+ tag = Tags(key) if not isinstance(key, Tags) else key
185
+ try:
186
+ cls.values[tag] = tag.typ()(value)
187
+ except TypeError:
188
+ logger.exception(
189
+ "trying to save %s = %s %s with wrong type not possible.",
190
+ tag,
191
+ value,
192
+ type(value),
193
+ )
189
194
  return
190
- self.setValue(key.full, value)
191
- logger.info("Saved the global setting %s = %s", key, value)
192
-
193
- def setSpreadsheet(self, sheet: Path) -> None:
194
- """Save spreadsheet path and emit the changed event."""
195
- if isinstance(sheet, Path):
196
- self.sheet = sheet.resolve(strict=True)
197
- logpath = str(self.sheet.parent / "excel2moodleLogFile.log")
198
- self.set(SettingsKey.LOGFILE, logpath)
199
- self.set(SettingsKey.SPREADSHEETFOLDER, self.sheet)
200
- self.shPathChanged.emit(sheet)
201
- return
195
+ logger.info("Saved %s = %s, type %s", key, value, tag.typ())
196
+ else:
197
+ logger.warning("got invalid local Setting %s = %s", key, value)
@@ -10,19 +10,20 @@ which can be accessed via ``Validator.question``
10
10
  """
11
11
 
12
12
  import logging
13
- from typing import TYPE_CHECKING
14
13
 
15
14
  import pandas as pd
16
15
 
16
+ from excel2moodle.core import stringHelpers
17
17
  from excel2moodle.core.exceptions import InvalidFieldException
18
- from excel2moodle.core.globals import DFIndex
19
-
20
- if TYPE_CHECKING:
21
- from types import UnionType
22
-
18
+ from excel2moodle.core.globals import QUESTION_TYPES, Tags
19
+ from excel2moodle.core.question import QuestionData
20
+ from excel2moodle.core.settings import Settings
21
+ from excel2moodle.question_types import QuestionTypeMapping
23
22
 
24
23
  logger = logging.getLogger(__name__)
25
24
 
25
+ settings = Settings()
26
+
26
27
 
27
28
  class Validator:
28
29
  """Validate the question data from the spreadsheet.
@@ -30,52 +31,15 @@ class Validator:
30
31
  Creates a dictionary with the data, for easier access later.
31
32
  """
32
33
 
33
- def __init__(self) -> None:
34
- self.allMandatory: dict[DFIndex, type | UnionType] = {
35
- DFIndex.TEXT: str,
36
- DFIndex.NAME: str,
37
- DFIndex.TYPE: str,
38
- }
39
- self.allOptional: dict[DFIndex, type | UnionType] = {
40
- DFIndex.PICTURE: int | str,
41
- }
42
- self.nfOpt: dict[DFIndex, type | UnionType] = {
43
- DFIndex.BPOINTS: str,
44
- }
45
- self.nfMand: dict[DFIndex, type | UnionType] = {
46
- DFIndex.RESULT: float | int,
47
- }
48
- self.nfmOpt: dict[DFIndex, type | UnionType] = {}
49
- self.nfmMand: dict[DFIndex, type | UnionType] = {
50
- DFIndex.RESULT: str,
51
- DFIndex.BPOINTS: str,
52
- }
53
- self.mcOpt: dict[DFIndex, type | UnionType] = {}
54
- self.mcMand: dict[DFIndex, type | UnionType] = {
55
- DFIndex.TRUE: str,
56
- DFIndex.FALSE: str,
57
- DFIndex.ANSTYPE: str,
58
- }
59
-
60
- self.mapper: dict = {
61
- "NF": (self.nfOpt, self.nfMand),
62
- "MC": (self.mcOpt, self.mcMand),
63
- "NFM": (self.nfmOpt, self.nfmMand),
64
- }
65
-
66
34
  def setup(self, df: pd.Series, index: int) -> None:
67
35
  self.df = df
68
36
  self.index = index
69
- self.mandatory = {}
70
- self.optional = {}
71
- typ = self.df.loc[DFIndex.TYPE]
72
- if typ not in self.mapper:
37
+ typ = self.df.loc[Tags.TYPE]
38
+ if typ not in QUESTION_TYPES:
73
39
  msg = f"No valid question type provided. {typ} is not a known type"
74
- raise InvalidFieldException(msg, "index:02d", DFIndex.TYPE)
75
- self.mandatory.update(self.allMandatory)
76
- self.optional.update(self.allOptional)
77
- self.mandatory.update(self.mapper[typ][1])
78
- self.optional.update(self.mapper[typ][0])
40
+ raise InvalidFieldException(msg, "index:02d", Tags.TYPE)
41
+ self.mandatory = QuestionTypeMapping[typ].value.mandatoryTags
42
+ self.optional = QuestionTypeMapping[typ].value.optionalTags
79
43
 
80
44
  def validate(self) -> None:
81
45
  qid = f"{self.index:02d}"
@@ -90,9 +54,9 @@ class Validator:
90
54
  if missing is not None:
91
55
  raise InvalidFieldException(msg, qid, missing)
92
56
 
93
- def getQuestionRawData(self) -> dict[str, str | float | list[str]]:
57
+ def getQuestionData(self) -> QuestionData:
94
58
  """Get the data from the spreadsheet as a dictionary."""
95
- self.qdata: dict[str, str | float | int | list] = {}
59
+ self.qdata: dict[str, int | float | list[str] | str] = {}
96
60
  for idx, val in self.df.items():
97
61
  if not isinstance(idx, str):
98
62
  continue
@@ -106,9 +70,26 @@ class Validator:
106
70
  self.qdata[idx] = [existing, val]
107
71
  else:
108
72
  self.qdata[idx] = val
109
- return self.qdata
110
-
111
- def _mandatory(self) -> tuple[bool, DFIndex | None]:
73
+ return self.formatQData()
74
+
75
+ def formatQData(self) -> QuestionData:
76
+ """Format the dictionary to The types for QuestionData."""
77
+ listTags = (Tags.BPOINTS, Tags.TRUE, Tags.FALSE, Tags.TEXT, Tags.QUESTIONPART)
78
+ for tag in listTags:
79
+ for key in self.qdata:
80
+ if key.startswith(tag) and not isinstance(self.qdata[key], list):
81
+ self.qdata[key] = stringHelpers.getListFromStr(self.qdata[key])
82
+ tol = float(self.qdata.get(Tags.TOLERANCE, 0))
83
+ if tol <= 0 or tol > 99:
84
+ self.qdata[Tags.TOLERANCE] = settings.get(Tags.TOLERANCE)
85
+ else:
86
+ self.qdata[Tags.TOLERANCE] = tol if tol < 1 else tol / 100
87
+
88
+ if self.qdata[Tags.TYPE] == "NFM":
89
+ self.qdata[Tags.EQUATION] = str(self.qdata[Tags.RESULT])
90
+ return QuestionData(self.qdata)
91
+
92
+ def _mandatory(self) -> tuple[bool, Tags | None]:
112
93
  """Detects if all keys of mandatory are filled with values."""
113
94
  checker = pd.Series.notna(self.df)
114
95
  for k in self.mandatory:
@@ -123,8 +104,8 @@ class Validator:
123
104
  return False, k
124
105
  return True, None
125
106
 
126
- def _typeCheck(self) -> tuple[bool, list[DFIndex] | None]:
127
- invalid: list[DFIndex] = []
107
+ def _typeCheck(self) -> tuple[bool, list[Tags] | None]:
108
+ invalid: list[Tags] = []
128
109
  for field, typ in self.mandatory.items():
129
110
  if field in self.df and isinstance(self.df[field], pd.Series):
130
111
  for f in self.df[field]:
excel2moodle/logger.py CHANGED
@@ -6,10 +6,11 @@ This includes emitting the signals for the main Window, to fast forward all logs
6
6
 
7
7
  import logging
8
8
 
9
- from PySide6.QtCore import QObject, Signal
9
+ from PySide6.QtCore import QObject, QSettings, Signal
10
10
 
11
- from excel2moodle.core.settings import Settings, SettingsKey
11
+ from excel2moodle.core.settings import Settings, Tags
12
12
 
13
+ qSettings = QSettings("jbosse3", "excel2moodle")
13
14
  settings = Settings()
14
15
 
15
16
  loggerConfig = {
@@ -36,7 +37,9 @@ loggerConfig = {
36
37
  "formatter": "file",
37
38
  "class": "logging.FileHandler",
38
39
  # "class": "logging.handlers.TimedRotatingFileHandler",
39
- "filename": settings.get(SettingsKey.LOGFILE),
40
+ "filename": qSettings.value(
41
+ Tags.LOGFILE, defaultValue=Tags.LOGFILE.default
42
+ ),
40
43
  # "when": "M",
41
44
  # "interval": 1,
42
45
  # "backupCount": "3",
@@ -75,7 +78,7 @@ class LogWindowHandler(logging.Handler):
75
78
  log_format = "[%(levelname)s] %(module)s: %(message)s"
76
79
  self.formatter = logging.Formatter(log_format)
77
80
  self.setFormatter(self.formatter)
78
- loglevel = settings.get(SettingsKey.LOGLEVEL)
81
+ loglevel = settings.get(Tags.LOGLEVEL)
79
82
  self.setLevel(loglevel)
80
83
  self.logLevelColors = {
81
84
  "DEBUG": "gray",
@@ -12,6 +12,7 @@ Both go into a module named ``excel2moodle.question_types.type.py``
12
12
  from enum import Enum
13
13
 
14
14
  from excel2moodle.core.category import Category
15
+ from excel2moodle.question_types.cloze import ClozeQuestion
15
16
  from excel2moodle.question_types.mc import MCQuestion
16
17
  from excel2moodle.question_types.nf import NFQuestion
17
18
  from excel2moodle.question_types.nfm import NFMQuestion
@@ -23,6 +24,7 @@ class QuestionTypeMapping(Enum):
23
24
  MC = MCQuestion
24
25
  NF = NFQuestion
25
26
  NFM = NFMQuestion
27
+ CLOZE = ClozeQuestion
26
28
 
27
29
  def create(
28
30
  self,