excel2moodle 0.3.2__py3-none-any.whl → 0.3.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.
- excel2moodle/__init__.py +18 -4
- excel2moodle/__main__.py +0 -10
- excel2moodle/core/category.py +4 -33
- excel2moodle/core/dataStructure.py +5 -2
- excel2moodle/core/parser.py +59 -68
- excel2moodle/core/question.py +48 -8
- excel2moodle/core/questionValidator.py +18 -0
- excel2moodle/core/stringHelpers.py +8 -1
- excel2moodle/ui/appUi.py +36 -15
- excel2moodle/ui/dialogs.py +2 -2
- excel2moodle/ui/settings.py +5 -4
- {excel2moodle-0.3.2.dist-info → excel2moodle-0.3.3.dist-info}/METADATA +4 -4
- {excel2moodle-0.3.2.dist-info → excel2moodle-0.3.3.dist-info}/RECORD +16 -16
- {excel2moodle-0.3.2.dist-info → excel2moodle-0.3.3.dist-info}/WHEEL +1 -1
- {excel2moodle-0.3.2.dist-info → excel2moodle-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.3.2.dist-info → excel2moodle-0.3.3.dist-info}/top_level.txt +0 -0
excel2moodle/__init__.py
CHANGED
@@ -17,12 +17,27 @@ Functionality
|
|
17
17
|
* Parse Numeric Questions, each into one XML file
|
18
18
|
* create single XML File from a selection of questions
|
19
19
|
"""
|
20
|
+
from importlib import metadata
|
20
21
|
from importlib.metadata import version
|
21
22
|
try:
|
22
23
|
__version__ = version("excel2moodle")
|
23
24
|
except Exception:
|
24
25
|
__version__ = "unknown"
|
25
26
|
|
27
|
+
|
28
|
+
if not __package__ == None:
|
29
|
+
meta = metadata.metadata(__package__)
|
30
|
+
e2mMetadata:dict = {
|
31
|
+
"version": __version__,
|
32
|
+
"name": meta['name'],
|
33
|
+
"description": meta['summary'],
|
34
|
+
"author": meta['author'],
|
35
|
+
"license": meta['license-expression'],
|
36
|
+
"documentation": "https://jbosse3.gitlab.io/excel2moodle",
|
37
|
+
"homepage": meta['project-url'].split()[1],
|
38
|
+
"issues": "https://gitlab.com/jbosse3/excel2moodle/issues",
|
39
|
+
}
|
40
|
+
|
26
41
|
# from excel2moodle.core import klausurGenerator
|
27
42
|
# from excel2moodle.core import numericMultiQ
|
28
43
|
# from excel2moodle.core import questionWriter
|
@@ -36,7 +51,6 @@ from excel2moodle.ui import settings
|
|
36
51
|
from PySide6.QtCore import QObject, Signal
|
37
52
|
import logging as logging
|
38
53
|
from logging import config as logConfig
|
39
|
-
from pathlib import Path
|
40
54
|
|
41
55
|
loggerConfig = {
|
42
56
|
'version': 1,
|
@@ -108,6 +122,6 @@ logging.config.dictConfig(config=loggerConfig)
|
|
108
122
|
qSignalLogger = LogHandler()
|
109
123
|
logger.addHandler(qSignalLogger)
|
110
124
|
|
111
|
-
|
112
|
-
|
113
|
-
|
125
|
+
|
126
|
+
for k,v in e2mMetadata.items():
|
127
|
+
print(f"{k}: \t {v}\n")
|
excel2moodle/__main__.py
CHANGED
@@ -1,21 +1,11 @@
|
|
1
1
|
"""Main Function to make the Package executable"""
|
2
2
|
|
3
|
-
from pathlib import Path
|
4
|
-
|
5
3
|
from PySide6 import QtWidgets, sys
|
6
|
-
import xml.etree.ElementTree as xmlET
|
7
4
|
|
8
5
|
from excel2moodle.core import dataStructure
|
9
6
|
from excel2moodle.ui import appUi as ui
|
10
7
|
from excel2moodle.ui.settings import Settings
|
11
8
|
|
12
|
-
import logging
|
13
|
-
|
14
|
-
katOutPath = None
|
15
|
-
excelFile = None
|
16
|
-
|
17
|
-
logger = logging.getLogger(__name__)
|
18
|
-
|
19
9
|
|
20
10
|
def main()->None:
|
21
11
|
app = QtWidgets.QApplication(sys.argv)
|
excel2moodle/core/category.py
CHANGED
@@ -39,48 +39,19 @@ class Category():
|
|
39
39
|
return self.NAME == other.NAME
|
40
40
|
return False
|
41
41
|
|
42
|
-
def
|
43
|
-
self.questions:dict[int,Question] = {}
|
44
|
-
validator = Validator(self)
|
45
|
-
for q in self.dataframe.columns:
|
46
|
-
logger.debug(f"Starting to check Validity of {q}")
|
47
|
-
qdat = self.dataframe[q]
|
48
|
-
if isinstance(qdat, pd.Series):
|
49
|
-
validator.setup(qdat, q)
|
50
|
-
check = False
|
51
|
-
try:
|
52
|
-
check = validator.validate()
|
53
|
-
except InvalidFieldException as e:
|
54
|
-
logger.error(f"Frage {self.id}{q:02d} ist invalid.", exc_info=e)
|
55
|
-
if check:
|
56
|
-
self.questions[q]=validator.question
|
57
|
-
try:
|
58
|
-
self.parseQ(self.questions[q])
|
59
|
-
except QNotParsedException as e:
|
60
|
-
logger.error(f"Frage {self.questions[q].id} konnte nicht erstellt werden", exc_info=e)
|
61
|
-
return None
|
62
|
-
|
63
|
-
def parsAll(self, tree: ET.Element = None)->None:
|
64
|
-
if tree is None:
|
65
|
-
tree = ET.Element("quiz")
|
66
|
-
tree.append(self.getCategoryHeader())
|
67
|
-
for q in self.questions.values():
|
68
|
-
self.parseQ(q, tree)
|
69
|
-
|
70
|
-
def parseQ(self, q:Question, xmlTree:ET._Element|None=None)->bool:
|
42
|
+
def parseQ(self, q:Question, questionData:dict|None=None, xmlTree:ET._Element|None=None)->bool:
|
71
43
|
if q.element is not None:
|
72
44
|
logger.info(f"Question {q.id} is already parsed")
|
73
45
|
return True
|
74
46
|
else:
|
75
|
-
series = self.dataframe[q.number]
|
76
47
|
if q.qtype == "NF":
|
77
|
-
parser = NFQuestionParser( q,
|
48
|
+
parser = NFQuestionParser( q, questionData)
|
78
49
|
logger.debug(f"setup a new NF parser ")
|
79
50
|
elif q.qtype == "MC":
|
80
|
-
parser = MCQuestionParser( q,
|
51
|
+
parser = MCQuestionParser( q, questionData)
|
81
52
|
logger.debug(f"setup a new MC parser ")
|
82
53
|
elif q.qtype == "NFM":
|
83
|
-
parser = NFMQuestionParser( q,
|
54
|
+
parser = NFMQuestionParser( q, questionData)
|
84
55
|
logger.debug(f"setup a new NFM parser ")
|
85
56
|
else:
|
86
57
|
logger.error(f"ERROR, couldn't setup Parser")
|
@@ -63,6 +63,7 @@ class QuestionDB():
|
|
63
63
|
index_col=0)
|
64
64
|
logger.info("Sucessfully read categoriesMetaData")
|
65
65
|
print(self.categoriesMetaData)
|
66
|
+
self.categories = {}
|
66
67
|
for sh in excelFile.sheet_names:
|
67
68
|
if sh.startswith("KAT"):
|
68
69
|
n = int(sh[4:])
|
@@ -98,7 +99,7 @@ class QuestionDB():
|
|
98
99
|
if check:
|
99
100
|
c.questions[q]=validator.question
|
100
101
|
try:
|
101
|
-
c.parseQ(c.questions[q])
|
102
|
+
c.parseQ(c.questions[q], validator.qdata)
|
102
103
|
except QNotParsedException as e:
|
103
104
|
logger.error(f"Frage {c.questions[q].id} konnte nicht erstellt werden", exc_info=e)
|
104
105
|
|
@@ -120,6 +121,7 @@ class QuestionDB():
|
|
120
121
|
def appendQElements(self,cat:Category, qList:list[Question], tree:ET.Element, includeHeader:bool=True)->None:
|
121
122
|
if includeHeader:
|
122
123
|
tree.append( cat.getCategoryHeader())
|
124
|
+
logger.debug(f"Appended a new category item {cat=}")
|
123
125
|
sameVariant = False
|
124
126
|
variant = 1
|
125
127
|
for q in qList:
|
@@ -133,7 +135,8 @@ class QuestionDB():
|
|
133
135
|
logger.debug(f"Die Fragen-Variante {variant} wurde gewählt")
|
134
136
|
q.assemble(variant)
|
135
137
|
else: print("skipping this question")
|
136
|
-
|
138
|
+
else:
|
139
|
+
q.assemble()
|
137
140
|
tree.append(q.element)
|
138
141
|
else: logger.warning(f"Frage {q} wurde nicht erstellt")
|
139
142
|
return None
|
excel2moodle/core/parser.py
CHANGED
@@ -1,12 +1,6 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
from unicodedata import category
|
4
1
|
from asteval import Interpreter
|
5
2
|
import lxml.etree as ET
|
6
|
-
# import xml.etree.ElementTree as ET
|
7
|
-
from typing import Union
|
8
3
|
from pathlib import Path
|
9
|
-
from numpy import isin
|
10
4
|
import pandas as pd
|
11
5
|
import base64 as base64
|
12
6
|
import logging as logging
|
@@ -20,21 +14,27 @@ from excel2moodle.core import stringHelpers
|
|
20
14
|
from excel2moodle.core.question import Picture, Question
|
21
15
|
import re as re
|
22
16
|
|
17
|
+
from excel2moodle import settings
|
18
|
+
|
23
19
|
|
24
20
|
logger = logging.getLogger(__name__)
|
25
|
-
|
21
|
+
f = Path("../Fragensammlung/Abbildungen_SVG/").resolve()
|
22
|
+
settings.set("core/pictureFolder", f)
|
23
|
+
svgFolder = settings.get("core/pictureFolder", default=f)
|
24
|
+
print(svgFolder)
|
26
25
|
|
27
26
|
class QuestionParser():
|
28
|
-
def __init__(self, question:Question,
|
27
|
+
def __init__(self, question:Question, data:dict):
|
29
28
|
self.question:Question = question
|
30
|
-
self.
|
29
|
+
self.rawInput = data
|
30
|
+
logger.debug(f"The following Data was provided for the question {self.question.id}:\n {self.rawInput =}")
|
31
31
|
self.genFeedbacks:list[XMLTags] = []
|
32
32
|
|
33
33
|
def hasPicture(self)->bool:
|
34
34
|
"""Creates a ``Picture`` object inside ``question``, if the question needs a pic"""
|
35
35
|
|
36
|
-
picKey = self.
|
37
|
-
if picKey != 0 and
|
36
|
+
picKey = self.rawInput[DFIndex.PICTURE]
|
37
|
+
if picKey != 0 and picKey != 'nan':
|
38
38
|
if not hasattr(self.question, 'picture'):
|
39
39
|
self.question.picture = Picture(picKey, svgFolder, self.question)
|
40
40
|
if self.question.picture.ready:
|
@@ -44,31 +44,32 @@ class QuestionParser():
|
|
44
44
|
def setMainText(self)->None:
|
45
45
|
paragraphs:list[ET._Element]=[TextElements.PLEFT.create()]
|
46
46
|
ET.SubElement(paragraphs[0],"b").text = f"ID {self.question.id}"
|
47
|
-
text = self.
|
47
|
+
text = self.rawInput[DFIndex.TEXT]
|
48
48
|
pcount = 0
|
49
49
|
for t in text:
|
50
50
|
if not pd.isna(t):
|
51
51
|
pcount +=1
|
52
52
|
paragraphs.append(TextElements.PLEFT.create())
|
53
53
|
paragraphs[-1].text = t
|
54
|
-
self.question.
|
54
|
+
self.question.qtextParagraphs = paragraphs
|
55
55
|
logger.debug(f"Created main Text {self.question.id} with:{pcount} paragraphs")
|
56
56
|
return None
|
57
|
-
|
57
|
+
|
58
58
|
def setBPoints(self)->None:
|
59
59
|
"""If there bulletPoints are set in the Spreadsheet it creates an unordered List-Element in ``Question.bulletList``"""
|
60
|
-
if DFIndex.BPOINTS in self.
|
61
|
-
bps = self.
|
60
|
+
if DFIndex.BPOINTS in self.rawInput:
|
61
|
+
bps:str = self.rawInput[DFIndex.BPOINTS]
|
62
62
|
try:
|
63
63
|
bulletList = self.formatBulletList(bps)
|
64
64
|
except IndexError as e:
|
65
65
|
raise QNotParsedException(f"konnt Bullet Liste {self.question.id} nicht generieren", self.question.id, exc_info=e)
|
66
|
-
|
67
|
-
|
66
|
+
logger.debug(f"Generated BPoint List: \n {ET.tostring(bulletList, encoding='unicode')}")
|
67
|
+
self.question.bulletList = bulletList
|
68
68
|
return None
|
69
69
|
|
70
70
|
def formatBulletList(self,bps:str)->ET.Element:
|
71
|
-
|
71
|
+
logger.debug(f"Formatting the bulletpoint list")
|
72
|
+
li:list[str] = stringHelpers.stripWhitespace( bps.split(';'))
|
72
73
|
name = []
|
73
74
|
var = []
|
74
75
|
quant = []
|
@@ -81,17 +82,23 @@ class QuestionParser():
|
|
81
82
|
quant.append(sc_split[3])
|
82
83
|
unit.append(sc_split[4])
|
83
84
|
for i in range(0, len(name)):
|
84
|
-
|
85
|
-
|
86
|
-
num_s =
|
87
|
-
else:
|
85
|
+
if re.fullmatch(r"{\w+}", quant[i]):
|
86
|
+
logger.debug(f"Got an variable bulletItem")
|
87
|
+
num_s = quant[i]
|
88
|
+
else:
|
89
|
+
logger.debug(f"Got a normal bulletItem")
|
90
|
+
num = quant[i].split(',')
|
91
|
+
if len(num)==2:
|
92
|
+
num_s = f"{str(num[0])},\\!{str(num[1])}~"
|
93
|
+
else: num_s = f"{str(num[0])},\\!0~"
|
88
94
|
bullet = TextElements.LISTITEM.create()
|
89
95
|
bullet.text=(f"{ name[i] }: \\( {var[i]} = {num_s} \\mathrm{{ {unit[i]} }}\\)\n")
|
90
96
|
unorderedList.append(bullet)
|
91
97
|
return unorderedList
|
92
98
|
|
93
|
-
def
|
94
|
-
|
99
|
+
def appendToTmpEle(self, eleName: str, text:str|DFIndex, txtEle=False, **attribs ):
|
100
|
+
"""Appends the text to the temporary Element"""
|
101
|
+
t = self.rawInput[text] if isinstance(text, DFIndex) else text
|
95
102
|
if txtEle is False:
|
96
103
|
self.tmpEle.append(eth.getElement(eleName, t, **attribs))
|
97
104
|
elif txtEle is True:
|
@@ -107,7 +114,7 @@ class QuestionParser():
|
|
107
114
|
for p in parser:
|
108
115
|
try:
|
109
116
|
for k, v in parserSettings[p][key].items():
|
110
|
-
self.
|
117
|
+
self.appendToTmpEle(k, text=v)
|
111
118
|
except KeyError as e:
|
112
119
|
msg = f"Invalider Input aus den Einstellungen Parser: {type(p) = }"
|
113
120
|
logger.error(msg, exc_info=e)
|
@@ -120,15 +127,16 @@ class QuestionParser():
|
|
120
127
|
Generates an new Question Element stored as ``self.tmpEle:ET.Element``
|
121
128
|
if no Exceptions are raised, ``self.tmpEle`` is passed to ``self.question.element``
|
122
129
|
"""
|
130
|
+
logger.info(f"Starting to parse {self.question.id}")
|
123
131
|
self.tmpEle = ET.Element(XMLTags.QUESTION, type = self.question.moodleType)
|
124
132
|
# self.tmpEle.set(XMLTags.TYPE, self.question.moodleType)
|
125
|
-
self.
|
126
|
-
self.
|
133
|
+
self.appendToTmpEle(XMLTags.NAME, text=DFIndex.NAME, txtEle=True)
|
134
|
+
self.appendToTmpEle(XMLTags.ID, text=self.question.id)
|
127
135
|
if self.hasPicture() :
|
128
136
|
self.tmpEle.append(self.question.picture.element)
|
129
137
|
self.tmpEle.append(ET.Element(XMLTags.QTEXT, format = "html"))
|
130
|
-
self.
|
131
|
-
self.
|
138
|
+
self.appendToTmpEle(XMLTags.POINTS, text=str(self.question.points))
|
139
|
+
self.appendToTmpEle(XMLTags.PENALTY, text="0.3333")
|
132
140
|
self.appendFromSettings()
|
133
141
|
for feedb in self.genFeedbacks:
|
134
142
|
self.tmpEle.append(eth.getFeedBEle(feedb))
|
@@ -186,34 +194,30 @@ class NFQuestionParser(QuestionParser):
|
|
186
194
|
self.genFeedbacks=[XMLTags.GENFEEDB]
|
187
195
|
|
188
196
|
def setAnswers(self)->list[ET.Element]:
|
189
|
-
result = self.
|
197
|
+
result = self.rawInput[DFIndex.RESULT]
|
190
198
|
ansEle:list[ET.Element]=[]
|
191
199
|
ansEle.append(self.getNumericAnsElement( result = result ))
|
192
200
|
return ansEle
|
193
201
|
|
194
202
|
class NFMQuestionParser(QuestionParser):
|
195
|
-
def __init__(self,
|
196
|
-
super().__init__(
|
203
|
+
def __init__(self, *args):
|
204
|
+
super().__init__(*args)
|
197
205
|
self.genFeedbacks=[XMLTags.GENFEEDB]
|
198
206
|
self.astEval = Interpreter()
|
199
207
|
|
200
208
|
def setAnswers(self)->None:
|
201
|
-
equation = self.
|
202
|
-
bps = self.
|
209
|
+
equation = self.rawInput[DFIndex.RESULT]
|
210
|
+
bps = str( self.rawInput[DFIndex.BPOINTS] )
|
203
211
|
ansElementsList:list[ET.Element]=[]
|
204
|
-
varNames:list[str]= self.
|
205
|
-
|
206
|
-
bulletPoints:list[ET.Element] = []
|
212
|
+
varNames:list[str]= self._getVarsList(bps)
|
213
|
+
self.question.variables, number = self._getVariablesDict(varNames)
|
207
214
|
for n in range(number):
|
208
|
-
self._setupAstIntprt(
|
215
|
+
self._setupAstIntprt(self.question.variables, n)
|
209
216
|
result = self.astEval(equation)
|
210
217
|
if isinstance(result, float):
|
211
218
|
ansElementsList.append(self.getNumericAnsElement( result = round(result,3) ))
|
212
|
-
bpli = self.insertVariablesToBPoints(varsDict, bps, n)
|
213
|
-
bulletPoints.append(self.formatBulletList(bpli))
|
214
219
|
self.question.answerVariants = ansElementsList
|
215
|
-
self.
|
216
|
-
self.setVariants(len(bulletPoints))
|
220
|
+
self.setVariants(len(ansElementsList))
|
217
221
|
return None
|
218
222
|
|
219
223
|
def setVariants(self, number:int):
|
@@ -225,39 +229,26 @@ class NFMQuestionParser(QuestionParser):
|
|
225
229
|
self.question.category.maxVariants = number if number <= mvar else mvar
|
226
230
|
|
227
231
|
|
228
|
-
|
229
|
-
@staticmethod
|
230
|
-
def insertVariablesToBPoints(varDict: dict, bulletPoints: str, index: int)-> str:
|
231
|
-
"""
|
232
|
-
Für jeden Eintrag im varDict, wird im bulletPoints String der Substring "{key}" durch value[index] ersetzt
|
233
|
-
"""
|
234
|
-
for k, v in varDict.items():
|
235
|
-
s = r"{" + str(k) + r"}"
|
236
|
-
matcher = re.compile(s)
|
237
|
-
bulletPoints = matcher.sub(str(v[index]), bulletPoints)
|
238
|
-
return bulletPoints
|
239
|
-
|
240
|
-
def _setupAstIntprt(self, var:dict[str, list[str]], index:int)->None:
|
232
|
+
def _setupAstIntprt(self, var:dict[str, list[float|int]], index:int)->None:
|
241
233
|
"""Ubergibt die Parameter mit entsprechenden Variablen-Namen an den asteval-Interpreter.
|
242
234
|
|
243
235
|
Dann kann dieser die equation lesen.
|
244
236
|
"""
|
245
|
-
for
|
246
|
-
|
247
|
-
value = comma.sub(".",v[index])
|
248
|
-
self.astEval.symtable[k] = float(value)
|
237
|
+
for name,value in var.items():
|
238
|
+
self.astEval.symtable[name] = value[index]
|
249
239
|
return None
|
250
240
|
|
251
|
-
def
|
241
|
+
def _getVariablesDict(self, keyList: list)-> tuple[dict[str,list[float]],int]:
|
252
242
|
"""Liest alle Variablen-Listen deren Name in ``keyList`` ist aus dem DataFrame im Column[index]"""
|
253
243
|
dic:dict = {}
|
254
244
|
num:int = 0
|
255
245
|
for k in keyList:
|
256
|
-
val = self.
|
246
|
+
val = self.rawInput[k]
|
257
247
|
if isinstance(val, str) :
|
258
|
-
li =
|
248
|
+
li = stringHelpers.stripWhitespace(val.split(";"))
|
259
249
|
num = len(li)
|
260
|
-
|
250
|
+
vars:list[float] = [ float(i.replace(",",".")) for i in li ]
|
251
|
+
dic[str(k)] = vars
|
261
252
|
else:
|
262
253
|
dic[str(k)] = [str(val)]
|
263
254
|
num = 1
|
@@ -265,7 +256,7 @@ class NFMQuestionParser(QuestionParser):
|
|
265
256
|
return dic, num
|
266
257
|
|
267
258
|
@staticmethod
|
268
|
-
def
|
259
|
+
def _getVarsList(bps: str|list[str])->list:
|
269
260
|
"""
|
270
261
|
Durchsucht den bulletPoints String nach den Variablen, die als "{var}" gekennzeichnet sind
|
271
262
|
"""
|
@@ -309,10 +300,10 @@ class MCQuestionParser(QuestionParser):
|
|
309
300
|
|
310
301
|
|
311
302
|
def setAnswers(self)->list[ET.Element]:
|
312
|
-
ansStyle = self.
|
313
|
-
true = stringHelpers.stripWhitespace(self.
|
303
|
+
ansStyle = self.rawInput[DFIndex.ANSTYPE]
|
304
|
+
true = stringHelpers.stripWhitespace(self.rawInput[DFIndex.TRUE].split(';'))
|
314
305
|
trueAnsList = stringHelpers.texWrapper(true, style=ansStyle)
|
315
|
-
false = stringHelpers.stripWhitespace(self.
|
306
|
+
false = stringHelpers.stripWhitespace(self.rawInput[DFIndex.FALSE].split(';'))
|
316
307
|
falseAnsList= stringHelpers.texWrapper(false, style=ansStyle)
|
317
308
|
truefrac = 1/len(trueAnsList)*100
|
318
309
|
falsefrac = 1/len(trueAnsList)*(-100)
|
excel2moodle/core/question.py
CHANGED
@@ -6,6 +6,8 @@ import base64 as base64
|
|
6
6
|
from excel2moodle.core import category, etHelpers
|
7
7
|
from excel2moodle.core.globals import XMLTags, TextElements, DFIndex, questionTypes, parserSettings
|
8
8
|
from excel2moodle.core.exceptions import QNotParsedException
|
9
|
+
from typing import Match
|
10
|
+
import re as re
|
9
11
|
|
10
12
|
|
11
13
|
logger = logging.getLogger(__name__)
|
@@ -19,14 +21,15 @@ class Question():
|
|
19
21
|
self.parent = parent
|
20
22
|
self.qtype: str = qtype
|
21
23
|
self.moodleType = questionTypes[qtype]
|
22
|
-
self.points = ( points if points
|
24
|
+
self.points = ( points if points != 0 else self.category.points)
|
23
25
|
self.element: ET.Element|None=None
|
24
26
|
self.picture:Picture
|
25
27
|
self.id:str
|
26
|
-
self.
|
27
|
-
self.bulletList:
|
28
|
+
self.qtextParagraphs: list[ET.Element] = []
|
29
|
+
self.bulletList: ET.Element|None = None
|
28
30
|
self.answerVariants: list[ET.Element] = []
|
29
31
|
self.variants:int|None = None
|
32
|
+
self.variables: dict[str, list[float|int]] = {}
|
30
33
|
self.setID()
|
31
34
|
self.standardTags = {
|
32
35
|
"hidden":"false"
|
@@ -41,17 +44,32 @@ class Question():
|
|
41
44
|
return "\n".join(li)
|
42
45
|
|
43
46
|
def assemble(self, variant:int=1)->None:
|
47
|
+
textElements:list[ET.Element] = []
|
48
|
+
textElements.extend(self.qtextParagraphs)
|
49
|
+
logger.debug(f"Starting assembly of { self.id }")
|
44
50
|
if self.element is not None:
|
45
51
|
mainText = self.element.find(XMLTags.QTEXT)
|
52
|
+
logger.debug(f"found existing Text in element {mainText = }")
|
53
|
+
txtele = mainText.find("text")
|
54
|
+
if txtele is not None:
|
55
|
+
mainText.remove(txtele)
|
56
|
+
logger.debug(f"removed prevously existing questiontext")
|
46
57
|
else: raise QNotParsedException("Cant assamble, if element is none", self.id)
|
47
|
-
if
|
48
|
-
|
58
|
+
if self.variants is not None:
|
59
|
+
textElements.append(self.getBPointVariant(variant-1))
|
60
|
+
else:
|
61
|
+
textElements.append(self.bulletList)
|
49
62
|
if hasattr(self, "picture") and self.picture.ready:
|
50
|
-
|
63
|
+
textElements.append(self.picture.htmlTag)
|
51
64
|
mainText.append(self.picture.element)
|
52
|
-
mainText.append(etHelpers.getCdatTxtElement(
|
53
|
-
self.element.insert(3, mainText)
|
65
|
+
mainText.append(etHelpers.getCdatTxtElement(textElements))
|
66
|
+
# self.element.insert(3, mainText)
|
67
|
+
logger.debug(f"inserted MainText to question element")
|
54
68
|
if len( self.answerVariants ) > 0:
|
69
|
+
ans = self.element.find(XMLTags.ANSWER)
|
70
|
+
if ans is not None:
|
71
|
+
self.element.remove(ans)
|
72
|
+
logger.debug("removed previous answer element")
|
55
73
|
self.element.insert(5, self.answerVariants[variant-1])
|
56
74
|
return None
|
57
75
|
|
@@ -60,6 +78,28 @@ class Question():
|
|
60
78
|
self.id: str = f"{self.category.id}{self.number:02d}"
|
61
79
|
else: self.id:str = str(id)
|
62
80
|
|
81
|
+
def getBPointVariant(self, variant:int)->ET.Element:
|
82
|
+
if self.bulletList is None:
|
83
|
+
return None
|
84
|
+
varPlaceholder = re.compile(r"{(\w+)}") # matches {a}, {some_var}, etc.
|
85
|
+
|
86
|
+
def replaceMatch(match: Match[str])->str|int|float:
|
87
|
+
key = match.group(1)
|
88
|
+
if key in self.variables:
|
89
|
+
value = self.variables[key][variant]
|
90
|
+
return f"{value}".replace(".",",\\!")
|
91
|
+
return match.group(0) # keep original if no match
|
92
|
+
|
93
|
+
unorderedList = TextElements.ULIST.create()
|
94
|
+
for li in self.bulletList:
|
95
|
+
listItemText = li.text or ""
|
96
|
+
bullet = TextElements.LISTITEM.create()
|
97
|
+
bullet.text = varPlaceholder.sub(replaceMatch, listItemText)
|
98
|
+
logger.debug(f"Inserted Variables into List: {bullet}")
|
99
|
+
unorderedList.append(bullet)
|
100
|
+
return unorderedList
|
101
|
+
|
102
|
+
|
63
103
|
class Picture():
|
64
104
|
def __init__(self, picKey:str, imgFolder:Path, question:Question):
|
65
105
|
self.pic = picKey
|
@@ -17,6 +17,8 @@ from excel2moodle.core.exceptions import InvalidFieldException, NanException
|
|
17
17
|
import pandas as pd
|
18
18
|
import logging
|
19
19
|
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
20
22
|
|
21
23
|
class Validator():
|
22
24
|
def __init__(self, category) -> None:
|
@@ -76,8 +78,24 @@ class Validator():
|
|
76
78
|
if missing is not None:
|
77
79
|
raise InvalidFieldException(msg, id, missing)
|
78
80
|
self._getQuestion()
|
81
|
+
self._getData()
|
79
82
|
return True
|
80
83
|
|
84
|
+
def _getData(self)->None:
|
85
|
+
self.qdata:dict[str, str|float|int|list]={}
|
86
|
+
for idx, val in self.df.items():
|
87
|
+
if not isinstance(idx, str):
|
88
|
+
logger.debug(f"Got a non String key in the spreadsheet, skipping it")
|
89
|
+
continue
|
90
|
+
if idx in self.qdata:
|
91
|
+
if isinstance(self.qdata[idx], list):
|
92
|
+
self.qdata[idx].append(val)
|
93
|
+
else:
|
94
|
+
existing = self.qdata[idx]
|
95
|
+
self.qdata[idx] = [existing, val]
|
96
|
+
else:
|
97
|
+
self.qdata[idx]=val
|
98
|
+
|
81
99
|
def _mandatory(self)->tuple[bool,DFIndex|None]:
|
82
100
|
"""detects if all keys of mandatory are filled with values"""
|
83
101
|
checker = pd.Series.notna(self.df)
|
@@ -8,9 +8,16 @@ import base64 as base64
|
|
8
8
|
def stripWhitespace(stringList):
|
9
9
|
stripped = []
|
10
10
|
for i in stringList:
|
11
|
-
|
11
|
+
s = i.strip()
|
12
|
+
if s:
|
13
|
+
stripped.append(s)
|
12
14
|
return stripped
|
13
15
|
|
16
|
+
def stringToFloat(string:str)->float:
|
17
|
+
string.replace(",",".")
|
18
|
+
return float(string)
|
19
|
+
|
20
|
+
|
14
21
|
def get_bullet_string(s):
|
15
22
|
"""Formatiert die Angaben zum Statischen System hübsch"""
|
16
23
|
split = s.split(';')
|
excel2moodle/ui/appUi.py
CHANGED
@@ -8,6 +8,7 @@ from PySide6 import QtCore
|
|
8
8
|
from PySide6.QtCore import Qt
|
9
9
|
from PySide6 import QtWidgets
|
10
10
|
from pathlib import Path
|
11
|
+
from excel2moodle import e2mMetadata
|
11
12
|
from excel2moodle.ui.windowMain import Ui_MoodleTestGenerator
|
12
13
|
from .windowEquationChecker import Ui_EquationChecker
|
13
14
|
from excel2moodle.ui import windowEquationChecker, dialogs
|
@@ -16,11 +17,8 @@ from excel2moodle.ui.treewidget import QuestionItem, CategoryItem
|
|
16
17
|
from excel2moodle.extra import equationVerification as eqVerif
|
17
18
|
from excel2moodle import qSignalLogger
|
18
19
|
from excel2moodle.ui.settings import Settings
|
19
|
-
from excel2moodle.ui.windowDoc import DocumentationWindow
|
20
20
|
import logging as logging
|
21
21
|
|
22
|
-
from excel2moodle import dirDocumentation
|
23
|
-
|
24
22
|
|
25
23
|
logger = logging.getLogger(__name__)
|
26
24
|
|
@@ -29,25 +27,22 @@ logger = logging.getLogger(__name__)
|
|
29
27
|
class MainWindow(QtWidgets.QMainWindow):
|
30
28
|
def __init__(self, settings:Settings, testDB:QuestionDB)->None:
|
31
29
|
super().__init__()
|
32
|
-
# self.questionGenerator = questionGenerator
|
33
|
-
# self.readSettings()
|
34
30
|
self.settings = settings
|
35
31
|
self.excelPath: Path|None = None
|
36
|
-
if self.excelPath is not None:
|
37
|
-
self.ui.buttonSpreadSheet.setText(self.excelPath.name)
|
38
32
|
self.mainPath = (self.excelPath.parent if self.excelPath is not None else None)
|
39
33
|
self.exportFile = Path()
|
40
34
|
self.testDB = testDB
|
41
35
|
self.ui = Ui_MoodleTestGenerator()
|
42
36
|
self.ui.setupUi(self)
|
43
37
|
|
44
|
-
# self.ui.buttonRefresh.clicked.connect(lambda: self.refreshList(self.test))
|
45
38
|
self.ui.treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
|
46
39
|
self.ui.treeWidget.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
|
40
|
+
self.ui.checkBoxIncludeCategories.setChecked(self.settings.value("testGen/includeCats", type=bool))
|
47
41
|
|
48
42
|
self.ui.retranslateUi(self)
|
49
43
|
logger.info(f"Settings are stored under: {self.settings.fileName()}")
|
50
44
|
self.ui.pointCounter.setReadOnly(True)
|
45
|
+
self.ui.questionCounter.setReadOnly(True)
|
51
46
|
self.setStatus("Wählen Sie bitte eine Excel Tabelle und einen Export Ordner für die Fragen aus")
|
52
47
|
try:
|
53
48
|
self.resize(self.settings.value("windowSize"))
|
@@ -68,7 +63,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
68
63
|
self.ui.buttonSpreadSheet.clicked.connect(self.onButSpreadsheet)
|
69
64
|
self.ui.buttonTestGen.clicked.connect(self.onButGenTest)
|
70
65
|
self.ui.actionPreviewQ.triggered.connect(self.previewQ)
|
71
|
-
self.ui.
|
66
|
+
self.ui.actionAbout.triggered.connect(self.onAbout)
|
72
67
|
self.settings.shPathChanged.connect(self.onSheetPathChanged)
|
73
68
|
|
74
69
|
|
@@ -77,6 +72,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
77
72
|
logger.debug("Slot, new Spreadsheet triggered")
|
78
73
|
self.spreadSheetPath = sheet
|
79
74
|
self.mainPath = sheet.parent
|
75
|
+
self.ui.buttonSpreadSheet.setText(str(sheet.name))
|
80
76
|
|
81
77
|
def updateLog(self,log)->None:
|
82
78
|
self.ui.loggerWindow.append(log)
|
@@ -84,14 +80,41 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
84
80
|
def setIncludeCategoriesSetting(self):
|
85
81
|
if self.ui.checkBoxIncludeCategories.isChecked():
|
86
82
|
self.settings.set("testGen/includeCats", True)
|
83
|
+
logger.debug("set includeCats to True")
|
87
84
|
else:
|
88
85
|
self.settings.set("testGen/includeCats", False)
|
86
|
+
logger.debug("set includeCats to False")
|
89
87
|
|
90
|
-
@QtCore.Slot()
|
91
|
-
def onOpenDocumentation(self):
|
92
|
-
documentationWindow = DocumentationWindow(dirDocumentation, self)
|
93
|
-
documentationWindow.show()
|
94
88
|
|
89
|
+
@QtCore.Slot()
|
90
|
+
def onAbout(self):
|
91
|
+
aboutMessage: str = f"""
|
92
|
+
<h1> About {e2mMetadata['name']}</h1><br>
|
93
|
+
<p style="text-align:center">
|
94
|
+
|
95
|
+
<b><a href="{e2mMetadata['homepage']}">{e2mMetadata['name']}</a> - {e2mMetadata['description']}</b>
|
96
|
+
</p>
|
97
|
+
<p style="text-align:center">
|
98
|
+
The documentation can be found under <b><a href="{e2mMetadata['documentation']}">{e2mMetadata['documentation']}</a></b>
|
99
|
+
</br>
|
100
|
+
</br>
|
101
|
+
If you encounter any issues please report them under the <a href="{e2mMetadata['issues']}"> repositories issues page</a>
|
102
|
+
</br>
|
103
|
+
</p>
|
104
|
+
<p style="text-align:center">
|
105
|
+
This project is maintained by {e2mMetadata['author']}.
|
106
|
+
<br>
|
107
|
+
Development takes place at <a href="{e2mMetadata['homepage']}">{e2mMetadata['homepage']}</a>
|
108
|
+
Contributions are very welcome
|
109
|
+
<br>
|
110
|
+
</p>
|
111
|
+
<p style="text-align:center">
|
112
|
+
<i>This project is published under {e2mMetadata['license']}, you are welcome, to share, modify and reuse the code.</i>
|
113
|
+
</p>
|
114
|
+
"""
|
115
|
+
QtWidgets.QMessageBox.information(self,
|
116
|
+
f"About {e2mMetadata['name']}",
|
117
|
+
aboutMessage)
|
95
118
|
|
96
119
|
def closeEvent(self, event):
|
97
120
|
self.settings.setValue("windowSize", self.size())
|
@@ -145,8 +168,6 @@ class MainWindow(QtWidgets.QMainWindow):
|
|
145
168
|
selectedFilter=("*.ods"))
|
146
169
|
self.excelPath = Path(file[0]).resolve()
|
147
170
|
self.settings.setSpreadsheet(self.excelPath)
|
148
|
-
self.mainPath = self.excelPath.parent
|
149
|
-
self.ui.buttonSpreadSheet.setText(self.excelPath.name)
|
150
171
|
logger.debug(f'Saved Spreadsheet Path: {self.excelPath}\n')
|
151
172
|
self.setStatus("[OK] Excel Tabelle wurde eingelesen")
|
152
173
|
return None
|
excel2moodle/ui/dialogs.py
CHANGED
@@ -65,9 +65,9 @@ class QuestinoPreviewDialog(QtWidgets.QDialog):
|
|
65
65
|
|
66
66
|
def setText(self)->None:
|
67
67
|
t = []
|
68
|
-
for text in self.question.
|
68
|
+
for text in self.question.qtextParagraphs:
|
69
69
|
t.append(ET.tostring(text, encoding='unicode'))
|
70
|
-
|
70
|
+
t.append(ET.tostring(self.question.bulletList, encoding='unicode'))
|
71
71
|
self.ui.questionText.setText("\n".join(t))
|
72
72
|
|
73
73
|
def setAnswers(self)->None:
|
excel2moodle/ui/settings.py
CHANGED
@@ -5,8 +5,8 @@ class Settings(QSettings):
|
|
5
5
|
shPathChanged = Signal(Path)
|
6
6
|
def __init__(self):
|
7
7
|
super().__init__("jbosse3", "excel2moodle")
|
8
|
-
if self.contains("spreadsheet"):
|
9
|
-
self.sheet = self.value("spreadsheet")
|
8
|
+
if self.contains("core/spreadsheet"):
|
9
|
+
self.sheet = self.value("core/spreadsheet")
|
10
10
|
try:
|
11
11
|
self.sheet.resolve(strict=True)
|
12
12
|
if self.sheet.is_file():
|
@@ -20,7 +20,8 @@ class Settings(QSettings):
|
|
20
20
|
|
21
21
|
|
22
22
|
def get(self, value, default=None):
|
23
|
-
self.value(value, default)
|
23
|
+
return self.value(value, default)
|
24
|
+
|
24
25
|
|
25
26
|
def set(self, setting, value):
|
26
27
|
self.setValue(setting, value)
|
@@ -28,7 +29,7 @@ class Settings(QSettings):
|
|
28
29
|
def setSpreadsheet(self, sheet:Path)->None:
|
29
30
|
if isinstance(sheet, Path):
|
30
31
|
self.sheet = sheet.resolve(strict=True)
|
31
|
-
self.setValue("spreadsheet", self.sheet)
|
32
|
+
self.setValue("core/spreadsheet", self.sheet)
|
32
33
|
self.shPathChanged.emit(sheet)
|
33
34
|
return None
|
34
35
|
|
@@ -1,11 +1,11 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: excel2moodle
|
3
|
-
Version: 0.3.
|
4
|
-
Summary: A
|
3
|
+
Version: 0.3.3
|
4
|
+
Summary: A package for converting questions from a spreadsheet, to valid moodle-xml
|
5
5
|
Author: Jakob Bosse
|
6
6
|
License-Expression: GPL-3.0-or-later
|
7
|
-
Project-URL:
|
8
|
-
Project-URL:
|
7
|
+
Project-URL: Repository, https://gitlab.com/jbosse3/excel2moodle.git
|
8
|
+
Project-URL: Documentation, https://jbosse3.gitlab.io/excel2moodle
|
9
9
|
Keywords: moodle,XML,teaching,question,converter
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
11
11
|
Classifier: Operating System :: OS Independent
|
@@ -1,31 +1,31 @@
|
|
1
|
-
excel2moodle/__init__.py,sha256=
|
2
|
-
excel2moodle/__main__.py,sha256=
|
1
|
+
excel2moodle/__init__.py,sha256=dsK3KrorPt06FUp18_dw2m4p9P8E7rFf9I0BR-4I28k,4263
|
2
|
+
excel2moodle/__main__.py,sha256=SsulFcOSlRBF2tLTUIdFgB0EJdSLGBIQ_HO53S4Sih8,541
|
3
3
|
excel2moodle/core/__init__.py,sha256=E7smxf7ESHlucvho4Gb5oaTHA7IsOv9J-UDIsoKwp84,399
|
4
|
-
excel2moodle/core/category.py,sha256=
|
5
|
-
excel2moodle/core/dataStructure.py,sha256=
|
4
|
+
excel2moodle/core/category.py,sha256=rZsQW5-Ud182xCrS4R8B0vrPMrI-azwtfzlTdy89SR4,2982
|
5
|
+
excel2moodle/core/dataStructure.py,sha256=h82r4wO2PhhHA4EqROjfkqWGUpg6UKQhkyrdjdisLxg,6343
|
6
6
|
excel2moodle/core/etHelpers.py,sha256=_fd-fGMVNWt1l8FY7xcA2uXOBahxtq8ggYxRYm4SwdA,2354
|
7
7
|
excel2moodle/core/exceptions.py,sha256=3OLVHMCBgETiOxSsirlsCPvlMN5UgEQcMkRDkCfrUs4,675
|
8
8
|
excel2moodle/core/globals.py,sha256=9V9-KBSsHMdsRZWg2wGrvjAd6dc3XZG4pNPnun0AHGE,3595
|
9
9
|
excel2moodle/core/numericMultiQ.py,sha256=InBrn-tsCCYwDb78sGR3FyFxVeN0GfIg63JGtr_tqg4,2751
|
10
|
-
excel2moodle/core/parser.py,sha256=
|
11
|
-
excel2moodle/core/question.py,sha256=
|
12
|
-
excel2moodle/core/questionValidator.py,sha256=
|
10
|
+
excel2moodle/core/parser.py,sha256=0bLP_bkTFu3URw08sn1zJCvA3YCKxDAC4a6I14koHho,13408
|
11
|
+
excel2moodle/core/question.py,sha256=zzdL_7BdWs0wUVl8-06gLkKu0Is3n0zU-2ZkaYhU504,5913
|
12
|
+
excel2moodle/core/questionValidator.py,sha256=taOrrR6pd5mN7SGpET9eLOvb1v5NWvg_VgMj8opQlZ4,4791
|
13
13
|
excel2moodle/core/questionWriter.py,sha256=kyUTrnjWrLEWdez5_7FPNJ0Cs_fF5d16oOQiVvHv3zA,10242
|
14
|
-
excel2moodle/core/stringHelpers.py,sha256=
|
14
|
+
excel2moodle/core/stringHelpers.py,sha256=9BqR7yx2bL8WknVMPuNlmWz05bgi4qWjFYHsgaftpZw,3141
|
15
15
|
excel2moodle/extra/__init__.py,sha256=e70OmuW3j7N4ErIIHlK4vkmCKcpRbitJCdJvzjego8c,339
|
16
16
|
excel2moodle/extra/equationVerification.py,sha256=FOvVBQZ357fbA5nYkHjvvWWVP3pQpo4RK3H7vru9v3A,4278
|
17
17
|
excel2moodle/ui/__init__.py,sha256=4EdGtpzwH3rgw4xW9E5x9kdPQYwKbo9rehHRZTNxCrQ,44
|
18
|
-
excel2moodle/ui/appUi.py,sha256=
|
19
|
-
excel2moodle/ui/dialogs.py,sha256=
|
18
|
+
excel2moodle/ui/appUi.py,sha256=TEqslEDExDOyYwCh7czg3YZggPdZewFiImbpzACLYLU,10533
|
19
|
+
excel2moodle/ui/dialogs.py,sha256=5dnzRN9cwaxjJsxA7I9v1imH4k6XVzzievYi3PR32XI,3038
|
20
20
|
excel2moodle/ui/questionPreviewDialog.py,sha256=_rJvz1GM90aNnj3P6SugEezK7JW6m74ZALgkChohWLM,4980
|
21
|
-
excel2moodle/ui/settings.py,sha256=
|
21
|
+
excel2moodle/ui/settings.py,sha256=PB0uBMCd8DIRSFfvUAPs5hK4UEcb0y7dG9ZJ--kOvuQ,1115
|
22
22
|
excel2moodle/ui/treewidget.py,sha256=xiYWhJeJ9o9Iubtr44HcyJas9v4m8aBsHNdokQAHi80,2357
|
23
23
|
excel2moodle/ui/variantDialog.py,sha256=snVaF3_YAc7NWjMRg7NzbjL_PzNbOpt4eiqElkE46io,5414
|
24
24
|
excel2moodle/ui/windowDoc.py,sha256=IciZpwrLnGzIQV1aCdKQBg6km3oufHGs8havTFzNJyU,1055
|
25
25
|
excel2moodle/ui/windowEquationChecker.py,sha256=fLyal3sbJwpthWCAxLB5vbSFOX23JoivoYksNp3mZVY,7925
|
26
26
|
excel2moodle/ui/windowMain.py,sha256=sB1ahkAwxFxO3EYP3X_MuB6ohgXwK5NUQHWeFo4eqrI,21062
|
27
|
-
excel2moodle-0.3.
|
28
|
-
excel2moodle-0.3.
|
29
|
-
excel2moodle-0.3.
|
30
|
-
excel2moodle-0.3.
|
31
|
-
excel2moodle-0.3.
|
27
|
+
excel2moodle-0.3.3.dist-info/licenses/LICENSE,sha256=ywQqe6Sitymkf2lV2NRcx_aGsaC-KbSl_EfEsRXmNRw,35135
|
28
|
+
excel2moodle-0.3.3.dist-info/METADATA,sha256=i7lC9k3j8ErKfWNnPAngdrnDAWIqC5V6egSB5uWivW8,2904
|
29
|
+
excel2moodle-0.3.3.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
30
|
+
excel2moodle-0.3.3.dist-info/top_level.txt,sha256=5V1xRUQ9o7UmOCmNoWCZPAuy5nXp3Qbzyqch8fUGT_c,13
|
31
|
+
excel2moodle-0.3.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|