excel2moodle 0.3.6__py3-none-any.whl → 0.4.0__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.
@@ -0,0 +1,201 @@
1
+ """Settings module provides the adjusted subclass of ``PySide6.QtCore.QSettings``."""
2
+
3
+ import logging
4
+ from enum import StrEnum
5
+ from pathlib import Path
6
+ from typing import ClassVar, Literal, overload
7
+
8
+ from PySide6.QtCore import QSettings, QTimer, Signal
9
+
10
+ import excel2moodle
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class SettingsKey(StrEnum):
16
+ """Settings Keys are needed to always acess the correct Value.
17
+
18
+ As the QSettings settings are accesed via strings, which could easily gotten wrong.
19
+ Further, this Enum defines, which type a setting has to be.
20
+ """
21
+
22
+ def __new__(
23
+ cls,
24
+ key: str,
25
+ place: str,
26
+ typ: type,
27
+ default: str | float | Path | bool | None,
28
+ ):
29
+ """Define new settings class."""
30
+ obj = str.__new__(cls, key)
31
+ obj._value_ = key
32
+ obj._place_ = place
33
+ obj._default_ = default
34
+ obj._typ_ = typ
35
+ return obj
36
+
37
+ def __init__(
38
+ self, _, place: str, typ: type, default: str | float | Path | None
39
+ ) -> None:
40
+ self._typ_ = typ
41
+ self._place_ = place
42
+ self._default_ = default
43
+ self._full_ = f"{self._place_}/{self._value_}"
44
+
45
+ @property
46
+ def default(self) -> str | int | float | Path | bool | None:
47
+ """Get default value for the key."""
48
+ return self._default_
49
+
50
+ @property
51
+ def place(self) -> str:
52
+ return self._place_
53
+
54
+ @property
55
+ def full(self) -> str:
56
+ return self._full_
57
+
58
+ def typ(self) -> type:
59
+ """Get default value for the key."""
60
+ return self._typ_
61
+
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)
95
+
96
+ @overload
97
+ def get(
98
+ self,
99
+ key: Literal[
100
+ SettingsKey.QUESTIONVARIANT,
101
+ SettingsKey.TOLERANCE,
102
+ SettingsKey.VERSION,
103
+ SettingsKey.POINTS,
104
+ SettingsKey.PICTUREWIDTH,
105
+ SettingsKey.ANSPICWIDTH,
106
+ ],
107
+ ) -> int: ...
108
+ @overload
109
+ def get(self, key: Literal[SettingsKey.INCLUDEINCATS]) -> bool: ...
110
+ @overload
111
+ def get(
112
+ self,
113
+ key: Literal[
114
+ SettingsKey.PICTURESUBFOLDER,
115
+ SettingsKey.LOGLEVEL,
116
+ SettingsKey.LOGFILE,
117
+ SettingsKey.CATEGORIESSHEET,
118
+ ],
119
+ ) -> str: ...
120
+ @overload
121
+ def get(
122
+ self,
123
+ key: Literal[SettingsKey.PICTUREFOLDER, SettingsKey.SPREADSHEETFOLDER],
124
+ ) -> Path: ...
125
+
126
+ def get(self, key: SettingsKey):
127
+ """Get the typesafe settings value.
128
+
129
+ If local Settings are stored, they are returned.
130
+ If no setting is made, the default value is returned.
131
+ """
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
139
+ if key.typ() is Path:
140
+ path: Path = self.value(key.full, defaultValue=key.default)
141
+ try:
142
+ path.resolve(strict=True)
143
+ except ValueError:
144
+ logger.warning(
145
+ f"The settingsvalue {key} couldn't be fetched with correct typ",
146
+ )
147
+ return key.default
148
+ logger.debug("Returning path setting: %s = %s", key, path)
149
+ 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
+ try:
153
+ logger.debug("Returning global setting: %s = %s", key, raw)
154
+ return key.typ()(raw)
155
+ except (ValueError, TypeError):
156
+ logger.warning(
157
+ f"The settingsvalue {key} couldn't be fetched with correct typ",
158
+ )
159
+ return key.default
160
+
161
+ def set(
162
+ self,
163
+ key: SettingsKey | str,
164
+ value: float | bool | Path | str,
165
+ local: bool = False,
166
+ ) -> 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")
189
+ 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
@@ -4,12 +4,15 @@ import base64
4
4
  from pathlib import Path
5
5
 
6
6
  import lxml.etree as ET
