excel2moodle 0.5.1__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- excel2moodle/core/bullets.py +98 -0
- excel2moodle/core/dataStructure.py +6 -7
- excel2moodle/core/globals.py +3 -8
- excel2moodle/core/parser.py +37 -65
- excel2moodle/core/question.py +144 -76
- excel2moodle/extra/variableGenerator.py +250 -0
- excel2moodle/question_types/cloze.py +269 -103
- excel2moodle/question_types/nfm.py +41 -102
- excel2moodle/ui/UI_mainWindow.py +63 -36
- excel2moodle/ui/UI_variableGenerator.py +197 -0
- excel2moodle/ui/appUi.py +107 -44
- excel2moodle/ui/dialogs.py +44 -77
- excel2moodle/ui/equationChecker.py +2 -2
- excel2moodle/ui/treewidget.py +9 -24
- {excel2moodle-0.5.1.dist-info → excel2moodle-0.6.0.dist-info}/METADATA +67 -2
- {excel2moodle-0.5.1.dist-info → excel2moodle-0.6.0.dist-info}/RECORD +20 -19
- excel2moodle/core/numericMultiQ.py +0 -80
- excel2moodle/ui/windowDoc.py +0 -27
- {excel2moodle-0.5.1.dist-info → excel2moodle-0.6.0.dist-info}/WHEEL +0 -0
- {excel2moodle-0.5.1.dist-info → excel2moodle-0.6.0.dist-info}/entry_points.txt +0 -0
- {excel2moodle-0.5.1.dist-info → excel2moodle-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {excel2moodle-0.5.1.dist-info → excel2moodle-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,250 @@
|
|
1
|
+
import logging
|
2
|
+
import random
|
3
|
+
|
4
|
+
from asteval import Interpreter
|
5
|
+
from PySide6.QtWidgets import (
|
6
|
+
QDialog,
|
7
|
+
QLineEdit,
|
8
|
+
QMainWindow,
|
9
|
+
QMessageBox,
|
10
|
+
QTableWidget,
|
11
|
+
QTableWidgetItem,
|
12
|
+
)
|
13
|
+
|
14
|
+
from excel2moodle.core.question import Parametrics
|
15
|
+
from excel2moodle.ui.UI_variableGenerator import Ui_VariableGeneratorDialog
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class VariableGeneratorDialog(QDialog):
|
21
|
+
def __init__(self, parent: QMainWindow, parametrics: Parametrics) -> None:
|
22
|
+
super().__init__(parent)
|
23
|
+
self.ui = Ui_VariableGeneratorDialog()
|
24
|
+
self.ui.setupUi(self)
|
25
|
+
self.origParametrics = parametrics
|
26
|
+
self._generatedParametrics: Parametrics = Parametrics(
|
27
|
+
parametrics.equations, parametrics._resultChecker, identifier="genr"
|
28
|
+
)
|
29
|
+
# Load existing rules
|
30
|
+
for rule in self.origParametrics.variableRules:
|
31
|
+
self.ui.listWidget_rules.addItem(rule)
|
32
|
+
|
33
|
+
self._populate_variables_table()
|
34
|
+
populateDataSetTable(
|
35
|
+
self.ui.tableWidget_existing_variables,
|
36
|
+
parametrics=self.origParametrics,
|
37
|
+
)
|
38
|
+
self._connect_signals()
|
39
|
+
|
40
|
+
# Initially hide the existing variables table and generated variables table
|
41
|
+
self.ui.tableWidget_existing_variables.hide()
|
42
|
+
self.ui.groupBox_generated_variables.hide()
|
43
|
+
|
44
|
+
self.aeval = Interpreter(minimal=True) # Initialize asteval interpreter
|
45
|
+
|
46
|
+
def _populate_variables_table(self) -> None:
|
47
|
+
self.ui.tableWidget_variables.setRowCount(len(self.origParametrics.variables))
|
48
|
+
for row, (var_name, values) in enumerate(
|
49
|
+
self.origParametrics.variables.items()
|
50
|
+
):
|
51
|
+
self.ui.tableWidget_variables.setItem(row, 0, QTableWidgetItem(var_name))
|
52
|
+
# Add QLineEdit for Min, Max, and Decimal Places
|
53
|
+
min_le = QLineEdit(str(min(values)) if values else "0")
|
54
|
+
max_le = QLineEdit(str(max(values)) if values else "100")
|
55
|
+
dec_le = QLineEdit("0") # Default to 0 decimal places
|
56
|
+
|
57
|
+
self.ui.tableWidget_variables.setCellWidget(row, 1, min_le)
|
58
|
+
self.ui.tableWidget_variables.setCellWidget(row, 2, max_le)
|
59
|
+
self.ui.tableWidget_variables.setCellWidget(row, 3, dec_le)
|
60
|
+
|
61
|
+
def _connect_signals(self) -> None:
|
62
|
+
self.ui.pushButton_addRule.clicked.connect(self._add_rule)
|
63
|
+
self.ui.pushButton_removeRule.clicked.connect(self._remove_rule)
|
64
|
+
self.ui.pushButton_generate.clicked.connect(self.generateSets)
|
65
|
+
self.ui.pushButton_cancel.clicked.connect(self.reject)
|
66
|
+
self.ui.pushButton_save.clicked.connect(self._save_variables_and_close)
|
67
|
+
self.ui.groupBox_existing_variables.toggled.connect(
|
68
|
+
self.ui.tableWidget_existing_variables.setVisible
|
69
|
+
)
|
70
|
+
|
71
|
+
def _add_rule(self) -> None:
|
72
|
+
rule_text = self.ui.lineEdit_newRule.text().strip()
|
73
|
+
if rule_text:
|
74
|
+
self.ui.listWidget_rules.addItem(rule_text)
|
75
|
+
self.ui.lineEdit_newRule.clear()
|
76
|
+
|
77
|
+
def _remove_rule(self) -> None:
|
78
|
+
for item in self.ui.listWidget_rules.selectedItems():
|
79
|
+
self.ui.listWidget_rules.takeItem(self.ui.listWidget_rules.row(item))
|
80
|
+
|
81
|
+
def generateSets(self) -> None:
|
82
|
+
self._generatedParametrics.resetVariables() # Clear previous generated sets
|
83
|
+
self._rule_error_occurred = False # Reset error flag
|
84
|
+
|
85
|
+
varConstraints = {}
|
86
|
+
for row in range(self.ui.tableWidget_variables.rowCount()):
|
87
|
+
var_name = self.ui.tableWidget_variables.item(row, 0).text()
|
88
|
+
varConstraints[var_name] = {
|
89
|
+
"min": float(self.ui.tableWidget_variables.cellWidget(row, 1).text()),
|
90
|
+
"max": float(self.ui.tableWidget_variables.cellWidget(row, 2).text()),
|
91
|
+
"decimal_places": int(
|
92
|
+
self.ui.tableWidget_variables.cellWidget(row, 3).text()
|
93
|
+
),
|
94
|
+
}
|
95
|
+
|
96
|
+
rules = [
|
97
|
+
self.ui.listWidget_rules.item(i).text()
|
98
|
+
for i in range(self.ui.listWidget_rules.count())
|
99
|
+
]
|
100
|
+
|
101
|
+
num_sets = self.ui.spinBox_numSets.value()
|
102
|
+
|
103
|
+
try:
|
104
|
+
generated_sets = [
|
105
|
+
self._findSet(varConstraints, rules) for _ in range(num_sets)
|
106
|
+
]
|
107
|
+
except IndexError as e:
|
108
|
+
logger.exception("Invalid variables in Rule:")
|
109
|
+
QMessageBox.critical(self, "Rule Error", f"{e}")
|
110
|
+
except ValueError as e:
|
111
|
+
logger.warning("No variable set found:")
|
112
|
+
QMessageBox.warning(
|
113
|
+
self,
|
114
|
+
"Generation Failed",
|
115
|
+
f"{e} Consider relaxing your rules or increasing the number of attempts.",
|
116
|
+
)
|
117
|
+
else:
|
118
|
+
# convert the generated_sets list[dict[str, float]] into dict[str, list[float]]
|
119
|
+
# [{A:7, B:8}, {A:11, B:9}] -> {A: [7, 11], B: [8, 9]}
|
120
|
+
newVariables = {}
|
121
|
+
for var in self.origParametrics.variables:
|
122
|
+
newVariables[var] = [dataSet[var] for dataSet in generated_sets]
|
123
|
+
self._generatedParametrics.variables = newVariables
|
124
|
+
self.ui.groupBox_generated_variables.show()
|
125
|
+
populateDataSetTable(
|
126
|
+
self.ui.tableWidget_generated_variables,
|
127
|
+
parametrics=self._generatedParametrics,
|
128
|
+
)
|
129
|
+
|
130
|
+
def _findSet(
|
131
|
+
self,
|
132
|
+
constraints: dict[str, dict[str, float | int]],
|
133
|
+
rules: list[str],
|
134
|
+
maxAttempts: int = 1000,
|
135
|
+
) -> dict[str, float]:
|
136
|
+
"""Generate Random numbers for each variable and check if the rules apply.
|
137
|
+
|
138
|
+
Raises
|
139
|
+
------
|
140
|
+
`IndexError`: if the evaluation of the rule returns `None`
|
141
|
+
`ValueError`: if fater `maxAttemps` no set is found
|
142
|
+
|
143
|
+
"""
|
144
|
+
attempts = 0
|
145
|
+
while attempts < maxAttempts:
|
146
|
+
current_set: dict[str, float] = {}
|
147
|
+
# Generate initial values based on min/max constraints
|
148
|
+
for var_name, constr in constraints.items():
|
149
|
+
min_val: float = constr["min"]
|
150
|
+
max_val: float = constr["max"]
|
151
|
+
dec_places: int = constr["decimal_places"]
|
152
|
+
|
153
|
+
if dec_places == 0:
|
154
|
+
current_set[var_name] = float(
|
155
|
+
random.randint(int(min_val), int(max_val))
|
156
|
+
)
|
157
|
+
else:
|
158
|
+
current_set[var_name] = round(
|
159
|
+
random.uniform(min_val, max_val), dec_places
|
160
|
+
)
|
161
|
+
if self._check_rules(current_set, rules):
|
162
|
+
logger.info("Found matching set after %s attemps", attempts)
|
163
|
+
return current_set
|
164
|
+
attempts += 1
|
165
|
+
msg = f"Could not generate a valid set after {maxAttempts} attempts."
|
166
|
+
raise ValueError(msg)
|
167
|
+
|
168
|
+
def _check_rules(
|
169
|
+
self, varSet: dict[str, float], rules: list[str], show_error: bool = True
|
170
|
+
) -> bool:
|
171
|
+
# Create a local scope for evaluating rules
|
172
|
+
self.aeval.symtable.update(varSet)
|
173
|
+
|
174
|
+
for rule in rules:
|
175
|
+
# Evaluate the rule using asteval
|
176
|
+
res = self.aeval(rule)
|
177
|
+
if res is None:
|
178
|
+
msg = f"Error evaluating rule '{rule}'"
|
179
|
+
raise IndexError(msg)
|
180
|
+
if res is False:
|
181
|
+
return False
|
182
|
+
return True
|
183
|
+
|
184
|
+
def _save_variables_and_close(self) -> None:
|
185
|
+
"""Format variables set to fit `Parametrics`."""
|
186
|
+
logger.info("Saving new variables to the question")
|
187
|
+
newVars = self.origParametrics.variables.copy()
|
188
|
+
for varName in newVars:
|
189
|
+
newVars[varName].extend(self._generatedParametrics.variables[varName])
|
190
|
+
self.origParametrics.variableRules = [
|
191
|
+
self.ui.listWidget_rules.item(i).text()
|
192
|
+
for i in range(self.ui.listWidget_rules.count())
|
193
|
+
]
|
194
|
+
self.origParametrics.variables = newVars
|
195
|
+
logger.info("Rules saved to Parametrics.")
|
196
|
+
self.accept()
|
197
|
+
|
198
|
+
|
199
|
+
def populateDataSetTable(
|
200
|
+
tableWidget: QTableWidget,
|
201
|
+
parametrics: Parametrics | None = None,
|
202
|
+
) -> None:
|
203
|
+
"""Insert all Variables with their values into `tableWidget`."""
|
204
|
+
if parametrics is None:
|
205
|
+
return
|
206
|
+
variables = parametrics.variables
|
207
|
+
variants = parametrics.variants
|
208
|
+
tableWidget.setRowCount(len(variables) + len(parametrics.results))
|
209
|
+
tableWidget.setColumnCount(variants + 1) # Variable Name + Variants
|
210
|
+
headers = ["Variable"] + [f"Set {i + 1}" for i in range(variants)]
|
211
|
+
tableWidget.setHorizontalHeaderLabels(headers)
|
212
|
+
for row, (var, values) in enumerate(variables.items()):
|
213
|
+
tableWidget.setItem(row, 0, QTableWidgetItem(var))
|
214
|
+
for col, value in enumerate(values):
|
215
|
+
tableWidget.setItem(row, col + 1, QTableWidgetItem(str(value)))
|
216
|
+
for row, results in parametrics.results.items():
|
217
|
+
logger.debug("adding Results to the DataSetTable: %s", results)
|
218
|
+
tableWidget.setItem(
|
219
|
+
len(variables) + row - 1, 0, QTableWidgetItem(f"Results: {row}")
|
220
|
+
)
|
221
|
+
for variant, res in enumerate(results):
|
222
|
+
tableWidget.setItem(
|
223
|
+
len(variables) + row - 1,
|
224
|
+
variant + 1,
|
225
|
+
QTableWidgetItem(str(res)),
|
226
|
+
)
|
227
|
+
tableWidget.resizeColumnsToContents()
|
228
|
+
|
229
|
+
|
230
|
+
# This part is for testing the UI independently
|
231
|
+
if __name__ == "__main__":
|
232
|
+
import sys
|
233
|
+
|
234
|
+
from PySide6.QtWidgets import QApplication
|
235
|
+
|
236
|
+
# Mock ParametricQuestion for testing
|
237
|
+
class MockParametricQuestion:
|
238
|
+
def __init__(self) -> None:
|
239
|
+
self.origParametrics.variables = {
|
240
|
+
"a": [1.0, 2.0, 3.0],
|
241
|
+
"b": [10, 20, 30],
|
242
|
+
"c": [0.5, 1.5, 2.5],
|
243
|
+
}
|
244
|
+
|
245
|
+
app = QApplication(sys.argv)
|
246
|
+
mock_question = MockParametricQuestion()
|
247
|
+
dialog = VariableGeneratorDialog(paramQuestion=mock_question)
|
248
|
+
if dialog.exec():
|
249
|
+
print("Generated Sets:", dialog.generatedVarSets())
|
250
|
+
sys.exit(app.exec())
|