excel2moodle 0.3.3__py3-none-any.whl → 0.3.5__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.
@@ -1,19 +1,33 @@
1
- import logging
2
- import lxml.etree as ET
1
+ import base64
2
+ import logging
3
+ import re
3
4
  from pathlib import Path
4
- import base64 as base64
5
-
6
- from excel2moodle.core import category, etHelpers
7
- from excel2moodle.core.globals import XMLTags, TextElements, DFIndex, questionTypes, parserSettings
8
- from excel2moodle.core.exceptions import QNotParsedException
9
- from typing import Match
10
- import re as re
5
+ from re import Match
11
6
 
7
+ import lxml.etree as ET
12
8
 
13
- logger = logging.getLogger(__name__)
14
-
15
- class Question():
16
- def __init__(self, category,name:str, number:int, parent=None, qtype:str="type", points:float=0):
9
+ from excel2moodle.core import etHelpers
10
+ from excel2moodle.core.exceptions import QNotParsedException
11
+ from excel2moodle.core.globals import (
12
+ TextElements,
13
+ XMLTags,
14
+ questionTypes,
15
+ )
16
+ from excel2moodle.logger import LogAdapterQuestionID
17
+
18
+ loggerObj = logging.getLogger(__name__)
19
+
20
+
21
+ class Question:
22
+ def __init__(
23
+ self,
24
+ category,
25
+ name: str,
26
+ number: int,
27
+ parent=None,
28
+ qtype: str = "type",
29
+ points: float = 0,
30
+ ) -> None:
17
31
  self.category = category
18
32
  self.katName = self.category.name
19
33
  self.name = name
@@ -21,73 +35,75 @@ class Question():
21
35
  self.parent = parent
22
36
  self.qtype: str = qtype
23
37
  self.moodleType = questionTypes[qtype]
24
- self.points = ( points if points != 0 else self.category.points)
25
- self.element: ET.Element|None=None
26
- self.picture:Picture
27
- self.id:str
38
+ self.points = points if points != 0 else self.category.points
39
+ self.element: ET.Element | None = None
40
+ self.picture: Picture
41
+ self.id: str
28
42
  self.qtextParagraphs: list[ET.Element] = []
29
- self.bulletList: ET.Element|None = None
43
+ self.bulletList: ET.Element | None = None
30
44
  self.answerVariants: list[ET.Element] = []
31
- self.variants:int|None = None
32
- self.variables: dict[str, list[float|int]] = {}
45
+ self.variants: int | None = None
46
+ self.variables: dict[str, list[float | int]] = {}
33
47
  self.setID()
34
- self.standardTags = {
35
- "hidden":"false"
36
- }
37
- logger.debug(f"Question {self.id} is initialized")
48
+ self.standardTags = {"hidden": "false"}
49
+ self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.id})
50
+ self.logger.debug("Sucess initializing")
38
51
 
39
- def __repr__(self)->str:
40
- li:list[str] = []
52
+ def __repr__(self) -> str:
53
+ li: list[str] = []
41
54
  li.append(f"Question v{self.category.version}")
42
- li.append(f'{self.id=}')
43
- li.append(f'{self.parent=}')
55
+ li.append(f"{self.id=}")
56
+ li.append(f"{self.parent=}")
44
57
  return "\n".join(li)
45
58
 
46
- def assemble(self, variant:int=1)->None:
47
- textElements:list[ET.Element] = []
59
+ def assemble(self, variant: int = 1) -> None:
60
+ textElements: list[ET.Element] = []
48
61
  textElements.extend(self.qtextParagraphs)
49
- logger.debug(f"Starting assembly of { self.id }")
62
+ self.logger.debug("Starting assembly")
50
63
  if self.element is not None:
51
64
  mainText = self.element.find(XMLTags.QTEXT)
52
- logger.debug(f"found existing Text in element {mainText = }")
65
+ self.logger.debug(f"found existing Text in element {mainText=}")
53
66
  txtele = mainText.find("text")