7
+ from pandas import pandas
7
8
 
8
9
 
9
- def stripWhitespace(stringList):
10
- stripped = []
11
- for i in stringList:
12
- s = i.strip()
10
+ def getListFromStr(stringList: str | list[str]) -> list[str]:
11
+ """Get a python List of strings from a semi-colon separated string."""
12
+ stripped: list[str] = []
13
+ li = stringList if isinstance(stringList, list) else stringList.split(";")
14
+ for i in li:
15
+ s = i.strip() if not pandas.isna(i) else None
13
16
  if s:
14
17
  stripped.append(s)
15
18
  return stripped
@@ -20,32 +23,6 @@ def stringToFloat(string: str) -> float:
20
23
  return float(string)
21
24
 
22
25
 
23
- def get_bullet_string(s):
24
- """Formatiert die Angaben zum Statischen System hübsch."""
25
- split = s.split(";")
26
- s_spl = stripWhitespace(split)
27
- name = []
28
- var = []
29
- quant = []
30
- unit = []
31
- for sc in s_spl:
32
- sc_split = sc.split()
33
- name.append(sc_split[0])
34
- var.append(sc_split[1])
35
- quant.append(sc_split[3])
36
- unit.append(sc_split[4])
37
- bulletString = ['</p><ul dir="ltr">']
38
- for i in range(len(s_spl)):
39
- num = quant[i].split(",")
40
- num_s = f"{num[0]!s},\\!{num[1]!s}~" if len(num) == 2 else f"{num[0]!s},\\!0~"
41
- bulletString.append('<li style="text-align: left;">')
42
- bulletString.append(
43
- f"{name[i]}: \\( {var[i]} = {num_s} \\mathrm{{ {unit[i]} }}\\) </li>\n",
44
- )
45
- bulletString.append("<br></ul>")
46
- return "\n".join(bulletString)
47
-
48
-
49
26
  def getBase64Img(imgPath):
50
27
  with open(imgPath, "rb") as img:
51
28
  return base64.b64encode(img.read()).decode("utf-8")
@@ -62,7 +39,7 @@ def getUnitsElementAsString(unit) -> None:
62
39
 
63
40
 
64
41
  def printDom(xmlElement: ET.Element, file: Path | None = None) -> None:
65
- """Prints the document tree of ``xmlTree`` to the ``file``, if specified, else dumps to stdout."""
42
+ """Prints the document tree of ``xmlTree`` to ``file``, if specified, else dumps to stdout."""
66
43
  documentTree = ET.ElementTree(xmlElement)
67
44
  if file is not None:
68
45
  if file.parent.exists():
@@ -73,11 +50,11 @@ def printDom(xmlElement: ET.Element, file: Path | None = None) -> None:
73
50
  pretty_print=True,
74
51
  )
75
52
  else:
76
- pass
53
+ print(xmlElement.tostring()) # noqa: T201
77
54
 
78
55
 
79
56
  def texWrapper(text: str | list[str], style: str) -> list[str]:
80
- r"""Puts the strings inside ``text`` into a LaTex environment.
57
+ r"""Put the strings inside ``text`` into a LaTex environment.
81
58
 
82
59
  if ``style == unit``: inside ``\\mathrm{}``
83
60
  if ``style == math``: inside ``\\( \\)``
@@ -16,11 +16,11 @@ import pandas as pd
16
16
 
17
17
  from excel2moodle.core.exceptions import InvalidFieldException
18
18
  from excel2moodle.core.globals import DFIndex
19
- from excel2moodle.core.question import Question
20
19
 
21
20
  if TYPE_CHECKING:
22
21
  from types import UnionType
23
22
 
23
+
24
24
  logger = logging.getLogger(__name__)
25
25
 
26
26
 
@@ -30,25 +30,25 @@ class Validator:
30
30
  Creates a dictionary with the data, for easier access later.
