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.
@@ -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())