Wed, 25 Nov 2020 20:10:41 +0100
Added the 'flask db migrate' command support.
# -*- coding: utf-8 -*- # Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the project support for flask-migrate. """ import os import glob from PyQt5.QtCore import pyqtSlot, QObject, QProcess from PyQt5.QtWidgets import QMenu, QDialog, QInputDialog, QLineEdit from E5Gui import E5MessageBox from E5Gui.E5Application import e5App from E5Gui.E5Action import E5Action import Utilities from ..FlaskCommandDialog import FlaskCommandDialog # TODO: add a submenu with action for the commands with command options class MigrateProject(QObject): """ Class implementing the flask-migrate project support. """ def __init__(self, plugin, project, parent=None): """ Constructor @param plugin reference to the plugin object @type ProjectFlaskPlugin @param project reference to the project object @type Project @param parent parent @type QObject """ super(MigrateProject, self).__init__(parent) self.__plugin = plugin self.__project = project self.__e5project = e5App().getObject("Project") def initActions(self): """ Public method to define the flask-migrate actions. """ self.actions = [] self.migrateConfigAct = E5Action( self.tr('Configure Migrate'), self.tr('&Configure Migrate'), 0, 0, self, 'flask_config_migrate') self.migrateConfigAct.setStatusTip(self.tr( 'Shows a dialog to edit the configuration for flask-migrate')) self.migrateConfigAct.setWhatsThis(self.tr( """<b>Configure Migrate</b>""" """<p>Shows a dialog to edit the configuration for""" """ flask-migrate.</p>""" )) self.migrateConfigAct.triggered.connect( self.__configureMigrate) self.actions.append(self.migrateConfigAct) self.migrateInstallAct = E5Action( self.tr('Install flask-migrate'), self.tr('&Install flask-migrate'), 0, 0, self, 'flask_install_migrate') self.migrateInstallAct.setStatusTip(self.tr( 'Installs the flask-migrate extension into the configured' ' environment')) self.migrateInstallAct.setWhatsThis(self.tr( """<b>Install flask-migrate</b>""" """<p>Installs the flask-migrate extension into the configured""" """ environment using the pip interface.</p>""" )) self.migrateInstallAct.triggered.connect( self.__installFlaskMigrate) self.actions.append(self.migrateInstallAct) self.migrateAvailabilityAct = E5Action( self.tr('Check flask-migrate Availability'), self.tr('Check flask-migrate &Availability'), 0, 0, self, 'flask_check_migrate') self.migrateAvailabilityAct.setStatusTip(self.tr( 'Check the availability of the flask-migrate extension')) self.migrateAvailabilityAct.setWhatsThis(self.tr( """<b>Check flask-migrate Availability</b>""" """<p>Check the availability of the flask-migrate extension.</p>""" )) self.migrateAvailabilityAct.triggered.connect( self.__checkAvailability) self.actions.append(self.migrateAvailabilityAct) ######################################################### ## action to initialize the database migration system ######################################################### self.migrateInitAct = E5Action( self.tr('Initialize Migrations'), self.tr('&Initialize Migrations'), 0, 0, self, 'flask_init_migrations') self.migrateInitAct.setStatusTip(self.tr( 'Initialize support for database migrations')) self.migrateInitAct.setWhatsThis(self.tr( """<b>Initialize Migrations</b>""" """<p>Initializes the support for database migrations to be""" """ stored in the configured migrations directory.</p>""" )) self.migrateInitAct.triggered.connect( self.__initMigrations) self.actions.append(self.migrateInitAct) ######################################################### ## action to create a new database migration ######################################################### self.migrateCreateAct = E5Action( self.tr('Create Migration'), self.tr('&Create Migration'), 0, 0, self, 'flask_create_migration') self.migrateCreateAct.setStatusTip(self.tr( 'Create a new migration for the current database')) self.migrateCreateAct.setWhatsThis(self.tr( """<b>Create Migration</b>""" """<p>Creates a new migration for the current database""" """ and stores it in the configured migrations directory.</p>""" )) self.migrateCreateAct.triggered.connect( self.__createMigration) self.actions.append(self.migrateCreateAct) ######################################################### ## action to up- and downgrade a databse ######################################################### # TODO: add action for flask db upgrade # TODO: add action for flask db downgrade def initMenu(self): """ Public method to initialize the flask-migrate menu. @return the menu generated @rtype QMenu """ menu = QMenu(self.tr("Database")) menu.setTearOffEnabled(True) menu.addAction(self.migrateConfigAct) menu.addSeparator() menu.addAction(self.migrateInitAct) menu.addSeparator() menu.addAction(self.migrateCreateAct) menu.addSeparator() menu.addAction(self.migrateAvailabilityAct) menu.addAction(self.migrateInstallAct) return menu def determineCapability(self): """ Public method to determine the availability of flask-migrate. """ available = self.__flaskMigrateAvailable() self.__project.setCapability("migrate", available) self.migrateConfigAct.setEnabled(available) self.migrateInstallAct.setEnabled(not available) self.migrateInitAct.setEnabled(available) def __flaskMigrateAvailable(self): """ Private method to check, if the 'flask-babel' package is available. @return flag indicating the availability of 'flask-babel' @rtype bool """ interpreter = self.__project.getVirtualenvInterpreter() if interpreter and Utilities.isinpath(interpreter): detector = os.path.join( os.path.dirname(__file__), "FlaskMigrateDetector.py") proc = QProcess() proc.setProcessChannelMode(QProcess.MergedChannels) proc.start(interpreter, [detector]) finished = proc.waitForFinished(30000) if finished and proc.exitCode() == 0: return True return False def __migrationsDirectory(self, abspath=False): """ Private method to calculate the path of the configured migrations directory. @param abspath flag indicating to return an absolute path @type bool @return path of the migrations directory @rtype str """ migrations = "" self.__ensureMigrateConfigured() migrations = self.__project.getData("migrate", "migrationsDirectory") if migrations: if abspath: migrations = self.__e5project.getAbsoluteUniversalPath( migrations) else: workdir = self.__project.getApplication()[0] migrations = os.path.relpath( self.__e5project.getAbsoluteUniversalPath(migrations), workdir ) else: if abspath: migrations = self.__e5project.getAbsoluteUniversalPath( "migrations") return migrations ######################################################## ## Menu related slots below ######################################################## @pyqtSlot() def __configureMigrate(self): """ Private slot to show a dialog to edit the migrate configuration. """ # TODO: implement MigrateConfigDialog from .MigrateConfigDialog import MigrateConfigDialog config = self.__project.getData("migrate", "") dlg = MigrateConfigDialog(config) if dlg.exec() == QDialog.Accepted: config = dlg.getConfiguration() self.__project.setData("migrate", "", config) def __ensureMigrateConfigured(self): """ Private method to ensure, that flask-migrate has been configured. """ config = self.__project.getData("migrate", "") if not config: self.__configureMigrate() @pyqtSlot() def __installFlaskMigrate(self): """ Private slot to install the flask-migrate extension into the configured environment. """ language = e5App().getObject("Project").getProjectLanguage() if language == "Python3": venvName = self.__plugin.getPreferences( "VirtualEnvironmentNamePy3") else: venvName = "" if venvName: interpreter = self.__project.getFullCommand("python") pip = e5App().getObject("Pip") pip.installPackages(["flask-migrate"], interpreter=interpreter) self.determineCapability() else: E5MessageBox.critical( None, self.tr("Install flask-migrate"), self.tr("The 'flask-migrate' extension could not be installed" " because no virtual environment has been" " configured.")) @pyqtSlot() def __checkAvailability(self): """ Private slot to check the availability of the 'flask-babel' extension. """ self.determineCapability() if self.__project.hasCapability("migrate"): msg = self.tr("The 'flask-migrate' extension is installed.") else: msg = self.tr("The 'flask-migrate' extension is not installed.") E5MessageBox.information( None, self.tr("flask-migrate Availability"), msg) ######################################################### ## slot to initialize the database migration system ######################################################### @pyqtSlot() def __initMigrations(self): """ Private slot to initialize the database migration system. """ title = self.tr("Initialize Migrations") self.__ensureMigrateConfigured() migrations = self.__migrationsDirectory() args = ["init"] if migrations: args += ["--directory", migrations] multidb = E5MessageBox.yesNo( None, self.tr("Multiple Databases"), self.tr("""Shall the support for multiple databases be""" """ activated?""")) if multidb: args.append("--multidb") dlg = FlaskCommandDialog( self.__project, title=title, msgSuccess=self.tr("\nMigrations initialized successfully.") ) if dlg.startCommand("db", args): dlg.exec() if dlg.normalExit(): for root, _dirs, files in os.walk( self.__migrationsDirectory(abspath=True) ): for fileName in files: fullName = os.path.join(root, fileName) self.__e5project.appendFile(fullName) browser = (e5App().getObject("ProjectBrowser") .getProjectBrowser("others")) alembic = os.path.join( self.__migrationsDirectory(abspath=True), "alembic.ini" ) browser.sourceFile.emit(alembic) ######################################################### ## slot to create a new database migration ######################################################### @pyqtSlot() def __createMigration(self): """ Private slot to create a new database migration. """ title = self.tr("Create Migration") self.__ensureMigrateConfigured() migrations = self.__migrationsDirectory() message, ok = QInputDialog.getText( None, title, self.tr("Enter a short message for the migration:"), QLineEdit.Normal) if ok: args = ["migrate"] if migrations: args += ["--directory", migrations] if message: args += ["--message", message] dlg = FlaskCommandDialog( self.__project, title=title, msgSuccess=self.tr("\nMigration created successfully.") ) if dlg.startCommand("db", args): dlg.exec() if dlg.normalExit(): versionsPattern = os.path.join( self.__migrationsDirectory(abspath=True), "versions", "*.py") for fileName in glob.iglob(versionsPattern): self.__e5project.appendFile(fileName) ######################################################### ## slots to up- and downgrade a databse #########################################################