diff --git a/CenRa_FLUX/flux_editor.py b/CenRa_FLUX/flux_editor.py index ea465e6..8d6bea6 100644 --- a/CenRa_FLUX/flux_editor.py +++ b/CenRa_FLUX/flux_editor.py @@ -1,7 +1,11 @@ # -*- coding: utf-8 -*- +""" +Module Flux Editor pour le plugin CenRA_FLUX +Permet de gérer et charger des couches depuis les bases de données PostGIS (SIG et REF) +""" from __future__ import absolute_import -# Import the PyQt and QGIS libraries +# Import des bibliothèques PyQt et QGIS from builtins import str from qgis.PyQt import QtCore, QtGui @@ -9,7 +13,17 @@ from qgis.PyQt.QtCore import QSettings from qgis.PyQt import QtWidgets from qgis.PyQt.QtGui import QIcon -from qgis.core import QgsDataSourceUri, QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsProject, QgsSettings, QgsApplication, QgsVectorLayer, QgsRasterLayer, QgsWkbTypes +from qgis.core import ( + QgsDataSourceUri, + QgsCoordinateReferenceSystem, + QgsCoordinateTransform, + QgsProject, + QgsSettings, + QgsApplication, + QgsVectorLayer, + QgsRasterLayer, + QgsWkbTypes +) from qgis.PyQt.QtWidgets import ( QDialog, QPushButton, @@ -31,9 +45,11 @@ from qgis.utils import iface import psycopg2 import psycopg2.extras +# Variable globale pour le mode debug global DeBUG DeBUG = 0 +# Création des icônes pour les couches raster et vecteur itemIconRaster = QTableWidgetItem() icon = QIcon() icon.addPixmap(QtGui.QPixmap(resources_path('icons', 'mIconRaster.svg')), QIcon.Mode(0), QIcon.State(1)) @@ -44,28 +60,36 @@ icon = QIcon() icon.addPixmap(QtGui.QPixmap(resources_path('icons', 'mIconVecteur.svg')), QIcon.Mode(0), QIcon.State(1)) itemIconVecteur.setIcon(icon) +# Récupération des informations de connexion à la base de données try: account = login_base('account') - user = account[0] - mdp = account[1] - host = account[2] - port = account[3] - dbname = account[4] - sigdb = account[5] - refdb = account[6] + user = account[0] # Nom d'utilisateur + mdp = account[1] # Mot de passe + host = account[2] # Hôte de la base de données + port = account[3] # Port de connexion + dbname = account[4] # Nom de la base de données + sigdb = account[5] # Base de données SIG + refdb = account[6] # Base de données REF except NameError: print('Fails to login DB for account') +# Chargement de l'interface utilisateur depuis le fichier .ui EDITOR_CLASS = load_ui('CenRa_Flux_base.ui') -targetCrs = QgsCoordinateReferenceSystem('EPSG:4326') -layerCrs = QgsCoordinateReferenceSystem('EPSG:2154') +# Configuration des systèmes de coordonnées de référence (CRS) +targetCrs = QgsCoordinateReferenceSystem('EPSG:4326') # WGS84 (coordonnées géographiques) +layerCrs = QgsCoordinateReferenceSystem('EPSG:2154') # Lambert 93 (projection française) TranformCRS = QgsCoordinateTransform(layerCrs, targetCrs, QgsProject.instance()) class Flux_Editor(QDialog, EDITOR_CLASS): + """ + Classe principale de l'éditeur de flux + Gère l'interface de sélection et de chargement des couches depuis les bases de données + """ def __init__(self, parent=None): + """Initialisation de l'interface et des composants""" _ = parent super().__init__() self.setupUi(self) @@ -74,29 +98,34 @@ class Flux_Editor(QDialog, EDITOR_CLASS): self.setWindowIcon(QtGui.QIcon(resources_path('icons', 'icon.png'))) self.first_start = None self.iface = iface + # Configuration des éléments visuels de l'interface self.label_3.setPixmap(QtGui.QPixmap(resources_path('ui', 'logo.png'))) self.commandLinkButton.setIcon(QtGui.QIcon(resources_path('ui', 'arrow-bottom.png'))) self.commandLinkButton_2.setIcon(QtGui.QIcon(resources_path('ui', 'arrow-up.png'))) + # Configuration du tableau des couches disponibles self.tableWidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger(0)) self.toolButton.setIcon(QtGui.QIcon(resources_path('ui', 'find.png'))) self.toolButton_2.setIcon(QtGui.QIcon(resources_path('ui', 'star.png'))) + # Ajout des options de base de données (SIG ou REF) self.comboBox_2.addItem("SIG") self.comboBox_2.addItem('REF') - self.commandLinkButton.clicked.connect(self.selection_flux) - self.tableWidget.itemDoubleClicked.connect(self.selection_flux) - self.pushButton_2.clicked.connect(self.limite_flux) - self.commandLinkButton_2.clicked.connect(self.suppression_flux) - self.tableWidget_2.itemDoubleClicked.connect(self.suppression_flux) - self.comboBox_2.currentIndexChanged.connect(self.bd_source) + # Connexion des signaux aux slots (événements de l'interface) + self.commandLinkButton.clicked.connect(self.selection_flux) # Bouton de sélection de flux + self.tableWidget.itemDoubleClicked.connect(self.selection_flux) # Double-clic sur une couche + self.pushButton_2.clicked.connect(self.limite_flux) # Bouton de chargement + self.commandLinkButton_2.clicked.connect(self.suppression_flux) # Bouton de suppression + self.tableWidget_2.itemDoubleClicked.connect(self.suppression_flux) # Double-clic pour supprimer + self.comboBox_2.currentIndexChanged.connect(self.bd_source) # Changement de base de données self.checkBox.hide() - self.toolButton.clicked.connect(self.getCanevas) - self.toolButton_2.clicked.connect(self.filtre_favorit) + self.toolButton.clicked.connect(self.getCanevas) # Filtrer par emprise du canevas + self.toolButton_2.clicked.connect(self.filtre_favorit) # Filtrer par favoris layout = QVBoxLayout() - self.lineEdit.textChanged.connect(self.filtre_dynamique) + self.lineEdit.textChanged.connect(self.filtre_dynamique) # Recherche dynamique layout.addWidget(self.lineEdit) self.viewer.hide() + # Configuration du menu de debug (caché par défaut) self.DeBUG.addItem('') self.DeBUG.addItem('Dev') self.DeBUG.addItem('01') @@ -105,13 +134,14 @@ class Flux_Editor(QDialog, EDITOR_CLASS): self.comboBox.currentIndexChanged.connect(self.initialisation_flux) self.DeBUG.currentIndexChanged.connect(self.SwitchDEBUG) - self.DeBUG.hide() + self.DeBUG.hide() # Menu debug caché par défaut def raise_(self): """Run method that performs all the real work""" self.bd_source() def SwitchDEBUG(self): + """Change les paramètres de connexion selon le mode debug sélectionné""" try: global user, mdp, host, port, dbname, sigdb, refdb account = login_base(self.DeBUG.currentText()) @@ -131,10 +161,12 @@ class Flux_Editor(QDialog, EDITOR_CLASS): self.DeBUG.show() def mousePressEvent(self, event): + """Détecte les clics sur le logo pour activer le mode debug (3 clics consécutifs)""" global DeBUG + # Zone du logo (coordonnées x: 330-560, y: 5-75) if 330 <= event.pos().x() <= 560 and 5 <= event.pos().y() <= 75: DeBUG = DeBUG + 1 - if DeBUG == 3: + if DeBUG == 3: # Activation après 3 clics DeBUG = 0 self.ModeDeBUG() else: @@ -142,20 +174,23 @@ class Flux_Editor(QDialog, EDITOR_CLASS): self.DeBUG.hide() def bd_source(self): + """Sélectionne la base de données source (SIG ou REF)""" self.activateWindow() bd_origine = self.comboBox_2.currentText() if bd_origine == 'REF': - self.run_ref() + self.run_ref() # Connexion à la base REF if bd_origine == 'SIG': - self.run_sig() + self.run_sig() # Connexion à la base SIG def run_ref(self): - """Run method that performs all the real work""" + """Initialise la connexion à la base de données REF""" + # Vider le tableau des flux sélectionnés while self.tableWidget_2.rowCount() > 0: self.tableWidget_2.removeRow(self.tableWidget_2.rowCount() - 1) global cur, con, dbtype dbtype = refdb + # Connexion à la base de données REF con = psycopg2.connect("host=" + host + " port=" + port + " dbname=" + dbtype + " user=" + user + " password=" + mdp) cur = con.cursor(cursor_factory=psycopg2.extras.DictCursor) self.combobox_custom() @@ -172,11 +207,13 @@ class Flux_Editor(QDialog, EDITOR_CLASS): pass def run_sig(self): - """Run method that performs all the real work""" + """Initialise la connexion à la base de données SIG""" + # Vider le tableau des flux sélectionnés while self.tableWidget_2.rowCount() > 0: self.tableWidget_2.removeRow(self.tableWidget_2.rowCount() - 1) global cur, con, dbtype dbtype = sigdb + # Connexion à la base de données SIG con = psycopg2.connect("host=" + host + " port=" + port + " dbname=" + dbtype + " user=" + user + " password=" + mdp) cur = con.cursor(cursor_factory=psycopg2.extras.DictCursor) self.combobox_custom() @@ -189,17 +226,20 @@ class Flux_Editor(QDialog, EDITOR_CLASS): self.tableWidget_2.removeRow(self.tableWidget_2.currentRow()) def AddOrDelToUserFav(self): + """Ajoute ou supprime une couche des favoris de l'utilisateur""" selected_items = self.tableWidget.selectedItems() - favorit_statut = selected_items[4].text() + favorit_statut = selected_items[4].text() # 0 = non favori, 1 = favori selected_items[4].tableWidget().removeCellWidget(selected_items[4].row(), 4) schema_name = selected_items[2].text() table_name = selected_items[3].text() + # Ajout aux favoris if favorit_statut == "0": - FAV = "mStarIconDel.png" + FAV = "mStarIconDel.png" # Icône étoile pleine selected_items[4].setText("1") + # Requête SQL pour ajouter aux favoris SQLAddFav = """INSERT INTO admin_sig.favtable (utilisateur, schema_name, table_name) VALUES ('""" + user + "','" + schema_name + "','" + table_name + """');""" if dbtype == sigdb: cur.execute(SQLAddFav) @@ -211,9 +251,11 @@ class Flux_Editor(QDialog, EDITOR_CLASS): conSIG.commit() conSIG.close() + # Suppression des favoris else: - FAV = "mStarIconAdd.png" + FAV = "mStarIconAdd.png" # Icône étoile vide selected_items[4].setText("0") + # Requête SQL pour supprimer des favoris SQLDelFav = """DELETE FROM admin_sig.favtable WHERE utilisateur LIKE '""" + user + """' AND schema_name LIKE '""" + schema_name + """' AND table_name LIKE '""" + table_name + """';""" if dbtype == sigdb: cur.execute(SQLDelFav) @@ -232,10 +274,11 @@ class Flux_Editor(QDialog, EDITOR_CLASS): self.FavButton.clicked.connect(self.AddOrDelToUserFav) def initialisation_flux(self): + """Initialise et remplit le tableau des couches disponibles""" if self.toolButton_2.text() == "1": self.filtre_favorit(None) self.tableWidget.clear() - if NoSignals == 0: + if NoSignals == 0: # Éviter les boucles infinies lors des mises à jour if dbtype == sigdb: if self.comboBox.currentText() == 'toutes les catégories': custom_list = schemaname_list @@ -253,11 +296,13 @@ class Flux_Editor(QDialog, EDITOR_CLASS): cur.execute(custom_list) list_schema = cur.fetchall() + # Vérification de la présence de couches raster dans la base SQLcountRaster = """SELECT schemaname,viewname FROM pg_catalog.pg_views WHERE schemaname LIKE 'public' AND viewname LIKE 'raster_columns';""" cur.execute(SQLcountRaster) RasterIF = len(cur.fetchall()) + # Récupération de la liste des couches raster si disponibles if RasterIF == 1: SQLloadRaster = """SELECT concat(r_table_schema,'.',r_table_name) from public.raster_columns; """ cur.execute(SQLloadRaster) @@ -268,6 +313,7 @@ class Flux_Editor(QDialog, EDITOR_CLASS): else: RasterList = [] + # Récupération des projets QGIS stockés dans la base de données SQLprojects = """SELECT schemaname, tablename FROM pg_catalog.pg_tables WHERE tablename LIKE 'qgis_projects'""" cur.execute(SQLprojects) list_projects = cur.fetchall() @@ -278,6 +324,7 @@ class Flux_Editor(QDialog, EDITOR_CLASS): cur.execute(SQLProjectsQgis) list_projects_qgis.append(cur.fetchall()) + # Récupération des droits d'accès de l'utilisateur sur les tables if self.comboBox.currentText() == 'toutes les catégories': SQLGrands = """SELECT concat(table_schema,'.',table_name) FROM information_schema.role_table_grants WHERE grantee in(SELECT rolname FROM pg_catalog.pg_roles WHERE oid in(SELECT roleid FROM pg_auth_members WHERE member = (SELECT usesysid FROM pg_catalog.pg_user WHERE usename = '""" + user + """'))) and privilege_type = 'SELECT';""" else: @@ -291,6 +338,7 @@ class Flux_Editor(QDialog, EDITOR_CLASS): for grandsFind in list_grands: GrandUser.append(grandsFind[0]) + # Récupération de la liste des favoris de l'utilisateur SQLFavTable = "SELECT concat(schema_name, '.', table_name) FROM admin_sig.favtable WHERE utilisateur LIKE '" + user + "';" if dbtype == refdb: conSIG = psycopg2.connect("host=" + host + " port=" + port + " dbname=" + sigdb + " user=" + user + " password=" + mdp) @@ -306,6 +354,7 @@ class Flux_Editor(QDialog, EDITOR_CLASS): for favFind in list_fav: FavList.append(favFind[0]) + # Remplissage du tableau avec les couches disponibles self.tableWidget.setRowCount(len(list_schema)) self.tableWidget.setColumnCount(5) i = 0 @@ -371,9 +420,11 @@ class Flux_Editor(QDialog, EDITOR_CLASS): self.tableWidget.setItem(i, 4, item) self.tableWidget.setCellWidget(i, 4, self.FavButton) + # Coloration des lignes selon les droits d'accès if (str(value[0]) + '.' + str(value[1])) in GrandUser: - pass + pass # L'utilisateur a les droits else: + # Coloration en violet si droits insuffisants for j in range(self.tableWidget.columnCount()): self.tableWidget.item(i, j).setBackground(QtGui.QColor(187, 134, 192, 50)) self.tableWidget.item(i, j).setToolTip('Droit insuffisant pour ouvrire la couche !') @@ -441,6 +492,7 @@ class Flux_Editor(QDialog, EDITOR_CLASS): self.filtre_dynamique(self.lineEdit.text()) def selection_flux(self): + """Ajoute une couche sélectionnée à la liste des flux à charger""" selected_row = 0 selected_items = self.tableWidget.selectedItems() @@ -469,7 +521,8 @@ class Flux_Editor(QDialog, EDITOR_CLASS): return len(existing_items) > 0 def limite_flux(self): - + """Vérifie le nombre de flux à charger et affiche un avertissement si > 5""" + # Avertissement si plus de 5 couches sélectionnées (risque de performance) if self.tableWidget_2.rowCount() > 5: self.QMBquestion = QMessageBox.question(iface.mainWindow(), u"Attention !", "Le nombre de flux à charger en une seule fois est limité à 5 pour des questions de performances. Souhaitez vous tout de même charger les " + str( @@ -485,6 +538,7 @@ class Flux_Editor(QDialog, EDITOR_CLASS): self.chargement_flux() def chargement_flux(self): + """Charge les couches sélectionnées dans QGIS""" managerAU = QgsApplication.authManager() managerAU.availableAuthMethodConfigs().keys() @@ -498,6 +552,7 @@ class Flux_Editor(QDialog, EDITOR_CLASS): } return switcher.get(type, "nothing") + # Vérification de la présence de l'extension PostGIS Raster SQLloadRaster = """SELECT concat(r_table_schema,'.',r_table_name) from public.raster_columns; """ SQLextension = """SELECT count(extname) FROM pg_catalog.pg_extension WHERE extname LIKE 'postgis_raster';""" @@ -514,9 +569,10 @@ class Flux_Editor(QDialog, EDITOR_CLASS): for rasterFind in list_raster: RasterList.append(rasterFind[0]) + # Chargement de chaque couche sélectionnée for row in range(0, self.tableWidget_2.rowCount()): - color_rgba_db = 855030089 - color_rgba_droit = 851150528 + color_rgba_db = 855030089 # Code couleur pour couche dans autre BD + color_rgba_droit = 851150528 # Code couleur pour droits insuffisants print(self.tableWidget_2.item(row, 1).background().color().rgba()) if self.tableWidget_2.item(row, 1).background().color().rgba() == color_rgba_droit: self.QMBquestion = QMessageBox.question(iface.mainWindow(), u"Attention !", "Vous ne disposez pas des droit pour la couche «" + str(self.tableWidget_2.item(row, 1).text()) + ' ' + str(self.tableWidget_2.item(row, 2).text()) + "» !", QMessageBox.StandardButton(0x00004000)) @@ -537,26 +593,31 @@ class Flux_Editor(QDialog, EDITOR_CLASS): code = self.tableWidget_2.item(row, 1).text() schema = self.tableWidget_2.item(row, 2).text() table = self.tableWidget_2.item(row, 3).text() + # Configuration de l'URI de connexion PostGIS uri = QgsDataSourceUri() uri.setConnection(host, port, dbtype, user, mdp) - # nom du schéma à remplacer: "hydrographie" à supprimer et mettre "couches_collaboratives" lorsqu'on aura regroupé les couches à modifier dans un même + + # Chargement selon le type de couche (raster, vecteur ou projet QGIS) if (schema + '.' + table) in RasterList: + # Chargement d'une couche raster uri.setDataSource(schema, table, "rast") - uri.setKeyColumn('rid') - uri.setSrid('2154') + uri.setKeyColumn('rid') # Clé primaire pour les rasters + uri.setSrid('2154') # Lambert 93 layer = QgsRasterLayer(uri.uri(), table, "postgresraster") QgsProject.instance().addMapLayer(layer) elif code == 'qgis': + # Chargement d'un projet QGIS stocké dans la base de données schema = self.tableWidget_2.item(row, 2).text() print(schema) table = self.tableWidget_2.item(row, 3).text() uri_project = 'postgresql://' + user + ':' + mdp + '@' + host + ':' + port + '?sslmode=disable&dbname=' + dbtype + "&schema=" + schema + '&project=' + table QgsProject.instance().read(uri_project) else: - uri.setDataSource(schema, table, "geom") - uri.setKeyColumn('gid') + # Chargement d'une couche vecteur + uri.setDataSource(schema, table, "geom") # Colonne géométrie + uri.setKeyColumn('gid') # Clé primaire - # Chargement de la couche PostGIS + # Détection du type de géométrie geom_type = 'SELECT right(st_geometrytype(geom),-3) as a FROM ' + schema + '.' + table + ' GROUP BY a' cur.execute(geom_type) list_typegeom = cur.fetchall() @@ -587,8 +648,9 @@ class Flux_Editor(QDialog, EDITOR_CLASS): self.QMBquestion = QMessageBox.question(iface.mainWindow(), u"Attention !", "La couche «" + str(self.tableWidget_2.item(row, 1).text()) + ' ' + str(self.tableWidget_2.item(row, 2).text()) + "» ne ce trouve pas dans cette BD !", QMessageBox.StandardButton(0x00004000)) def combobox_custom(self): + """Remplit la liste déroulante des catégories selon la base de données sélectionnée""" global NoSignals - NoSignals = 1 + NoSignals = 1 # Désactive temporairement les signaux pour éviter les boucles if dbtype == sigdb: self.toolButton.setEnabled(1) self.comboBox.clear() @@ -615,7 +677,8 @@ class Flux_Editor(QDialog, EDITOR_CLASS): NoSignals = 0 def filtre_favorit(self, filter_fav): - if self.toolButton_2.text() == "0": + """Active ou désactive le filtre des favoris""" + if self.toolButton_2.text() == "0": # Activation du filtre self.toolButton_2.setText("1") self.toolButton_2.setDown(True) if self.lineEdit.text() != 'Recherche par mots-clés': @@ -635,6 +698,8 @@ class Flux_Editor(QDialog, EDITOR_CLASS): self.tableWidget.setRowHidden(i, False) def filtre_dynamique(self, filter_text): + """Filtre dynamique des couches selon le texte saisi""" + # Remplacement des espaces par des underscores pour la recherche if filter_text.find(' ') >= 0: filter_text = filter_text.replace(" ", "_") for i in range(self.tableWidget.rowCount()): @@ -650,6 +715,8 @@ class Flux_Editor(QDialog, EDITOR_CLASS): break def getCanevas(self): + """Filtre les couches selon l'emprise du canevas QGIS actuel""" + # Récupération de l'emprise du canevas poly = iface.mapCanvas().extent() geom = (str(poly.xMinimum()) + ',' + str(poly.yMinimum()) + ',' + str(poly.xMaximum()) + ',' + str(poly.yMaximum()))