oqtopus 0.2.0__py3-none-any.whl → 1.0.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.
- oqtopus/core/module.py +176 -32
- oqtopus/core/module_operation_task.py +234 -0
- oqtopus/core/module_package.py +27 -10
- oqtopus/core/modules_config.py +2 -0
- oqtopus/core/package_prepare_task.py +240 -25
- oqtopus/gui/database_connection_widget.py +12 -5
- oqtopus/gui/database_create_dialog.py +3 -3
- oqtopus/gui/database_duplicate_dialog.py +4 -4
- oqtopus/gui/logs_widget.py +94 -7
- oqtopus/gui/main_dialog.py +118 -31
- oqtopus/gui/module_selection_widget.py +110 -22
- oqtopus/gui/module_widget.py +647 -61
- oqtopus/gui/parameters_groupbox.py +25 -13
- oqtopus/gui/plugin_widget.py +13 -0
- oqtopus/gui/project_widget.py +5 -0
- oqtopus/gui/settings_dialog.py +2 -0
- oqtopus/oqtopus_plugin.py +10 -1
- oqtopus/ui/module_selection_widget.ui +96 -96
- oqtopus/ui/module_widget.ui +72 -58
- oqtopus/ui/settings_dialog.ui +18 -11
- oqtopus/utils/plugin_utils.py +113 -19
- oqtopus/utils/qt_utils.py +54 -0
- {oqtopus-0.2.0.dist-info → oqtopus-1.0.0.dist-info}/METADATA +1 -1
- oqtopus-1.0.0.dist-info/RECORD +47 -0
- {oqtopus-0.2.0.dist-info → oqtopus-1.0.0.dist-info}/WHEEL +1 -1
- tests/test_imports.py +59 -0
- oqtopus-0.2.0.dist-info/RECORD +0 -45
- {oqtopus-0.2.0.dist-info → oqtopus-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {oqtopus-0.2.0.dist-info → oqtopus-1.0.0.dist-info}/top_level.txt +0 -0
oqtopus/gui/module_widget.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
import psycopg
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from pum.upgrader import Upgrader
|
|
7
|
-
from qgis.PyQt.QtCore import Qt
|
|
5
|
+
import yaml
|
|
6
|
+
from qgis.PyQt.QtCore import QTimer
|
|
8
7
|
from qgis.PyQt.QtWidgets import QMessageBox, QWidget
|
|
9
8
|
|
|
10
9
|
from ..core.module import Module
|
|
10
|
+
from ..core.module_operation_task import ModuleOperationTask
|
|
11
11
|
from ..core.module_package import ModulePackage
|
|
12
|
+
from ..libs.pum.pum_config import PumConfig
|
|
13
|
+
from ..libs.pum.schema_migrations import SchemaMigrations
|
|
12
14
|
from ..utils.plugin_utils import PluginUtils, logger
|
|
13
|
-
from ..utils.qt_utils import CriticalMessageBox,
|
|
15
|
+
from ..utils.qt_utils import CriticalMessageBox, QtUtils
|
|
14
16
|
|
|
15
17
|
DIALOG_UI = PluginUtils.get_ui_class("module_widget.ui")
|
|
16
18
|
|
|
@@ -29,19 +31,136 @@ class ModuleWidget(QWidget, DIALOG_UI):
|
|
|
29
31
|
|
|
30
32
|
self.moduleInfo_install_pushButton.clicked.connect(self.__installModuleClicked)
|
|
31
33
|
self.moduleInfo_upgrade_pushButton.clicked.connect(self.__upgradeModuleClicked)
|
|
34
|
+
self.moduleInfo_roles_pushButton.clicked.connect(self.__rolesClicked)
|
|
35
|
+
self.uninstall_button.clicked.connect(self.__uninstallModuleClicked)
|
|
36
|
+
self.moduleInfo_cancel_button.clicked.connect(self.__cancelOperationClicked)
|
|
32
37
|
|
|
33
38
|
self.__current_module_package = None
|
|
34
39
|
self.__database_connection = None
|
|
40
|
+
self.__pum_config = None
|
|
41
|
+
self.__data_model_dir = None
|
|
42
|
+
|
|
43
|
+
# Background operation task
|
|
44
|
+
self.__operation_task = ModuleOperationTask(self)
|
|
45
|
+
self.__operation_task.signalProgress.connect(self.__onOperationProgress)
|
|
46
|
+
self.__operation_task.signalFinished.connect(self.__onOperationFinished)
|
|
47
|
+
|
|
48
|
+
# Timeout timer for detecting hung operations
|
|
49
|
+
self.__cancel_timeout_timer = QTimer(self)
|
|
50
|
+
self.__cancel_timeout_timer.setSingleShot(True)
|
|
51
|
+
self.__cancel_timeout_timer.timeout.connect(self.__onCancelTimeout)
|
|
52
|
+
|
|
53
|
+
# Hide cancel button and progress bar initially
|
|
54
|
+
self.moduleInfo_cancel_button.setVisible(False)
|
|
55
|
+
self.moduleInfo_progressbar.setVisible(False)
|
|
35
56
|
|
|
36
57
|
def setModulePackage(self, module_package: Module):
|
|
58
|
+
# Clean up old hook imports before loading new version
|
|
59
|
+
if self.__pum_config is not None:
|
|
60
|
+
try:
|
|
61
|
+
self.__pum_config.cleanup_hook_imports()
|
|
62
|
+
except Exception:
|
|
63
|
+
# Ignore errors during cleanup
|
|
64
|
+
pass
|
|
65
|
+
|
|
37
66
|
self.__current_module_package = module_package
|
|
38
67
|
self.__packagePrepareGetPUMConfig()
|
|
39
68
|
self.__updateModuleInfo()
|
|
40
69
|
|
|
70
|
+
def clearModulePackage(self):
|
|
71
|
+
"""Clear module package state and disable the stacked widget."""
|
|
72
|
+
# Cancel any running operations before clearing
|
|
73
|
+
if self.__operation_task.isRunning():
|
|
74
|
+
logger.warning("Canceling running operation due to module package change")
|
|
75
|
+
self.__operation_task.cancel()
|
|
76
|
+
# Don't wait - just reset UI immediately to avoid freezing
|
|
77
|
+
# The finished signal will be emitted when the thread stops
|
|
78
|
+
|
|
79
|
+
# Reset UI state immediately
|
|
80
|
+
self.__resetOperationUI()
|
|
81
|
+
|
|
82
|
+
# Clean up any imported modules from hooks to prevent conflicts
|
|
83
|
+
if self.__pum_config is not None:
|
|
84
|
+
try:
|
|
85
|
+
self.__pum_config.cleanup_hook_imports()
|
|
86
|
+
except Exception:
|
|
87
|
+
# Ignore errors during cleanup
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
self.__current_module_package = None
|
|
91
|
+
self.__pum_config = None
|
|
92
|
+
self.__data_model_dir = None
|
|
93
|
+
self.__updateModuleInfo()
|
|
94
|
+
|
|
95
|
+
def close(self):
|
|
96
|
+
"""Clean up resources when the widget is closed."""
|
|
97
|
+
# Cancel any running operations
|
|
98
|
+
if self.__operation_task.isRunning():
|
|
99
|
+
logger.warning("Canceling running operation due to widget close")
|
|
100
|
+
self.__operation_task.cancel()
|
|
101
|
+
|
|
102
|
+
# Clean up hook imports to release sys.path and sys.modules entries
|
|
103
|
+
if self.__pum_config is not None:
|
|
104
|
+
try:
|
|
105
|
+
self.__pum_config.cleanup_hook_imports()
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
def isOperationRunning(self) -> bool:
|
|
110
|
+
"""Return True if an operation is currently running."""
|
|
111
|
+
return self.__operation_task.isRunning()
|
|
112
|
+
|
|
41
113
|
def setDatabaseConnection(self, connection: psycopg.Connection):
|
|
114
|
+
# Cancel any running operations before changing database
|
|
115
|
+
if self.__operation_task.isRunning():
|
|
116
|
+
logger.warning("Canceling running operation due to database connection change")
|
|
117
|
+
self.__operation_task.cancel()
|
|
118
|
+
# Don't wait - just reset UI immediately to avoid freezing
|
|
119
|
+
|
|
120
|
+
# Reset UI state immediately
|
|
121
|
+
self.__resetOperationUI()
|
|
122
|
+
|
|
42
123
|
self.__database_connection = connection
|
|
43
124
|
self.__updateModuleInfo()
|
|
44
125
|
|
|
126
|
+
def __resetOperationUI(self):
|
|
127
|
+
"""Reset UI elements related to operations."""
|
|
128
|
+
self.__setOperationInProgress(False)
|
|
129
|
+
|
|
130
|
+
def __setOperationInProgress(self, in_progress: bool):
|
|
131
|
+
"""Enable or disable UI elements based on whether an operation is in progress.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
in_progress: True to disable UI (operation starting), False to enable (operation finished)
|
|
135
|
+
"""
|
|
136
|
+
# Main operation buttons - disable during operation
|
|
137
|
+
self.moduleInfo_install_pushButton.setEnabled(not in_progress)
|
|
138
|
+
self.db_parameters_CreateAndGrantRoles_upgrade_checkBox.setEnabled(not in_progress)
|
|
139
|
+
self.moduleInfo_upgrade_pushButton.setEnabled(not in_progress)
|
|
140
|
+
self.moduleInfo_roles_pushButton.setEnabled(not in_progress)
|
|
141
|
+
self.uninstall_button.setEnabled(not in_progress)
|
|
142
|
+
|
|
143
|
+
# Stacked widget contains all the form controls
|
|
144
|
+
self.moduleInfo_stackedWidget.setEnabled(not in_progress)
|
|
145
|
+
|
|
146
|
+
# Cancel button and progress bar - only visible during operation
|
|
147
|
+
self.moduleInfo_cancel_button.setVisible(in_progress)
|
|
148
|
+
self.moduleInfo_cancel_button.setEnabled(in_progress)
|
|
149
|
+
if not in_progress:
|
|
150
|
+
self.moduleInfo_cancel_button.setText(self.tr("Cancel"))
|
|
151
|
+
|
|
152
|
+
self.moduleInfo_progressbar.setVisible(in_progress)
|
|
153
|
+
if not in_progress:
|
|
154
|
+
self.moduleInfo_progressbar.setValue(0)
|
|
155
|
+
|
|
156
|
+
# Parent controls (module selection, database connection)
|
|
157
|
+
if self.parent() is not None:
|
|
158
|
+
parent_dialog = self.parent()
|
|
159
|
+
if hasattr(parent_dialog, "moduleSelection_groupBox"):
|
|
160
|
+
parent_dialog.moduleSelection_groupBox.setEnabled(not in_progress)
|
|
161
|
+
if hasattr(parent_dialog, "db_groupBox"):
|
|
162
|
+
parent_dialog.db_groupBox.setEnabled(not in_progress)
|
|
163
|
+
|
|
45
164
|
def __packagePrepareGetPUMConfig(self):
|
|
46
165
|
package_dir = self.__current_module_package.source_package_dir
|
|
47
166
|
|
|
@@ -70,7 +189,17 @@ class ModuleWidget(QWidget, DIALOG_UI):
|
|
|
70
189
|
return
|
|
71
190
|
|
|
72
191
|
try:
|
|
73
|
-
|
|
192
|
+
with open(pumConfigFilename) as file:
|
|
193
|
+
# since pum 1.3, the module id is mandatory in the pum config
|
|
194
|
+
config_data = yaml.safe_load(file)
|
|
195
|
+
if "pum" not in config_data:
|
|
196
|
+
config_data["pum"] = {}
|
|
197
|
+
if "module" not in config_data["pum"]:
|
|
198
|
+
config_data["pum"]["module"] = self.__current_module_package.module.id
|
|
199
|
+
base_path = Path(pumConfigFilename).parent
|
|
200
|
+
self.__pum_config = PumConfig(
|
|
201
|
+
base_path=base_path, install_dependencies=True, **config_data
|
|
202
|
+
)
|
|
74
203
|
except Exception as exception:
|
|
75
204
|
CriticalMessageBox(
|
|
76
205
|
self.tr("Error"),
|
|
@@ -117,6 +246,20 @@ class ModuleWidget(QWidget, DIALOG_UI):
|
|
|
117
246
|
).exec()
|
|
118
247
|
return
|
|
119
248
|
|
|
249
|
+
# Check that the module ID in the PUM config matches the selected module
|
|
250
|
+
pum_module_id = self.__pum_config.config.pum.module
|
|
251
|
+
selected_module_id = self.__current_module_package.module.id
|
|
252
|
+
if pum_module_id != selected_module_id:
|
|
253
|
+
CriticalMessageBox(
|
|
254
|
+
self.tr("Error"),
|
|
255
|
+
self.tr(
|
|
256
|
+
f"Module ID mismatch: The selected module is '{selected_module_id}' but the PUM configuration specifies '{pum_module_id}'."
|
|
257
|
+
),
|
|
258
|
+
None,
|
|
259
|
+
self,
|
|
260
|
+
).exec()
|
|
261
|
+
return
|
|
262
|
+
|
|
120
263
|
try:
|
|
121
264
|
parameters = self.parameters_groupbox.parameters_values()
|
|
122
265
|
|
|
@@ -124,34 +267,44 @@ class ModuleWidget(QWidget, DIALOG_UI):
|
|
|
124
267
|
if (
|
|
125
268
|
self.__current_module_package.type == ModulePackage.Type.PULL_REQUEST
|
|
126
269
|
or self.__current_module_package.type == ModulePackage.Type.BRANCH
|
|
270
|
+
or self.__current_module_package.prerelease
|
|
127
271
|
):
|
|
128
272
|
logger.warning(
|
|
129
|
-
"Installing module from branch
|
|
273
|
+
"Installing module from branch, pull request, or prerelease: set parameter beta_testing to True"
|
|
130
274
|
)
|
|
131
275
|
beta_testing = True
|
|
132
276
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
277
|
+
# Warn user before installing in beta testing mode
|
|
278
|
+
reply = QMessageBox.warning(
|
|
279
|
+
self,
|
|
280
|
+
self.tr("Beta Testing Installation"),
|
|
281
|
+
self.tr(
|
|
282
|
+
"You are about to install this module in BETA TESTING mode.\n\n"
|
|
283
|
+
"This means the module will not be allowed to receive future updates through normal upgrade process.\n"
|
|
284
|
+
"We strongly discourage using this for production databases.\n\n"
|
|
285
|
+
"Are you sure you want to continue?"
|
|
286
|
+
),
|
|
287
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
288
|
+
QMessageBox.StandardButton.No,
|
|
144
289
|
)
|
|
290
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
# Start background install operation
|
|
294
|
+
options = {
|
|
295
|
+
"roles": self.db_parameters_CreateAndGrantRoles_install_checkBox.isChecked(),
|
|
296
|
+
"grant": self.db_parameters_CreateAndGrantRoles_install_checkBox.isChecked(),
|
|
297
|
+
"beta_testing": beta_testing,
|
|
298
|
+
"allow_multiple_modules": PluginUtils.get_allow_multiple_modules(),
|
|
299
|
+
"install_demo_data": self.db_demoData_checkBox.isChecked(),
|
|
300
|
+
"demo_data_name": (
|
|
301
|
+
self.db_demoData_comboBox.currentText()
|
|
302
|
+
if self.db_demoData_checkBox.isChecked()
|
|
303
|
+
else None
|
|
304
|
+
),
|
|
305
|
+
}
|
|
145
306
|
|
|
146
|
-
|
|
147
|
-
demo_data_name = self.db_demoData_comboBox.currentText()
|
|
148
|
-
upgrader.install_demo_data(
|
|
149
|
-
connection=self.__database_connection,
|
|
150
|
-
name=demo_data_name,
|
|
151
|
-
parameters=parameters,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
self.__database_connection.commit()
|
|
307
|
+
self.__startOperation("install", parameters, options)
|
|
155
308
|
|
|
156
309
|
except Exception as exception:
|
|
157
310
|
CriticalMessageBox(
|
|
@@ -159,66 +312,499 @@ class ModuleWidget(QWidget, DIALOG_UI):
|
|
|
159
312
|
).exec()
|
|
160
313
|
return
|
|
161
314
|
|
|
162
|
-
|
|
315
|
+
def __upgradeModuleClicked(self):
|
|
316
|
+
if self.__current_module_package is None:
|
|
317
|
+
CriticalMessageBox(
|
|
318
|
+
self.tr("Error"), self.tr("Please select a module package first."), None, self
|
|
319
|
+
).exec()
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
if self.__database_connection is None:
|
|
323
|
+
CriticalMessageBox(
|
|
324
|
+
self.tr("Error"), self.tr("Please select a database service first."), None, self
|
|
325
|
+
).exec()
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
if self.__pum_config is None:
|
|
329
|
+
CriticalMessageBox(
|
|
330
|
+
self.tr("Error"), self.tr("No valid module available."), None, self
|
|
331
|
+
).exec()
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
# Check that the module ID in the PUM config matches the selected module
|
|
335
|
+
pum_module_id = self.__pum_config.config.pum.module
|
|
336
|
+
selected_module_id = self.__current_module_package.module.id
|
|
337
|
+
if pum_module_id != selected_module_id:
|
|
338
|
+
CriticalMessageBox(
|
|
339
|
+
self.tr("Error"),
|
|
340
|
+
self.tr(
|
|
341
|
+
f"Module ID mismatch: The selected module is '{selected_module_id}' but the PUM configuration specifies '{pum_module_id}'."
|
|
342
|
+
),
|
|
343
|
+
None,
|
|
344
|
+
self,
|
|
345
|
+
).exec()
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
# Check that the module ID matches the installed module in the database
|
|
349
|
+
sm = SchemaMigrations(self.__pum_config)
|
|
350
|
+
installed_beta_testing = False
|
|
351
|
+
with self.__database_connection.transaction():
|
|
352
|
+
if sm.exists(self.__database_connection):
|
|
353
|
+
migration_details = sm.migration_details(self.__database_connection)
|
|
354
|
+
installed_module_id = migration_details.get("module")
|
|
355
|
+
installed_beta_testing = migration_details.get("beta_testing", False)
|
|
356
|
+
|
|
357
|
+
if installed_module_id and installed_module_id != pum_module_id:
|
|
358
|
+
CriticalMessageBox(
|
|
359
|
+
self.tr("Error"),
|
|
360
|
+
self.tr(
|
|
361
|
+
f"Module ID mismatch: The database contains module '{installed_module_id}' but you are trying to upgrade with '{pum_module_id}'."
|
|
362
|
+
),
|
|
363
|
+
None,
|
|
364
|
+
self,
|
|
365
|
+
).exec()
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
# Confirm upgrade if installed module is in beta testing
|
|
369
|
+
if installed_beta_testing:
|
|
370
|
+
reply = QMessageBox.question(
|
|
371
|
+
self,
|
|
372
|
+
self.tr("Confirm Upgrade"),
|
|
373
|
+
self.tr(
|
|
374
|
+
"The installed module is in BETA TESTING mode.\n\n"
|
|
375
|
+
"Are you sure you want to upgrade? \n"
|
|
376
|
+
"This is not a recommended action: \n"
|
|
377
|
+
"if the installed version has missing or different changelogs, \n"
|
|
378
|
+
"the upgrade may fail or cause further issues."
|
|
379
|
+
),
|
|
380
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
381
|
+
QMessageBox.StandardButton.No,
|
|
382
|
+
)
|
|
383
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
parameters = self.parameters_groupbox.parameters_values()
|
|
388
|
+
|
|
389
|
+
beta_testing = False
|
|
390
|
+
if (
|
|
391
|
+
self.__current_module_package.type == ModulePackage.Type.PULL_REQUEST
|
|
392
|
+
or self.__current_module_package.type == ModulePackage.Type.BRANCH
|
|
393
|
+
or self.__current_module_package.prerelease
|
|
394
|
+
):
|
|
395
|
+
logger.warning(
|
|
396
|
+
"Upgrading module from branch, pull request, or prerelease: set parameter beta_testing to True"
|
|
397
|
+
)
|
|
398
|
+
beta_testing = True
|
|
399
|
+
|
|
400
|
+
# Start background upgrade operation
|
|
401
|
+
options = {
|
|
402
|
+
"beta_testing": beta_testing,
|
|
403
|
+
"force": installed_beta_testing,
|
|
404
|
+
"roles": self.db_parameters_CreateAndGrantRoles_upgrade_checkBox.isChecked(),
|
|
405
|
+
"grant": self.db_parameters_CreateAndGrantRoles_upgrade_checkBox.isChecked(),
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
self.__startOperation("upgrade", parameters, options)
|
|
409
|
+
|
|
410
|
+
except Exception as exception:
|
|
411
|
+
CriticalMessageBox(
|
|
412
|
+
self.tr("Error"), self.tr("Can't upgrade the module:"), exception, self
|
|
413
|
+
).exec()
|
|
414
|
+
|
|
415
|
+
def __uninstallModuleClicked(self):
|
|
416
|
+
if self.__current_module_package is None:
|
|
417
|
+
CriticalMessageBox(
|
|
418
|
+
self.tr("Error"), self.tr("Please select a module package first."), None, self
|
|
419
|
+
).exec()
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
if self.__database_connection is None:
|
|
423
|
+
CriticalMessageBox(
|
|
424
|
+
self.tr("Error"), self.tr("Please select a database service first."), None, self
|
|
425
|
+
).exec()
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
if self.__pum_config is None:
|
|
429
|
+
CriticalMessageBox(
|
|
430
|
+
self.tr("Error"), self.tr("No valid module available."), None, self
|
|
431
|
+
).exec()
|
|
432
|
+
return
|
|
433
|
+
|
|
434
|
+
# Check if uninstall hooks are defined
|
|
435
|
+
if not self.__pum_config.config.uninstall:
|
|
436
|
+
CriticalMessageBox(
|
|
437
|
+
self.tr("Error"),
|
|
438
|
+
self.tr(
|
|
439
|
+
"No uninstall configuration found. The module does not provide uninstall functionality."
|
|
440
|
+
),
|
|
441
|
+
None,
|
|
442
|
+
self,
|
|
443
|
+
).exec()
|
|
444
|
+
return
|
|
445
|
+
|
|
446
|
+
# Check if the installed version matches the selected version
|
|
447
|
+
sm = SchemaMigrations(self.__pum_config)
|
|
448
|
+
version_warning = ""
|
|
449
|
+
with self.__database_connection.transaction():
|
|
450
|
+
if not sm.exists(self.__database_connection):
|
|
451
|
+
raise Exception("Module is not installed in the database. This should not happen.")
|
|
452
|
+
installed_version = sm.baseline(self.__database_connection)
|
|
453
|
+
selected_version = self.__pum_config.last_version()
|
|
454
|
+
if installed_version != selected_version:
|
|
455
|
+
version_warning = (
|
|
456
|
+
f"\n\n⚠️ WARNING: Version mismatch detected!\n"
|
|
457
|
+
f"Installed version: {installed_version}\n"
|
|
458
|
+
f"Selected version: {selected_version}\n\n"
|
|
459
|
+
f"This could be an issue as the uninstall instructions may not match the installed datamodel."
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# Confirm uninstall with user
|
|
463
|
+
reply = QMessageBox.question(
|
|
163
464
|
self,
|
|
164
|
-
self.tr("
|
|
465
|
+
self.tr("Confirm Uninstall"),
|
|
165
466
|
self.tr(
|
|
166
|
-
f"
|
|
467
|
+
f"Are you sure you want to uninstall module '{self.__current_module_package.module.name}'?\n\n"
|
|
468
|
+
f"This action will remove all module data from the database and cannot be undone."
|
|
469
|
+
f"{version_warning}"
|
|
167
470
|
),
|
|
471
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
472
|
+
QMessageBox.StandardButton.No,
|
|
168
473
|
)
|
|
169
|
-
|
|
170
|
-
|
|
474
|
+
|
|
475
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
parameters = self.parameters_groupbox.parameters_values()
|
|
480
|
+
|
|
481
|
+
# Start background uninstall operation
|
|
482
|
+
self.__startOperation("uninstall", parameters, {})
|
|
483
|
+
|
|
484
|
+
except Exception as exception:
|
|
485
|
+
CriticalMessageBox(
|
|
486
|
+
self.tr("Error"), self.tr("Can't uninstall the module:"), exception, self
|
|
487
|
+
).exec()
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
def __rolesClicked(self):
|
|
491
|
+
"""Create and grant roles for the current module."""
|
|
492
|
+
if self.__current_module_package is None:
|
|
493
|
+
CriticalMessageBox(
|
|
494
|
+
self.tr("Error"), self.tr("Please select a module package first."), None, self
|
|
495
|
+
).exec()
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
if self.__database_connection is None:
|
|
499
|
+
CriticalMessageBox(
|
|
500
|
+
self.tr("Error"), self.tr("Please connect to a database first."), None, self
|
|
501
|
+
).exec()
|
|
502
|
+
return
|
|
503
|
+
|
|
504
|
+
if self.__pum_config is None:
|
|
505
|
+
CriticalMessageBox(
|
|
506
|
+
self.tr("Error"), self.tr("Module configuration not loaded."), None, self
|
|
507
|
+
).exec()
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
parameters = self.parameters_groupbox.parameters_values()
|
|
512
|
+
|
|
513
|
+
# Start background roles operation
|
|
514
|
+
self.__startOperation("roles", parameters, {})
|
|
515
|
+
|
|
516
|
+
except Exception as exception:
|
|
517
|
+
CriticalMessageBox(
|
|
518
|
+
self.tr("Error"), self.tr("Can't create and grant roles:"), exception, self
|
|
519
|
+
).exec()
|
|
520
|
+
return
|
|
521
|
+
|
|
522
|
+
def __show_error_state(self, message: str, on_label=None):
|
|
523
|
+
"""Display an error state and hide the widget content."""
|
|
524
|
+
label = on_label or self.moduleInfo_selected_label
|
|
525
|
+
label.setText(self.tr(message))
|
|
526
|
+
QtUtils.setForegroundColor(label, PluginUtils.COLOR_WARNING)
|
|
527
|
+
# Hide the stacked widget entirely when in error state
|
|
528
|
+
self.moduleInfo_stackedWidget.setVisible(False)
|
|
529
|
+
# Also hide uninstall button since module info is not valid
|
|
530
|
+
self.uninstall_button.setVisible(False)
|
|
531
|
+
|
|
532
|
+
def __show_database_info_page(self):
|
|
533
|
+
"""Show database information page when no module package is selected."""
|
|
534
|
+
with self.__database_connection.transaction():
|
|
535
|
+
installed_schemas = SchemaMigrations.schemas_with_migrations(
|
|
536
|
+
self.__database_connection
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# Show selected module label with prompt
|
|
540
|
+
self.moduleInfo_selected_label.setText(
|
|
541
|
+
self.tr("No module package selected. Please select a module from the left panel.")
|
|
171
542
|
)
|
|
543
|
+
QtUtils.setForegroundColor(self.moduleInfo_selected_label, PluginUtils.COLOR_WARNING)
|
|
172
544
|
|
|
173
|
-
|
|
545
|
+
# Show database info in installation label
|
|
546
|
+
if installed_schemas:
|
|
547
|
+
schema_list = ", ".join([f"<b>{schema}</b>" for schema in installed_schemas])
|
|
548
|
+
self.moduleInfo_installation_label.setText(
|
|
549
|
+
self.tr(f"Database has installed modules in schemas: {schema_list}")
|
|
550
|
+
)
|
|
551
|
+
else:
|
|
552
|
+
self.moduleInfo_installation_label.setText(
|
|
553
|
+
self.tr("No modules installed in this database")
|
|
554
|
+
)
|
|
555
|
+
QtUtils.resetForegroundColor(self.moduleInfo_installation_label)
|
|
174
556
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
557
|
+
# Hide the stacked widget since no module is selected
|
|
558
|
+
self.moduleInfo_stackedWidget.setVisible(False)
|
|
559
|
+
|
|
560
|
+
# Hide uninstall button
|
|
561
|
+
self.uninstall_button.setVisible(False)
|
|
562
|
+
|
|
563
|
+
def __show_install_page(self, version: str):
|
|
564
|
+
"""Switch to install page and configure it."""
|
|
565
|
+
# Check for modules in other schemas
|
|
566
|
+
with self.__database_connection.transaction():
|
|
567
|
+
installed_schemas = SchemaMigrations.schemas_with_migrations(
|
|
568
|
+
self.__database_connection
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
module_name = self.__current_module_package.module.name
|
|
572
|
+
module_id = self.__current_module_package.module.id
|
|
573
|
+
if installed_schemas:
|
|
574
|
+
schema_list = ", ".join([f"<b>{schema}</b>" for schema in installed_schemas])
|
|
575
|
+
self.moduleInfo_installation_label.setText(
|
|
576
|
+
self.tr(
|
|
577
|
+
f"No module <b>{module_name} ({module_id})</b> installed in this schema.<br>Module(s) in other schema(s): {schema_list}"
|
|
578
|
+
)
|
|
579
|
+
)
|
|
580
|
+
else:
|
|
581
|
+
self.moduleInfo_installation_label.setText(
|
|
582
|
+
self.tr(f"No module <b>{module_name} ({module_id})</b> installed")
|
|
583
|
+
)
|
|
584
|
+
QtUtils.resetForegroundColor(self.moduleInfo_installation_label)
|
|
585
|
+
self.moduleInfo_install_pushButton.setText(self.tr(f"Install {version}"))
|
|
586
|
+
self.moduleInfo_stackedWidget.setCurrentWidget(self.moduleInfo_stackedWidget_pageInstall)
|
|
587
|
+
# Ensure the stacked widget is visible when showing a valid page
|
|
588
|
+
self.moduleInfo_stackedWidget.setVisible(True)
|
|
589
|
+
|
|
590
|
+
def __show_upgrade_page(
|
|
591
|
+
self,
|
|
592
|
+
module_name: str,
|
|
593
|
+
baseline_version: str,
|
|
594
|
+
target_version: str,
|
|
595
|
+
beta_testing: bool = False,
|
|
596
|
+
):
|
|
597
|
+
"""Switch to upgrade page and configure it."""
|
|
598
|
+
beta_text = " (BETA TESTING)" if beta_testing else ""
|
|
599
|
+
|
|
600
|
+
# Check for modules in other schemas
|
|
601
|
+
sm = SchemaMigrations(self.__pum_config)
|
|
602
|
+
with self.__database_connection.transaction():
|
|
603
|
+
other_schemas = sm.exists_in_other_schemas(self.__database_connection)
|
|
604
|
+
|
|
605
|
+
# Build installation info text
|
|
606
|
+
install_text = f"Installed: module {module_name} at version {baseline_version}{beta_text}."
|
|
607
|
+
if other_schemas:
|
|
608
|
+
schema_list = ", ".join([f"<b>{schema}</b>" for schema in other_schemas])
|
|
609
|
+
install_text += f"<br>Module(s) in other schema(s): {schema_list}"
|
|
610
|
+
|
|
611
|
+
self.moduleInfo_installation_label.setText(install_text)
|
|
612
|
+
if beta_testing:
|
|
613
|
+
QtUtils.setForegroundColor(
|
|
614
|
+
self.moduleInfo_installation_label, PluginUtils.COLOR_WARNING
|
|
615
|
+
)
|
|
616
|
+
else:
|
|
617
|
+
QtUtils.resetForegroundColor(self.moduleInfo_installation_label)
|
|
618
|
+
self.moduleInfo_upgrade_pushButton.setText(self.tr(f"Upgrade to {target_version}"))
|
|
619
|
+
self.moduleInfo_stackedWidget.setCurrentWidget(self.moduleInfo_stackedWidget_pageUpgrade)
|
|
620
|
+
# Ensure the stacked widget is visible when showing a valid page
|
|
621
|
+
self.moduleInfo_stackedWidget.setVisible(True)
|
|
622
|
+
|
|
623
|
+
# Enable/disable upgrade button and show/hide roles button based on version comparison
|
|
624
|
+
if target_version <= baseline_version:
|
|
625
|
+
self.moduleInfo_upgrade_pushButton.setDisabled(True)
|
|
626
|
+
self.db_parameters_CreateAndGrantRoles_upgrade_checkBox.setDisabled(True)
|
|
627
|
+
# Show roles button when upgrade is not possible (same or higher version installed)
|
|
628
|
+
self.moduleInfo_roles_pushButton.setVisible(True)
|
|
629
|
+
logger.info(
|
|
630
|
+
f"Selected version {target_version} is equal to or lower than installed version {baseline_version}"
|
|
631
|
+
)
|
|
632
|
+
else:
|
|
633
|
+
self.moduleInfo_upgrade_pushButton.setEnabled(True)
|
|
634
|
+
self.db_parameters_CreateAndGrantRoles_upgrade_checkBox.setEnabled(True)
|
|
635
|
+
# Hide roles button when upgrade is possible
|
|
636
|
+
self.moduleInfo_roles_pushButton.setVisible(False)
|
|
637
|
+
|
|
638
|
+
def __configure_uninstall_button(self):
|
|
639
|
+
"""Show/hide uninstall button based on configuration."""
|
|
640
|
+
has_uninstall = bool(
|
|
641
|
+
self.__pum_config
|
|
642
|
+
and self.__pum_config.config.uninstall
|
|
643
|
+
and len(self.__pum_config.config.uninstall) > 0
|
|
180
644
|
)
|
|
181
|
-
|
|
645
|
+
self.uninstall_button.setVisible(has_uninstall)
|
|
182
646
|
|
|
183
647
|
def __updateModuleInfo(self):
|
|
184
648
|
if self.__current_module_package is None:
|
|
185
|
-
|
|
186
|
-
|
|
649
|
+
# List installed modules if available
|
|
650
|
+
if self.__database_connection is not None:
|
|
651
|
+
self.__show_database_info_page()
|
|
652
|
+
else:
|
|
653
|
+
self.__show_error_state("No module package selected")
|
|
187
654
|
return
|
|
188
655
|
|
|
189
656
|
if self.__database_connection is None:
|
|
190
|
-
self.
|
|
191
|
-
|
|
657
|
+
self.__show_error_state(
|
|
658
|
+
"No database connection available", on_label=self.moduleInfo_installation_label
|
|
659
|
+
)
|
|
192
660
|
return
|
|
193
661
|
|
|
194
662
|
if self.__pum_config is None:
|
|
195
|
-
self.
|
|
196
|
-
QtUtils.setForegroundColor(self.moduleInfo_label, PluginUtils.COLOR_WARNING)
|
|
663
|
+
self.__show_error_state("No PUM config available")
|
|
197
664
|
return
|
|
198
665
|
|
|
199
666
|
migrationVersion = self.__pum_config.last_version()
|
|
200
667
|
sm = SchemaMigrations(self.__pum_config)
|
|
201
668
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
669
|
+
# Set the selected module info
|
|
670
|
+
self.moduleInfo_selected_label.setText(
|
|
671
|
+
self.tr(
|
|
672
|
+
f"Module selected:{self.__current_module_package.module.name} - {migrationVersion}"
|
|
673
|
+
)
|
|
674
|
+
)
|
|
675
|
+
QtUtils.resetForegroundColor(self.moduleInfo_selected_label)
|
|
676
|
+
|
|
677
|
+
self.moduleInfo_stackedWidget.setEnabled(True)
|
|
678
|
+
self.__configure_uninstall_button()
|
|
679
|
+
|
|
680
|
+
# Wrap read-only queries in transaction to prevent idle connections
|
|
681
|
+
with self.__database_connection.transaction():
|
|
682
|
+
if sm.exists(self.__database_connection):
|
|
683
|
+
# Module is installed - show upgrade page
|
|
684
|
+
baseline_version = sm.baseline(self.__database_connection)
|
|
685
|
+
migration_details = sm.migration_details(self.__database_connection)
|
|
686
|
+
installed_beta_testing = migration_details.get("beta_testing", False)
|
|
687
|
+
self.__show_upgrade_page(
|
|
688
|
+
self.__current_module_package.module.name,
|
|
689
|
+
baseline_version,
|
|
690
|
+
migrationVersion,
|
|
691
|
+
installed_beta_testing,
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
logger.info(f"Migration table details: {migration_details}")
|
|
695
|
+
else:
|
|
696
|
+
# Module not installed - show install page
|
|
697
|
+
self.__show_install_page(migrationVersion)
|
|
208
698
|
|
|
209
|
-
|
|
210
|
-
|
|
699
|
+
def __startOperation(self, operation: str, parameters: dict, options: dict):
|
|
700
|
+
"""Start a background module operation."""
|
|
701
|
+
# Disable UI during operation
|
|
702
|
+
self.__setOperationInProgress(True)
|
|
703
|
+
|
|
704
|
+
# Start the background task
|
|
705
|
+
if operation == "install":
|
|
706
|
+
self.__operation_task.start_install(
|
|
707
|
+
self.__pum_config, self.__database_connection, parameters, **options
|
|
708
|
+
)
|
|
709
|
+
elif operation == "upgrade":
|
|
710
|
+
self.__operation_task.start_upgrade(
|
|
711
|
+
self.__pum_config, self.__database_connection, parameters, **options
|
|
712
|
+
)
|
|
713
|
+
elif operation == "uninstall":
|
|
714
|
+
self.__operation_task.start_uninstall(
|
|
715
|
+
self.__pum_config, self.__database_connection, parameters, **options
|
|
716
|
+
)
|
|
717
|
+
elif operation == "roles":
|
|
718
|
+
self.__operation_task.start_roles(
|
|
719
|
+
self.__pum_config, self.__database_connection, parameters, **options
|
|
211
720
|
)
|
|
212
721
|
|
|
213
|
-
|
|
214
|
-
|
|
722
|
+
def __cancelOperationClicked(self):
|
|
723
|
+
"""Cancel the current operation."""
|
|
724
|
+
self.moduleInfo_cancel_button.setEnabled(False)
|
|
725
|
+
self.moduleInfo_cancel_button.setText(self.tr("Canceling..."))
|
|
726
|
+
self.__operation_task.cancel()
|
|
727
|
+
logger.info("Operation cancel requested by user")
|
|
728
|
+
# Don't wait here - the __onOperationFinished signal will handle UI cleanup
|
|
729
|
+
|
|
730
|
+
# Start a timeout timer in case the operation hangs
|
|
731
|
+
self.__cancel_timeout_timer.start(5000) # 5 second timeout
|
|
732
|
+
|
|
733
|
+
def __onCancelTimeout(self):
|
|
734
|
+
"""Handle timeout when cancel doesn't complete."""
|
|
735
|
+
if self.__operation_task.isRunning():
|
|
736
|
+
logger.error("Operation did not respond to cancel request, forcing termination")
|
|
737
|
+
self.__operation_task.terminate()
|
|
738
|
+
# Force UI reset
|
|
739
|
+
self.__resetOperationUI()
|
|
740
|
+
# Show warning
|
|
741
|
+
QMessageBox.warning(
|
|
742
|
+
self,
|
|
743
|
+
self.tr("Operation Terminated"),
|
|
744
|
+
self.tr(
|
|
745
|
+
"The operation did not respond to the cancel request and was forcefully terminated. "
|
|
746
|
+
"The database may be in an inconsistent state. Please verify manually."
|
|
747
|
+
),
|
|
215
748
|
)
|
|
216
749
|
|
|
750
|
+
def __onOperationProgress(self, message: str, current: int, total: int):
|
|
751
|
+
"""Handle progress updates from background operation."""
|
|
752
|
+
# Update progress bar only, don't touch the installation label
|
|
753
|
+
if total > 0:
|
|
754
|
+
# Determinate progress
|
|
755
|
+
self.moduleInfo_progressbar.setFormat(message)
|
|
756
|
+
self.moduleInfo_progressbar.setTextVisible(True)
|
|
757
|
+
self.moduleInfo_progressbar.setMaximum(total)
|
|
758
|
+
self.moduleInfo_progressbar.setValue(current)
|
|
759
|
+
logger.debug(f"Progress update: {current}/{total} - {message}")
|
|
217
760
|
else:
|
|
218
|
-
#
|
|
219
|
-
self.
|
|
220
|
-
|
|
221
|
-
self.
|
|
222
|
-
self.
|
|
223
|
-
|
|
761
|
+
# Indeterminate progress
|
|
762
|
+
self.moduleInfo_progressbar.setFormat(message)
|
|
763
|
+
self.moduleInfo_progressbar.setTextVisible(True)
|
|
764
|
+
self.moduleInfo_progressbar.setMaximum(0)
|
|
765
|
+
self.moduleInfo_progressbar.setValue(0)
|
|
766
|
+
|
|
767
|
+
def __onOperationFinished(self, success: bool, error_message: str):
|
|
768
|
+
"""Handle completion of background operation."""
|
|
769
|
+
# Stop the timeout timer if running
|
|
770
|
+
self.__cancel_timeout_timer.stop()
|
|
771
|
+
|
|
772
|
+
# Always reset UI state, even if already reset
|
|
773
|
+
self.__resetOperationUI()
|
|
774
|
+
|
|
775
|
+
if success:
|
|
776
|
+
# Show success message
|
|
777
|
+
operation = self.__operation_task._ModuleOperationTask__operation
|
|
778
|
+
if operation == "install":
|
|
779
|
+
operation_name = "installed"
|
|
780
|
+
elif operation == "upgrade":
|
|
781
|
+
operation_name = "upgraded"
|
|
782
|
+
elif operation == "uninstall":
|
|
783
|
+
operation_name = "uninstalled"
|
|
784
|
+
elif operation == "roles":
|
|
785
|
+
operation_name = "roles created and granted"
|
|
786
|
+
else:
|
|
787
|
+
operation_name = "completed"
|
|
788
|
+
|
|
789
|
+
QMessageBox.information(
|
|
790
|
+
self,
|
|
791
|
+
self.tr(f"Module {operation_name}"),
|
|
792
|
+
self.tr(
|
|
793
|
+
f"Module '{self.__current_module_package.module.name}': {operation_name} successfully."
|
|
794
|
+
),
|
|
795
|
+
)
|
|
796
|
+
logger.info(
|
|
797
|
+
f"Module '{self.__current_module_package.module.name}': {operation_name} successfully."
|
|
224
798
|
)
|
|
799
|
+
|
|
800
|
+
# Refresh module info
|
|
801
|
+
self.__updateModuleInfo()
|
|
802
|
+
else:
|
|
803
|
+
# Show error message only if there's an actual error (not just cancellation)
|
|
804
|
+
if error_message:
|
|
805
|
+
CriticalMessageBox(
|
|
806
|
+
self.tr("Error"),
|
|
807
|
+
self.tr(f"Operation failed: {error_message}"),
|
|
808
|
+
None,
|
|
809
|
+
self,
|
|
810
|
+
).exec()
|