excel2moodle 0.5.0__tar.gz → 0.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. excel2moodle-0.5.2/PKG-INFO +146 -0
  2. excel2moodle-0.5.2/README.md +125 -0
  3. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/core/dataStructure.py +3 -3
  4. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/core/question.py +6 -6
  5. excel2moodle-0.5.2/excel2moodle/question_types/cloze.py +342 -0
  6. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/question_types/nfm.py +6 -5
  7. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/ui/appUi.py +18 -22
  8. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/ui/dialogs.py +9 -5
  9. excel2moodle-0.5.2/excel2moodle.egg-info/PKG-INFO +146 -0
  10. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle.egg-info/SOURCES.txt +0 -11
  11. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/pyproject.toml +10 -1
  12. excel2moodle-0.5.2/test/test_nfmParsing.py +85 -0
  13. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/test/test_picture.py +16 -14
  14. excel2moodle-0.5.0/PKG-INFO +0 -63
  15. excel2moodle-0.5.0/README.md +0 -42
  16. excel2moodle-0.5.0/docs/_build/html/exampleQuestions.html +0 -598
  17. excel2moodle-0.5.0/docs/_build/html/excel2moodle.core.html +0 -1686
  18. excel2moodle-0.5.0/docs/_build/html/excel2moodle.extra.html +0 -236
  19. excel2moodle-0.5.0/docs/_build/html/excel2moodle.html +0 -466
  20. excel2moodle-0.5.0/docs/_build/html/excel2moodle.ui.html +0 -666
  21. excel2moodle-0.5.0/docs/_build/html/genindex.html +0 -906
  22. excel2moodle-0.5.0/docs/_build/html/howto.html +0 -383
  23. excel2moodle-0.5.0/docs/_build/html/index.html +0 -227
  24. excel2moodle-0.5.0/docs/_build/html/py-modindex.html +0 -227
  25. excel2moodle-0.5.0/docs/_build/html/search.html +0 -132
  26. excel2moodle-0.5.0/docs/_build/html/userReference.html +0 -443
  27. excel2moodle-0.5.0/excel2moodle/question_types/cloze.py +0 -207
  28. excel2moodle-0.5.0/excel2moodle.egg-info/PKG-INFO +0 -63
  29. excel2moodle-0.5.0/test/test_nfmParsing.py +0 -30
  30. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/LICENSE +0 -0
  31. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/MANIFEST.in +0 -0
  32. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/__init__.py +0 -0
  33. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/__main__.py +0 -0
  34. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/core/__init__.py +0 -0
  35. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/core/category.py +0 -0
  36. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/core/etHelpers.py +0 -0
  37. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/core/exceptions.py +0 -0
  38. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/core/globals.py +0 -0
  39. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/core/numericMultiQ.py +0 -0
  40. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/core/parser.py +0 -0
  41. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/core/settings.py +0 -0
  42. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/core/stringHelpers.py +0 -0
  43. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/core/validator.py +0 -0
  44. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/extra/__init__.py +0 -0
  45. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/extra/equationVerification.py +0 -0
  46. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/logger.py +0 -0
  47. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/question_types/__init__.py +0 -0
  48. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/question_types/mc.py +0 -0
  49. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/question_types/nf.py +0 -0
  50. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/ui/UI_equationChecker.py +0 -0
  51. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/ui/UI_exportSettingsDialog.py +0 -0
  52. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/ui/UI_mainWindow.py +0 -0
  53. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/ui/UI_variantDialog.py +0 -0
  54. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/ui/__init__.py +0 -0
  55. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/ui/equationChecker.py +0 -0
  56. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/ui/treewidget.py +0 -0
  57. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle/ui/windowDoc.py +0 -0
  58. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle.egg-info/dependency_links.txt +0 -0
  59. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle.egg-info/entry_points.txt +0 -0
  60. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle.egg-info/requires.txt +0 -0
  61. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/excel2moodle.egg-info/top_level.txt +0 -0
  62. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/setup.cfg +0 -0
  63. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/test/test_parseQuestion.py +0 -0
  64. {excel2moodle-0.5.0 → excel2moodle-0.5.2}/test/test_questionDataGet.py +0 -0
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: excel2moodle
3
+ Version: 0.5.2
4
+ Summary: A package for converting questions from a spreadsheet, to valid moodle-xml
5
+ Author: Jakob Bosse
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Repository, https://gitlab.com/jbosse3/excel2moodle.git
8
+ Project-URL: Documentation, https://jbosse3.gitlab.io/excel2moodle
9
+ Keywords: moodle,XML,teaching,question,converter,open educational Ressource
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: pyside6>=6.8.0
16
+ Requires-Dist: pandas>=2.1.3
17
+ Requires-Dist: lxml>=5.4.0
18
+ Requires-Dist: asteval>=1.0.6
19
+ Requires-Dist: python-calamine>=0.3.2
20
+ Dynamic: license-file
21
+
22
+ # excel 2 Moodle
23
+ ![Logo](excel2moodleLogo.png "Logo excel2moodle"){width=35%}
24
+
25
+ This Python program helps to create Moodle questions in less time.
26
+ The idea is to write the questions data into a spreadsheet file, from which the program generates the moodle compliant xml Files.
27
+ All questions or a selection of questions can be exported into one xml file to be imported into moodle.
28
+
29
+ ## Concept
30
+ The concept is, to store the different questions into categories of similar types and difficulties of questions, for each of which, a separated sheet in the Spreadsheet document should be created.
31
+
32
+ A `settings` sheet contains global settings to be used for all questions and categories.
33
+ Another sheet stores metadata for the different categories of questions.
34
+ And each category lives inside a separate sheet inside the spreadsheet document.
35
+
36
+ ## Getting Started
37
+
38
+ ### Installation
39
+ To get started with excel2moodle first have a look at the [installation](https://jbosse3.gitlab.io/excel2moodle/howto.html#excel2moodle-unter-windows-installieren)
40
+ If you already have python and uv installed, it is as easy as running `uv tool install excel2moodle`.
41
+
42
+ ### [ Documentation ](https://jbosse3.gitlab.io/excel2moodle/index.html)
43
+ Once excel2moodle is installed you can checkout the [example question sheet](https://gitlab.com/jbosse3/excel2moodle/-/tree/master/example?ref_type=heads)
44
+ in the repository.
45
+
46
+ Some steps are already documented as [ tutorials ](https://jbosse3.gitlab.io/excel2moodle/howto.html)
47
+ you can follow along.
48
+
49
+ And please have a look into the [**user Reference**](https://jbosse3.gitlab.io/excel2moodle/userReference.html)
50
+ of the documentation.
51
+ That part explains each part of defining a question.
52
+
53
+
54
+ ## Functionality
55
+ * Equation Verification:
56
+ + this tool helps you to validate the correct equation for the parametrized Questions.
57
+ * Question Preview:
58
+ + This helps you when selecting the correct questions for the export.
59
+ * Export Options:
60
+ + you can export the questions preserving the categories in moodle
61
+
62
+ ### Question Types
63
+ * Generate multiple Choice Questions:
64
+ + The answers can be pictures or normal text
65
+ * Generate Numeric Questions
66
+ * Generate parametrized numeric Questions
67
+ * Generate parametrized cloze Questions
68
+
69
+
70
+ ![MainWindow](mainWindow.png "Logo excel2moodle"){width=80%}
71
+
72
+ ## Licensing and authorship
73
+ excel2moodle is lincensed under the latest [GNU GPL license](https://gitlab.com/jbosse3/excel2moodle/-/blob/master/LICENSE)
74
+ Initial development was made by Richard Lorenz, and later taken over by Jakob Bosse
75
+
76
+ ## Supporting
77
+ A special thanks goes to the [Civil Engineering Departement of the Fachhochschule Potsdam](https://www.fh-potsdam.de/en/study-further-education/departments/civil-engineering-department)
78
+ where i was employed as a student associate to work on this project.
79
+
80
+ If You want to support my work as well, you can by me a [coffee](https://ko-fi.com/jbosse3)
81
+
82
+ # Changelogs
83
+
84
+ ## 0.5.2 (2025-06-30)
85
+ Extended Documentation and bugfix for import Module
86
+
87
+ ### bugfix (2 changes)
88
+
89
+ - [Default question variant saved and reused.](https://gitlab.com/jbosse3/excel2moodle/-/commit/097705ba83727463a9b27cd76e99814a7ecf28df)
90
+ - [bugfix: Import module working again](https://gitlab.com/jbosse3/excel2moodle/-/commit/5f293970bcdac3858911cdcc102b72714af057bd)
91
+
92
+ ### documentation (1 change)
93
+
94
+ - [documentation: Added how to build question database](https://gitlab.com/jbosse3/excel2moodle/-/commit/71ceb122aa37e8bf2735b659359ae37d81017599)
95
+
96
+ ### feature (1 change)
97
+
98
+ - [Implemented MC question string method](https://gitlab.com/jbosse3/excel2moodle/-/commit/c4f2081d0000ee60322fe8eec8468fa3317ce7be)
99
+
100
+ ### improvement (1 change)
101
+
102
+ - [Implemented ClozePart object](https://gitlab.com/jbosse3/excel2moodle/-/commit/878f90f45e37421384c4f8f602115e7596b4ceb9)
103
+
104
+ ## 0.5.2 (2025-06-30)
105
+ Extended Documentation and bugfix for import Module
106
+
107
+ ### bugfix (2 changes)
108
+
109
+ - [Default question variant saved and reused.](https://gitlab.com/jbosse3/excel2moodle/-/commit/097705ba83727463a9b27cd76e99814a7ecf28df)
110
+ - [bugfix: Import module working again](https://gitlab.com/jbosse3/excel2moodle/-/commit/5f293970bcdac3858911cdcc102b72714af057bd)
111
+
112
+ ### documentation (1 change)
113
+
114
+ - [documentation: Added how to build question database](https://gitlab.com/jbosse3/excel2moodle/-/commit/71ceb122aa37e8bf2735b659359ae37d81017599)
115
+
116
+ ### feature (1 change)
117
+
118
+ - [Implemented MC question string method](https://gitlab.com/jbosse3/excel2moodle/-/commit/c4f2081d0000ee60322fe8eec8468fa3317ce7be)
119
+
120
+ ### improvement (1 change)
121
+
122
+ - [Implemented ClozePart object](https://gitlab.com/jbosse3/excel2moodle/-/commit/878f90f45e37421384c4f8f602115e7596b4ceb9)
123
+
124
+ ## 0.5.1 (2025-06-24)
125
+ Minor docs improvement and question variant bugfix
126
+
127
+ ### bugfix (1 change)
128
+
129
+ - [Bullet points variant didn't get updated](https://gitlab.com/jbosse3/excel2moodle/-/commit/7b4ad9e9c8a4216167ae019859ebaa8def81d57f)
130
+
131
+ ## 0.5.0 (2025-06-20)
132
+ settings handling improved
133
+
134
+ ### feature (2 changes)
135
+
136
+ - [Pixmaps and vector graphics scaled to fit in preview](https://gitlab.com/jbosse3/excel2moodle/-/commit/00a6ef13fb2a0046d7641e24af6cf6f08642390e)
137
+ - [feature: category Settings implemented](https://gitlab.com/jbosse3/excel2moodle/-/commit/d673cc3f5ba06051aa37bc17a3ef0161121cb730)
138
+
139
+ ### improvement (1 change)
140
+
141
+ - [Tolerance is harmonized by questionData.get()](https://gitlab.com/jbosse3/excel2moodle/-/commit/8d1724f4877e1584cc531b6b3f278bdea68b5831)
142
+
143
+ ### Settings Errors are logged (1 change)
144
+
145
+ - [Log Errors in settings Sheet](https://gitlab.com/jbosse3/excel2moodle/-/commit/07e58f957c69ea818db1c5679cf89e287817ced3)
146
+
@@ -0,0 +1,125 @@
1
+ # excel 2 Moodle
2
+ ![Logo](excel2moodleLogo.png "Logo excel2moodle"){width=35%}
3
+
4
+ This Python program helps to create Moodle questions in less time.
5
+ The idea is to write the questions data into a spreadsheet file, from which the program generates the moodle compliant xml Files.
6
+ All questions or a selection of questions can be exported into one xml file to be imported into moodle.
7
+
8
+ ## Concept
9
+ The concept is, to store the different questions into categories of similar types and difficulties of questions, for each of which, a separated sheet in the Spreadsheet document should be created.
10
+
11
+ A `settings` sheet contains global settings to be used for all questions and categories.
12
+ Another sheet stores metadata for the different categories of questions.
13
+ And each category lives inside a separate sheet inside the spreadsheet document.
14
+
15
+ ## Getting Started
16
+
17
+ ### Installation
18
+ To get started with excel2moodle first have a look at the [installation](https://jbosse3.gitlab.io/excel2moodle/howto.html#excel2moodle-unter-windows-installieren)
19
+ If you already have python and uv installed, it is as easy as running `uv tool install excel2moodle`.
20
+
21
+ ### [ Documentation ](https://jbosse3.gitlab.io/excel2moodle/index.html)
22
+ Once excel2moodle is installed you can checkout the [example question sheet](https://gitlab.com/jbosse3/excel2moodle/-/tree/master/example?ref_type=heads)
23
+ in the repository.
24
+
25
+ Some steps are already documented as [ tutorials ](https://jbosse3.gitlab.io/excel2moodle/howto.html)
26
+ you can follow along.
27
+
28
+ And please have a look into the [**user Reference**](https://jbosse3.gitlab.io/excel2moodle/userReference.html)
29
+ of the documentation.
30
+ That part explains each part of defining a question.
31
+
32
+
33
+ ## Functionality
34
+ * Equation Verification:
35
+ + this tool helps you to validate the correct equation for the parametrized Questions.
36
+ * Question Preview:
37
+ + This helps you when selecting the correct questions for the export.
38
+ * Export Options:
39
+ + you can export the questions preserving the categories in moodle
40
+
41
+ ### Question Types
42
+ * Generate multiple Choice Questions:
43
+ + The answers can be pictures or normal text
44
+ * Generate Numeric Questions
45
+ * Generate parametrized numeric Questions
46
+ * Generate parametrized cloze Questions
47
+
48
+
49
+ ![MainWindow](mainWindow.png "Logo excel2moodle"){width=80%}
50
+
51
+ ## Licensing and authorship
52
+ excel2moodle is lincensed under the latest [GNU GPL license](https://gitlab.com/jbosse3/excel2moodle/-/blob/master/LICENSE)
53
+ Initial development was made by Richard Lorenz, and later taken over by Jakob Bosse
54
+
55
+ ## Supporting
56
+ A special thanks goes to the [Civil Engineering Departement of the Fachhochschule Potsdam](https://www.fh-potsdam.de/en/study-further-education/departments/civil-engineering-department)
57
+ where i was employed as a student associate to work on this project.
58
+
59
+ If You want to support my work as well, you can by me a [coffee](https://ko-fi.com/jbosse3)
60
+
61
+ # Changelogs
62
+
63
+ ## 0.5.2 (2025-06-30)
64
+ Extended Documentation and bugfix for import Module
65
+
66
+ ### bugfix (2 changes)
67
+
68
+ - [Default question variant saved and reused.](https://gitlab.com/jbosse3/excel2moodle/-/commit/097705ba83727463a9b27cd76e99814a7ecf28df)
69
+ - [bugfix: Import module working again](https://gitlab.com/jbosse3/excel2moodle/-/commit/5f293970bcdac3858911cdcc102b72714af057bd)
70
+
71
+ ### documentation (1 change)
72
+
73
+ - [documentation: Added how to build question database](https://gitlab.com/jbosse3/excel2moodle/-/commit/71ceb122aa37e8bf2735b659359ae37d81017599)
74
+
75
+ ### feature (1 change)
76
+
77
+ - [Implemented MC question string method](https://gitlab.com/jbosse3/excel2moodle/-/commit/c4f2081d0000ee60322fe8eec8468fa3317ce7be)
78
+
79
+ ### improvement (1 change)
80
+
81
+ - [Implemented ClozePart object](https://gitlab.com/jbosse3/excel2moodle/-/commit/878f90f45e37421384c4f8f602115e7596b4ceb9)
82
+
83
+ ## 0.5.2 (2025-06-30)
84
+ Extended Documentation and bugfix for import Module
85
+
86
+ ### bugfix (2 changes)
87
+
88
+ - [Default question variant saved and reused.](https://gitlab.com/jbosse3/excel2moodle/-/commit/097705ba83727463a9b27cd76e99814a7ecf28df)
89
+ - [bugfix: Import module working again](https://gitlab.com/jbosse3/excel2moodle/-/commit/5f293970bcdac3858911cdcc102b72714af057bd)
90
+
91
+ ### documentation (1 change)
92
+
93
+ - [documentation: Added how to build question database](https://gitlab.com/jbosse3/excel2moodle/-/commit/71ceb122aa37e8bf2735b659359ae37d81017599)
94
+
95
+ ### feature (1 change)
96
+
97
+ - [Implemented MC question string method](https://gitlab.com/jbosse3/excel2moodle/-/commit/c4f2081d0000ee60322fe8eec8468fa3317ce7be)
98
+
99
+ ### improvement (1 change)
100
+
101
+ - [Implemented ClozePart object](https://gitlab.com/jbosse3/excel2moodle/-/commit/878f90f45e37421384c4f8f602115e7596b4ceb9)
102
+
103
+ ## 0.5.1 (2025-06-24)
104
+ Minor docs improvement and question variant bugfix
105
+
106
+ ### bugfix (1 change)
107
+
108
+ - [Bullet points variant didn't get updated](https://gitlab.com/jbosse3/excel2moodle/-/commit/7b4ad9e9c8a4216167ae019859ebaa8def81d57f)
109
+
110
+ ## 0.5.0 (2025-06-20)
111
+ settings handling improved
112
+
113
+ ### feature (2 changes)
114
+
115
+ - [Pixmaps and vector graphics scaled to fit in preview](https://gitlab.com/jbosse3/excel2moodle/-/commit/00a6ef13fb2a0046d7641e24af6cf6f08642390e)
116
+ - [feature: category Settings implemented](https://gitlab.com/jbosse3/excel2moodle/-/commit/d673cc3f5ba06051aa37bc17a3ef0161121cb730)
117
+
118
+ ### improvement (1 change)
119
+
120
+ - [Tolerance is harmonized by questionData.get()](https://gitlab.com/jbosse3/excel2moodle/-/commit/8d1724f4877e1584cc531b6b3f278bdea68b5831)
121
+
122
+ ### Settings Errors are logged (1 change)
123
+
124
+ - [Log Errors in settings Sheet](https://gitlab.com/jbosse3/excel2moodle/-/commit/07e58f957c69ea818db1c5679cf89e287817ced3)
125
+
@@ -131,7 +131,7 @@ class QuestionDB:
131
131
  if Tags.IMPORTMODULE in self.settings:
132
132
  logger.warning(
133
133
  "Appending: %s to sys.path. All names defined by it will be usable",
134
- sheetPath,
134
+ sheetPath.parent,
135
135
  )
136
136
  sys.path.append(str(sheetPath.parent))
137
137
  if Tags.PICTURESUBFOLDER not in self.settings:
@@ -286,7 +286,7 @@ class QuestionDB:
286
286
  self.signals.categoryQuestionsReady.emit(category)
287
287
 
288
288
  @classmethod
289
- def setupAndParseQuestion(cls, category: Category, qNumber: int) -> Question | None:
289
+ def setupAndParseQuestion(cls, category: Category, qNumber: int) -> Question:
290
290
  """Check if the Question Data is valid. Then parse it.
291
291
 
292
292
  The Question data is accessed from `category.dataframe` via its number
@@ -322,7 +322,7 @@ class QuestionDB:
322
322
  question = QuestionTypeMapping[qtype].create(category, validData)
323
323
  if question.isParsed:
324
324
  locallogger.info("Question already parsed")
325
- return None
325
+ return question
326
326
  if isinstance(question, NFQuestion):
327
327
  cls.nfParser.setup(question)
328
328
  locallogger.debug("setup a new NF parser ")
@@ -161,15 +161,15 @@ class Question:
161
161
  def assemble(self, variant=0) -> None:
162
162
  """Assemble the question to the valid xml Tree."""
163
163
  mainText = self._getTextElement()
164
- self.logger.debug("Starting assembly")
165
- self._setAnswerElement(variant=variant)
166
- textParts = self._assembleMainTextParts()
164
+ self.logger.info("Starting assembly variant: %s", variant)
165
+ self._assembleAnswer(variant=variant)
166
+ textParts = self._assembleText(variant=variant)
167
167
  if hasattr(self, "picture") and self.picture.ready:
168
168
  mainText.append(self.picture.element)
169
169
  self.logger.debug("Appended Picture element to text")
170
170
  mainText.append(etHelpers.getCdatTxtElement(textParts))
171
171
 
172
- def _assembleMainTextParts(self, variant=0) -> list[ET.Element]:
172
+ def _assembleText(self, variant=0) -> list[ET.Element]:
173
173
  """Assemble the Question Text.
174
174
 
175
175
  Intended for the cloze question, where the answers parts are part of the text.
@@ -202,7 +202,7 @@ class Question:
202
202
  return self.bulletList
203
203
  return None
204
204
 
205
- def _setAnswerElement(self, variant: int = 0) -> None:
205
+ def _assembleAnswer(self, variant: int = 0) -> None:
206
206
  pass
207
207
 
208
208
  def _setID(self, id=0) -> None:
@@ -229,7 +229,7 @@ class ParametricQuestion(Question):
229
229
  def replaceMatch(match: Match[str]) -> str | int | float:
230
230
  key = match.group(1)
231
231
  if key in self.variables:
232
- value = self.variables[key][variant]
232
+ value = self.variables[key][variant - 1]
233
233
  return f"{value}".replace(".", ",\\!")
234
234
  return match.group(0) # keep original if no match
235
235
 
@@ -0,0 +1,342 @@
1
+ """Implementation of tde cloze question type.
2
+
3
+ This question type is like the NFM but supports multiple fields of answers.
4
+ All Answers are calculated off an equation using the same variables.
5
+ """
6
+
7
+ import logging
8
+ import math
9
+ import re
10
+ from typing import Literal, overload
11
+
12
+ import lxml.etree as ET
13
+
14
+ from excel2moodle.core.exceptions import QNotParsedException
15
+ from excel2moodle.core.globals import (
16
+ Tags,
17
+ TextElements,
18
+ )
19
+ from excel2moodle.core.question import ParametricQuestion
20
+ from excel2moodle.core.settings import Tags
21
+ from excel2moodle.question_types.nfm import NFMQuestionParser
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class ClozePart:
27
+ def __init__(
28
+ self,
29
+ question: ParametricQuestion,
30
+ text: list[str],
31
+ ) -> None:
32
+ self.question = question
33
+ self.text: list[ET.Element] = self._setupText(text)
34
+ if not self.text:
35
+ msg = f"Answer part for cloze question {self.question.id} is invalid without partText"
36
+ raise ValueError(msg)
37
+
38
+ @property
39
+ def points(self) -> float:
40
+ if hasattr(self, "_points"):
41
+ return self._points
42
+ return 0.0
43
+ self.question.logger.error("Invalid call to points of unparsed cloze part")
44
+ return 0.0
45
+
46
+ @points.setter
47
+ def points(self, points: float) -> None:
48
+ self._points = points if points > 0 else 0.0
49
+
50
+ @property
51
+ def typ(self) -> Literal["MC", "NFM"] | None:
52
+ if hasattr(self, "_typ"):
53
+ return self._typ
54
+ return None
55
+
56
+ @property
57
+ def mcAnswerString(self) -> str:
58
+ if hasattr(self, "_mcAnswer"):
59
+ return self._mcAnswer
60
+ msg = "No MC Answer was set"
61
+ raise ValueError(msg)
62
+
63
+ @mcAnswerString.setter
64
+ def mcAnswerString(self, answerString: str) -> None:
65
+ self._mcAnswer: str = answerString
66
+
67
+ def _setupText(self, text: list[str]) -> ET.Element:
68
+ textList: list[ET.Element] = []
69
+ for t in text:
70
+ textList.append(TextElements.PLEFT.create())
71
+ textList[-1].text = t
72
+ return textList
73
+
74
+ def setAnswer(
75
+ self,
76
+ equation: str | None = None,
77
+ trueAns: list[str] | None = None,
78
+ falseAns: list[str] | None = None,
79
+ ) -> bool:
80
+ if falseAns is not None:
81
+ self.falseAnswers: list[str] = falseAns
82
+ if trueAns is not None:
83
+ self.trueAnswers: list[str] = trueAns
84
+ if equation is not None:
85
+ self.equation: str = equation
86
+ check = False
87
+ t = hasattr(self, "trueAnswers")
88
+ f = hasattr(self, "falseAnswers")
89
+ eq = hasattr(self, "equation")
90
+ if t and f and not eq:
91
+ self._typ: Literal["MC", "NFM"] = "MC"
92
+ return True
93
+ if eq and not t and not f:
94
+ self._typ: Literal["MC", "NFM"] = "NFM"
95
+ self.nfResults: list[float] = []
96
+ return True
97
+ return False
98
+
99
+ def __repr__(self) -> str:
100
+ answers: str = (
101
+ self.equation
102
+ if self.typ == "NFM"
103
+ else f"{self.trueAnswers}\n {self.falseAnswers}"
104
+ )
105
+ return f"Cloze Part {self.typ}\n Answers: '{answers}'"
106
+
107
+
108
+ class ClozeQuestion(ParametricQuestion):
109
+ """Cloze Question Type."""
110
+
111
+ def __init__(self, *args, **kwargs) -> None:
112
+ super().__init__(*args, **kwargs)
113
+ self.questionParts: dict[int, ClozePart] = {}
114
+ self.questionTexts: list[ET.Element] = []
115
+
116
+ @property
117
+ def partsNum(self) -> int:
118
+ return len(self.questionParts)
119
+
120
+ @property
121
+ def points(self) -> float:
122
+ pts: float = 0
123
+ if self.isParsed:
124
+ for p in self.questionParts.values():
125
+ pts = pts + p.points
126
+ else:
127
+ pts = self.rawData.get(Tags.POINTS)
128
+ return pts
129
+
130
+ def _assembleAnswer(self, variant: int = 1) -> None:
131
+ for partNum, part in self.questionParts.items():
132
+ if part.typ == "MC":
133
+ ansStr = part.mcAnswerString
134
+ self.logger.info("MC answer part: %s ", ansStr)
135
+ elif part.typ == "NFM":
136
+ result = part.nfResults[variant - 1]
137
+ ansStr = ClozeQuestionParser.getNumericAnsStr(
138
+ result,
139
+ self.rawData.get(Tags.TOLERANCE),
140
+ wrongSignCount=self.rawData.get(Tags.WRONGSIGNPERCENT),
141
+ points=part.points,
142
+ )
143
+ self.logger.info("NF answer part: %s ", ansStr)
144
+ else:
145
+ msg = "Type of the answer part is invalid"
146
+ raise QNotParsedException(msg, self.id)
147
+ ul = TextElements.ULIST.create()
148
+ item = TextElements.LISTITEM.create()
149
+ item.text = ansStr
150
+ ul.append(item)
151
+ part.text.append(ul)
152
+ self.logger.debug("Appended part %s %s to main text", partNum, part)
153
+ part.text.append(ET.Element("hr"))
154
+ self.questionTexts.extend(part.text)
155
+
156
+ def _assembleText(self, variant=0) -> list[ET.Element]:
157
+ textParts = super()._assembleText(variant=variant)
158
+ self.logger.debug("Appending QuestionParts to main text")
159
+ textParts.extend(self.questionTexts)
160
+ return textParts
161
+
162
+
163
+ class ClozeQuestionParser(NFMQuestionParser):
164
+ """Parser for the cloze question type."""
165
+
166
+ def __init__(self, *args, **kwargs) -> None:
167
+ super().__init__(*args, **kwargs)
168
+ self.question: ClozeQuestion
169
+
170
+ def setup(self, question: ClozeQuestion) -> None:
171
+ self.question: ClozeQuestion = question
172
+ super().setup(question)
173
+
174
+ def _parseAnswers(self) -> None:
175
+ self._setupParts()
176
+ self._parseAnswerParts()
177
+
178
+ def _setupParts(self) -> None:
179
+ parts: dict[int, ClozePart] = {
180
+ self.getPartNumber(key): ClozePart(self.question, self.rawInput[key])
181
+ for key in self.rawInput
182
+ if key.startswith(Tags.QUESTIONPART)
183
+ }
184
+ partsNum = len(parts)
185
+ equations: dict[int, str] = self._getPartValues(Tags.RESULT)
186
+ trueAnsws: dict[int, list[str]] = self._getPartValues(Tags.TRUE)
187
+ falseAnsws: dict[int, list[str]] = self._getPartValues(Tags.FALSE)
188
+ points: dict[int, float] = self._getPartValues(Tags.POINTS)
189
+ for num, part in parts.items():
190
+ eq = equations.get(num)
191
+ true = trueAnsws.get(num)
192
+ false = falseAnsws.get(num)
193
+ part.setAnswer(equation=eq, trueAns=true, falseAns=false)
194
+ if len(points) == 0:
195
+ pts = round(self.rawInput.get(Tags.POINTS) / partsNum, 3)
196
+ for part in parts.values():
197
+ part.points = pts
198
+ elif len(points) != partsNum:
199
+ logger.warning(
200
+ "Some Answer parts are missing the points, they will get the standard points"
201
+ )
202
+ for num, part in parts.items():
203
+ p = points.get(num)
204
+ part.points = p if p is not None else self.rawInput.get(Tags.POINTS)
205
+
206
+ self.question.questionParts = parts
207
+
208
+ @overload
209
+ def _getPartValues(self, Tag: Literal[Tags.RESULT]) -> dict[int, str]: ...
210
+ @overload
211
+ def _getPartValues(self, Tag: Literal[Tags.POINTS]) -> dict[int, float]: ...
212
+ @overload
213
+ def _getPartValues(
214
+ self, Tag: Literal[Tags.TRUE, Tags.FALSE]
215
+ ) -> dict[int, list[str]]: ...
216
+ def _getPartValues(self, Tag):
217
+ tagValues: dict = {
218
+ self.getPartNumber(key): self.rawInput[key]
219
+ for key in self.rawInput
220
+ if key.startswith(Tag)
221
+ }
222
+ self.logger.warning("Found part data %s: %s", Tag, tagValues)
223
+ return tagValues
224
+
225
+ def _parseAnswerParts(self) -> None:
226
+ """Parse the numeric or MC result items."""
227
+ try:
228
+ bps = str(self.rawInput[Tags.BPOINTS])
229
+ except KeyError:
230
+ bps = None
231
+ number = 1
232
+ else:
233
+ varNames: list[str] = self._getVarsList(bps)
234
+ self.question.variables, number = self._getVariablesDict(varNames)
235
+ for variant in range(number):
236
+ self.setupAstIntprt(self.question.variables, variant)
237
+ for partNum, part in self.question.questionParts.items():
238
+ if part.typ == "NFM":
239
+ result = self._calculateNFMPartResult(part, partNum, variant)
240
+ part.nfResults.append(result)
241
+ logger.debug("Appended NF part %s result: %s", partNum, result)
242
+ elif part.typ == "MC":
243
+ ansStr = self.getMCAnsStr(
244
+ part.trueAnswers, part.falseAnswers, points=part.points
245
+ )
246
+ part.mcAnswerString = ansStr
247
+ logger.debug("Appended MC part %s: %s", partNum, ansStr)
248
+ self._setVariants(number)
249
+
250
+ def _calculateNFMPartResult(
251
+ self, part: ClozePart, partNum: int, variant: int
252
+ ) -> float:
253
+ result = self.astEval(part.equation)
254
+ if isinstance(result, float):
255
+ try:
256
+ firstResult = self.rawInput[f"{Tags.FIRSTRESULT}:{partNum}"]
257
+ except KeyError:
258
+ firstResult = 0.0
259
+ if variant == 0 and not math.isclose(result, firstResult, rel_tol=0.002):
260
+ self.logger.warning(
261
+ "The calculated result %s differs from given firstResult: %s",
262
+ result,
263
+ firstResult,
264
+ )
265
+ return result
266
+ msg = f"The expression {part.equation} could not be evaluated."
267
+ raise QNotParsedException(msg, self.question.id)
268
+
269
+ def getPartNumber(self, indexKey: str) -> int:
270
+ """Return the number of the question Part.
271
+
272
+ The number should be given after the `@` sign.
273
+ This is number is used, to reference the question Text
274
+ and the expected answer fields together.
275
+ """
276
+ try:
277
+ num = re.findall(r":(\d+)$", indexKey)[0]
278
+ except IndexError:
279
+ msg = f"No :i question Part value given for {indexKey}"
280
+ raise QNotParsedException(msg, self.question.id)
281
+ else:
282
+ return int(num)
283
+
284
+ @staticmethod
285
+ def getNumericAnsStr(
286
+ result: float,
287
+ tolerance: float,
288
+ points: float = 1,
289
+ wrongSignCount: int = 50,
290
+ wrongSignFeedback: str = "your result has the wrong sign (+-)",
291
+ ) -> str:
292
+ """Generate the answer string from `result`.
293
+
294
+ Parameters.
295
+ ----------
296
+ wrongSignCount:
297
+ If the wrong sign `+` or `-` is given, how much of the points should be given.
298
+ Interpreted as percent.
299
+ tolerance:
300
+ The relative tolerance, as fraction
301
+
302
+ """
303
+ absTol = f":{round(result * tolerance, 3)}"
304
+ answerParts: list[str | float] = [
305
+ "{",
306
+ points,
307
+ ":NUMERICAL:=",
308
+ round(result, 3),
309
+ absTol,
310
+ "~%",
311
+ wrongSignCount,
312
+ "%",
313
+ round(result * (-1), 3),
314
+ absTol,
315
+ f"#{wrongSignFeedback}",
316
+ "}",
317
+ ]
318
+ answerPStrings = [str(part) for part in answerParts]
319
+ return "".join(answerPStrings)
320
+
321
+ @staticmethod
322
+ def getMCAnsStr(
323
+ true: list[str],
324
+ false: list[str],
325
+ points: float = 1,
326
+ ) -> str:
327
+ """Generate the answer string for the MC answers."""
328
+ truePercent: float = round(100 / len(true), 1)
329
+ falsePercent: float = round(100 / len(false), 1)
330
+ falseList: list[str] = [f"~%-{falsePercent}%{ans}" for ans in false]
331
+ trueList: list[str] = [f"~%{truePercent}%{ans}" for ans in true]
332
+ answerParts: list[str | float] = [
333
+ "{",
334
+ points,
335
+ ":MULTIRESPONSE:",
336
+ ]
337
+ answerParts.extend(trueList)
338
+ answerParts.extend(falseList)
339
+ answerParts.append("}")
340
+
341
+ answerPStrings = [str(part) for part in answerParts]
342
+ return "".join(answerPStrings)