54
67
  if txtele is not None:
55
68
  mainText.remove(txtele)
56
- logger.debug(f"removed prevously existing questiontext")
57
- else: raise QNotParsedException("Cant assamble, if element is none", self.id)
58
- if self.variants is not None:
59
- textElements.append(self.getBPointVariant(variant-1))
69
+ self.logger.debug("removed previously existing questiontext")
60
70
  else:
71
+ msg = "Cant assamble, if element is none"
72
+ raise QNotParsedException(msg, self.id)
73
+ if self.variants is not None:
74
+ textElements.append(self.getBPointVariant(variant - 1))
75
+ elif self.bulletList is not None:
61
76
  textElements.append(self.bulletList)
62
77
  if hasattr(self, "picture") and self.picture.ready:
63
78
  textElements.append(self.picture.htmlTag)
64
79
  mainText.append(self.picture.element)
65
80
  mainText.append(etHelpers.getCdatTxtElement(textElements))
66
81
  # self.element.insert(3, mainText)
67
- logger.debug(f"inserted MainText to question element")
68
- if len( self.answerVariants ) > 0:
82
+ self.logger.debug("inserted MainText to element")
83
+ if len(self.answerVariants) > 0:
69
84
  ans = self.element.find(XMLTags.ANSWER)
70
85
  if ans is not None:
71
86
  self.element.remove(ans)
72
- logger.debug("removed previous answer element")
73
- self.element.insert(5, self.answerVariants[variant-1])
74
- return None
87
+ self.logger.debug("removed previous answer element")
88
+ self.element.insert(5, self.answerVariants[variant - 1])
75
89
 
76
- def setID(self, id = 0)->None:
90
+ def setID(self, id=0) -> None:
77
91
  if id == 0:
78
92
  self.id: str = f"{self.category.id}{self.number:02d}"
79
- else: self.id:str = str(id)
93
+ else:
94
+ self.id: str = str(id)
80
95
 
81
- def getBPointVariant(self, variant:int)->ET.Element:
96
+ def getBPointVariant(self, variant: int) -> ET.Element:
82
97
  if self.bulletList is None:
83
98
  return None
84
- varPlaceholder = re.compile(r"{(\w+)}") # matches {a}, {some_var}, etc.
99
+ # matches {a}, {some_var}, etc.
100
+ varPlaceholder = re.compile(r"{(\w+)}")
85
101
 
86
- def replaceMatch(match: Match[str])->str|int|float:
102
+ def replaceMatch(match: Match[str]) -> str | int | float:
87
103
  key = match.group(1)
88
104
  if key in self.variables:
89
105
  value = self.variables[key][variant]
90
- return f"{value}".replace(".",",\\!")
106
+ return f"{value}".replace(".", ",\\!")
91
107
  return match.group(0) # keep original if no match
92
108
 
93
109
  unorderedList = TextElements.ULIST.create()
@@ -95,52 +111,67 @@ class Question():
95
111
  listItemText = li.text or ""
96
112
  bullet = TextElements.LISTITEM.create()
97
113
  bullet.text = varPlaceholder.sub(replaceMatch, listItemText)
98
- logger.debug(f"Inserted Variables into List: {bullet}")
114
+ self.logger.debug(f"Inserted Variables into List: {bullet}")
99
115
  unorderedList.append(bullet)
100
116
  return unorderedList
101
117
 
102
118
 
103
- class Picture():
104
- def __init__(self, picKey:str, imgFolder:Path, question:Question):
119
+ class Picture:
120
+ def __init__(self, picKey: str, imgFolder: Path, question: Question) -> None:
105
121
  self.pic = picKey
106
- self.ready:bool = False
122
+ self.ready: bool = False
107
123
  self.question = question
124
+ self.logger = LogAdapterQuestionID(loggerObj, {"qID": self.question.id})
108
125
  self.imgFolder = (imgFolder / question.katName).resolve()
