excel2moodle 0.3.5__py3-none-any.whl → 0.3.7__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.
@@ -3,7 +3,6 @@ import re
3
3
 
4
4
  import lxml.etree as ET
5
5
  import pandas as pd
6
- from asteval import Interpreter
7
6
 
8
7
  import excel2moodle.core.etHelpers as eth
9
8
  from excel2moodle.core import stringHelpers
@@ -14,7 +13,6 @@ from excel2moodle.core.globals import (
14
13
  XMLTags,
15
14
  feedbackStr,
16
15
  feedBElements,
17
- parserSettings,
18
16
  )
19
17
  from excel2moodle.core.question import Picture, Question
20
18
  from excel2moodle.logger import LogAdapterQuestionID
@@ -32,16 +30,19 @@ class QuestionParser:
32
30
  Important to implement the answers methods.
33
31
  """
34
32
 
35
- def __init__(self, question: Question, data: dict) -> None:
33
+ def __init__(self) -> None:
36
34
  """Initialize the general Question parser."""
35
+ self.genFeedbacks: list[XMLTags] = []
36
+ self.logger: logging.LoggerAdapter
37
+
38
+ def setup(self, question: Question) -> None:
37
39
  self.question: Question = question
38
- self.rawInput = data
40
+ self.rawInput = question.rawData
39
41
  self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.question.id})
40
42
  self.logger.debug(
41
43
  "The following Data was provided: %s",
42
44
  self.rawInput,
43
45
  )
44
- self.genFeedbacks: list[XMLTags] = []
45
46
 
46
47
  def hasPicture(self) -> bool:
47
48
  """Create a ``Picture`` object ``question``if the question needs a pic."""
@@ -75,12 +76,12 @@ class QuestionParser:
75
76
  bps: str = self.rawInput[DFIndex.BPOINTS]
76
77
  try:
77
78
  bulletList = self.formatBulletList(bps)
78
- except IndexError as e:
79
+ except IndexError:
79
80
  msg = f"konnt Bullet Liste {self.question.id} nicht generieren"
80
81
  raise QNotParsedException(
81
82
  msg,
82
83
  self.question.id,
83
- exc_info=e,
84
+ # exc_info=e,
84
85
  )
85
86
  self.logger.debug(
86
87
  "Generated BPoint List: \n %s",
@@ -90,7 +91,7 @@ class QuestionParser:
90
91
 
91
92
  def formatBulletList(self, bps: str) -> ET.Element:
92
93
  self.logger.debug("Formatting the bulletpoint list")
93
- li: list[str] = stringHelpers.stripWhitespace(bps.split(";"))
94
+ li: list[str] = stringHelpers.getListFromStr(bps)
94
95
  name = []
95
96
  var = []
96
97
  quant = []
@@ -138,24 +139,12 @@ class QuestionParser:
138
139
  elif txtEle is True:
139
140
  self.tmpEle.append(eth.getTextElement(eleName, t, **attribs))
140
141
 
141
- def appendFromSettings(self, key="standards") -> None:
142
- """Appends 1 to 1 mapped Elements defined in the parserSettings to the element."""
143
- parser = ["Parser"]
144
- if isinstance(self, MCQuestionParser):
145
- parser.append("MCParser")
146
- elif isinstance(self, NFQuestionParser):
147
- parser.append("NFParser")
148
- for p in parser:
149
- try:
150
- for k, v in parserSettings[p][key].items():
151
- self.appendToTmpEle(k, text=v)
152
- except KeyError as e:
153
- msg = f"Invalider Input aus den Einstellungen Parser: {
154
- type(p) =}"
155
- self.logger.exception(msg, exc_info=e)
156
- raise QNotParsedException(msg, self.question.id, exc_info=e)
142
+ def _appendStandardTags(self) -> None:
143
+ """Append the elements defined in the ``cls.standardTags``."""
144
+ for k, v in type(self.question).standardTags.items():
145
+ self.appendToTmpEle(k, text=v)
157
146
 
158
- def parse(self, xmlTree: ET._Element | None = None) -> None:
147
+ def parse(self) -> None:
159
148
  """Parse the Question.
160
149
 
161
150
  Generates an new Question Element stored as ``self.tmpEle:ET.Element``
