Sat, 28 Nov 2020 19:26:34 +0100
Added actions to show a migrations summary and migrations history.
--- a/PluginFlask.e4p Thu Nov 26 20:11:25 2020 +0100 +++ b/PluginFlask.e4p Sat Nov 28 19:26:34 2020 +0100 @@ -27,6 +27,7 @@ <Source>ProjectFlask/FlaskMigrateExtension/FlaskMigrateDetector.py</Source> <Source>ProjectFlask/FlaskMigrateExtension/MigrateConfigDialog.py</Source> <Source>ProjectFlask/FlaskMigrateExtension/MigrateProjectExtension.py</Source> + <Source>ProjectFlask/FlaskMigrateExtension/MigrateSummaryDialog.py</Source> <Source>ProjectFlask/FlaskMigrateExtension/__init__.py</Source> <Source>ProjectFlask/FormSelectionDialog.py</Source> <Source>ProjectFlask/Project.py</Source> @@ -41,6 +42,7 @@ <Form>ProjectFlask/FlaskBabelExtension/PyBabelConfigDialog.ui</Form> <Form>ProjectFlask/FlaskCommandDialog.ui</Form> <Form>ProjectFlask/FlaskMigrateExtension/MigrateConfigDialog.ui</Form> + <Form>ProjectFlask/FlaskMigrateExtension/MigrateSummaryDialog.ui</Form> <Form>ProjectFlask/FormSelectionDialog.ui</Form> <Form>ProjectFlask/RoutesDialog.ui</Form> <Form>ProjectFlask/RunServerDialog.ui</Form>
--- a/ProjectFlask/FlaskMigrateExtension/MigrateProjectExtension.py Thu Nov 26 20:11:25 2020 +0100 +++ b/ProjectFlask/FlaskMigrateExtension/MigrateProjectExtension.py Sat Nov 28 19:26:34 2020 +0100 @@ -23,7 +23,6 @@ # TODO: add a submenu with action for the commands with command options -# TODO: add a submenu to show the created SQL commands (--sql option) class MigrateProject(QObject): """ Class implementing the flask-migrate project support. @@ -45,6 +44,8 @@ self.__project = project self.__e5project = e5App().getObject("Project") + + self.__migrationSummaryDialog = None def initActions(self): """ @@ -156,10 +157,9 @@ """<p>Upgrades the database to the current migration.</p>""" )) self.upgradeDatabaseAct.triggered.connect( - self.__upgradeDatabase) + self.upgradeDatabase) self.actions.append(self.upgradeDatabaseAct) - # TODO: add action for flask db downgrade self.downgradeDatabaseAct = E5Action( self.tr('Downgrade Database'), self.tr('&Downgrade Database'), @@ -172,8 +172,44 @@ """<p>Downgrades the database to the previous version.</p>""" )) self.downgradeDatabaseAct.triggered.connect( - self.__downgradeDatabase) + self.downgradeDatabase) self.actions.append(self.downgradeDatabaseAct) + + ######################################################### + ## actions to show migrations history information + ######################################################### + + self.migrationSummaryAct = E5Action( + self.tr('Show Migrations Summary'), + self.tr('Show Migrations &Summary'), + 0, 0, + self, 'flask_show_migrations_summary') + self.migrationSummaryAct.setStatusTip(self.tr( + 'Show a summary of the created database migrations')) + self.migrationSummaryAct.setWhatsThis(self.tr( + """<b>Show Migrations Summary</b>""" + """<p>Shows a summary list of the created database""" + """ migrations.</p>""" + )) + self.migrationSummaryAct.triggered.connect( + self.__showMigrationsSummary) + self.actions.append(self.migrationSummaryAct) + + self.migrationHistoryAct = E5Action( + self.tr('Show Migrations History'), + self.tr('Show Migrations &History'), + 0, 0, + self, 'flask_show_migrations_history') + self.migrationHistoryAct.setStatusTip(self.tr( + 'Show the full history of the created database migrations')) + self.migrationHistoryAct.setWhatsThis(self.tr( + """<b>Show Migrations History</b>""" + """<p>Shows the full history of the created database""" + """ migrations.</p>""" + )) + self.migrationHistoryAct.triggered.connect( + self.__showMigrationsHistory) + self.actions.append(self.migrationHistoryAct) def initMenu(self): """ @@ -194,6 +230,9 @@ menu.addAction(self.upgradeDatabaseAct) menu.addAction(self.downgradeDatabaseAct) menu.addSeparator() + menu.addAction(self.migrationSummaryAct) + menu.addAction(self.migrationHistoryAct) + menu.addSeparator() menu.addAction(self.migrateAvailabilityAct) menu.addAction(self.migrateInstallAct) @@ -264,6 +303,14 @@ return migrations + def projectClosed(self): + """ + Public method to handle the closing of a project. + """ + for dlg in (self.__migrationSummaryDialog,): + if dlg is not None: + dlg.close() + ######################################################## ## Menu related slots below ######################################################## @@ -421,9 +468,12 @@ ######################################################### @pyqtSlot() - def __upgradeDatabase(self): + def upgradeDatabase(self, revision=None): """ - Private slot to upgrade the database to the current migration. + Public slot to upgrade the database to the current migration. + + @param revision migration revision to upgrade to + @type str """ title = self.tr("Upgrade Database") @@ -433,6 +483,8 @@ args = ["upgrade"] if migrations: args += ["--directory", migrations] + if revision: + args.append(revision) dlg = FlaskCommandDialog( self.__project, title=title, @@ -442,11 +494,14 @@ dlg.exec() @pyqtSlot() - def __downgradeDatabase(self): + def downgradeDatabase(self, revision=None): """ - Private slot to downgrade the database to the previous version. + Public slot to downgrade the database to the previous version. + + @param revision migration revision to downgrade to + @type str """ - title = self.tr("downgrade Database") + title = self.tr("Downgrade Database") self.__ensureMigrateConfigured() migrations = self.__migrationsDirectory() @@ -454,6 +509,8 @@ args = ["downgrade"] if migrations: args += ["--directory", migrations] + if revision: + args.append(revision) dlg = FlaskCommandDialog( self.__project, title=title, @@ -461,3 +518,41 @@ ) if dlg.startCommand("db", args): dlg.exec() + + ######################################################### + ## slots to show migrations history information + ######################################################### + + @pyqtSlot() + def __showMigrationsSummary(self): + """ + Private slot to show a migrations history summary. + """ + from .MigrateSummaryDialog import MigrateSummaryDialog + + self.__ensureMigrateConfigured() + migrations = self.__migrationsDirectory() + + if self.__migrationSummaryDialog is None: + self.__migrationSummaryDialog = MigrateSummaryDialog( + self.__project, self, migrations=migrations) + + self.__migrationSummaryDialog.showSummary() + + @pyqtSlot() + def __showMigrationsHistory(self): + """ + Private slot to show the full migrations history. + """ + title = self.tr("Migrations History") + + self.__ensureMigrateConfigured() + migrations = self.__migrationsDirectory() + + args = ["history", "--indicate-current", "--verbose"] + if migrations: + args += ["--directory", migrations] + + dlg = FlaskCommandDialog(self.__project, title=title) + if dlg.startCommand("db", args): + dlg.exec()
--- /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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ProjectFlask/FlaskMigrateExtension/MigrateSummaryDialog.ui Sat Nov 28 19:26:34 2020 +0100 @@ -0,0 +1,137 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MigrateSummaryDialog</class> + <widget class="QDialog" name="MigrateSummaryDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>500</height> + </rect> + </property> + <property name="windowTitle"> + <string>Migrations Summary</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTreeWidget" name="summaryWidget"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <column> + <property name="text"> + <string>Old Revision</string> + </property> + </column> + <column> + <property name="text"> + <string>New Revision</string> + </property> + </column> + <column> + <property name="text"> + <string>Message</string> + </property> + </column> + <column> + <property name="text"> + <string>Labels</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPushButton" name="upgradeButton"> + <property name="toolTip"> + <string>Press to upgrade the database to the selected migration</string> + </property> + <property name="text"> + <string>Upgrade</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="downgradeButton"> + <property name="toolTip"> + <string>Press to downgrade the database to the selected migration</string> + </property> + <property name="text"> + <string>Downgrade</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>MigrateSummaryDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>227</x> + <y>479</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>MigrateSummaryDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>295</x> + <y>485</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- a/ProjectFlask/Project.py Thu Nov 26 20:11:25 2020 +0100 +++ b/ProjectFlask/Project.py Sat Nov 28 19:26:34 2020 +0100 @@ -427,6 +427,8 @@ for dlg in (self.__serverDialog, self.__routesDialog): if dlg is not None: dlg.close() + + self.__migrateProject.projectClosed() def supportedPythonVariants(self): """
--- a/ProjectFlask/RoutesDialog.py Thu Nov 26 20:11:25 2020 +0100 +++ b/ProjectFlask/RoutesDialog.py Sat Nov 28 19:26:34 2020 +0100 @@ -88,7 +88,7 @@ else: E5MessageBox.critical( None, - self.tr("Run Flask Server"), + self.tr("Flask Routes"), self.tr("""The Flask process could not be started.""")) for column in range(self.routesList.columnCount()): self.routesList.resizeColumnToContents(column)