Added actions to show a migrations summary and migrations history.

Sat, 28 Nov 2020 19:26:34 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 28 Nov 2020 19:26:34 +0100
changeset 35
65a377b7a52c
parent 34
a91c6a1eb23f
child 36
548dea93941c

Added actions to show a migrations summary and migrations history.

PluginFlask.e4p file | annotate | diff | comparison | revisions
ProjectFlask/FlaskMigrateExtension/MigrateProjectExtension.py file | annotate | diff | comparison | revisions
ProjectFlask/FlaskMigrateExtension/MigrateSummaryDialog.py file | annotate | diff | comparison | revisions
ProjectFlask/FlaskMigrateExtension/MigrateSummaryDialog.ui file | annotate | diff | comparison | revisions
ProjectFlask/Project.py file | annotate | diff | comparison | revisions
ProjectFlask/RoutesDialog.py file | annotate | diff | comparison | revisions
--- 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)

eric ide

mercurial