@@ -163,7 +152,6 @@ class QuestionParser:
163
152
  """
164
153
  self.logger.info("Starting to parse")
165
154
  self.tmpEle = ET.Element(XMLTags.QUESTION, type=self.question.moodleType)
166
- # self.tmpEle.set(XMLTags.TYPE, self.question.moodleType)
167
155
  self.appendToTmpEle(XMLTags.NAME, text=DFIndex.NAME, txtEle=True)
168
156
  self.appendToTmpEle(XMLTags.ID, text=self.question.id)
169
157
  if self.hasPicture():
@@ -171,11 +159,9 @@ class QuestionParser:
171
159
  self.tmpEle.append(ET.Element(XMLTags.QTEXT, format="html"))
172
160
  self.appendToTmpEle(XMLTags.POINTS, text=str(self.question.points))
173
161
  self.appendToTmpEle(XMLTags.PENALTY, text="0.3333")
174
- self.appendFromSettings()
162
+ self._appendStandardTags()
175
163
  for feedb in self.genFeedbacks:
176
164
  self.tmpEle.append(eth.getFeedBEle(feedb))
177
- if xmlTree is not None:
178
- xmlTree.append(self.tmpEle)
179
165
  ansList = self.setAnswers()
180
166
  self.setMainText()
181
167
  self.setBPoints()
@@ -251,145 +237,3 @@ class QuestionParser:
251
237
  tolerancePercent = 100 * tolerance if tolerance < 1 else tolerance
252
238
  self.logger.debug("Using tolerance %s percent", tolerancePercent)
253
239
  return int(tolerancePercent)
254
-
255
-
256
- class NFQuestionParser(QuestionParser):
257
- """Subclass for parsing numeric questions."""
258
-
259
- def __init__(self, *args) -> None:
260
- super().__init__(*args)
261
- self.genFeedbacks = [XMLTags.GENFEEDB]
262
-
263
- def setAnswers(self) -> list[ET.Element]:
264
- result = self.rawInput[DFIndex.RESULT]
265
- ansEle: list[ET.Element] = []
266
- tol = self.rawInput[DFIndex.TOLERANCE]
267
- ansEle.append(self.getNumericAnsElement(result=result, tolerance=tol))
268
- return ansEle
269
-
270
-
271
- class NFMQuestionParser(QuestionParser):
272
- def __init__(self, *args) -> None:
273
- super().__init__(*args)
274
- self.genFeedbacks = [XMLTags.GENFEEDB]
275
- self.astEval = Interpreter()
276
-
277
- def setAnswers(self) -> None:
278
- equation = self.rawInput[DFIndex.RESULT]
279
- bps = str(self.rawInput[DFIndex.BPOINTS])
280
- ansElementsList: list[ET.Element] = []
281
- varNames: list[str] = self._getVarsList(bps)
282
- self.question.variables, number = self._getVariablesDict(varNames)
283
- for n in range(number):
284
- self._setupAstIntprt(self.question.variables, n)
285
- result = self.astEval(equation)
286
- if isinstance(result, float):
287
- tol = self.rawInput[DFIndex.TOLERANCE]
288
- ansElementsList.append(
289
- self.getNumericAnsElement(result=round(result, 3), tolerance=tol),
290
- )
291
- self.question.answerVariants = ansElementsList
292
- self.setVariants(len(ansElementsList))
293
-
294
- def setVariants(self, number: int) -> None:
295
- self.question.variants = number
296
- mvar = self.question.category.maxVariants
297
- if mvar is None:
298
- self.question.category.maxVariants = number
299
- else:
300
- self.question.category.maxVariants = min(number, mvar)
301
-
302
- def _setupAstIntprt(self, var: dict[str, list[float | int]], index: int) -> None:
303
- """Setup the asteval Interpreter with the variables."""
304
- for name, value in var.items():
305
- self.astEval.symtable[name] = value[index]
306
-
307
- def _getVariablesDict(self, keyList: list) -> tuple[dict[str, list[float]], int]:
308
- """Liest alle Variablen-Listen deren Name in ``keyList`` ist aus dem DataFrame im Column[index]."""
309
- dic: dict = {}
310
- num: int = 0
311
- for k in keyList:
312
- val = self.rawInput[k]
313
- if isinstance(val, str):
314
- li = stringHelpers.stripWhitespace(val.split(";"))
315
- num = len(li)
316
- vars: list[float] = [float(i.replace(",", ".")) for i in li]
317
- dic[str(k)] = vars
318
- else:
319
- dic[str(k)] = [str(val)]
320
- num = 1
321
- return dic, num
322
-
323
- @staticmethod
324
- def _getVarsList(bps: str | list[str]) -> list:
325
- """Durchsucht den bulletPoints String nach den Variablen, die als "{var}" gekennzeichnet sind."""
326
- vars = []
327
- if isinstance(bps, list):
328
- for _p in bps:
329
- vars.extend(re.findall(r"\{\w\}", str(bps)))
330
- else:
331
- vars = re.findall(r"\{\w\}", str(bps))
332
- variablen = []
333
- for v in vars:
334
- variablen.append(v.strip("{}"))
335
- return variablen
336
-
337
-
338
- class MCQuestionParser(QuestionParser):
339
- def __init__(self, *args) -> None:
340
- super().__init__(*args)
341
- self.genFeedbacks = [
342
- XMLTags.CORFEEDB,
343
- XMLTags.PCORFEEDB,
344
- XMLTags.INCORFEEDB,
345
- ]
346
-
347
- def getAnsElementsList(
348
- self,
349
- answerList: list,
350
- fraction: float = 50,
351
- format="html",
352
- ) -> list[ET.Element]:
353
- elementList: list[ET.Element] = []
354
- for ans in answerList:
355
- p = TextElements.PLEFT.create()
356
- p.text = str(ans)
357
- text = eth.getCdatTxtElement(p)
358
- elementList.append(
359
- ET.Element(XMLTags.ANSWER, fraction=str(fraction), format=format),
360
- )
361
- elementList[-1].append(text)
362
- if fraction < 0:
363
- elementList[-1].append(
364
- eth.getFeedBEle(
365
- XMLTags.ANSFEEDBACK,
366
- text=feedbackStr["wrong"],
367
- style=TextElements.SPANRED,
368
- ),
369
- )
370
- elif fraction > 0:
371
- elementList[-1].append(
372
- eth.getFeedBEle(
373
- XMLTags.ANSFEEDBACK,
374
- text=feedbackStr["right"],
375
- style=TextElements.SPANGREEN,
376
- ),
377
- )
378
- return elementList
379
-
380
- def setAnswers(self) -> list[ET.Element]:
381
- ansStyle = self.rawInput[DFIndex.ANSTYPE]
382
- true = stringHelpers.stripWhitespace(self.rawInput[DFIndex.TRUE].split(";"))
383
- trueAnsList = stringHelpers.texWrapper(true, style=ansStyle)
384
- self.logger.debug(f"got the following true answers \n {trueAnsList=}")
385
- false = stringHelpers.stripWhitespace(self.rawInput[DFIndex.FALSE].split(";"))
386
- falseAnsList = stringHelpers.texWrapper(false, style=ansStyle)
387
- self.logger.debug(f"got the following false answers \n {falseAnsList=}")
388
- truefrac = 1 / len(trueAnsList) * 100
389
- falsefrac = 1 / len(trueAnsList) * (-100)
390
- self.tmpEle.find(XMLTags.PENALTY).text = str(round(truefrac / 100, 4))
391
- ansList = self.getAnsElementsList(trueAnsList, fraction=round(truefrac, 4))
392
- ansList.extend(
393
- self.getAnsElementsList(falseAnsList, fraction=round(falsefrac, 4)),
394
- )
395
- return ansList
@@ -1,14 +1,17 @@
1
1
  import base64
2
2
  import logging
3
3
  import re
4
+ import typing
4
5
  from pathlib import Path
5
6
  from re import Match
6
7
 
7
8
  import lxml.etree as ET
8
9
 
9
10
  from excel2moodle.core import etHelpers
11
+ from excel2moodle.core.category import Category
10
12
  from excel2moodle.core.exceptions import QNotParsedException
11
13
  from excel2moodle.core.globals import (
14
+ DFIndex,
12
15
  TextElements,
13
16
  XMLTags,
14
17
  questionTypes,
@@ -19,22 +22,37 @@ loggerObj = logging.getLogger(__name__)
19
22
 
20
23
 
21
24
  class Question:
25
+ standardTags: typing.ClassVar[dict[str, str | float]] = {
26
+ "hidden": 0,
27
+ }
28
+
29
+ def __init_subclass__(cls, **kwargs) -> None:
30
+ super().__init_subclass__(**kwargs)
31
+ subclassTags = getattr(cls, "standartTags", {})
32
+ superclassTags = super(cls, cls).standardTags
33
+ mergedTags = superclassTags.copy()
34
+ mergedTags.update(subclassTags)
35
+ cls.standardTags = mergedTags
36
+
37
+ @classmethod
38
+ def addStandardTags(cls, key, value) -> None:
39
+ cls.standardTags[key] = value
40
+
22
41
  def __init__(
23
42
  self,
24
- category,
25
- name: str,
26
- number: int,
43
+ category: Category,
44
+ rawData: dict[str, float | str | int | list[str]],
27
45
  parent=None,
28
- qtype: str = "type",
29
46
  points: float = 0,
30
47
  ) -> None:
48
+ self.rawData = rawData
31
49
  self.category = category
32
50
  self.katName = self.category.name
33
- self.name = name
34
- self.number = number
51
+ self.name: str = self.rawData.get(DFIndex.NAME)
52
+ self.number: int = self.rawData.get(DFIndex.NUMBER)
35
53
  self.parent = parent
36
- self.qtype: str = qtype
37
- self.moodleType = questionTypes[qtype]
54
+ self.qtype: str = self.rawData.get(DFIndex.TYPE)
55
+ self.moodleType = questionTypes[self.qtype]
38
56
  self.points = points if points != 0 else self.category.points
39
57
  self.element: ET.Element | None = None
40
58
  self.picture: Picture
@@ -45,7 +63,6 @@ class Question:
45
63
  self.variants: int | None = None
46
64
  self.variables: dict[str, list[float | int]] = {}
47
65
  self.setID()
48
- self.standardTags = {"hidden": "false"}
49
66
  self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.id})
50
67
  self.logger.debug("Sucess initializing")
51
68
 
@@ -143,11 +160,11 @@ class Picture:
143
160
  exc_info=e,
144
161
  )
145
162
 
146
- def __getBase64Img(self, imgPath):
163
+ def _getBase64Img(self, imgPath):
147
164
  with open(imgPath, "rb") as img:
148
165
  return base64.b64encode(img.read()).decode("utf-8")
149
166
 
150
- def __setImgElement(self, dir: Path, picID: int) -> None:
167
+ def _setImgElement(self, dir: Path, picID: int) -> None:
151
168
  """Gibt das Bild im dirPath mit dir qID als base64 encodiert mit den entsprechenden XML-Tags zurück."""
152
169
  self.path: Path = (dir / str(picID)).with_suffix(".svg")
153
170
  self.element: ET.Element = ET.Element(
@@ -156,11 +173,11 @@ class Picture:
156
173
  path="/",
157
174
  encoding="base64",
158
175
  )
159
- self.element.text = self.__getBase64Img(self.path)
176
+ self.element.text = self._getBase64Img(self.path)
160
177
 
161
178
  def __getImg(self) -> bool:
162
179
  try:
163
- self.__setImgElement(self.imgFolder, int(self.picID))
180
+ self._setImgElement(self.imgFolder, int(self.picID))
164
181
  self.htmlTag = ET.Element(
165
182
  "img",
166
183
  src=f"@@PLUGINFILE@@/{self.path.name}",
@@ -6,9 +6,11 @@ from pathlib import Path
6
6
  import lxml.etree as ET
7
7
 
8
8
 
9
- def stripWhitespace(stringList):
10
- stripped = []
11
- for i in stringList:
9
+ def getListFromStr(stringList: str) -> list[str]:
10
+ """Get a python List of strings from a semi-colon separated string."""
11
+ stripped: list[str] = []
12
+ li = stringList.split(";")
13
+ for i in li:
12
14
  s = i.strip()
13
15
  if s:
14
16
  stripped.append(s)
@@ -20,32 +22,6 @@ def stringToFloat(string: str) -> float:
20
22
  return float(string)
21
23
 
22
24
 
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
25
  def getBase64Img(imgPath):
50
26
  with open(imgPath, "rb") as img:
51
27
  return base64.b64encode(img.read()).decode("utf-8")
@@ -62,7 +38,7 @@ def getUnitsElementAsString(unit) -> None:
62
38
 
63
39
 
64
40
  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."""