109
- self.htmlTag:ET.Element
110
- self.path:Path
126
+ self.htmlTag: ET.Element
127
+ self.path: Path
111
128
  self._setPath()
112
- if hasattr(self, 'picID'):
129
+ if hasattr(self, "picID"):
113
130
  self.ready = self.__getImg()
114
131
 
115
- def _setPath(self):
132
+ def _setPath(self) -> None:
116
133
  if self.pic == 1:
117
134
  self.picID = self.question.id
118
135
  else:
119
136
  selectedPic = self.pic[2:]
120
- logger.debug(f"got a picture key {selectedPic =}")
137
+ self.logger.debug("Got the picture key: %s", selectedPic)
121
138
  try:
122
139
  self.picID = f"{self.question.category.id}{int(selectedPic):02d}"
123
140
  except ValueError as e:
124
- logger.warning(msg=f"Bild-ID konnte aus dem Key: {self.pic = }nicht festgestellt werden", exc_info=e)
141
+ self.logger.warning(
142
+ msg=f"Bild-ID konnte aus dem Key: {self.pic} nicht festgestellt werden",
143
+ exc_info=e,
144
+ )
125
145
 
126
146
  def __getBase64Img(self, imgPath):
127
- with open(imgPath, 'rb') as img:
128
- img64 = base64.b64encode(img.read()).decode('utf-8')
129
- return img64
130
-
131
- def __setImgElement(self, dir:Path, picID:int)->None:
132
- """gibt das Bild im dirPath mit dir qID als base64 encodiert mit den entsprechenden XML-Tags zurück"""
133
- self.path:Path = ( dir/ str(picID) ).with_suffix('.svg')
134
- self.element:ET.Element = ET.Element("file", name=f'{self.path.name}', path='/', encoding='base64')
147
+ with open(imgPath, "rb") as img:
148
+ return base64.b64encode(img.read()).decode("utf-8")
149
+
150
+ def __setImgElement(self, dir: Path, picID: int) -> None:
151
+ """Gibt das Bild im dirPath mit dir qID als base64 encodiert mit den entsprechenden XML-Tags zurück."""
152
+ self.path: Path = (dir / str(picID)).with_suffix(".svg")
153
+ self.element: ET.Element = ET.Element(
154
+ "file",
155
+ name=f"{self.path.name}",
156
+ path="/",
157
+ encoding="base64",
158
+ )
135
159
  self.element.text = self.__getBase64Img(self.path)
136
160
 
137
-
138
- def __getImg(self)->bool:
161
+ def __getImg(self) -> bool:
139
162
  try:
140
163
  self.__setImgElement(self.imgFolder, int(self.picID))
141
- self.htmlTag = ET.Element("img", src=f"@@PLUGINFILE@@/{self.path.name}", alt=f"Bild {self.path.name}", width="500")
164
+ self.htmlTag = ET.Element(
165
+ "img",
166
+ src=f"@@PLUGINFILE@@/{self.path.name}",
167
+ alt=f"Bild {self.path.name}",
168
+ width="500",
169
+ )
142
170
  return True
143
171
  except FileNotFoundError as e:
144
- logger.warning(msg=f"Bild {self.picID} konnte nicht gefunden werden ", exc_info=e)
172
+ self.logger.warning(
173
+ msg=f"Bild {self.picID} konnte nicht gefunden werden ",
174
+ exc_info=e,
175
+ )
145
176
  self.element = None
146
177
  return False
@@ -1,51 +1,57 @@
1
- """This Module checks if the data inside the Spreadsheet is valid
1
+ """This Module checks if the data inside the Spreadsheet is valid.
2
2
 
3
3
  Those things are considered:
4
4
 
5
5
  #. The mandatory entries must not be ``Nan``
6
6
  #. All fields must have the right data-type
7
7
 