31
31
  """
32
32
 
33
- def __init__(self, category) -> None:
34
- self.question: Question
35
- self.category = category
36
- self.mandatory: dict[DFIndex, type | UnionType] = {
33
+ def __init__(self) -> None:
34
+ self.allMandatory: dict[DFIndex, type | UnionType] = {
37
35
  DFIndex.TEXT: str,
38
36
  DFIndex.NAME: str,
39
37
  DFIndex.TYPE: str,
40
38
  }
41
- self.optional: dict[DFIndex, type | UnionType] = {
42
- DFIndex.BPOINTS: str,
39
+ self.allOptional: dict[DFIndex, type | UnionType] = {
43
40
  DFIndex.PICTURE: int | str,
44
41
  }
45
- self.nfOpt: dict[DFIndex, type | UnionType] = {}
42
+ self.nfOpt: dict[DFIndex, type | UnionType] = {
43
+ DFIndex.BPOINTS: str,
44
+ }
46
45
  self.nfMand: dict[DFIndex, type | UnionType] = {
47
46
  DFIndex.RESULT: float | int,
48
47
  }
49
48
  self.nfmOpt: dict[DFIndex, type | UnionType] = {}
50
49
  self.nfmMand: dict[DFIndex, type | UnionType] = {
51
50
  DFIndex.RESULT: str,
51
+ DFIndex.BPOINTS: str,
52
52
  }
53
53
  self.mcOpt: dict[DFIndex, type | UnionType] = {}
54
54
  self.mcMand: dict[DFIndex, type | UnionType] = {
@@ -63,35 +63,41 @@ class Validator:
63
63
  "NFM": (self.nfmOpt, self.nfmMand),
64
64
  }
65
65
 
66
- def setup(self, df: pd.Series, index: int) -> bool:
66
+ def setup(self, df: pd.Series, index: int) -> None:
67
67
  self.df = df
68
68
  self.index = index
69
+ self.mandatory = {}
70
+ self.optional = {}
69
71
  typ = self.df.loc[DFIndex.TYPE]
72
+ if typ not in self.mapper:
73
+ 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)
70
77
  self.mandatory.update(self.mapper[typ][1])
71
78
  self.optional.update(self.mapper[typ][0])
72
- return True
73
79
 
74
- def validate(self) -> bool:
75
- id = f"{self.category.id}{self.index:02d}"
80
+ def validate(self) -> None:
81
+ qid = f"{self.index:02d}"
76
82
  checker, missing = self._mandatory()
77
83
  if not checker:
78
- msg = f"Question {id} misses the key {missing}"
84
+ msg = f"Question {qid} misses the key {missing}"
79
85
  if missing is not None:
80
- raise InvalidFieldException(msg, id, missing)
86
+ raise InvalidFieldException(msg, qid, missing)
81
87
  checker, missing = self._typeCheck()
82
88
  if not checker:
83
- msg = f"Question {id} has wrong typed data {missing}"
89
+ msg = f"Question {qid} has wrong typed data {missing}"
84
90
  if missing is not None:
85
- raise InvalidFieldException(msg, id, missing)
86
- self._getQuestion()
87
- self._getData()
88
- return True
91
+ raise InvalidFieldException(msg, qid, missing)
89
92
 
90
- def _getData(self) -> None:
93
+ def getQuestionRawData(self) -> dict[str, str | float | list[str]]:
94
+ """Get the data from the spreadsheet as a dictionary."""
91
95
  self.qdata: dict[str, str | float | int | list] = {}
92
96
  for idx, val in self.df.items():
93
97
  if not isinstance(idx, str):
94
98
  continue
99
+ if pd.isna(val):
100
+ continue
95
101
  if idx in self.qdata:
96
102
  if isinstance(self.qdata[idx], list):
97
103
  self.qdata[idx].append(val)
@@ -100,6 +106,7 @@ class Validator:
100
106
  self.qdata[idx] = [existing, val]
101
107
  else:
102
108
  self.qdata[idx] = val
109
+ return self.qdata
103
110
 
104
111
  def _mandatory(self) -> tuple[bool, DFIndex | None]:
105
112
  """Detects if all keys of mandatory are filled with values."""
@@ -119,26 +126,17 @@ class Validator:
119
126
  def _typeCheck(self) -> tuple[bool, list[DFIndex] | None]:
120
127
  invalid: list[DFIndex] = []
121
128
  for field, typ in self.mandatory.items():
122
- if isinstance(self.df[field], pd.Series):
129
+ if field in self.df and isinstance(self.df[field], pd.Series):
123
130
  for f in self.df[field]:
124
131
  if pd.notna(f) and not isinstance(f, typ):
125
132
  invalid.append(field)
126
133
  elif not isinstance(self.df[field], typ):
127
134
  invalid.append(field)
128
135
  for field, typ in self.optional.items():
129
- if field in self.df:
130
- if not isinstance(self.df[field], typ) and pd.notna(self.df[field]):
131
- invalid.append(field)
136
+ if field in self.df and isinstance(self.df[field], pd.Series):
137
+ for f in self.df[field]:
138
+ if pd.notna(f) and not isinstance(f, typ):
139
+ invalid.append(field)
132
140
  if len(invalid) == 0:
133
141
  return True, None
134
142
  return False, invalid
135
-
136
- def _getQuestion(self) -> None:
137
- name = self.df[DFIndex.NAME]
138
- qtype = self.df[DFIndex.TYPE]
139
- self.question = Question(
140
- self.category,
141
- name=str(name),
142
- number=self.index,
143
- qtype=str(qtype),
144
- )
excel2moodle/logger.py CHANGED
@@ -8,7 +8,7 @@ import logging
8
8
 
9
9
  from PySide6.QtCore import QObject, Signal
10
10
 
11
- from excel2moodle.ui.settings import Settings, SettingsKey
11
+ from excel2moodle.core.settings import Settings, SettingsKey
12
12
 
13
13
  settings = Settings()
14
14
 
@@ -0,0 +1,33 @@
1
+ """Implementations of all different question types.
2
+
3
+ For each question type supported by excel2moodle here are the implementations.
4
+ Two classes need to be defined for each type:
5
+
6
+ * The Question class subclassing core.Question()
7
+ * The Parser class subclassing core.Parser()
8
+
9
+ Both go into a module named ``excel2moodle.question_types.type.py``
10
+ """
11
+
12
+ from enum import Enum
13
+
14
+ from excel2moodle.core.category import Category
15
+ from excel2moodle.question_types.mc import MCQuestion
16
+ from excel2moodle.question_types.nf import NFQuestion
17
+ from excel2moodle.question_types.nfm import NFMQuestion
18
+
19
+
20
+ class QuestionTypeMapping(Enum):
21
+ """The Mapping between question-types name and the classes."""
22
+
23
+ MC = MCQuestion
24
+ NF = NFQuestion
25
+ NFM = NFMQuestion
26
+
27
+ def create(
28
+ self,
29
+ category: Category,
30
+ questionData: dict[str, str | int | float | list[str]],
31
+ **kwargs,
32
+ ):
33
+ return self.value(category, questionData, **kwargs)
@@ -0,0 +1,127 @@
1
+ """Multiple choice Question implementation."""
2
+
3
+ from typing import ClassVar
4
+
5
+ import lxml.etree as ET
6
+
7
+ import excel2moodle.core.etHelpers as eth
8
+ from excel2moodle.core import stringHelpers
9
+ from excel2moodle.core.exceptions import InvalidFieldException
10
+ from excel2moodle.core.globals import (
11
+ DFIndex,
12
+ TextElements,
13
+ XMLTags,
14
+ feedbackStr,
15
+ )
16
+ from excel2moodle.core.parser import QuestionParser
17
+ from excel2moodle.core.question import Picture, Question
18
+ from excel2moodle.core.settings import SettingsKey
19
+
20
+
21
+ class MCQuestion(Question):
22
+ """Multiple-choice Question Implementation."""
23
+
24
+ standardTags: ClassVar[dict[str, str | float]] = {
25
+ "single": "false",
26
+ "shuffleanswers": "true",
27
+ "answernumbering": "abc",
28
+ "showstandardinstruction": "0",
29
+ "shownumcorrect": "",
30
+ }
31
+
32
+ def __init__(self, *args, **kwargs) -> None:
33
+ super().__init__(*args, **kwargs)
34
+ self.AnsStyles = ["math", "unit", "text", "picture"]
35
+
36
+
37
+ class MCQuestionParser(QuestionParser):
38
+ """Parser for the multiple choice Question."""
39
+
40
+ def __init__(self) -> None:
41
+ super().__init__()
42
+ self.genFeedbacks = [
43
+ XMLTags.CORFEEDB,
44
+ XMLTags.PCORFEEDB,
45
+ XMLTags.INCORFEEDB,
46
+ ]
47
+
48
+ def setup(self, question: MCQuestion) -> None:
49
+ self.question: MCQuestion = question
50
+ super().setup(question)
51
+
52
+ def getAnsElementsList(
53
+ self,
54
+ answerList: list,
55
+ fraction: float = 50,
56
+ format="html",
57
+ ) -> list[ET.Element]:
58
+ elementList: list[ET.Element] = []
59
+ for i, ans in enumerate(answerList):
60
+ p = TextElements.PLEFT.create()
61
+ if self.answerType == "picture":
62
+ p.append(ans)
63
+ else:
64
+ p.text = str(ans)
65
+ text = eth.getCdatTxtElement(p)
66
+ elementList.append(
67
+ ET.Element(XMLTags.ANSWER, fraction=str(fraction), format=format),
68
+ )
69
+ elementList[-1].append(text)
70
+ if fraction < 0:
71
+ elementList[-1].append(
72
+ eth.getFeedBEle(
73
+ XMLTags.ANSFEEDBACK,
74
+ text=feedbackStr["wrong"],
75
+ style=TextElements.SPANRED,
76
+ ),
77
+ )
78
+ if self.answerType == "picture":
79
+ elementList[-1].append(self.falseImgs[i].element)
80
+ elif fraction > 0:
81
+ elementList[-1].append(
82
+ eth.getFeedBEle(
83
+ XMLTags.ANSFEEDBACK,
84
+ text=feedbackStr["right"],
85
+ style=TextElements.SPANGREEN,
86
+ ),
87
+ )
88
+ if self.answerType == "picture":
89
+ elementList[-1].append(self.trueImgs[i].element)
90
+ return elementList
91
+
92
+ def setAnswers(self) -> list[ET.Element]:
93
+ self.answerType = self.rawInput[DFIndex.ANSTYPE]
94
+ true = stringHelpers.getListFromStr(self.rawInput[DFIndex.TRUE])
95
+ false = stringHelpers.getListFromStr(self.rawInput[DFIndex.FALSE])
96
+ if self.answerType not in self.question.AnsStyles:
97
+ msg = f"The Answer style: {self.answerType} is not supported"
98
+ raise InvalidFieldException(msg, self.question.id, DFIndex.ANSTYPE)
99
+ if self.answerType == "picture":
100
+ f = self.settings.get(SettingsKey.PICTUREFOLDER)
101
+ imgFolder = (f / self.question.katName).resolve()
102
+ w = self.settings.get(SettingsKey.ANSPICWIDTH)
103
+ self.trueImgs: list[Picture] = [
104
+ Picture(t, imgFolder, self.question.id, width=w) for t in true
105
+ ]
106
+ self.falseImgs: list[Picture] = [
107
+ Picture(t, imgFolder, self.question.id, width=w) for t in false
108
+ ]
109
+ trueAnsList: list[str] = [pic.htmlTag for pic in self.trueImgs]
110
+ falseAList: list[str] = [pic.htmlTag for pic in self.falseImgs]
111
+ else:
112
+ trueAnsList: list[str] = stringHelpers.texWrapper(
113
+ true, style=self.answerType
114
+ )
115
+ self.logger.debug(f"got the following true answers \n {trueAnsList=}")
116
+ falseAList: list[str] = stringHelpers.texWrapper(
117
+ false, style=self.answerType
118
+ )
119
+ self.logger.debug(f"got the following false answers \n {falseAList=}")
120
+ truefrac = 1 / len(trueAnsList) * 100
121
+ falsefrac = 1 / len(falseAList) * (-100)
122
+ self.tmpEle.find(XMLTags.PENALTY).text = str(round(truefrac / 100, 4))
123
+ ansList = self.getAnsElementsList(trueAnsList, fraction=round(truefrac, 4))
124
+ ansList.extend(
125
+ self.getAnsElementsList(falseAList, fraction=round(falsefrac, 4)),
126
+ )
127
+ return ansList
@@ -0,0 +1,29 @@
1
+ """Numerical question implementation."""
2
+
3
+ import lxml.etree as ET
4
+
5
+ from excel2moodle.core.globals import (
6
+ DFIndex,
7
+ XMLTags,
8
+ )
9
+ from excel2moodle.core.parser import QuestionParser
10
+ from excel2moodle.core.question import Question
11
+
12
+
13
+ class NFQuestion(Question):
14
+ def __init__(self, *args, **kwargs) -> None:
15
+ super().__init__(*args, **kwargs)
16
+
17
+
18
+ class NFQuestionParser(QuestionParser):
19
+ """Subclass for parsing numeric questions."""
20
+
21
+ def __init__(self) -> None:
22
+ super().__init__()
23
+ self.genFeedbacks = [XMLTags.GENFEEDB]
24
+
25
+ def setAnswers(self) -> list[ET.Element]:
26
+ result = self.rawInput[DFIndex.RESULT]
27
+ ansEle: list[ET.Element] = []
28
+ ansEle.append(self.getNumericAnsElement(result=result))
29
+ return ansEle