41
+ """Prints the document tree of ``xmlTree`` to ``file``, if specified, else dumps to stdout."""
66
42
  documentTree = ET.ElementTree(xmlElement)
67
43
  if file is not None:
68
44
  if file.parent.exists():
@@ -73,11 +49,11 @@ def printDom(xmlElement: ET.Element, file: Path | None = None) -> None:
73
49
  pretty_print=True,
74
50
  )
75
51
  else:
76
- pass
52
+ print(xmlElement.tostring()) # noqa: T201
77
53
 
78
54
 
79
55
  def texWrapper(text: str | list[str], style: str) -> list[str]:
80
- r"""Puts the strings inside ``text`` into a LaTex environment.
56
+ r"""Put the strings inside ``text`` into a LaTex environment.
81
57
 
82
58
  if ``style == unit``: inside ``\\mathrm{}``
83
59
  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,31 +63,35 @@ 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):
@@ -100,6 +104,7 @@ class Validator:
100
104
  self.qdata[idx] = [existing, val]
101
105
  else:
102
106
  self.qdata[idx] = val
107
+ return self.qdata
103
108
 
104
109
  def _mandatory(self) -> tuple[bool, DFIndex | None]:
105
110
  """Detects if all keys of mandatory are filled with values."""
@@ -127,18 +132,8 @@ class Validator:
127
132
  invalid.append(field)