8
- If Those checks pass, a question is created, which can be accessed via ``Validator.question``
8
+ If Those checks pass, a question is created,
9
+ which can be accessed via ``Validator.question``
9
10
  """
10
11
 
11
- from types import UnionType
12
+ import logging
13
+ from typing import TYPE_CHECKING
12
14
 
13
- from pandas.core.series import notna
14
- from excel2moodle.core.question import Question
15
- from excel2moodle.core.globals import DFIndex
16
- from excel2moodle.core.exceptions import InvalidFieldException, NanException
17
15
  import pandas as pd
18
- import logging
16
+
17
+ from excel2moodle.core.exceptions import InvalidFieldException
18
+ from excel2moodle.core.globals import DFIndex
19
+ from excel2moodle.core.question import Question
20
+
21
+ if TYPE_CHECKING:
22
+ from types import UnionType
19
23
 
20
24
  logger = logging.getLogger(__name__)
21
25
 
22
26
 
23
- class Validator():
27
+ class Validator:
28
+ """Validate the question data from the spreadsheet.
29
+
30
+ Creates a dictionary with the data, for easier access later.
31
+ """
32
+
24
33
  def __init__(self, category) -> None:
25
- self.question:Question
34
+ self.question: Question
26
35
  self.category = category
27
- self.mandatory: dict[DFIndex, type|UnionType] = {
36
+ self.mandatory: dict[DFIndex, type | UnionType] = {
28
37
  DFIndex.TEXT: str,
29
38
  DFIndex.NAME: str,
30
39
  DFIndex.TYPE: str,
31
40
  }
32
- self.optional: dict[DFIndex, type|UnionType] = {
33
- DFIndex.BPOINTS : str,
34
- DFIndex.NAME: str,
35
- DFIndex.PICTURE: int|str,
36
- }
37
- self.nfOpt: dict[DFIndex, type|UnionType] = {
38
- DFIndex.RESULT: float|int,
39
- }
40
- self.nfMand: dict[DFIndex, type|UnionType] = {
41
- }
42
- self.nfmOpt: dict[DFIndex, type|UnionType] = {
41
+ self.optional: dict[DFIndex, type | UnionType] = {
42
+ DFIndex.BPOINTS: str,
43
+ DFIndex.PICTURE: int | str,
43
44
  }
44
- self.nfmMand: dict[DFIndex, type|UnionType] = {
45
+ self.nfOpt: dict[DFIndex, type | UnionType] = {}
46
+ self.nfMand: dict[DFIndex, type | UnionType] = {
47
+ DFIndex.RESULT: float | int,
45
48
  }
46
- self.mcOpt: dict[DFIndex, type|UnionType] = {
49
+ self.nfmOpt: dict[DFIndex, type | UnionType] = {}
50
+ self.nfmMand: dict[DFIndex, type | UnionType] = {
51
+ DFIndex.RESULT: str,
47
52
  }
48
- self.mcMand: dict[DFIndex, type|UnionType] = {
53
+ self.mcOpt: dict[DFIndex, type | UnionType] = {}
54
+ self.mcMand: dict[DFIndex, type | UnionType] = {
49
55
  DFIndex.TRUE: str,
50
56
  DFIndex.FALSE: str,
51
57
  DFIndex.ANSTYPE: str,
@@ -57,15 +63,15 @@ class Validator():
57
63
  "NFM": (self.nfmOpt, self.nfmMand),
58
64
  }
59
65
 
60
- def setup(self, df:pd.Series, index:int)->bool:
66
+ def setup(self, df: pd.Series, index: int) -> bool:
61
67
  self.df = df
62
68
  self.index = index
63
69
  typ = self.df.loc[DFIndex.TYPE]
64
70
  self.mandatory.update(self.mapper[typ][1])
65
71
  self.optional.update(self.mapper[typ][0])
66
72
  return True
67
-
68
- def validate(self )->bool:
73
+
74
+ def validate(self) -> bool:
69
75
  id = f"{self.category.id}{self.index:02d}"
70
76
  checker, missing = self._mandatory()
71
77
  if not checker:
@@ -74,32 +80,31 @@ class Validator():
74
80
  raise InvalidFieldException(msg, id, missing)
75
81
  checker, missing = self._typeCheck()
76
82
  if not checker:
77
- msg = f"Question {id} misses keys {missing}"
83
+ msg = f"Question {id} has wrong typed data {missing}"
78
84
  if missing is not None:
79
85
  raise InvalidFieldException(msg, id, missing)
80
86
  self._getQuestion()
81
87
  self._getData()
82
88
  return True
83
89
 
84
- def _getData(self)->None:
85
- self.qdata:dict[str, str|float|int|list]={}
90
+ def _getData(self) -> None:
91
+ self.qdata: dict[str, str | float | int | list] = {}
86
92
  for idx, val in self.df.items():
87
93
  if not isinstance(idx, str):
88
- logger.debug(f"Got a non String key in the spreadsheet, skipping it")
89
94
  continue
90
95
  if idx in self.qdata:
91
96
  if isinstance(self.qdata[idx], list):
92
- self.qdata[idx].append(val)
97
+ self.qdata[idx].append(val)
93
98
  else:
94
99
  existing = self.qdata[idx]
95
100
  self.qdata[idx] = [existing, val]
96
101
  else:
97
- self.qdata[idx]=val
102
+ self.qdata[idx] = val
98
103
 
99
- def _mandatory(self)->tuple[bool,DFIndex|None]:
100
- """detects if all keys of mandatory are filled with values"""
104
+ def _mandatory(self) -> tuple[bool, DFIndex | None]:
105
+ """Detects if all keys of mandatory are filled with values."""
101
106
  checker = pd.Series.notna(self.df)
102
- for k in self.mandatory.keys():
107
+ for k in self.mandatory:
103
108
  try:
104
109
  c = checker[k]
105
110
  except KeyError:
@@ -108,35 +113,32 @@ class Validator():
108
113
  if not c.any():
109
114
  return False, k
110
115
  elif not c:
111
- return False, k,
116
+ return False, k
112
117
  return True, None
113
118
 
114
- def _typeCheck(self)->tuple[bool, list[DFIndex]|None]:
115
- invalid:list[DFIndex] = []
119
+ def _typeCheck(self) -> tuple[bool, list[DFIndex] | None]:
120
+ invalid: list[DFIndex] = []
116
121
  for field, typ in self.mandatory.items():
117
122
  if isinstance(self.df[field], pd.Series):
118
123
  for f in self.df[field]:
119
- if pd.notna(f):
120
- if not isinstance(f, typ):
121
- invalid.append(field)
124
+ if pd.notna(f) and not isinstance(f, typ):
125
+ invalid.append(field)
122
126
  elif not isinstance(self.df[field], typ):
123
127
  invalid.append(field)
124
128
  for field, typ in self.optional.items():
125
129
  if field in self.df:
126
- if not isinstance(self.df[field], typ):
130
+ if not isinstance(self.df[field], typ) and pd.notna(self.df[field]):
127
131
  invalid.append(field)
128
132
  if len(invalid) == 0:
129
133
  return True, None
130
- else:
131
- return False, invalid
134
+ return False, invalid
132
135
 
133
-
134
- def _getQuestion(self)->None:
136
+ def _getQuestion(self) -> None:
135
137
  name = self.df[DFIndex.NAME]
136
138
  qtype = self.df[DFIndex.TYPE]
137
- self.question=Question(self.category,
138
- name = str(name),
139
- number = self.index,
140
- qtype = str(qtype))
141
- return None
142
-
139
+ self.question = Question(
140
+ self.category,
141
+ name=str(name),
142
+ number=self.index,
143
+ qtype=str(qtype),
144
+ )