--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ProjectFlask/FlaskMigrateExtension/MigrateSummaryDialog.py Sat Nov 28 19:26:34 2020 +0100 @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog showing a summary of all created.migrations. +""" + +from PyQt5.QtCore import pyqtSlot, Qt, QProcess, QEventLoop, QTimer +from PyQt5.QtGui import QGuiApplication +from PyQt5.QtWidgets import ( + QDialog, QDialogButtonBox, QAbstractButton, QTreeWidgetItem +) + +from E5Gui import E5MessageBox + +from .Ui_MigrateSummaryDialog import Ui_MigrateSummaryDialog + + +class MigrateSummaryDialog(QDialog, Ui_MigrateSummaryDialog): + """ + Class implementing a dialog showing a summary of all created.migrations. + """ + def __init__(self, project, migrateProject, migrations="", parent=None): + """ + Constructor + + @param migrateProject reference to the migrate project extension + @type MigrateProject + @param project reference to the project object + @type Project + @param migrations directory path containing the migrations + @type str + @param parent reference to the parent widget + @type QWidget + """ + super(MigrateSummaryDialog, self).__init__(parent) + self.setupUi(self) + + self.__refreshButton = self.buttonBox.addButton( + self.tr("Refresh"), QDialogButtonBox.ActionRole) + self.__refreshButton.clicked.connect(self.showSummary) + + self.__project = project + self.__migrateProject = migrateProject + self.__migrations = migrations + self.__process = None + + def showSummary(self): + """ + Public method to show the migrations summary. + """ + workdir, env = self.__project.prepareRuntimeEnvironment() + if env is not None: + self.show() + self.raise_() + + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setFocus( + Qt.OtherFocusReason) + QGuiApplication.processEvents(QEventLoop.ExcludeUserInputEvents) + + command = self.__project.getFlaskCommand() + + self.__process = QProcess() + self.__process.setProcessEnvironment(env) + self.__process.setWorkingDirectory(workdir) + + args = ["db", "history", "--indicate-current"] + if self.__migrations: + args += ["--directory", self.__migrations] + + QGuiApplication.setOverrideCursor(Qt.WaitCursor) + self.__process.start(command, args) + ok = self.__process.waitForStarted(10000) + if ok: + ok = self.__process.waitForFinished(10000) + if ok: + out = str(self.__process.readAllStandardOutput(), "utf-8") + self.__processOutput(out) + else: + E5MessageBox.critical( + None, + self.tr("Migrations Summary"), + self.tr("""The Flask process did not finish within""" + """ 10 seconds.""")) + else: + E5MessageBox.critical( + None, + self.tr("Migrations Summary"), + self.tr("""The Flask process could not be started.""")) + for column in range(self.summaryWidget.columnCount()): + self.summaryWidget.resizeColumnToContents(column) + QGuiApplication.restoreOverrideCursor() + + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) + self.buttonBox.button(QDialogButtonBox.Close).setFocus( + Qt.OtherFocusReason) + + def __processOutput(self, output): + """ + Private method to process the flask output and populate the summary + list. + + @param output output of the flask process + @type str + """ + self.summaryWidget.clear() + self.upgradeButton.setEnabled(False) + self.downgradeButton.setEnabled(False) + + lines = output.splitlines() + for line in lines: + isCurrent = False + oldRev, rest = line.split("->") + rest, message = rest.split(",", 1) + newRev, *labels = rest.split() + if labels: + labelList = [ + label.replace("(", "").replace(")", "") + for label in labels + ] + labelsStr = ", ".join(labelList) + if "current" in labelList: + isCurrent = True + else: + labelsStr = "" + + itm = QTreeWidgetItem(self.summaryWidget, [ + oldRev.strip(), + newRev.strip(), + message.strip(), + labelsStr, + ]) + if isCurrent: + font = itm.font(0) + font.setBold(True) + for column in range(self.summaryWidget.columnCount()): + itm.setFont(column, font) + + @pyqtSlot() + def on_summaryWidget_itemSelectionChanged(self): + """ + Private slot to handle the selection of an entry. + """ + enable = bool(self.summaryWidget.selectedItems()) + self.upgradeButton.setEnabled(enable) + self.downgradeButton.setEnabled(enable) + + @pyqtSlot() + def on_upgradeButton_clicked(self): + """ + Private slot to upgrade to the selected revision + """ + itm = self.summaryWidget.selectedItems()[0] + rev = itm.text(1) + self.__migrateProject.upgradeDatabase(revision=rev) + self.showSummary() + + @pyqtSlot() + def on_downgradeButton_clicked(self): + """ + Private slot to downgrade to the selected revision + """ + itm = self.summaryWidget.selectedItems()[0] + rev = itm.text(1) + self.__migrateProject.downgradeDatabase(revision=rev) + self.showSummary() + + @pyqtSlot(QAbstractButton) + def on_buttonBox_clicked(self, button): + """ + Private slot handling a button press of the button box + + @param button reference to the pressed button + @type QAbstractButton + """ + if button is self.buttonBox.button(QDialogButtonBox.Cancel): + self.__cancelProcess() + + @pyqtSlot() + def __cancelProcess(self): + """ + Private slot to terminate the current process. + """ + if ( + self.__process is not None and + self.__process.state() != QProcess.NotRunning + ): + self.__process.terminate() + QTimer.singleShot(2000, self.__process.kill) + self.__process.waitForFinished(3000) + + self.__process = None