128
133
  for field, typ in self.optional.items():
129
134
  if field in self.df:
130
- if not isinstance(self.df[field], typ) and pd.notna(self.df[field]):
135
+ if pd.notna(self.df[field]) and not isinstance(self.df[field], typ):
131
136
  invalid.append(field)
132
137
  if len(invalid) == 0:
133
138
  return True, None
134
139
  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
@@ -90,7 +90,6 @@ class LogWindowHandler(logging.Handler):
90
90
  log_message = self.format(record)
91
91
  color = self.logLevelColors.get(record.levelname, "black")
92
92
  prettyMessage = f'<span style="color:{color};">{log_message}</span>'
93
- print("emitting new log signal") # noqa:T201
94
93
  self.emitter.signal.emit(prettyMessage)
95
94
 
96
95
 
@@ -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,93 @@
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.globals import (
10
+ DFIndex,
11
+ TextElements,
12
+ XMLTags,
13
+ feedbackStr,
14
+ )
15
+ from excel2moodle.core.parser import QuestionParser
16
+ from excel2moodle.core.question import Question
17
+
18
+
19
+ class MCQuestion(Question):
20
+ """Multiple-choice Question Implementation."""
21
+
22
+ standardTags: ClassVar[dict[str, str | float]] = {
23
+ "single": "false",
24
+ "shuffleanswers": "true",
25
+ "answernumbering": "abc",
26
+ "showstandardinstruction": "0",
27
+ "shownumcorrect": "",
28
+ }
29
+
30
+ def __init__(self, *args, **kwargs) -> None:
31
+ super().__init__(*args, **kwargs)
32
+
33
+
34
+ class MCQuestionParser(QuestionParser):
35
+ """Parser for the multiple choice Question."""
36
+
37
+ def __init__(self) -> None:
38
+ super().__init__()
39
+ self.genFeedbacks = [
40
+ XMLTags.CORFEEDB,
41
+ XMLTags.PCORFEEDB,
42
+ XMLTags.INCORFEEDB,
43
+ ]
44
+
45
+ def getAnsElementsList(
46
+ self,
47
+ answerList: list,
48
+ fraction: float = 50,
49
+ format="html",
50
+ ) -> list[ET.Element]:
51
+ elementList: list[ET.Element] = []
52
+ for ans in answerList:
53
+ p = TextElements.PLEFT.create()
54
+ p.text = str(ans)
55
+ text = eth.getCdatTxtElement(p)
56
+ elementList.append(
57
+ ET.Element(XMLTags.ANSWER, fraction=str(fraction), format=format),
58
+ )
59
+ elementList[-1].append(text)
60
+ if fraction < 0:
61
+ elementList[-1].append(
62
+ eth.getFeedBEle(
63
+ XMLTags.ANSFEEDBACK,
64
+ text=feedbackStr["wrong"],
65
+ style=TextElements.SPANRED,
66
+ ),
67
+ )
68
+ elif fraction > 0:
69
+ elementList[-1].append(
70
+ eth.getFeedBEle(
71
+ XMLTags.ANSFEEDBACK,
72
+ text=feedbackStr["right"],
73
+ style=TextElements.SPANGREEN,
74
+ ),
75
+ )
76
+ return elementList
77
+
78
+ def setAnswers(self) -> list[ET.Element]:
79
+ ansStyle = self.rawInput[DFIndex.ANSTYPE]
80
+ true = stringHelpers.getListFromStr(self.rawInput[DFIndex.TRUE])
81
+ trueAnsList = stringHelpers.texWrapper(true, style=ansStyle)
82
+ self.logger.debug(f"got the following true answers \n {trueAnsList=}")
83
+ false = stringHelpers.getListFromStr(self.rawInput[DFIndex.FALSE])
84
+ falseAnsList = stringHelpers.texWrapper(false, style=ansStyle)
85
+ self.logger.debug(f"got the following false answers \n {falseAnsList=}")
86
+ truefrac = 1 / len(trueAnsList) * 100
87
+ falsefrac = 1 / len(trueAnsList) * (-100)
88
+ self.tmpEle.find(XMLTags.PENALTY).text = str(round(truefrac / 100, 4))
89
+ ansList = self.getAnsElementsList(trueAnsList, fraction=round(truefrac, 4))
90
+ ansList.extend(
91
+ self.getAnsElementsList(falseAnsList, fraction=round(falsefrac, 4)),
92
+ )
93
+ return ansList